统一设备模型
为了降低设备多样性带来的 Linux 驱动开发的复杂度,实现设备的热插拔处理和电源管理等功能,Linux 内核提出了 device model 的概念。device model 将硬件设备归纳、分类,抽象出一套标准的数据结构和接口,驱动的开发则简化为对内核所规定的数据结构的填充和实现。
技术背景
一切皆文件,这是 Linux 的哲学之一。设备当然也不例外,它们往往被抽象成文件,存放在 /dev 目录下供用户进程进行操作。用户通过这些设备文件,可以实现对硬件进行相应的操作。而这些设备文件,需要由对应的设备文件系统来负责管理。
在 kernel 2.6 之前,完成这一使命的是 devfs。devfs 是 Linux 2.4 引入的一个虚拟的文件系统,挂载在 /dev 目录下。可以动态地为设备在 /dev 下创建或删除相应的设备文件,只生成存在设备的节点。然而它存在以下缺点:
- 可分配的设备号数目 (major / minor) 受到限制
- 设备映射不确定,一个设备所对应的设备文件可能发生改变
- 设备名称在内核或模块中写死,违反了内核开发的原则
- 缺乏热插拔机制
随着 kernel 的发展,从 Linux 2.6 起,devfs 被 sysfs + udev 所取代。sysfs + udev 在设计哲学和现实中的易用性都比 devfs 更优,自此 sysfs + udev 的组合走上 mainline ,直至目前,依然作为 Linux 的设备管理手段。
kobject
数据结构
kobject 时 Linux 设备模型的基础,它主要提供如下功能:
- 通过
parent指针,可以将所有kobject以层次结构的形式组合起来。 - 使用
reference count,来记录kobject被引用的次数,并在引用次数变为 0 时把它释放 - 和
sysfs 文件系统配合,将每一个kobject 及其特性,以文件的形式,开放到用户空间
struct kobject
在 Linux 中,kobject几乎不会单独存在,Linux driver 开发者,很少会直接使用 kobject 以及它提供的接口,而是使用构建在 kobject 之上的设备模型接口。 kobject 的主要功能,就是内嵌在一个大型的数据结构中,为这个数据结构提供一些底层的功能实现。
|
|
struct kobj_type
kobject_type 类似于对 kobject 的派生,包含不同 kobj_type 的 kobject 可以看做不同的子类。通过实现相同的函数来实现多态。在这种设计下,每一个内嵌 kobject 的数据结构(如 kset、device、device_driver 等),都要实现自己的 kobj_type,并实现其中的函数。
kobj_type 结构如下:
|
|
kobj_type的定义会如实地在 sysfs 中反映,其中的属性 attribute 会以 attribute.name 为文件名在该目录下创建文件。
attibute 是内核空间和用户空间进行信息交互的一种方法。例如某个 driver 定义了一个变量,却希望用户空间程序可以修改该变量,以控制 driver 的运行行为,那么就可以将该变量以 sysfs attribute 的形式开放出来。attribute 分为普通的 attribute 和二进制 attribute :
- 使用 attribute 生成的 sysfs 文件,只能用字符串的形式读写
- struct bin_attribute 在 attribute 的基础上,增加了 read、write 等函数,因此它所生成的 sysfs 文件可以用任何方式读写
- 类似于 bin_attribute ,我们可以用包含 attribute 的方式对 attribute 进行扩展,定义出 device_attribute 、 class_attribute 或一些设备自定义属性
|
|
attribute_group 是属性组,将一组属性打包成一个对象,其包含了以 attribute 和 bin_attribute 指针数组。
|
|
对该文件进行读写会调用 sysfs_ops 中定义的 show() 和 store()
|
|
struct kset
kset 是 kobject 的容器,用于表示某一类型的 kobject 。kset 维护了其包含的 kobject 链表,链表的最后一项执向 kset.kobj 。
|
|
注意和 kobj_type的关联,kobject 会利用成员 kset 找到自已所属的 kset,设置自身的 ktype 为 kset.kobj.ktype 。当没有指定 kset 成员时,才会用 ktype 来建立关系。
此外,kobject 调用的是它所属 kset 的 uevent 操作函数来发送 uevent,如果 kobject 不属于任何 kset ,则无法发送 uevent。
功能实现
kobject大多数情况下会嵌在其它数据结构中使用,其使用流程如下:
- 定义一个
struct kset类型的指针,并在初始化时为它分配空间,添加到内核中 - 根据实际情况,定义自己所需的数据结构原型,该数据结构中包含有
kobject - 定义一个适合自己的
ktype,并实现其中回调函数 - 在需要使用到包含
kobject的数据结构时,动态分配该数据结构,并分配kobject空间,添加到内核中 - 每一次引用数据结构时,调用
kobject_get接口增加引用计数;引用结束时,调用kobject_put接口,减少引用计数 - 当引用计数减少为 0 时,
kobject模块调用ktype所提供的release接口,释放上层数据结构以及kobject的内存空间
上面有提过,
kobject大多数情况下会嵌在其它数据结构中使用,有一种例外 kobject 单独使用的情况是:开发者只需要在 sysfs 中创建一个目录,而不需要其它的 kset、ktype 的操作。这时可以直接调用
kobject_create_and_add接口,分配一个 kobject 结构并把它添加到 kernel 中。
kobject 的分配和释放
kobject 必须动态分配,而不能静态定义或者位于堆栈之上,它的分配方法有两种:
- 通过
kmalloc自行分配(一般是跟随上层数据结构分配),并在初始化后添加到 kernel。这种方式分配的 kobject,会在引用计数变为 0 时,由kobject_put调用其 ktype 的release接口,释放内存空间,其涉及接口如下:
|
|
kobject_add最终会调用内部接口kobject_add_internal,将kobject添加到 kernel ,其主要逻辑为:
- 校验 kobj 以及 kobj->name 的合法性,若不合法打印错误信息并退出
- 调用
kobject_get增加该 kobject 的 parent 的引用计数(如果存在 parent 的话)- 如果存在 kset(即 kobj->kset 不为空),则调用
kobj_kset_join接口加入 kset。同时,如果该 kobject 没有 parent,却存在 kset,则将它的 parent 设为 kset(kset 是一个特殊的 kobject),并增加 kset 的引用计数- 通过
create_dir接口,调用sysfs的相关接口,在 sysfs 下创建该 kobject 对应的目录- 如果创建失败,执行后续的回滚操作,否则将 kobj->state_in_sysfs 置为 1
- 使用
kobject_create创建
kobject 模块可以使用 kobject_create 自行分配空间,并内置了一个 ktype (dynamic_kobj_type) ,用于在计数为 0 是释放空间。代码如下:
|
|
kobject 引用计数的修改
通过 kobject_get 和 kobject_put 可以修改 kobject 的引用计数,并在计数为 0 时,调用 ktype的 release 接口,释放占用空间。
|
|
kobject_release,通过 kref 结构,获取 kobject 指针,并调用 kobject_cleanup 接口继续。
kobject_cleanup,负责释放 kobject 占用的空间,主要执行逻辑如下:
- 检查该 kobject 是否有 ktype,如果没有,打印警告信息
- 如果该 kobject 向用户空间发送了 ADD uevent 但没有发送 REMOVE uevent,补发 REMOVE uevent
- 如果该 kobject 有在 sysfs 文件系统注册,调用 kobject_del 接口,删除它在 sysfs 中的注册
- 调用该 kobject 的 ktype 的 release 接口,释放内存空间
- 释放该 kobject 的 name 所占用的内存空间
kset 的初始化与注册
kset 是一个特殊的 kobject,因此其初始化、注册等操作也会调用 kobject 的相关接口,除此之外,会有它特有的部分。和 kobject 一样,kset 的内存分配,可以由上层软件通过 kmalloc自行分配,也可以由 kobject 模块负责分配,具体如下:
|
|
sysfs
sysfs 是一个基于内存的虚拟文件系统,它和 kobject 一起,可以将 kernel 的数据结构导出到用户空间,Linux 的设备模型在 sysfs 体现为:
- Kernel Objects: 目录
- Object Atrributes: 文件(文件内容为属性值)
- Object Relationships: 链接文件
目录结构
sysfs 负责以设备树的形式向用户空间提供直观的设备和驱动信息:
|
|
以硬盘 vda 为例,既可以在块设备目录 /sys/block/ 下找到,又可以在所有设备目录 /sys/devices 下找到,
|
|
目录以文件的形式提供了设备的信息,比如 dev 记录了主设备号和次设备号,size 记录了分区大小,uevent 存放了 uevent 的标识符等:
|
|
sysfs 映射
sysfs 本质上是对 Linux 设备模型中各个数据结构的映射,它是通过 VFS 的接口去读写 kobject 的层次结构后动态建立的内存文件系统。
从代码实现上看出,当前版本的 sysfs 实际上基于 kernfs 实现。sysfs_init 通过 kernfs_create_root 创建新的 kernfs 层级,然后将其保存在静态全局变量中,供各处使用。然后通过 register_filesystem 将其注册为名为 sysfs 的文件系统。
|
|
目录映射
每个 kobject 都在 sysfs 中对应一个目录,因此在将 kobject 添加到 Kernel 时会调用 sysfs 文件系统的创建目录接口,创建和 Kobject 对应的目录,相关代码调用链路如下:
kobject_add => kobject_add_varg => kobject_add_internal => create_dir => sysfs_create_dir
这里是详细 create_dir 的相关代码:
- 如果 kobj 有 parent ,则它的父节点为
kobj->parent->sd,否则为根目录节点sysfs_root - 然后将其作为参数调用在父节点目录下创建一个名为
kobj->name的目录
|
|
属性映射
kobject 的属性在 sysfs 中对应的是文件,在 linux 内核中,attibute 文件的创建由 sysfs_create_file 接口完成的:
|
|
所有的文件系统,都会定义一个 struct file_operations变量,用于描述本文件系统的操作接口,sysfs 也不例外:
|
|
attribute 文件的 read 操作,会由 VFS 转到 sysfs_file_operations 的 sysfs_read_file 接口上,其处理逻辑如下:
|
|
这里的 struct sysfs_ops 指针哪来的?好吧,我们再看看 sysfs_open_file 接口吧。
|
|
可以看到,sysfs_buffer 的 ops 指针来自于 kobject 中 ktype 的 sysfs_ops 指针。如果从属的 kobject(就是 attribute 文件所在的目录)没有 ktype,或者没有 ktype->sysfs_ops指针,是不允许它注册任何 attribute 的!
以 class 为例
让我们通过设备模型 class 中有关 sysfs 的实现,来总结一下 sysfs 的应用方式。首先,在 class.c 中,定义了 Class 所需的 ktype 以及 sysfs_ops 类型的变量,如下:
|
|
由前面章节的描述可知,所有 class_type 的 kobject下面的 attribute 文件的读写操作,都会交给 class_attr_show和 class_attr_store两个接口处理,以 class_attr_show 为例:
|
|
该接口使用 container_of 从 struct attribute 类型的指针中取得一个 class 模块的自定义指针:struct class_attribute,该指针中包含了 class 模块自身的 show 和 store 接口。下面是 struct class_attribute 的声明:
|
|
因此,所有需要使用 attribute 的模块,都不会直接定义 struct attribute 变量,而是通过一个自定义的数据结构,该数据结构的一个成员是 struct attribute 类型的变量,并提供 show 和 store 回调函数。然后在该模块 ktype 所对应的 struct sysfs_ops变量中,实现该本模块整体的 show 和 store 函数,并在被调用时,转接到自定义数据结构 struct class_attribute中的 show 和 store 函数中。这样,每个 atrribute 文件,实际上对应到一个自定义数据结构变量中了。
device
数据结构
struct device
device 描述了一项设备,对应的数据结构 device
|
|
struct device_private
其中维护了类型为 device_private 的指针 p :
|
|
klist_node 用来作为在所属 driver 链表、所属 bus 链表等中的节点。
设备通过 device_register 来注册到系统中,通过 device_unregister 来从系统中卸载。
功能实现
在设备模型框架下,设备驱动的开发是一件很简单的事情,主要包括 2 个步骤:
- 分配一个
struct device类型的变量,填充必要的信息后,把它注册到内核中。 - 分配一个
struct device_driver类型的变量,填充必要的信息后,把它注册到内核中。
这两步完成后,内核会在合适的时机,调用 struct device_driver 变量中的 probe、remove、suspend、resume 等回调函数,从而触发或者终结设备驱动的执行。而所有的驱动程序逻辑,都会由这些回调函数实现,此时,驱动开发者眼中便不再有设备模型,转而只关心驱动本身的实现。
以上两个步骤的补充说明:
- 一般情况下,Linux 驱动开发很少直接使用 device 和 device_driver,因为内核在它们之上又封装了一层,如 soc device、platform device 等等,而这些层次提供的接口更为简单、易用
- 内核提供很多 struct device 结构的操作接口(具体可以参考 include/linux/device.h 和 drivers/base/core.c 的代码),主要包括初始化(device_initialize)、注册到内核(device_register)、分配存储空间+初始化+注册到内核(device_create)等等,可以根据需要使用。
- device 和 device_driver 必须具备相同的名称,内核才能完成匹配操作,进而调用 device_driver 中的相应接口。这里的同名,作用范围是同一个 bus 下的所有 device 和 device_driver。
- device 和 device_driver 必须挂载在一个 bus 之下,该 bus 可以是实际存在的,也可以是虚拟的。
- driver 开发者可以在 struct device 变量中,保存描述设备特征的信息,如寻址空间、依赖的 GPIOs 等,因为 device 指针会在执行 probe 等接口时传入,这时 driver 就可以根据这些信息,执行相应的逻辑操作了。
attribute 读写
在 sysfs 中我们看到,大多数时候,attribute 文件的读写数据流为:
vfs---->sysfs---->kobject---->attibute---->kobj_type---->sysfs_ops---->xxx_attribute
其中 kobj_type、sysfs_ops 和 xxx_attribute都是由包含 kobject 的上层数据结构实现。Linux 内核中关于该内容的例证到处都是,device 也不无例外的提供了这种例子,如下:
|
|
至于 driver 的 attribute,则要简单的多,其数据流为:
vfs---->sysfs---->kobject---->attribute---->driver_attribute
代码如下:
|
|
device_type
device_type 是内嵌在 struct device结构中的一个数据结构,用于指明设备的类型,并提供一些额外的辅助功能。它的的形式如下:
|
|
device_type 的功能包括:
- name 表示该类型的名称,当该类型的设备添加到内核时,内核会发出
DEVTYPE=name类型的 uevent,告知用户空间某个类型的设备 available 了 - groups,该类型设备的公共 attribute 集合。设备注册时,会同时注册这些 attribute,这就是面向对象中继承的概念
- uevent,所有相同类型的设备,会有一些共有的 uevent 需要发送,由该接口实现
- devnode,devtmpfs 有关的内容,暂不说明
- release,如果 device 结构没有提供 release 接口,就要查询它所属的 type 是否提供,用于释放 device 变量所占的空间
root device
在 sysfs 中有这样一个目录:/sys/devices,系统中所有的设备,都归集在该目录下。有些设备,是通过 device_register 注册到 Kernel 并体现在 /sys/devices/xxx/ 下。但有时候我们仅仅需要在 /sys/devices/ 下注册一个目录,该目录不代表任何的实体设备,这时可以使用下面的接口:
|
|
该接口会调用 device_register函数,向内核中注册一个设备,但是没必要注册与之对应的 driver。
driver
数据结构
struct device_driver
设备依赖于 driver 来进行驱动,对应的数据结构为 device_driver
|
|
struct driver_private
其中维护了类型为 driver_private 的指针 p :
|
|
其维护了 driver 自身的私有属性,比如由于它也是 kobject 的子类,因此包含了 kobj 。可通过 driver_create_file / driver_remove_file 来增删属性,属性将直接作用于 p->kobj 。
功能实现
设备驱动 probe
probe 是指在 Linux 内核中,如果存在相同名称的 device 和 device_driver,内核就会执行 device_driver 中的 probe回调函数。该函数就是所有 driver 的入口,可以执行诸如硬件设备初始化、字符设备注册、设备文件操作 ops 注册等动作。
设备驱动 prove 的时机按照自动触发和手动触发有如下几种:
- 将
struct device类型的变量注册到内核中时自动触发(device_register,device_add,device_create_vargs,device_create) - 将
struct device_driver类型的变量注册到内核中时自动触发(driver_register) - 手动查找同一 bus 下的所有
device_driver,如果有和指定 device 同名的 driver,执行 probe 操作(device_attach) - 手动查找同一 bus 下的所有
device,如果有和指定 driver 同名的 device,执行 probe 操作(driver_attach) - 自行调用 driver 的 probe 接口,并在该接口中将该 driver 绑定到某个 device 结构中,即设置 dev->driver(device_bind_driver)
probe 动作实际是由 bus 模块实现的,这不难理解: device 和 device_driver 都是挂载在 bus 这根线上,因此只有 bus 最清楚应该为哪些 device、哪些 driver 配对。每个 bus 都有一个 drivers_autoprobe 变量,用于控制是否在 device 或者 driver 注册时,自动 probe。该变量默认为 1(即自动 probe),bus 模块将它开放到 sysfs 中了,因而可在用户空间修改,进而控制 probe 行为。
设备驱动 remove
remove 是 probe 的反操作,发生在 device 或者 device_driver 任何一方从内核注销时,其原理类似。
bus
在 Linux 设备模型中,Bus(总线)是一类特殊的设备,它是连接处理器和其它设备之间的通道(channel)。为了方便设备模型的实现,内核规定,系统中的每个设备都要连接在一个 Bus 上,这个 Bus 可以是一个内部 Bus、虚拟 Bus 或者 Platform Bus。
内核通过 struct bus_type 结构,抽象 Bus,它是在 include/linux/device.h 中定义的。本文会围绕该结构,描述 Linux 内核中 Bus 的功能,以及相关的实现逻辑。最后,会简单的介绍一些标准的 Bus(如 Platform),介绍它们的用途、它们的使用场景。
数据结构
struct bus_type
对 bus 模块而言,核心数据结构就是 struct bus_type:
|
|
struct subsys_private
其中维护了类型为 subsys_private 的指针 p ,它维护了 bus 自身的私有属性:
- 挂接在该总线上的设备集合
devices_kset - 与该总线相关的驱动程序集合
drivers_kset
|
|
对应到 sysfs 中,每个 bus_type 对象都对应 /sys/bus 目录下的一个子目录。子目录下必有 devices 和 drivers 文件夹,里面存放指向相应设备和驱动的符号链接。以 pci_express 类型的 bus_type 为例:
|
|
根据上面的核心数据结构,可以总结出 bus 模块的功能包括:
- bus 的注册和注销
- 本 bus 下有 device 或者 device_driver 注册到内核时的处理
- 本 bus 下有 device 或者 device_driver 从内核注销时的处理
- device_drivers 的 probe 处理
- 管理 bus 下的所有 device 和 device_driver
功能实现
bus 注册
bus 的注册是由 bus_register接口实现的,该接口的原型是在 include/linux/device.h 中声明的,并在 drivers/base/bus.c 中实现,其原型如下:
|
|
该功能的执行逻辑如下:
- 为
bus_type中struct subsys_private类型的指针分配空间,并更新priv->bus和bus->p两个指针为正确的值 - 初始化
priv->subsys.kobj的 name、kset、ktype 等字段,启动 name 就是该 bus 的 name(它会体现在 sysfs 中),kset 和 ktype 由 bus 模块实现,分别为bus_kset和bus_ktype - 调用
kset_register将priv->subsys注册到内核中,该接口同时会向 sysfs 中添加对应的目录,如/sys/bus/spi - 调用
bus_create_file向 bus 目录下添加一个uevent attribute,如/sys/bus/spi/uevent - 调用
kset_create_and_add分别向内核添加devices和device_drivers kset,同时会体现在 sysfs 中 - 初始化 priv 指针中的 mutex、klist_devices 和 klist_drivers 等变量
- 调用
add_probe_files接口,在 bus 下添加drivers_probe和drivers_autoprobe两个 attribute,如/sys/bus/spi/drivers_probe和/sys/bus/spi/drivers_autoprobedrivers_probe允许用户空间程序主动出发指定 bus 下的 device_driver 的 probe 动作drivers_autoprobe控制是否在 device 或 device_driver 添加到内核时,自动执行 probe
- 调用
bus_add_attrs,添加由bus_attrs指针定义的 bus 的默认 attribute,这些 attributes 最终会体现在/sys/bus/xxx目录下
device 和 device_driver 添加
内核提供了 device_register 和 driver_register 两个接口,供各个 driver 模块使用。而这两个接口的核心逻辑,是通过 bus 模块的 bus_add_device和 bus_add_driver实现的,下面我们看看这两个接口的处理逻辑。
这两个接口都是在 drivers/base/base.h中声明,在 drivers/base/bus.c中实现,其原型为:
|
|
bus_add_device 的处理逻辑:
-
调用内部的
device_add_attrs接口,将由bus->dev_attrs指针定义的默认 attribute 添加到内核中,它们会体现在/sys/devices/xxx/xxx_device/目录中 -
调用
sysfs_create_link接口,将该 device 在 sysfs 中的目录,链接到该 bus 的 devices 目录下,例如:1 2 3 4 5$ ls -al /sys/bus/i2c/devices/ total 0 drwxr-xr-x 2 root root 0 Mar 28 21:43 . drwxr-xr-x 4 root root 0 Mar 28 21:43 .. lrwxrwxrwx 1 root root 0 Mar 29 14:36 i2c-0 -> ../../../devices/pci0000:00/0000:00:01.3/i2c-0其中
/sys/devices/.../,为该 device 在 sysfs 中真正的位置,而为了方便管理,内核在该设备所在的 bus 的xxx_bus/devices目录中,创建了一个符号链接 -
调用
sysfs_create_link接口,在该设备的 sysfs 目录中(如/sys/devices/platform/alarmtimer/)中,创建一个指向该设备所在 bus 目录的链接,取名为 subsystem,例如:1 2 3 4 5 6 7 8 9 10$ /sys/devices/platform/alarmtimer/ -al total 0 drwxr-xr-x 3 root root 0 Mar 28 21:43 . drwxr-xr-x 11 root root 0 Mar 28 21:43 .. lrwxrwxrwx 1 root root 0 Mar 29 14:38 driver -> ../../../bus/platform/drivers/alarmtimer -rw-r--r-- 1 root root 4096 Mar 29 14:38 driver_override -r--r--r-- 1 root root 4096 Mar 29 14:38 modalias drwxr-xr-x 2 root root 0 Mar 29 14:38 power lrwxrwxrwx 1 root root 0 Mar 28 21:43 subsystem -> ../../../bus/platform -rw-r--r-- 1 root root 4096 Mar 28 21:43 uevent -
最后,要把该设备指针保存在
bus->priv->klist_devices中
bus_add_driver的处理逻辑:
- 为该 driver 的
struct driver_private指针(priv)分配空间,并初始化其中的priv->klist_devices、priv->driver、priv->kobj.kset 等变量,同时将该指针保存在device_driver的 p 处 - 将 driver 的 kset(priv->kobj.kset)设置为 bus 的 drivers kset(bus->p->drivers_kset),这就意味着所有 driver 的 kobject 都位于 bus->p->drivers_kset 之下(即 /sys/bus/xxx/drivers 目录下)
- 以 driver 的名字为参数,调用
kobject_init_and_add接口,在 sysfs 中注册 driver 的 kobject,体现在/sys/bus/xxx/drivers/目录下,如/sys/bus/spi/drivers/spidev - 将该 driver 保存在 bus 的
klist_drivers链表中,并根据 drivers_autoprobe 的值,选择是否调用 driver_attach 进行 probe - 调用
driver_create_file接口,在 sysfs 的该 driver 的目录下,创建 uevent attribute - 调用
driver_add_attrs接口,在 sysfs 的该 driver 的目录下,创建由bus->drv_attrs指针定义的默认 attribute - 同时根据 suppress_bind_attrs 标志,决定是否在 sysfs 的该 driver 的目录下,创建 bind 和 unbind attribute
driver 的 probe
driver 的 probe 时机及过程,其中大部分的逻辑会依赖 bus 模块的实现,主要为 bus_probe_device 和 driver_attach 接口。同样,这两个接口都是在 drivers/base/base.h 中声明,在 drivers/base/bus.c 中实现。
这两个结构的行为类似,逻辑也很简单,即:
- 搜索所在的 bus,比对是否有同名的 device_driver(或 device)
- 如果有并且该设备没有绑定 Driver 则调用
device_driver的 probe 接口。
虚拟 bus
在 Linux 内核中,有三种比较特殊的 bus,分别是 system bus、virtual bus 和 platform bus。它们并不是一个实际存在的 bus(像 USB、I2C 等),而是为了方便设备模型的抽象,而虚构的:
- system bus 是旧版内核提出的概念,用于抽象系统设备(如 CPU、Timer 等等)。而新版内核认为它是个坏点子,因为任何设备都应归属于一个普通的子系统(New subsystems should use plain subsystems, drivers/base/bus.c, line 1264),所以就把它抛弃了(不建议再使用,它的存在只为兼容旧有的实现)。
- virtaul bus 是一个比较新的 bus,主要用来抽象那些虚拟设备,所谓的虚拟设备,是指不是真实的硬件设备,而是用软件模拟出来的设备,例如虚拟机中使用的虚拟的网络设备(有关该 bus 的描述,可参考该链接处的解释:https://lwn.net/Articles/326540/)
- platform bus 就比较普通,它主要抽象集成在 CPU(SOC)中的各种设备。这些设备直接和 CPU 连接,通过总线寻址和中断的方式,和 CPU 交互信息。
class
A class is a higher-level view of a device that abstracts out low-level implementation details
数据结构
struct class
此 class 并非 C++ 中的关键字 class ,而是用于表示一种设备分类,对应的数据结构为 class
|
|
class 只是一种抽象的概念,用于描述接口相似的一类设备。其存在的意义主要是方便用户能够基于设备的功能进行快速的定位,而不必通过思索设备所处的位置、连接方式等来定位设备。
struct class_interface
struct class_interface 是这样的一个结构:它允许 class driver 在 class 下有设备添加或移除的时候,调用预先设置好的回调函数(add_dev 和 remove_dev)。那调用它们做什么呢?想做什么都行(例如修改设备的名称),由具体的 class driver 实现。
该结构的定义如下:
|
|
功能实现
class 的注册
class 的注册,是由 __class_register接口实现的,它的处理逻辑和 bus 的注册类似,主要包括:
- 为 class 结构中的
struct subsys_private类型的指针(cp)分配空间,并初始化其中的字段,包括cp->subsys.kobj.kset、cp->subsys.kobj.ktype等等 - 调用
kset_register,注册该 class。该过程结束后,在/sys/class/目录下,就会创建对应该 class(子系统)的目录 - 调用
add_class_attrs接口,将 class 结构中class_attrs指针所指向的 attribute,添加到内核中。执行完后,在/sys/class/xxx_class/目录下,就会看到这些 attribute 对应的文件
device 注册时,和 class 有关的动作
struct device 结构会包含一个 struct class 指针(这从侧面说明了 class 是 device 的集合)。当某个 class driver 向内核注册了一个 class 后,需要使用该 class 的 device,通过把自身的 class 指针指向该 class 即可,剩下的事情,就由内核在注册 device 时处理了。
本节,我们讲一下在 device 注册时,和 class 有关的动作:
device 的注册最终是由 device_add接口(drivers/base/core.c)实现了,该接口中和 class 有关的动作包括:
- 调用
device_add_class_symlinks接口,创建描述的各种符号链接,即:- 在对应 class 的目录下,创建指向 device 的符号链接;
- 在 device 的目录下,创建名称为 subsystem、指向对应 class 目录的符号链接
- 调用
device_add_attrs,添加由 class 指定的 attributes(class->dev_attrs) - 如果存在对应该 class 的 add_dev 回调函数,调用该回调函数
uevent
在引子中提到,从 Linux 2.6 起,devfs 被 sysfs + udev 所取代。我们已经知道 sysfs 就是 Linux 统一设备模型的体现,那么 udev 是什么呢?
udev 是从 Linux 2.6 一直沿用至今的设备管理器,挂载并管理着 /dev 。和 devfs 不同的是,它运行在用户态中,允许用户进行自定义配置,并根据配置在收到事件时在 /dev 下创建设备文件。主要由三部分组成:
-
libudev 函数库,提供获取设备信息的接口。已集成到 systemd 中
-
udevd 处于用户空间的管理软件,管理 / dev 下的设备文件,已集成到 systemd 中,包括以下内容:
- udisks 通过 dbus 提供对存储设备的访问接口
-
upower 通过 dbus 提供电源管理的接口
- NetworkManager 通过 dbus 提供网络配置的接口
-
udevadm 命令行工具。可用来向 udevd 发送指令
Uevent 是 Kobject 的一部分,用于在 Kobject 状态发生改变时,例如增加、移除等,通知用户空间程序。用户空间程序收到这样的事件后,会做相应的处理。
该机制通常是用来支持热拔插设备的,例如 U 盘插入后,USB 相关的驱动软件会动态创建用于表示该 U 盘的 device 结构(相应的也包括其中的 kobject),并告知用户空间程序,为该 U 盘动态的创建/dev/目录下的设备节点,更进一步,可以通知其它的应用程序,将该 U 盘设备 mount 到系统中,从而动态的支持该设备。
由此可知,Uevent 的机制是比较简单的,设备模型中任何设备有事件需要上报时,会触发 Uevent 提供的接口。Uevent 模块准备好上报事件的格式后,可以通过两个途径把事件上报到用户空间:
- 一种是通过 kmod 模块,直接调用用户空间的可执行文件
- 另一种是通过 netlink 通信机制,将事件从内核空间传递给用户空间。
数据结构
kobject.h 定义了 uevent 相关的常量和数据结构,如下:
kobject_action
kobject_action 定义了 event 的类型,包括:
|
|
struct kobj_uevent_env
前面有提到过,在利用 Kmod 向用户空间上报 event 事件时,会直接执行用户空间的可执行文件。而在 Linux 系统,可执行文件的执行,依赖于环境变量,因此 kobj_uevent_env 用于组织此次事件上报时的环境变量。
|
|
struct kset_uevent_ops
kset_uevent_ops 是为 kset 量身订做的一个数据结构,里面包含 filter 和 uevent 两个回调函数,用处如下:
- filter,当任何 Kobject 需要上报 uevent 时,它所属的 kset 可以通过该接口过滤,阻止不希望上报的 event,从而达到从整体上管理的目的
- name,该接口可以返回 kset 的名称。如果一个 kset 没有合法的名称,则其下的所有 Kobject 将不允许上报 uvent
- uevent,当任何 Kobject 需要上报 uevent 时,它所属的 kset 可以通过该接口统一为这些 event 添加环境变量。因为很多时候上报 uevent 时的环境变量都是相同的,因此可以由 kset 统一处理,就不需要让每个 Kobject 独自添加了
|
|
功能实现
uevent 模块提供了如下的 API(这些 API 的实现是在 lib/kobject_uevent.c文件中):
|
|
kobject_uevent_env
kobject_uevent_env,以 envp 为环境变量,上报一个指定 action 的 uevent,环境变量的作用是为执行用户空间程序指定运行环境。具体动作如下:
- 查找 kobj 本身或者其 parent 是否从属于某个 kset,如果不是,则报错返回
- 由此可以说明,如果一个 kobject 没有加入 kset,是不允许上报 uevent 的
- 查看
kobj->uevent_suppress是否设置,如果设置,则忽略所有的 uevent 上报并返回- 由此可知,可以通过 Kobject 的
uevent_suppress标志,管控 Kobject 的 uevent 的上报
- 由此可知,可以通过 Kobject 的
- 如果所属的 kset 有
uevent_ops->filter函数,则调用该函数,过滤此次上报- kset 可以通过 filter 接口过滤不希望上报的 event,从而达到整体的管理效果
- 判断所属的 kset 是否有合法的名称,否则不允许上报 uevent
- 分配一个用于此次上报的、存储环境变量的 buffer(结果保存在 env 指针中),并获得该 Kobject 在 sysfs 中路径信息(用户空间软件需要依据该路径信息在 sysfs 中访问它)
- 调用
add_uevent_var接口,将 Action、路径信息、subsystem 等信息,添加到 env 指针中 - 如果传入的 envp 不空,则解析传入的环境变量中,同样调用
add_uevent_var接口,添加到 env 指针中 - 如果所属的 kset 存在 u
event_ops->uevent接口,调用该接口,添加 kset 统一的环境变量到 env 指针 - 根据 ACTION 的类型,设置
kobj->state_add_uevent_sent和kobj->state_remove_uevent_sent变量,以记录正确的状态 - 调用
add_uevent_var接口,添加格式为SEQNUM=%llu的序列号 - 如果定义了
CONFIG_NET,则使用 netlink 发送该 uevent - 以 uevent_helper、subsystem 以及添加了标准环境变量
HOME=/,PATH=/sbin:/bin:/usr/sbin:/usr/bin的 env 指针为参数,调用 kmod 模块提供的call_usermodehelper函数,上报 uevent。- 其中
uevent_helper的内容是由内核配置项CONFIG_UEVENT_HELPER_PATH决定的,该配置项指定了一个用户空间程序,用于解析上报的 uevent,例如"/sbin/hotplug”。 call_usermodehelper的作用,就是 fork 一个进程,以 uevent 为参数,执行uevent_helper
- 其中
kobject_uevent
kobject_uevent 和 kobject_uevent_env 功能一样,只是没有指定任何的环境变量。
add_uevent_var
add_uevent_var,以格式化字符的形式(类似 printf、printk 等),将环境变量 copy 到 env 指针中。
kobject_action_type
kobject_action_type,将 enum kobject_action 类型的 Action,转换为字符串。
用户态 udevd
udevd 是 udev 机制的核心,负责动态在 /dev 下创建设备文件。
udev 运行在用户态,脱离驱动层的关联,基于这种设计实现,用户可以通过编写规则来动态删除和修改 /dev 下的设备文件,任意命名设备。每当 udevd 收到 uevent 时就会去匹配规则,一旦匹配上了,执行规则对应的操作。重要软件的 rule 存放在 /lib/udev/rules.d/ 下,用户自定义的规则放到 /etc/udev/rules.d/ 下,以 rules 为扩展名。命名规则类似于 grub 脚本,udev 将按数字前缀从小到大进行匹配,依次生效。
匹配主要基于几个字段:
- SUBSYSTEM:设备类型
- ACTION:设备触发的操作,如 add/change/remove
- ATTR / ATTRS:设备的属性,如 class/vendor/ro/removable/size 等等,这些属性其实都能在
/sys下对应的文件读出来。 - KERNEL:kernel 对设备的命名。如 sd*/input*
- ENV:环境设置,用来在多个 rule 之间传递信息
可用于以下用途:
- 设置环境变量(有后续事件需要使用)
- 在 /dev 下创建设备的别名(符号链接)
- 运行特定的指令
当存储设备通过 USB 连接时,udevd 会通知 udisksd-daemon 去处理,挂载该设备。当网线插入时,udevd 会通知 NetworkManager-daemon 去进行相应的 IP 配置(dhclient)。
举个例子:
-
创建 udev 规则文件
/etc/udev/rules.d/81-usb-keyboard.rules,内容如下:ACTION=="add", ATTRS{name}=="*Keyboard*", SUBSYSTEMS=="input", SYMLINK+="my_kbd" RUN+="/sbin/insmod /my_kbd.ko" -
当该 usb keyboard 插入时,收到 uevent :
ACTION = add name = xxxx-Keyboard-xxxx subsystems = input idVendor = xxxx idProduct = xxxx -
udevd 根据 uevent 匹配规则,发现 /etc/udev/rules.d/81-usb-keyboard.rules 匹配,因此运行该规则,创建一个设备的符号链接 /dev/my_kbd ,指向真正的设备,同时加载 my_kbd.ko 模块。
-
继续匹配规则,凡是符合的规则都会被运行
测试实战
在 linux 内核代码 samples/kobject 路径下中有 kobject 和 kset 的 示例代码,你也可以在我的 Github 中找到,下面结合上述介绍来理解这段代码:
kobject-example
- 模块初始化是创建了名为
kobject-example的 kobject - 为 kobject 关联了 3 个 attribute,每个
attribute实现了自己的show和store函数 - 模块卸载的时候使用
kobject_put减少引用
|
|
加载到内核后,可以在 /sys/kernel 看到对应的目录和属性:
|
|
kset-example
- 创建了名为
kset-example的 kset,同时为 kset 关联了 3 个 kobject - foo 这三个 object 都是继承自
kobject,并且为其kobj成员指定了kset和ktype
|
|
加载模块到内核后,可以看到每个 kobject 都是一个目录:
|
|
参考资料
- Linux Kernel Documetation, Everything you never wanted to know about kobjects, ksets, and ktypes
- LWN, The zen of kobjects
- Linux Kernel Documentation, sysfs, The filesystem for exporting kernel objects
- LWN, sysfs: separate out kernfs
- Linux Device Drivers, The Linux Device Model
- 蜗窝 Linux 统一设备模型
- sysfs、udev 和 它们背后的 Linux 统一设备模型
-
No backlinks found.