4. kset与ktype
在Linux设备模型中,kobject是最小的引用计数对象,但它本身只解决“存在性”和“生命周期”问题,并不负责“归类”和“批量操作”。真正让设备模型形成树状层次、并支持统一属性读写与事件上报的,是kset与ktype这一对组合。理解它们,是掌握sysfs目录组织、设备热插拔通知以及udev/mdev自动创建设备节点的关键。
一、kset的组织结构:同类kobject的容器
kset本质上是“一组同类型kobject的集合”,它把多个kobject组织在同一个sysfs目录下,并维护它们之间的父子关系。kset自身也内嵌一个kobject,因此kset在sysfs中同样表现为一个目录。典型结构如下:
- kset.kobj:kset自身的kobject,对应sysfs中的目录节点。
- kset.list:链表头,挂载属于该kset的所有kobject。
- kset.ktype:指向该kset默认使用的ktype,决定属性与uevent行为。
- kset.uevent_ops:该kset的热插拔事件操作集,负责过滤与变量追加。
当调用kobject_add()把一个kobject加入某个kset时,内核会把它挂到kset.list上,并在sysfs中创建对应目录。kset与kobject之间是“一对多”的聚合关系,而kset与父kset之间又可以形成嵌套,从而构成设备树。例如/sys/devices、/sys/bus、/sys/class都是顶层kset,其下再挂载具体子系统kset。
二、ktype的操作方法:属性与释放策略
ktype(struct kobj_type)描述“某一类kobject长什么样、怎么操作”。它通常包含三个核心成员:
| 成员 | 作用 | 典型实现 |
|---|---|---|
| release | 引用计数归零时释放对象 | kfree() 或 container_of 后释放 |
| sysfs_ops | 属性show/store回调 | device_show / device_store |
| default_attrs | 默认属性数组 | dev_attr_* 集合 |
操作方法上,ktype通过sysfs_ops把用户空间的read()/write()映射到内核的show()/store()。当用户读取/sys/.../attr时,内核找到对应kobject的ktype,再调用其show;写入时调用store。释放路径则由release兜底,确保kobject被真正回收。
static struct kobj_type my_ktype = {
.release = my_release,
.sysfs_ops = &my_sysfs_ops,
.default_attrs = my_default_attrs,
};
static void my_release(struct kobject *kobj)
{
struct my_obj *obj = container_of(kobj, struct my_obj, kobj);
kfree(obj);
}
需要注意:kset可以指定ktype,kobject也可以单独指定ktype。若kobject未显式指定,则继承所属kset的ktype。这种“默认继承 + 局部覆盖”的机制,使同类设备共享属性模板,又允许个别对象定制行为。
三、热插拔事件与uevent机制
当设备被添加或删除时,内核需要通知用户空间,以便udev/mdev创建或删除/dev节点、加载驱动模块。这条通知链路就是uevent。其核心流程是:
- 设备注册时调用
kobject_uevent(),传入KOBJ_ADD等动作。 - 内核沿kobject的父链向上查找,找到所属kset的
uevent_ops。 - 调用
uevent_ops->filter()判断是否允许上报,再调用uevent()追加环境变量。 - 通过netlink广播
UEVENT消息,或调用/sbin/hotplug。 - 用户空间udev接收后,读取sysfs属性,创建设备节点并执行规则。
常见的uevent环境变量包括ACTION、DEVPATH、SUBSYSTEM、MAJOR、MINOR、DEVNAME等。下面是一个典型的内核日志片段:
KOBJ_ADD: DEVPATH=/devices/virtual/misc/mydev
SUBSYSTEM=misc
ACTION=add
MAJOR=10
MINOR=58
DEVNAME=mydev
udevadm monitor --kernel --property 实时观察内核发出的uevent消息,快速定位属性缺失或filter拦截问题。
四、案例与常见误区复盘
假设我们要实现一个虚拟字符设备,并希望它出现在/sys/class/myclass/mydev下,同时热插拔时自动创建/dev/mydev。正确做法是:先创建myclass这个kset,再创建kobject并指定ktype,最后在设备注册完成后调用kobject_uevent(&kobj, KOBJ_ADD)。若只创建了kobject却没有加入kset,sysfs目录会缺少父级,udev也无法通过SUBSYSTEM匹配规则。
常见误区包括:
- 忘记实现release:kobject引用计数归零时内核会告警,对象无法释放。
- 在uevent中做耗时操作:uevent运行在原子上下文,不能睡眠。
- 混淆kset与ktype:kset管“归类与事件”,ktype管“属性与释放”。
- filter返回0导致事件被静默丢弃:用户空间永远收不到通知。
下面这张图展示了kset、kobject、ktype与uevent之间的整体关系:kset作为容器聚合多个kobject,ktype为它们提供属性与释放策略,而uevent_ops则把设备状态变化沿父链上报到用户空间。
总结来看,kset负责“把同类对象组织成树”,ktype负责“定义这类对象的属性与释放方式”,uevent则负责“把对象状态变化通知用户空间”。三者配合,才构成了Linux设备模型中可管理、可观察、可自动化的完整闭环。