3、设备驱动模型基石:kobject、kset与ktype,sysfs文件系统原理
说实话,很多搞Linux驱动的人,写了好几年代码,对 kobject 还是一知半解。我刚开始也是这样,总觉得这东西就是个“内核里的对象”,具体干嘛用,说不清楚。
直到有一次,我需要调试一个USB热插拔的bug。设备插上去,驱动加载了,但用户态就是收不到通知。我翻来覆去查代码,最后发现是 kobject 的uevent没发出去。那一刻我才明白——kobject不是摆设,它是整个设备模型的神经末梢。
这一章,我们就把它彻底讲透。
3.1 为什么需要kobject?
你想想看,Linux内核里有多少设备?PCI设备、USB设备、平台设备、I2C设备……它们千奇百怪,但总得有个统一的管理方式吧?
kobject 就是干这个的。它提供了一套通用的“对象化”机制,让所有设备都能用同一套规则来:
- 引用计数:谁在用这个设备,用完了没,不会出现野指针。
- 生命周期管理:设备什么时候创建,什么时候销毁,清清楚楚。
- sysfs导出:设备的信息能暴露到用户态,方便调试和配置。
- uevent通知:设备来了、走了,用户态能收到消息。
说白了,kobject 就是内核里所有“东西”的基类。没有它,设备模型就是一盘散沙。
核心理解: kobject 本身不存储设备数据,它只负责“管理”和“连接”。真正的设备数据,放在包含它的结构体里(比如 struct device)。
3.2 kobject 结构体解剖
我们直接看代码。这是 include/linux/kobject.h 里的核心结构体,我挑重点讲:
struct kobject {
const char *name; // 对象名称,会出现在sysfs里
struct list_head entry; // 用于挂入kset的链表
struct kobject *parent; // 父对象,形成层次结构
struct kset *kset; // 所属的kset
struct kobj_type *ktype; // 对象类型,包含操作函数
struct kernfs_node *sd; // sysfs目录项,内部使用
struct kref kref; // 引用计数
unsigned int state_initialized:1; // 初始化标志
unsigned int state_in_sysfs:1; // 是否已注册到sysfs
unsigned int state_add_uevent_sent:1; // uevent是否已发送
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1; // 是否抑制uevent
};
嗯,这里要注意几个关键字段:
name:你在sysfs里看到的目录名就是它。比如/sys/devices/pci0000:00/里的pci0000:00。parent:形成树形结构。没有父对象的,就是顶层对象。kref:引用计数。我见过很多驱动崩溃,就是因为引用计数没处理好,对象被提前释放了。uevent_suppress:这个标志位很实用。如果你不想让某个设备触发uevent,把它置1就行。我曾经用它来屏蔽一些内部虚拟设备的通知。
3.3 kset:对象的集合
kset 是什么?它就是一个 kobject 的集合。你可以把它理解成一个“文件夹”,里面放着一堆同类型的 kobject。
struct kset {
struct list_head list; // 链表头,挂接所有kobject
spinlock_t list_lock;
struct kobject kobj; // kset本身也是一个kobject
const struct kset_uevent_ops *uevent_ops; // uevent操作函数集
};
有意思的是,kset 自己也是一个 kobject。所以它也能出现在sysfs里,也能有父对象。这种“自包含”的设计,在Linux内核里很常见。
我的经验: 当你需要管理一组同类型的设备时,用kset非常方便。比如所有块设备可以放在一个kset里,所有网络设备放在另一个里。这样uevent的发送和接收都统一了。
3.4 ktype:定义对象的行为
ktype 定义了 kobject 的“行为”。它包含三个核心部分:
struct kobj_type {
void (*release)(struct kobject *kobj); // 释放函数,必须实现
const struct sysfs_ops *sysfs_ops; // sysfs读写操作
struct attribute **default_attrs; // 默认属性列表
};
这里我特别强调一下 release 函数。很多初学者会忘记实现它,结果内核直接报错:
WARNING: CPU: 0 PID: 1 at lib/kobject.c:214 kobject_release+0x5c/0x68
kobject: 'xxx' (ffffffffa0000000): does not have a release() function
这个错误我踩过不止一次。记住:每个kobject都必须有release函数,否则引用计数归零时,内核不知道该怎么释放它。
避坑指南: release函数里要做什么?通常是释放包含这个kobject的父结构体。比如你的驱动定义了一个 struct my_device { struct kobject kobj; ... },那么release里就要调用 kfree(container_of(kobj, struct my_device, kobj))。
3.5 sysfs文件系统原理
sysfs 是一个基于内存的虚拟文件系统,通常挂载在 /sys 下。它的作用很简单:把内核里的 kobject 层次结构,映射成用户态的文件目录树。
举个例子:
/sys/
├── devices/ # 所有设备
│ ├── platform/ # 平台设备
│ └── pci0000:00/ # PCI设备
├── class/ # 设备类
│ ├── net/ # 网络设备
│ ├── input/ # 输入设备
│ └── tty/ # 终端设备
├── block/ # 块设备
└── bus/ # 总线
每个目录对应一个 kobject,目录里的文件对应 attribute。读写这些文件,就会调用 ktype 里定义的 sysfs_ops 函数。
我画了一张图,帮你理清它们之间的关系:
3.6 实际操作:创建一个简单的kobject
光说不练假把式。我们写一个最简单的例子,在 /sys/kernel/ 下创建一个名为 my_test 的目录:
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/module.h>
static struct kobject *my_kobj;
static int __init my_init(void)
{
int ret;
// 在 /sys/kernel/ 下创建 kobject
my_kobj = kobject_create_and_add("my_test", kernel_kobj);
if (!my_kobj) {
pr_err("Failed to create kobject\n");
return -ENOMEM;
}
pr_info("my_test kobject created at /sys/kernel/my_test\n");
return 0;
}
static void __exit my_exit(void)
{
kobject_put(my_kobj);
pr_info("my_test kobject removed\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
加载这个模块后,你会在 /sys/kernel/my_test/ 看到一个空目录。虽然里面没有文件,但已经证明 kobject 成功注册到了sysfs。
小技巧: kernel_kobj 是内核预定义的顶层kobject,指向 /sys/kernel/。类似的还有 firmware_kobj、hypervisor_kobj 等。直接用它们做父对象,省得自己创建顶层目录。
3.7 添加attribute:让kobject有“内容”
空目录没什么用。我们给它加一个文件,让用户态能读写数据:
static int my_value;
static ssize_t my_value_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", my_value);
}
static ssize_t my_value_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
sscanf(buf, "%du", &my_value);
return count;
}
static struct kobj_attribute my_value_attr =
__ATTR(my_value, 0664, my_value_show, my_value_store);
static int __init my_init(void)
{
int ret;
my_kobj = kobject_create_and_add("my_test", kernel_kobj);
if (!my_kobj)
return -ENOMEM;
ret = sysfs_create_file(my_kobj, &my_value_attr.attr);
if (ret) {
kobject_put(my_kobj);
return ret;
}
return 0;
}
现在 /sys/kernel/my_test/my_value 就是一个可读写的文件了。用 cat 读,用 echo 写,非常方便。
注意: 权限位 0664 表示所有用户可读,只有所有者可写。在生产环境中,别给太高的权限,否则用户态能随意修改内核参数,容易出安全问题。
3.8 避坑总结
最后,我把自己踩过的坑列出来,你遇到了可以直接对照:
| 问题 | 现象 | 原因 | 解决方法 |
|---|---|---|---|
| release函数缺失 | 内核报WARNING,kobject泄漏 | ktype->release未实现 | 实现release,调用kfree |
| uevent不触发 | 用户态收不到设备事件 | uevent_suppress被置1 | 检查标志位,确保为0 |
| sysfs文件权限错误 | 用户态无法读写 | 权限位设置不当 | 检查umask和文件权限 |
| kobject重复创建 | sysfs创建失败 | 同名kobject已存在 | 使用唯一名称,或先删除旧的 |
嗯,这一章的内容就到这里。kobject、kset、ktype 这三个概念,是理解整个设备驱动模型的基础。你只要记住一句话:kobject是节点,kset是容器,ktype是行为,sysfs是镜子。后面讲设备、总线、驱动的时候,你会反复看到它们的身影。