LV200-自定义总线
已经学习完 kobject 相关的东西了,来注册一个自己的总线?
在设备模型中,包含总线、设备、驱动和类四个概念。在前面的笔记中,学习了设备模型的基本框架 kobject 和 kset。这一节,我们来注册一个自己的总线。
一、总线
其实下面这些大部分在《LV005-设备模型简介》这一节中都已经大概了解过了,这里简单回顾一下吧。
1. 总线简介
1.1 总线与平台总线
在设备驱动模型中, 引入总线的概念可以对驱动代码和设备信息进行分离。但是驱动中总线的概念是软件层面的一种抽象,与我们 SOC 中物理总线的概念并不严格相等:
- 物理总线:芯片与各个功能外设之间传送信息的公共通信干线,其中又包括数据总线、地址总线和控制总线,以此来传输各种通信时序。
- 驱动总线:负责管理设备和驱动。制定设备和驱动的匹配规则,一旦总线上注册了新的设备或者是新的驱动,总线将尝试为它们进行配对。
总线是连接处理器和设备之间的桥梁,总线代表着同类设备需要共同遵守的工作时序,是连接处理器和设备之间的桥梁。我们接触到的设备大部分是依靠总线来进行通信的, 它们之间的物理连接如图所示:

总线驱动则负责实现总线的各种行为,其管理着两个链表,分别是添加到该总线的设备链表以及注册到该总线的驱动链表。当我们向总线添加(移除)一个设备(驱动)时,便会在对应的列表上添加新的节点, 同时对挂载在该总线的驱动以及设备进行匹配,在匹配过程中会忽略掉那些已经有驱动匹配的设备。

一般对于 I2C、SPI、USB 这些常见类型的物理总线来说,Linux 内核会自动创建与之相应的驱动总线,因此 I2C 设备、SPI 设备、 USB 设备自然是注册挂载在相应的总线上。但是,实际项目开发中还有很多结构简单的设备,对它们进行控制并不需要特殊的时序。 它们也就没有相应的物理总线,比如 led、rtc 时钟、蜂鸣器、按键等等,Linux 内核将不会为它们创建相应的驱动总线。
为了使这部分设备的驱动开发也能够遵循设备驱动模型,Linux 内核引入了一种虚拟的总线——平台总线( platform bus )。 平台总线用于管理、挂载那些没有相应物理总线的设备,这些设备被称为 平台设备,对应的设备驱动则被称为 平台驱动。 平台设备驱动的核心依然是 Linux 设备驱动模型,平台设备使用 platform_device 结构体来进行表示,其继承了设备驱动模型中的 device 结构体。 而平台驱动使用 platform_driver 结构体来进行表示,其则是继承了设备驱动模型中的 device_driver 结构体。
1.2 struct bus_type
在内核中使用结构体 bus_type 来表示总线,它定义在:device.h - include/linux/device.h - struct bus_type
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
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 (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*online)(struct device *dev);
int (*offline)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
int (*num_vf)(struct device *dev);
int (*dma_configure)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p;
struct lock_class_key lock_key;
bool need_parent_lock;
};- name : 指定总线的名称,当新注册一种总线类型时,会在/sys/bus 目录创建一个新的目录,目录名就是该参数的值;
- drv_groups、dev_groups、bus_groups : 分别表示驱动、设备以及总线的属性。这些属性可以是内部变量、字符串等等。通常会在对应的/sys 目录下在以文件的形式存在,对于驱动而言,在目录
/sys/bus/<bus-name>/driver/<driver-name>存放了设备的默认属性;设备则在目录/sys/bus/<bus-name>/devices/<driver-name>中。这些文件一般是可读写的,用户可以通过读写操作来获取和设置这些 attribute 的值。 - match : 当向总线注册一个新的设备或者是新的驱动时,会调用该回调函数。该回调函数主要负责判断是否有注册了的驱动适合新的设备,或者新的驱动能否驱动总线上已注册但没有驱动匹配的设备;
- uevent : 总线上的设备发生添加、移除或者其它动作时,就会调用该函数,来通知驱动做出相应的对策。
- probe : 当总线将设备以及驱动相匹配之后,执行该回调函数, 最终会调用驱动提供的 probe 函数。
- remove : 当设备从总线移除时,调用该回调函数;
- suspend、resume : 电源管理的相关函数,当总线进入睡眠模式时,会调用 suspend 回调函数;而 resume 回调函数则是在唤醒总线的状态下执行;
- pm : 电源管理的结构体,存放了一系列跟总线电源管理有关的函数,与 device_driver 结构体中的 pm_ops 有关;
- p : 该结构体用于存放特定的私有数据,其成员 klist_devices 和 klist_drivers 记录了挂载在该总线的设备和驱动;
其实上面哪些注释里面都有。
1.3 总线的注册与注销
1.3.1 bus_register()
在实际编写 linux 驱动模块时,Linux 内核已经为我们写好了大部分总线驱动,正常情况下我们一般不会去注册一个新的总线, 内核中提供了 bus_register 函数来注册总线,以及 bus_unregister 函数来注销总线,它定义在 bus.c - drivers/base/bus.c - bus_register
/**
* bus_register - register a driver-core subsystem
* @bus: bus to register
*
* Once we have that, we register the bus with the kobject
* infrastructure, then register the children subsystems it has:
* the devices and drivers that belong to the subsystem.
*/
int bus_register(struct bus_type *bus);【参数说明】
- bus: bus_type 类型的结构体指针
【返回值】
- 成功: 0
- 失败: 负数
1.3.2 bus_unregister()
对应的,我们注销一个总线驱动,可以通过 bus_unregister()函数,它定义在:bus.c - drivers/base/bus.c - bus_unregister
/**
* bus_unregister - remove a bus from the system
* @bus: bus.
*
* Unregister the child subsystems and the bus itself.
* Finally, we call bus_put() to release the refcount
*/
void bus_unregister(struct bus_type *bus);【参数说明】
- bus : bus_type 类型的结构体指针
【返回值】 无
1.4 总结
当我们成功注册总线时,会在/sys/bus/目录下创建一个新目录,目录名为我们新注册的总线名。bus 目录中包含了当前系统中已经注册了的所有总线,例如 i2c,spi,platform 等。我们看到每个总线目录都拥有两个子目录 devices 和 drivers, 分别记录着挂载在该总线的所有设备以及驱动。
sumu@sumu-virtual-machine:/sys$ tree bus -L 2
bus
├── ac97
|#...
├── i2c
│ ├── devices
│ ├── drivers
│ ├── drivers_autoprobe
│ ├── drivers_probe
│ └── uevent
|#...
├── spi
│ ├── devices
│ ├── drivers
│ ├── drivers_autoprobe
│ ├── drivers_probe
│ └── uevent
├── usb
│ ├── devices
│ ├── drivers
│ ├── drivers_autoprobe
│ ├── drivers_probe
│ └── uevent
|#...
133 directories, 135 files2. 总线属性文件
2.1 BUS_ATTR()
BUS_ATTR()是一个宏,它定义在 device.h - include/linux/device.h - BUS_ATTR:
#define BUS_ATTR(_name, _mode, _show, _store) \
struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)
#define BUS_ATTR_RW(_name) \
struct bus_attribute bus_attr_##_name = __ATTR_RW(_name)
#define BUS_ATTR_RO(_name) \
struct bus_attribute bus_attr_##_name = __ATTR_RO(_name)struct bus_attribute 定义在 device.h - include/linux/device.h - struct bus_attribute
struct bus_attribute {
struct attribute attr;
ssize_t (*show)(struct bus_type *bus, char *buf);
ssize_t (*store)(struct bus_type *bus, const char *buf, size_t count);
};2.2 bus_create_file()
bus_create_file()函数,声明在 device.h - include/linux/device.h - bus_create_file
extern int __must_check bus_create_file(struct bus_type *,
struct bus_attribute *);参数说明:
- bus:指向总线类型结构体 struct bus_type 的指针,表示要创建属性文件的总线。
- attr:指向属性 struct bus_attribute 的指针,表示要创建的属性文件的属性。
使用 bus_create_file()函数,会在
/sys/bus/<bus-name>下创建对应的文件。
2.3 bus_remove_file()
bus_remove_file()函数,声明在 device.h - include/linux/device.h - bus_remove_file:
extern void bus_remove_file(struct bus_type *, struct bus_attribute *);2.4 使用实例
在调用 bus_create_file() 函数之前,需要先定义好属性结构体 struct bus_attribute ,并将其相关字段填充好。有两种方式,一种是直接定义:
struct bus_attribute sbus_attr_name_var = {
.attr = {
.name = "sbus_attr_name", // 属性的名称, 将会显示在/sys/bus/<bus-name>/ 下,并可以使用 cat/echo 进行操作
.mode = 0664, // 属性的访问权限
},
.show = sbus_attr_name_show, // 属性的 show 回调函数
.store = sbus_attr_name_store, // 属性的 show 回调函数
};
ret = bus_create_file(&sbus, &sbus_attr_name_var);另一种是通过宏来定义:
BUS_ATTR(name_var, S_IRUSR, sbus_attr_name_show, sbus_attr_name_store); // 设置该文件的文件权限为文件拥有者可读,组内成员以及其他成员不可操作
ret = bus_create_file(&sbus, &bus_attr_name_var);show 和 store 函数实现如下:
static ssize_t sbus_attr_name_show(struct bus_type *bus, char *buf)
{
ssize_t count = 0;
count = sprintf(buf, "%s\n", "attr");
PRT("bus->name=%s count=%d\n", bus->name, count);
return count;
}
static ssize_t sbus_attr_name_store(struct bus_type *bus, const char *buf, size_t count)
{
PRT("bus->name=%s count=%d\n", bus->name, count);
sscanf(buf, "%s\n", attr);
return count;
}3. 总线注册 demo
3.1 demo 源码
源码可以看这里,这里有两个,一个是只创建总线,另一个还为总线添加了属性文件操作。
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */
#include <linux/kobject.h>
#include <linux/slab.h>
#include <linux/device.h>
#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"
// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif
#define SBUS_NAME "sbus" // 总线名称, /sys/bus 中将会显示这个名字
typedef struct __SBUS_ATTR_VAR_{
char name_attr[32];
}sbus_attr_var_t;
sbus_attr_var_t sbus_attr = {0}; // sbus 的属性
/**
* @brief sbus_match()
* @note 负责总线下的设备以及驱动匹配, 使用字符串比较的方式,通过对比驱动以及设备的名字来确定是否匹配,如果相同, 则说明匹配成功
* @param [in]
* @param [out]
* @retval 匹配成功,返回 1;反之,则返回 0,这里应该是驱动内部有判断的地方,若是成功不是返回 1,则不会执行
* 驱动中的 probe 函数
*/
static int sbus_match(struct device *dev, struct device_driver *drv)
{
const char *device_name = dev_name(dev);
int len = strlen(drv->name);
PRT("dev name is %s, drv name is %s, name len = %d\n", device_name, drv->name, len);
if (!strncmp(device_name, drv->name, len))
{
PRT("dev %s <---> drv %s, match success!\n", drv->name, device_name);
return 1;
}
//else 的话就是没有匹配上
return 0;
}
/**
* @brief sbus_probe()
* @note 设备探测的回调函数
* @param [in]
* @param [out]
* @retval
*/
static int sbus_probe(struct device *dev)
{
struct device_driver *drv = dev->driver;
if (drv->probe)
{
drv->probe(dev);
}
return 0;
}
// 定义一个新的总线,名为 sbus,总线结构体中最重要的一个成员,便是 match 回调函数
static struct bus_type sbus = {
.name = SBUS_NAME, // 总线的名称 "sbus"
.match = sbus_match, // 设备和驱动程序匹配的回调函数
.probe = sbus_probe, // 设备探测的回调函数
};
/**
* @brief sbus_attr_name_show()
* @note 提供 show 回调函数,这样用户便可以通过 cat 命令, 来查询相关属性参数
* @param [in]
* @param [out]
* @retval
*/
static ssize_t sbus_attr_name_show(struct bus_type *bus, char *buf)
{
ssize_t count = 0;
count = sprintf(buf, "%s\n", sbus_attr.name_attr);
PRT("bus->name=%s count=%d\n", bus->name, count);
return count;
}
/**
* @brief sbus_attr_name_store()
* @note 提供 store 回调函数,这样用户便可以通过 echo 命令, 来修改相关属性参数
* @param [in]
* @param [out]
* @retval
*/
static ssize_t sbus_attr_name_store(struct bus_type *bus, const char *buf, size_t count)
{
PRT("bus->name=%s count=%d\n", bus->name, count);
sscanf(buf, "%s\n", sbus_attr.name_attr);
return count;
}
// 下面这个宏可以简化属性变量的定义,从定义可以看出,这里定义的属性的变量前缀是 bus_attr_,后面要再拼接 name 才行
// #define BUS_ATTR(_name, _mode, _show, _store)
// struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)
struct bus_attribute sbus_attr_name_var = {
.attr = {
.name = "sbus_attr_name", // 属性的名称, 将会显示在/sys/bus/SBUS_NAME/ 下,并可以使用 cat/echo 进行操作
.mode = 0664, // 属性的访问权限
},
.show = sbus_attr_name_show, // 属性的 show 回调函数
.store = sbus_attr_name_store, // 属性的 show 回调函数
};
/**
* @brief sbus_demo_init()
* @note
* @param [in]
* @param [out]
* @retval
*/
static __init int sbus_demo_init(void)
{
int ret = 0;
printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
__LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
PRT("sbus_demo module init!\n");
snprintf(sbus_attr.name_attr, sizeof(sbus_attr.name_attr), SBUS_NAME);
ret = bus_register(&sbus);
if(ret < 0)
{
PRTE("bus_register fail!\n");
goto err_bus_register;
}
ret = bus_create_file(&sbus, &sbus_attr_name_var);
if(ret < 0)
{
PRTE("bus_create_file fail!\n");
goto err_bus_create_file;
}
return 0;
err_bus_create_file:
bus_unregister(&sbus);
err_bus_register:
return ret;
}
/**
* @brief sbus_demo_exit()
* @note
* @param [in]
* @param [out]
* @retval
*/
static __exit void sbus_demo_exit(void)
{
bus_remove_file(&sbus, &sbus_attr_name_var);
bus_unregister(&sbus); // 取消注册总线
PRT("sbus_demo module exit!\n");
}
module_init(sbus_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sbus_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数
/* 模块信息(通过 modinfo chrdev_led_demo 查看) */
MODULE_LICENSE("GPL v2"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */3.2 开发板测试
我们编译完,把对对应的驱动拷贝到开发板中,按以下步骤测试效果。
- (1)一开始的 /sys/bus 目录

- (2)加载模块

加载模块后,会发现,在/sys/bus 目录生成了 sbus 目录,sbus 目录中会有一些默认创建的目录和属性文件,我们自己创建的属性文件也在这里。
- (3)查看的 sbus_attr_name 值

- (4)修改 sbus_attr_name 的值

- (5)卸载模块

可以看到,卸载模块后,资源释放,sbus 目录被删除,相关的属性文件也都删除了。
二、总线怎么注册的?
那肯定是从 bus_register() 函数开始了。
1. kobject 的使用
我们进入到开发板的/sys/bus/sbus 目录:

如上图所示,为什么在 sys/bus 目录下会生成 sbus 目录以及对应的 devices,drivers, drivers_autoprobe,drivers_probe,uevent 目录和属性呢?
在开发板/sys 目录下的目录都对应一个 kobject,所以我们猜测 sbus 目录和 devices,drivers 目录和 kobject 有关系的。而 kobject 一般要嵌入到其他结构体中去使用。如下图所示,kobject 嵌入到 struct device 结构体中:
在 struct device 结构体中包含了 kobject 结构体,而 struct bus_type 结构体又包含了 struct device 结构体。如下图所示:
所以我们猜测这些/sys/bus/下的目录是和 struct device 结构体中的 kobj 有关系。
2. bus_register()
这个函数我们来按行详细分析一下:
/**
* bus_register - register a driver-core subsystem
* @bus: bus to register
*
* Once we have that, we register the bus with the kobject
* infrastructure, then register the children subsystems it has:
* the devices and drivers that belong to the subsystem.
*/
int bus_register(struct bus_type *bus)
{
int retval;
struct subsys_private *priv;
struct lock_class_key *key = &bus->lock_key;
// (1) 分配并初始化一个 subsys_private 结构体,用于保存子系统的相关信息
priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
if (!priv)
return -ENOMEM;
// (2) 将 bus 结构体与 subsys_private 关联起来
priv->bus = bus;
bus->p = priv;
// (3) 初始化一个阻塞通知链表 bus_notifier
BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);
// (4) 设置子系统的名称
retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);
if (retval)
goto out;
// (5) 设置子系统的 kset 和 ktype
priv->subsys.kobj.kset = bus_kset;
priv->subsys.kobj.ktype = &bus_ktype;
priv->drivers_autoprobe = 1;
// (6) 注册子系统的 kset
retval = kset_register(&priv->subsys);
if (retval)
goto out;
// (7) 在总线上创建一个属性文件
retval = bus_create_file(bus, &bus_attr_uevent);
if (retval)
goto bus_uevent_fail;
// (8) 创建并添加 "devices" 子目录的 kset
priv->devices_kset = kset_create_and_add("devices", NULL,
&priv->subsys.kobj);
if (!priv->devices_kset) {
retval = -ENOMEM;
goto bus_devices_fail;
}
// (9) 创建并添加 "drivers" 子目录的 kset
priv->drivers_kset = kset_create_and_add("drivers", NULL,
&priv->subsys.kobj);
if (!priv->drivers_kset) {
retval = -ENOMEM;
goto bus_drivers_fail;
}
// (10) 初始化接口链表、互斥锁和设备/驱动的 klist
INIT_LIST_HEAD(&priv->interfaces);
__mutex_init(&priv->mutex, "subsys mutex", key);
klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
klist_init(&priv->klist_drivers, NULL, NULL);
// (11) 添加驱动探测文件
retval = add_probe_files(bus);
if (retval)
goto bus_probe_files_fail;
// (12) 添加总线的属性组
retval = bus_add_groups(bus, bus->bus_groups);
if (retval)
goto bus_groups_fail;
// (13) 打印调试信息,表示总线注册成功
pr_debug("bus: '%s': registered\n", bus->name);
return 0;
// (14) 错误处理
bus_groups_fail:
remove_probe_files(bus);
bus_probe_files_fail:
kset_unregister(bus->p->drivers_kset);
bus_drivers_fail:
kset_unregister(bus->p->devices_kset);
bus_devices_fail:
bus_remove_file(bus, &bus_attr_uevent);
bus_uevent_fail:
kset_unregister(&bus->p->subsys);
out:
kfree(bus->p);
bus->p = NULL;
return retval;
}
EXPORT_SYMBOL_GPL(bus_register);2.1 (1)—— 第 857 行

这一段,分配并初始化一个 struct subsys_private 结构体,用于保存子系统的相关信息。
struct subsys_private 是一个结构体,用于保存驱动核心子系统(bus)的私有信息。每个 子系统都可以有私有数据,这些私有数据就存储在 struct subsys_private 结构体中。
那么什么是子系统呢? 在 Linux 中,子系统是一种机制,用于将特定功能的实现抽象为一个独立的实体。它提供 了一种方便的方式,将相关的代码和数据结构组织在一起,以实现特定的功能。子系统可以被视为一个功能模块,它封装了相关的功能和操作,使得用户和应用程序可以通过统一的接口与 其交互。 在 Linux 中,存在许多常见的子系统,每个子系统都负责实现特定的功能。以下是一些常见的子系统示例。
虚拟文件系统(VFS)子系统:VFS 子系统提供了对不同文件系统的统一访问接口,使得应用程序可以透明地访问各种文件系统(如 ext4、NTFS、FAT 等),而无需关心底层文件系 统的具体实现。
设备驱动子系统:设备驱动子系统管理和控制硬件设备的驱动程序。它提供了与硬件设备 交互的接口,使得应用程序可以通过驱动程序与设备进行通信和控制。
网络子系统:网络子系统负责管理和控制网络相关的功能。它包括网络协议栈、套接字接 口、网络设备驱动程序等,用于实现网络通信和网络协议的处理。
内存管理子系统:内存管理子系统负责管理系统的物理内存和虚拟内存。它包括内存分配、 页面置换、内存映射等功能,用于有效地分配和管理系统的内存资源。
进程管理子系统:进程管理子系统负责管理和控制系统中的进程。它包括进程的创建、调 度、终止等功能,以及进程间通信的机制,如信号、管道、共享内存等。
电源管理子系统:电源管理子系统负责管理和控制系统的电源管理功能。它可以用于控制 电源的开关、电源模式的切换、节能功能的实现等。
文件系统子系统:文件系统子系统负责管理和控制文件系统的创建、格式化、挂载、数据 存取等操作。它支持各种文件系统类型,如 ext4、FAT、NTFS 等。
图形子系统:图形子系统负责管理和控制图形显示功能,包括显示驱动程序、窗口管理、 图形渲染等。它提供了图形界面的支持,使得用户可以通过图形方式与计算机交互。
2.2 (2)—— 第 861 行
将 bus 结构体与 subsys_private 关联起来。
将 priv 结构体中的 bus 成员设置为当前注册的总线。这样做 的目的是将 bus 成员与当前总线建立关联。通过将 bus 成员设置为当前总线,priv 结构体可以 获取并访问与该总线相关的信息和功能。这种关联可以使 priv 结构体在操作当前总线时更加方 便和高效。
将 priv 结构体指针存储在当前注册的总线结构体的成员 p 中, 目的是让当前注册的总线结构体能够快速地找到并访问与之关联的 priv 结构体。通过将 priv 结构体指针存储在总线结构体的成员中,总线可以轻松地获取与之相关的私有数据结构。这种关联使得总线能够直接访问和操作与特定总线相关的数据和功能,而无需通过其他方式来查找或传递指针。
2.3 (10)—— 第 896 行
klist_init() 函数用于初始化两个内核链表(klist),分别是 priv-> klist_devices 和 priv-> klist_drivers。
这行代码初始化了名为 priv-> klist_devices 的内核链表。klist_devices_get 和 klist_devices_put 是两个回调函数,用 于在向链表添加或移除元素时执行相应的操作。通常,这些回调函数用于在链表中的每个 元素被引用或释放时执行额外的操作。例如,当设备被添加到链表时,klist_devices_get() 函数可能会增加设备的引用计数;当设备从链表中移除时,klist_devices_put() 函数可能会减少设备的引用计数。
这行代码初始化了名为 priv-> klist_drivers 的内核链表,但与第一个初始化不同,这里没有提供回调函数。因此,这个链表在添加或移除元 素时不会执行额外的操作。这种情况下,链表主要用于存储驱动程序对象,而不需要附加的处理逻辑。
2.4 总结
通过分析 bus_register() 函数,我们对设备模型有了更深层次的了解:
(1)kobject 和 kset 是设备模型的基本框架,它们可以嵌入到其他结构体中以提供设备模型 的功能。kobject 代表设备模型中的一个对象,而 kset 则是一组相关的 kobject 的集合。
(2)属性文件在设备模型中具有重要作用,它们用于在内核空间和用户空间之间进行数据交 换。属性文件可以通过 sysfs 虚拟文件系统在用户空间中表示为文件,用户可以读取或写入这 些文件来与设备模型进行交互。属性文件允许用户访问设备的状态、配置和控制信息,从而实 现了设备模型的管理和配置。
(3)sysfs 虚拟文件系统在设备模型中扮演关键角色,它可以将设备模型的组织层次展现出 来。通过 sysfs,设备模型中的对象、属性和关系可以以目录和文件的形式在用户空间中表示。 这种组织形式使用户能够以层次结构的方式浏览和管理设备模型,从而方便地获取设备的信 息、配置和状态。sysfs 提供了一种统一的接口,使用户能够通过文件系统操作来与设备模型 进行交互,提供了设备模型的可视化和可操作性。