设备驱动 - Linux Device Driver Model

写在前面

理解device driver model,我觉得最难的就是“忘掉自己曾经知道的”。学习内核很多情况下都需要这项技能:忘掉自己知道的,我们才不至于望文生义想当然,才能去接纳新知识,才能够耐着性子去阅读思考,才会更关注“为什么要这样呢”。所以如果刚接触Linux内核的你对设备驱动存在些许认识或者幻想,现在都请忘了吧。文中出现的术语如device、driver等,为了避免想当然,一开始都请把它当成一个名字而已,不要试图猜测其语义,也最好不要翻译成中文(所以请原谅文中的中英文混杂)。当我们理解了以后再看这些术语,我们就会恍然大悟它们为什么叫这个名字了。

Device Driver Model的组成部分

这一节我们介绍Device Driver Model的组件:device、driver、bus和class。为什么要使用这些组件(本质上是数据结构)乃至为什么要使用Device Driver Model?网上有人总结得很精辟:统一了编写设备驱动的格式,使驱动开发从论述题变为填空体,从而简化了设备驱动的开发[1]。

kobject——骨架

我阅读的关于Device Driver Model的资料一般都会在一开始介绍kobject,然而并没有告诉我们为什么要了解kobject。对熟悉面向对象编程概念的读者来说,一言以蔽之:kobject是device driver model中所有组件的基类,是模型的骨架,因此非常重要。如果对面向对象的概念不熟悉也不要紧,下面具体介绍为何我们需要kobject。

Device Driver Model是一种”类树状”的模型——本质上它是一张网,但大体上是树的形态。它把所有组件对象组织成树,然后又用符号链接在树上构建一些新的关系。一个简单的示意图如下:

device_model_tree_1

为了表示这个关系,内核使用了kobject数据结构,利用kobject包含的parent、children、sibling等域对关系进行描述。

kobject的另外一个重要作用就是对各组件对象的引用进行计数。计数的好处在于可以保证资源会在合适的时候被释放。

不过kobject自身功能有限,仅仅能构建出一个关系骨架。事实上内核里通常并不直接使用这个数据结构,而是选择将kobject嵌入到真正完成动作的、更复杂的数据结构里,例如下面即将讨论的Device Driver Mode组件——device、driver、bus和class。Device Driver Model中的树形结构更精确的示意图如下:

device_model_tree_2

后面我们会专门介绍所谓的“kobject嵌入到真正完成动作的、更复杂的数据结构”:device、driver等。现在我们介绍与kobject相关的另外两个数据结构:kset和ktype。

简言之,kset是盛放kobject的容器。从sysfs文件系统角度解释就是:一个kobject对应sysfs的一个目录(kobject属性对应其中的文件)。为了使sysfs结构层次化,需要建立高层次的目录去容纳kobject对应的目录,这些高层次的目录就是kset对应的目录。

ktype数据结构表示kobject的类型。它事实上是一组数据结构:kobj_set、kset_ktype等等。光看名字也不知道到底是什么,但我们还是尝试解释一下问什么起这个名字:ktype主要是定义一些针对kobject对象的操作。一类对象共享一组操作,取名ktype可能就是出于这个考虑吧。

driver——描述驱动的数据结构

注意的是driver本身不是驱动,它只是描述驱动的结构。下面我们用英文driver代表描述驱动的数据结构,中文“驱动”或“设备驱动”代表真正控制设备的驱动程序逻辑。后续的“device-设备”、”bus-总线“等也基本满足这个规则。

另外需要了解,驱动程序逻辑并不一定要有driver这一套东西。驱动逻辑本质上就是内核空间的一系列的回调函数(熟悉内核的读者已经知道这些回调函数具体是文件系统的fops函数指针),实现方法不一而足。但还记得吗?我们的device driver model就是为了统一驱动开发设计的,所以要使自己开发的驱动符合规范,就尽可能使用device driver model,所以还是为自己的驱动加上driver这一套吧!

device——描述设备的数据结构

bus——描述总线的数据结构

仔细观察这些数据结构我们会发现,driver和device都有一个域指向bus,bus中也有两个链表保存所有与之相关的driver和device。所以它们三者关系是这样的: 每个device都隶属于一个bus,表示连接在这个bus上的设备。每个driver也隶属于一个bus,表示它是可以驱动这个bus上某一设备的驱动。bus保存着这些device和driver,这样就可以在插入新driver模块时匹配那些没有被驱动的device,也可以在加入新device时匹配相应的driver。这里的匹配就是遍历这两个链表。具体过程见下文。

class——描述应用层视图的数据结构

设备文件

让我们离开水深火热的内核来到应用层:我们作为用户在使用设备时并没有考虑到上文中描述的种种,而是通过“设备文件”这一简单接口轻松使用各类设备。在linux设计理念中,“一切皆文件”。设备也往往被抽象成一种特殊文件——“设备文件”。关于这个理念的理解,请参考拙作设备驱动 in a nutshell

在这里,我们解释设备文件是如何关联上对应设备的。首先介绍系统调用mknod:

mknod -- make device special file

mknod应用程序的mannul中如是说。它的功能就是用来创建设备文件——再详细一些,就是在目标文件系统树中创建一个表示设备文件的节点。一般我们将设备文件保存在/dev/目录下,但在其它目录中创建设备文件也是允许的。作为应用程序,或是系统调用,mknod的主要参数有两个:主设备号和次设备号。例如我们可以在当前目录创建设备文件freeman:

> mknod freeman c 12 21
> ls
> crw-r--r--  1 root     wheel   12,  21  8 22 15:36 freeman

这就创建了一个主设备号为12,次设备号21的字符设备(’c’表示字符设备)。我们可以尝试对该设备文件进行读写,发现操作总是不成功,提示“没有写权限”或是“设备尚未配置”——当然,我们的设备文件目前只是一个空壳,还没有关联上正真的设备。如何将两者关联上呢?

一个设备的设备号分为两类:主设备号和次设备号。前者表示设备的型号:如果两台设备型号相同,那么它们就有相同的主设备号,相同的设备当然可以使用同一驱动。那么如何分清两台型号相同的设备呢?这就需要用到次设备号。

既然驱动是和主设备号一一对应的,那么内核就可以根据mknod创建设备文件中的主设备号关联上设备驱动程序,从而关联上设备。具体过程如下:同操作普通文件一样,操作设备文件前需要open设备文件。这个open运行过程中了解到需要打开的文件是设备文件,那么它会去拿着设备号去搜索驱动。如果有幸对应驱动曾经被注册过,那么搜索过程必将成功结束。一旦找到对应驱动,就会将设备文件的fops赋值为驱动提供的fops。这个fops是个文件操作集合(file operations)的缩写,其中定义了包括read、write在内的各种文件操作在内核中的实现。例如我们读设备文件,最终就会调用对应驱动程序中定义的read操作——设备文件知道了自己该如何处理各种文件操作。这样从用户角度看,通过操作设备文件就可以为所欲为地操作底层设备了。

从2.6内核之后,设备节点就可以通过udev-uevent机制自动创建了。原理实际上并没有什么不同:原来需要手动输入的主次设备号现在由sysfs自动提供,通过class_create和device_create函数往sys文件系统中添加设备,udev检测到/sys目录的变动会根据变化在/dev目录下创建对应的设备节点[2]。

Put Them Together

这一节我们尝试回答一个问题:从设备接入计算机到一切就绪,中间到底发生了什么?在回答这个问题之前,我们先来看一看LDD3的问题。LDD3是专门讲解Linux设备驱动程序设计的经典书籍,LDD3的读者应该还记得书中的第一个例子——简单字符设备scull(代码在此)。仔细研究scull的代码我们发现上文中提到的知识几乎一个也没体现——scull注册了一个cdev结构就结束了!(可能有人怀疑cdev会嵌入一个device结构体,事实上并没有!)这正是linux设计的巧妙之处:如果一个驱动开发者想做快速开发,他完全可以使用一组上层API来完成工作而不用清楚了解底下到底发生了什么。cdev就是一个高层次的封装,可以认为它是VFS(Virtual Filesystem)层面的结构,并不是device driver model的组件。所以从scull中,以及LDD3前面的其它几个例子中,看不到device driver model的影子,读者也不必惊慌,LDD3在后面有整整一章介绍了device driver model。

LDD3的问题不仅在于层次高,还在于它使用了“虚拟设备”做例子。LDD3为了不让准备实验材料的过程给读者造成障碍,并没有以实际硬件设备为例,而是选择把一段内存虚拟成设备进行操作(感谢LDD3的良苦用心)。这个方法在大部分情况下工作得非常好,但是在体会设备模型时却成了阻碍。我们分析阻碍的成因:用内存虚拟出的设备和真实设备的区别在于,内存是一直存在于系统中,没有一个“插在电脑上”的过程。所以LDD3例子的逻辑就是:

模块初始化中初始化设备

而知道真相后,我们看到真实的故事远比这个复杂:

模块初始化中注册driver
新driver加入系统后,bus开始为之匹配它可以驱动的device (bus的match操作)
新device加入到系统后,bus开始为之匹配driver (bus的match操作)
driver开始probe device,probe被认为是驱动程序的入口,会完成初始化设备的任务

我们看到,不管driver还是device加入系统,都会引起相关bus的match操作。这样可以保证不论driver先加入系统还是device先加入系统,device driver model都能很好地工作。如果device先加入系统,那就像买了一台超级复杂的机器(device),翻箱倒柜找不到配套的说明书(driver),需要等对方把说明书邮过来。如果XXX的说明书先到,那就把家里的机器都翻出来对一遍,发现没有名叫XXX的机器,那么就等着京东把XXX机器发过来。

新driver加入到系统,一般是在用户手动加载驱动模块时(也可以由udev自动加载,请看下文),在模块初始化代码中完成的。新device什么时候加入呢?一种情况是开机初始化系统时bus枚举设备时发生的。除此之外,系统运行过程中也可以加入device:热插拔。不是所有bus都无条件地支持热插拔。常用的支持热插拔 bus有著名的USB。USB总线可以侦测新插入的设备,并由USB子系统核心向系统注册对应新的device。如果没有合适的driver去匹配这个新device,系统会借助uevent机制结合应用空间的udev组件,去加载驱动模块(有兴趣可以翻阅一下拙作:《Linux热插拔机制的介绍和应用》,该文介绍了udev的应用)。小结一下:udev在device driver model中至少做两件事——自动创建设备文件节点和自动按需加载驱动模块。

不是说“虚拟设备”就不能体现device driver model,下面是github上的一个例子sparrow——“麻雀虽小,五脏具全”。它的device注册过程硬编码在一个模块初始化代码中,手动加载模块就可以注册device(可以认为是模拟热插拔)。

https://github.com/wowotech/sparrow

这一节的剩余部分解释一个新的问题:上述的这些动作(“注册”、“匹配”等)是谁来完成的?一个简单的回答是:总线。因为设备被连接在总线上,所以总线来完成这个任务逻辑上理所应当。

总线也可以被当作设备进行注册。和一般设备不同,它还有一些特殊的属性要注册,包括:match和event。这是与总线相关的driver或device被注册时需要调用的动作。为了方便和自己相关的device和driver结构的注册,总线一般会提供一些自己的注册函数封装通用的device_register/driver_register。下面是platform总线提供的两个注册函数:

platform_device_register
     |-device_initialize(&pdev->dev);
     |     |-kobject_init
     |-platform_device_add(pdev)
          |-device_add
               |-kobject_add
               |-device_create_file
               |-device_create_file(uevent)
               |-device_add_class_symlinks
               |-device_add_attrs
               |-bus_add_device
                    |-Add device's bus attributes
                    |-Create links to device's bus
                    |-Add the device to its bus's list of devices
               |-device_create_file(dev)
               |-device_create_sys_dev_entry
               |-kobject_uevent
               |-bus_probe_device - probe drivers for a new device   <—— 看这里
               |-有关class的处理

platform_driver_register(xxx_drv)
     |-drv->driver.bus/probe/remove…
     |-bus_add_driver
               |-loop to search and bind driver to devices     <——这里
                        |-driver_match_device                  <——这里
                                |-bus->match                   <——这里
                        |-driver_probe_device                  <——这里
                                |-driver->probe                <——这里
                |-module stuff
                |-driver_create_file(uevent)
                |-driver_add_groups
                |-add_bind_files
       |-driver_add_groups
       |-kobject_uevent

可以看到注册过程中均有上文中描述的“匹配”操作发生,以及uevent对应用层udev的通知。

参考

[1] 蜗窝科技. Linux设备模型(1)_基本概念. http://www.wowotech.net/device_model/13.html

[2] 在驱动模块初始化函数中实现设备节点的自动创建. http://blog.csdn.net/zhenwenxian/article/details/5424434