Linux驱动 2026年6月9日 96 分钟

Linux内核对象模型kobject-kset-sysfs-uevent详解

Linux内核对象模型kobject-kset-sysfs-uevent详解 1. 为什么 Linux 内核需要对象模型…

Linux内核对象模型kobject-kset-sysfs-uevent详解

[!abstract] Linux 内核对象模型是驱动模型、sysfs、热插拔事件、设备层级管理背后的公共底座。

这套模型的核心角色包括 kobjectksetkobj_typesysfsueventkobject 负责单个对象,kset 负责组织一组对象,kobj_type 描述对象行为,sysfs 负责把对象层级暴露给用户空间,uevent 负责把对象变化通知给用户空间。

这篇笔记不只是解释 API 名字,而是按下面这条主线来讲:

  1. 先回答 Linux 为什么需要内核对象模型。
  2. 再拆 kobjectksetkobj_type 的结构体和包含关系。
  3. 再追踪对象如何进入 sysfs、如何发 uevent、如何管理生命周期。
  4. 最后结合这棵 Linux 4.1.15 内核树里的真实源码,解释它们在 deviceclassbus 里的落地方式。

1. 为什么 Linux 内核需要对象模型

如果没有这套对象模型,内核各个子系统都会自己处理下面这些共性问题:

  • 这个对象叫什么名字
  • 这个对象在层级树中的父节点是谁
  • 这个对象什么时候创建、什么时候销毁
  • 这个对象怎样导出到用户空间
  • 用户空间如何通过 sysfs 读写它
  • 这个对象变化时,要不要通知用户空间

这些问题在设备、总线、类、模块、固件信息、IOMMU group、cpufreq policy、cpuidle state 等对象上都会反复出现。

Linux 的做法不是让每个子系统各写一套,而是抽一层公共对象模型出来:

  • kobject 负责“一个对象”的统一管理
  • kset 负责“一组对象”的统一组织

所以你看到的:

  • /sys/kernel
  • /sys/devices
  • /sys/class
  • /sys/bus
  • /sys/firmware

表面上看是很多目录,底层其实很多都落在 kobject / kset 这套机制上。

2. 一句话理解这两个对象

先建立最短心智模型:

  • kobject:内核里的一个“对象节点”
  • kset:一组 kobject 的“集合和组织者”

再进一步:

  • kobject 解决的是“单个对象如何被命名、计数、挂到 sysfs、加入层级、发事件”
  • kset 解决的是“多个对象如何作为一个子系统被组织、查找、统一处理 uevent”

还要再加一个经常一起出现的角色:

  • kobj_type:定义“这一类对象怎么释放、默认有哪些属性、sysfs 的 show/store 入口是什么”

所以可以把三者关系记成:

  • kobject 是对象本体
  • kset 是对象所属的组织
  • kobj_type 是对象所属的类型规则

3. 它们之间的关系图

flowchart TD
    A[业务对象 struct xxx] --> B[内嵌 struct kobject]
    B --> C[parent]
    B --> D[kset]
    B --> E[ktype]
    B --> F[kref]
    B --> G[sysfs节点 sd]
    D --> H[struct kset]
    H --> I[成员链表 list]
    H --> J[内嵌 kobject]
    H --> K[uevent_ops]
    E --> L[release]
    E --> M[sysfs_ops]
    E --> N[default_attrs]
    B --> O[sysfs目录/文件]
    B --> P[uevent]

这个图里最关键的 3 个点:

  1. 真实业务对象通常是“内嵌一个 kobject”,不是“等于 kobject
  2. kset 自己内部也嵌了一个 kobject
  3. kobjectksetkobj_type 三者是配合使用,不是互相替代

4. struct kobject 详解

struct kobject 定义在 include/linux/kobject.h,可以直接看 include/linux/kobject.h:63

它的核心字段如下:

struct kobject {
    const char              *name;
    struct list_head        entry;
    struct kobject          *parent;
    struct kset             *kset;
    struct kobj_type        *ktype;
    struct kernfs_node      *sd;
    struct kref             kref;
    ...
};

下面逐个解释。

4.1 name

对象名字。

它最直观的作用就是对应 sysfs 目录名。

例如 /sys/kernel 里的 kernel,对应的就是 kernel_kobj 的名字。

如果一个对象名字为空,kobject_add_internal() 会直接报错,这段检查在 lib/kobject.c:208

4.2 parent

父对象指针。

它决定这个对象挂在层级树的哪一层。

比如:

  • kernel_kobj 的 parent 是 NULL
  • /sys/kernel/kobject_example 中的 kobject_example,其 parent 是 kernel_kobj
  • 设备对象的 parent 可能是另一个设备的 kobj

4.3 kset

这个对象属于哪个集合。

一旦设置了 ksetkobject_add_internal() 会做两件重要的事:

  1. 把对象挂进 kset 的成员链表
  2. 如果你没显式指定 parent,就默认把 kset->kobj 当 parent

对应源码在:

这说明 kset 不只是“记账”,还会影响对象在层级树里的位置。

4.4 ktype

对象类型描述。

它不是 C 语言意义上的“类型系统”,而是 kobject 世界中的“行为描述表”。

通过它,内核知道:

  • 这个对象最后怎么释放
  • 它的 sysfs show/store 回调从哪进
  • 注册时自动创建哪些默认属性文件

4.5 sd

sysfs/kernfs 节点。

你可以把它理解成“这个 kobject 在 sysfs 里的实体句柄”。

创建目录时会被建立,删除目录时会被移除。

4.6 kref

引用计数。

这是生命周期管理的核心。

谁拿到了对象引用,就应当:

  • 增引用:kobject_get()
  • 释放引用:kobject_put()

一旦引用计数归零,就进入清理与释放流程。

4.7 state_*

这些标志位是内部状态机,用来记录:

  • 是否已经初始化
  • 是否已经加入 sysfs
  • 是否已经发过 KOBJ_ADD
  • 是否已经发过 KOBJ_REMOVE
  • 是否要抑制 uevent

它们主要服务于内部安全检查和自动清理。

5. struct kobj_type 详解

kobj_typeinclude/linux/kobject.h:115

struct kobj_type {
    void (*release)(struct kobject *kobj);
    const struct sysfs_ops *sysfs_ops;
    struct attribute **default_attrs;
    ...
};

这是理解 kobject 的关键。

5.1 release

最重要的回调,没有之一。

kobject 的引用计数归零后,最终会走到:

这就是对象最后真正释放自己的地方。

[!warning] release 必须有。

如果没有,说明对象生命周期设计是不完整的。

kobject_cleanup() 里,内核甚至会专门打印“does not have a release() function”这样的调试信息,见 lib/kobject.c:608

5.2 sysfs_ops

这个对象类型在 sysfs 层面的通用读写入口。

对普通 kobj_attribute 来说,常见默认实现是 kobj_sysfs_ops,定义在 lib/kobject.c:783

也就是说:

  • 用户读 sysfs 文件时,先进入 sysfs_ops.show
  • 再由它把 attribute 转回更具体的属性结构
  • 最后再进入真正的 foo_show() / foo_store()

5.3 default_attrs

默认属性文件数组。

当对象目录被创建后,populate_dir() 会遍历它,自动帮你建文件,源码在 lib/kobject.c:49

这非常适合“一类对象都带同一组属性”的场景。

6. struct kset 详解

struct kset 定义在 include/linux/kobject.h:167

struct kset {
    struct list_head list;
    spinlock_t list_lock;
    struct kobject kobj;
    const struct kset_uevent_ops *uevent_ops;
};

它本质上包含 4 个部分:

  • 一个成员链表 list
  • 一个保护链表的锁 list_lock
  • 一个“代表整个集合自身”的 kobject
  • 一组 uevent 策略 uevent_ops

这里最容易忽略的是:

kset 自己也是一个 kobject

这意味着:

  • kset 自己可以出现在 sysfs 里
  • 其他 kobject 可以挂在它下面
  • 它既是“集合”,又是“目录节点”

7. kset_uevent_ops 的意义

定义在 include/linux/kobject.h:131

它不是给单个对象用的,而是给一整个集合用的。

作用有 3 个:

7.1 filter

决定某个对象变化时,这个 uevent 要不要发。

7.2 name

决定这个事件对用户空间表现成什么 SUBSYSTEM

7.3 uevent

往 uevent 环境变量里再补充额外键值对。

这意味着 kset 不只是存对象,它还定义了这类对象和用户空间交互时的“公共策略”。

8. kobject 的完整生命周期

这一节是最重要的主线。

8.1 初始化:kobject_init()

入口在 lib/kobject.c:314

它做的事情很集中:

  • 检查参数合法性
  • 初始化内部引用计数和链表节点
  • 标记对象已经初始化
  • ktype 记下来

真正的内部初始化在 lib/kobject.c:187

8.2 加入层级树:kobject_add()

入口在 lib/kobject.c:382

这一步做的事情包括:

  1. 给对象设置名字
  2. 设置 parent
  3. 如果有 kset,加入 kset 成员链表
  4. 创建 sysfs 目录
  5. 创建默认属性文件

实际底层核心是 kobject_add_internal(),在 lib/kobject.c:200

8.3 初始化并加入:kobject_init_and_add()

入口在 lib/kobject.c:417

它只是把前两步打包:

  • kobject_init()
  • kobject_add()

这是最常用的 API 之一。

8.4 创建 sysfs 目录:create_dir()

lib/kobject.c:66

这里会:

  • sysfs_create_dir_ns()
  • 再调 populate_dir()

也就是说,一个 kobject 一旦成功加入系统,通常就已经在 sysfs 里有目录了。

8.5 自动创建默认属性:populate_dir()

lib/kobject.c:49

如果 ktype->default_attrs 不为空,内核会自动给你在目录下建文件。

8.6 发 add 事件:kobject_uevent(KOBJ_ADD)

这里有个特别容易踩坑的点:

kobject_add() 不会自动发 KOBJ_ADD

这一点在 lib/kobject.c:377 的注释里写得很清楚。

所以对动态对象,正确套路通常是:

  1. kobject_init_and_add()
  2. 建好属性文件
  3. kobject_uevent(..., KOBJ_ADD)

这也是官方 samples/kobject/kset-example.c 的写法。

8.7 获取与释放引用:kobject_get() / kobject_put()

API 在:

这是对象生命周期的标准入口。

8.8 最终清理:kobject_cleanup()

lib/kobject.c:600

它会做几件很关键的善后:

  1. 如果已经发过 ADD 但没发 REMOVE,自动补 REMOVE
  2. 如果还在 sysfs 里,自动 kobject_del()
  3. 调用 ktype->release()
  4. 释放名字内存

所以它不是简单 free,而是完整的对象退场流程。

9. kset 的完整生命周期

9.1 初始化:kset_init()

lib/kobject.c:751

它做的事情是:

  • 初始化内嵌 kobject
  • 初始化成员链表
  • 初始化自旋锁

9.2 注册:kset_register()

lib/kobject.c:793

这里和普通 kobject 有一个差别:

kset_register() 成功后会自动发 KOBJ_ADD

lib/kobject.c:804

9.3 动态创建:kset_create_and_add()

lib/kobject.c:918

这是最方便的用法:

  • 分配 kset
  • 设名字
  • uevent_ops
  • 设 parent
  • 注册进系统

9.4 注销:kset_unregister()

lib/kobject.c:812

它会:

  • 从 sysfs 删除
  • kobject_put(&k->kobj)

9.5 查成员:kset_find_obj()

lib/kobject.c:829

它会遍历这个 kset 的成员链表,按名字找对象。

这再次说明:

  • kset 不是抽象概念
  • 它内部真的维护了一组对象成员

10. sysfs 视角下如何看 kobject

很多人学 kobject 时会把注意力都放在结构体上,但其实它最大的可见效果在 sysfs

10.1 目录来自哪里

来自 kobject 自己。

一个 kobject 成功 add 后,通常对应一个 sysfs 目录。

10.2 文件来自哪里

常见有两条路:

  1. ktype->default_attrs
  2. 手动 sysfs_create_file() / sysfs_create_group()

10.3 读写回调如何落地

sysfs 层最终需要统一函数签名,而具体子系统又想拿到自己的业务对象。

所以常见套路是:

  1. show/store 先接收通用的 struct kobject *
  2. 再用 container_of() 转回自定义对象
  3. 再调用真正业务层的读写函数

官方 samples/kobject/kset-example.c 就是标准范例。

11. uevent 视角下如何看 kset

kobject_uevent_env()lib/kobject_uevent.c:164

它的一个核心逻辑是:

  1. 从当前对象往上找祖先
  2. 找到第一个带 kset 的对象
  3. 使用这个 ksetuevent_ops

对应源码在 lib/kobject_uevent.c:183lib/kobject_uevent.c:196

然后它会准备:

  • ACTION=
  • DEVPATH=
  • SUBSYSTEM=

lib/kobject_uevent.c:238lib/kobject_uevent.c:246

所以从用户空间看见的“这个对象属于哪个 subsystem”,很多时候本质上就是由 kset 决定的。

12. 真实实例一:/sys/kernel

这是最简单、最适合入门的真实例子。

kernel_kobj 定义在 kernel/ksysfs.c:187,初始化在 kernel/ksysfs.c:213

关键代码:

kernel_kobj = kobject_create_and_add("kernel", NULL);
sysfs_create_group(kernel_kobj, &kernel_attr_group);

这说明:

  • /sys/kernel 本身就是一个 kobject
  • 它下面的很多通用属性文件,是通过 sysfs_create_group() 建出来的

这个例子说明什么时候只用 kobject 就够:

  • 只是想创建一个固定目录
  • 没有必要维护一组动态对象
  • 也不需要复杂的集合级 uevent 策略

13. 真实实例二:/sys/firmware/sys/hypervisor

这两个也很直接:

它们都采用:

kobject_create_and_add("firmware", NULL);
kobject_create_and_add("hypervisor", NULL);

这再次说明:

  • 只需要一个顶层 sysfs 节点时,直接用 kobject 就非常自然

14. 真实实例三:struct device

这是最重要的真实实例。

struct device 本身就内嵌了一个 kobject,定义在 include/linux/device.h:723

struct device {
    ...
    struct kobject kobj;
    ...
};

这意味着:

  • 设备模型不是“另外搞一套对象系统”
  • 它直接构建在 kobject 之上

14.1 device_ktype

设备对象的类型规则定义在 drivers/base/core.c:266

static struct kobj_type device_ktype = {
    .release    = device_release,
    .sysfs_ops  = &dev_sysfs_ops,
    .namespace  = device_namespace,
};

这里清楚地体现了前面讲的分工:

  • release:设备最后怎么释放
  • sysfs_ops:设备 sysfs 文件怎么读写

14.2 devices_kset

设备集合 devices_ksetdrivers/base/core.c:1381 创建:

devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);

它对应 /sys/devices

14.3 设备初始化

drivers/base/core.c:654device_initialize() 里:

dev->kobj.kset = devices_kset;
kobject_init(&dev->kobj, &device_ktype);

也就是:

  • 每个 struct device 都属于 devices_kset
  • 每个 struct device 都遵循 device_ktype

14.4 设备真正加入系统

drivers/base/core.c:976device_add() 里,会执行:

error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);

drivers/base/core.c:1025

之后再:

kobject_uevent(&dev->kobj, KOBJ_ADD);

drivers/base/core.c:1070

这条链路说明:

  • 设备能出现在 /sys/devices
  • 设备能发热插拔事件

底层都与 kobject 直接相关。

15. 真实实例四:class_kset/sys/class

class_ksetdrivers/base/class.c:588 创建:

class_kset = kset_create_and_add("class", NULL, NULL);

这就是 /sys/class 的根。

类的内部不是简单用 struct class 自己充当 kobject,而是通过 subsys_private 来承载对象模型,定义在 drivers/base/base.h:28

这个结构里内嵌了:

struct kset subsys;
struct kset *devices_kset;
struct kset *drivers_kset;

所以:

  • class 的 sysfs 呈现本质上是一组 kset 结构
  • 它不是“只有一个目录”,而是一个完整的子系统组织结构

在注册类时,代码会做:

cp->subsys.kobj.kset = class_kset;
cp->subsys.kobj.ktype = &class_ktype;
kset_register(&cp->subsys);

对应 drivers/base/class.c:193drivers/base/class.c:201

16. 真实实例五:bus_kset/sys/bus

bus_ksetdrivers/base/bus.c:1265 创建:

bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL);

这就是 /sys/bus

每个总线注册时会做:

priv->subsys.kobj.kset = bus_kset;
priv->subsys.kobj.ktype = &bus_ktype;
retval = kset_register(&priv->subsys);

对应 drivers/base/bus.c:892drivers/base/bus.c:897

然后还会再创建两个子集合:

priv->devices_kset = kset_create_and_add("devices", NULL, &priv->subsys.kobj);
priv->drivers_kset = kset_create_and_add("drivers", NULL, &priv->subsys.kobj);

对应 drivers/base/bus.c:904drivers/base/bus.c:912

这就是为什么你在 sysfs 里会看到:

  • /sys/bus/platform/devices
  • /sys/bus/platform/drivers

这样的组织结构。

[!tip] 这里是理解 kset 最直观的地方:

kset 不是一个抽象名词,它就是 sysfs 里一层层“集合目录”的直接实现基础。

17. deviceclassbuskobject / kset 的包含关系图

前面我们已经分别讲了 struct deviceclass_ksetbus_kset,但如果不把它们放到同一张图里,脑子里还是容易“各懂各的”,不容易真正串起来。

这一节专门把驱动模型里最常见的几个对象放在一张图上看。

17.1 第一层:谁直接内嵌 kobject,谁通过私有包装结构间接使用

flowchart TD
    DEV[struct device] --> DEVKOBJ[内嵌 struct kobject kobj]
    DEV --> DEVP[struct device_private *p]
    DEVKOBJ --> DEVSET[dev->kobj.kset = devices_kset]

    CLS[struct class] --> CLSP[subsys_private *p]
    CLSP --> CLSSUBSYS[内嵌 struct kset subsys]
    CLSSUBSYS --> CLSKOBJ[内嵌 struct kobject]
    CLSKOBJ --> CLASSROOT[class_kset]

    BUS[struct bus_type] --> BUSP[subsys_private *p]
    BUSP --> BUSSUBSYS[内嵌 struct kset subsys]
    BUSSUBSYS --> BUSKOBJ[内嵌 struct kobject]
    BUSKOBJ --> BUSROOT[bus_kset]
    BUSP --> BUSDEVS[struct kset *devices_kset]
    BUSP --> BUSDRVS[struct kset *drivers_kset]

    DRVPRIV[struct driver_private] --> DRVKOBJ[内嵌 struct kobject]
    MODKOBJ[struct module_kobject] --> MODINNER[内嵌 struct kobject]

这张图最值得你盯住的结论有 4 个:

  1. struct device 是最“直接”的,它本体里就内嵌了 struct kobject
  2. struct classstruct bus_type 本体里没有直接放 kobject,而是通过 subsys_private 这个私有包装结构承载对象模型
  3. subsys_private 里面嵌的是 struct kset subsys,而 kset 里面又嵌着一个 kobject
  4. 所以 class / bus 这种“大组织”更自然地使用 kset,而 device 这种“单个具体对象”更自然地直接使用 kobject

17.2 第二层:这些对象最终映射到 sysfs 的哪几棵树

flowchart TD
    SYS[/sys]
    SYS --> DEVICES[/sys/devices]
    SYS --> CLASS[/sys/class]
    SYS --> BUS[/sys/bus]

    DEVICES --> DEVKSET[devices_kset]
    DEVKSET --> DEVOBJ[device对象的kobj]

    CLASS --> CLASSKSET[class_kset]
    CLASSKSET --> CLASSOBJ[class自己的subsys.kobj]

    BUS --> BUSKSET[bus_kset]
    BUSKSET --> BUSOBJ[bus自己的subsys.kobj]
    BUSOBJ --> BUSDEVDIR[bus下的devices子kset]
    BUSOBJ --> BUSDRVDIR[bus下的drivers子kset]

这一层图是“包含关系”在 sysfs 中的落地效果。

你可以这样理解:

  • /sys/devices 更偏“所有设备对象的大本营”,所以它直接由 devices_kset 组织
  • /sys/class 更偏“按功能类别组织”,所以根是 class_kset
  • /sys/bus 更偏“按总线组织”,所以根是 bus_kset

17.3 为什么 deviceclass / bus 的组织方式不一样

从建模角度看,这是很自然的:

  • device 是单个对象,它的重点是“我自己是谁”
  • classbus 是组织容器,它们的重点是“我下面管理哪些对象”

所以:

  • device 直接内嵌 kobject
  • class / bus 倾向于通过 kset 组织子对象

这也解释了为什么我们前面在源码里看到:

18. sysfsshow/store 完整调用链

这一节专门回答一个学习内核时很容易卡住的问题:

用户空间执行一次 cat /sys/.../fooecho 1 > /sys/.../foo,到底是怎么一路走到驱动或内核对象自己的 show/store 回调里的?

如果把这件事说短一点:

  • VFS 并不是直接调用 kobject
  • VFS 先落到 kernfs
  • kernfs 再根据 sysfs 预先注册好的 kernfs_ops 反调到 sysfs
  • sysfs 再根据 kobject -> ktype -> sysfs_ops 找到真正的 show/store
  • 最后才进入具体对象类型自己的属性回调

18.1 先回答:sysfs 文件是怎么建出来的

先别急着看读写,先看这个文件从哪来。

常见有两条路径:

  1. kobj_type.default_attrs
  2. 手动 sysfs_create_file() / sysfs_create_group()

18.1.1 默认属性路径

如果对象的 kobj_type.default_attrs 不为空,那么在 kobject_add_internal()create_dir() 后,会继续调 populate_dir(),见:

populate_dir() 会遍历 default_attrs,对每个属性调用:

sysfs_create_file(kobj, attr);

18.1.2 属性组路径

如果代码里手动调用:

sysfs_create_group(kobj, &attr_group);

调用链会进入:

也就是说,最后真正把文件节点建出来的是 kernfs

18.1.3 一张时序图看清 sysfs_create_group()__kernfs_create_file()

sequenceDiagram
    participant Caller as 调用者
    participant Group as sysfs_create_group()
    participant Internal as internal_create_group()
    participant Dir as kobj->sd / kernfs_create_dir()
    participant Files as create_files()
    participant AddFile as sysfs_add_file_mode_ns()
    participant Kernfs as __kernfs_create_file()

    Caller->>Group: sysfs_create_group(kobj, grp)
    Group->>Internal: internal_create_group(kobj, 0, grp)
    alt grp->name != NULL
        Internal->>Dir: kernfs_create_dir(kobj->sd, grp->name, ..., kobj)
    else grp->name == NULL
        Internal->>Dir: 直接使用 kobj->sd
    end
    Internal->>Files: create_files(parent_kn, kobj, grp, update=0)
    loop grp->attrs 中每个 attribute
        Files->>Files: mode = attr->mode
        opt grp->is_visible 存在
            Files->>Files: mode = grp->is_visible(kobj, attr, index)
            alt mode == 0
                Files-->>Files: 跳过这个属性
            end
        end
        Files->>AddFile: sysfs_add_file_mode_ns(parent, attr, false, mode, ns)
        AddFile->>AddFile: 从 parent->priv 取到 kobject
        AddFile->>AddFile: 读取 kobj->ktype->sysfs_ops
        AddFile->>AddFile: 选择 ro/rw/wo 对应的 kernfs_ops
        AddFile->>Kernfs: __kernfs_create_file(parent, attr->name, mode, PAGE_SIZE, ops, attr, ns, key)
        Kernfs-->>AddFile: 返回新的 kernfs_node
    end
    Internal-->>Group: 返回 0 或错误码
    Group-->>Caller: 返回 0 或错误码

把这张图配合源码一起看,整个创建过程可以拆成 5 层职责:

  1. sysfs_create_group() 只是一个薄入口,真正逻辑在 internal_create_group(),见 fs/sysfs/group.c:140
  2. internal_create_group() 决定“这组属性挂在哪个目录下”:
    • grp->name 就先建一个子目录;
    • 没有 grp->name 就直接挂在 kobj->sd 对应目录下,见 fs/sysfs/group.c:94
  3. create_files() 负责把 attribute_group 展开成一个个具体属性,同时处理 grp->is_visible()、权限过滤、失败回滚,见 fs/sysfs/group.c:35
  4. sysfs_add_file_mode_ns() 负责把“sysfs 层的属性语义”翻译成“kernfs 层的文件语义”:
    • 它先从 parent->priv 找回这个目录对应的 kobject
    • 再从 kobj->ktype->sysfs_ops 判断这个属性最终能不能读、能不能写;
    • 然后选择合适的 kernfs_ops,见 fs/sysfs/file.c:247
  5. __kernfs_create_file() 才是真正分配并挂接 kernfs_node 的地方,见 fs/sysfs/file.c:297

这里最关键的“埋线”有两根:

  • 目录节点这一侧:parent->priv 指回拥有该目录的 kobject
  • 文件节点这一侧:新建文件节点的 priv 指向当前 attribute

正因为创建阶段提前把这两份上下文绑好,后面的 read() / write() 才能从 kernfs -> sysfs -> kobj->ktype->sysfs_ops -> 具体 show/store 一层层回到真正对象。

18.2 创建文件时,sysfskernfs 填了什么关键信息

这一步很关键,因为后面读写能不能走回正确回调,全靠这里提前埋好“线索”。

fs/sysfs/file.c:247fs/sysfs/file.c:298 里,sysfs_add_file_mode_ns() 做了两件特别重要的事:

  1. 根据 kobj->ktype->sysfs_ops 选择合适的 kernfs_ops
  2. attr 指针作为 priv 传给 __kernfs_create_file()

这里的关键对象关系是:

  • 目录节点 kobj->sd 对应这个 kobject
  • 目录节点的 priv 指向 kobject
  • 文件节点的 priv 指向 attribute
  • 文件节点的 attr.ops 指向 kernfs_ops

这就为后面的读写回调准备好了两份关键上下文:

  • “我属于哪个 kobject
  • “我对应哪个属性文件”

18.3 用户空间 open() 进来后,先到 kernfs_file_fops

真正接到 VFS 层读写请求的 file operations 在 fs/kernfs/file.c:886

const struct file_operations kernfs_file_fops = {
    .read    = kernfs_fop_read,
    .write   = kernfs_fop_write,
    .open    = kernfs_fop_open,
    ...
};

所以:

  • open("/sys/...") 最终会进 kernfs_fop_open()
  • read() 最终会进 kernfs_fop_read()
  • write() 最终会进 kernfs_fop_write()

kernfs_fop_open()fs/kernfs/file.c:612

这里会:

  • 取出 kernfs_node
  • 看这个节点支持哪些 kernfs_ops
  • 分配 kernfs_open_file
  • 如果是常规可读属性,通常会 seq_open(file, &kernfs_seq_ops),见 fs/kernfs/file.c:697

所以,普通 sysfs 文本属性读路径通常会走 seq_file 机制。

18.4 read() 路径:从 cat /sys/.../foo 到最终 show()

这一段我们分层展开。

18.4.1 第一层:VFS 到 kernfs

用户执行:

cat /sys/kernel/kobject_example/foo

主线入口是:

kernfs_fop_read()fs/kernfs/file.c:243

if (of->kn->flags & KERNFS_HAS_SEQ_SHOW)
    return seq_read(file, user_buf, count, ppos);
else
    return kernfs_file_direct_read(of, user_buf, count, ppos);

普通 sysfs 文本属性通常走 seq_read() 这条路。

18.4.2 第二层:seq_read()kernfs_seq_show()

seq_read() 会驱动 kernfs_seq_ops,定义在 fs/kernfs/file.c:171

其中 .show 对应的是:

它做的事情很短,但非常关键:

return of->kn->attr.ops->seq_show(sf, v);

也就是说,kernfs 本身不懂你的 kobject 是什么,它只是把控制权交给这个文件节点绑定的 kernfs_ops

18.4.3 第三层:kernfs 回到 sysfs

对普通 sysfs 属性文件,前面 sysfs_add_file_mode_ns() 已经给它绑定了:

  • sysfs_file_kfops_ro
  • sysfs_file_kfops_rw

其中 .seq_show = sysfs_kf_seq_show,定义在 fs/sysfs/file.c:190fs/sysfs/file.c:198

于是调用继续进入:

它会做两件关键事情:

struct kobject *kobj = of->kn->parent->priv;
const struct sysfs_ops *ops = sysfs_file_ops(of->kn);
count = ops->show(kobj, of->kn->priv, buf);

这里你要牢牢记住:

  • of->kn->parent->priv 取回的是这个文件所属目录的 kobject
  • of->kn->priv 取回的是这个文件自己的 attribute

18.4.4 第四层:找到这个对象类型的 sysfs_ops

sysfs_file_ops()fs/sysfs/file.c:28

return kobj->ktype ? kobj->ktype->sysfs_ops : NULL;

这一步完成了最关键的一跳:

  • 从“文件节点”
  • 回到“这个文件属于哪个 kobject
  • 再回到“这个 kobject 的类型怎么定义 sysfs 行为”

18.4.5 第五层:进入具体对象类型的 show()

如果这是最普通的 kobj_attribute 路径,那么 kobj->ktype->sysfs_ops 常常是:

里面:

.show = kobj_attr_show,
.store = kobj_attr_store,

然后 kobj_attr_show()lib/kobject.c:759

kattr = container_of(attr, struct kobj_attribute, attr);
return kattr->show(kobj, kattr, buf);

这一步完成的事情是:

  • 把通用的 struct attribute * 还原成 struct kobj_attribute *
  • 再调用你自己定义的 foo_show() / bar_show()

所以普通 kobject 属性读路径的最终形态可以总结成:

cat /sys/.../foo
-> VFS read
-> kernfs_fop_read
-> seq_read
-> kernfs_seq_show
-> sysfs_kf_seq_show
-> sysfs_file_ops
-> kobj->ktype->sysfs_ops->show
-> kobj_attr_show
-> 具体 foo_show()

18.5 write() 路径:从 echo 1 > /sys/.../foo 到最终 store()

写路径和读路径相似,但不走 seq_show,而是直接走写回调。

18.5.1 第一层:VFS 到 kernfs_fop_write()

入口在 fs/kernfs/file.c:270

static ssize_t kernfs_fop_write(struct file *file,
                                const char __user *user_buf,
                                size_t count, loff_t *ppos)

它会先做几件事情:

  • 分配内核缓冲区
  • copy_from_user()
  • 手动补 '\0' 保证字符串结束,见 fs/kernfs/file.c:308

然后调用:

ops = kernfs_ops(of->kn);
len = ops->write(of, buf, len, *ppos);

fs/kernfs/file.c:310fs/kernfs/file.c:312

18.5.2 第二层:kernfs 回到 sysfs_kf_write()

对普通 sysfs 属性文件,ops->write 通常是:

它会继续:

const struct sysfs_ops *ops = sysfs_file_ops(of->kn);
struct kobject *kobj = of->kn->parent->priv;
return ops->store(kobj, of->kn->priv, buf, count);

这和读路径完全对称:

  • 取回 kobject
  • 取回 attribute
  • 找到该对象类型的 sysfs_ops.store

18.5.3 第三层:进入具体 store()

如果是普通 kobj_attribute,则会进入:

再由它做:

kattr = container_of(attr, struct kobj_attribute, attr);
return kattr->store(kobj, kattr, buf, count);

最终进入你自己定义的 foo_store()

所以普通 kobject 属性写路径可以总结成:

echo 1 > /sys/.../foo
-> VFS write
-> kernfs_fop_write
-> copy_from_user
-> sysfs_kf_write
-> sysfs_file_ops
-> kobj->ktype->sysfs_ops->store
-> kobj_attr_store
-> 具体 foo_store()

18.6 device 属性为什么又是另一层封装

前面讲的是最通用的 kobject 路径。

但你在驱动里更常见的往往不是 struct kobj_attribute,而是:

  • struct device_attribute
  • DEVICE_ATTR(...)

这时最终走到的 sysfs_ops 就不是 kobj_sysfs_ops,而是设备自己的:

static const struct sysfs_ops dev_sysfs_ops = {
    .show = dev_attr_show,
    .store = dev_attr_store,
};

18.6.1 设备读路径最后怎么落地

dev_attr_show()drivers/base/core.c:113

struct device_attribute *dev_attr = to_dev_attr(attr);
struct device *dev = kobj_to_dev(kobj);
ret = dev_attr->show(dev, dev_attr, buf);

这说明设备属性层又做了一次“通用到具体”的还原:

  • kobject 还原出 struct device
  • attribute 还原出 struct device_attribute

最后才调用你驱动里真正写的:

ssize_t xxx_show(struct device *dev,
                 struct device_attribute *attr,
                 char *buf)

18.6.2 设备写路径最后怎么落地

dev_attr_store()drivers/base/core.c:129

struct device_attribute *dev_attr = to_dev_attr(attr);
struct device *dev = kobj_to_dev(kobj);
ret = dev_attr->store(dev, dev_attr, buf, count);

所以设备属性路径的最终形态是:

echo xxx > /sys/devices/.../attr
-> kernfs_fop_write
-> sysfs_kf_write
-> dev_sysfs_ops.store
-> dev_attr_store
-> 具体设备属性 store(dev, attr, buf, count)

18.7 一张总图把 show/store 完整串起来

flowchart TD
    A[用户空间 cat/echo /sys/.../attr] --> B[VFS read/write]
    B --> C[kernfs_file_fops]
    C --> D[kernfs_fop_open]
    C --> E[kernfs_fop_read]
    C --> F[kernfs_fop_write]
    E --> G[seq_read 或 direct_read]
    G --> H[kernfs_seq_show]
    H --> I[sysfs_kf_seq_show]
    F --> J[sysfs_kf_write]
    I --> K[sysfs_file_ops: 取 kobj->ktype->sysfs_ops]
    J --> K
    K --> L1[kobj_sysfs_ops.show/store]
    K --> L2[dev_sysfs_ops.show/store]
    L1 --> M1[kobj_attr_show/store]
    L2 --> M2[dev_attr_show/store]
    M1 --> N1[具体 kobj_attribute 回调]
    M2 --> N2[具体 device_attribute 回调]

18.8 读写链路里几个最容易忽略的细节

18.8.1 普通 sysfs 文本属性读常常走 seq_file

这也是为什么你看到的是 seq_show 路径,而不是最朴素的 read() 直接调 show()

18.8.2 写路径会把用户缓冲区复制到内核,并补 '\0'

fs/kernfs/file.c:304fs/kernfs/file.c:308

这也是为什么很多 store() 里会直接把它当字符串解析。

18.8.3 echo 1 > file 往往会带换行

因为 shell 的 echo 默认会附带 \n

所以 store() 实现里通常要能接受带换行的字符串。

18.8.4 sysfs 不适合复杂的大块数据协议

kernfs_fop_write() 的注释也能看出,它并不鼓励复杂 partial write 语义,见 fs/kernfs/file.c:264

sysfs 更适合:

  • 简单状态
  • 小型文本配置
  • 单值或少量键值

而不适合承载复杂用户态协议。

19. 官方样例一:最小 kobject 用法

样例文件是 samples/kobject/kobject-example.c

关键代码在:

主线非常简单:

  1. kobject_create_and_add("kobject_example", kernel_kobj)
  2. sysfs_create_group(example_kobj, &attr_group)

效果是:

  • /sys/kernel/ 下创建一个 kobject_example
  • 里面挂 3 个属性文件:foobazbar

这个例子适合理解:

  • 如何快速创建一个 sysfs 目录
  • 如何用属性组挂一批文件

20. 官方样例二:kset 管一组对象

样例文件是 samples/kobject/kset-example.c

这是学习 kobject / kset 的黄金样例。

20.1 自定义对象

它定义了一个真实业务对象:

struct foo_obj {
    struct kobject kobj;
    int foo;
    int baz;
    int bar;
};

这正是最典型的模式:

  • 业务对象内嵌 kobject

20.2 自定义属性类型

它还定义了 struct foo_attribute,再用 container_of() 把通用 attribute 还原为业务属性对象。

这展示了 sysfs 框架和业务层之间的桥接方式。

20.3 自定义 kobj_type

foo_ktypesamples/kobject/kset-example.c:189

static struct kobj_type foo_ktype = {
    .sysfs_ops = &foo_sysfs_ops,
    .release = foo_release,
    .default_attrs = foo_default_attrs,
};

这很好地体现了 kobj_type 的三个核心作用:

  • 释放对象
  • 决定 sysfs 入口
  • 指定默认属性

20.4 创建 kset

samples/kobject/kset-example.c:248

example_kset = kset_create_and_add("kset_example", NULL, kernel_kobj);

20.5 创建成员对象

samples/kobject/kset-example.c:222

retval = kobject_init_and_add(&foo->kobj, &foo_ktype, NULL, "%s", name);

注意,在这之前它先做了:

foo->kobj.kset = example_kset;

也就是:

  • 先告诉对象它属于哪个集合
  • 再让它进对象层级

20.6 显式发 add 事件

samples/kobject/kset-example.c:232

kobject_uevent(&foo->kobj, KOBJ_ADD);

这再次说明:

  • 动态对象通常要自己发 KOBJ_ADD

21. 如何使用:三种常见场景

21.1 场景一:只是想在 /sys/kernel 下建一个固定目录

这时通常只需要:

  • kobject_create_and_add()
  • sysfs_create_group()

适用场景:

  • 小型调试接口
  • 模块级状态导出
  • 系统级静态节点

21.2 场景二:要管理一类自定义对象

这时更适合:

  • 自定义业务结构体,内嵌 kobject
  • 自定义 kobj_type
  • 必要时用 kset 组织多个对象

适用场景:

  • 一组逻辑实例
  • 每个实例都要有独立 sysfs 目录
  • 每个实例都带同样一批属性

21.3 场景三:你其实在做设备/总线/类

这时通常不应该从裸 kobject 开始。

更推荐优先使用已经封装好的高层对象:

  • struct device
  • struct class
  • struct bus_type

因为这些对象已经把:

  • kobject
  • kset
  • sysfs
  • uevent
  • 引用计数

这些通用问题打包好了。

22. 什么时候不该直接上 kobject

这是一个很实用的问题。

如果你只是需要:

  • 普通链表节点
  • 单纯引用计数
  • 一个内部对象,但根本不打算进 sysfs

那就没必要强行用 kobject

因为 kobject 不只是“多一个字段”,它代表的是一整套对象模型义务:

  • 要考虑名字
  • 要考虑生命周期
  • 要考虑 release
  • 要考虑用户空间可见性

所以它适合的是“需要进入内核对象模型”的对象,不是所有对象。

23. 常见易错点

23.1 忘记写 release

这是最常见、也最严重的设计缺口之一。

23.2 直接 kfree() 而不是 kobject_put()

对象一旦纳入 kobject 生命周期,就应该通过引用计数退场。

23.3 以为 kobject_add() 会自动发 KOBJ_ADD

它不会。

23.4 动态对象发 uevent 太早

正确顺序应该是:

  1. kobject_init_and_add()
  2. 建好属性
  3. kobject_uevent(KOBJ_ADD)

23.5 没搞清 parentkset 的差别

parent 决定层级。

kset 决定集合归属,而且在未显式设 parent 时,还会影响默认父对象。

23.6 把 kset 误以为只是一个链表

它还是一个带 kobject 的 sysfs 目录节点,还携带 uevent_ops

23.7 业务代码里滥用裸 kobject

如果本来就是设备模型场景,很多时候应该直接用 struct device

24. 最后的整体心智模型

把前面所有内容收束成一张图:

flowchart TD
    A[业务对象] --> B[内嵌 kobject]
    B --> C[kobj_type]
    B --> D[parent]
    B --> E[kset]
    C --> F[release]
    C --> G[sysfs_ops]
    C --> H[default_attrs]
    E --> I[成员列表]
    E --> J[uevent策略]
    B --> K[sysfs目录]
    B --> L[属性文件]
    B --> M[uevent事件]

最终你应该建立下面这几个直觉:

  1. kobject 是“对象最小公共壳”
  2. kset 是“对象集合 + 子系统组织者”
  3. kobj_type 是“对象行为模板”
  4. sysfs 是它们对用户空间的文件化呈现
  5. uevent 是它们对用户空间的事件化通知

25. 一段最实用的结论

如果只保留一句最有用的话:

kobject 解决“单个对象如何被内核统一管理并映射到 sysfs”,kset 解决“多个对象如何作为一个子系统被组织、查找并统一发 uevent”。
struct deviceclassbus 这些你平时最常见的驱动模型对象,本质上都建立在这套机制之上。

26. 推荐继续阅读路径

如果你想继续顺着这条线往下打通,建议按这个顺序看:

  1. samples/kobject/kobject-example.c
  2. samples/kobject/kset-example.c
  3. drivers/base/core.c
  4. drivers/base/class.c
  5. drivers/base/bus.c
  6. lib/kobject.c
  7. lib/kobject_uevent.c

再配合下面这些已有笔记一起看,会更顺:

  • [[iMX6ULL设备树从DTS到设备创建全过程详解]]
  • [[device_node转换为platform_device的完整调用链路]]
  • [[Linux GPIO子系统从零基础到驱动开发与使用开发完全指南]]
  • [[struct device到device_add再到sysfs与uevent完整链路详解]]
上一篇 Linux内核kobject与kset详解

Linux内核kobject与kset详解 1. 为什么 Linux 内核需要 kobject 和 kset 如果没有这...

下一篇 arch_initcall何时被调用及device_node何时加入platform总线

arch_initcall 何时被调用及 device_node 何时加入 platform 总线 问题一:arch_i...