一,字符设备模型初始化
字符驱动程序,大家都是从字符驱动程序开始学习驱动的,之前我们使用字符驱动程序。这里我们简单分析一些字符驱动内部模型。字符设备驱动模型的源码在fs/char_dev.c文件中。我们就先看下这个部分的初始化函数chrdev_init,这个函数会在系统初始化时运行。
- void __init chrdev_init(void)
- {
- cdev_map = kobj_map_init(base_probe, &chrdevs_lock); //创建并初始化一个kobj_map结构
- }
cdev_map是一个kobj_map指针,它的主要功能是表示其在子系统中的结构和保持字符设备变量,kobj_map定义如下,
- struct kobj_map {
- struct probe { //这个结构体中包含了一个probe的结构的定义
- struct probe *next; //链表中下一个元素
- dev_t dev; //设备号??(同一个255中节点不是可以与多个设备吗)
- unsigned long range; //
- struct module *owner; //
- kobj_probe_t *get; //获取data中的kobj函数
- int (*lock)(dev_t, void *);
//锁
- void *data;
- } *probes[255]; //255长度的probe结构指针数组
- struct semaphore *sem; //信号量
- };
接下来我们看它的初始化函数kobj_map_init,这个函数主要做了一下事情
创建了一个kobj_map和一个probe结构;初始化base;将kobj_map中的probe指针数组全部赋值为base;返回kobj_map的指针
- struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct semaphore *sem)
- {
- struct kobj_map *p = kmalloc(sizeof(struct kobj_map), GFP_KERNEL); //创建一个kobj_map
- struct probe *base = kzalloc(sizeof(*base), GFP_KERNEL); //创建一个probe
- 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->sem = sem;
- return p;
- }
那我们就可以知道chrdev_init函数在初始化过程中主要就是创建了一个kobj_map变量。
二,注册字符设备
字符驱动中register_chrdev函数将注册一个字符设备,这个函数会分配设备号,创建cdev和添加cdev到系统内核。
- int register_chrdev(unsigned int major, const char *name,
- struct file_operations *fops)
- {
- struct char_device_struct *cd;
- struct cdev *cdev;
- char *s;
- int err = -ENOMEM;
- cd = __register_chrdev_region(major, 0, 256, name); //1,分配设备号相关
- if (IS_ERR(cd))
- return PTR_ERR(cd);
-
- cdev = cdev_alloc(); //2,分配cedv
- if (!cdev)
- goto out2;
- cdev->owner = fops->owner; //拥有者
- cdev->ops = fops; //函数操作集
- kobject_set_name(&cdev->kobj, "%s", name); //设定kobject的名称
- for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))
- *s = '!';
-
- err = cdev_add(cdev, MKDEV(cd->major, 0), 256); //3,添加cdev
- if (err)
- goto out;
- cd->cdev = cdev; //char_device_struct的cdev
- return major ? 0 : cd->major; //如果major不是0则返回0,是0则返回分配的主设备号
- out:
- kobject_put(&cdev->kobj);
- out2:
- kfree(__unregister_chrdev_region(cd->major, 0, 256));
- return err;
- }
分析这个函数我们需要将其分解为几个部分,逐步讲解。
1,分配设备号相关
(1)char_device_struct结构是字符驱动模型子系统中由于表示一个字符设备占用设备号的结构。chrdevs变量是它的一个数组指针,这个255个元素的数组指针是用来保存所以的字符设备,所有的字符设备模255后的结果将作为数组的索引存入相应的位置,这个结构的使用将在后面的分析中更详尽的看到。
static struct char_device_struct {
struct char_device_struct *next; //链表中的下一个字符设备。
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct file_operations *fops;
struct cdev *cdev; /* will die */
} *chrdevs[MAX_PROBE_HASH];
(2)这里我们大概说明一下,字符设备是如何根据设备号放入chrdevs数组中的,假如一个主设备号为258的字符设备加入,则258模255得3,所以将生成的char_device_struct加入到chrdevs[3]中。之后如果又有一个主设备号为3的设备加入,则3模255得3,这时这个设备仍然可以加入(因为主设备号不同),但是chrdevs只有255个,怎么存储呢?我们看到char_device_struct结构中有一个链表的指针next,所以他将保存主设备号模相同的字符设备,而形成一个链表,如果此时还有一个主设备号为3的设备加入,但是它的次设备号不予现有的chrdevs[3]中链表上的设备的次设备号有重叠,依旧可以加入到这个链表中。
- /*
- * Register a single major with a specified minor range.
- *
- * If major == 0 this functions will dynamically allocate a major and return
- * its number.
- *
- * If major > 0 this function will attempt to reserve the passed range of
- * minors and will return zero on success.
- *
- * Returns a -ve errno on failure.
- */
- static struct char_device_struct *
- __register_chrdev_region(unsigned int major, unsigned int baseminor,
- int minorct, const char *name)
- {
- struct char_device_struct *cd, **cp;
- int ret = 0;
- int i;
- cd = kmalloc(sizeof(struct char_device_struct), GFP_KERNEL); //(1)
- if (cd == NULL)
- return ERR_PTR(-ENOMEM);
- memset(cd, 0, sizeof(struct char_device_struct));
- down(&chrdevs_lock);
- /* temporary */
- if (major == 0) { //如果主设备为0,即是想要由系统给出一个设备号,
- for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
- if (chrdevs[i] == NULL) //从chrdevs最后一个元素看是否是空闲
- break;
- }
- if (i == 0) {
- ret = -EBUSY;
- goto out;
- }
- major = i; //将找到的空闲chrdevs的索引赋值给major
- ret = major;
- }
-
- //填充新创建的char_device_struct的成员
- cd->major = major;
- cd->baseminor = baseminor;
- cd->minorct = minorct;
- strncpy(cd->name,name, 64);
- i = major_to_index(major); //将主设备模255
- //(2)
- for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) //循环i对于chrdevs中的链表,内进入此for中则说明chrdevs[i]已经有东西了(*cp),对于chrdevs[i]为空时,则不会进入此for循环中
- if ((*cp)->major > major || //这样设计会使链表中的主设备号是増序,即3,258...这样
- ((*cp)->major == major && (*cp)->baseminor >= baseminor)) //链表结束遍历的条件是,现节点上的主设备号大于了新的主设备号,或者主设备号相同时,现节点次设备号大于等于新节点次设备号。这样的目的是可以使链表的排序是先以主设备号递增,主设备号相同次设备号递增。
- break;
- if (*cp && (*cp)->major == major &&
- (*cp)->baseminor < baseminor + minorct) { //不能加入链表的条件(主设备号相同,且新设备的次设备号范围与现节点上的次设备号有重叠(即大于))
- ret = -EBUSY;
- goto out;
- }
- cd->next = *cp; //将新节点插入链表
- *cp = cd;
- up(&chrdevs_lock);
- return cd;
- out:
- up(&chrdevs_lock);
- kfree(cd);
- return ERR_PTR(ret);
- }
2,分配cedv
- struct cdev *cdev_alloc(void)
- {
- struct cdev *p = kmalloc(sizeof(struct cdev), GFP_KERNEL); //分配一个cdev
- if (p) { //如果分配成功
- memset(p, 0, sizeof(struct cdev)); //初始化为0
- p->kobj.ktype = &ktype_cdev_dynamic; //cdev的kbject的ktype为ktype_cdev_dynamic
- INIT_LIST_HEAD(&p->list); //初始化链表
- kobject_init(&p->kobj); //初始化kobj
- }
- return p;
- }
3,添加cdev函数
- int cdev_add(struct cdev *p, dev_t dev, unsigned count)
- {
- p->dev = dev; cdev的设备号
- p->count = count; //次设备号数量
- return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); //(1)
- }
(1)cdev_map是kobj_map结构的全局变量,在系统初始化时初始化前面已经讲过了
这个kobj_map函数,原理和__register_chrdev_region这个函数很相似,也是根据设备号,将变量加入到一个全局变量的列表当中。只不过这里创建的probe结构加入的全局变量是cdev_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)
- n = 255;
- p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);
- if (p == NULL)
- return -ENOMEM;
- 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;
- }
- down(domain->sem);
- 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;
- }
- up(domain->sem);
- return 0;
- }
至此,register_chrdev函数分析完了,在写字符驱动时我们知道,调用完这个函数,字符设备已经注册好了。可是我们还是不清楚当用户层调用open时系统如何找到对应的函数。
三,打开设备节点
这里我们看下设备节点,设备节点文件在/dev目录中,在注册完设备驱动后,我们可以mknod手动创建设备节点或者创建类设备理论udevs自动创建设备节点。创建设备节点都是由用户空间发出,调用到sys_mknod系统调用,它将通过/dev目录上挂载的文件系统创建一个设备节点文件,生成一个新的inode,设备号也将记录在这个inode中。详细的创建设备节点关系到文件系统的知识,这时很大的一块我们先不做涉足。
使用驱动时首先open要打开设备节点,我们这里看一下这个open如何最终找到驱动程序中的open的。在这里我们还有说一下文件系统创建设备节点时的一个部分才能,理解open调用的路线。在生成设备节点的inode时会调用一个和驱动密切相关的函数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的成员inode->i_rdev,inode->i_fop。i_redv表示设备的设备号,它是由生成节点系统调用(sys_mknod)时传递的设备号。def_chr_fops字符设备默认函数操作集,我们看一个这个默认的操作集的定义(fs/char_dev.c)。
- const struct file_operations def_chr_fops = {
- .open = chrdev_open,
- };
我们看到这里只有一个open函数。那么我们在open一个/dev下的字符设备时都调用的是这个chrdev_open函数,那怎么才能调用到我们驱动字节的open函数内。我们要分析一下chrdev_open,
- static int chrdev_open(struct inode *inode, struct file *filp)
- {
- struct cdev *p;
- struct cdev *new = NULL;
- int ret = 0;
- spin_lock(&cdev_lock);
- p = inode->i_cdev; //inode关联的cdev
- if (!p) { //第一open时,p是空的
- struct kobject *kobj;
- int idx;
- spin_unlock(&cdev_lock);
- kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); //寻找设备号对应cdev的kobj
- if (!kobj)
- return -ENXIO;
- new = container_of(kobj, struct cdev, kobj); //获得kobj的cdev
- spin_lock(&cdev_lock);
- /* Check i_cdev again in case somebody beat us to it while
- we dropped the lock. */
- p = inode->i_cdev;
- if (!p) {
- inode->i_cdev = p = new; //将找到的cdev
- list_add(&inode->i_devices, &p->list); //将inode加入到cdev的链表中
- new = NULL;
- } else if (!cdev_get(p))
- ret = -ENXIO;
- } else if (!cdev_get(p))
- ret = -ENXIO;
- spin_unlock(&cdev_lock);
- cdev_put(new);
- if (ret)
- return ret;
- //filp是file接口变量,file代表一个进程打开的设备文件,不同的进程打开同一个设备节点将分别为每个进程创建一个file变量
- ret = -ENXIO;
- filp->f_op = fops_get(p->ops); //将cdev中的函数操作即设置给file->f_op
- if (!filp->f_op)
- goto out_cdev_put;
- if (filp->f_op->open) { //重点就在这里了,这个是cdev的函数操作集,将打开open
- ret = filp->f_op->open(inode,filp);
- if (ret)
- goto out_cdev_put;
- }
- return 0;
- out_cdev_put:
- cdev_put(p);
- return ret;
- }
- struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
- {
- struct kobject *kobj;
- struct probe *p;
- unsigned long best = ~0UL;
- retry:
- mutex_lock(domain->lock);
- for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) { //找到设备号对于的probe
- struct kobject *(*probe)(dev_t, int *, void *);
- struct module *owner;
- void *data;
- 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); //probe是p->get,在cdev_add函数中这个函数其实是exact_match函数(1)
- /* Currently ->owner protects _only_ ->probe() itself. */
- module_put(owner);
- if (kobj)
- return kobj;
- goto retry;
- }
- mutex_unlock(domain->lock);
- return NULL;
- }
(1)这个函数就是将传来的数据强转成cdev,返回其中的kobj
- static struct kobject *exact_match(dev_t dev, int *part, void *data)
- {
- struct cdev *p = data;
- return &p->kobj;
- }
这里我们总结一下cdev_map和chrdevs。
1.注册设备号时,调用register_chrdev,此函数调用__register_chrdev_region分配设备号
2.__register_chrdev_region函数创建并初始化了char_device_struct结构的变量,初始化的变量包括主设备号(major),次设备号(baseminor),设备数量(minorct)名字(name)等并加入到chrdevs全局变量,此时成员file_operations还没有指定,现在只是占了chrdevs中的位置和存储了设备号相关的信息。
3.接着创建并初始化了cdev,设置了ower和f_op。接着cdev_add函数,此函数先设置了cdev的设备号(cdev的成员此时都设置完了),然后调用了kobj_map函数
4.kobj_map函数做的事情主要是,创建一个probe结构(kobj_map结构的成员结构),并用传递来的cdev填充data成员,owner,设备号,get函数指针(exact_match),和lock函数指针(exact_lock)。最后将这个填充号的probe添加如cdev_map变量。
5.将第2不中的char_device_struct结构的cdev成员赋值。
至此添加任务完成了,在open时如何查找,我们接着总结
1.在chrdev_open函数中,将设备号并将cdev_map传递给kobj_lookup函数,
2.kobj_lookup函数,根据设备号得到probe,取出data即cdev。然后用exact_match函数返回cdev中的kobject。
3.container_of函数获得kobj的包含变量cdev,之后将cdev的f_op设置给file的f_op。至此file就和f_op关联上了。
学习至此,感觉没看懂,还是有几个重要的疑问,
1.chrdevs仅仅就是为设备号占了个位,并没有做什么,其file_operations都没有用到。之后在打开设备节点时也无关chrdevs。那这样要这个这么大的变量做什么,如果仅仅为设备号占了个位,cdev_map变量完全可以代劳(他们的组织结构一样,都是一个255的数组,数组项中都是个链表。那cdev_map也可以表示那些设备号被占用了。)
2.在chrdev_open函数中,既然在cdev_map中找到了相应probe变量,那probe变量中的data就是cdev,为什么不直接用。费要用kobj_lookup函数将probe变量中的data(就是cdev)的kobj搞出来,在用container_of还原成cdev。这样的多此一举又是为什么呢?
阅读(2231) | 评论(0) | 转发(0) |