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。

kset解决的是“批量管理”问题:统一目录、统一属性、统一事件。kobject解决的是“单个对象”问题:引用计数、生命周期、sysfs入口。

二、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。其核心流程是:

  1. 设备注册时调用kobject_uevent(),传入KOBJ_ADD等动作。
  2. 内核沿kobject的父链向上查找,找到所属kset的uevent_ops。
  3. 调用uevent_ops->filter()判断是否允许上报,再调用uevent()追加环境变量。
  4. 通过netlink广播UEVENT消息,或调用/sbin/hotplug。
  5. 用户空间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
调试uevent时,可用 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 关系结构 kset(容器) kobj → sysfs 目录 list → 挂载多个 kobject ktype → 默认属性模板 uevent_ops → 事件过滤 kobject A kobject B kobject C ktype release() sysfs_ops default_attrs show / store 属性/释放 uevent 上报链路 kobject_uevent uevent_ops filter / uevent netlink 广播 UEVENT 消息 udev/mdev 创建 /dev ACTION=add / DEVPATH / SUBSYSTEM / MAJOR / MINOR / DEVNAME

总结来看,kset负责“把同类对象组织成树”,ktype负责“定义这类对象的属性与释放方式”,uevent则负责“把对象状态变化通知用户空间”。三者配合,才构成了Linux设备模型中可管理、可观察、可自动化的完整闭环。