3. 设备驱动模型核心:kobject、kset、ktype 的关系与作用,sysfs 文件系统的原理

Linux 设备驱动模型的核心目标是统一管理系统中所有的设备、驱动、总线和类,并通过 sysfs 文件系统向用户空间暴露一个层次化的视图。理解 kobject、kset 和 ktype 这三个基础数据结构,是掌握驱动模型的关键。

3.1 核心数据结构:kobject

kobject(Kernel Object)是设备驱动模型中最基本的对象,它封装了对象的引用计数、名称、父对象指针以及 sysfs 中的目录入口。几乎所有的“设备”、“驱动”、“总线”在内核中都以 kobject 或其嵌入的容器结构存在。

关键点: kobject 本身不单独存在,它总是嵌入在一个更大的结构体中(如 struct device、struct device_driver)。通过 container_of() 宏可以从 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;
    unsigned int state_add_uevent_sent:1;
    unsigned int state_remove_uevent_sent:1;
    unsigned int uevent_suppress:1;
};

3.2 对象集合:kset

kset 是 kobject 的集合,它将同类型的 kobject 组织在一起,并作为这些对象的父目录出现在 sysfs 中。kset 本身也包含一个 kobject,因此它可以被嵌套。

面试高频题: kset 与 kobject 的关系是什么?
答案: kset 是 kobject 的“容器”或“集合”。一个 kobject 可以通过其 kset 字段加入一个 kset,同时 kset 自身的 kobject 可以作为子对象的 parent,从而在 sysfs 中形成目录层次。
struct kset {
    struct list_head list;          // 链表中所有 kobject 的头部
    spinlock_t      list_lock;
    struct kobject  kobj;           // kset 自身的 kobject,用于在 sysfs 中创建目录
    const struct kset_uevent_ops *uevent_ops; // 用户空间事件通知操作
};

3.3 对象类型:ktype

ktype 定义了同一类 kobject 的共性行为,主要包括属性操作和释放函数。所有属于同一个 ktype 的 kobject 共享相同的 sysfs_ops 和 release 方法。

struct kobj_type {
    void (*release)(struct kobject *kobj);          // 引用计数归零时的释放函数
    const struct sysfs_ops *sysfs_ops;              // sysfs 文件读写操作
    struct attribute **default_attrs;               // 默认属性列表(已废弃,改用属性组)
    const struct attribute_group **default_groups;  // 默认属性组
};

3.4 三者的关系与作用

下图展示了 kobject、kset、ktype 在设备模型中的协作关系:

kobject、kset、ktype 关系图 kobject、kset、ktype 关系与作用 kset (集合容器) struct kset kobj (kset 自身) parent: NULL kobject A parent: &kset.kobj kobject B parent: &kset.kobj ktype (对象类型) struct kobj_type release() 引用计数归零时调用 sysfs_ops show() / store() default_groups 属性组列表 ktype 指针 kset 指针 list 链表 说明:kset 通过 list 链表管理多个 kobject;每个 kobject 通过 ktype 指针共享同一套属性操作和释放方法。

3.5 sysfs 文件系统原理

sysfs 是一个基于内存的虚拟文件系统,通常挂载在 /sys 目录下。它将内核中的 kobject 层次结构导出为用户空间的目录和文件,使得用户可以通过读写文件来查看或修改内核对象的属性。

3.5.1 sysfs 与 kobject 的映射

sysfs 元素 内核对象 说明
目录 kobject 每个 kobject 在 sysfs 中对应一个目录,目录名为 kobject 的 name 字段
文件 struct attribute kobject 的属性通过 sysfs_ops 的 show/store 方法实现读写
子目录 kobject->parent 父子关系通过 parent 指针建立,形成树形结构
符号链接 sysfs_create_link() 用于创建跨层次的关联,如 device 到 driver 的链接

3.5.2 sysfs 的典型目录结构

/sys/
├── bus/               # 总线类型(如 pci, usb, platform)
│   ├── pci/
│   │   ├── devices/   # 该总线上的设备符号链接
│   │   └── drivers/   # 该总线上的驱动
│   └── platform/
├── class/             # 设备类(如 net, input, tty)
│   ├── net/
│   └── input/
├── devices/           # 所有设备的真实目录树(按硬件拓扑)
│   └── .../
├── firmware/          # 固件信息
└── kernel/            # 内核参数

3.5.3 sysfs 的读写流程

当用户空间读取或写入 sysfs 文件时,内核会经历以下调用链:

  1. VFS 层:用户调用 read()/write(),VFS 根据文件路径找到对应的 dentry 和 inode。
  2. sysfs 层:sysfs 的 inode 操作函数被调用,通过 kernfs 找到对应的 kernfs_node。
  3. 属性操作:从 kernfs_node 获取关联的 kobject,再通过 kobject->ktype->sysfs_ops 找到 show() 或 store() 方法。
  4. 驱动层:调用驱动注册的属性读写回调函数,完成实际的数据传输。
常见面试陷阱: 面试官可能会问“sysfs 与 procfs 的区别”。
标准回答: procfs 主要用于输出进程和系统运行时信息(如 /proc/cpuinfo),文件内容通常由内核直接生成;而 sysfs 严格对应 kobject 层次结构,每个文件对应一个内核属性,具有统一的 show/store 接口,更适合设备模型的管理。

3.6 实战:创建一个简单的 kobject 并导出到 sysfs

以下代码演示了如何在内核模块中创建一个 kobject,并将其添加到 sysfs 中:

#include <linux/module.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>

static struct kobject *example_kobj;

// 属性读回调
static ssize_t my_value_show(struct kobject *kobj,
                             struct kobj_attribute *attr, char *buf)
{
    return sprintf(buf, "42\n");
}

// 属性写回调
static ssize_t my_value_store(struct kobject *kobj,
                              struct kobj_attribute *attr,
                              const char *buf, size_t count)
{
    int val;
    if (sscanf(buf, "%d", &val) != 1)
        return -EINVAL;
    pr_info("my_value set to %d\n", val);
    return count;
}

// 定义属性
static struct kobj_attribute my_attr =
    __ATTR(my_value, 0664, my_value_show, my_value_store);

// 属性组
static struct attribute *attrs[] = {
    &my_attr.attr,
    NULL,
};

static struct attribute_group attr_group = {
    .attrs = attrs,
};

static int __init my_module_init(void)
{
    int ret;

    // 创建 kobject,父对象为 kernel_kobj(/sys/kernel)
    example_kobj = kobject_create_and_add("my_demo", kernel_kobj);
    if (!example_kobj)
        return -ENOMEM;

    // 创建属性组
    ret = sysfs_create_group(example_kobj, &attr_group);
    if (ret)
        kobject_put(example_kobj);

    return ret;
}

static void __exit my_module_exit(void)
{
    kobject_put(example_kobj);
}

module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");

加载模块后,在 /sys/kernel/my_demo/my_value 下会出现一个可读写的文件。

3.7 面试核心问题清单

问题 核心要点
kobject 的作用是什么? 提供引用计数、名称管理、父子层次结构,是 sysfs 目录的基础单元
kset 与 kobject 的关系? kset 是 kobject 的集合,通过链表管理,同时 kset 自身的 kobject 作为父目录
ktype 为什么是共享的? 同一类 kobject 具有相同的属性操作和释放逻辑,共享 ktype 可以节省内存并统一行为
sysfs 文件如何与内核属性关联? 通过 kobject->ktype->sysfs_ops 中的 show/store 回调实现
如何手动创建 sysfs 文件? 使用 sysfs_create_file() 或 sysfs_create_group(),配合 struct kobj_attribute
面试加分项: 提到 kernfs(内核文件系统)是 sysfs 的底层实现,它分离了文件系统逻辑与 kobject 管理,使得 sysfs 的代码更加清晰。在 Linux 3.14 之后,sysfs 完全基于 kernfs 重构。