在 2.5 版本之前,内核没有办法描述和管理对象,代码的可重用性也没有像现在这样增强。换句话说,既没有设备拓扑,也没有组织。没有关于子系统关系的信息,也没有关于系统如何组合的信息。接下来是 Linux 设备模型 ( LDM ),介绍:
- 类的概念,将相同类型的设备或展示相同功能的设备分组(例如,鼠标和键盘都是输入设备)。
- 通过名为
sysfs
的虚拟文件系统与用户空间通信,以便让用户空间管理和枚举设备及其公开的属性。 - 使用引用计数(在托管资源中大量使用)管理对象生命周期。
- 电源管理,以处理设备关闭的顺序。
- 代码的可重用性。类和框架公开接口,表现得像任何向它们注册的驱动都必须遵守的契约。
- LDM 在内核中引入了一种类似于面向对象的编程风格。
在本章中,我们将利用 LDM,通过sysfs
文件系统将一些属性导出到用户空间。
在本章中,我们将涵盖以下主题:
- 介绍 LDM 数据结构(驱动、设备、总线)
- 按类型收集内核对象
- 处理内核
sysfs
界面
目标是构建一个完整的 DT 来映射系统中的每个物理设备,并介绍它们的层次结构。已经创建了一个通用的结构来表示可能是设备模型一部分的任何对象。LDM 的上层依赖于内核中表示为struct bus_type
实例的总线;由struct device_driver
结构表示的设备驱动,以及作为struct device
结构实例表示的最后一个元素的设备。在本节中,我们将设计一个总线驱动 packt bus,以便深入了解 LDM 数据结构和机制。
总线是设备和处理器之间的通道链路。管理总线并将其协议输出到设备的硬件实体称为总线控制器。例如,USB 控制器提供 USB 支持。I2C 控制器提供 I2C 总线支持。因此,总线控制器作为一个独立的设备,必须像任何设备一样进行注册。它将是需要坐在总线上的设备的父设备。换句话说,总线上的每台设备都必须有指向总线设备的父字段。总线在内核中由struct bus_type
结构表示:
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
struct device_attribute *dev_attrs; /* use dev_groups instead */
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *dev, struct device_driver *drv);
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);
const struct dev_pm_ops *pm;
struct subsys_private *p;
struct lock_class_key lock_key;
};
以下是结构中元素的含义:
match
:这是一个回调,每当总线上添加了新的设备或驱动时都会调用。回调必须足够智能,并且当设备和驱动匹配时应该返回非零值,两者都作为参数给出。match
回调的主要目的是允许总线确定给定的驱动或其他逻辑是否可以处理特定的设备,如果给定的驱动支持给定的设备。大多数情况下,验证是通过简单的字符串比较来完成的(设备和驱动名称,表和 DT 兼容属性)。对于枚举设备(PCI、USB),验证是通过将驱动支持的设备标识与给定设备的设备标识进行比较来完成的,而不会牺牲总线特定的功能。probe
:这是一个回调,当一个新的设备或驱动被添加到总线上,在匹配已经发生之后。该函数负责分配特定的总线设备结构,并调用给定驱动的probe
函数,该函数应该管理设备(之前分配的)。remove
:当一个设备要从总线上移除时,这被调用。suspend
:这是总线上的设备需要进入睡眠模式时调用的方法。resume
:当总线上的设备必须退出睡眠模式时,这种情况被称为休眠。pm
:这是总线的一组电源管理操作,会调用特定设备驱动的pm-ops
。drv_groups
:这是一个指向struct attribute_group
元素列表(数组)的指针,每个元素都有一个指向struct attribute
元素列表(数组)的指针。它代表总线上设备驱动的默认属性。传递到该字段的属性将被提供给在总线上注册的每个驾驶员。这些属性可以在/sys/bus/<bus-name>/drivers/<driver-name>
的驾驶员目录中找到。dev_groups
:表示总线上设备的默认属性。传递到该字段的属性(通过struct attribute_group
元素的列表/数组)将被给予在总线上注册的每个设备。这些属性可以在/sys/bus/<bus-name>/devices/<device-name>.
的设备目录中找到bus_group
:保存总线向核心注册时自动添加的默认属性集(组)。
除了定义bus_type
之外,总线控制器驱动还必须定义扩展通用struct device_driver
的总线专用驱动结构,以及扩展通用struct device
结构的总线专用设备结构,这两者都是设备模型核心的一部分。总线驱动还必须为探测时发现的每个物理设备分配特定于总线的设备结构,并负责初始化设备的bus
和parent
字段,以及向 LDM 核注册设备。这些字段必须指向总线设备和总线驱动中定义的bus_type
结构。LDM 核心使用它来构建设备层次结构并初始化其他字段。
在我们的示例中,下面是获取 packt 设备和 packt 驱动的两个助手宏,给定一个通用的struct device
和struct driver
:
#define to_packt_driver(d) container_of(d, struct packt_driver, driver)
#define to_packt_device(d) container_of(d, struct packt_device, dev)
然后是用于识别 packt 设备的结构:
struct packt_device_id {
char name[PACKT_NAME_SIZE];
kernel_ulong_t driver_data; /* Data private to the driver */
};
以下是 packt 专用设备和驱动结构:
/*
* Bus specific device structure
* This is what a packt device structure looks like
*/
struct packt_device {
struct module *owner;
unsigned char name[30];
unsigned long price;
struct device dev;
};
/*
* Bus specific driver structure
* This is what a packt driver structure looks like
* You should provide your device's probe and remove function.
* may be release too
*/
struct packt_driver {
int (*probe)(struct packt_device *packt);
int (*remove)(struct packt_device *packt);
void (*shutdown)(struct packt_device *packt);
};
每条总线内部管理两个重要列表;添加并驻留在其上的设备列表,以及向其注册的驱动列表。每当您在总线上添加/注册或删除/取消注册设备/驱动时,相应的列表都会用新条目进行更新。总线驱动必须提供帮助函数来注册/注销可以处理总线上的设备的设备驱动,以及注册/注销总线上的设备的帮助函数。这些辅助函数总是包装 LDM 核心提供的通用函数,它们是driver_register()
、device_register()
、driver_unregister
和device_unregister()
。
/*
* Now let us write and export symbols that people writing
* drivers for packt devices must use.
*/
int packt_register_driver(struct packt_driver *driver)
{
driver->driver.bus = &packt_bus_type;
return driver_register(&driver->driver);
}
EXPORT_SYMBOL(packt_register_driver);
void packt_unregister_driver(struct packt_driver *driver)
{
driver_unregister(&driver->driver);
}
EXPORT_SYMBOL(packt_unregister_driver);
int packt_device_register(struct packt_device *packt)
{
return device_register(&packt->dev);
}
EXPORT_SYMBOL(packt_device_register);
void packt_unregister_device(struct packt_device *packt)
{
device_unregister(&packt->dev);
}
EXPORT_SYMBOL(packt_device_unregister);
用于分配 packt 设备的功能如下。必须使用它来创建总线上任何物理设备的实例:
/*
* This function allocate a bus specific device structure
* One must call packt_device_register to register
* the device with the bus
*/
struct packt_device * packt_device_alloc(const char *name, int id)
{
struct packt_device *packt_dev;
int status;
packt_dev = kzalloc(sizeof *packt_dev, GFP_KERNEL);
if (!packt_dev)
return NULL;
/* new devices on the bus are son of the bus device */
strcpy(packt_dev->name, name);
packt_dev->dev.id = id;
dev_dbg(&packt_dev->dev,
"device [%s] registered with packt bus\n", packt_dev->name);
return packt_dev;
out_err:
dev_err(&adap->dev, "Failed to register packt client %s\n", packt_dev->name);
kfree(packt_dev);
return NULL;
}
EXPORT_SYMBOL_GPL(packt_device_alloc);
int packt_device_register(struct packt_device *packt)
{
packt->dev.parent = &packt_bus;
packt->dev.bus = &packt_bus_type;
return device_register(&packt->dev);
}
EXPORT_SYMBOL(packt_device_register);
总线控制器本身就是一个设备,在 99%的情况下,总线是平台设备(甚至是提供枚举的总线)。例如,PCI 控制器是一个平台设备,其各自的驱动也是。为了向内核注册总线,必须使用bus_register(struct *bus_type)
功能。packt 总线结构如下所示:
/*
* This is our bus structure
*/
struct bus_type packt_bus_type = {
.name = "packt",
.match = packt_device_match,
.probe = packt_device_probe,
.remove = packt_device_remove,
.shutdown = packt_device_shutdown,
};
总线控制器本身就是一个设备,它必须向内核注册,并将被用作位于总线上的设备的父设备。这是在总线控制器的probe
或init
功能中完成的。对于 packt 总线,代码如下:
/*
* Bus device, the master.
*
*/
struct device packt_bus = {
.release = packt_bus_release,
.parent = NULL, /* Root device, no parent needed */
};
static int __init packt_init(void)
{
int status;
status = bus_register(&packt_bus_type);
if (status < 0)
goto err0;
status = class_register(&packt_master_class);
if (status < 0)
goto err1;
/*
* After this call, the new bus device will appear
* under /sys/devices in sysfs. Any devices added to this
* bus will shows up under /sys/devices/packt-0/.
*/
device_register(&packt_bus);
return 0;
err1:
bus_unregister(&packt_bus_type);
err0:
return status;
}
当设备由总线控制器驱动注册时,设备的父成员必须指向总线控制器设备,并且其总线属性必须指向总线类型以构建物理 DT。要注册 packt 设备,必须调用packt_device_register
,作为分配给packt_device_alloc
的参数:
int packt_device_register(struct packt_device *packt)
{
packt->dev.parent = &packt_bus;
packt->dev.bus = &packt_bus_type;
return device_register(&packt->dev);
}
EXPORT_SYMBOL(packt_device_register);
全局设备层次结构允许系统中的每个设备以通用方式表示。这使得内核可以轻松地遍历 DT,创建适当有序的电源管理过渡:
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
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);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
};
struct device_driver
为核心定义一组简单的操作,以便在每个设备上执行这些操作:
* name
代表驾驶员姓名。通过与设备名称进行比较,它可以用于匹配。* bus
代表司机乘坐的公交车。公共汽车司机必须填写这个字段。module
表示拥有驱动的模块。在 99%的情况下,应该将该字段设置为THIS_MODULE
。of_match_table
是指向struct of_device_id
数组的指针。struct of_device_id
结构用于通过一个名为 DT 的特殊文件执行 OF 匹配,该文件在引导过程中被传递给内核:
struct of_device_id {
char compatible[128];
const void *data;
};
suspend
和resume
回调提供电源管理功能。当设备从系统中物理移除时,或者当其参考计数达到0
时,调用remove
回调。remove
回调也在系统重启期间调用。probe
是尝试将驱动绑定到设备时运行的探测回调。总线驱动负责调用设备驱动的probe
功能。group
是指向struct attribute_group
列表(数组)的指针,用作驱动的默认属性。使用此方法,而不是单独创建属性。
driver_register()
是用于向总线注册设备驱动的低级功能。它将驾驶员添加到公共汽车的驾驶员列表中。当设备驱动注册到总线时,内核遍历总线的设备列表,并为每个没有相关驱动的设备调用总线的匹配回调,以便找出驱动是否可以处理任何设备。
当匹配发生时,设备和设备驱动绑定在一起。将设备与设备驱动相关联的过程称为绑定。
现在回到司机注册我们的 packt 巴士,必须使用packt_register_driver(struct packt_driver *driver)
,这是driver_register()
的包装。在注册 packt 驱动之前,必须填写*driver
参数。LDM 内核提供了帮助函数,用于遍历向总线注册的驱动列表:
int bus_for_each_drv(struct bus_type * bus,
struct device_driver * start,
void * data, int (*fn)(struct device_driver *,
void *));
这个助手遍历总线的驱动列表,并为列表中的每个驱动调用fn
回调。
结构设备是用于描述和表征系统上每个设备的通用数据结构,无论它是否是物理设备。它包含有关设备物理属性的详细信息,并提供适当的链接信息来构建合适的设备树和参考计数:
struct device {
struct device *parent;
struct kobject kobj;
const struct device_type *type;
struct bus_type *bus;
struct device_driver *driver;
void *platform_data;
void *driver_data;
struct device_node *of_node;
struct class *class;
const struct attribute_group **groups;
void (*release)(struct device *dev);
};
-
* parent
代表设备的父设备,用于构建设备树层次结构。当向总线注册时,总线驱动负责向总线设备设置该字段。 -
* bus
代表设备所在的总线。公共汽车司机必须填写这个字段。 -
* type
标识设备类型。 -
kobj
是句柄引用计数和设备模型支持中的 kobject。 -
* of_node
是指向与设备相关联的 OF (DT)节点的指针。由公共汽车司机来设置这个字段。 -
platform_data
是设备专用平台数据的指针。通常在设备供应期间在特定于板的文件中声明。 -
driver_data
是驱动私有数据的指针。 -
class
是设备所属类的指针。 -
* group
是指向struct attribute_group
列表(数组)的指针,用作设备的默认属性。使用此方法,而不是单独创建属性。 -
release
是当设备引用计数达到零时调用的回调。公交有责任设置该字段。packt 公共汽车司机向您展示了如何做到这一点。
device_register
是 LDM 核提供的向总线注册设备的功能。在这个调用之后,驱动的总线列表被迭代以找到支持这个设备的驱动,然后这个设备被添加到总线的设备列表中。device_register()
内部通话device_add()
:
int device_add(struct device *dev)
{
[...]
bus_probe_device(dev);
if (parent)
klist_add_tail(&dev->p->knode_parent,
&parent->p->klist_children);
[...]
}
内核提供的迭代总线设备列表的助手函数是bus_for_each_dev
:
int bus_for_each_dev(struct bus_type * bus,
struct device * start, void * data,
int (*fn)(struct device *, void *));
每当添加设备时,内核都会调用总线驱动的匹配方法(bus_type->match
)。如果匹配函数表示该设备有驱动,内核将调用总线驱动(bus_type->probe
)的probe
功能,给定设备和驱动作为参数。然后由总线驱动调用设备驱动的probe
方法(driver->probe
)。对于我们的 packt 总线驱动,用来注册设备的函数是packt_device_register(struct packt_device *packt)
,内部调用device_register
,其中参数是一个分配了packt_device_alloc
的 packt 设备。
木头下面的 LDM 依赖于三个重要的结构,它们是 kobj,kobj_type 和 kset。让我们看看这些结构是如何包含在设备模型中的。
kobject 是设备模型的核心,在幕后运行。它给内核带来了一种类似 OO 的编程风格,主要用于引用计数和公开设备层次以及它们之间的关系。kobjects 引入了通用对象属性封装的概念,例如使用引用计数:
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct sysfs_dirent *sd;
struct kref kref;
/* Fields out of our interest have been removed */
};
name
指向这个 kobject 的名称。您可以使用kobject_set_name(struct kobject *kobj, const char *name)
功能对此进行更改。parent
是指向这个 kobject 的父对象的指针。它用于构建一个层次结构来描述对象之间的关系。sd
指向一个struct sysfs_dirent
结构,该结构表示 sysfs 结构内部 sysfs 索引节点中的这个 kobject。kref
提供对 kobject 的引用计数。ktype
描述对象,kset
告诉我们这个对象属于哪一组(组)对象。
嵌入 kobject 的每个结构都被嵌入并接收 kobject 提供的标准化函数。嵌入的 ko object 将使该结构成为对象层次结构的一部分。
container_of
宏用于获取 kobject 所属对象的指针。每个内核设备都直接或间接嵌入一个 kobject 属性。在添加到系统之前,必须使用kobject_create()
函数分配 koobject,该函数将返回一个空 koobject,必须使用kobj_init()
初始化该 koobject,给定已分配和未初始化的 koobject 指针及其kobj_type
指针作为参数:
struct kobject *kobject_create(void)
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
kobject_add()
功能用于向系统添加和链接一个 kobject,同时根据其层次结构和默认属性创建其目录。反向功能为kobject_del()
:
int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...);
kobject_create
和kobject_add
的反函数都是kobject_put
。在随书提供的源代码中,将 kobject 与系统联系起来的摘录如下:
/* Somewhere */
static struct kobject *mykobj;
mykobj = kobject_create();
if (mykobj) {
kobject_init(mykobj, &mytype);
if (kobject_add(mykobj, NULL, "%s", "hello")) {
err = -1;
printk("ldm: kobject_add() failed\n");
kobject_put(mykobj);
mykobj = NULL;
}
err = 0;
}
人们可以使用kobject_create_and_add
,内部称之为kobject_create and kobject_add
。drivers/base/core.c
下面的摘录展示了如何使用它:
static struct kobject * class_kobj = NULL;
static struct kobject * devices_kobj = NULL;
/* Create /sys/class */
class_kobj = kobject_create_and_add("class", NULL);
if (!class_kobj) {
return -ENOMEM;
}
/* Create /sys/devices */
devices_kobj = kobject_create_and_add("devices", NULL);
if (!devices_kobj) {
return -ENOMEM;
}
If a kobject has a NULL
parent, then kobject_add
sets parent to kset. If both are NULL
, object becomes a child-member of the top-level sys directory
一个struct kobj_type
结构描述了对象的行为。kobj_type
结构描述了通过ktype
字段嵌入 kobject 的对象类型。每一个嵌入 koobject 的结构都需要一个对应的kobj_type
,它将控制 koobject 被创建和销毁时以及属性被读取或写入时会发生什么。每个 kobject 都有一个类型为struct kobj_type
的字段,代表内核对象类型:
struct kobj_type {
void (*release)(struct kobject *);
const struct sysfs_ops sysfs_ops;
struct attribute **default_attrs;
};
一个struct kobj_type
结构允许内核对象共享共同的操作(sysfs_ops
),不管这些对象在功能上是否相关。这种结构的字段足够有意义。release
是一个回调函数,当你的对象需要被释放时,它会被kobject_put()
函数调用。你必须在这里释放你的对象持有的内存。可以使用container_of
宏来获取指向对象的指针。sysfs_ops
字段指向 sysfs 操作,而default_attrs
定义了与此 kobject 关联的默认属性。sysfs_ops
是访问 sysfs 属性时调用的一组回调(sysfs 操作)。default_attrs
是一个指向struct attribute
元素列表的指针,这些元素将用作该类型每个对象的默认属性:
struct sysfs_ops {
ssize_t (*show)(struct kobject *kobj,
struct attribute *attr, char *buf);
ssize_t (*store)(struct kobject *kobj,
struct attribute *attr,const char *buf,
size_t size);
};
show
是读取任何具有此kobj_type
的 kobject 的属性时调用的回调。缓冲区的长度总是PAGE_SIZE
,即使要显示的值是简单的char
。应该设置buf
的值(使用scnprintf
,成功时返回实际写入缓冲区的数据大小(以字节为单位),失败时返回负错误。store
用于写目的。其buf
参数最多PAGE_SIZE
但可以小一些。它返回成功时从缓冲区实际读取的数据大小(以字节为单位)或失败时的负错误(或者如果收到不需要的值)。可以使用get_ktype
获取给定对象的kobj_type
:
struct kobj_type *get_ktype(struct kobject *kobj);
在书中的例子中,我们的k_type
变量代表我们的 kobject 的类型:
static struct sysfs_ops s_ops = {
.show = show,
.store = store,
};
static struct kobj_type k_type = {
.sysfs_ops = &s_ops,
.default_attrs = d_attrs,
};
这里show
和store
回调的定义如下:
static ssize_t show(struct kobject *kobj, struct attribute *attr, char *buf)
{
struct d_attr *da = container_of(attr, struct d_attr, attr);
printk( "LDM show: called for (%s) attr\n", da->attr.name );
return scnprintf(buf, PAGE_SIZE,
"%s: %d\n", da->attr.name, da->value);
}
static ssize_t store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t len)
{
struct d_attr *da = container_of(attr, struct d_attr, attr);
sscanf(buf, "%d", &da->value);
printk("LDM store: %s = %d\n", da->attr.name, da->value);
return sizeof(int);
}
内核对象集(kset)主要是将相关的内核对象组合在一起。ksets 是 kobjects 的集合。换句话说,kset 将相关的 kobjects 收集到一个地方,例如所有块设备:
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
};
list
是 kset 中所有 kobjects 的链表list_lock
是保护链表访问的自旋锁kobj
表示集合的基类
每个注册的(添加到系统中的)kset 对应一个 sysfs 目录。可以使用kset_create_and_add()
功能创建和添加 kset,使用kset_unregister()
功能删除 kset:
struct kset * kset_create_and_add(const char *name,
const struct kset_uevent_ops *u,
struct kobject *parent_kobj);
void kset_unregister (struct kset * k);
向集合中添加 kobject 就像在右侧 kset 中指定其 kset 字段一样简单:
static struct kobject foo_kobj, bar_kobj;
example_kset = kset_create_and_add("kset_example", NULL, kernel_kobj);
/*
* since we have a kset for this kobject,
* we need to set it before calling the kobject core.
*/
foo_kobj.kset = example_kset;
bar_kobj.kset = example_kset;
retval = kobject_init_and_add(&foo_kobj, &foo_ktype,
NULL, "foo_name");
retval = kobject_init_and_add(&bar_kobj, &bar_ktype,
NULL, "bar_name");
现在在模块exit
功能中,在 kobject 及其属性被移除后:
kset_unregister(example_kset);
属性是由 kobjects 导出到用户空间的 sysfs 文件。属性表示可以从用户空间读取、写入或同时读取和写入的对象属性。也就是说,每个嵌入结构 koobject 的数据结构都可以公开 koobject 本身提供的默认属性(如果有的话),或者自定义属性。换句话说,属性将内核数据映射到 sysfs 中的文件。
属性定义如下所示:
struct attribute {
char * name;
struct module *owner;
umode_t mode;
};
用于在文件系统中添加/删除属性的内核函数有:
int sysfs_create_file(struct kobject * kobj,
const struct attribute * attr);
void sysfs_remove_file(struct kobject * kobj,
const struct attribute * attr);
让我们尝试定义将导出的两个属性,每个属性由一个属性表示:
struct d_attr {
struct attribute attr;
int value;
};
static struct d_attr foo = {
.attr.name="foo",
.attr.mode = 0644,
.value = 0,
};
static struct d_attr bar = {
.attr.name="bar",
.attr.mode = 0644,
.value = 0,
};
要单独创建每个枚举属性,我们必须调用以下内容:
sysfs_create_file(mykobj, &foo.attr);
sysfs_create_file(mykobj, &bar.attr);
从属性开始的好地方是内核源码中的samples/kobject/kobject-example.c
。
到目前为止,我们已经看到了如何单独添加属性并在每个属性上调用(直接或间接通过包装函数,如device_create_file()
、class_create_file()
等)sysfs_create_file()
。如果我们能打一次电话,为什么还要打多次呢?这里是属性组的来源。它依赖于struct attribute_group
结构:
struct attribute_group {
struct attribute **attrs;
};
当然,我们已经删除了不感兴趣的字段。attr
s 字段是指向NULL
终止属性列表的指针。每个属性组必须有一个指向struct attribute
元素列表/数组的指针。组只是一个帮助器包装器,使管理多个属性变得更加容易。
用于向文件系统添加/删除组属性的内核函数有:
int sysfs_create_group(struct kobject *kobj,
const struct attribute_group *grp)
void sysfs_remove_group(struct kobject * kobj,
const struct attribute_group * grp)
前面定义的两个属性可以嵌入到一个struct attribute_group
中,只需调用一次就可以将它们都添加到系统中:
static struct d_attr foo = {
.attr.name="foo",
.attr.mode = 0644,
.value = 0,
};
static struct d_attr bar = {
.attr.name="bar",
.attr.mode = 0644,
.value = 0,
};
/* attrs is a pointer to a list (array) of attributes */
static struct attribute * attrs [] =
{
&foo.attr,
&bar.attr,
NULL,
};
static struct attribute_group my_attr_group = {
.attrs = attrs,
};
这里唯一需要调用的函数是:
sysfs_create_group(mykobj, &my_attr_group);
这比调用每个属性要好得多。
Sysfs
是一个非持久的虚拟文件系统,它提供了系统的全局视图,并通过内核对象的 kobjects 公开了内核对象的层次结构(拓扑)。每个 kobjects 显示为一个目录,目录中的文件表示内核变量,由相关的 kobject 导出。这些文件称为属性,可以读取或写入。
如果任何注册的 koobject 在 sysfs 中创建了一个目录,那么创建目录的位置取决于 koobject 的父对象(它也是一个 koobject)。很自然,目录是作为 kobject 父目录的子目录创建的。这突出了用户空间的内部对象层次结构。sysfs 中的顶级目录代表对象层次结构的公共祖先,即对象所属的子系统。
顶级 sysfs 目录可以在/sys/
目录下找到:
/sys$ tree -L 1
├── block
├── bus
├── class
├── dev
├── devices
├── firmware
├── fs
├── hypervisor
├── kernel
├── module
└── power
block
包含系统上每个块设备的目录,每个目录包含设备上分区的子目录。bus
包含系统上已注册的总线。dev
包含以原始方式注册的设备节点(没有层次结构),每个节点都是/sys/devices
目录中真实设备的符号链接。devices
给出系统中设备的拓扑视图。firmware
显示了系统特定的低级子系统树,如:ACPI、电喷、OF (DT)。fs
列出系统上实际使用的文件系统。kernel
保存内核配置选项和状态信息。Modules
是已加载模块的列表。
这些目录中的每一个都对应一个 kobject,其中一些被导出为内核符号。这些是:
kernel_kobj
对应/sys/kernel
power_kobj
为/sys/power
firmware_kobj
代表/sys/firmware
,在drivers/base/firmware.c
源文件中导出hypervisor_kobj
为/sys/hypervisor
,出口于drivers/base/hypervisor.c
fs_kobj
对应于/sys/fs
,在fs/namespace.c
文件中导出
但是,class/
、dev/
、devices/
是在引导时通过内核源码中drivers/base/core.c
的devices_init
函数创建的,block/
是在block/genhd.c
中创建的,bus/
是在drivers/base/bus.c
中作为 kset 创建的。
当一个 koobject 目录被添加到 sysfs(使用kobject_add
)时,它的添加位置取决于 koobject 的父位置。如果设置了父指针,它将作为子目录添加到父目录中。如果父指针为空,则添加为kset->kobj
内的子目录。如果既没有设置父字段也没有设置 kset 字段,它将映射到 sysfs ( /sys
)中的根目录。
可以使用sysfs_{create|remove}_link
功能在现有对象(目录)上创建/删除符号链接:
int sysfs_create_link(struct kobject * kobj,
struct kobject * target, char * name);
void sysfs_remove_link(struct kobject * kobj, char * name);
这将允许一个对象存在于多个位置。创建函数将创建一个名为name
的符号链接,指向target
koobject sysfs 条目。一个众所周知的例子是出现在/sys/bus
和/sys/devices
的设备。创建的符号链接即使在target
移除后也将保持不变。你要知道target
什么时候拆下,然后拆下对应的符号链接。
现在我们知道默认文件集是通过 kobjects 和 kset 中的 ktype 字段,通过kobj_type
的default_attrs
字段提供的。在大多数情况下,默认属性就足够了。但是有时一个 ktype 的实例可能需要它自己的属性来提供数据或功能,而不是一个更一般的 ktype 所共享的。
回想一下,用于在默认设置上添加/删除新属性(或属性组)的低级功能是:
int sysfs_create_file(struct kobject *kobj,
const struct attribute *attr);
void sysfs_remove_file(struct kobject *kobj,
const struct attribute *attr);
int sysfs_create_group(struct kobject *kobj,
const struct attribute_group *grp);
void sysfs_remove_group(struct kobject * kobj,
const struct attribute_group * grp);
sysfs 中当前存在接口层。除了创建自己的 ktype 或 kobject 来添加属性之外,您还可以使用当前存在的属性:设备、驱动、总线和类属性。他们的描述如下:
除了设备结构中嵌入的 kobject 提供的默认属性之外,您还可以创建自定义属性。用于此目的的结构是struct device_attribute
,它只不过是围绕标准struct attribute
的一个包装,以及一组显示/存储属性值的回调:
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev,
struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count);
};
他们的声明是通过DEVICE_ATTR
宏完成的:
DEVICE_ATTR(_name, _mode, _show, _store);
每当您使用DEVICE_ATTR
声明设备属性时,前缀dev_attr_
会添加到属性名称中。例如,如果您使用设置为 foo 的_name
参数声明一个属性,该属性将可以通过dev_attr_foo
变量名访问。
为了理解为什么,让我们看看include/linux/device.h
中DEVICE_ATTR
宏是如何定义的:
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
最后,您可以使用device_create_file
和device_remove_file
功能添加/删除这些:
int device_create_file(struct device *dev,
const struct device_attribute * attr);
void device_remove_file(struct device *dev,
const struct device_attribute * attr);
下面的示例演示了如何将所有这些放在一起:
static ssize_t foo_show(struct device *child,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", foo_value);
}
static ssize_t bar_show(struct device *child,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", bar_value);
}
以下是属性的静态声明:
static DEVICE_ATTR(foo, 0644, foo_show, NULL);
static DEVICE_ATTR(bar, 0644, bar_show, NULL);
以下代码显示了如何在系统上实际创建文件:
if ( device_create_file(dev, &dev_attr_foo) != 0 )
/* handle error */
if ( device_create_file(dev, &dev_attr_bar) != 0 )
/* handle error*/
对于清理,属性移除在移除功能中完成,如下所示:
device_remove_file(wm->dev, &dev_attr_foo);
device_remove_file(wm->dev, &dev_attr_bar);
您可能想知道,我们过去是如何以及为什么为同一 koobject/kt type 的所有属性定义相同的存储/显示回调集的,现在,我们为每个属性使用一个自定义回调集。第一个原因是因为,设备子系统定义了自己的属性结构,它包装了标准的属性结构,其次,它不是显示/存储属性的值,而是使用container_of
宏提取struct device_attribute
给出一个通用的struct attribute
,然后根据用户的动作执行显示/存储回调。以下是drivers/base/core.c
的摘录,显示了设备对象的sysfs_ops
:
static ssize_t dev_attr_show(struct kobject *kobj,
struct attribute *attr,
char *buf)
{
struct device_attribute *dev_attr = to_dev_attr(attr);
struct device *dev = kobj_to_dev(kobj);
ssize_t ret = -EIO;
if (dev_attr->show)
ret = dev_attr->show(dev, dev_attr, buf);
if (ret >= (ssize_t)PAGE_SIZE) {
print_symbol("dev_attr_show: %s returned bad count\n",
(unsigned long)dev_attr->show);
}
return ret;
}
static ssize_t dev_attr_store(struct kobject *kobj, struct attribute *attr,
const char *buf, size_t count)
{
struct device_attribute *dev_attr = to_dev_attr(attr);
struct device *dev = kobj_to_dev(kobj);
ssize_t ret = -EIO;
if (dev_attr->store)
ret = dev_attr->store(dev, dev_attr, buf, count);
return ret;
}
static const struct sysfs_ops dev_sysfs_ops = {
.show = dev_attr_show,
.store = dev_attr_store,
};
公交(在drivers/base/bus.c
)、司机(在drivers/base/bus.c
)和班级(在drivers/base/class.c
)属性的原理是一样的。他们使用container_of
宏提取自己特定的属性结构,然后调用嵌入其中的 show/store 回调。
它依赖于struct bus_attribute
结构:
struct bus_attribute {
struct attribute attr;
ssize_t (*show)(struct bus_type *, char * buf);
ssize_t (*store)(struct bus_type *, const char * buf, size_t count);
};
总线属性使用BUS_ATTR
宏声明:
BUS_ATTR(_name, _mode, _show, _store)
使用BUS_ATTR
声明的任何总线属性都将前缀bus_attr_
添加到属性变量名中:
#define BUS_ATTR(_name, _mode, _show, _store) \
struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)
使用bus_{create|remove}_file
功能创建/删除它们:
int bus_create_file(struct bus_type *, struct bus_attribute *);
void bus_remove_file(struct bus_type *, struct bus_attribute *);
使用的结构为struct driver_attribute
:
struct driver_attribute {
struct attribute attr;
ssize_t (*show)(struct device_driver *, char * buf);
ssize_t (*store)(struct device_driver *, const char * buf,
size_t count);
};
该声明依赖于DRIVER_ATTR
宏,该宏将以driver_attr_
作为属性变量名的前缀:
DRIVER_ATTR(_name, _mode, _show, _store)
宏观定义是:
#define DRIVER_ATTR(_name, _mode, _show, _store) \
struct driver_attribute driver_attr_##_name = __ATTR(_name, _mode, _show, _store)
创建/删除依赖于driver_{create|remove}_file
功能:
int driver_create_file(struct device_driver *,
const struct driver_attribute *);
void driver_remove_file(struct device_driver *,
const struct driver_attribute *);
struct class_attribute
是基础结构:
struct class_attribute {
struct attribute attr;
ssize_t (*show)(struct device_driver *, char * buf);
ssize_t (*store)(struct device_driver *, const char * buf,
size_t count);
};
类属性的声明依赖于CLASS_ATTR
:
CLASS_ATTR(_name, _mode, _show, _store)
正如宏的定义所示,任何用CLASS_ATTR
声明的类属性都将前缀class_attr_
添加到属性变量名中:
#define CLASS_ATTR(_name, _mode, _show, _store) \
struct class_attribute class_attr_##_name = __ATTR(_name, _mode, _show, _store)
最后,文件的创建和删除通过class_{create|remove}_file
功能完成:
int class_create_file(struct class *class,
const struct class_attribute *attr);
void class_remove_file(struct class *class,
const struct class_attribute *attr);
Notice that device_create_file()
, bus_create_file()
, driver_create_file()
, and class_create_file()
all make an internal call to sysfs_create_file()
. As they all are kernel objects, they have a kobject
embedded into their structure. That kobject
is then passed as a parameter to sysfs_create_file
, as you can see as follows:
int device_create_file(struct device *dev,
const struct device_attribute *attr)
{
[...]
error = sysfs_create_file(&dev->kobj, &attr->attr);
[...]
}
int class_create_file(struct class *cls,
const struct class_attribute *attr)
{
[...]
error =
sysfs_create_file(&cls->p->class_subsys.kobj,
&attr->attr);
return error;
}
int bus_create_file(struct bus_type *bus,
struct bus_attribute *attr)
{
[...]
error =
sysfs_create_file(&bus->p->subsys.kobj,
&attr->attr);
[...]
}
这里我们将看到如何不使 CPU 浪费轮询来感知 sysfs 属性数据可用性。想法是使用poll
或select
系统调用来等待属性内容的改变。使 sysfs 属性可修改的补丁是由尼尔·布朗和格雷格·克罗-哈特曼创建的。kobject 管理器(有权访问 kobject 的驱动)必须支持通知,以便在内容更改时允许poll
或select
返回(被释放)。达到目的的神奇功能来自内核端,就是sysfs_notify()
:
void sysfs_notify(struct kobject *kobj, const char *dir,
const char *attr)
如果dir
参数非空,则用于查找子目录,该子目录包含属性(推测由sysfs_create_group
创建)。每个属性一个int
,每个对象一个wait_queuehead
,每个打开的文件一个 int。
poll
将返回POLLERR|POLLPRI
,select
将返回 fd,无论它是在等待读取、写入还是异常。阻止投票是从用户的角度进行的。sysfs_notify()
应该只有在你调整了你的内核属性值之后才会被调用。
Think of the poll()
(or select()
) code as a subscriber to notice a change in an attribute of interest, and sysfs_notify()
as a publisher, notifying subscribers of any changes.
以下是随书提供的代码摘录,它是属性的存储函数:
static ssize_t store(struct kobject *kobj, struct attribute *attr,
const char *buf, size_t len)
{
struct d_attr *da = container_of(attr, struct d_attr, attr);
sscanf(buf, "%d", &da->value);
printk("sysfs_foo store %s = %d\n", a->attr.name, a->value);
if (strcmp(a->attr.name, "foo") == 0){
foo.value = a->value;
sysfs_notify(mykobj, NULL, "foo");
}
else if(strcmp(a->attr.name, "bar") == 0){
bar.value = a->value;
sysfs_notify(mykobj, NULL, "bar");
}
return sizeof(int);
}
来自用户空间的代码必须像这样来感知数据变化:
- 打开文件属性。
- 对所有内容进行虚拟阅读。
- 呼叫轮询请求
POLLERR|POLLPRI
(选择/排除也有效)。 - 当
poll
(或select
)返回时(表示某个值发生了变化),读取数据发生变化的文件内容。 - 关闭文件并转到循环的顶部。
当怀疑 sysfs 属性可被轮询时,设置一个合适的超时值。用户空间示例与书籍示例一起提供。
现在,您已经熟悉了 LDM 概念及其数据结构(总线、类、设备驱动和设备),包括低级数据结构(即kobject
、kset
、kobj_types
)和属性(或其组合),对象如何在内核中表示(因此 sysfs 和设备拓扑)不再是秘密。您将能够创建一个属性(或组),通过 sysfs 公开您的设备或驱动功能。如果上一个话题你看得很清楚,我们将进入下一个第 14 章、引脚控制和 GPIO 子系统,大量使用sysfs
的力量。