0%

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
2
3
4
int __must_check sysfs_create_file(struct kobject *kobj,
const struct attribute *attr);
void sysfs_remove_file(struct kobject *kobj,
const struct attribute *attr);

除此之外还有一个数据结构attribute_group很常用:

1
2
3
4
5
6
7
8
9
struct attribute_group {
const char *name;
umode_t (*is_visible)(struct kobject *,
struct attribute *, int);
umode_t (*is_bin_visible)(struct kobject *,
struct bin_attribute *, int);
struct attribute **attrs;
struct bin_attribute **bin_attrs;
};

attrs字段是一个指针,他指向以NULL结尾的属性列表,每个属性组必须赋予一个指向struct attribute元素列表/数组的指针,该group只是一个helper包装器,以便管理多个属性。 在实际使用中可以定义多个属性文件然后嵌入在struct attribute_group结构体中,之后调用如下函数可以一次将多个属性添加/删除到系统。

1
2
3
4
int __must_check sysfs_create_group(struct kobject *kobj,
const struct attribute_group *grp);
void sysfs_remove_group(struct kobject *kobj,
const struct attribute_group *grp);

下图展示了kset和kobject通常的组织关系,形成层级关系,当其他结构需要构建层级关系或需要引用计数的时候都可以将这个数据结构嵌入进去:原图

用法(API)

  1. 创建kobjec的代码必须初始化该对象:
1
void kobject_init(struct kobject *kobj, struct kobj_type *ktype);
  1. 正确创建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本身。

  1. 由于kobject的名称是在添加到内核时设置的,因此不应直接操作kobject的名称。 如果必须更改kobject的名称,请调用kobject_rename():
1
int kobject_rename(struct kobject *kobj, const char *new_name);
  1. 有一个辅助函数可以初始化kobject并将其添加到内核同时调用kobject_init_and_add():
1
2
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,
struct kobject *parent, const char *fmt, ...);
  1. 在kobject注册到kobject核心后,您需要通知它已被创造。 这可以通过调用kobject_uevent()来完成:
1
int kobject_uevent(struct kobject *kobj, enum kobject_action action);

当kobject从内核中删除时, KOBJ_REMOVE的uevent将由kobject核心自动调用,因此调用者不必担心手动执行此操作。

  1. kobject的关键功能之一是用作嵌入它的对象的引用计数器,用于操作kobject引用计数的函数是:
1
2
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);

当引用被释放时,对kobject_put()的调用将减少引用计数,并可能释放对象。

  1. 每个注册的kset对应sysfs目录,可以使用kset_create_and_add()函数创建和添加kset,使用kset_unregister()函数将其删除
1
2
3
4
kset * __must_check kset_create_and_add(const char *name,
const struct kset_uevent_ops *u,
struct kobject *parent_kobj)
void kset_unregister(struct kset *kset)

因为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
2
3
4
5
6
int device_register(struct device *dev)
{
device_initialize(dev);
return device_add(dev);
}
EXPORT_SYMBOL_GPL(device_register);

初始化设备并将其添加到系统中。其中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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct class {
const char *name; // class的名称
struct module *owner;

struct class_attribute *class_attrs; // class的默认属性
const struct attribute_group **dev_groups; // 属于该class的device的默认属性
struct kobject *dev_kobj; // 代表该class的层级关系

int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env); // 当有设备添加到class或从class移除时调用
char *(*devnode)(struct device *dev, umode_t *mode);

void (*class_release)(struct class *class); //class销毁时调用
void (*dev_release)(struct device *dev); //release一个device

int (*suspend)(struct device *dev, pm_message_t state); //休眠时调用
int (*resume)(struct device *dev); //唤醒时调用
int (*shutdown)(struct device *dev); //关机时调用

const struct kobj_ns_type_operations *ns_type;
const void *(*namespace)(struct device *dev);

const struct dev_pm_ops *pm; //属于该class的device的默认电源管理操作

struct subsys_private *p; //子系统,跟bus下的相同含义
};

其实这里class就是一个多设备容器,这个容器就是方便管理设备的(当然bus也是管理设备的数据结构,bus是管理同一总线上的设备和设备驱动的),class将设备进行分类管理(另一个维度,区别于bus)并提供api方便驱动开发人员进行管理设备。

Uevent

Uevent是Kobject的一部分,用于在Kobject状态发生改变时,例如增加、移除等,通知用户空间程序。用户空间程序收到这样的事件后,会做相应的处理。该机制通常是用来支持热拔插设备的,从而动态的支持该设备。

实现

uevent相关数据结构如下:原图

主要的实现函数就是kobject_uevent函数,如下:

1
2
3
4
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}

调用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设备驱动开发》