31 | 瞧一瞧Linux:如何获取所有设备信息?
下载APP
关闭
渠道合作
推荐作者
31 | 瞧一瞧Linux:如何获取所有设备信息?
2021-07-19 LMOS 来自北京
《操作系统实战45讲》
课程介绍
讲述:陈晨
时长16:42大小15.26M
你好,我是 LMOS。
前面我们已经完成了 Cosmos 的驱动设备的建立,还写好了一个真实的设备驱动。
今天,我们就来看看 Linux 是如何管理设备的。我们将从 Linux 如何组织设备开始,然后研究设备驱动相关的数据结构,最后我们还是要一起写一个 Linux 设备驱动实例,这样才能真正理解它。
感受一下 Linux 下的设备信息
Linux 的设计哲学就是一切皆文件,各种设备在 Linux 系统下自然也是一个个文件。不过这个文件并不对应磁盘上的数据文件,而是对应着存在内存当中的设备文件。实际上,我们对设备文件进行操作,就等同于操作具体的设备。
既然我们了解万事万物,都是从最直观的感受开始的,想要理解 Linux 对设备的管理,自然也是同样的道理。那么 Linux 设备文件在哪个目录下呢?其实现在我们在 /sys/bus 目录下,就可以查看所有的设备了。
Linux 用 BUS(总线)组织设备和驱动,我们在 /sys/bus 目录下输入 tree 命令,就可以看到所有总线下的所有设备了,如下图所示。
Linux设备文件
上图中,显示了部分 Linux 设备文件,有些设备文件是链接到其它目录下文件,这不是重点,重点是你要在心中有这个目录层次结构,即总线目录下有设备目录,设备目录下是设备文件。
数据结构
我们接着刚才的图往下说,我们能感觉到 Linux 的驱动模型至少有三个核心数据结构,分别是总线、设备和驱动,但是要像上图那样有层次化地组织它们,只有总线、设备、驱动这三个数据结构是不够的,还得有两个数据结构来组织它们,那就是 kobject 和 kset,下面我们就去研究它们。
kobject 与 kset
kobject 和 kset 是构成 /sys 目录下的目录节点和文件节点的核心,也是层次化组织总线、设备、驱动的核心数据结构,kobject、kset 数据结构都能表示一个目录或者文件节点。下面我们先来研究一下 kobject 数据结构,代码如下所示。
每一个 kobject,都对应着 /sys 目录下(其实是 sysfs 文件系统挂载在 /sys 目录下) 的一个目录或者文件,目录或者文件的名字就是 kobject 结构中的 name。
我们从 kobject 结构中可以看出,它挂载在 kset 下,并且指向了 kset,那 kset 是什么呢?我们来分析分析,它是 kobject 结构的容器吗?
其实是也不是,因为 kset 结构中本身又包含一个 kobject 结构,所以它既是 kobject 的容器,同时本身还是一个 kobject。kset 结构代码如下所示。
看到这里你应该知道了,kset 不仅仅自己是个 kobject,还能挂载多个 kobject,这说明 kset 是 kobject 的集合容器。在 Linux 内核中,至少有两个顶层 kset,代码如下所示。
我知道,你可能很难想象许多个 kset 和 kobject 在逻辑上形成的层次结构,所以我为你画了一幅图,你可以结合这张示意图理解这个结构。
kset与kobject
上图中展示了一个类似文件目录的结构,这正是 kset 与 kobject 设计的目标之一。kset 与 kobject 结构只是基础数据结构,但是仅仅只有它的话,也就只能实现这个层次结构,其它的什么也不能干,根据我们以往的经验可以猜出,kset 与 kobject 结构肯定是嵌入到更高级的数据结构之中使用,下面我们继续探索。
总线
kset、kobject 结构只是开胃菜,这个基础了解了,我们还要回到研究 Linux 设备与驱动的正题上。我们之前说过了,Linux 用总线组织设备和驱动,由此可见总线是 Linux 设备的基础,它可以表示 CPU 与设备的连接,那么总线的数据结构是什么样呢?我们一起来看看。
Linux 把总线抽象成 bus_type 结构,代码如下所示。
可以看出,上面代码的 bus_type 结构中,包括总线名字、总线属性,还有操作该总线下所有设备通用操作函数的指针,其各个函数的功能我在代码注释中已经写清楚了。
从这一点可以发现,总线不仅仅是组织设备和驱动的容器,还是同类设备的共有功能的抽象层。下面我们来看看 subsys_private,它是总线的驱动核心的私有数据,其中有我们想知道的秘密,代码如下所示。
看到这里,你应该明白 kset 的作用了,我们通过 bus_kset 可以找到所有的 kset,通过 kset 又能找到 subsys_private,再通过 subsys_private 就可以找到总线了,也可以找到该总线上所有的设备与驱动。
设备
虽然 Linux 抽象出了总线结构,但是 Linux 还需要表示一个设备,下面我们来探索 Linux 是如何表示一个设备的。
其实,在 Linux 系统中设备也是一个数据结构,里面包含了一个设备的所有信息。代码如下所示。
device 结构很大,这里删除了我们不需要关心的内容。另外,我们看到 device 结构中同样包含了 kobject 结构,这使得设备可以加入 kset 和 kobject 组建的层次结构中。device 结构中有总线和驱动指针,这能帮助设备找到自己的驱动程序和总线。
驱动
有了设备结构,还需要有设备对应的驱动,Linux 是如何表示一个驱动的呢?同样也是一个数据结构,其中包含了驱动程序的相关信息。其实在 device 结构中我们就看到了,就是 device_driver 结构,代码如下。
在 device_driver 结构中,包含了驱动程序的名字、驱动程序所在模块、设备探查和电源相关的回调函数的指针。在 driver_private 结构中同样包含了 kobject 结构,用于组织所有的驱动,还指向了驱动本身,你发现没有,bus_type 中的 subsys_private 结构的机制如出一辙。
文件操作函数
前面我们学习的都是 Linux 驱动程序的核心数据结构,我们很少用到,只是为了让你了解最基础的原理。
其实,在 Linux 系统中提供了更为高级的封装,Linux 将设备分成几类分别是:字符设备、块设备、网络设备以及杂项设备。具体情况你可以参考我后面梳理的图表。
设备类型一览表
这些类型的设备的数据结构,都会直接或者间接包含基础的 device 结构,我们以杂项设备为例子研究一下,Linux 用 miscdevice 结构表示一个杂项设备,代码如下。
miscdevice 结构就是一个杂项设备,它一般在驱动程序代码文件中静态定义。我们清楚地看见有个 this_device 指针,它指向下层的、属于这个杂项设备的 device 结构。
但是这里重点是 file_operations 结构,设备一经注册,就会在 sys 相关的目录下建立设备对应的文件结点,对这个文件结点打开、读写等操作,最终会调用到驱动程序对应的函数,而对应的函数指针就保存在 file_operations 结构中,我们现在来看看这个结构。
file_operations 结构中的函数指针有 31 个,我删除了我们不熟悉的函数指针,我们了解原理,不需要搞清楚所有函数指针的功能。
那么,Linux 如何调用到这个 file_operations 结构中的函数呢?我以打开操作为例给你讲讲,Linux 的打开系统调用接口会调用 filp_open 函数,filp_open 函数的调用路径如下所示。
看到这里,我们就知道了,file_operations 结构的地址存在一个文件的 inode 结构中。在 Linux 系统中,都是用 inode 结构表示一个文件,不管它是数据文件还是设备文件。
到这里,我们已经清楚了文件操作函数以及它的调用流程。
驱动程序实例
我们想要真正理解 Linux 设备驱动,最好的方案就是写一个真实的驱动程序实例。下面我们一起应用前面的基础,结合 Linux 提供的驱动程序开发接口,一起实现一个真实驱动程序。
这个驱动程序的主要工作,就是获取所有总线和其下所有设备的名字。为此我们需要先了解驱动程序的整体框架,接着建立我们总线和设备,然后实现驱动程序的打开、关闭,读写操作函数,最后我们写个应用程序,来测试我们的驱动程序。
驱动程序框架
Linux 内核的驱动程序是在一个可加载的内核模块中实现,可加载的内核模块只需要两个函数和模块信息就行,但是我们要在模块中实现总线和设备驱动,所以需要更多的函数和数据结构,它们的代码如下。
一个最简单的驱动程序框架的内核模块就写好了,该有的函数和数据结构都有了,那些数据结构都是静态定义的,它们的内部字段我们在前面也已经了解了。这个模块一旦加载就会执行 miscdrv_init 函数,卸载时就会执行 miscdrv_exit 函数。
建立设备
Linux 系统也提供了很多专用接口函数,用来建立总线和设备。下面我们先来建立一个总线,然后在总线下建立一个设备。
首先来说说建立一个总线,Linux 系统提供了一个 bus_register 函数向内核注册一个总线,相当于建立了一个总线,我们需要在 miscdrv_init 函数中调用它,代码如下所示。
bus_register 函数会在系统中注册一个总线,所需参数就是总线结构的地址 (&devicesinfo_bus),返回非 0 表示注册失败。现在我们来看看,在 bus_register 函数中都做了些什么事情,代码如下所示。
我删除了很多你不用关注的代码,看到这里,你应该知道总线是怎么通过 subsys_private 把设备和驱动关联起来的(通过 bus_type 和 subsys_private 结构互相指向),下面我们看看怎么建立设备。我们这里建立一个 misc 杂项设备。misc 杂项设备需要定一个数据结构,然后调用 misc 杂项设备注册接口函数,代码如下。
上面的代码中,静态定义了 miscdevice 结构的变量 misc_dev,miscdevice 结构我们在前面已经了解过了,最后调用 misc_register 函数注册了 misc 杂项设备。
misc_register 函数到底做了什么,我们一起来看看,代码如下所示。
可以看出,misc_register 函数只是负责分配设备号,以及把 miscdev 加入链表,真正的核心工作由 device_create_with_groups 函数来完成,代码如下所示。
到这里,misc 设备的注册就搞清楚了,下面我们来测试一下看看结果,看看 Linux 系统是不是多了一个总线和设备。
你可以在本课程的代码目录中,执行 make 指令,就会产生一个 miscdvrv.ko 内核模块文件,我们把这个模块文件加载到 Linux 系统中就行了。
为了看到效果,我们还必须要做另一件事情。 在终端中用 sudo cat /proc/kmsg 指令读取 /proc/kmsg 文件,该文件是内核 prink 函数输出信息的文件。指令如下所示。
insmod 指令是加载一个内核模块,一旦加载成功就会执行 miscdrv_init 函数。如果不出意外,你在终端中会看到如下图所示的情况。
驱动测试
这说明我们设备已经建立了,你应该可以在 /dev 目录看到一个 devicesinfo 文件,同时你在 /sys/bus/ 目录下也可以看到一个 devicesinfobus 文件。这就是我们建立的设备和总线的文件节点的名称。
打开、关闭、读写函数
建立了设备和总线,有了设备文件节点,应用程序就可以打开、关闭以及读写这个设备文件了。
虽然现在确实可以操作设备文件了,只不过还不能完成任何实际功能,因为我们只是写好了框架函数,所以我们下面就去写好并填充这些框架函数,代码如下所示。
以上三个函数,仍然没干什么实际工作,就是打印该函数所在文件的行号和名称,然后返回 0 就完事了。回到前面,我们的目的是要获取 Linux 中所有总线上的所有设备,所以在读函数中来实现是合理的。
具体实现的代码如下所示。
正常情况下,我们是不能获取 bus_kset 地址的,它是所有总线的根,包含了所有总线的 kobject,Linux 为了保护 bus_kset,并没有在 bus_type 结构中直接包含 kobject,而是让总线指向一个 subsys_private 结构,在其中包含了 kobject 结构。
所以,我们要注册一个总线,这样就能拔出萝卜带出泥,得到 bus_kset,根据它又能找到所有 subsys_private 结构中的 kobject,接着找到 subsys_private 结构,反向查询到 bus_type 结构的地址。
然后调用 Linux 提供的 bus_for_each_dev 函数,就可以遍历一个总线上的所有设备,它每遍历到一个设备,就调用一个函数,这个函数是用参数的方式传给它的,在我们代码中就是 misc_find_match 函数。
在调用 misc_find_match 函数时,会把一个设备结构的地址和另一个指针作为参数传递进来。最后就能打印每个设备的名称了。
测试驱动
驱动程序已经写好,加载之后会自动建立设备文件,但是驱动程序不会主动工作,我们还需要写一个应用程序,对设备文件进行读写,才能测试驱动。我们这里这个驱动对打开、关闭、写操作没有什么实际的响应,但是只要一读就会打印所有设备的信息了。
下面我们来写好这个应用,代码如下所示。
你可以这样操作:切换到本课程的代码目录 make 一下,然后加载 miscdrv.ko 模块,最后在终端中执行 sudo ./app,就能在另一个已经执行了 sudo cat /proc/kmsg 的终端中,看到后面图片这样形式的数据。
获取设备名称
上图是我系统中总线名和设备名,你的计算机上可能略有差异,因为我们的计算机硬件可能不同,所以有差异是正常的,不必奇怪。
重点回顾
尽管 Linux 驱动模型异常复杂,我们还是以最小的成本,领会了 Linux 驱动模型设计的要点,还动手写了个小小的驱动程序。现在我来为你梳理一下这节课的重点。
首先,我们通过查看 sys 目录下的文件层次结构,直观感受了一下 Linux 系统的总线、设备、驱动是什么情况。
然后,我们了解一些重要的数据结构,它们分别是总线、驱动、设备、文件操作函数结构,还有非常关键的 kset 和 kobject,这两个结构一起组织了总线、设备、驱动,最终形成了类目录文件这样的层次结构。
最后,我们建立一个驱动程序实例,从驱动程序框架开始,我们了解如何建立一个总线和设备,编写了对应的文件操作函数,在读操作函数中实现扫描了所有总线上的所有设备,并打印总线名称和设备名称,还写了个应用程序进行了测试,检查有没有达到预期的功能。
如果你对 Linux 是怎么在总线上注册设备和驱动,又对驱动和设备怎么进行匹配感兴趣的话,也可以自己阅读 Linux 内核代码,其中有很多驱动实例,你可以研究和实验,动手和动脑相结合,我相信你一定可以搞清楚的。
思考题
为什么无论是我们加载 miscdrv.ko 内核模块,还是运行 app 测试,都要在前面加上 sudo 呢?
欢迎你在留言区记录你的学习收获,也欢迎你把这节课分享给你身边的小伙伴,一起拿下 Linux 设备驱动的内容。
我是 LMOS,我们下节课见!
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 9
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
30 | 部门响应:设备如何处理内核I/O包?
下一篇
32 | 仓库结构:如何组织文件?
精选留言(9)
- neohope置顶2021-08-06关于驱动程序Demo 一、miscdrv是一个内核模块 1、四个操作函数,封装在file_operations结构中,包括: misc_open在打开设备文件时执行 misc_release在关闭设备文件时执行 misc_read在读取设备时执行 misc_write在写入设备时执行 file_operations又被封装在miscdevice中,在注册设备时传入 2、devicesinfo_bus_match函数用于总线设备的过滤,被封装在bus_type结构中 bus_type描述了总线结构,在总线注册时传入 3、module_init和module_exit声明入口和出口函数: miscdrv_init注册设备和总线,在安装内核模块时执行 miscdrv_exit反注册设备和总线,在卸载内核模块时执行 4、只有misc_read比较复杂: A、通过注册时的devicesinfo_bus获取kset,枚举kset中的每一个kobj B、对于每个kobj,通过container_of转换为subsys_private C、对于每个subsys_private,枚举其bus中每个设备,并通过misc_find_match函数进行处理 D、misc_find_match会在kmsg中输出设备名称 二、app.c 就是打开设备,写一下,读一下,关闭设备,主要是触发设备输出 三、执行顺序,需要两个Terminal,T1和T2 1、T1:make 2、T1:sudo insmod miscdrv.ko 3、T2:sudo cat /proc/kmsg 4、T1:sudo ./app 5、T2:ctrl+c 6、T1:sudo rmmod miscdrv.ko展开
作者回复: 老铁 可谓 学习能力爆表
8 - neohope置顶2021-08-06关于数据结构 一、目录组织相关结构 kobject结构表示sysfs一个目录或者文件节点,同时提供了引用计数或生命周期管理相关功能; kset结构,可以看作一类特殊的kobject,可以作为kobject的集合;同时承担了发送用户消息的功能; Linux通过kobject和 kset来组织sysfs下的目录结构。但两者之间关系,却并非简单的文件和目录的关系。每个kobject的父节点,需要通过parent和kset两个属性来决定: A、无parent、无kset,则将在sysfs的根目录(即/sys/)下创建目录; B、无parent、有kset,则将在kset下创建目录;并将kobj加入kset.list; C、有parent、无kset,则将在parent下创建目录; D、有parent、有kset,则将在parent下创建目录,并将kobj加入kset.list; kobject和kset并不会单独被使用,而是嵌入到其他结构中发挥作用。 二、总线与设备结构 bus_type结构,表示一个总线,其中 subsys_private中包括了kset; device结构,表示一个设备,包括驱动指针、总线指针和kobject; device_driver结构,表示一个驱动,其中 driver_private包括了kobject; 上面说的kset和kobject的目录组织关系,起始就是存在于这些数据结构中的; 通过kset和kobject就可以实现总线查找、设备查找等功能; 三、初始化 全局kset指针devices_kset管理所有设备 全局kset指针bus_kset管理所有总线 初始化调用链路: kernel_init->kernel_init_freeable->do_basic_setup->driver_init ->devices_init设备初始化 ->buses_init总线初始化 四、设备功能函数调用 miscdevice结构,表示一个杂项设备; 其中 file_operations包含了全部功能函数指针; 以打开一个设备文件为例,其调用链路为: filp_open->file_open_name->do_filp_open->path_openat->do_o_path->vfs_open->do_dentry_open 通过file_operations获取了open函数指针,并进行了调用展开
作者回复: 是的 哈哈
8 - 艾恩凝置顶2022-05-09以前作为使用者去写驱动,现在以提供者的角度去分析驱动,又来到了熟悉的file_operations结构了,写过简单的内核2.6 和 3.4版本的驱动,每个版本都稍有区别,盲猜这个是5版本的,毕竟有些陌生的函数,刚才看到了一个评论驱动是如何操作硬件的,来个最简单的按键驱动就知道了,这还没谈谈设备树,设备树也应该谈谈,到底是哪个大聪明想到的设备树共 1 条评论1
- 青玉白露2021-07-19在Linux系统中,sudo可以获取超级用户的权利,它之后的命令可以在内核态下进行工作。 而加载miscdrv.ko模块和app测试都需要在内核态下进行。
作者回复: 是的
共 2 条评论5 - pedro2021-07-19加载内核模块,使用内核驱动,得 sudo 权限
作者回复: 哈哈 这问题又太简单了
2 - Zhang2022-06-25终于看到这了,看到曙光了感觉,打个卡,感觉今天课后作业简单了
编辑回复: 6666,进展很快嘛,继续加油!
- Geek_Lawrence2022-01-29驱动程序仍然是“软件”部分,“软件”如何驱动“硬件”,这部分能否更细致点,想了解下驱动程序是如何连接硬件并且驱动硬件工作的,其更加底层的工作原理是什么呢?
作者回复: 那你需要了解每一种设备的编程细节 ,这通常需要熟读每一种设备的编程数据手册
1 - Mingjie2021-11-28老师,HDMI驱动是属于哪类驱动设备?我觉得是块类型设备,不是有显存这个说法嘛,我想的对吗
作者回复: 属于是MISC
共 2 条评论 - 驰往2021-08-31既陌生又熟悉的代码。曾几何时,大学的时候就是从platform device入手,深入学习驱动设备模型,然而由于工作环境,越工作越上层,最终没有踏入内核这片圣地。如今再学操作系统,希望能够借此机会,了却当年的初心😄。
作者回复: 哈哈 程序员的三大浪漫之一