Linux 驱动之设备驱动模型
kobject
概述
- Kobjects 有一个 name 和一个引用计数,还具有父指针(允许将对象排列成层次结构)
- ktype 是嵌入在 kobject 结构体中的对象。 每个 kobject 结构都需要一个相应的 ktype。 ktype 控制 kobject 在创建和销毁时的行为
- kset 是一组 kobjects(kset 本身包含一个 kobject), 这些 kobject 可以拥有相同的 ktype,kset 也是 sysfs 中的一个子目录。Ksets 可以支持 kobjects 的“热插拔”(uevent 事件)
kobject 的数据结构和实现
下图展示了 kobject、ktype 和 kset 三者数据结构之间的关系:原图
sysfs_ops: show 是回调函数,在读取具有该 kobj_type 的所有属性时调用他。缓冲区长度始终是 PAGE_SIZE,在成功时返回写入缓冲区的数据大小,失败返回错误码。写入的时候调用 store 函数,其参数 buf 最大为 PAGE_SIZE,成功返回写入数据大小,失败返回错误码
uevent_ops: 是此 kset 的 uevent 操作集
attribute: 是由 kobject 导出到用户空间 sysfs 文件,attribute 表示可以从用户空间读取、写入或同时具有这两者的对象属性,属性将内核数据映射到 sysfs 中的文件
用于从文件系统添加/删除属性的函数如下:
1 | int __must_check sysfs_create_file(struct kobject *kobj, |
除此之外还有一个数据结构 attribute_group 很常用:
1 | struct attribute_group { |
attrs 字段是一个指针,他指向以 NULL 结尾的属性列表,每个属性组必须赋予一个指向 struct attribute 元素列表/数组的指针,该 group 只是一个 helper 包装器,以便管理多个属性。 在实际使用中可以定义多个属性文件然后嵌入在 struct attribute_group 结构体中,之后调用如下函数可以一次将多个属性添加/删除到系统。
1 | int __must_check sysfs_create_group(struct kobject *kobj, |
下图展示了 kset 和 kobject 通常的组织关系,形成层级关系,当其他结构需要构建层级关系或需要引用计数的时候都可以将这个数据结构嵌入进去:原图
用法(API)
- 创建 kobjec 的代码必须初始化该对象:
1 | void kobject_init(struct kobject *kobj, struct kobj_type *ktype); |
- 正确创建 kobject 需要 ktype,因为每个 kobject 都必须有一个关联的 kobj_type。 调用 kobject_init() 后,要向 sysfs 注册 kobject,必须调用函数 kobject_add():
1 | int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...); |
如果 kobject 要与特定的 kset 相关联,kobj->kset 必须在调用 kobject_add() 之前赋值。 如果 kset 是与 kobject 相关联的,那么 kobject 的父级可以在调用 kobject_add() 时设置为 NULL,然后 kobject 的父级将是 kset 本身。
- 由于 kobject 的名称是在添加到内核时设置的,因此不应直接操作 kobject 的名称。 如果必须更改 kobject 的名称,请调用 kobject_rename():
1 | int kobject_rename(struct kobject *kobj, const char *new_name); |
- 有一个辅助函数可以初始化 kobject 并将其添加到内核同时调用 kobject_init_and_add():
1 | int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype, |
- 在 kobject 注册到 kobject 核心后,您需要通知它已被创造。 这可以通过调用 kobject_uevent() 来完成:
1 | int kobject_uevent(struct kobject *kobj, enum kobject_action action); |
当 kobject 从内核中删除时, KOBJ_REMOVE 的 uevent 将由 kobject 核心自动调用,因此调用者不必担心手动执行此操作。
- kobject 的关键功能之一是用作嵌入它的对象的引用计数器,用于操作 kobject 引用计数的函数是:
1 | struct kobject *kobject_get(struct kobject *kobj); |
当引用被释放时,对 kobject_put() 的调用将减少引用计数,并可能释放对象。
- 每个注册的 kset 对应 sysfs 目录,可以使用 kset_create_and_add() 函数创建和添加 kset,使用 kset_unregister() 函数将其删除
1 | kset * __must_check kset_create_and_add(const char *name, |
因为 kobject 是动态的,所以它们不能静态声明或在堆栈上声明,而是始终动态分配。 内核的未来版本将包含对静态创建的 kobjects 的运行时检查,并将警告开发人员这种不正确的使用。
example
参考 samples/kobject/kobject-example.c
bus、device 和 driver
数据结构
要理解 linux 驱动的设计,首先要理清楚 linux 驱动最重要的几个数据结构,struct device, struct device_driver, struct bus 这些数据结构是理解 Linux 驱动的关键,下面先从整体上看一下这几个数据结构之间的关系,如下:原图
bus: bus 结构体用于抽象系统中总线的数据结构,这个可以是实际的总线,例如 iic、spi 总线,也可以是虚拟总线 platform 总线。struct bus 结构体管理挂载在该 bus 下的 struct device 和 struct device_driver, 负责 device 和 device_driver 的匹配,调用 probe 等工作。其中管理 struct device 和 struct device_driver 的功能独立出来成为一个子系统叫 subsys_private,该数据结构除了管理该 bus 下的设备和驱动外还用于处理 bus,device 和 device_driver 的一些默认属性(公共属性),uevent 事件等。可以看到 subsys_private 数据结构下有两个链表一个用于挂载 device 另一个用于挂载 device_driver, 从而实现 bus 对 device 和 device_driver 管理
device: device 结构体用于抽象驱动设备,系统下挂载的设备都是通过 struct device 结构体来描述,其中 dts 里定义的很多节点都会转换为 struct device 结构体,用于描述一个设备信息,管理设备用到的资源等。device 结构体下一个重要结构是 device_private,该结构体成员 knode_bus 就是用于挂载到上面提到的 bus 下的 subsys_private 结构体中的 klist_devices
device_driver: device_driver 结构体用于描述对 struct device 结构体描述的设备的驱动方法,比如对于通信协议的实现,对控制器的操作等。这样设备和设备的驱动实现分离单独管理,而设备和驱动分离后两者的匹配工作就是 bus 完成的,device_driver 是用户需要编写的具体操作设备的方法和流程。同样 struct device_driver 结构体下的 driver_private 的 knode_bus 用于链接到 struct bus 下 subsys_private 结构体中的 klist_drivers
三者数据结构实际使用中的连接关系可能如下:原图
bus 和 device 以及 device_driver 下都有一个 private 或 subsys 结构用来处理一些共性的工作,是一种很好的抽象结构。
实现
好了到现在我们已经清楚最重要的三个数据结构之间的关系了,接下来是关于 device 和 device_driver 是如何匹配上的,device 和 device_driver 一个描述了设备一个是拥有驱动设备的方法,但是 linux 下设备非常多,每个设备都要有一个对应的驱动程序来驱动,下面详细看下 device 和 device_driver 的关联过程。
device 的注册过程
1 | int device_register(struct device *dev) |
初始化设备并将其添加到系统中。其中 device_initialize 初始化 device 结构体,主要是 kobject_init 初始化 kobj 结构体,其他就是初始化一些锁、链表指针之类的,重点是 device_add 这个函数,这个函数主要完成以下工作:
1. 初始化 device_private 结构体
2. 使用 bus->dev_name + dev->id 为 dev->init_name 自动命名
3. 增加 device 引用次数
4. 如果没有 class,则子系统为设备指定默认的 root 路径 (bus->dev_root)
5. kobject 名称在此函数中设置并添加到 kobject 层次结构中
6. 通知新设备加入
7. 为 device 创建 sysfs 文件
8. 将 device 添加到 bus(将 dev->p->knode_bus 加入到 us->p->klist_devices)
9. 创建 dev 目录
10. 通知客户端有新设备添加
11. 调用最重要的函数为 device_probe_driver
下面主要介绍 bus_probe_device 这个函数,由于函数调用层级较多,这里不打算将函数一一列出,而是梳理出调用关系,如下:原图
这里需要注意的是 match 函数的实现是具体的 bus 实现的函数,例如 platform bus 会实现自己的 match 函数,后续会有文章进行介绍,关于 probe 函数,一般 bus 也会实现自己的 probe 函数,然后 bus 下的 probe 函数会调用 driver 数据结构的 probe 函数。
driver 的注册过程
driver 的注册是通过 driver_register 该函数完成的,感兴趣的读者可以去阅读源码。同样关于 driver 的注册这里也贴一张图:原图
driver 和 device 的注册类似,都会在自己挂载的 bus 上去遍历对方的所有设备或驱动然后进行遍历匹配,当匹配上了将两个数据结构关联然后调用 driver 的 probe 函数。而 driver 的 probe 函数也就是驱动程序的入口,用户需要实现的操作都在这个函数里实现。
bus 的注册过程
上面已经将 device 和 driver 介绍完了,device 和 driver 都将注册到 bus,然后 bus 管理两个链表一个 device 链表一个 driver 链表,现在我们再看看 bus 数据本身的一注册过程。 bus_register 函数完成 bus 的注册,该函数主要完成以下工作:
1. 为 subsys_private 分配内存空间
2. 初始化 subsys_private 结构体,并将 bus->p 指向这块内存
3. 设置 priv->subsys.kobj 的 name 为 bus->name
4. 初始化 subsys_private 的 kobj、kset,ktype 结构体
5. 注册 kset,会在 sysfs 文件系统下创建目录
6. 向 bus 目录下添加一个 uevent attribute
7. 分别向内核添加 devices 和 device_drivers kset,会体现在 sysfs 中
8. 初始化 subsys_private 里的 mutex、klist_devices 和 klist_drivers 等变量
9. 在 bus 下添加 drivers_probe 和 drivers_autoprobe 两个 attribute,如/sys/bus/spi/drivers_probe 和/sys/bus/spi/drivers_autoprobe),其中 drivers_probe 允许用户空间程序主动出发指定 bus 下的 device_driver 的 probe 动作,而 drivers_autoprobe 控制是否在 device 或 device_driver 添加到内核时,自动执行 probe
10. 调用 bus_add_attrs,添加由 bus_attrs 指针定义的 bus 的默认 attribute,这些 attributes 最终会体现在/sys/bus/xxx 目录下
bus 的注册基本上是对自己内部的数据初始化以便于具体的 bus 结构体使用,例如 platform 总线进行使用。
class
class 数据结构是一种抽象结构,class 是将一些共性的属性或操作提取出来单独成为一个数据结构以提高代码复用率减少重复代码。官方文档的描述是:
A device class describes a type of device, like an audio or network device.
下面是 class 数据结构内容:
1 | struct class { |
其实这里 class 就是一个多设备容器,这个容器就是方便管理设备的(当然 bus 也是管理设备的数据结构,bus 是管理同一总线上的设备和设备驱动的),class 将设备进行分类管理(另一个维度,区别于 bus)并提供 api 方便驱动开发人员进行管理设备。
Uevent
Uevent 是 Kobject 的一部分,用于在 Kobject 状态发生改变时,例如增加、移除等,通知用户空间程序。用户空间程序收到这样的事件后,会做相应的处理。该机制通常是用来支持热拔插设备的,从而动态的支持该设备。
实现
uevent 相关数据结构如下:原图
主要的实现函数就是 kobject_uevent 函数,如下:
1 | int kobject_uevent(struct kobject *kobj, enum kobject_action action) |
调用 kobject_uevent_env 函数:该函数主要完成以下工作:
1. 查找 kobj 本身或者其 parent 是否从属于某个 kset
2. 该 kobject 不属于某个 kset,返回报错。如果一个 kobject 没有加入 kset,是不允许上报 uevent 的
3. 查看 kobj->uevent_suppress 是否设置,如果设置,则忽略所有的 uevent 上报并返回。可以通过 Kobject 的 uevent_suppress 标志,管控 Kobject 的 uevent 的上报
4. 如果所属的 kset 有 uevent_ops->filter 函数,则调用该函数,过滤此次上报,kset 可以通过 filter 接口过滤不希望上报的 event,从而达到整体的管理效果
5. 判断所属的 kset 是否有合法的名称(称作 subsystem,和前期的内核版本有区别),否则不允许上报 uevent
6. 分配一个用于此次上报的、存储环境变量的 buffer(结果保存在 env 指针中),并获得该 Kobject 在 sysfs 中路径信息(用户空间软件需要依据该路径信息在 sysfs 中访问它)
7. 获取 kobject 完整路径
8. 调用 add_uevent_var 接口,将 Action、路径信息、subsystem 等信息,添加到 env 指针中
9. 如果传入的 envp_ext 不空,则解析传入的环境变量中,同样调用 add_uevent_var 接口,添加到 env 指针中
10. 如果所属的 kset 存在 uevent_ops->uevent 接口,调用该接口,添加 kset 统一的环境变量到 env 指针
11. 在对象中标记“添加”和“删除”事件,以确保在自动清理期间向用户空间发送适当的事件。如果对象确实发送了“添加”事件,则核心将自动生成“删除”,如果调用者尚未完成
12. 用 add_uevent_var 接口,添加格式为"SEQNUM=%llu”的序列号
13. 如果定义了"CONFIG_NET”,则使用 netlink 发送该 uevent
13. 以 uevent_helper、subsystem 以及添加了标准环境变量(HOME=/,PATH=/sbin:/bin:/usr/sbin:/usr/bin)的 env 指针为参数
14. 调用 kmod 模块提供的 call_usermodehelper 函数,上报 uevent。其中 uevent_helper 的内容是由内核配置项 CONFIG_UEVENT_HELPER_PATH(位于。/drivers/base/Kconfig) 决定的(可参考 lib/kobject_uevent.c, line 32),该配置项指定了一个用户空间程序(或者脚本),用于解析上报的 uevent,例如"/sbin/hotplug”。call_usermodehelper 的作用,就是 fork 一个进程,以 uevent 为参数,执行 uevent_helper
在系统启动后,大部分的设备已经 ready,可以根据需要,重新指定一个 uevent helper,以便检测系统运行过程中的热拔插事件。这可以通过把 helper 的路径写入到"/sys/kernel/uevent_helper”文件中实现。实际上,内核通过 sysfs 文件系统的形式,将 uevent_helper 数组开放到用户空间,供用户空间程序修改访问,具体可参考"./kernel/sysfs.c”中相应的代码,这里不再详细描述。
platfor 总线
platform 数据结构
platform 数据结构同样是对 device、device_driver、bus 数据结构的再封装,我将 platform 的数据结构列出来如下:原图
platform 总线实现
首先还是将数据结构之间的关系列出来,如下:原图
上图将 platform 总线的实现概要列举出来,下面详细看看 platform 总线的几个重要函数:
platform_probe: 在上面介绍 device_driver 的时候介绍 register 的时候讲到 probe 函数的调用时机,在 device_driver 注册的时候会执行总线的 probe 函数,对于 platform 总线就是由 platform 总线再调用 device_driver 的 probe 函数
platform_match: 这个函数提供了 4 种匹配规则,具体如下:
- driver_override 匹配:这个是最优先的匹配将 device 的 driver_override 和 device_driver 的 name 进行匹配
- of_driver_match_device 匹配:这个把 device_driver 的 of_match_table 和 device 里的 of_node 进行匹配,of_node 由 dts 里的 compatible 字符串进行比较匹配
- acpi_driver_match_device 匹配:acpi 匹配规则进行匹配,这个没详细看
- platform_match_id 匹配:通过 platform_driver->id_table->name 和 platform_device->name 进行匹配
platform_pm_suspend: 在我们编写驱动的时候休眠唤醒是一个非常关键的操作,那么 platform 总线实现的休眠函数就是 platform_pm_suspend,platform 总线的 suspend 的实现就是调用 device_driver 的 suspend 函数,跟 probe 函数类似
总结
这里也说明了 platform 总线并没有操作具体实际的硬件,总线的实现就是调用驱动的具体函数。有了这些总线我们在实际编写驱动的时候就非常方便,我们只需要实现 device_driver 结构体里的具体函数然后将这个结构体注册到 platform 总线就完事了,具体函数的调用时机由总线负责,不需要驱动编写人员编写。
参考文献
窝窝科技-设备驱动模型
窝窝科技-Linux
设备模型 (3)_Uevent
《linux 内核文档》
《linux 设备驱动开发》