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 函数。

我画了一张图,帮你理清它们之间的关系:

kobject / kset / ktype / sysfs 关系图 kobject name, parent, kref kset, ktype, sd kset list, list_lock kobj, uevent_ops ktype release() sysfs_ops, attrs 所属 定义类型 sysfs 文件系统 将 kobject 层次结构映射为 /sys 下的目录树 每个 attribute 对应一个文件,读写触发 sysfs_ops 注册到 uevent 通知机制 设备添加/移除时,通过 kset_uevent_ops 向用户态发送事件 触发 核心流程:kobject 注册 → sysfs 创建目录 → 用户态通过文件访问 → 设备变化时发送 uevent

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是镜子。后面讲设备、总线、驱动的时候,你会反复看到它们的身影。