引言
Linux 内核的设备模型(Device Model)是整个驱动子系统的基石。无论你是编写字符设备驱动、platform 驱动,还是 PCI/USB 驱动,底层都依赖同一个统一框架:kobject — kset — ktype 三层抽象。理解这套机制,不仅能让你轻松掌握 sysfs 文件系统的工作原理,还能明白驱动如何与用户态通信、引用计数如何管理对象生命周期、uevent 如何触发热插拔响应。本文从源码级别深度剖析设备模型的内部实现,并给出实战中的关键模式和常见陷阱。
1. 设备模型的设计哲学
1.1 为什么需要统一设备模型
Linux 2.5/2.6 时代之前,内核中设备管理各自为政:PCI 子系统维护自己的设备链表,USB 有自己的 hub 结构,字符设备只需调用 register_chrdev。这种混乱带来三大问题:
- 电源管理无法统一:休眠时需要遍历所有设备调用 .suspend(),但没有统一列表
- 拓扑关系缺失:无法表示"这个 PCI 设备下面挂了一个 USB 控制器,上面又挂了网卡"这种父子关系
- 用户态不可见:/dev 只能看到设备节点,看不到设备的属性、驱动绑定状态、电源信息
2.6 内核引入的设备模型通过四个核心问题解决上述困境:
| 需求 | 设备模型解决方案 |
|---|---|
| 电源管理 | 统一设备链表,树形拓扑支持有序 suspend/resume |
| 拓扑关系 | 父指针 + kset 子树,形成设备树(Device Tree) |
| 用户态通信 | sysfs 每个 kobject 对应一个目录,attribute 对应文件 |
| 对象生命周期 | kref 引用计数自动释放,避免 use-after-free |
2. kobject:一切皆对象
2.1 数据结构定义
kobject 是整个设备模型的基础单元,定义在 include/linux/kobject.h:
struct kobject {
const char *name; // 对象名称(sysfs 目录名)
struct list_head entry; // 挂入 kset 的链表节点
struct kobject *parent; // 父对象(决定 sysfs 层级位置)
struct kset *kset; // 所属 kset(容器)
struct kobj_type *ktype; // 操作类型(释放函数、sysfs 操作)
struct kernfs_node *sd; // sysfs 目录节点(kernfs_node)
struct kref kref; // 引用计数
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release_work; // 调试模式下延迟释放
#endif
unsigned int state_in_sysfs:1; // 是否已在 sysfs 中
unsigned int state_add_uevent_sent:1; // ADD uevent 是否已发
unsigned int state_remove_uevent_sent:1; // REMOVE uevent 是否已发
unsigned int uevent_suppress:1; // 是否抑制 uevent
};
关键点在于:kobject 很少单独使用,通常被嵌入到更大的结构体中(如 device、driver_device、cdev)。这种"嵌入式"设计是 Linux 内核的惯用手法,通过 container_of 宏反向获取宿主结构体指针。
2.2 引用计数与生命周期
kobject 通过 struct kref kref 实现引用计数管理:
struct kref {
refcount_t refcount; // 使用 refcount_t 防止溢出
};
// 增加引用
struct kobject *kobject_get(struct kobject *kobj);
// 减少引用,归零时调用 ktype->release()
int kobject_put(struct kobj *kobj);
这里的 refcount_t(而非 atomic_t)是安全强化:在 REFCOUNT_MAX/2 时saturation,溢出后不再回绕到 0,从而阻止 "放完再用的释放后引用"攻击。release 函数由 ktype 定义:
struct kobj_type {
void (*release)(struct kobject *kobj); // 引用归零时调用
const struct sysfs_ops *sysfs_ops; // show/store 函数对
struct attribute **default_attrs; // 默认属性数组(NULL 结尾)
const struct attribute_group **default_groups; // 默认属性组
const struct kobj_ns_type_operations *(*ns_type)(struct kobject *);
};
2.3 初始化与注册流程
kobject 必须经过显式初始化才能使用:
// 方式1:初始化独立 kobject
void kobject_init(struct kobject *kobj, struct kobj_type *ktype);
// 方式2:初始化并添加到 sysfs(一步到位)
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,
struct kobject *parent, const char *fmt, ...);
// 方式3:最简单——创建+初始化+添加到 sysfs
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent);
kobject_init 内部做了什么?查看源码(lib/kobject.c):
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
{
char *err_str;
if (!kobj) {
err_str = "invalid kobject pointer!";
goto error;
}
if (!ktype) {
err_str = "must have a ktype to be initialized properly\n";
goto error;
}
kref_init(&kobj->kref); // 引用计数 = 1
INIT_LIST_HEAD(&kobj->entry);
kobj->ktype = ktype;
// parent/kset/name 在此时为 NULL,需后续设置
}
注意:kref_init 将引用计数设为 1。这意味着创建者持有第一个引用,后续通过 kobject_get/put 管理其他引用。当最后一个引用者调用 kobject_put 时,引用归零触发 ktype->release()。
3. kset:kobject 的容器与 uevent 过滤器
kset 本质上是 kobject 的集合,它附加了 uevent 过滤和热插拔处理功能:
struct kset {
struct list_head list; // 链表头:挂载所有属于此 kset 的 kobject
spinlock_t list_lock; // 保护 list 的自旋锁
struct kobject kobj; // 内嵌的 kobject(kset 本身也出现在 sysfs 中)
const struct kset_uevent_ops *uevent_ops; // uevent 过滤回调
};
一个 kset 在 sysfs 中对应一个目录,其中每个 kobject 都是它的一个子目录。例如 /sys/bus/pci/ 对应 pci_bus_type 的 kset,下面每个设备都对应一个子目录。
3.1 uevent 过滤器的实战价值
在某些场景下,你可能不希望用户态接收到某个 kobject 的 uevent(例如虚拟测试设备)。通过 kset_uevent_ops 可以精确过滤:
static const struct kset_uevent_ops my_uevent_ops = {
.filter = my_filter_func, // 返回 0 则不发送 uevent
.uevent = my_uevent_func, // 自定义环境变量
};
// filter 函数原型
int my_filter_func(struct kset *kset, struct kobject *kobj)
{
// 示例:隐藏名字含 "test_" 的对象
if (strncmp(kobj->name, "test_", 5) == 0)
return 0; // 过滤掉,不发送 uevent
return 1; // 不过滤
}
4. sysfs 与 attribute 系统
4.1 attribute 定义
sysfs 中的每个文件对应一个 attribute:
struct attribute {
const char *name; // 文件名(不含路径)
umode_t mode; // 文件权限(S_IRUGO/S_IWUSR 等)
};
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *, char *buf);
ssize_t (*store)(struct kobject *, struct attribute *, const char *buf, size_t count);
};
内核提供便捷宏简化定义:
// 定义名为 "vendor" 的属性(只读)
struct attribute vendor_attr = {
.name = "vendor",
.mode = S_IRUGO,
};
// 更简洁的方式
__ATTR(vendor, S_IRUGO, vendor_show, NULL);
// 设备级别的快速宏(推荐)
DEVICE_ATTR(vendor, S_IRUGO, vendor_show, NULL);
// 展开后自动生成 dev_attr_vendor 并关联到设备 kobject
4.2 show/store 的实现模式
static ssize_t vendor_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct my_dev *my = dev_get_drvdata(dev);
return sysfs_emit(buf, "0x%04x\n", my->vendor_id);
// 注意:Linux 5.10+ 推荐 sysfs_emit() 替代 sprintf(),自带溢出检查
}
sysfs_emit() 是较新内核(~5.10+)提供的安全封装,比对 sprintf 增加了 PAGE_SIZE 溢出保护。老代码若需兼容,可直接使用 scnprintf(buf, PAGE_SIZE, ...)。
4.3 attribute_group:批量属性管理
对于驱动中常见的"一组属性"(如统计信息组、配置组),使用 attribute_group 更优雅:
static struct attribute *my_stats_attrs[] = {
&dev_attr_rx_packets.attr,
&dev_attr_tx_packets.attr,
&dev_attr_rx_errors.attr,
NULL,
};
static const struct attribute_group my_stats_group = {
.name = "stats", // NULL 表示无子目录,"stats" 表示 /sys/.../stats/ 下
.attrs = my_stats_attrs,
};
// 还有 is_visible 回调控制动态可见性
static const struct attribute_group my_config_group = {
.attrs = my_config_attrs,
.is_visible = my_config_visible, // 根据运行时状态决定是否显示
};
static const struct attribute_group *my_groups[] = {
&my_stats_group,
&my_config_group,
NULL,
};
// 在 probe 时调用:sysfs_create_group(&dev->kobj, &my_stats_group);
// 或在 device 结构中设置 .groups = my_groups,自动创建
5. device 结构体:kobject 的最常见使用者
真正硬件驱动开发中最常打交道的结构体是 struct device(include/linux/device.h),它继承了 kobject:
struct device {
struct kobject kobj;
struct device *parent; // 父设备(拓扑关系)
struct device_private *p; // 私有数据
const char *init_name; // 初始化名称(可能被重命名)
const struct device_type *type;
struct bus_type *bus; // 所属总线类型(pci_bus_type 等)
struct device_driver *driver; // 绑定的驱动
void *driver_data; // 驱动私有数据
void *platform_data; // platform 设备时的平台数据
...
};
所有子类结构体都遵循同样的模式:内嵌 kobject + 自定义字段,通过 container_of 转换:
// include/linux/device.h 中的转换宏
static inline struct device *kobj_to_dev(struct kobject *kobj)
{
return container_of(kobj, struct device, kobj);
}
5.1 设备注册的标准流程
struct device *my_dev;
// 1. 分配设备结构体(带 kobj 初始化)
my_dev = device_create(my_class, // 所属的 class(决定 /sys/class/ 位置)
parent, // 父设备
devt, // 主次设备号
drvdata, // 驱动私有数据
"mydevice%d", id); // 名字格式
// 2. 设置设备释放回调(在 kref 归零时调用)
my_dev->release = my_device_release;
// 3. 通过 uevent 通知用户态
kobject_uevent(&my_dev->kobj, KOBJ_ADD);
// 4. 卸载流程
device_destroy(my_class, my_dev->devt); // 从 sysfs 移除、发 KOBJ_REMOVE、引用减 1
6. class 与 bus_type:设备分类骨架
6.1 class:功能视角的分类
class 按"功能用途"对设备分类,例如 /sys/class/net/ 下是所有网卡:
struct class {
const char *name;
struct module *owner;
const struct attribute_group **class_groups;
const struct attribute_group **dev_groups;
struct knode_class *node;
...
};
// 创建 class
struct class *my_class = class_create(THIS_MODULE, "my_driver_class");
// 创建类下的设备(自动生成 /dev 节点)
dev = device_create(my_class, NULL, devt, NULL, "mydevice%d", minor);
// 缺点:需要手动 mknod 或额外 udev 支持
6.2 bus_type:通信总线的抽象
bus_type 定义了物理总线接口,包含驱动匹配、probe/remove 等核心回调:
struct bus_type {
const char *name;
int (*match)(struct device *dev, struct device_driver *drv); // 能否匹配
int (*probe)(struct device *dev); // 匹配后初始化
void (*remove)(struct device *dev); // 断开时清理
int (*suspend)(struct device *dev, pm_message_t state); // 电源管理
int (*resume)(struct device *dev);
const struct attribute_group **dev_groups; // 设备默认属性组
const struct attribute_group **drv_groups; // 驱动默认属性组
struct subsys_private *p;
};
以 platform_bus 为例,其 match 函数通过比较驱动名和设备名匹配。PCI 总线则比较 vendor/device ID。USB 总线根据接口类/子类/协议匹配。
7. 设备树(Device Tree)与 ACPI 的结合
在 ARM/ARM64/RISC-V 架构中,设备从一个静态的 .dts 文件描述。启动时,设备树被解析为一系列 platform_device:
// 设备树节点
my_device: my_device@0 {
compatible = "myvendor,mydevice-v2";
reg = <0x10000000 0x1000>; // 寄存器基地址 + 长度
interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
clock-frequency = <1000000>;
};
// 内核中的 of_device_id 表
static const struct of_device_id my_of_match[] = {
{ .compatible = "myvendor,mydevice-v1" },
{ .compatible = "myvendor,mydevice-v2" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
// platform_driver 注册
static struct platform_driver my_pdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = "my_device",
.of_match_table = my_of_match, // 设备树匹配表
.pm = &my_pm_ops,
},
};
注意 .of_match_table 字段:总线遍历设备树节点时,将节点的 compatible 值与驱动表中的 compatible 逐一比较,匹配成功后调用 probe。
8. 实战:从零创建一个虚拟设备并注册到 sysfs
通过一个最小化的虚拟设备示例,完整展现设备模型的典型使用:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/string.h>
static struct kset *my_kset;
static int my_value = 42;
// show/store 实现
static ssize_t value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
return sysfs_emit(buf, "%d\n", my_value);
}
static ssize_t value_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t count)
{
int ret = kstrtoint(buf, 10, &my_value);
return ret ? ret : count;
}
// 使用 __ATTR 宏定义属性
static struct kobj_attribute value_attr = __ATTR(value, 0644, value_show, value_store);
static struct attribute *my_attrs[] = {
&value_attr.attr,
NULL,
};
static const struct attribute_group my_group = {
.attrs = my_attrs,
};
static int __init my_init(void)
{
int ret;
// 1. 创建 kset(在 /sys/kernel/ 下)
my_kset = kset_create_and_add("my_demo", NULL, kernel_kobj);
if (!my_kset)
return -ENOMEM;
// 2. 创建一个 kobject 并添加到 kset
struct kobject *my_obj;
my_obj = kobject_create_and_add("my_device", &my_kset->kobj);
if (!my_obj) {
kset_unregister(my_kset);
return -ENOMEM;
}
// 3. 创建属性组
ret = sysfs_create_group(my_obj, &my_group);
if (ret) {
kobject_put(my_obj);
kset_unregister(my_kset);
return ret;
}
pr_info("my_demo: loaded, see /sys/kernel/my_demo/my_device/value\n");
return 0;
}
static void __exit my_exit(void)
{
// 清理顺序:先移除属性,再释放对象
// kset_unregister 会同时释放其内嵌 kobj 及其所有子 kobject 的引用
kset_unregister(my_kset);
pr_info("my_demo: unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
加载模块后,可以在终端查看:
$ cat /sys/kernel/my_demo/my_device/value
42
$ echo 100 > /sys/kernel/my_demo/my_device/value
$ cat /sys/kernel/my_demo/my_device/value
100
9. 高级主题:uevent 机制与用户态联动
9.1 uevent 的发送流程
kobject_uevent() 通过 netlink 套接字向用户态广播设备事件。典型的 uevent 环境变量包括:
ACTION=add // add/remove/change/move/online/offline
DEVPATH=/class/mydriver/mydevice0 // sysfs 路径
SUBSYSTEM=mydriver // 子系统名
SEQNUM=12345 // 序列号(用户态排序用)
MAJOR=250 // 主设备号
MINOR=0 // 次设备号
9.2 自定义 uevent 环境变量
在发射前可以追加自定义键值对:
char *envp[] = {
"ACTION=add",
"MY_CUSTOM_KEY=my_value",
NULL,
};
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
char *envp[]);
// 示例:添加自定义数据
char *ext_envp[] = { "MY_FIRMWARE_VER=2.1", NULL };
char *new_envp[32];
int i = kobject_setup_env ext...
10. 常见陷阱与最佳实践
10.1 必须设置 release 函数
当调用 device_create() 时,如果未设置 dev.release,内核调试框架会发出警告:
BUG: atomic: late sanity check failed, please implement device_release!
这是因为 device_create 会将设备的引用计数托付给 release 函数释放。如果没有 release,设备永远不会被释放,导致 kset 引用泄漏。
10.2 kobject_put 与 kobject_del 的区别
kobject_del 从 sysfs 层级和父 kset 中移除对象(但不一定释放内存),kobject_put 减少引用计数。通常二者配合使用:
// 推荐顺序
kobject_del(kobj); // 1. 从拓扑中移除
kobject_put(kobj); // 2. 引用减一(若为零则调 release 释放内存)
// 注意:不要先 put 再 del,否则 del 时对象可能已经不在 kset 中
10.3 内核配置 CONFIG_DEBUG_KOBJECT_RELEASE
调试选项可帮助发现 release 函数缺失:通过延迟释放(delayed_work),若释放前被访问则有 use-after-free 风险,配合 KASAN 可精确定位。
10.4 kobject 的递归引用问题
当父设备的 kobject 引用了子设备的 kobject 时(某些驱动实现),需要特别注意释放顺序,否则形成"父等子、子等父"的引用环。内核的解决方式是:通过 parent 指针实现层级关系,不额外增加引用计数。设计驱动时遵循 "parent 仅做反向指针引用,不调用 kobject_get" 即可避免。
11. 内核 6.x 演进与新特性
近年来设备模型也在持续演进:
- kernfs 重写(3.14+):sysfs 内部被重构为独立的 kernfs 框架,不仅服务 sysfs,还成为 cgroup 等子系统的基础
- Rust for Linux 的设备抽象:kernel crate 提供结构体实现
IntoFence或将ARef(Arc 的引用)替代 kref,bring 设备模型的引用计数类型安全 - 设备属性可见性控制:attribute_group 的
is_visible回调在 5.x 后更加灵活,支持运行时动态决策 - fw_devlink 依赖跟踪:5.11 引入的 firmware device link 自动建立设备间依赖关系,保证 probe 按依赖顺序进行
总结
kobject/kset/ktype 构成了 Linux 内核设备模型的三位一体。它们不仅提供了对象生命周期管理(kref)、层级组织(parent/kset)、用户态接口(sysfs)三大核心能力,还通过 uevent 实现了内核态与用户态的异步通信。理解这套机制后,编写驱动设备时就不再是"照猫画虎",而是清楚每一步操作在模型中的含义。推荐进一步阅读 drivers/base/core.c(设备模型核心实现)和 Documentation/ABI/(sysfs ABI 规范)。

发表评论 取消回复