字符设备驱动程序内核机制

文章内容来自于:《深入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。至此,设备节点的所有相关铺垫工作都已经结束,接下来可以看看打开一个设备文件到底意味着什么。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值