Linux内核设备驱动模型
设备驱动模型概述
早期linux内核为设备驱动开发者提供了很少的基本功能接口:申请动态内存,保留I/O地址范围等。早期的硬件设备编程困难,即使属于同一总线上的两个不同硬件设备之间也很少有共性。因此,需要为设备驱动开发者设计一个通用的模型。现如今,情况不一样了。总线类型规范了硬件设备的内部设计,导致如今的即使是不同类型的设备都支持相似的功能,针对这样的设备的设备驱动应该关心以下实现:电源管理,即插即用和热插拔。系统中每一个硬件设备的电源都由内核来管理。比如一个电池供电的电脑进入“standby”模式,内核必须强制每一个硬件设备为低电状态。因此,每一个可以设定为”standby“状态的设备的驱动必须包含一个可以将硬件设备设定为低电状态的回调函数。同时硬件设备也要按照精确的顺序设定为”standby“状态,否则有些硬件设备可能进入错误的电源状态,例如,linux内核必须先将硬盘设定为”standby“状态,然后才是硬盘控制器,如果该顺序反了,硬盘控制器将不能给硬盘发送命令。为了实现该类的操作,Linux2.6提供了一些数据结构和函数接口,这些数据结构和接口为系统中所有的总线,设备和设备驱动提供了统一的操作方法。这个框架就叫做设备驱动模型。
核心数据结构Kobject
kobject是设备驱动模型的核心数据结构。它被固有的绑定在sysfs文件系统中:每一个kobject相当于sysfs中的一个目录。kobject通常被嵌入到更大的对象中用来描述设备驱动模型的组件,因此这样的对象称作”容器“,总线,设备和设备驱动都是典型的容器。在容器中嵌入一个kobject使得内核能够:
- 为容器维护一个引用计数
- 为容器维护一个层级结构列表或集合
- 为容器的属性提供一个用户模式下的视图
Kobject在内核中的定义如下:
1 | struct kobject { |
- name属性定义了该kobject的名字。
- entry是用来将kobject链入kset中的链表元素。
- parent是指向其父节点的指针,用来支持层级结构。一般指向其kset容器中的kobj对象。
- ktype是该对象的类型,即包含该kobject的容器的类型。ktype在内核中的定义如下:
1 | struct kobj_type { |
其中,release回调函数在释放kobject本身的时候被调用。sysfs_ops是sysfs的操作函数表,default_attrs是该sysfs的默认属性。
sd属性表示该kobject对象的sysfs层级别构的构建块。
kref是该对象的引用计数,也就是包含该kobject的容器的引用计数。通过kobject_get和kobject_put函数增加和减少该计数器的值,当kref计数器的值为0时,调用kobj_type结构中的release回调函数,该函数通常实现为用来释放该kobject所属的容器(仅当容器本身为动态创建时)。
kset是该kobject的组,即kset是同一类型kobject的集合(也可以不属于同一类型)。同时,kset本身又包含自己的kobject对象。kset具有以下功能:
- kset是一组kobject对象的容器,内核可以通过kset来跟踪设备和设备驱动
- kset也是sysfs中的一个子目录,该目录显示了所有关联到的kobject。每个kset都包含一个kobject对象,该对象可以被设置为其它关联到的kobject的父节点。sysfs层级结构的顶层目录就是这样设计的。
- kset支持kobject的热插拔,并且影响uevent事件向用户空间报告的方式。
kset结构在内核中的定义如下:
1
2
3
4
5
6struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
const struct kset_uevent_ops *uevent_ops;
};list是该集合中kobject的链表头。list_lock是kset中kobject链表头操作的保护自旋锁。kobj是kset对象本身的kobject元素。uevent_ops是该kset的uevent操作函数表,kset_uevent_ops在内核中的定义如下:
1
2
3
4
5
6struct kset_uevent_ops {
int (* const filter)(struct kset *kset, struct kobject *kobj);
const char *(* const name)(struct kset *kset, struct kobject *kobj);
int (* const uevent)(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env);
};其中,filter函数用来决定某个uevent是否被发送到特定kobject的用户空间,该函数返回0表示uevent不会被发送。name函数用来覆盖uevent发送到用户空间的kset的默认名称。uevent函数在uevent要发送到用户空间以运行更多的环境变量添加到uevent时调用。
总结:kobject通常作为一个对象直接嵌入到其它类型的数据结构中,嵌入kobject的对象称作容器,ktype表示kobject的类型,也表示容器的类型,ktype提供了该类kobject的release方法和sysfs操作方法。kset是kobject的容器,kobject通过list_head链表元素链接在kset的list头节点上,一般来说一个kset中的kobject是同一类型的,即共享同一个ktype。注册到设备驱动模型中的所有kobject和kset对象的层级关系都在sysfs中呈现。
kobject和kset的初始化和添加接口:
1 | /*kobject初始化函数*/ |
kobject和kset使用实例,参考linux内核源码:samples/kobject/kset-example.c和samples/kobject/kobject-example.c
sysfs文件系统
sysfs是一个给用户层提供设备驱动模型中所有对象的层级关系视图的文件系统,它被挂载的路径是/sys/目录,存在于/sys/目录下的每一个子目录都是一个kset对象(这里说的对象是kobject的容器)或者kobject关联到sysfs的目录。/sys/目录下存在以下目录(kset对象/kobject):
1 | block class devices fs module |
这些目录有的是通过kset_create_and_add接口注册到sysfs上的kset对象,比如class,devices,module,bus,以bus目录为例,在内核中该目录的注册代码为:
1 | bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL); |
有的是通过kobject_create_and_add接口注册到sysfs上的kobject对象,比如fs,目录,其注册代码为:
1 | fs_kobj = kobject_create_and_add("fs", NULL); |
目录说明:
block(kobject):存放的是所有的块设备对象(kobject)的链接文件,这些块设备源文件在devices目录中。
class(kset):存放的是所有注册到设备驱动模型中的class对象,由于class是device容器,所以在每一个/sys/class目录的子目录中,存放的是该class包含的所以device对象的链接文件。
devices(kset):存放的是所有设备驱动模型中的device对象,device对象是层级结构的,每一个device对象关联的/sys/devices/中的目录中,每一个文件是该device对象的属性文件,每一个目录是该device子设备对象,即每一个device对象是所有其关联的sysfs目录的所有子目录对应的device对象的父设备对象。
fs(kobject):是文件系统基础对象,其子目录是具体文件系统的kset对象。每个文件系统的目录中存放的是其特有对象目录。
module(kset):存放的是所有有参数的module对象,这些对象一般包含一个parameters对象和一些属性文件。parameters目录中的每一个文件都表示模块的一个参数。
bus(kset):存放的是所有的总线的kset对象,每一个总线kset对象又包含一些属性文件和两个kset对象,分别是devices和drivers,分别表示注册打扫该总线上的设备和驱动列表。由于所有的设备都已经在/sys/devices目录中了,所以devices对象中的子设备对象都是相应的/sys/devices中的对象的链接。
dev(kobject):该目录为一个kobject,里面包含两个kobject对象,一个名为char,一个名为block。char目录中存放的是以【主设备号:此设备号】命名的字符设备的对象的链接目录文件,和char对象类似,block对象目录存放以主次设备号命名的块设备的对象的链接文件,它们都指向了/sys/devices中的设备对象。
firmware(kobject):存放固件相关的对象,比如设备树对象和设备树属性文件等。
kernel(kobject):存放内核中可调整参数属性文件和kobject对象或kset对象。
power(kobject):power对象存放的是电源管理子系统相关属性文件。
设备驱动模型组件
设备驱动模型由一些表示总线,设备和设备驱动等的基础数据结构组成。
Devices
设备驱动模型中的每一个设备用一个device对象表示,设备通常用来描述硬件信息。device对象在内核中的定义如下:
1 | struct device { |
device关联的sysfs的目录是/sys/devices。device也是层级结构的,device的parent成员指向其父亲节点,同时,该device又可能是其它节点的父节点。kobj成员是该对象的kobject对象,主要用来维护其计数器,通过get_device和put_device接口增加和减少设备计数。bus成员是该device所属总线的指针。driver成员指针指向该device关联的驱动结构。class指针指向该device所属的class。devres_head成员是其资源链表头,用来管理自己的资源。release函数在释放该device时调用。type指针指向一个device_type结构类型,用来描述该device的类型。该结构主要实现了一组操作函数,即通过操作分类。of_node成员是该设备的设备树节点。p成员指向一个device_private结构,用来描述该设备的驱动数据的核心部分信息。该结构在内核中的定义如下:
1 | struct device_private { |
其中klist_children是该device的说有子节点的头节点。knode_parent是同级别列表中的节点。knode_driver是驱动列表中的节点。knode_bus表示其在bus的设备链表中的节点。deferred_probe是链入deferred_probe_list的链表元素,deferred_probe_list主要用来做因为某些资源不可得或依赖其它驱动先probe等原因需要延迟的probe操作。drvier_data是驱动特殊数据。device指向该device_private自身所属的device。
device注册接口为device_register,取消注册接口为device_unreigster。
device的注册分两个步骤,首先是调用device_initialize初始化device的kobj成员以及其它一些固有成员,接着调用device_add接口添加device。device_add函数主要依次完成了以下 操作:
申请和初始化其device_private结构成员:
1 | if (!dev->p) { |
调用kobject_add添加kobject:
1 | error = kobject_add(&dev->kobj, dev->kobj.parent, NULL); |
kobject_add同时会在sys目录下或者sys目录的子目录下(取决于parent)创建其目录。
调用device_create_file在sys目录中对应的目录下创建uevent文件。
1 | error = device_create_file(dev, &uevent_attr); |
如果该设备有设备号,则在其关联的目录下创建dev文件,用来获取该设备的主次设备号,并且在/sys/class目录下(如果设备有class对象)或者/sys/dev/目录下为其创建链接,并在devtmpfs中创建设备节点。
1 | if (MAJOR(dev->devt)) { |
由上面的代码片段可知,在系统中不但可以使用mdev通过扫描/sys/class来在/dev/下创建设备节点,也可以通过挂载devtmpfs到/dev/目录下实现设备节点可见。在系统中可以通过下面的命令挂载devtmpfs:
1 | mount -t devtmpfs none /dev |
接着通过device_add_class_symlinks接口在/sys/class/xxx下创建符号链接文件。(只有关联到某个已经创建的class的设备才会在class关联到的/sys/class/目录下创建链接文件,在该接口中如果判断dev->class为空立即返回)
1 | error = device_add_class_symlinks(dev); |
接着调用device_add_attrs在/sys/devices下相关的目录中创建其属性文件。
1 | error = device_add_attrs(dev); |
接着调用bus_add_device将该dev添加到dev所属bus的设备链表上。同时,为该dev在/sys/bus/目录中其相关bus中创建符号链接文件,并且在该dev对应的sys/devices/目录中创建名为subsystem的符号链接目录,subsystem是来自该设备所属总线的subsys中的kset成员,该过程见Buses章节中向总线添加设备一节的描述:
1 | error = bus_add_device(dev); |
接着调用dpm_sysfs_add接口在sys/devices下该设备的目录中添加power目录以及在power目录中添加一些属性文件。
1 | error = dpm_sysfs_add(dev); |
接着向总线通知链发送总线添加设备通知,在向用户空间发送kobj添加事件。
1 | /* Notify clients of device addition. This call must come |
接着调用bus_probe_device,在该函数中判断总线是否设置了drivers_autoprobe,如果设置了,则调用device_attach函数来和对应的驱动进行绑定,device_attach实现过程见Buses章节的设备和驱动的绑定一节。绑定驱动之后再调用所有注册到总线的subsys_interface的add_dev回调函数。
1 | bus_probe_device(dev); |
然后如果该设备有父设备,在将该设备链接到父设备的子设备链表中。
1 | if (parent) |
最后判断如果该设备属于某个class,则将该设备添加到其所属class的设备链表上,并将该添加事件通知到class上的所有interface 上。
1 | if (dev->class) { |
至此,device向内核设备驱动模型的添加结束。主要建立了设备和sysfs的关联,和驱动的绑定,和总线的关联以及和devtmpfs的关联等。device的取消注册接口device_unreigster的过程和注册接口的相反,依次取消关联和删除设备。
Drivers
在设备驱动模型中,驱动通常提供设备的操作方法,即设备能力的代码实现。驱动在内核中用device_driver结构体来描述:
1 | struct device_driver { |
其中,bus成员指向该驱动所属的总线,of_match_table成员主要用来和设备中的of_node节点做匹配。其中的回调函数主要用来实现热插拔,即插即用,以及电源管理相关的功能。probe函数在总线通过设备匹配到驱动或通过驱动匹配到设备的时候调用。remove函数在关联的设备移除的时候或者驱动本身退出的时候调用以取消关联。shutdown函数在关机的时候调用以停止关联的设备。suspend用来使设备进入睡眠模式,低电模式。resume函数用来将设备从睡眠模式唤醒。pm指针成员指向匹配到的设备的电源管理操作集。p指针指向该驱动核心的私有数据,该成员的类型定义为:
1 | struct driver_private { |
其中,kobj是该driver的kobject对象,主要用来维护所属驱动的使用计数,通过get_driver和put_driver函数来增加或减少计数。klist_devices是该driver关联的设备的链表头,一般在总线匹配到设备的时候将设备加如该链表。knode_bus是driver加入总线上驱动链表的链表元素。mkobj指向驱动所属module_kobject,module_kobject用来关联/sys/module中的对象。driver指针指向该driver_pribate结构所属的device_driver对象。
和device对象一样,device_driver对象通常静态嵌入在更大的容器对象中,比如platform_driver对象的driver成员就是一个device_driver结构。
device_driver的注册接口是driver_register,该接口用来向内核设备驱动模型中添加一个device_driver对象,并为其在sysfs中创建关联。
driver_register函数分析:
driver_register中首先通过driver_find函数在总线上根据其name属性查找该driver,如果找到则返回,不再添加,防止其重复添加。
1 | other = driver_find(drv->name, drv->bus); |
接着调用bus_add_driver接口将device_driver对象添加到其所属的总线上,为该device_driver绑定设备,在sysfs中创建相关对象,以及创建其与总线和设备的关联。bus_add_driver详细过程参加Buses章节的向总线添加驱动一节。
1 | ret = bus_add_driver(drv); |
最后在driver对应的sysfs目录中添加组目录,在组目录中添加组文件。至此,整个driver_register过程结束。
1 | ret = driver_add_groups(drv, drv->groups); |
driver_unregister接口和driver_register接口相反,首先driver目录中删除groups文件,然后调用bus_remove_driver接口从总线上删除driver相关文件。
1 | void driver_unregister(struct device_driver *drv) |
Buses
总线是处理器和设备之间的通道,在设备驱动模型里面,所有的设备都通过一个总线连接在一起,总线包括内部总线和虚拟总线。
在设备驱动模型中,kset是kobject的集合,subsystem是kset的集合,每一个总线都是一个subsystem,关联的susfs目录/sys/bus/是基础对象,所有总线(subsystem)被注册到该对象上。每一个总线subsystem中包含两个kset对象,它们关联的sysfs总线上目录名分别为devices和drivers。drivers包含了添加到该总线上的所有的device_drivers对象(kobject),devices目录包含了所有添加到该总线上的device对象(kobject),由于所有device对象已经注册在devices目录中,所以在总线subsystem的devices(kset)中的device对象都是其对应的/sys/devices中的链接文件。
在设备驱动模型中,当一个驱动要注册时,会调用到总线的驱动添加接口将驱动添加到总线,像总线添加驱动的时候会根据总线设定的规则匹配可以被该驱动处理的所有设备,并与之建立关联。当一个设备注册的时候也会调用总线添加设备的接口将设备添加到总线上,和总线创建关联,同时会在总线上遍历驱动,根据总线的匹配规则匹配到自己的驱动,并和驱动创建关联。
内核中总线描述符定义如下:
1 | struct bus_type { |
name:指向该总线的名称。
dev_name:在子系统中枚举设备是使用的设备名指针。
dev_root:默认的父设备。
bus_attrs:默认的总线属性,在sysfs中以文件形式呈现。
dev_attrs:默认的设备属性,在sysfs中以文件形式呈现。
drv_attrs:默认的驱动属性,在sysfs中以文件形式呈现。
match:驱动匹配回调函数,当一个设备或驱动向总线注册时被调用,如果匹配成功返回非零值。
uevent:当设备添加,移除或其它生成uevents添加环境变量事件时调用。
probe:当一个设备或驱动添加到该总线上时,如果设备和驱动匹配成功,就会调用该回调函数,一般用来初始化设备。
remove:当一个设备从该总线上移除时调用,以释放设备相关的资源。
shutdown:在系统关机的时候调用以停止设备。
suspend:当总线上的设备要进入睡眠模式的时候调用。
resume:当要唤醒睡眠中的设备时调用。
pm:表示该总线上的电源管理的操作函数集。
iommu_ops:该总线上IOMMU特定测操作集合,用来关联某个总线上的IOMMU驱动,运行总线做一些特殊的设定操作。
p:subsys_private类型的总线子系统私有数据。包含总线的kset,以及设备和驱动链表等。
subsys_private类型在内核中的定义如下:
1 | struct subsys_private { |
subsys:定义该子系统的kset。
devices_kset:该总线子系统的devices目录
interfaces:总线子系统上的interfaces链表。
mutex:用来保护devices和interfaces链表的互斥锁。
drivers_kset:该总线子系统的drivers目录。
klist_devices:devices目录中的设备的链表。
klist_drivers:drivers目录中的驱动的链表。
bus_notifier:总线上通知链头。
drivers_autoprobe:驱动和是否可以动态匹配的属性标记。
bus:指向该subsys_private结构所属的总线对象。
glue_dirs:放置在父设备直接的glue目录,用来避免命名空间冲突。
class:指向该结构关联的class对象。
总线的注册
总线注册接口为bus_register,是一个宏函数,实际调用的接口是__bus_register函数。
1 |
|
在__bus_register函数中首先为要注册的总线对象申请了subsys_private对象,并初始化了总线的通知链头:
1 | priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL); |
接着初始化总线p成员的subsys(kset),并通过kset_register注册该kset对象,即在/sys/bus下创建了以该总线的名字为名称的目录。
1 | retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name); |
接着在刚创建的sysfs关联的目录下创建了uevent属性文件,同时在该目录添加了devices和drivers两个kset对象,即在该目录创建了devices和drivers两个目录。uevent文件用来控制内核向用户空间发送uevent事件,对应的向该文件写入的字符串为:“add”,“remove”,“change”,“move”, “online”, “offline”。
1 | retval = bus_create_file(bus, &bus_attr_uevent); |
接着初始化了总线p成员的interfaces链表头,mutex互斥锁,klist_devices设备链表以及klist_drivers驱动链表。
1 | INIT_LIST_HEAD(&priv->interfaces); |
最后先向总线关联的sysfs目录中添加了drivers_probe和drivers_autoprobe属性文件,然后添加了其它总线属性文件。drivers_autoprobe属性表示设备或驱动添加的时候是否匹配并调用驱动probe函数(创建关联),该属性值为非零表示自动创建关联。drivers_probe文件权限为只写,向该文件写入设备名称,则内核开始在总线上找到该设备,并为其和总线上的驱动创建关联。
1 | retval = add_probe_files(bus); |
至此,完成了整个总线基础部分的注册以及其和sysfs的关联。
向总线添加设备(device)
向总线添加设备的接口为bus_add_device函数,该函数首先从设备中获取到设备设定的所属总线:
1 | struct bus_type *bus = bus_get(dev->bus); |
接着向设备中添加总线上设定的设备属性文件,即在设备关联的sysfs目录中添加总线上设定的设备属性文件:
1 | error = device_add_attrs(bus, dev); |
再接着在总线的devices目录中为设备所属sysfs目录创建链接文件,同时在设备所属sysfs目录中创建名为subsystem的链接文件,subsystem链接文件指向总线在sysfs中所属的目录。
1 | error = sysfs_create_link(&bus->p->devices_kset->kobj, |
最后通过设备的p成员的knode_bus(klist链表成员)将该设备添加到总线的klist_devices链表上。
1 | klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices); |
至此完成设备和总线的关联以及设备所属sysfs中的目录和总线所属sysfs中的目录的关联。
向总线添加驱动(device_driver)
向总线添加驱动的接口为bus_add_driver函数,该函数先从要添加驱动中获得驱动设定的总线:
1 | bus = bus_get(drv->bus); |
接着为该驱动申请driver_private对象,并初始化其设备链表头,初始化其kset为总线的驱动kset对象,即要注册的驱动对象在sysfs下的父目录是sysfs中总线目录的drivers目录(/sys/bus/xxx/drivers)然后在该目录中创建该驱动对象。
1 | priv = kzalloc(sizeof(*priv), GFP_KERNEL); |
接着判断总线的drivers_autoprobe属性,如果该属性值不为零,则调用driver_attach函数将驱动和匹配到的设备绑定,绑定成功后将该驱动添加到总线的驱动链表上。同时将该驱动和module_kset创建关联,即在该驱动关联的sysfs的目录中创建名为module的链接文件,指向该驱动所属module在/sys/module目录中的对象,并在module所属的sysfs目录中创建drivers目录,同时在该module目录的drivers目录中创建正在添加的device_driver对象的链接文件:
1 | if (drv->bus->p->drivers_autoprobe) { |
接着在该device_driver所属的sysfs目录中添加uevent文件,添加总线上指定的属性文件,如果没有设置该驱动的suppress_bind_attrs标记,则同时在该目录中添加bind和ubind文件。uevent属性文件用来控制内核向用户空间发送kobject事件。bind和ubind属性文件用来控制内核让驱动绑定或解除绑定写入bind或ubind文件的设备名表示的设备:
1 | error = driver_create_file(drv, &driver_attr_uevent); |
最后调用kobject_uevent函数向用户空间发送kobject的添加事件,完成驱动向总线的添加过程:
1 | kobject_uevent(&priv->kobj, KOBJ_ADD); |
设备和驱动的绑定
device和device_driver在注册过程中通过driver_attach和device_attach函数来互相创建绑定关系,首先看driver_attach函数(驱动绑定设备),该函数遍历总线上的所有设备,为所有设备指向__driver_attach函数:
1 | int driver_attach(struct device_driver *drv) |
在__driver_attach函数中首先调用driver_match_device函数为驱动匹配设备,其实就是调用总线的match回调函数,如果匹配成功则调用driver_probe_device函数为设备和驱动建立绑定关系:
1 | if (!driver_match_device(drv, dev)) |
再看device_attach函数(设备绑定驱动),该函数先是判断是否该设备已经关联了驱动,如果关联的驱动则判断是否已经和该关联驱动建立绑定关系,如果没有则调用device_bind_driver函数和该关联的驱动建立绑定关系。如果没有关联任何驱动,则遍历设备所属总线上的所有驱动,为其执行__device_attach函数:
1 | if (dev->driver) { |
再__device_attach函数中,同样首先通过driver_match_device函数调用总线上的match函数和驱动做匹配,如果匹配成功则调用driver_probe_device函数绑定设备和驱动:
1 | static int __device_attach(struct device_driver *drv, void *data) |
设备和驱动都最终通过driver_probe_device函数来和彼此建立绑定关系,下面看看该函数的实现。
在driver_probe_device函数中,通过调用really_probe函数使设备和驱动建立绑定:
1 | ret = really_probe(dev, drv); |
在really_probe函数中,首先让device的driver指针指向要绑定的device_driver对象。并通过driver_sysfs_add函数发送驱动绑定的通知到所有注册到其所属总线上的通知块,在驱动所属的sysfs目录中创建设备sysfs目录的链接文件,在设备所属的sysfs目录中创建名sysfs目录的链接文件,名字为driver:
1 | dev->driver = drv; |
接着调用probe回调函数,如果设置了总线的probe回调函数,则调用总线的probe函数,否则调用驱动的probe回调函数:
1 | if (dev->bus->probe) { |
最后调用driver_bound函数完成设备和驱动的绑定以及其对应sysfs中对象的关联。在driver_bound函数首先将设备添加到驱动的设备链表头中,接着将所有延迟probe列表中的该设备删除,最后向所有注册到总线上的通知块发送绑定结束的通知。
1 | driver_bound(dev); |
Classes
class是设备高级别的抽象,屏蔽了设备底层的实现细节。class结构在内核中的定义如下:
1 | struct class { |
name:描述class的名字。
owner:指向class所属的模块。
class_attrs:class对象的属性文件。
dev_attrs:class关联的设备的属性文件。
dev_bin_attrs:属于该class的设备的二进制文件。
dev_kobj:class的kobject对象。
dev_uevent:当一个设备添加到该class或从该class移除时调用,或者其它情况生成uevent事件时调用。
devnode:用来在/dev/目录下创建设备文件时回调。
class_release:用来释放class。
dev_release:用来释放class中的设备。
suspend:使该class中的设备进入睡眠模式时调用。
resume:从睡眠模式中唤醒设备时调用。
namespace:该class的命名空间。
pm:电源关联相关操作集合。
p:驱动核心私有数据。该结构和总线中的p成员相同。
创建和注册
class的创建和注册接口为class_create,class单独的注册接口为class_register,class_create和class_register都是调用__class_register函数注册的。在这里只关注class_create接口。
class_create是一个宏函数,其真正调用的是__class_create函数。
在__class_create函数中,首先创建class对象,并根据初始化其名称和所属模块,然后设置其class_reliase回调函数:
1 | cls = kzalloc(sizeof(*cls), GFP_KERNEL); |
然后调用__class_register函数注册刚创建的class:
1 | retval = __class_register(cls, key); |
在__class_register函数中首先为class的p成员申请空间并初始化其设备链表头,interfaces链表头,sysfs关联目录的名称等:
1 | cp = kzalloc(sizeof(*cp), GFP_KERNEL); |
接着设置class包含的设备在sysfs下所属的目录为/sys/dev目录:
1 | /* set the default /sys/dev directory for devices of this class */ |
然后设置其所属的kset对象为class_kset,即class被注册到的sysfs目录为/sys/class目录。接着调用kset_register将该class注册到/sys/class目录中。再接着通过add_class_attrs向其注册的目录中添加class的属性文件。
1 | cp->subsys.kobj.kset = class_kset; |
向class添加设备
向class中创建设备的接口是device_create函数,该函数主要通过调用device_create_vargs函数添加设备,在device_create_vargs函数中,先创建了device对象,并根据传入参数设置了其devt和class以及parent属性。并设置了device对象的release回调函数,以及device对象的drvdata:
1 | dev = kzalloc(sizeof(*dev), GFP_KERNEL); |
最后根据传入的参数设置了device对象在sysfs中名字,并调用device_register函数将设备添加到总线和sysfs中,正如在Devices章节分析的那样,这里设置了device对象的class成员和devt成员后,在device_register函数中不但会将该device对象添加到/sys/devices中,并且会在该class对应/sys/class中的目录中创建该设备的链接,同时在/sys/dev/目录下创建该设备的链接,同时也会在devtmpfs中创建设备节点。
/dev下设备文件的创建
在linux系统中,可以通过mknod程序手动创建设备文件。
在较早版本的内核中,可以在内核中创建class,并在class中创建device对象并设置device对象的devno,这样注册到sysfs的device对象会在/sys/class目录中对应的class目录中创建device对象的链接文件。当系统启动初始化的时候,用户空间的udev或者mdev程序可以扫描/sys/class目录,对具有设备号的设备自动在/dev/目录下为其创建设备文件。
在较新版本的内核中引入了devtmpfs的概念,对于属于class和有devno的device对象,在其注册阶段就会在devtmpfs中创建设备文件,系统启动后不需要udev或mdev的扫描,直接通过下面的命令挂载devtmpfs到/dev目录即可:
1 | mount -t devtmpfs none /dev |
内核uevent向用户空间的通知
内核发送事件
从设备驱动模型各组件的注册过程可知内核通过kobject_uevent函数向用户空间发送通知,kobject_uevent函数大概流程如下:
1.通过发送事件的kobject获取到其kset对象,从kset中获取到其kset_uevent_ops。
1 | /* search the kset we belong to */ |
2.判断该kobj本事是否设置了uevent_suppress,该属性不为零表示该kobject对象不发送uevent。
1 | /* skip the event, if uevent_suppress is set*/ |
3.执行kset_uevent_ops中的filter回调函数,已确定该kobj的事件是否被过滤。
1 | /* skip the event, if the filter returns zero. */ |
4.确定其subsystem名字。
1 | /* originating subsystem */ |
5.申请环境变量buf,设置环境变量。
1 | /* environment buffer */ |
6.如果是添加或移除事件,在kobject对象中的相关属性中标记,以确保该kobject能正确的被cleanup,同时向环境变量缓冲区中设置事件序号。
1 | /* |
7.将事件和其环境变量缓冲区内容通过netlink发送到用户空间。
1 | /* send netlink message */ |
8.如果用户空间设置了uevent_helper程序,则通过call_usermodehelper调用该程序。
1 | /* call uevent_helper, usually only enabled during early boot */ |
用户空间处理事件
用户空间一般用mdev/udev工具处理内核发送到用户空间的事件。内核一般用两种机制完成该过程,一种是通过netlink网络将事件信息发送到用户mdev/udev守护进程。一种是直接调用用户程序,这就要求用户空间向内核设置一个用户空间处理事件的程序,需内核开启CONFIG_HOTPLUG配置,以可通过procfs或sysfs向内核设置该程序,默认由CONFIG_UEVENT_HELPER_PATH配置设定,一般的默认设置是/sbin/hotplug,通过sysfs或procfs设置方法如下:
1 | echo /sbin/mdev > /proc/sys/kernel/hotplug |
以mdev为例说明,mdev主要负责两个功能,第一是在系统启动的时候扫描/sys/class目录,将该目录下有设备号的设备在/dev/目录下创建设备文件。第二个功能就是接受内核uevent事件,在/dev下动态添加设备或改变设备的属性。
设备驱动模型应用实例
实例说明
通过一个简单的设备驱动模型的应用实例验证设备驱动模型。
内核samples代码由关于kset和kobject的测试代码,在这里直接使用一些原有的对象,在其基础之上做进一步验证。
实现一个ddm(device driver model)模块,向系统中注册一个新的总线ddm,并为其注册设备的父设备为ddm_bus设备,在注册总线之后,将总线上的drivers_autoprobe属性值设置为0,是为了手动绑定ddm总线上的设备和驱动。然后定义一种ddm_device类型的设备和ddm_driver类型的驱动,总线上简单的通过名字的比较匹配ddm设备和ddm驱动。并实现ddm设备的注册和取消注册接口以及ddm驱动的注册和取消注册接口。在模块加载函数中注册总线设备父设备以及总线,之后再注册一个ddm驱动和两个ddm设备,以此来验证设备驱动模型。在用户空间sysfs视角,该模块实现以下功能:
1.在/sys/devices目录中新增ddm_bus目录,在ddm_bus目录中存在ddm_driver-0和ddm_driver-1两个目录。
2.在/sys/bus目录中新增ddm目录,在ddm目录的driver目录中存在ddm_driver目录以及ddm_driver-0和ddm_driver-1两个目录的链接文件。
3.在/sys/module下新增ddm目录,在ddm目录中存在ddm_driver目录的链接文件。
实例代码
1 |
|
实例测试
将实例代码编译成模块文件ddm.ko,将ddm.ko通过insmod程序加载到内核后可以看到:
1.在/sys/module目录下出现ddm目录,该目录的drivers目录中中存在指向/sys/bus/ddm/drivers/ddm_driver目录的链接文件。
2./sys/bus目录中出现ddm目录,在该ddm目录中的drivers目录下有一个ddm_driver目录,devices目录下有两个链接文件ddm_driver-0和ddm_driver-1
3.在/sys/devices目录中出现ddm_bus目录,该目录中有ddm_driver-0和ddm_driver-1两个目录。
以上现象说明模块实例成功向内核注册了ddm总线,并在ddm总线上注册了一个ddm_driver和两个设备(ddm_driver-0和ddm_driver-1)。
接下来向/sys/bus/ddm/drivers_probe或者/sys/bus/ddm/drivers/ddm_driver/bind文件中写入“ddm_driver-0”:
1 | echo ddm_driver-0 > /sys/bus/ddm/drivers_probe 或者 |
出现的新现象为:在/sys/bus/ddm/drivers/ddm_driver目录中出现了ddm_driver-0的链接文件,指向/sys/devices/ddm_bus/ddm_driver-0目录。在/sys/bus/ddm/devices/ddm_driber-0目录中出现了driver链接文件,指向/sys/bus/ddm/drivers/ddm_driver。
以上现象说明,通过向总线的drivers_probe或者驱动的bind属性文件写入要绑定的设备名后,内核成功将了匹配到的设备和驱动建立绑定关系。