Linux设备管理(二)_从cdev_add说起

时间:2022-12-30 12:25:46

我在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--。

Linux设备管理(二)_从cdev_add说起

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设备管理&lpar;五&rpar;&lowbar;写自己的sysfs接口

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

  2. Linux操作系统&lpar;二&rpar;&lowbar;快速入门

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

  3. Linux设备管理(四)&lowbar;从sysfs回到ktype

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

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

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

  5. Linux设备管理(三)&lowbar;总线设备的挂接

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

  6. Linux设备管理(一)&lowbar;kobject&comma; kset&comma;ktype分析

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

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

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

  8. Linux操作系统学习&lowbar;操作系统是如何工作的

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

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

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

随机推荐

  1. 【Cocos2d-x 3&period;x】 动作类Action源码分析

    游戏设计中,动作是不可缺少的,Cocos2d-x中所有的动作都继承自Action类,而Action类继承自Ref和Clonable类,整个动作类继承体系如图: FiniteTimeAction是所有瞬 ...

  2. 第1周 支路变量、元件、KCL和KVL

    第1周的内容,介绍了: 电阻.独立源.受控元件等实体元器件, 电流.电压.功率等抽象名词, 端口.参考方向等分析时的概念工具, KCL.KVL两大分析定律, 解线性电路的普适方法----2B法. 引入 ...

  3. &lbrack;NOIP2010&rsqb; 普及组

    三国游戏 题目内容不放了 由于电脑总是会拆掉最大的组合,所以玩家最多只能得到数值第二大的组合 那么找出第二大的组合就行了 #include<iostream> #include<cs ...

  4. IIS不定期Crash和Oracle&OpenCurlyDoubleQuote;未处理的内部错误&lpar;-2&rpar;”的问题分析

    问题描述:系统不定期报出Oracle“未处理的内部错误(-2)”,严重时IIS会Crash 典型异常日志如下: Exception type:   System.AccessViolationExce ...

  5. oracle 10G以上版本 树形查询新加的几个功能

    1.判断当前节点是否叶子节点 在 Oracle 10g 中,还有其他更多关于层次查询的新特性 .例如,有的时候用户更关心的是每个层次分支中等级最低的内容.那么你就可以利用伪列函数CONNECT_BY_ ...

  6. Oracle学习笔记之PL&sol;SQL编程

           SQL(Structure Query Language)的含义是结构化查询语句,最早由Boyce和Chambedin在1974年提出,称为SEQUEL语言.1976年,IBM公司的Sa ...

  7. LeetCode 102&period; Binary Tree Level Order Traversal 二叉树的层次遍历 C&plus;&plus;

    Given a binary tree, return the level order traversal of its nodes' values. (ie, from left to right, ...

  8. 数据结构python编程总结

    大数据.空间限制 布隆过滤器 使用很少的空间就可以将准确率做到很高的程度(网页黑名单系统.垃圾邮件过滤系统.爬虫的网址判重系统等) 有一定的失误率 单个样本的大小不影响布隆过滤器的大小 n个输入.k个 ...

  9. 微信小程序记账本进度三

    //index.jsvar util = require("../../utils/util.js"); //获取应用实例 var app = getApp(); Page({ d ...

  10. FCC JS基础算法题&lpar;1&rpar;&colon;Factorialize a Number&lpar;计算一个整数的阶乘&rpar;

    题目描述: 如果用字母n来代表一个整数,阶乘代表着所有小于或等于n的整数的乘积.阶乘通常简写成 n!例如: 5! = 1 * 2 * 3 * 4 * 5 = 120. 算法: function fac ...