字符设备访问流程
《LDD3》这本书中“字符设备驱动程序”一章有这样一段话:
只要cdev_add返回了,我们的设备就“活”了,它的操作就会被内核调用。
这里就研究一下cdev_add究竟如何让设备“活”过来,以及用户空间访问字符设备节点时,内核的处理流程。
从cdev_add开始分析
先从cdev_add()入手:
1 | int cdev_add(struct cdev *p, dev_t dev, unsigned count) |
这个函数除了增加parent的引用计数外,只有一个kobj_map()的函数调用,所以重要的操作应该都通过该函数进行;函数定义再drivers/base/map.c中:
1 | int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range, |
cdev_map
结合cdev_add()调用该函数时传入的参数,发现一个重要的参数和数据结构,struct kobj_map类型的cdev_map,这是一个定义在fs/char_dev.c中的全局变量;先来看一下struct kobj_map的结构定义:
1 | struct kobj_map { |
kobj_map中有一个指向struct probe结构的数组,长度为255,而struct probe结构中包含了设备号、模块的owner等信息。
cdev_map的初始化操作通过chrdev_init()进行,而该函数又直接调用了kobj_map_init():
1 | struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct mutex *lock) |
初始化过程非常简单,除了分配内存,还初始化了一个probe结构的base,并将cdev_map中probes所有元素指向这个base,所以初始化后的cdev_map结构如下图:

kobj_map()
分析完cdev_map的初始化,继续回到kobj_map()函数;前面说到cdev_map被作为参数传递给kobj_map()函数,接下来分析一下kobj_map()的实现,注释后的代码如下:
1 | int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range, |
mutex_lock()之前的部分比较好理解,就是根据传入的参数,创建并设置了probe结构;mutex_lock()和mutex_unlock()之间的部分需要单独分析:
1 | …… |
在这个for循环中,index表示设备的主设备号,p表示一个probe的实例,这段代码主要作用就是将kobj_map结构的probes数组中的元素指向前面创建的probe实例。
循环中第一行,创建一个struct probe的指针指向kobj_map->probes中的某个位置,这个位置并不直接指向第index个位置,而是通过index%255计算得到,因为index是12位的主设备号,取值范围大于255,取模操作可以保证索引不会溢出。
接下来的while循环条件比较复杂,暂时先忽略while循环的内容;for循环中最后两句则是将probe[index%255]这个位置指向p,p->next指向原来的值,最终cdev_map会得到这样的结构:

接着再看刚刚忽略的while循环;简单来说,这里的while循环作用就是将一条probe链上的probe实例按照range的值从小到大排序。
在kobj_map_init()中这样一条语句,将base的range设置为unsigned long的最大值:
1 | base->range = ~0 |
这里我们以probes数组的第0个元素为例,此时probes[0]结构如下图:

假设此时插入一个range为1的probe实例到probes[0],此时(*s)->range = MAX > range = 1,所以结构会变为这样:

这种情况下,再插入一个range为2的probe实例,这时(*s)->range = 1 < range = 2,会进入到while循环中,执行s = &(*s)->next:

执行完后,s将指向probe_0的next域,而(*s)则指向base,此时(*s)->range = MAX > range = 2,离开while循环,最终得到如下结构:

所以最终得到的cdev_map中,每条probe链都是按range从小到大排序的,并且每条链的末尾都指向初始化时创建的base。
字符设备的访问
前面分析完了cdev_map的初始化流程,到目前为止,cdev结构已经添加到cdev_map中,但是从用户空间访问设备节点时,如何找到对应的file_operations函数呢?
kobj_lookup()
drivers/base/map.c中还有一个重要的函数kobj_lookup,先来从这个函数进行分析:
1 | struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index); |
从函数定义来看,这个函数的作用是根据设备号,从kobj_map中找到对应的probe,并从中返回对应driver的kobject;看一下这个函数的实现部分:
1 | …… |
kobj_lookup()根据主设备号从kobj_map->probes中找到对应的链表进行遍历,并通过计算probe中设备号的范围来匹配正确的probe结构;而kobj_map()时注册的probe()函数则用来从data中获取kobject;对应到cdev_map上,data指向的是cdev结构,而probe()函数指针指向exact_match()函数:
1 | static struct kobject *exact_match(dev_t dev, int *part, void *data) |
exact_match()的作用就是从data中获取kobject。
既然获取到了kobject,那就可以使用container_of()获取对应的cdev结构,所以char_dev.c中一定有对应的函数会调用kobj_lookup()。
chrdev_open()
经过搜索,在char_dev.c中找到了函数chrdev_open()调用kobj_lookup():
1 | /* |
从注释来看,这个函数在用户空间每次访问字符设备时调用;来看一下哪里会注册这个函数:
1 | const struct file_operations def_chr_fops = { |
chrdev_open()函数注册在def_chr_fops结构体中,继续搜索一下这个结构体会被赋值给谁:
1 | void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev) |
在fs/inode.c中的函数init_special_inode()里,def_chr_fops被赋值给了字符设备的inode结构体,这样在访问字符设备时,就会通过其inode访问到def_chr_fops->chrdev_open(),但是目前为止还没有调用我们自己为设备注册的file_operations,回过头来继续看chrdev_open()的实现:
1 | static int chrdev_open(struct inode *inode, struct file *filp) |
函数实现经过精简后,非常容易看出实现过程:第一次访问时,inode->i_cdev为空,使用kobj_lookup()结合container_of()获取cdev,并将cdev赋值给inode->i_cdev,然后使用fops_get()获取我们设置的fops,再使用replace_fops()将我们的fops设置到filp文件指针上,然后调用filp->f_op->open(),至此成功访问到我们自己的open函数;当再次访问该字符设备时,inode->i_cdev已经被赋值,无需再次通过kobj_lookup()查找对应的cdev结构。