字符設備驅動程序內核機制

文章內容來自於:《深入Linux設備驅動程序內核機制》第2章字符設備驅動程序

2.2  struct file_operations

在開始討論字符設備驅動程序內核機制前,有必要先交代一下struct file_operations數據結構,其定義如下:

  1. <include/linux/fs.h> 
  2. struct file_operations {  
  3.     struct module *owner;  
  4.     loff_t (*llseek) (struct file *, loff_t, int);  
  5.     ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  
  6.     ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);  
  7.     ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);  
  8.     ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);  
  9.     int (*readdir) (struct file *, void *, filldir_t);  
  10.     unsigned int (*poll) (struct file *, struct poll_table_struct *);  
  11.     long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);  
  12.     long (*compat_ioctl) (struct file *, unsigned int, unsigned long);  
  13.     int (*mmap) (struct file *, struct vm_area_struct *);  
  14.     int (*open) (struct inode *, struct file *);  
  15.     int (*flush) (struct file *, fl_owner_t id);  
  16.     int (*release) (struct inode *, struct file *);  
  17.     int (*fsync) (struct file *, int datasync);  
  18.     int (*aio_fsync) (struct kiocb *, int datasync);  
  19.     int (*fasync) (int, struct file *, int);  
  20.     int (*lock) (struct file *, int, struct file_lock *);  
  21.     ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);  
  22.     unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long,  
  23.                                 unsigned long);  
  24.     int (*check_flags)(int);  
  25.     int (*flock) (struct file *, int, struct file_lock *);  
  26.     ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);  
  27.     ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);  
  28.     int (*setlease)(struct file *, long, struct file_lock **);  
  29.     long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);  
  30. };  

可以看到,struct file_operations的成員變量幾乎全是函數指針,因爲本書的後續章節會陸續討論到這個結構體中絕大多數成員的實現,所以這裏不再解釋其各自的用途。讀者也許很快會發現,現實中字符設備驅動程序的編寫,其實基本上是圍繞着如何實現struct file_operations中的那些函數指針成員而展開的。通過內核文件系統組件在其間的穿針引線,應用程序中對文件類函數的調用,比如read()等,將最終被轉接到struct file_operations中對應函數指針的具體實現上。

該結構中唯一非函數指針類成員owner,表示當前struct file_operations對象所屬的內核模塊,幾乎所有的設備驅動程序都會用THIS_MODULE宏給owner賦值,該宏的定義爲:

  1. <include/linux/module.h> 
  2. #define THIS_MODULE (&__this_module)  

__this_module是內核模塊的編譯工具鏈爲當前模塊產生的struct module類型對象,所以THIS_MODULE實際上是當前內核模塊對象的指針,file_operations中的owner成員可以避免當file_operations中的函數正在被調用時,其所屬的模塊被從系統中卸載掉。如果一個設備驅動程序不是以模塊的形式存在,而是被編譯進內核,那麼THIS_MODULE將被賦值爲空指針,沒有任何作用。

2.3  字符設備的內核抽象(1)

顧名思義,字符設備驅動程序管理的核心對象是字符設備。從字符設備驅動程序的設計框架角度出發,內核爲字符設備抽象出了一個具體的數據結構struct cdev,其定義如下:

  1. <include/linux/cdev.h> 
  2. struct cdev {  
  3.     struct kobject kobj;  
  4.     struct module *owner;  
  5.     const struct file_operations *ops;  
  6.     struct list_head list;  
  7.     dev_t dev;  
  8.     unsigned int count;  
  9. };  
在本章後續的內容中將陸續看到它們的實際用法,這裏只把這些成員的作用簡單描述如下:
  1. struct kobject kobj 
內嵌的內核對象,其用途將在"Linux設備驅動模型"一章中討論。
  1. struct module *owner 
字符設備驅動程序所在的內核模塊對象指針。
  1. const struct file_operations *ops 
字符設備驅動程序中一個極其關鍵的數據結構,在應用程序通過文件系統接口呼叫到設備驅動程序中實現的文件操作類函數的過程中,ops指針起着橋樑紐帶的作用。
  1. struct list_head list 
用來將系統中的字符設備形成鏈表。
  1. dev_t dev 
字符設備的設備號,由主設備號和次設備號構成。
  1. unsigned int count 

隸屬於同一主設備號的次設備號的個數,用於表示由當前設備驅動程序控制的實際同類設備的數量。

設備驅動程序中可以用兩種方式來產生struct cdev對象。一是靜態定義的方式,比如在前面的那個示例程序中,通過下列代碼靜態定義了一個struct cdev對象:

  1. static struct cdev chr_dev; 
另一種是在程序的執行期通過動態分配的方式產生,比如:
  1. static struct cdev *p = kmalloc(sizeof(struct cdev), GFP_KERNEL); 
其實Linux內核源碼中提供了一個函數cdev_alloc,專門用於動態分配struct cdev對象。cdev_alloc不僅會爲struct cdev對象分配內存空間,還會對該對象進行必要的初始化:
  1. <fs/char_dev.c> 
  2. struct cdev *cdev_alloc(void)  
  3. {  
  4.     struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);  
  5.     if (p) {  
  6.         INIT_LIST_HEAD(&p->list);  
  7.         kobject_init(&p->kobj, &ktype_cdev_dynamic);  
  8.     }  
  9.     return p;  
  10. }  

需要注意的是,內核引入struct cdev數據結構作爲字符設備的抽象,僅僅是爲了滿足系統對字符設備驅動程序框架結構設計的需要,現實中一個具體的字符硬件設備的數據結構的抽象往往要複雜得多,在這種情況下struct cdev常常作爲一種內嵌的成員變量出現在實際設備的數據機構中,比如:

  1. struct my_keypad_dev{  
  2.     //硬件相關的成員變量  
  3.     int a;   
  4.     int b;  
  5.     int c;  
  6.     …  
  7.     //內嵌的struct cdev數據結構  
  8.     struct cdev cdev;  
  9. };  

2.3  字符設備的內核抽象(2)

在這樣的情況下,如果要動態分配一個struct real_char_dev對象,cdev_alloc函數顯然就無能爲力了,此時只能使用下面的方法:

  1. static struct real_char_dev *p = kzalloc(sizeof(struct real_char_dev), GFP_KERNEL); 

前面討論瞭如何分配一個struct cdev對象,接下來的一個話題是如何初始化一個cdev對象,內核爲此提供的函數是cdev_init:

  1. <fs/char_dev.c> 
  2. void cdev_init(struct cdev *cdev, const struct file_operations *fops)  
  3. {  
  4.     memset(cdev, 0, sizeof *cdev);  
  5.     INIT_LIST_HEAD(&cdev->list);  
  6.     kobject_init(&cdev->kobj, &ktype_cdev_default);  
  7.     cdev->ops = fops;  
  8. }  

函數的代碼非常直白,不再贅述。一個struct cdev對象在被最終加入系統前,都應該被初始化,無論是直接通過cdev_init或者是其他途徑。理由很簡單,這是Linux系統中字符設備驅動程序框架設計的需要。

照理在談完cdev對象的分配和初始化之後,下面應該討論如何將一個cdev對象加入到系統了,但是由於這個過程需要用到設備號相關的技術點,所以暫且先來探討設備號的問題。

2.4  設備號的構成與分配

本節開始討論設備號相關的問題,不過設備號對於設備驅動程序而言究竟意味着什麼,換句話說,它在內核中起着怎樣的作用,本節暫不討論,這裏只關心它在內核中是如何分配和管理的。

2.4.1  設備號的構成

Linux系統中一個設備號由主設備號和次設備號構成,Linux內核用主設備號來定位對應的設備驅動程序,而次設備號則由驅動程序使用,用來標識它所管理的若干同類設備。因此,從這個角度而言,設備號作爲一種系統資源,必須仔細加以管理,以防止因設備號與驅動程序錯誤的對應關係所帶來的混亂。

Linux用dev_t類型變量來標識一個設備號,這是個32位的無符號整數:

  1. <include/linux/types.h> 
  2. typedef __u32 __kernel_dev_t;  
  3. typedef __kernel_dev_t      dev_t;  
圖2-2顯示了2.6.39版本內核中設備號的構成:
 
圖2-2  Linux的設備號的構成
在這一內核版本中,dev_t的低20位用來表示次設備號,高12位用來表示主設備號。隨着內核版本的演變,上述的主次設備號的構成也許會發生改變,所以設備驅動程序開發者應該避免直接使用主次設備號所佔有的位寬來獲得對應的主設備號或次設備號。爲了保證在主次設備號位寬發生改變時,現有的程序依然可以正常工作,內核提供瞭如下幾個宏供設備驅動程序操作設備號時使用:
  1. <include/linux/kdev_t.h> 
  2. #define MAJOR(dev)      ((unsigned int) ((dev) >> MINORBITS))  
  3. #define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))  
  4. #define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))  
MAJOR宏用來從一個dev_t類型的設備號中提取出主設備號,MINOR宏則用來提取設備號中的次設備號。MKDEV則是將主設備號ma和次設備號mi合成一個dev_t類型的設備號。在上述宏定義中,MINORBITS宏在2.6.39版本中定義的值是20,如果之後的內核對主次設備號所佔用的位寬重新進行調整,例如將MINORBITS改成12,只要設備驅動程序堅持使用MAJOR、MINOR和MKDEV來操作設備號,那麼這部分代碼應該無須修改就可以在新內核中運行。


2.4.2  設備號的分配與管理(1)

在內核源碼中,涉及設備號分配與管理的函數主要有以下兩個:

register_chrdev_region函數

該函數的代碼實現如下:

  1. <fs/char_dev.c> 
  2. int register_chrdev_region(dev_t from, unsigned count, const char *name)  
  3. {  
  4.     struct char_device_struct *cd;  
  5.     dev_t to = from + count;  
  6.     dev_t n, next;  
  7.  
  8.     for (n = from; n < ton = next) {  
  9.         next = MKDEV(MAJOR(n)+1, 0);  
  10.         if (next > to)  
  11.             next = to;  
  12.         cd = __register_chrdev_region(MAJOR(n), MINOR(n),  
  13.                    next - n, name);  
  14.         if (IS_ERR(cd))  
  15.             goto fail;  
  16.     }  
  17.     return 0;  
  18. fail:  
  19.     to = n;  
  20.     for (n = from; n < ton = next) {  
  21.         next = MKDEV(MAJOR(n)+1, 0);  
  22.         kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));  
  23.     }  
  24.     return PTR_ERR(cd);  
  25. }  
該函數的第一參數from表示的是一個設備號,第二參數count是連續設備編號的個數,代表當前驅動程序所管理的同類設備的個數,第三參數name表示設備或者驅動的名稱。register_chrdev_region的核心功能體現在內部調用的__register_chrdev_region函數中,在討論這個函數之前,先要看一個全局性的指針數組chrdevs,它是內核用於設備號分配與管理的核心元素,其定義如下:
  1. <fs/char_dev.c> 
  2. static struct char_device_struct  {  
  3.     struct char_device_struct *next;  
  4.     unsigned int major;  
  5.     unsigned int baseminor;  
  6.     int minorct;  
  7.     char name[64];  
  8.     struct cdev *cdev;      /* will die */  
  9. } *chrdevs[CHRDEV_MAJOR_HASH_SIZE ];  

這個數組中的每一項都是一個指向struct char_device_struct類型的指針。系統剛開始運行時,該數組的初始狀態如圖2-3所示:

現在回過頭來看看register_chrdev_region函數,這個函數要完成的主要功能是將當前設備驅動程序要使用的設備號記錄到chrdevs數組中,有了這種對設備號使用情況的跟蹤,系統就可以避免不同的設備驅動程序使用同一個設備號的情形出現。這意味着當設備驅動程序調用這個函數時,事先已經明確知道它所要使用的設備號,之所以調用這個函數,是要將所使用的設備號納入到內核的設備號管理體系中,防止別的驅動程序錯誤使用到。當然如果它試圖使用的設備號已經被之前某個驅動程序使用了,調用將不會成功,register_chrdev_region函數將會返回一個負的錯誤碼告知調用者,如果調用成功,函數返回0。

 
圖2-3  初始狀態的chrdevs數組結構
上述這些設備號功能的實現其實最終發生在register_chrdev_region函數內部所調用的__register_chrdev_region函數中,它會首先分配一個struct char_device_struct類型的對象cd,然後對其進行一些初始化:
  1. <fs/char_dev.c> 
  2. static struct char_device_struct *  
  3. __register_chrdev_region(unsigned int major, unsigned int baseminor,  
  4.                int minorct, const char *name)  
  5. {  
  6.     cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);  
  7.     …  
  8.     cd->majormajor = major;  
  9.     cd->baseminorbaseminor = baseminor;  
  10.     cd->minorctminorct = minorct;  
  11.     strlcpy(cd->name, name, sizeof(cd->name));  

這個過程完成之後,它開始搜索chrdevs數組,搜索是以哈希表的形式進行的,爲此必須首先獲取一個散列關鍵值,正如讀者所預料的那樣,它用主設備號來生成這個關鍵值:

  1. 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函數的調用如下:

  1. 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,次設備號爲0,當它調用register_chrdev_region(MKDEV(2, 0), 1, "augdev")來向系統註冊設備號時,因爲2 % 255 = 2,所以也將索引到chrdevs數組的第2項。雖然數組的第2項中已經有"demodev"設備在使用,但是因爲這次註冊的設備號是MKDEV(2, 0),與設備"demodev"的設備號MKDEV(257, 0)並不衝突,所以註冊總會成功。因爲Linux在將設備"augdev"對應的struct char_device_struct對象節點加入到哈希表中時,採用了插入排序,這導致同一哈希列表將按照major的大小遞增排列,因此此時的chrdevs數組狀態如圖2-5所示:
 
圖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之間,其定義如下:

  1. <fs/char_dev.c> 
  2. int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,  
  3.             const char *name)  
  4. {  
  5.     struct char_device_struct *cd;  
  6.     cd = __register_chrdev_region(0, baseminor, count, name);  
  7.     if (IS_ERR(cd))  
  8.         return PTR_ERR(cd);  
  9.     *dev = MKDEV(cd->major, cd->baseminor);  
  10.     return 0;  
  11. }  
這個函數的核心調用也是__register_chrdev_region,相對於register_chrdev_region,alloc_chrdev_region在調用__register_chrdev_region時,第一個參數爲0,這將導致__register_chrdev_region執行下面的邏輯:
  1. <fs/char_dev.c> 
  2. static struct char_device_struct *  
  3. __register_chrdev_region(unsigned int major, unsigned int baseminor,  
  4.                int minorct, const char *name)  
  5. {  
  6.     …  
  7.     if (major == 0) {  
  8.         for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {  
  9.             if (chrdevs[i] == NULL)  
  10.                 break;  
  11.         }  
  12.         if (i == 0) {  
  13.             ret = -EBUSY;  
  14.             goto out;  
  15.         }  
  16.         major = i;  
  17.         ret = major;  
  18.     }  
  19.     …  
  20. }  
上述代碼片段的實現原理非常簡單,它在for循環中從chrdevs數組的最後一項(也就是第254項)依次向前掃描,如果發現該數組中的某項,比如第i項,對應的數值爲NULL,那麼就把該項對應的索引值i作爲分配的主設備號返回給驅動程序,同時生成一個struct char_device_struct節點,並將其加入到chrdevs[i]對應的哈希鏈表中。如果從第254項一直到第1項,這其中所有的項對應的指針都不爲NULL,那麼函數失敗並返回一非0值,表明動態分配設備號失敗。如果分配成功,所分配的主設備號將記錄在struct char_device_struct對象cd中,並將該對象返回給alloc_chrdev_region函數,後者通過下面的代碼將新分配的設備號返回給函數的調用者:
  1. *dev = MKDEV(cd->major, cd->baseminor); 
設備號作爲一種系統資源,當所對應的設備驅動程序被卸載時,很顯然要把其所佔用的設備號歸還給系統,以便分配給其他內核模塊使用。不管是用register_chrdev_region還是alloc_chrdev_region 註冊或者分配的設備號,在Linux中都由下面的函數負責釋放:
  1. <fs/char_dev.c> 
  2. 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源碼中的實現如下:

  1. <fs/char_dev.c> 
  2. int cdev_add(struct cdev *p, dev_t dev, unsigned count)  
  3. {  
  4.     p->devdev = dev;  
  5.     p->countcount = count;  
  6.     return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);  
  7. }  

其中,參數p爲要加入系統的字符設備對象的指針,dev爲該設備的設備號,count表示從次設備號開始連續的設備數量。

cdev_add的核心功能通過kobj_map函數來實現,後者通過操作一個全局變量cdev_map來把設備(*p)加入到其中的哈希鏈表中。cdev_map的定義如下:

  1. <fs/char_dev.c> 
  2. static struct kobj_map *cdev_map;  
這是一個struct kobj_map指針類型的全局變量,在Linux系統啓動期間由chrdev_init函數負責初始化。struct kobj_map的定義如下:
  1. <drivers/base/map.c> 
  2. struct kobj_map {  
  3.     struct probe {  
  4.         struct probe *next;  
  5.         dev_t dev;  
  6.         unsigned long range;  
  7.         struct module *owner;  
  8.         kobj_probe_t *get;  
  9.         int (*lock)(dev_t, void *);  
  10.         void *data;  
  11.     } *probes[255];  
  12.     struct mutex *lock;  
  13. };  
kobj_map函數中哈希表的實現原理和前面註冊分配設備號中的幾乎完全一樣,通過要加入系統的設備的主設備號major(major=MAJOR(dev))來獲得probes數組的索引值i(i = major % 255),然後把一個類型爲struct probe的節點對象加入到probes[i]所管理的鏈表中,如圖2-6所示。其中struct probe所在的矩形塊中的深色部分是我們重點關注的內容,記錄了當前正在加入系統的字符設備對象的有關信息。其中,dev是它的設備號,range是從次設備號開始連續的設備數量,data是一void *變量,指向當前正要加入系統的設備對象指針p。圖2-6展示了兩個滿足主設備號major % 255 = 2的字符設備通過調用cdev_add之後,cdev_map所展現出來的數據結構狀態。
 
圖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函數的實現如下:

  1. <fs/char_dev.c> 
  2. void cdev_del(struct cdev *p)  
  3. {  
  4.     cdev_unmap(p->dev, p->count);  
  5.     kobject_put(&p->kobj);  
  6. }  
對於以內核模塊形式存在的驅動程序,作爲通用的規則,模塊的卸載函數應負責調用這個函數來將所管理的設備對象從系統中移除。


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,命令形式爲:

  1. root@LinuxDev:/home/dennis# mknod /dev/demodev c 2 0 
上述命令成功執行後,將會在/dev目錄下生成一個名爲demodev的字符設備節點。如果用strace工具來跟蹤一下上面的命令,會發現如下輸出(刪去了若干不相關部分):
  1. root@LinuxDev:/home/dennis# strace mknod /dev/demodev c 2 0  
  2. execve("/bin/mknod", ["mknod", "/dev/demodev", "c", "30","0"], [/* 36 vars */]) = 0  
  3. …  
  4. mknod("/dev/demodev", S_IFCHR|0666, makedev(30,0)) = 0  
  5. …  

可見Linux下的mknod命令最終是通過調用mknod函數來實現的,調用時的重要參數有兩個,一是設備文件名("/dev/demodev"),二是設備號(makedev(30,0))。設備文件名主要在用戶空間使用(比如用戶空間程序調用open函數時),而內核空間則使用inode來表示相應的文件。本書只關注內核空間的操作,對於前面的mknod命令,它將通過系統調用sys_mknod進入內核空間,這個系統調用的原型是:

  1. <include/linux/syscalls.h> 
  2. 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函數,其定義如下:

  1. <fs/inode.c> 
  2. void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)  
  3. {  
  4.     inode->i_mode = mode;  
  5.     if (S_ISCHR(mode)) {  
  6.         inode->i_fop = &def_chr_fops;  
  7.         inode->i_rdev = rdev;  
  8.     } else if (S_ISBLK(mode)) {  
  9.         inode->i_fop = &def_blk_fops;  
  10.         inode->i_rdev = rdev;  
  11.     } else if (S_ISFIFO(mode))  
  12.         inode->i_fop = &def_fifo_fops;  
  13.     else if (S_ISSOCK(mode))  
  14.         inode->i_fop = &bad_sock_fops;  
  15.     else  
  16.         printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"  
  17.                   " inode %s:%lu\n", mode, inode->i_sb->s_id,  
  18.                   inode->i_ino);  
  19. }  

這個函數最主要的功能便是爲新生成的inode初始化其中的i_fop和i_rdev成員。設備文件節點inode中的i_rdev成員用來表示該inode所對應設備的設備號,通過參數rdev爲其賦值。設備號在由sys_mknod發起的整個內核調用鏈中進行傳遞,最早來自於用戶空間的mknod命令行參數。

i_fop成員的初始化根據是字符設備還是塊設備而有不同的賦值。對於字符設備,fop指向def_chr_fops,後者主要定義了一個open操作:

  1. <fs/char_dev.c> 
  2. const struct file_operations def_chr_fops = {  
  3.     .open = chrdev_open,  
  4.     …  
  5. };  
相對於字符設備,塊設備的def_blk_fops的定義則要有點複雜:
  1. <fs/block_dev.c> 
  2. const struct file_operations def_blk_fops = {  
  3.     .open       = blkdev_open,  
  4.     .release    = blkdev_close,  
  5.     .llseek = block_llseek,  
  6.     .read       = do_sync_read,  
  7.     .write      = do_sync_write,  
  8.     .aio_read   = generic_file_aio_read,  
  9.     .aio_write  = blkdev_aio_write,  
  10.     .mmap       = generic_file_mmap,  
  11.     .fsync      = blkdev_fsync,  
  12.     .unlocked_ioctl = block_ioctl,  
  13. #ifdef CONFIG_COMPAT  
  14.     .compat_ioctl   = compat_blkdev_ioctl,  
  15. #endif  
  16.     .splice_read    = generic_file_splice_read,  
  17.     .splice_write   = generic_file_splice_write,  
  18. };  
關於塊設備,將在本書第11章"塊設備驅動程序"中詳細討論,這裏依然把考察的重點放在字符設備上。字符設備inode中的i_fop指向def_chr_fops。至此,設備節點的所有相關鋪墊工作都已經結束,接下來可以看看打開一個設備文件到底意味着什麼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章