设备驱动 - in a nutshell



“一切皆文件”

熟悉unix的人都知道有这么一种思想:一切皆文件。设备也被抽象成文件,并有个专有名称叫“设备文件”。计算机设备种类数也数不清,每个设备都需要在操作系统中定义自己的接口,这样整个系统就混乱的不成样子了:

“我是硬盘,我能接收指令,也能发送数据,给我留两个系统调用hard_disk_cmd和hard_disk_data”
“我是串口,我能发送和接收数据,也给我留两个serial_send和serial_recv”
“我是一个灯泡,我能接受亮度调整的指令,给我留个light_adjust”
“我是个咖啡机,我能接受命令make_coffee,你可以使用ready_coffee查询咖啡是不是做好了”
……

人们想到将设备的差异封装在底层,向上层提供一个统一的接口。什么接口比较好使呢?文件系统!文件系统提供的操作十分丰富,文件读和写和IO设备的输入输出刚好对应,一些非IO的控制类操作,文件系统也提供ioctl操作与之对应。特别是linux系统提供的VFS(Virtual File System,虚拟文件系统)这一抽象,使得一切皆文件的实现更加优雅。

“嘿硬盘,当用户朝你对应的设备文件写数据的时候,你就自己把这些数据写到你的介质上吧”
“嘿串口,你也是,当用户朝你对应的设备文件读/写数据的时候,你也自己把缓冲里的内容递给系统/发出去”
“灯泡,如果用户往你对应的设备文件里写了一个值,你就自己根据这个值调整亮度吧”
“咖啡机,如果你看见我在你对应的设备文件里写了一个make,那你就开始做咖啡吧。做好了把文件内容改成ready。”

linux的这个框架似乎非常完美!用户不用知道设备的详细信息,就可以为所欲为地操作自己的设备。因为在用户看来,所有插在计算机上的设备都是桌面上的一个文件。可以用记事本打开查看里面的值以获取设备目前的状态。也可以往文件里写点东西,就能改变设备的状态。对设备的编程也很容易,比如我们写一个定时煮咖啡的程序(假设咖啡机的设备文件为/dev/coffee_maker):

 #!/bin/bash
sleep(3600);
echomake” > coffee_maker

好了,我们编写了一个三行的咖啡机程序,它会在一个小时以后自动煮咖啡。感受到设备文件使用的方便了吗?当我们往咖啡机的设备文件coffee_maker里写”make”的时候,实际是调用了系统调用sys_write,sys_write当时是蒙圈的,它不知道自己写的文件是个咖啡机。还好sys_write不负责主要的事情,它只是跑到内核里求助VFS。VFS知道coffee_maker这个文件的底细——它不是一个普通文件,而是一个设备文件。这时,VFS就执行专属于这台咖啡机的动作——煮咖啡!那么问题来了,VFS怎么知道如何煮咖啡?

驱动的本质:一系列的回调函数



谁教会VFS煮咖啡的?答案是:驱动程序!我们都有经历,插上一个设备,但计算机似乎不知道怎么使用它。有人提醒我们:“你装驱动了吗?”

联系上面我们说的驱动程序,我们不难发现驱动的本质:驱动程序就是一系列的回调函数,插在操作系统上,等着VFS不知道拿设备怎么办时调用。

“这是一个USB接口的咖啡机”

紧接着又一个问题:系统如何为一个插入的设备匹配对应的驱动?你端着一杯正在发光的咖啡问出这个问题,因为你怀疑系统用控制灯泡的驱动去控制咖啡机。

生厂商号和设备号架起了设备和驱动的桥梁。先说设备这一边:每个生产商都被赋予了一个全球唯一的生产商号,如Intel生厂商号为8086。生厂商在自己内部,又会对自己生产的设备赋予一个设备号。一般来说,生厂商号和设备号就能唯一确定一种设备。设备通过总线连接在计算机上,例如我们平常使用的U盘,使用USB(Universal Serial Bus)总线。当一个设备插上总线后,会向系统主动报上自己的名号:生厂商号和设备号。

再说驱动这一边:驱动在安装进系统的时候,会向系统提交一张表,报告自己的工作范围:它能管理哪些硬件。表的内容是啥呢?聪明的你已经想到了,就是驱动能够管理的硬件的生厂商号和设备号。系统把这些表汇总起来,一旦有新硬件插进来,就去查查看看哪个驱动愿意管这个设备。一旦找到了并测试通过,驱动程序就开始初始化。初始化工作最重要的一步,就是创建上文中我们大谈特谈的设备文件。至此,设备文件、驱动、设备至上而下的通道已经贯通了,用户可以为所欲为了!

一点补充

用户态驱动

硬件资源一般是由操作系统保护起来的,只有在内核态(高特权级状态)下才能被访问,用户态的程序是没有办法直接访问的。驱动程序作为直接控制设备的软件,可想而知是工作在内核态。可是有些同学可能听说过一种叫做“用户态驱动”的东西,这是个啥呢?

为什么要使用用户态驱动?首先用户态态编程容易呀:编程环境我们都熟悉,又有丰富的库,还可以使用自己喜欢的语言进行编写。另外,据说用户态驱动因为没有用户态-内核态切换的开销,有时候效率会比内核态驱动高一些。

用户态驱动是怎么实现的呢?对硬件的直接访问一般是不能再用户态进行的(在Intel x86体系下,我曾经听说过有个叫IOPL的标志位,可以控制IO端口是不是可以被用户态程序直接访问)。所以,应该是有一段功能简单的驱动把基本的操作通过设备文件暴露给用户态,然后再在用户态进行编程,扩展它的功能并提供更高层次的功能抽象。用户态驱动最终表现形式是一个软件库,供其它程序调用,或者是以服务器的形式,与应用程序进行交互,如X server。而内核态的驱动,往往作为内核的一个模块。

动态加载

之前的文章《Linux热插拔机制的介绍和应用》