370 likes | 489 Vues
Chap 3- 字元裝置驅動程式. Outline. Introduction 3.1 scull 的設計藍圖 3.2 主編號與次編號 3.3 重要的資料結構 3.4 註冊字元裝置 3.5 open 與 release 3.6 Scull 的記憶體用法規劃 3.7 read 與 write. 3--Introduction. 範例: scull--( Simple Character Utility for Loading Localities )
E N D
Outline • Introduction • 3.1 scull的設計藍圖 3.2 主編號與次編號 • 3.3 重要的資料結構 3.4 註冊字元裝置 • 3.5 open 與 release 3.6 Scull 的記憶體用法規劃 • 3.7 read 與 write
3--Introduction • 範例:scull--( Simple Character Utility for Loading Localities ) • Makefile, main.c, access.c, empty.c, pipe.c, scull.h, scull.init, scull_load, scull_unload, alpha.checkthem • scull的作用是“讓使用者可把一塊記憶區當成字元裝置來使用”scull所驅動的目標裝置是一塊記憶區 • 不需依賴任何“特殊”硬體 • 只要有linux平台就可以編譯與執行 • 未提供任何實用功能,只展示核心與char driver之間軟體介面
3.1--scull的設計藍圖 • 定義驅動程式要提供哪些功能給user-space的程式 • 可循序存取(字元裝置) or 可隨機存取(區塊裝置) • 模擬單一裝置(ex:一機多體 or 多個同類裝置) • Scull所模擬出的每一種裝置,分別由不同類型的模組予以實現(相同的machine差別在於policy的不同) • scull0~scull3四個由記憶區所構成的裝置,兼具“共通” “持續” • scullpipe0~scullpipe3四個FIFO裝置( blocking與nonblocking ) • Scullsingle一次只容許一個被行程存取 • Scullpriv允許每各終端機都有權開啟一次,分屬不同行程 • Sculluid允許開始多次,限同一使用者。(回傳錯誤碼) • Scullwuid允許開始多次,限同一使用者。(推延,等待)
3.2--主編號與次編號 • 主編號(major number)(0~255) • 代表裝置所配合的驅動程式 • 當核心收到open()系統呼叫時,就是依據“主編號”來選擇驅動程式 • 次編號(minor number)(0~255) • 驅動程式以次編號來辨認同類裝置的個體 • 核心本身用不到,只有驅動程式自己才知道次編號的意義 • 當使用者要存取字元裝置時,必須透過檔案系統裡的“代表名稱”特殊檔(special file) 、裝置檔(device file) 、 檔案系統樹的節點(node),集中在/dev/目錄下。 • 裝置類型: • “c”代表char driver的特殊檔 • “b”代表block driver的裝置檔
3.2.1—裝置編號的配置與釋放 • 大部份常見的裝置幾乎都有固定的主編號,可在核心源碼樹的Documentation/devices.txt檔案內找到一份“裝置-主編號”對照表。<挑選可用主編號> less /usr/src/kernel/linux-3.0.8/Documentation/devices.txt • “實驗性或自家使用”的主編號: • 60~63 、 120~127、240.254<真正公開給大眾使用的驅動程式不該使用這些範圍內的主編號> extern int register_chrdev_region(dev_t first, unsigned int count, const char *name); extern int unregister_chrdev_region (dev_t first , unsigned int count);
3.2.1—裝置編號的配置與釋放 • 在呼叫register_chrdev_region()時: • major引數給‘0’: =>回傳值為‘>0 & <255’核心分配的主編號 • major引數給‘>0 & <255’: =>回傳值為‘0’表示核心同意你的要求 • 發生錯誤時: => 回傳值為‘負數’ • 如果你的驅動程式會被用於廣大群眾,或者有可能被內入正式核心,則須設法申請專用的主編號。
3.2.2—動態配置主編號 • alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name) • 動態分配主編號的缺點是無法事先建立/dev/ 裝置節點,因為每次分配到的主編號不一定相同
3.2.2—動態配置主編號 cat /proc/devices Character devices: 1 mem 2 pty 226 drm 253 scull 254 pcmcia Block devices: 2 fd 3 ide0
3.3.1—檔案作業 • 驅動程式內部以一個file結構來代表一個已開啟的裝置 • 核心透過一個file_operations結構來存取驅動程式內部的作業函式(method)<都定義在linux/fs.h> less /usr/src/kernel/linux-3.0.8/include/linux/fs.h • file結構包含一個f_op欄位,他是一個指向file_operations結構的指標 • file_operation結構本身是由一系列“函式指標”所構成,這些指標分別指向驅動程式的各項作業函式。 Ex:核心收到操作檔案的系統戶叫(ex:read( )),依據系統呼叫指定的fd找出對應驅動程式的file結構,然後再循線從file_operations結構中找出對應於該系統呼叫的作業函式scull_read( )
Read() Scull_read()
3.3.1—檔案作業 • struct module *owner; • file_operations的第一個欄位不是函式指標,而是一個指向結構所屬模組的指標。 • loff_t (*llseek) (struct file *, loff_t, int); • seek作業方法的作用,是改變檔案目前的讀寫點位置。 • ssize_t (*read) (struct file *,char _ _user*,size_t,loff_t *); • 用於擷取出裝置上的資料。
ssize_t (*write) (struct file *, const char *, size_t, loff_t *); • 將資料寫入裝置。 • int (*readdir) (struct file *,void *,filldir_t); • 對於裝置檔而言,此欄位必須是NULL,因為他是用來讀取目錄,而且只對檔案系統有意義。 • unsigned int (*poll) (struct file *, struct poll_table_struct *); • 查詢裝置的I/O狀態。 • int (*mmap) (struct file *, struct vm_area_struct *); • mmap用來將I/O memory對應到行程的位址空間。若驅動程式沒提供此方法,則mmap()系統呼叫將傳回-ENODEV
int (*ioctl) (struct inode *, struct file *, unsigned int,unsigned long); • 每個裝置或多或少都有其特殊功能,標準的作業方法不見得能提供應用程式所需要的一切功能(ex:格式化),對於隨裝置而定的功能,應用程式可透過ioctl()系統呼叫來執行一系列裝置特有命令,而ioctl作業發法的任務,就是實現在類特殊命令。 • int (*open) (struct inode *, struct file *); • 開啟,這必定是裝置檔操作程序的第一步,但是驅動程式並非一定要宣告一個對應方法不可。如果將此欄位指向NULL,開啟裝置的動作一定會成功,只不過驅動程式不會收到通知而已。 • int (*flush) (struct file *); • 這是行程在關閉其裝置檔之前的必要動作。flush應該執行任何還沒解決的作業程序。請不要將此方法與fsnc()系統呼叫聯想在一起。目前,只有NFS需要用到flush。如果讓flush指向NULL,不會有動作發生。 • int (*release) (struct inode *, struct file *); • 在file結構被釋放之前,此方法會被呼叫。(並非,每次呼叫close( )都會觸發release)
int (*fsync) (struct inode *, struct dentry *, int); • 此為fsync( )系統呼叫的實際後台,其作用是出清(flush)任何延滯的資料。如果驅動程式不提供此作業方法,系統呼叫會傳回-EINVAL • int (*fasync) (struct inode *, struct flie *, int); • 當裝置的FASYNC旗標出現變化時,核心就會呼叫驅動程式的fasync的作業方法。非同步通知(asynchronous notification)。 • int (*lock) (struct file *, int, struct file_lock *); • 檔案鎖定。Lock的作業方法用來實現檔案所訂的效果。對一般檔案而言,這項更能絕不可避免,但是幾乎沒有任何驅動程式時作此作業方法。 • ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); • ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); • “分散讀取(scatter read)”以及“累積寫出(gather write)”。
3.3.2—file 結構 • fops變數名稱用來表示file_operation結構(或是指向此結構的指標) • 在file_operations結構中的每一個欄位,都必須指向驅動程式中負責特定作業方法。對於驅動程式不支援的作業項目(method),其對應欄位就必須指向NULL,當核心遇到NULL時會採取適當的預設行為。 • 當再重新編譯新核心之後,file_operations會指向NULL,會採取預設行為。 • 每一種作業方法,其回傳值都有同樣的意義:0代表成功,負值為錯誤碼。
3.3.2—file 結構 • File與一般應用程式常用的FILE毫無關係。File定義在C函式庫內,不可能出現在核心程式碼理,而file結構也絕不會出現在user-space應用程式理。 • struct file代表一個已開啟的檔案(open file),對於系統上每一個已開啟的檔案,在kernel-space裡都有一個對應的struct file。每一個file結構都是在核心收到open()系統呼叫時自動建立的,任何作用在檔案的作業方法與核心函式,都會收到該檔案的file結構。close()系統… • struct file不同於磁碟檔案<磁碟檔案是由struct inode來表示> • 在這裡file是結構,而filp是指向file結構的指標 • file => struct ; filp => pointer
3.3.2—file 結構 • file結構的重要欄位: • mode_t f_mode; • 代表檔案的“存取模式”,可能的存取方式包括“可讀”(FMODE_READ),”可寫”(FMODE_WRITE)或“可讀可寫”(FMODE_READ | FMODE_WRITE)。ioctl作業方法或許會想要檢查此欄位來確定讀寫權限,但是read,write則不需要執行權限檢查,因為核心在收到read(),write()系統呼叫時,就已經幫你檢查過了。 • loff_t f_pos; • 檔案目前的讀寫位置。loff_t是一個64bit值。若驅動程式想知道檔案目前的讀寫點位置,可以讀取此值,但是絕對不應該直接修改此值。read, write作業方法應該透過它們從自己第三引數收到的指標(loff_t *)來更新讀寫點位置,而不是直接修改filp->f_pos.
unsigned short f_flags; • 各項“檔案旗標”的組合,像是O_RDONLY, O_NONBLOCK與O_SYNC等等。驅動程式在進行非推延作業(nonblocking operation)時,會需要檢查這些旗標。(所有的旗標都定義在<linux/fcntl.h>) • struct file_operations *f_op; • f_op為指向file_operation結構的指標。每當需要提調(dispatch)任何檔案作業時,核心會讀取此指標,找出對應的作業方法。核心絕不會快取filp->f_op的值<,ex:主編號為1的檔案(/dev/null, /dev/zero …)其搭配的open作業方法會依據開啟對象的次編號,將flip->f_op指向不同的作業方法,此技巧使得主編號相同的一系列檔案,可以表現出多種不同的行為模式,核心容許這種替換檔案作業方法的能力,相當於物件導向的“method overrideing”技術> • void *private_data; • Private_data是相當有用的資源,可供我們保存生命其跨越多次系統呼叫的狀態資訊。驅動程式可以自由決定要如何運用此指標,甚至完全忽略,但必須記得在release method裡釋放掉此指標所占用的記憶體。
3.3.2—file 結構 • struct dentry *f_dentry; • 檔案所屬的“目錄項”結構(directory entry,通常不理會dentry結構,頂多也只是透過file->f_dentry->d_inode來存取inode結構而已。 • file結構實際的欄位超過以上所述。事實上驅動程式決不會自己填寫file結構,而只會存其他地方產生的結構。
3.3.3 - inode 結構 • 核心內部使用inode結構代表檔案,不同於file結構, inode結構是代表已開啟的檔案的FD。 • dev_t i_rdev; • 對於代表檔案裝置檔的inodes,此欄位含有實際的裝置編號(主次編號的組合)。 • struct cdev *i_cdec • struct cdev 是核心內部用來表示字元裝置的結構。對於代表字元裝置的inode,此欄位含有一個指向該結構的指標。
3.5— open & release • Open作業方法提供驅動程式一個機會,為後續的作業進行任何必要的初始化準備工作。此外,open通常還會遞增目標裝置的“用量計次,以免模組檔案關閉之前被謝載。 • Release作業方法負責遞減用量計次(usage count)。 <定義在 linux/module.h> • MOD_INC_USE_COUNT :將目前模組使用次數加一 • MOD_DEC_USE_COUNT:將目前模組使用次數減一
3.5.1— open作業方法 • 大部份驅動程式的open作業方法,應該執行下列動作: • 遞增用量計次 • 檢查裝置特有的錯誤(ex:數據機佔線, 沒放光碟片…) • 如果目標裝置是第一次被開啟,則應該進行初始化程序 • 辨認次編號,並更新f_op指標 • 配置並裝填任何要放在filp->private_data的資料結構 • 開啟的第一步是是要看目標裝置的次編號為何?<在scull是檢查inode->i_rdev> less main.c
3.5.2— release作業方法 • Release作業方法扮演的腳色正好與open相反。有時函式名稱會是device_close(),而非device_release()。不管名稱如何,主要工作流程都一樣: • 釋放open配置給filp->private_data的任何東西 • 在最後一次關閉時,將目標裝置關機 • 遞減用量計次 • <基本上scull沒有硬體可以關機,所以程式碼相當簡潔> Int scull_release (struct inode *inode, struct file *filp) { return 0; }
3.5.2— release作業方法 • 如果再open有用遞增用量計次,則release就必須遞減之。因為只要用量計次沒歸零,核心就會拒絕卸載模組。 • 有時候,一個檔案被開啟與被關閉的次數不一定相同,如何能夠確定用量計次一定正確? • dup( )和fork( )系統呼叫會直接建立以開檔案的副本,而不會啟動驅動程式的open,但這些副本在程式結束時卻會觸發close( )系統呼叫… • ANS:並非每次close( )系統呼叫都會觸發release作業方法。close( )只有再真正需要釋放裝置上的資料時,才會去呼叫release這就是為何取名release而不是close的原因 • 其它類型的裝置有各自的關閉函式,因為scull_open( )已經依據裝置類型,讓filp->f_op指向該類裝置的作業方法。
3.6— Scull的記憶體用法規劃 • 本節只專注scull的記憶體配置策略,而不涉及真實驅動程式所需的硬體管理計技巧 • scull裝置是一個長度可隨資料量而變的記憶區 • 寫入的資料越多,長度自然增加 • 如果用較短的資料去蓋寫它,長度也會跟著縮減 • 不限制“裝置”區的大小 • 在結構上,每個scull裝置都是一個“指標鏈結串列”(a linked list of pointers),每個指標分別指向一個Scull_Dev結構(最多可代表四百萬位元組)。 • 這個陣列含有1000個指標 配額集(quantum set) • 每個指標指向一個4000bytes大小的區域 配額(quantum)
3.6 — Scull 的記憶體用法規劃 • 配額與配額集應該佔用多少記憶量才合適? • 是操作策略(policy)上的問題,與機制(machanism)無關。 • “擬定合理的預設值,同時提供使用者修改的彈性” • Scull容許使用者以多種方式來修改配額量: • 編譯期:修改scull.h內的SCULL_QUANTUM與SCULL_QSET 常數值。 • 模組載入期:scull_quantum與scull_qset變數值。 • 執行期:使用ioctl( )修改目前設定值與預設值。 • 如何決定預設值? • “半滿配額與配額集所浪費掉的記憶量”與“如果配額量太小,以致於需要釋放原有記憶體,重新配置,重建串鏈所造成的效率損耗”兩者之間找出平衡點。 • kmalloc( )的內部設計也應該列入考量…
Scull_Dev Scull_Dev Scull_Dev next next next data data data scull 裝置的記憶佈局 配額集 個別配額
3.7—read & write ssize_t read(struct file *filp, char *buff, size_t count, loff_t *offp); ssize_t write(struct file *filp, const char *buff, size_t count, loff_t *offp); • filp檔案指標 • buff引數指向user-space的緩衝區 • count要被傳輸的資料量 • offp是一個指向“long offset type”的物件指標,代表 使用者正在存取的檔案位置
3.8—read & write • “跨空間”資料傳輸(在kernel-space與user-space之間傳遞資料) • scull的read作業方法,需有可將整段資料寫到user-sapce的能力。write作業方法,需要能從kernel-space讀取一整段資料的能力。 • 由以下兩個核心函式(具有檢查功能)提供: • 預防動作必須注意(user-space有換出功能-swapped out) unsigned long copy_to_user(void *to, const void *from, unsigned long count ); unsigned long copy_from_user(void *to, const void *from, unsigned long count );
ssize_t dev_read( struct file *file, char *buf, size_t count, loff_t *ppos); struct file f_count f_flags f_mode Buffer (in the Kernel-space ) Buffer ( in the user-space or Libc ) Copy_to_user( ) f_pos …. …. 核心空間(不可置換) 使用者空間 (可被置換) Read 作業方法的引數之應用
3.7.1—read 作業方法 • 若傳回值等於當初傳給read( )系統呼叫的count引數,那表示當初要求傳輸的資料已經全數傳輸成功了。 • 若傳回值大於0,但小於count,則表示只順利傳輸了部份資料,原因多與目標裝置有關,通常應用程式會再嘗試重新讀取。 • 若傳回值為0。代表已經遇到檔案尾端(EOF)。 • 若傳回值為-1(不可能有其他負數),則代表發生了某種錯誤。
3.7.2—write 作業方法 • 若傳回值等於count,表示要求的資料量已經全數被寫入裝置。 • 若傳回值為正數值,但小於count,表示只有部份資料被寫入裝置。呼叫者應該再試幾次,將其餘資料也寫入裝置。 • 若傳回值為0,表示沒寫入任何資料。不代表發生錯誤,所以不會傳回代碼。應該是著重新發出write( )呼叫。 • 若傳回負值,表示發生了某種錯誤。如同read的狀況,錯誤代碼的意義定義在<linux/errno.h>
裝置類型 主編號 次編號 代表名稱 DEMO-1 ls –al /dev/ |less brw-rw---- 1 root disk 66, 72 Apr 11 2002 sdak8 brw-rw---- 1 root disk 66, 73 Apr 11 2002 sdak9 crw-r--r-- 1 root root 253, 1 Mar 1 22:58 scull1 crw-r--r-- 1 root root 253, 2 Mar 1 22:58 scull2 crw-r--r-- 1 root root 253, 3 Mar 1 22:58 scull3 brw-rw---- 1 root disk 8, 0 Apr 11 2002 sda crw-rw---- 1 root uucp 154, 18 Apr 11 2002 ttySR18
DEMO-2 • 檔案系統製作裝置節點的命令是mknod,必須有特權身分(root)才能使用此工具。至少需要四個引數… • <代表名稱> <裝置類型> <主編號> <次編號> mknod /dev/ant c 252 0 • 像任何儲存在磁碟上的普通檔案一樣,mknod所產生的裝置節點會被保存下來,除非刻意刪除它們。用一般的rm命令即可辦到…<不使用時未刪除及佔用空間> rm /dev/ant
DEMO-3 • ./scull_load • cat /proc/devices • ./scull_unload
DEMO-4 • make • insmod ./scull.ko • 可以在記憶區製造出一個字元裝置,並且對它做寫入資料或讀取