引言

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 规范)。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部