Linux内核对象模型kobject-kset-sysfs-uevent详解
[!abstract] Linux 内核对象模型是驱动模型、
sysfs、热插拔事件、设备层级管理背后的公共底座。这套模型的核心角色包括
kobject、kset、kobj_type、sysfs和uevent:kobject负责单个对象,kset负责组织一组对象,kobj_type描述对象行为,sysfs负责把对象层级暴露给用户空间,uevent负责把对象变化通知给用户空间。这篇笔记不只是解释 API 名字,而是按下面这条主线来讲:
- 先回答 Linux 为什么需要内核对象模型。
- 再拆
kobject、kset、kobj_type的结构体和包含关系。- 再追踪对象如何进入
sysfs、如何发uevent、如何管理生命周期。- 最后结合这棵
Linux 4.1.15内核树里的真实源码,解释它们在device、class、bus里的落地方式。
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 个点:
- 真实业务对象通常是“内嵌一个
kobject”,不是“等于kobject” kset自己内部也嵌了一个kobjectkobject、kset、kobj_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
这个对象属于哪个集合。
一旦设置了 kset,kobject_add_internal() 会做两件重要的事:
- 把对象挂进
kset的成员链表 - 如果你没显式指定
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_type 在 include/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 的引用计数归零后,最终会走到:
kobject_put()→ lib/kobject.c:669kobject_release()→ lib/kobject.c:648kobject_cleanup()→ lib/kobject.c:600ktype->release()→ lib/kobject.c:627
这就是对象最后真正释放自己的地方。
[!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。
这一步做的事情包括:
- 给对象设置名字
- 设置 parent
- 如果有
kset,加入kset成员链表 - 创建 sysfs 目录
- 创建默认属性文件
实际底层核心是 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()
这里会:
- 调
sysfs_create_dir_ns() - 再调
populate_dir()
也就是说,一个 kobject 一旦成功加入系统,通常就已经在 sysfs 里有目录了。
8.5 自动创建默认属性:populate_dir()
如果 ktype->default_attrs 不为空,内核会自动给你在目录下建文件。
8.6 发 add 事件:kobject_uevent(KOBJ_ADD)
这里有个特别容易踩坑的点:
kobject_add()不会自动发KOBJ_ADD。
这一点在 lib/kobject.c:377 的注释里写得很清楚。
所以对动态对象,正确套路通常是:
kobject_init_and_add()- 建好属性文件
kobject_uevent(..., KOBJ_ADD)
这也是官方 samples/kobject/kset-example.c 的写法。
8.7 获取与释放引用:kobject_get() / kobject_put()
API 在:
这是对象生命周期的标准入口。
8.8 最终清理:kobject_cleanup()
它会做几件很关键的善后:
- 如果已经发过
ADD但没发REMOVE,自动补REMOVE - 如果还在 sysfs 里,自动
kobject_del() - 调用
ktype->release() - 释放名字内存
所以它不是简单 free,而是完整的对象退场流程。
9. kset 的完整生命周期
9.1 初始化:kset_init()
它做的事情是:
- 初始化内嵌
kobject - 初始化成员链表
- 初始化自旋锁
9.2 注册:kset_register()
这里和普通 kobject 有一个差别:
kset_register()成功后会自动发KOBJ_ADD。
9.3 动态创建:kset_create_and_add()
这是最方便的用法:
- 分配
kset - 设名字
- 设
uevent_ops - 设 parent
- 注册进系统
9.4 注销:kset_unregister()
它会:
- 从 sysfs 删除
kobject_put(&k->kobj)
9.5 查成员:kset_find_obj()
它会遍历这个 kset 的成员链表,按名字找对象。
这再次说明:
kset不是抽象概念- 它内部真的维护了一组对象成员
10. sysfs 视角下如何看 kobject
很多人学 kobject 时会把注意力都放在结构体上,但其实它最大的可见效果在 sysfs。
10.1 目录来自哪里
来自 kobject 自己。
一个 kobject 成功 add 后,通常对应一个 sysfs 目录。
10.2 文件来自哪里
常见有两条路:
ktype->default_attrs- 手动
sysfs_create_file()/sysfs_create_group()
10.3 读写回调如何落地
sysfs 层最终需要统一函数签名,而具体子系统又想拿到自己的业务对象。
所以常见套路是:
show/store先接收通用的struct kobject *- 再用
container_of()转回自定义对象 - 再调用真正业务层的读写函数
官方 samples/kobject/kset-example.c 就是标准范例。
11. uevent 视角下如何看 kset
kobject_uevent_env() 在 lib/kobject_uevent.c:164。
它的一个核心逻辑是:
- 从当前对象往上找祖先
- 找到第一个带
kset的对象 - 使用这个
kset的uevent_ops
对应源码在 lib/kobject_uevent.c:183 到 lib/kobject_uevent.c:196。
然后它会准备:
ACTION=DEVPATH=SUBSYSTEM=
见 lib/kobject_uevent.c:238 到 lib/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
这两个也很直接:
firmware_kobj在 drivers/base/firmware.c:18hypervisor_kobj在 drivers/base/hypervisor.c:16
它们都采用:
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_kset 在 drivers/base/core.c:1381 创建:
devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);
它对应 /sys/devices。
14.3 设备初始化
在 drivers/base/core.c:654 的 device_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:976 的 device_add() 里,会执行:
error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
之后再:
kobject_uevent(&dev->kobj, KOBJ_ADD);
这条链路说明:
- 设备能出现在
/sys/devices - 设备能发热插拔事件
底层都与 kobject 直接相关。
15. 真实实例四:class_kset 与 /sys/class
class_kset 在 drivers/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:193 到 drivers/base/class.c:201。
16. 真实实例五:bus_kset 与 /sys/bus
bus_kset 在 drivers/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:892 到 drivers/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:904 到 drivers/base/bus.c:912。
这就是为什么你在 sysfs 里会看到:
/sys/bus/platform/devices/sys/bus/platform/drivers
这样的组织结构。
[!tip] 这里是理解
kset最直观的地方:
kset不是一个抽象名词,它就是 sysfs 里一层层“集合目录”的直接实现基础。
17. device、class、bus 与 kobject / kset 的包含关系图
前面我们已经分别讲了 struct device、class_kset、bus_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 个:
struct device是最“直接”的,它本体里就内嵌了struct kobjectstruct class和struct bus_type本体里没有直接放kobject,而是通过subsys_private这个私有包装结构承载对象模型subsys_private里面嵌的是struct kset subsys,而kset里面又嵌着一个kobject- 所以
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 为什么 device 和 class / bus 的组织方式不一样
从建模角度看,这是很自然的:
device是单个对象,它的重点是“我自己是谁”class和bus是组织容器,它们的重点是“我下面管理哪些对象”
所以:
device直接内嵌kobjectclass/bus倾向于通过kset组织子对象
这也解释了为什么我们前面在源码里看到:
device_initialize()里直接kobject_init(&dev->kobj, &device_ktype),见 drivers/base/core.c:654class/bus注册时则会走kset_register(&cp->subsys)或kset_register(&priv->subsys),见 drivers/base/class.c:201 和 drivers/base/bus.c:896
18. sysfs 的 show/store 完整调用链
这一节专门回答一个学习内核时很容易卡住的问题:
用户空间执行一次
cat /sys/.../foo或echo 1 > /sys/.../foo,到底是怎么一路走到驱动或内核对象自己的show/store回调里的?
如果把这件事说短一点:
- VFS 并不是直接调用
kobject - VFS 先落到
kernfs kernfs再根据sysfs预先注册好的kernfs_ops反调到sysfssysfs再根据kobject -> ktype -> sysfs_ops找到真正的show/store- 最后才进入具体对象类型自己的属性回调
18.1 先回答:sysfs 文件是怎么建出来的
先别急着看读写,先看这个文件从哪来。
常见有两条路径:
kobj_type.default_attrs- 手动
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);
调用链会进入:
sysfs_create_group()→ fs/sysfs/group.c:140internal_create_group()→ fs/sysfs/group.c:94create_files()→ fs/sysfs/group.c:35sysfs_add_file_mode_ns()→ fs/sysfs/file.c:241__kernfs_create_file()→ fs/sysfs/file.c:297
也就是说,最后真正把文件节点建出来的是 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 层职责:
sysfs_create_group()只是一个薄入口,真正逻辑在internal_create_group(),见 fs/sysfs/group.c:140。internal_create_group()决定“这组属性挂在哪个目录下”:- 有
grp->name就先建一个子目录; - 没有
grp->name就直接挂在kobj->sd对应目录下,见 fs/sysfs/group.c:94。
- 有
create_files()负责把attribute_group展开成一个个具体属性,同时处理grp->is_visible()、权限过滤、失败回滚,见 fs/sysfs/group.c:35。sysfs_add_file_mode_ns()负责把“sysfs 层的属性语义”翻译成“kernfs 层的文件语义”:- 它先从
parent->priv找回这个目录对应的kobject; - 再从
kobj->ktype->sysfs_ops判断这个属性最终能不能读、能不能写; - 然后选择合适的
kernfs_ops,见 fs/sysfs/file.c:247。
- 它先从
__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 创建文件时,sysfs 往 kernfs 填了什么关键信息
这一步很关键,因为后面读写能不能走回正确回调,全靠这里提前埋好“线索”。
在 fs/sysfs/file.c:247 到 fs/sysfs/file.c:298 里,sysfs_add_file_mode_ns() 做了两件特别重要的事:
- 根据
kobj->ktype->sysfs_ops选择合适的kernfs_ops - 把
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_file_fops.read = kernfs_fop_read(),见 fs/kernfs/file.c:886
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 对应的是:
kernfs_seq_show(),见 fs/kernfs/file.c:150
它做的事情很短,但非常关键:
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:190 和 fs/sysfs/file.c:198。
于是调用继续进入:
sysfs_kf_seq_show(),见 fs/sysfs/file.c:42
它会做两件关键事情:
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取回的是这个文件所属目录的kobjectof->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 常常是:
kobj_sysfs_ops,见 lib/kobject.c:783
里面:
.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:310 到 fs/kernfs/file.c:312。
18.5.2 第二层:kernfs 回到 sysfs_kf_write()
对普通 sysfs 属性文件,ops->write 通常是:
sysfs_kf_write(),见 fs/sysfs/file.c:122
它会继续:
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,则会进入:
kobj_attr_store(),见 lib/kobject.c:771
再由它做:
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_attributeDEVICE_ATTR(...)
这时最终走到的 sysfs_ops 就不是 kobj_sysfs_ops,而是设备自己的:
dev_sysfs_ops,定义在 drivers/base/core.c:141
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:304 到 fs/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。
关键代码在:
主线非常简单:
kobject_create_and_add("kobject_example", kernel_kobj)sysfs_create_group(example_kobj, &attr_group)
效果是:
- 在
/sys/kernel/下创建一个kobject_example - 里面挂 3 个属性文件:
foo、baz、bar
这个例子适合理解:
- 如何快速创建一个 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_ktype 在 samples/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 devicestruct classstruct bus_type
因为这些对象已经把:
kobjectkset- 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 太早
正确顺序应该是:
kobject_init_and_add()- 建好属性
- 再
kobject_uevent(KOBJ_ADD)
23.5 没搞清 parent 和 kset 的差别
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事件]
最终你应该建立下面这几个直觉:
kobject是“对象最小公共壳”kset是“对象集合 + 子系统组织者”kobj_type是“对象行为模板”sysfs是它们对用户空间的文件化呈现uevent是它们对用户空间的事件化通知
25. 一段最实用的结论
如果只保留一句最有用的话:
kobject解决“单个对象如何被内核统一管理并映射到 sysfs”,kset解决“多个对象如何作为一个子系统被组织、查找并统一发 uevent”。struct device、class、bus这些你平时最常见的驱动模型对象,本质上都建立在这套机制之上。
26. 推荐继续阅读路径
如果你想继续顺着这条线往下打通,建议按这个顺序看:
samples/kobject/kobject-example.csamples/kobject/kset-example.cdrivers/base/core.cdrivers/base/class.cdrivers/base/bus.clib/kobject.clib/kobject_uevent.c
再配合下面这些已有笔记一起看,会更顺:
- [[iMX6ULL设备树从DTS到设备创建全过程详解]]
- [[device_node转换为platform_device的完整调用链路]]
- [[Linux GPIO子系统从零基础到驱动开发与使用开发完全指南]]
- [[struct device到device_add再到sysfs与uevent完整链路详解]]