Baurine's Blog

Linux 中基本设备驱动、驱动子系统和设备模型的关系

June 02, 2013

其实三者的关系很好区分,因为我自己在学的时候,学完基本的设备驱动后,接着学了设备模型,又学驱动子系统,结果就把他们搞混了,一直稀里糊涂的,后来某一天者恍然大悟,于是乎把自己的认识写下来。

Linux 的设备分为三大类,字符设备,块设备和网络设备。这里以字符设备为例。在不借助设备模型和驱动子系统的情况下,写一个最基本的字符设备的驱动,主要是实现 cdev 结构体,这个结构体里最重要的是 file_operations 结构体,这个结构体是一个函数集,包括 llseek,read,write,ioctl 等需要驱动实现的函数。驱动还需要自己去申请设备号,最后通过 cdev_add() 函数将 cdev 结构体注册到 VFS 中。

整体框架是这样的:

      [VFS]
------------------
   [字符设备驱动]   ==> 自己实现 file_operations,
                      device_create,cdev_add...
------------------
      字符设备     ==> 硬件

键盘、鼠标、按钮都是典型的字符设备,利用上面的这种方法写出来的驱动,会发现,它们 90% (我猜测的数据) 的代码是相同的。于是内核开发者们把这些相同的代码抽取出来,又抽象出一层,称为驱动子系统,键盘、鼠标这些输入设备的驱动抽象出来的就是 input 子系统。Linux 系统中的驱动子系统有很多种,字符设备抽象出来的的驱动子系统除了 input 子系统,还有 framebuffer 子系统,sound 子系统,v2l (video for linux) 子系统,其它的还有网络子系统,mtd 子系统。

驱动子系统帮忙实现了同类设备的相同操作的大量代码,驱动本身则只用实现少量的差异性操作。对于 input 驱动来说,input 驱动子系统帮忙实现了 file_operations,申请设备号,创建设备,cdev_add 等操作,而 input 驱动只需实现去接收输入,然后向上层报告输入事件和输入的数据即可,节省的代码不是一丁点。

驱动子系统会把需要驱动需要实现的差异性操作声明为一个结构体,然后让驱动去实现,驱动实现后再注册到相应的驱动子系统中,比如 input 子系统需要驱动实现的的结构体就是 input_dev,驱动实现后 (其实就是填充结构体的各个成员嘛),通过 input_register_device() 注册到 input 子系统中,同时,驱动通过子系统提供的 input_event 结构体向上层报告输入事件和数据。

使用了 input 子系统的整体框架:

      [VFS]
-------------------
   [input 子系统]   ==> 内核提供,实现 file_operations,
                       device_create,cdev_add...
-------------------
 [键盘驱动][鼠标驱动] ==> 自己实现,只需实现获取输入,向上层报告事件
-------------------
   键盘     鼠标   ==> 硬件

而至于设备模型,一般说它们是驱动和设备的外衣。我这里只说 platform_deviceplatform_driver。我的理解是,在没有设备模型的情况下,设备的一些硬件参数,比如寄存器地址,中断号,都是直接写在驱动中的,耦合性比较大,一旦这些参数变了,整个驱动就要重编。设备模型首先把这部分内容抽取出来,放到了一个独立的结构体中,比如 platform_device,用来描述一个设备,我觉得它相当于一个配置文件。驱动通过 platform_get_resource()platform_device 中获取寄存器地址或中断号。如果参数改变,则只需修改 platform_device 即可,驱动本身无需任何修改。

platform_driver,最大的好处是可以支持设备的热插拔。如果不使用设备模型,驱动的入口函数 (实际是个宏) module_init 要创建设备,申请和分配资源。只要驱动一加载,即使设备不存在,这些操作也会马上执行。如果使用设备模型,则驱动的入口函数 module_init 中几乎不用做什么事,只需调用 platform_driver_register() 去注册一个 platform_driver。原来的创建设备,申请和分配资源等操作都挪到了 platform_driver 结构体的 probe 函数中。但是,这个 probe 函数不像原来的 module_init 那样会立即执行。只有当对应的设备存在时或后来插入时,probe 函数才会真正地被调用,实现了一种动态的功能。

明白了这一点后,我也突然明白了,Windows 驱动中,从 NT 驱动和 WDM 驱动,最大的变化也是如此。NT 驱动不支持热插拔,在 NT 驱动中,设备的创建在直接在驱动入口函数 DriverEntry 中进行的,而在 WDM 驱动,设备的创建等操作被移动了一个专门的例程 AddDevice 中了,相当于前面所说的 probe 函数,这样的话,AddDevice 例程就可以在检测到设备时再被动态调用。所以这就是为什么 WDM 驱动可以支持热插拔的原因。


Comments