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

kobj_map()
分析完cdev_map的初始化,继续回到kobj_map()函数;前面说到cdev_map被作为参数传递给kobj_map()函数,接下来分析一下kobj_map()的实现,注释后的代码如下:
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,struct module *module, kobj_probe_t *probe,int (*lock)(dev_t, void *), void *data){unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1; /* 设备号范围跨了多少个主设备号 */unsigned index = MAJOR(dev); /* 该设备的起始主设备号 */unsigned i;struct probe *p;if (n > 255) /* 虽然主设备号为12位,但是此处限制设备号范围最多跨255个主设备号 */n = 255;p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL); /* 每个主设备号一个probe结构 */if (p == NULL)return -ENOMEM;/* 逐个初始化probe结构 */for (i = 0; i < n; i++, p++) {p->owner = module;p->get = probe;p->lock = lock;p->dev = dev;p->range = range;p->data = data; /* 传入的data实际上是cdev结构的实例 */}mutex_lock(domain->lock);for (i = 0, p -= n; i < n; i++, p++, index++) {struct probe **s = &domain->probes[index % 255];while (*s && (*s)->range < range)s = &(*s)->next;p->next = *s;*s = p;}mutex_unlock(domain->lock);return 0;}
mutex_lock()之前的部分比较好理解,就是根据传入的参数,创建并设置了probe结构;mutex_lock()和mutex_unlock()之间的部分需要单独分析:
……for (i = 0, p -= n; i < n; i++, p++, index++) {struct probe **s = &domain->probes[index % 255];while (*s && (*s)->range < range)s = &(*s)->next;p->next = *s;*s = p;}……
在这个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的最大值:
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,先来从这个函数进行分析:
struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index);
从函数定义来看,这个函数的作用是根据设备号,从kobj_map中找到对应的probe,并从中返回对应driver的kobject;看一下这个函数的实现部分:
……for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) {struct kobject *(*probe)(dev_t, int *, void *);struct module *owner;void *data;/* 设备号与当前probe不匹配,继续寻找 */if (p->dev > dev || p->dev + p->range - 1 < dev)continue;if (p->range - 1 >= best) /* 达到链表尾,退出循环 */break;if (!try_module_get(p->owner))continue;owner = p->owner;data = p->data;probe = p->get;best = p->range - 1;*index = dev - p->dev;if (p->lock && p->lock(dev, data) < 0) {module_put(owner);continue;}mutex_unlock(domain->lock);kobj = probe(dev, index, data); /* 使用kobj_map()时注册的probe函数获取kobj *//* Currently ->owner protects _only_ ->probe() itself. */module_put(owner);if (kobj)return kobj;goto retry;}……
kobj_lookup()根据主设备号从kobj_map->probes中找到对应的链表进行遍历,并通过计算probe中设备号的范围来匹配正确的probe结构;而kobj_map()时注册的probe()函数则用来从data中获取kobject;对应到cdev_map上,data指向的是cdev结构,而probe()函数指针指向exact_match()函数:
static struct kobject *exact_match(dev_t dev, int *part, void *data){struct cdev *p = data;return &p->kobj;}
exact_match()的作用就是从data中获取kobject。
既然获取到了kobject,那就可以使用container_of()获取对应的cdev结构,所以char_dev.c中一定有对应的函数会调用kobj_lookup()。
chrdev_open()
经过搜索,在char_dev.c中找到了函数chrdev_open()调用kobj_lookup():
/** Called every time a character special file is opened*/static int chrdev_open(struct inode *inode, struct file *filp)
从注释来看,这个函数在用户空间每次访问字符设备时调用;来看一下哪里会注册这个函数:
const struct file_operations def_chr_fops = {.open = chrdev_open,.llseek = noop_llseek,};
chrdev_open()函数注册在def_chr_fops结构体中,继续搜索一下这个结构体会被赋值给谁:
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;}……}
在fs/inode.c中的函数init_special_inode()里,def_chr_fops被赋值给了字符设备的inode结构体,这样在访问字符设备时,就会通过其inode访问到def_chr_fops->chrdev_open(),但是目前为止还没有调用我们自己为设备注册的file_operations,回过头来继续看chrdev_open()的实现:
static int chrdev_open(struct inode *inode, struct file *filp){const struct file_operations *fops;struct cdev *p;struct cdev *new = NULL;int ret = 0;spin_lock(&cdev_lock);p = inode->i_cdev;if (!p) {……kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);……new = container_of(kobj, struct cdev, kobj);spin_lock(&cdev_lock);……if (!p) {inode->i_cdev = p = new;list_add(&inode->i_devices, &p->list);new = NULL;} else if (!cdev_get(p))ret = -ENXIO;} else if (!cdev_get(p))ret = -ENXIO;……fops = fops_get(p->ops);……replace_fops(filp, fops);if (filp->f_op->open) {ret = filp->f_op->open(inode, filp);……}return 0;……}
函数实现经过精简后,非常容易看出实现过程:第一次访问时,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结构。
