文章內容來自於:《深入Linux設備驅動程序內核機制》第2章字符設備驅動程序
2.2 struct file_operations
在開始討論字符設備驅動程序內核機制前,有必要先交代一下struct file_operations數據結構,其定義如下:
- <include/linux/fs.h>
- struct file_operations {
- struct module *owner;
- loff_t (*llseek) (struct file *, loff_t, int);
- ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
- ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
- ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
- ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
- int (*readdir) (struct file *, void *, filldir_t);
- unsigned int (*poll) (struct file *, struct poll_table_struct *);
- long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
- long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
- int (*mmap) (struct file *, struct vm_area_struct *);
- int (*open) (struct inode *, struct file *);
- int (*flush) (struct file *, fl_owner_t id);
- int (*release) (struct inode *, struct file *);
- int (*fsync) (struct file *, int datasync);
- int (*aio_fsync) (struct kiocb *, int datasync);
- int (*fasync) (int, struct file *, int);
- int (*lock) (struct file *, int, struct file_lock *);
- ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
- unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long,
- unsigned long);
- int (*check_flags)(int);
- int (*flock) (struct file *, int, struct file_lock *);
- ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
- ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
- int (*setlease)(struct file *, long, struct file_lock **);
- long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
- };
可以看到,struct file_operations的成員變量幾乎全是函數指針,因爲本書的後續章節會陸續討論到這個結構體中絕大多數成員的實現,所以這裏不再解釋其各自的用途。讀者也許很快會發現,現實中字符設備驅動程序的編寫,其實基本上是圍繞着如何實現struct file_operations中的那些函數指針成員而展開的。通過內核文件系統組件在其間的穿針引線,應用程序中對文件類函數的調用,比如read()等,將最終被轉接到struct file_operations中對應函數指針的具體實現上。
該結構中唯一非函數指針類成員owner,表示當前struct file_operations對象所屬的內核模塊,幾乎所有的設備驅動程序都會用THIS_MODULE宏給owner賦值,該宏的定義爲:
- <include/linux/module.h>
- #define THIS_MODULE (&__this_module)
__this_module是內核模塊的編譯工具鏈爲當前模塊產生的struct module類型對象,所以THIS_MODULE實際上是當前內核模塊對象的指針,file_operations中的owner成員可以避免當file_operations中的函數正在被調用時,其所屬的模塊被從系統中卸載掉。如果一個設備驅動程序不是以模塊的形式存在,而是被編譯進內核,那麼THIS_MODULE將被賦值爲空指針,沒有任何作用。
2.3 字符設備的內核抽象(1)
顧名思義,字符設備驅動程序管理的核心對象是字符設備。從字符設備驅動程序的設計框架角度出發,內核爲字符設備抽象出了一個具體的數據結構struct cdev,其定義如下:
- <include/linux/cdev.h>
- struct cdev {
- struct kobject kobj;
- struct module *owner;
- const struct file_operations *ops;
- struct list_head list;
- dev_t dev;
- unsigned int count;
- };
- struct kobject kobj
- struct module *owner
- const struct file_operations *ops
- struct list_head list
- dev_t dev
- unsigned int count
隸屬於同一主設備號的次設備號的個數,用於表示由當前設備驅動程序控制的實際同類設備的數量。
設備驅動程序中可以用兩種方式來產生struct cdev對象。一是靜態定義的方式,比如在前面的那個示例程序中,通過下列代碼靜態定義了一個struct cdev對象:
- static struct cdev chr_dev;
- static struct cdev *p = kmalloc(sizeof(struct cdev), GFP_KERNEL);
- <fs/char_dev.c>
- struct cdev *cdev_alloc(void)
- {
- struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
- if (p) {
- INIT_LIST_HEAD(&p->list);
- kobject_init(&p->kobj, &ktype_cdev_dynamic);
- }
- return p;
- }
需要注意的是,內核引入struct cdev數據結構作爲字符設備的抽象,僅僅是爲了滿足系統對字符設備驅動程序框架結構設計的需要,現實中一個具體的字符硬件設備的數據結構的抽象往往要複雜得多,在這種情況下struct cdev常常作爲一種內嵌的成員變量出現在實際設備的數據機構中,比如:
- struct my_keypad_dev{
- //硬件相關的成員變量
- int a;
- int b;
- int c;
- …
- //內嵌的struct cdev數據結構
- struct cdev cdev;
- };
2.3 字符設備的內核抽象(2)
在這樣的情況下,如果要動態分配一個struct real_char_dev對象,cdev_alloc函數顯然就無能爲力了,此時只能使用下面的方法:
- static struct real_char_dev *p = kzalloc(sizeof(struct real_char_dev), GFP_KERNEL);
前面討論瞭如何分配一個struct cdev對象,接下來的一個話題是如何初始化一個cdev對象,內核爲此提供的函數是cdev_init:
- <fs/char_dev.c>
- void cdev_init(struct cdev *cdev, const struct file_operations *fops)
- {
- memset(cdev, 0, sizeof *cdev);
- INIT_LIST_HEAD(&cdev->list);
- kobject_init(&cdev->kobj, &ktype_cdev_default);
- cdev->ops = fops;
- }
函數的代碼非常直白,不再贅述。一個struct cdev對象在被最終加入系統前,都應該被初始化,無論是直接通過cdev_init或者是其他途徑。理由很簡單,這是Linux系統中字符設備驅動程序框架設計的需要。
照理在談完cdev對象的分配和初始化之後,下面應該討論如何將一個cdev對象加入到系統了,但是由於這個過程需要用到設備號相關的技術點,所以暫且先來探討設備號的問題。
2.4 設備號的構成與分配
本節開始討論設備號相關的問題,不過設備號對於設備驅動程序而言究竟意味着什麼,換句話說,它在內核中起着怎樣的作用,本節暫不討論,這裏只關心它在內核中是如何分配和管理的。
2.4.1 設備號的構成
Linux系統中一個設備號由主設備號和次設備號構成,Linux內核用主設備號來定位對應的設備驅動程序,而次設備號則由驅動程序使用,用來標識它所管理的若干同類設備。因此,從這個角度而言,設備號作爲一種系統資源,必須仔細加以管理,以防止因設備號與驅動程序錯誤的對應關係所帶來的混亂。
Linux用dev_t類型變量來標識一個設備號,這是個32位的無符號整數:
- <include/linux/types.h>
- typedef __u32 __kernel_dev_t;
- typedef __kernel_dev_t dev_t;
圖2-2 Linux的設備號的構成 |
- <include/linux/kdev_t.h>
- #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
- #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
- #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
2.4.2 設備號的分配與管理(1)
在內核源碼中,涉及設備號分配與管理的函數主要有以下兩個:
register_chrdev_region函數
該函數的代碼實現如下:
- <fs/char_dev.c>
- int register_chrdev_region(dev_t from, unsigned count, const char *name)
- {
- struct char_device_struct *cd;
- dev_t to = from + count;
- dev_t n, next;
- for (n = from; n < to; n = next) {
- next = MKDEV(MAJOR(n)+1, 0);
- if (next > to)
- next = to;
- cd = __register_chrdev_region(MAJOR(n), MINOR(n),
- next - n, name);
- if (IS_ERR(cd))
- goto fail;
- }
- return 0;
- fail:
- to = n;
- for (n = from; n < to; n = next) {
- next = MKDEV(MAJOR(n)+1, 0);
- kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
- }
- return PTR_ERR(cd);
- }
- <fs/char_dev.c>
- static struct char_device_struct {
- struct char_device_struct *next;
- unsigned int major;
- unsigned int baseminor;
- int minorct;
- char name[64];
- struct cdev *cdev; /* will die */
- } *chrdevs[CHRDEV_MAJOR_HASH_SIZE ];
這個數組中的每一項都是一個指向struct char_device_struct類型的指針。系統剛開始運行時,該數組的初始狀態如圖2-3所示:
現在回過頭來看看register_chrdev_region函數,這個函數要完成的主要功能是將當前設備驅動程序要使用的設備號記錄到chrdevs數組中,有了這種對設備號使用情況的跟蹤,系統就可以避免不同的設備驅動程序使用同一個設備號的情形出現。這意味着當設備驅動程序調用這個函數時,事先已經明確知道它所要使用的設備號,之所以調用這個函數,是要將所使用的設備號納入到內核的設備號管理體系中,防止別的驅動程序錯誤使用到。當然如果它試圖使用的設備號已經被之前某個驅動程序使用了,調用將不會成功,register_chrdev_region函數將會返回一個負的錯誤碼告知調用者,如果調用成功,函數返回0。
圖2-3 初始狀態的chrdevs數組結構 |
- <fs/char_dev.c>
- static struct char_device_struct *
- __register_chrdev_region(unsigned int major, unsigned int baseminor,
- int minorct, const char *name)
- {
- cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
- …
- cd->majormajor = major;
- cd->baseminorbaseminor = baseminor;
- cd->minorctminorct = minorct;
- strlcpy(cd->name, name, sizeof(cd->name));
這個過程完成之後,它開始搜索chrdevs數組,搜索是以哈希表的形式進行的,爲此必須首先獲取一個散列關鍵值,正如讀者所預料的那樣,它用主設備號來生成這個關鍵值:
- i = major_to_index(major);
這是個非常簡單的獲得散列關鍵值的方法,i = major % 255。此後函數將對chrdevs[i]元素管理的鏈表進行掃描,如果chrdevs[i]上已經有了鏈表節點,表明之前有別的設備驅動程序使用的主設備號散列到了chrdevs[i]上,爲此函數需要相應的邏輯確保當前正在操作的設備號不會與這些已經在使用的設備號發生衝突,如果有衝突,函數將返回錯誤碼,表明本次調用沒有成功。如果本次調用使用的設備號與chrdevs[i]上已有的設備號沒有發生衝突,先前分配的struct char_device_struct對象cd將加入到chrdevs[i]領銜的鏈表中成爲一個新的節點。沒有必要再仔細分析__register_chrdev_region函數中的相關代碼了,接下來以一個具體的例子來了解這一過程。
在chrdevs數組尚處於初始狀態的情形下,假設現在有一個設備驅動程序要使用的主設備號是257,次設備號分別是0、1、2和3(意味着該驅動程序將管理四個同類型的設備)。它對register_chrdev_region函數的調用如下:
- int ret = register_chrdev_region(MKDEV(257, 0), 4, "demodev");
2.4.2 設備號的分配與管理(2)
上述對register_chrdev_region函數的調用完畢後,chrdevs數組的狀態將變成圖2-4所示(圖中假設新分配的struct char_device_struct節點的基地址爲0xC8000004,這些節點基地址數值只是用來使讀者有個直觀的概念,並非代表系統中實際分配的地址值):
圖2-4 主設備號257註冊後的chrdevs數組狀態 |
圖2-5 主設備號2加入後的chrdevs數組狀態 |
一個有趣的事實是,在圖2-5的基礎上,假設有另一個設備驅動程序調用register_chrdev_region函數向系統註冊,主設備號也爲257,那麼只要其次設備號所在的範圍[baseminor, baseminor + minorct]不與設備"demodev"的次設備號範圍發生重疊,系統依然會生成一個新的struct char_device_struct節點並加入到對應的哈希鏈表中。在主設備號相同的情況下,如果次設備號的範圍有重疊,則意味着有設備號的衝突,這將導致對register_chrdev_region函數的調用失敗。對主設備號相同的若干struct char_device_struct對象,當系統將其加入鏈表時,將根據其baseminor成員的大小進行遞增排序。
alloc_chrdev_region函數
該函數由系統協助分配設備號,分配的主設備號範圍將在1~254之間,其定義如下:
- <fs/char_dev.c>
- int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
- const char *name)
- {
- struct char_device_struct *cd;
- cd = __register_chrdev_region(0, baseminor, count, name);
- if (IS_ERR(cd))
- return PTR_ERR(cd);
- *dev = MKDEV(cd->major, cd->baseminor);
- return 0;
- }
- <fs/char_dev.c>
- static struct char_device_struct *
- __register_chrdev_region(unsigned int major, unsigned int baseminor,
- int minorct, const char *name)
- {
- …
- if (major == 0) {
- for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
- if (chrdevs[i] == NULL)
- break;
- }
- if (i == 0) {
- ret = -EBUSY;
- goto out;
- }
- major = i;
- ret = major;
- }
- …
- }
- *dev = MKDEV(cd->major, cd->baseminor);
- <fs/char_dev.c>
- void unregister_chrdev_region(dev_t from, unsigned count);
函數在chrdevs數組中查找參數from和count所對應的struct char_device_struct對象節點,找到以後將其從鏈表中刪除並釋放該節點所佔用的內存,從而將對應的設備號釋放以供其他設備驅動模塊使用。
以上討論了內核中用於設備號分配與管理的技術細節,焦點是register_chrdev_region和alloc_chrdev_region兩個函數,除了alloc_chrdev_region還具有讓系統協助分配一個主設備號的功能外,它們最主要的作用其實都是通過chrdevs數組來跟蹤系統中設備號的使用情況,以防止實際使用中出現設備號衝突的情況。這是內核提供給設備驅動程序使用的一種預防性措施,並沒有必然的理由說設備驅動程序一定要使用這兩個函數,如果可以確定設備驅動程序將要使用的設備號不會與系統中已有的設備號發生衝突,完全可以繞開它們。但很明顯這是一種非常糟糕的習慣,如果某些設備驅動程序沒有使用系統提供的register_chrdev_region或者alloc_chrdev_region函數,那麼系統將失去一個對設備號使用情況進行跟蹤的措施。既然內核在設備驅動程序的框架設計中定義了這種規則,作爲設備驅動程序的實際開發者,沒有理由不去遵循這些規則。
2.5 字符設備的註冊
前面已經討論了字符設備對象的分配、初始化及設備號等概念,在一個字符設備初始化階段完成之後,就可以把它加入到系統中,這樣別的模塊纔可以使用它。把一個字符設備加入到系統中所需調用的函數爲cdev_add,它在Linux源碼中的實現如下:
- <fs/char_dev.c>
- int cdev_add(struct cdev *p, dev_t dev, unsigned count)
- {
- p->devdev = dev;
- p->countcount = count;
- return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
- }
其中,參數p爲要加入系統的字符設備對象的指針,dev爲該設備的設備號,count表示從次設備號開始連續的設備數量。
cdev_add的核心功能通過kobj_map函數來實現,後者通過操作一個全局變量cdev_map來把設備(*p)加入到其中的哈希鏈表中。cdev_map的定義如下:
- <fs/char_dev.c>
- static struct kobj_map *cdev_map;
- <drivers/base/map.c>
- struct kobj_map {
- struct probe {
- struct probe *next;
- dev_t dev;
- unsigned long range;
- struct module *owner;
- kobj_probe_t *get;
- int (*lock)(dev_t, void *);
- void *data;
- } *probes[255];
- struct mutex *lock;
- };
圖2-6 通過cdev_add向系統中加入設備 |
所以,簡單地說,設備驅動程序通過調用cdev_add把它所管理的設備對象的指針嵌入到一個類型爲struct probe的節點之中,然後再把該節點加入到cdev_map所實現的哈希鏈表中。
對系統而言,當設備驅動程序成功調用了cdev_add之後,就意味着一個字符設備對象已經加入到了系統,在需要的時候,系統就可以找到它。對用戶態的程序而言,cdev_add調用之後,就已經可以通過文件系統的接口呼叫到我們的驅動程序,本章稍後將會詳細描述這一過程。
不過在開始文件系統如何通過cdev_map來使用驅動程序提供的服務這個話題之前,我們要來看看與cdev_add相對應的另一個函數cdev_del。其實光通過這個函數名,讀者想必也想到這個函數的作用了:在cdev_add中我們動態分配了struct probe類型的節點,那麼當對應的設備從系統中移除時,顯然需要將它們從鏈表中刪除並釋放節點所佔用的內存空間。在cdev_map所管理的鏈表中查找對應的設備節點時使用了設備號。cdev_del函數的實現如下:
- <fs/char_dev.c>
- void cdev_del(struct cdev *p)
- {
- cdev_unmap(p->dev, p->count);
- kobject_put(&p->kobj);
- }
2.6 設備文件節點的生成
在Linux系統下,設備文件是種特殊的文件類型,其存在的主要意義是溝通用戶空間程序和內核空間驅動程序。換句話說,用戶空間的應用程序要想使用驅動程序提供的服務,需要經過設備文件來達成。當然,如果你的驅動程序只是爲內核中的其他模塊提供服務,則沒有必要生成對應的設備文件。
按照通用的規則,Linux系統所有的設備文件都位於/dev目錄下。/dev目錄在Linux系統中算是一個比較特殊的目錄,在Linux系統早期還不支持動態生成設備節點時,/dev目錄就是掛載的根文件系統下的/dev,對這個目錄下所有文件的操作使用的是根文件系統提供的接口。比如,如果Linux系統掛載的根文件系統是ext3,那麼對/dev目錄下所有目錄/文件的操作都將使用ext3文件系統的接口。隨着後來Linux內核的演進,開始支持動態設備節點的生成 ,使得系統在啓動過程中會自動生成各個設備節點,這就使得/dev目錄不必要作爲一個非易失的文件系統的形式存在。因此,當前的Linux內核在掛載完根文件系統之後,會在這個根文件系統的/dev目錄上重新掛載一個新的文件系統devtmpfs,後者是個基於系統RAM的文件系統實現。當然,對動態設備節點生成的支持並不意味着一定要將根文件系統中的/dev目錄重新掛載到一個新的文件系統上,事實上動態生成設備節點技術的重點並不在文件系統上面。
動態設備節點的特性需要其他相關技術的支持,在後續的章節中會詳細描述這些特性。目前先假定設備節點是通過Linux系統下的mknod命令靜態創建。爲方便敘述,下面用一個具體的例子來描述設備文件產生過程中的一些關鍵要素,這個例子的任務很簡單:在一個ext3類型的根文件系統中的/dev目錄下用mknod命令來創建一個新的設備文件節點demodev,對應的驅動程序使用的設備主設備號爲2,次設備號是0,命令形式爲:
- root@LinuxDev:/home/dennis# mknod /dev/demodev c 2 0
- root@LinuxDev:/home/dennis# strace mknod /dev/demodev c 2 0
- execve("/bin/mknod", ["mknod", "/dev/demodev", "c", "30","0"], [/* 36 vars */]) = 0
- …
- mknod("/dev/demodev", S_IFCHR|0666, makedev(30,0)) = 0
- …
可見Linux下的mknod命令最終是通過調用mknod函數來實現的,調用時的重要參數有兩個,一是設備文件名("/dev/demodev"),二是設備號(makedev(30,0))。設備文件名主要在用戶空間使用(比如用戶空間程序調用open函數時),而內核空間則使用inode來表示相應的文件。本書只關注內核空間的操作,對於前面的mknod命令,它將通過系統調用sys_mknod進入內核空間,這個系統調用的原型是:
- <include/linux/syscalls.h>
- long sys_mknod(const char __user *filename, int mode, unsigned dev);
注意sys_mknod的最後一個參數dev,它是由用戶空間的mknod命令構造出的設備號。sys_mknod系統調用將通過/dev目錄上掛載的文件系統接口來爲/dev/demodev生成一個新的inode ,設備號將被記錄到這個新的inode對象上。
圖2-7展示了通過ext3文件系統在/dev目錄下生成一個新的設備節點/dev/demodev的主要流程。
圖2-7 ext3文件系統mknod的主要流程 |
完整了解設備節點產生的整個過程需要知曉VFS和特定文件系統的技術細節。然而從驅動程序員的角度來說,沒有必要知道文件系統相關的所有細節,只需關注文件系統和驅動程序間是如何建立上關聯的就足夠了。
sys_mknod首先在根文件系統ext3的根目錄"/"下尋找dev目錄所對應的inode,圖中對應的inode編號爲168,ext3文件系統的實現會通過某種映射機制,通過inode編號最終得到該inode在內存中的實際地址(圖中由標號1的線段表示)。接下來會通過dev的inode結構中的i_op成員指針所指向的ext3_dir_inode_operations(這是個struct inode_operations類型的指針),來調用該對象中的mknod方法,這將導致ext3_mknod函數被調用。
ext3_mknod函數的主要作用是生成一個新的inode(用來在內核空間表示demodev設備文件節點,demodev設備節點文件與新生成的inode之間的關聯在圖2-7中由標號5的線段表示)。在ext3_mknod中會調用一個和設備驅動程序關係密切的init_special_inode函數,其定義如下:
- <fs/inode.c>
- void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
- {
- inode->i_mode = mode;
- if (S_ISCHR(mode)) {
- inode->i_fop = &def_chr_fops;
- inode->i_rdev = rdev;
- } else if (S_ISBLK(mode)) {
- inode->i_fop = &def_blk_fops;
- inode->i_rdev = rdev;
- } else if (S_ISFIFO(mode))
- inode->i_fop = &def_fifo_fops;
- else if (S_ISSOCK(mode))
- inode->i_fop = &bad_sock_fops;
- else
- printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
- " inode %s:%lu\n", mode, inode->i_sb->s_id,
- inode->i_ino);
- }
這個函數最主要的功能便是爲新生成的inode初始化其中的i_fop和i_rdev成員。設備文件節點inode中的i_rdev成員用來表示該inode所對應設備的設備號,通過參數rdev爲其賦值。設備號在由sys_mknod發起的整個內核調用鏈中進行傳遞,最早來自於用戶空間的mknod命令行參數。
i_fop成員的初始化根據是字符設備還是塊設備而有不同的賦值。對於字符設備,fop指向def_chr_fops,後者主要定義了一個open操作:
- <fs/char_dev.c>
- const struct file_operations def_chr_fops = {
- .open = chrdev_open,
- …
- };
- <fs/block_dev.c>
- const struct file_operations def_blk_fops = {
- .open = blkdev_open,
- .release = blkdev_close,
- .llseek = block_llseek,
- .read = do_sync_read,
- .write = do_sync_write,
- .aio_read = generic_file_aio_read,
- .aio_write = blkdev_aio_write,
- .mmap = generic_file_mmap,
- .fsync = blkdev_fsync,
- .unlocked_ioctl = block_ioctl,
- #ifdef CONFIG_COMPAT
- .compat_ioctl = compat_blkdev_ioctl,
- #endif
- .splice_read = generic_file_splice_read,
- .splice_write = generic_file_splice_write,
- };