一、前言
Linux内核支持大量的硬件设备,且这个数量一直在增加。那么代码内部的拓扑和复杂性等都在急剧上升,这会导致代码变得杂乱和提升管理难度。为了做好设备驱动的管理,降低驱动的开发难度,还要兼容设备的热插拔和电源管理等,Linux内核需要归纳和分类硬件设备,抽象出一套标准的数据结构和接口,而这就是 统一设备模型
二、抽象统一设备驱动
我们可以通过下面这个图来简单的理解内核是如何组织设备及驱动的
在图中,我们可以抽象出几个概念:总线(Bus)、设备(Device)、驱动(Driver) 和 类(Class)。
- 总线:为了而昂 CPU 和 多个 设备 之间进行信息交互的通道,由此抽象出总线。所有的设备都连接到 总线(无论CPU的外设总线和虚拟总线platform Bus) 上。
- 类:Linux内核中的 类 不是 对面对象程序设计中的类,而是指 具有相似功能或属性的设备。由于设备之间的功能或属性相同,所以可以在多个设备之间抽象出一套 统一的数据结构和接口,这就是 类。从属于 相同类的设备驱动程序 就不再需要重复定义公共属性,直接从类中继承即可。
- 设备:将系统中所有硬件设备的共同属性,比如 名字、 属性、从属总线 和 类 等信息抽象出来,即成为 设备
- 驱动:Linux内核使用 驱动 来描述硬件设备的驱动程序,驱动包含了 设备初始化、电源管理接口 和 设备操作接口,而驱动开发基本围绕这些规定的接口进行开发。
2.1 kobject
kobject 是 Linue统一模型 的基础,也是比较难理解的一个部分。前面提到的 4 个数据结构可以将大量的硬件设备组织起来,所以需要大量的数据结构来描述 硬件。这些 数据结构 拥有一些共同的功能,为了将这些功能抽象统一,所以诞生了 kobject。
PS:每个 kobject都会在 sysfs系统 以 目录的形式 出现。
kobject 支持以下功能:
- 对象的引用计数:用于跟踪内核对象的声明周期。当内核中没有驱动或者代码引用对象时,说明对象的声明周期已经结束,可以删除。
- sysfs表述:sysfs 显示的每一个对象都对应一个 kobject,用于应用层与内核进行交互
- 数据结构关联:在Linux内核中会有大量的设备连接,由此会形成一个 多层次的体系结构,kobject 实现将多个设备组织起来并行程体系。
- 热插拔事件:内核中使用 kobject 实现 热插拔功能。当系统中有硬件发生 热插拔 时,将产生事件并通知到 用户空间
一般情况下,kobject 不会单独出现。它主要是嵌入到一个数据结构中。把 高级对象 接入到 统一设备模型 中,比如 cdev。
struct cdev {
struct kobject kobj;//内嵌到cdev中的kobject
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
}
内核通过组织 kobject,将所有的内核对象(包括上面提到的 4 个数据结构) 组成成树状图。通过访问树状上的 kobject节点 即可访问到指定 高级对象。
kobject 支持以下的一些基本接口:
struct kobject {
const char *name;//在sysfs中显示的目录名臣
struct list_head entry;//用于链入到kset
struct kobject *parent;//指向其parent kobject,由此形成层次结构
struct kset *kset;//指向其所在的kset
struct kobj_type *ktype;//执行该kobject所属的ktype
......
unsigned int state_initialized:1;//指示该kobject是否已经初始化
unsigned int state_in_sysfs:1;//指示该kobject是否已经显示在sysfs
unsigned int state_add_uevent_sent:1;//记录是否已经想用户空间发送add uevent时间
unsigned int state_remove_uevent_sent:1;//记录是否已经想用户空间发送remove uevent时间
unsigned int uevent_suppress:1;//如果该字段为1,则忽略所有需要上报的uevent时间
};
/*
补充:uevent提供了想用户空间通知的功能。当发生了kobjetct的添加、修改和删除动作时会通知用户空间
*/
/* 初始化kobject,将引用计数设置为1 */
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
/* 设置kobject的名字,用于在sysfs中显示 */
int kobject_set_name(struct kobject *kobj, const char *fmt, ...)
/* 增加kobject的引用计数,引用成功将返回kobject的指针,否则返回NULL */
struct kobject *kobject_get(struct kobject *kobj)
/* 减少kobject的引用计数,当计数减小到为0时释放该kobject */
void kobject_put(struct kobject *kobj)
2.2 kobj_type
kobject 大多数的使用场景是内嵌在大型的数据结构中,如 kset、device_driver 等。这些数据结构是动态分配和 动态释放 的。那么在什么时候释放呢?每个 kobject 需要在 引用计数 为 0 时进行释放,但内嵌着 kobject 的大型数据结构如何释放呢?
ktype 中的 release回调函数 负责 释放kobject(甚至是包含Kobject的数据结构) 的内存空间。为了能够释放上层的大型数据结构,所以 ktype及其内部函数 是由 上层数据结构 所在的模块实现。因为只有它自己才知道如何通过 kobject指针 找到需要释放的生成数据结构指针,从而进行释放。
kobject结构 本身并没有包含 release函数,而是由一个 kobj_type结构 来负责对该类型进行跟踪,我们也可以在 kobject结构 中看到一个名为 ktype 的成员,其原型为:
struct kobj_type {
void (*release)(struct kobject *kobj);//kobject的release方法
const struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
};
2.3 kset
内核通常会使用 kobject 将多个对象链接起来形成一个分层的结构体系。有 2 种机制用于实现这种链接,即 parent指针 和 kset。
kobject结果 中的 parent成员 是一个 kobject结构类型的指针,指向了分成结构总的上一层节点。对 parent指针 来说,最重要的用途就是在 sysfs 中定位对象。
kset 是嵌入了 相同类型结构的kobject集合。从数据结构来看,kset 像是 kobj_type结构 的扩充。但 kset 关心的是 对象的集合,而 kobj_type 关心的是 对象的类型。
其原型如下:
struct 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);
};
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
const struct kset_uevent_ops *uevent_ops;
};
可以看到 kset 本身也内嵌了一个 kobject,所以 kobject 的操作也同样适用于 kset。另外 kset 中的 list成员 用以链接 kobject,每个在 kset 中的 kobject元素 都会在 sysfs 中呈现,前提是 kset 已经被设置并添加到系统中。而单独的 kojbect 未必在 sysfs 中出现。
kset 在一个标准的内核链表中保存了它的子节点,链表上的这些节点(即 kobject)会将它们的 parent指针 指向对应的 kset 的 kobject成员,如下图所示:
一般情况下:
- kobject 的 kset指针成员 指向了其所在的 kset。
- kobject 的 parent指针成员 指向了其所在 kset 的 kobject指针成员。但未必每一个 kobject 都未必是指向其所在的 kset,即使这种情况非常少见。
kset 中也有一个 kobj_type指针成员,指向了对应的 kobj_type结构体。该 kobj_type结构体 用来描述它所包含的 kobject。需要注意的是,一般优先使用 kobject 本身的 kobj_type成员,如果当 kobject 中的 kobj_type指针为空,则使用其所在的 kset 的 kobj_type成员。
三、sysfs属性文件
kobject 和 kset 都会在 sysfs机制 中显现它们的功能作用。对于每个 kobject,在 sysfs 中都有对应的目录,每个目下的文件对应为 kobject 的属性,这些内容有内核实现。
只要我们往内核中添加一个 kobject,sysfs 也会显示对应的目录,对于 kset 也是同理。关于 sysfs 与 kset、kobject 的关系大致总结如下:
- kobject 在 sysfs 中的表示始终是一个目录。因此,往系统添加 kobject 时将会在 sysfs 中创建一个目录,该目录下一般含有一个或多个属性
- 分配给 kobject 名字就是在 sysfs 中的 目录名。因此,在 sysfs 处于同一层的 kobject 名字必须是 唯一的,且分配给 kobject 的名字必须是合法的 目录名(比如不能含 反斜杠)。
- kobject 在 sysfs 中的位置与其 parent指针 有关。如果其 parent指针为空而kset指针不为空 ,则它会被添加到其所在 kset 所在的目录层次。如果 parent和kset都为空,则会被创建在 sysfs的 最高层。
前面说到 kobject 可以拥有许多 属性,这些 属性 保存在其 kobj_type成员 中,如下:
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;//属性列表
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
};
struct attribute {
const char *name;//属性名,该名字会在sysfs中显示
umode_t mode;//属性的保护位,通常有只读S_IRUGO、只写S_IWUSR和读写等几种操作模式,相似可以查看<linux/stat.h>
}
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *, char *);
ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t);
};
可以在 kobj_type 中看到 default_attrs成员 和 sysfs_ops成员 :
- default_attrs:是一个 二级指针,用于保存由一个或多个属性组成的 属性列表。
- sysfs_ops:提供 属性 的实现方法,有 show 和 store 两种方法。
根据 LDD3 的说法:当用户在应用层对 属性文件 进行读写时,对调用 sysfs_ops 中的方法,并将属性对应的指针传递给方法。从而可以让 sysfs_opfs 判断当前读写的是属性从而进行相关的操作。
根据笔者的理解,这种用法目前比较少见,在常见的驱动框架中添加属性可以通过其他方式。
- 使用 内核宏 定义 属性 并实现 show方法 和 store方法
- 使用将定义好的属性组成为 属性列表
- 定义一个 属性组,将 属性列表 添加进 属性组
- 将 属性组 注册进某一 驱动框架 下,由内核生成 sysfs 下的属性节点
下面为内核中常见的属性定义宏
/* from <include/linux/device.h> */
/* 对设备的使用 */
#define DEVICE_ATTR(_name, _mode, _show, _store) struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
/* 对总线使用 */
#define BUS_ATTR(_name, _mode, _show, _store) struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)
/* 对类使用 */
#define __ATTR_RW(_name) __ATTR(_name, (S_IWUSR | S_IRUGO), _name##_show, _name##_store)
#define CLASS_ATTR_RW(_name) struct class_attribute class_attr_##_name = __ATTR_RW(_name)
/* 对设备使用 */
#define DRIVER_ATTR_RW(_name) struct driver_attribute driver_attr_##_name = __ATTR_RW(_name)
/* 生成属性组 */
#define ATTRIBUTE_GROUPS(_name) \
static const struct attribute_group _name##_group = { \
.attrs = _name##_attrs, \
};
四、总线、设备和驱动
编写或者移植 设备驱动 时,我们一般不需要直接接触 内核总线类型,一般总线驱动在芯片厂家提供的 bsp或sdk 中,我们直接使用即可。本节内容主要是为了梳理内核 统一设备模型 的架构,进一步理解内核。
4.1 总线
总线 的用于在多个设备之间进行连接,以互相通信。而在 统一设备模型 中,总线(Bus) 本身也是一类特殊的 设备,它用于连接 处理器 和 外设。Linux内核 规定每个设备都需要挂接在一个 总线(Bus) 上。当然了,这个 总线 不一定就是实际存在的总线(如 I2C、USB等),也有可能是虚拟总线(如 platform Bus)。
在 统一设备模型 中,使用以下结构来描述 总线:
/* from <inlcude/linux/device.h> */
struct bus_type {
/* 总线名称 */
const char *name;
/* 总线上的设备名称,当总线上的设备没有命名时将使用该成员 */
const char *dev_name;
/* 总线匹配device和driver时的回调函数,如果匹配成功返回非0值以便后续进行处理 */
int (*match)(struct device *dev, struct device_driver *drv);
/*
一个由具体的bus driver实现的回调函数。
当任何属于该Bus的device,发生添加、移除或者其它动作时,
Bus模块的核心逻辑就会调用该接口,以便bus driver能够修改环境变量
*/
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
/*
如果该总线上的device需要probe话,需要保证该device所在的bus是被初始化过、确保能正确工作的。
所以需要在执行driver的probe前,先执行它所在bus的probe。
remove的过程相反。
*/
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
/*
一些电源相关的接口
*/
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
/* bus自身的私有数据 */
struct subsys_private *p;
};
struct subsys_private {
/* subsys是该总线在sysfs下的表现形式,代表了该总线 */
struct kset subsys;
/*
devices_kset和drivers_kset则是bus下面的两个kset
分别包括该bus下所有的device和device_driver。
*/
struct kset *devices_kset;
struct kset *drivers_kset;
/*
klist_devices和klist_drivers是两个链表,
分别保存了本bus下所有的device和device_driver的指针,以方便查找。
*/
struct klist klist_devices;
struct klist klist_drivers;
/* 用于控制该bus下的drivers或者device是否自动probe */
unsigned int drivers_autoprobe:1;
/* bus和class指针,分别保存上层的bus或者class指针。 */
struct bus_type *bus;
struct class *class;
};
Linux内核总线 一般有以下的行为及功能:
- 完成 总线bus 的 注册 和 注销
- 实现该 总线bus 的 device 或者 device_driver 的添加和删除(在 添加 时会在 device 在 sysfs 中的位置链接到该 总线bus 下的 device目录。而在设备的 sysfs 目录中也会创建一个链接指向其所属的 bus目录)
- 实现该 总线bus 的 device_drivers 的 probe
- 管理 总线bus 下的所有 device 和 device_driver
值得注意的是,bus 中的 drivers_autoprobe变量(默认为1),用于控制 是否在device或者driver注册时,自动probe。bus模块 将它开放到 sysfs 中了,因而可在 用户空间 修改,进而控制 probe行为。
4.2 设备和驱动
在 Linux 中设备和驱动往往是分不开。通常来讲,设备更多的是表现为 硬件信息,而驱动表现为 驱动代码。下面将分别对这 2 个基本单位进行讲述。
在 Linux 底层,每一个设备都使用 device结构 来描述,该结构非常复杂,这里参照参考链接中的文章做一些省略:
struct device {
/* 设备的父设备,一般是指其所在的总线或者控制器 */
struct device *parent;
/* 私有数据指针,用于保运子设备链表 */
struct device_private *p;
/* 设备对应的kobject */
struct kobject kobj;
/* 设备初始化名称 */
const char *init_name;
/* type类似于kobject和kobj_type的关系 */
const struct device_type *type;
/* 该设备所属的总线 */
struct bus_type *bus;
/* 该设备所属的驱动 */
struct device_driver *driver;
/*
一般用于保存具体的驱动数据。某些驱动程序可以将一些私有的数据暂存在这里。
需要使用的时候再拿出来,因此设备并不关心该指针的实际含义
*/
void *platform_data;
/* 大家所熟悉的设备号 */
dev_t devt;
/* 设备所属的类 */
struct class *class;
/* 设备的属性组,会在sysfs下显示 */
const struct attribute_group **groups;
};
/*
device_type是内嵌在struct device结构中的一个数据结构,用于指明设备的类型。
提供一些额外的辅助功能。
*/
struct device_type {
/*
表示device_type的名称。
当带有该类型的设备添加到内核时,内核会发出 DEVTYPE=name类型的uevent。
用以告知用户空间某个类型的设备available了
*/
const char *name;
/*
带有该类型设备的属性组。
设备注册时,会同时注册这些attribute。跟device本身的groups类似。
*/
const struct attribute_group **groups;
/*
所有相同类型的设备,会有一些共有的uevent需要发送,由ueventh函数实现
*/
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
char *(*devnode)(struct device *dev, umode_t *mode,
kuid_t *uid, kgid_t *gid);
/*
如果device本身没有注册release接口,就要查询它所属的类型是否有提供。
用于释放device变量所占的空间
*/
void (*release)(struct device *dev);
};
而每个驱动怎用 device_driver 来描述,如下所示:
struct device_driver {
/* 驱动名,用于匹配设备 */
const char *name;
/* 设备所属总线 */
struct bus_type *bus;
/*
是否启动sysfs中的bind和unbind attribute机制,
该机制可以在用户空间手动为驱动解绑/绑定指定的设备
*/
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
enum probe_type probe_type;
/*
probe函数是匹配到设备后需要执行的函数
remove函数是移除设备是需要执行的函数
*/
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
/*
shutdown、suspend和resume是电源管理相关的函数
*/
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
/* 驱动的属性组,在sysfs下显示 */
const struct attribute_group **groups;
/* 私有数据指针 */
struct driver_private *p;
};
在设备模型框架下,一般开发包括 2 个步骤:
- 分配一个 struct device类型 的变量并填充必要的信息后,把它注册到内核中。按着笔者的理解,在带有 设备树 和 platform driver 的情况下,一般会在设备初始化时由系统转化为 设备数据结构。
- 分配一个 struct device_driver类型 的变量并填充必要的信息后,把它注册到内核中。
- 实现 driver 的 probe、remove等函数,从而触发 初始化和移除等操作。
关于 probe 的执行时机,笔者介绍 I2C框架 的文章里面有做一些流程说明,有兴趣的读者可以前往阅读。当然了,那只是一个例子,只是以点见面,帮助大家理解。
其实 device 和 device_driver 很少会直接使用,一般都会在其上封装一层数据结构,比如 platform_device。
这里需要主题的是,device 和 device_driver 必须挂在同一个 bus 下。这样才可以触发 probe 等函数。一般当系统初始化时会根据 设备树 的解析结果,将相应的挂到某一条 总线 下,该 总线 可以是虚拟的或者实际存在的,通常是 platform_bus,而设备会被解析成 platform_device。而在编写驱动时我们则是对 platform_driver 进行操作。
五、类
类(class) 是一种 设备 的高层视图,它抽象出底层的实现细节。它将一系列功能类似的设备抽象出来,比如一些相似的 设备 需要向用户空间提供相似的 接口,如果每个将设备的驱动都实现一遍的话,就会导致内核有大量的冗余代码,这就是极大的浪费。此时类就可以帮助我们对设备进行抽象,节省代码。
所有的类都在 /sys/class
目录下,class 的代码结构如下:
struct class {
/* 类的名称,显示在/sys/class/目录下 */
const char *name;
struct module *owner;
class_atrrs,x。
/* 类的默认属性组,会在类注册到内核时,会自动在/sys/class/xxx_class下创建对应的属性文件 */
const struct attribute_group **class_groups;
/* 该类下每个设备的的默认属性组,会在类注册到内核时,会自动在该类下的设备目录创建对应的属性文件 */
const struct attribute_group **dev_groups;
/*
表示该类下的设备在/sys/dev/下的目录
现在一般有char和block两个,如果dev_kobj为NULL,则默认选择char
*/
struct kobject *dev_kobj;
/* 当该类类下有设备发生变化时,会调用类的uevent回调函数 */
int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
/* 用于release类自身的回调函数。 */
void (*class_release)(struct class *class);
/*
该回调函数用于release该类下的设备。在device_release接口中,会依次检查device、device_type以及device所属的类是否注册release接口,如果有则调用相应的release接口release设备指针。
*/
void (*dev_release)(struct device *dev);
};
类 提供了在 设备加入或这离开类时获取信息 的触发机制,该机制称为 类接口,其原型如下:
struct class_interface {
struct list_head node;
struct class *class;
/*
当设备加入类中,都将调用下面的add或remove接口。
当加入时,函数为设备做一些其他的必要设置,通常不是添加属性,但也可以做其他工作
当离开时,可以做一些必要的清理工作
*/
int (*add_dev) (struct device *, struct class_interface *);
void (*remove_dev) (struct device *, struct class_interface *);
};
类 在 sysfs 下的处理逻辑一般表现为:
- 在
/sys/class/
目录下,创建一个 类 的目录 - 在 类目录 下,创建每一个属于该类的 设备符号链接,通过这种方式可以在该 类 下的目录访问属于该类的 设备的所有属性
- 同时,设备在 sysfs 中也会创建一个符号链接来链接到 所属类 的目录
六、结语
本文简单的说明了 Linux统一设备类型 的基本结构,以此增进对设备驱动的理解。通过对设备类型的架构理解,可以比较清楚的知道Linux如何组织设备及驱动,有助于我们理解驱动设备模块的编写。在编写本文时结合了 《LLD3》 中的内容,书中的内容已经落后于现在的 Linux源码,所以没有把各个接口都罗列出来。读者们可以去阅读 include/linux/device.h
头文件来获取接口的相关知识。这篇文章鸽了挺久的,因为最近笔者事情比较多,所以产出速度也慢一些,可能质量也会受到一些影响。希望读者多多包涵,如果有错误的地方也欢迎指正。
七、参考链接
- 统一设备模型
- 《LLD3》