我在Linux字符设备驱动框架一文中已经简单的介绍了字符设备驱动的基本的编程框架,这里我们来探讨一下Linux内核(以4.8.5内核为例)是怎么管理字符设备的,即当我们获得了设备号,分配了cdev结构,注册了驱动的操作方法集,最后进行cdev_add()的时候,究竟是将哪些内容告诉了内核,内核又是怎么管理我的cdev结构的,这就是本文要讨论的内容。我们知道,Linux内核对设备的管理是基于kobject的(参见Linux设备管理(一)_kobject_kset_kobj_type),这点从我们的cdev结构中就可以看出,所以,接下来,你将看到"fs/char_dev.c"中实现的操作字符设备的函数都是基于"lib/kobject.c"以及"drivers/base/map.c"中对kobject操作的函数。好,现在我们从cdev_add()开始一层层的扒。

cdev_map对象

//fs/char_dev.c
27 static struct kobj_map *cdev_map;

内核中关于字符设备的操作函数的实现放在"fs/char_dev.c"中,打开这个文件,首先注意到就是这个在内核中不常见的静态全局变量cdev_map(27),我们知道,为了提高软件的内聚性,Linux内核在设计的时候尽量避免使用全局变量作为函数间数据传递的方式,而建议多使用形参列表,而这个结构体变量在这个文件中到处被使用,所以它应该是描述了系统中所有字符设备的某种信息,带着这样的想法,我们可以在"drivers/base/map.c"中找到kobj_map结构的定义:

//drivers/base/map.c
19 struct kobj_map {
20 struct probe {
21 struct probe *next;
22 dev_t dev;
23 unsigned long range;
24 struct module *owner;
25 kobj_probe_t *get;
26 int (*lock)(dev_t, void *);
27 void *data;
28 } *probes[255];
29 struct mutex *lock;
30 };

从中可以看出,kobj_map的核心就是一个struct probe类型、大小为255的数组,而在这个probe结构中,第一个成员next(21)显然是将这些probe结构通过链表的形式连接起来,dev_t类型的成员dev显然是设备号,get(25)和lock(26)分别是两个函数接口,最后的重点来了,void作为C语言中的万金油类型,在这里就是我们cdev结构(通过后面的分析可以看出),所以,这个cdev_map是一个struct kobj_map类型的指针,其中包含着一个struct probe*类型、大小为255的数组,数组的每个元素指向的一个probe结构封装了一个设备号和相应的设备对象(这里就是cdev),下图中体现两种常见的对设备号和cdev管理的方式,其一是一个cdev对象对应这一个/多个设备号的情况, 在cdev_map中, 一个probes对象就对应一个主设备号,多个设备号对应一个cdev时,其实只是次设备号在变,主设备号还是一样的,所以是同一个probes对象;其二是当主设备号超过255时,会进行probe复用,此时probe->next就派上了用场,比如probe[200],可以表示设备号200,455...3895等所有对255取余是200的数字, 参见下文的kobj_map--58--。

cdev_add

了解了cdev_map的功能,我们就可以一探cdev_add()。从中可以看出,其工作显然是交给了kobj_map()

cdev_add()

--460-->就是将我们之前获得设备号和设备号长度填充到cdev结构中,

--468-->kobject_get()将kobject的计数减一,并返回struct kobject*

//fs/char_dev.c
456 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
457 {
458 int error;
459
460 p->dev = dev;
461 p->count = count;
462
463 error = kobj_map(cdev_map, dev, count, NULL,
464 exact_match, exact_lock, p);
465 if (error)
466 return error;
467
468 kobject_get(p->kobj.parent);
469
470 return 0;
471 }

kobj_map()

这个函数在内核的设备管理中占有重要的地位,这里我们只从字符设备的角度分析它的功能,这个函数的设计也很单纯,就是封装好一个probe结构并将它的地址放入probes数组进而封装进cdev_map,。

kobj_map()

--48-55-->根据传入的设备号的个数,将设备号和cdev依次封装到kmalloc_array()分配的n个probe结构中

--57-63-->就是遍历probs数组,直到找到一个值为NULL的元素,再将probe的地址存入probes, 将设备号对255取余后与probes的下标对应。至此,我们就将我们的cdev放入的内核的数据结构

//drivers/base/map.c
32 int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
33 struct module *module, kobj_probe_t *probe,
34 int (*lock)(dev_t, void *), void *data)
35 {
36 unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
37 unsigned index = MAJOR(dev);
38 unsigned i;
39 struct probe *p;
...
44 p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);
...
48 for (i = 0; i < n; i++, p++) {
49 p->owner = module;
50 p->get = probe;
51 p->lock = lock;
52 p->dev = dev;
53 p->range = range;
54 p->data = data;
55 }
56 mutex_lock(domain->lock);
57 for (i = 0, p -= n; i < n; i++, p++, index++) {
58 struct probe **s = &domain->probes[index % 255];
59 while (*s && (*s)->range < range)
60 s = &(*s)->next;
61 p->next = *s;
62 *s = p;
63 }
64 mutex_unlock(domain->lock);
65 return 0;
66 }

chrdev_open()

将设备放入的内核,我们再来看看内核是怎么找到一个特定的cdev的。

首先,在一个字符设备文件被创建的时候,内核会构造相应的inode,作为一种特殊文件,其inode初始化的时候,就会做一些准备工作

//fs/char_dev.c
429 const struct file_operations def_chr_fops = {
430 .open = chrdev_open,
431 .llseek = noop_llseek,
432 };
//fs/inode.c
1923 void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
1924 {
1925 inode->i_mode = mode;
1926 if (S_ISCHR(mode)) {
1927 inode->i_fop = &def_chr_fops; //Here
1928 inode->i_rdev = rdev; //and Here
1929 } else if (S_ISBLK(mode)) {
1930 inode->i_fop = &def_blk_fops;
1931 inode->i_rdev = rdev;
1932 } else if (S_ISFIFO(mode))
1933 inode->i_fop = &pipefifo_fops;
1934 else if (S_ISSOCK(mode))
1935 ; /* leave it no_open_fops */
1936 else
1937 printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
1938 " inode %s:%lu\n", mode, inode->i_sb->s_id,
1939 inode->i_ino);
1940 }

由此可见,对一个字符设备的访问流程大概是:文件路径=>inode=>chrdev_open()=>(kobj_lookup=>)inode.i_cdev=>cdev.fops.my_chr_open()。所以只要通过VFS找到了inode,就可以找到chrdev_open(),这里我们就来关注一个chrdev_open()是怎么从内核的数据结构中找到我们的cdev并执行其中的my_chr_open()的。比较有意思的是,虽然我们有了字符设备的设备文件,inode也被构造并初始化了, 但是在第一次调用chrdev_open()之前,这个inode和具体的chr_dev对象并没有直接关系,而只是通过设备号建立的"间接"关系。在第一次调用chrdev_open()之后, inode->i_cdev才被根据设备号找到的cdev对象赋值,此后inode才和具体的cdev对象直接联系在了一起

//fs/char_dev.c
326 static struct kobject *cdev_get(struct cdev *p)
327 {
328 struct module *owner = p->owner;
329 struct kobject *kobj;
330
331 if (owner && !try_module_get(owner))
332 return NULL;
333 kobj = kobject_get(&p->kobj);
...
336 return kobj;
337 } 351 static int chrdev_open(struct inode *inode, struct file *filp)
352 {
353 const struct file_operations *fops;
354 struct cdev *p;
355 struct cdev *new = NULL;
356 int ret = 0;
...
359 p = inode->i_cdev;
360 if (!p) {
361 struct kobject *kobj;
362 int idx;
...
364 kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
...
367 new = container_of(kobj, struct cdev, kobj);
369 /* Check i_cdev again in case somebody beat us to it while
370 we dropped the lock. */
371 p = inode->i_cdev;
372 if (!p) {
373 inode->i_cdev = p = new;
374 list_add(&inode->i_devices, &p->list);
375 new = NULL;
376 } else if (!cdev_get(p))
377 ret = -ENXIO;
378 } else if (!cdev_get(p))
379 ret = -ENXIO;
...
386 fops = fops_get(p->ops);
...
390 replace_fops(filp, fops);
391 if (filp->f_op->open) {
392 ret = filp->f_op->open(inode, filp);
...
395 }
396
397 return 0;
398
399 out_cdev_put:
400 cdev_put(p);
401 return ret;
402 }

chrdev_open()

--359-->尝试将inode->i_cdev(一个cdev结构指针)保存在局部变量p中,

--360-->如果p为空,即inode->i_cdev为空,

--364-->我们就根据inode->i_rdev(设备号)通过kobj_lookup()搜索cdev_map,并返回与之对应kobj

--367-->由于kobject是cdev的父类,我们根据container_of很容易找到相应的cdev结构并将其保存在inode->i_cdev中,

--374-->找到了cdev,我们就可以将inode->devices挂接到inode->i_cdev的管理链表中,这样下次就不用重新搜索,

--378-->直接cdev_get()即可。

--386-->找到了我们的cdev结构,我们就可以将其中的操作方法集inode->i_cdev->ops传递给filp->f_ops(386-390),

--392-->这样,我们就可以回调我们的设备打开函数my_chr_open();如果我们没有实现自己的open接口,就什么都不做,也不是错

扒完了字符设备的注册过程,不知各位看官有没有发现,全程没有一个初始化cdev.kobj的函数!到此为止,我们都是通过cdev_map来管理系统里的字符设备的,所以,我们并不能在sysfs找到我们此时注册的字符设备,更深层的原因是内核中并不直接使用cdev作为一个设备,而是将其作为一个设备接口,使用这个接口我们可以派生出misc设备,输入设备,LCD等等,当初始化这些具体的字符设备的时候,相应的list_head对象才可能被打开挂接到相应的链表,并初始化kobj。即如果希望sysfs中找到我们的字符设备,我们就必须对cdev.kobj进行初始化,挂接到合适的kset,这也就是导出设备信息到sysfs以便自动创建设备文件的原理

彩蛋

Linux中几乎所有的"设备"都是"device"的子类,无论是平台设备还是i2c设备还是网络设备,但唯独字符设备不是,从"Linux字符设备驱动框架"一文中我们可以看出cdev并不是继承自device,从"Linux设备管理(二)_从cdev_add说起"一文中我们可以看出注册一个cdev对象到内核其实只是将它放到cdev_map中,直到"Linux设备管理(四)_从sysfs回到ktype"一文中对device_create的分析才知道此时才创建device结构并将kobj挂接到相应的链表,,所以,基于历史原因,当下cdev更合适的一种理解是一种接口(使用mknod时可以当作设备),而不是而一个具体的设备,和platform_device,i2c_device有着本质的区别

Linux设备管理(二)_从cdev_add说起的更多相关文章

  1. Linux设备管理(五)_写自己的sysfs接口

    我们在Linux设备管理(一)_kobject, kset,ktype分析一文中介绍了kobject的相关知识,在Linux设备管理(二)_从cdev_add说起和Linux设备管理(三)_总线设备的 ...

  2. Linux操作系统(二)_快速入门

    环境 安装VM ware,输入VM key 在VM上安装CentOS 6.5 设置网络,能在本机上ping通 通过终端连接工具:Xshell或SecureCRT,连接Linux服务器 实操可能出现的问 ...

  3. Linux设备管理(四)_从sysfs回到ktype

    sysfs是一个基于ramfs的文件系统,在2.6内核开始引入,用来导出内核对象(kernel object)的数据.属性到用户空间.与同样用于查看内核数据的proc不同,sysfs只关心具有层次结构 ...

  4. Linux设备管理(四)_从sysfs回到ktype【转】

    转自:https://www.cnblogs.com/xiaojiang1025/archive/2016/12/21/6202298.html sysfs是一个基于ramfs的文件系统,在2.6内核 ...

  5. Linux设备管理(三)_总线设备的挂接

    扒完了字符设备,我们来看看平台总线设备,平台总线是Linux中的一种虚拟总线,我们知道,总线+设备+驱动是Linux驱动模型的三大组件,设计这样的模型就是将驱动代码和设备信息相分离,对于稍微复杂一点的 ...

  6. Linux设备管理(一)_kobject, kset,ktype分析

    Linux内核大量使用面向对象的设计思想,通过追踪源码,我们甚至可以使用面向对象语言常用的UML类图来分析Linux设备管理的"类"之间的关系.这里以4.8.5内核为例从kobje ...

  7. 基于samba实现win7与linux之间共享文件_阳仔_新浪博客

    基于samba实现win7与linux之间共享文件_阳仔_新浪博客 然后启动samba执行如下指令: /dev/init.d/smb start 至此完成全部配置.

  8. Linux操作系统学习_操作系统是如何工作的

    实验五:Linux操作系统是如何工作的? 学号:SA1****369 操作系统工作的基础:存储程序计算机.堆栈(函数调用堆栈)机制和中断机制 首先要整明白的一个问题是什么是存储程序计算机?其实存储程序 ...

  9. Linux(二)—— Unix&Linux 的基本概念

    Linux(二)-- Unix&Linux 的基本概念 计算机 = 主机(host)+ 终端(terminal) 主机 = 内核 + 实用工具 内核(kernel) 当计算机启动时,计算机要经 ...

随机推荐

  1. Javascript 的执行环境(execution context)和作用域(scope)及垃圾回收

    执行环境有全局执行环境和函数执行环境之分,每次进入一个新执行环境,都会创建一个搜索变量和函数的作用域链.函数的局部环境不仅有权访问函数作用于中的变量,而且可以访问其外部环境,直到全局环境.全局执行环境 ...

  2. Microsoft Loves Linux

    微软新任CEO纳德拉提出的“Microsoft Loves Linux”,并且微软宣布.NET框架的开源,近期Microsoft不但宣布了Linux平台的SQL Server,还宣布了Microsof ...

  3. 从备考PMP到与项目经理同呼吸

    前言 PMP是什么梗? 项目管理专业人士资格认证.它是由美国项目管理协会(Project Management Institute(PMI)发起的,严格评估项目管理人员知识技能是否具有高品质的资格认证 ...

  4. iOS微信里打开app,Universal Links

    这两天在弄分享,从第三方应用或者浏览器打开自己app的东西 传统的方式是通过URL Scheme的方式,但是iOS9以后又出了新的更完美的方式Universal Links. 传统的URL Schem ...

  5. 【C#公共帮助类】 ToolsHelper帮助类

    这个帮助类,目前我们只用到了两个,我就先更新这两个,后面有用到的,我会继续更新这个Helper帮助类 在Tools.cs中 有很多方法 跟Utils里是重复的,而且Utils里的方法更加新一点,大家可 ...

  6. Javascript中的valueOf与toString

    基本上,javascript中所有数据类型都拥有valueOf和toString这两个方法,null除外.它们俩解决javascript值运算与显示的问题,本文将详细介绍,有需要的朋友可以参考下. t ...

  7. 太多选择——企业如何选择合适的BI工具?

    在没认清现状前,企业当然不能一言不合就上BI. BI不同于一般的企业管理软件,不能简单归类为类似用于提高管理的ERP和WMS,或用于提高企业效率的OA.BPM.BI的本质应该是通过展现数据,用于加强企 ...

  8. 亡命之徒aaaaaa.......chao

    前端是一个看似入门门槛不高,但要学好很难的领域.前端的知识体系庞杂又松散,技术演进快,如果摸不清脉络的话很容易陷入盲人摸象的困境甚至跑偏.其实只要掌握了正确的方法,学习前端和学好前端就只是个时间问题. ...

  9. 透过浏览器看HTTP缓存

    作为前端开发人员,对于我们的站点或应用的缓存机制我们能做的似乎不多,但这些却是与我们关注的性能息息相关的部分,站点没有做任何缓存机制,我们的页面可能会因为资源的下载和渲染变得很慢,但大家都知道去找前端 ...

  10. Git入门资料汇总

    Git是一个非常好用的版本控制工具,同时,它也是一个相对比较复杂的工具,想要掌握它还是需要花一番功夫的.网络上关于Git的入门资料已经很多了,我就不再重复了,直接把我学习的文章放在这里. Git详解 ...