其实三者的关系很好区分,因为我自己在学的时候,学完基本的设备驱动后,接着学了设备模型,又学驱动子系统,结果就把他们搞混了,一直稀里糊涂的,后来某一天者恍然大悟,于是乎把自己的认识写下来。
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_device
和 platform_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 驱动可以支持热插拔的原因。