Linux设备模型核心:kset、kobject与ktype的职责与协作

发布时间:2026/10/27 19:52:56
Linux设备模型核心:kset、kobject与ktype的职责与协作 1. 从一次设备节点混乱说起为什么需要 kset/kobj/ktype 这层抽象很多人在接触 Linux 设备模型时第一反应是能注册设备、能出/sys节点就行了于是直接拿device_create或者更底层的kobject_create_and_add拼凑代码。我最早也是这么干的结果在一个模拟项目里注册了十几个虚拟设备节点跑起来之后发现/sys下的目录层级完全乱套本该属于同一类的设备散落在不同父目录下热插拔事件上报时找不到统一的归属卸载模块时还得手动一个个清理漏一个就留下悬空引用。那次排查花了我整整一个下午最后才意识到问题不在注册这一步而在于我压根没用对kset、kobject、ktype这三件套的分工。这篇内容就是围绕这三个 API 展开的。kobject是设备模型里最基础的引用计数对象kset是一组同类型kobject的集合容器ktype则定义了某一类kobject的默认行为释放、属性、uevent 等。三者配合构成了/sys目录树、设备分类、热插拔通知的底层骨架。适合谁看如果你已经能写一个简单的字符设备驱动但对/sys下那些目录是怎么长出来的、kset和kobject到底谁管谁、ktype的release为什么必须实现这些问题还犯迷糊那这篇就是给你准备的。我会从实际代码结构出发把三者的职责边界、注册流程、引用计数陷阱、以及我踩过的几个典型坑都摊开讲清楚。需要先说明一点内核版本差异比较大kobject的初始化接口在早期是kobject_init配合手动赋值后来演进出kobject_init_and_add这类封装kset的创建也从静态定义转向kset_create_and_add动态创建。下面讲的内容以较新的动态创建方式为主线遇到老接口会顺带提一句差异你对照自己手上的内核版本取舍。2. kobject引用计数与 sysfs 入口的最小单元2.1 kobject 到底承载了什么把kobject理解成一个能被/sys看见的对象的身份证 计数器最贴切。它本身不关心你是设备、驱动还是总线只负责三件事维护引用计数refcount_t、挂到某个kset里形成层级、在/sys下对应一个目录项。源码里struct kobject的核心字段大致是这些struct kobject { const char *name; /* sysfs 目录名 */ struct list_head entry; /* 挂入所属 kset 的链表节点 */ struct kobject *parent; /* 父对象决定 sysfs 层级 */ struct kset *kset; /* 所属集合 */ struct kobj_type *ktype; /* 行为描述 */ struct kernfs_node *sd; /* sysfs 目录项 */ refcount_t kref; /* 引用计数 */ /* ... 省略状态位等字段 */ };这里有个容易忽略的点parent和kset是两个独立的概念。parent决定/sys里的目录嵌套关系kset决定它属于哪个逻辑集合。大多数情况下两者一致父对象就是 kset 内嵌的那个 kobject但完全可以出现父目录是 A却属于集合 B的情况。我见过有人把这两个混为一谈结果kset的 uevent 广播发不到预期对象上。引用计数的意义在于生命周期管理。kobject_get加一kobject_put减一减到零触发ktype-release回调做真正的内存释放。这个设计的好处是只要还有人持有引用对象就不会被提前销毁避免了用着用着内存没了的经典悬空指针问题。2.2 初始化与注册的标准姿势动态创建一个 kobject 并挂到 sysfs最常用的接口是int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype, struct kobject *parent, const char *fmt, ...);参数含义很直白kobj是你自己分配的结构体内嵌的那个 kobjectktype指定行为parent指定父目录传NULL就挂到/sys根下最后是变参格式化的目录名。调用成功后/sys/parent/name目录就出现了。这里必须强调一个顺序问题先kobject_init_and_add再暴露给外部使用。我踩过的坑是这样的——在初始化完成之前就把 kobject 指针存进了全局链表另一个线程恰好此时遍历链表拿到它并调用kobject_get而此时kref还没被正确初始化计数从垃圾值开始加后面kobject_put永远减不到零对象泄漏。正确做法是初始化成功后再发布指针发布动作本身要配合同步原语比如加锁或者rcu_assign_pointer。对应的清理路径是kobject_put不是kfree。很多人第一次写会直接kfree(kobj)结果release回调没被调用sysfs 目录项残留后续同名注册直接失败。记住kobject 的销毁权在引用计数手里你只负责 put。2.3 release 回调为什么不能省kobj_type里的release是强制的内核在kobject_put减到零时会调用它。如果你不实现内核会打印警告并且对象内存永远不会被回收。这个回调里该做什么释放你包裹 kobject 的那层结构体内存以及清理你在初始化阶段申请的其他资源。static void my_obj_release(struct kobject *kobj) { struct my_obj *obj container_of(kobj, struct my_obj, kobj); kfree(obj); /* 释放外层结构体kobject 是内嵌的 */ }container_of这个宏在这里是标配因为 kobject 通常是内嵌在更大的结构体里的release 拿到的是内嵌成员的指针得反推回外层。这个模式在设备模型里到处都是device、driver、bus_type全是这么组织的。有个细节值得说release 回调的执行上下文不保证是进程上下文也不保证能睡眠所以里面不要做可能阻塞的操作比如拿 mutex、调kmalloc(GFP_KERNEL)。我一般只做kfree这类轻量释放复杂清理放到显式的销毁函数里在 put 之前完成。3. kset把同类 kobject 收拢成一个集合3.1 kset 与 kobject 的包含关系kset本质上是一组 kobject 的容器 一个内嵌的 kobject 作为集合自身的代表。源码结构大致是struct kset { struct list_head list; /* 集合内所有 kobject 的链表 */ spinlock_t list_lock; struct kobject kobj; /* 集合自身的 kobject对应 sysfs 目录 */ const struct kset_uevent_ops *uevent_ops; /* 热插拔事件策略 */ /* ... */ };注意kobj是内嵌的这意味着每个 kset 在/sys下也有自己的目录。当你把一个 kobject 加进 kset通过kobject_add时指定 parent 为 kset 的 kobj或者直接设置kobj-kset它会同时进入list链表并在 sysfs 里成为 kset 目录的子项。创建 kset 的现代接口是struct kset *kset_create_and_add(const char *name, const struct kset_uevent_ops *uevent_ops, struct kobject *parent_kobj);name是 sysfs 目录名uevent_ops决定这个集合里的对象在热插拔时怎么上报parent_kobj指定挂载位置。销毁用kset_unregister它内部会对内嵌 kobject 做 put。3.2 为什么用 kset 而不是自己维护链表有人会问我不就是想要个同类对象列表吗自己搞个struct list_head加锁维护不行吗行但你得自己解决三件事sysfs 目录的自动创建与层级、热插拔事件的统一上报、以及对象销毁时的自动摘链。kset 把这三件事都包了。具体说当 kobject 通过kobject_add挂到 kset 上时内核自动把它加入listkobject_del时自动摘除。你不需要在 release 里手动list_del否则会二次删除导致链表损坏——这个坑我见过不止一次表现是卸载模块时偶发 panic堆栈指向__list_del_entry_valid。热插拔这块kset_uevent_ops提供三个回调filter决定某对象是否上报、name生成DEVTYPE之类的标识、uevent追加自定义环境变量。如果你有一批虚拟设备需要用户态感知插拔用 kset 统一管理比每个设备单独发 netlink 消息干净得多。3.3 kset 的引用计数与生命周期kset 内嵌的 kobject 也有引用计数kset_get/kset_put是对它的封装。这里有个容易混淆的地方kset 的引用计数和它内部成员的引用计数是独立的。一个 kobject 被加进 kset 时并不会自动给 kset 加引用反过来 kset 销毁时也不会自动清理还在里面的成员内核会打印警告说kset 还有成员未移除。所以正确的销毁顺序是先把集合内所有 kobject 逐个kobject_delkobject_put确认list空了再kset_unregister。我在一个模拟项目里图省事直接kset_unregister了事结果内核日志刷了一屏 kobject: %s (%p): is not initialized, yet kobject_put() is being called虽然没立刻崩但那些残留对象的 release 再也不会被触发内存就这么漏了。4. ktype给一类 kobject 定义统一行为4.1 ktype 与 kset 的职责区分新手最容易把ktype和kset搞混因为这俩名字都带类型/集合的意味。一句话区分kset 管谁和谁是一伙的ktype 管这一伙对象长什么样、怎么死。struct kobj_type的核心字段struct kobj_type { void (*release)(struct kobject *kobj); /* 销毁回调必填 */ const struct sysfs_ops *sysfs_ops; /* 属性读写方法 */ const struct attribute_group **default_groups; /* 默认属性组 */ /* ... */ };release前面讲过了。sysfs_ops配合default_groups决定这个 kobject 在 sysfs 下暴露哪些属性文件、读写时走哪个函数。比如你有一类传感器对象每个都要暴露value和enable两个属性就把属性定义放进default_groups读写逻辑放进sysfs_ops所有用这个 ktype 初始化的 kobject 自动获得这套属性不用一个个手动sysfs_create_file。这个批量定义的能力是 ktype 最大的价值。我早期写驱动时每个设备手动建属性文件十几个设备就是十几份重复代码改一个属性名要改十几处。后来统一到 ktype 的default_groups属性定义只写一份清爽太多。4.2 一个 ktype 服务多个 kset 的典型场景ktype 和 kset 不是一一绑定的。同一个 ktype 可以被不同 kset 里的 kobject 共用只要这些对象的销毁逻辑和属性集合一致。举个实际的结构假设你有一批虚拟通道对象分成输入通道和输出通道两个 kset但两类通道的释放逻辑、属性文件完全相同那就定义一个 ktype两个 kset 里的 kobject 初始化时都传这个 ktype。反过来同一个 kset 里的对象理论上也可以用不同 ktype但这么做通常没意义因为 kset 的语义就是同类集合。我建议保持一个 kset 对应一个 ktype的简单映射除非有明确理由打破它否则维护成本会上升。4.3 default_groups 的属性定义实操属性组的标准写法static ssize_t value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { struct my_obj *obj container_of(kobj, struct my_obj, kobj); return sysfs_emit(buf, %d\n, obj-value); } static ssize_t value_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) { struct my_obj *obj container_of(kobj, struct my_obj, kobj); int ret, val; ret kstrtoint(buf, 0, val); if (ret) return ret; obj-value val; return count; } static struct kobj_attribute value_attr __ATTR(value, 0644, value_show, value_store); static struct attribute *my_obj_attrs[] { value_attr.attr, NULL, }; ATTRIBUTE_GROUPS(my_obj); static struct kobj_type my_obj_ktype { .release my_obj_release, .sysfs_ops kobj_sysfs_ops, .default_groups my_obj_groups, };ATTRIBUTE_GROUPS宏会自动生成my_obj_groups数组省去手写attribute_group结构体的样板代码。sysfs_emit是较新内核推荐的输出函数比sprintf安全它会检查缓冲区边界。kstrtoint比simple_strtol更严格会拒绝非法输入建议统一用它。有个细节__ATTR的权限位0644表示用户可读写、组和其他只读。如果你希望属性只读用0444并且store传NULL。权限位写错不会导致编译错误但运行时会发现写文件返回EACCES排查时容易误以为是逻辑问题。5. 三者协作的完整注册与销毁链路5.1 从零搭一个最小可用的对象体系把前面三块拼起来一个完整的注册流程大致是这样以动态创建为例struct my_obj { struct kobject kobj; int value; }; static struct kset *my_kset; static int __init my_init(void) { struct my_obj *obj; int ret; /* 1. 创建 kset挂到 /sys/kernel 下 */ my_kset kset_create_and_add(my_objects, NULL, kernel_kobj); if (!my_kset) return -ENOMEM; /* 2. 分配并初始化一个成员对象 */ obj kzalloc(sizeof(*obj), GFP_KERNEL); if (!obj) { ret -ENOMEM; goto err_kset; } /* 3. 初始化 kobject父对象设为 kset 的内嵌 kobj */ ret kobject_init_and_add(obj-kobj, my_obj_ktype, my_kset-kobj, obj0); if (ret) { kobject_put(obj-kobj); /* 注意失败也要 put */ goto err_kset; } return 0; err_kset: kset_unregister(my_kset); return ret; }这里有个非常关键的细节kobject_init_and_add失败时仍然需要调用kobject_put。因为kobject_init内部已经把引用计数初始化为 1add 失败只是没挂上 sysfs计数还在。不 put 就泄漏。这个行为很多人不知道我第一次遇到时以为是内核 bug翻了源码才明白设计如此。销毁路径反过来static void __exit my_exit(void) { /* 假设 obj 指针保存在某处 */ kobject_put(obj-kobj); /* 触发 release释放 obj */ kset_unregister(my_kset); /* 销毁集合 */ }顺序不能反。先 put 成员再 unregister 集合。如果先 unregister 集合成员对象的parent指向的 kobject 已经没了后续 put 时访问parent可能出问题。5.2 引用计数在协作中的传递关系把引用计数的流转画清楚很多 bug 就自然消失了。我总结成一张表操作对 kobject 计数的影响对 kset 计数的影响kobject_init_and_add成功1初始持有无直接变化kobject_get1无kobject_put-1减到 0 触发 release无kset_create_and_add无1内嵌 kobj 初始持有kset_get无1kset_unregister无-1减到 0 触发 kset 释放从表里能看出kset 和成员 kobject 的计数是两条独立的线。这也是为什么销毁时必须两边都处理。我见过一种错误写法以为kset_unregister会自动 put 所有成员于是只调它结果成员全部泄漏。内核不会替你做这件事因为成员可能还被别处引用着贸然清理会破坏引用语义。5.3 常见注册失败点与排查顺序注册链路出问题时按这个顺序排查效率最高同名冲突/sys下已存在同名目录kobject_add返回-EEXIST。检查是不是上次卸载没清理干净或者两个模块用了同一个名字。ktype 为 NULL 或 release 未实现内核会警告并拒绝。确认kobj_type结构体里release字段非空。parent 已销毁父 kobject 被提前 put 了add 时访问悬空指针。检查父对象的生命周期是否覆盖子对象。内存分配失败kzalloc返回 NULL通常是上下文用错了 GFP 标志在原子上下文用了GFP_KERNEL。我一般在这几个点各加一条pr_debug出问题时看日志就能定位到具体哪一步。比盲目加 printk 高效得多。6. 几个让我印象深刻的踩坑记录6.1 在 release 里访问 kset 导致的 UAF有一次我在release回调里想顺手把对象从某个自定义链表摘掉而那个链表的头挂在 kset 的私有数据里。代码大概是这样static void my_release(struct kobject *kobj) { struct my_obj *obj container_of(kobj, struct my_obj, kobj); struct my_kset_priv *priv ...; /* 从 kset 取私有数据 */ list_del(obj-node); /* 访问了 kset 相关结构 */ kfree(obj); }问题在于如果 kset 已经被kset_unregister销毁priv就是悬空指针list_del直接踩到已释放内存。表现是偶发 panic而且只在先销毁 kset 再 put 成员的错误顺序下触发复现率不高排查了很久。修复方式是把链表操作从 release 里挪出来放到显式的销毁函数里在 put 之前完成。release 只做纯粹的kfree。这个教训让我形成了一个习惯release 回调里只碰 kobject 自身和外层结构体绝不访问任何可能先于它销毁的外部结构。6.2 kset 名字里的斜杠引发的目录错乱kset_create_and_add的name参数如果包含/内核会把它当成路径分隔符在 sysfs 里创建多级目录。我当初为了分类清晰写了个group/subgroup这样的名字结果目录是建出来了但后续kobject_add时 parent 指向 kset 的 kobj路径拼接出现意外嵌套/sys下多出一层莫名其妙的目录。sysfs 的目录名规则比想象中严格不能含/不能是.或..长度也有限制。命名时用下划线或短横线代替分隔符比如group_subgroup。这个规则对 kobject 的name同样适用。6.3 属性文件读写时的并发问题default_groups里的属性读写函数默认没有加锁多个进程同时读写同一个属性会竞争。我早期的一个计数器属性就吃过亏两个进程同时store读-改-写不是原子的最终值比预期少。修复很简单在store里加个 mutex 或者用atomic_tstatic ssize_t value_store(...) { int val, ret kstrtoint(buf, 0, val); if (ret) return ret; mutex_lock(obj-lock); obj-value val; mutex_unlock(obj-lock); return count; }注意 mutex 不能用在中断上下文如果属性可能被中断路径访问得换成 spinlock 或者atomic_set。这个选择取决于你的数据被谁访问没有通用答案。6.4 忘记处理 kobject_init_and_add 的失败返回值这个坑最基础但最常见。kobject_init_and_add返回非零时对象处于已初始化但未挂载的中间状态必须 put 掉。我见过有人直接return ret对象泄漏。更隐蔽的是如果这个函数在循环里被调用多次每次失败都漏一个跑久了内存曲线缓慢上升很难第一时间联想到是这里。统一写法是ret kobject_init_and_add(...); if (ret) { kobject_put(obj-kobj); return ret; }把 put 和失败处理绑死形成肌肉记忆。7. 关于接口选型与版本差异的几点经验7.1 什么时候用 kset什么时候直接 kobject不是所有场景都需要 kset。如果你只有一个孤立的 kobject或者几个对象之间没有同类集合的语义直接kobject_init_and_add挂到合适的 parent 下就行不必为了用而用。kset 的价值在批量管理 统一 uevent 自动链表维护当你有三个以上同类对象、或者需要热插拔上报时它才划算。反过来如果你发现自己在手动维护一个list_head来跟踪同类 kobject那基本就是该上 kset 的信号了。自己维护链表意味着你要处理加锁、摘链、销毁顺序这些 kset 都替你做了。7.2 新旧接口的对照不同内核版本接口差异集中在初始化和创建上整理成表方便对照用途较新接口早期接口创建 ksetkset_create_and_addkset_createkset_register初始化 kobjectkobject_init_and_addkobject_initkobject_add创建并添加kobject_create_and_add手动 alloc init add属性输出sysfs_emitsprintf/scnprintf老接口并非不能用只是步骤多、容易漏掉中间的错误处理。新接口把初始化 添加合并成一个原子操作失败时语义更清晰。如果你维护的代码要兼容多个内核版本可以用宏做条件编译但更实际的做法是锁定目标内核版本用对应版本的接口避免为了兼容引入一堆#if。7.3 调试设备模型的实用手段排查 kset/kobject 问题时我常用的几个手段看/sys实际结构find /sys/kernel/my_objects -type d确认目录层级是否符合预期。看引用计数/sys/kobject/name下有uevent等文件但计数不直接暴露得靠内核日志里的kobject调试信息需要开CONFIG_DEBUG_KOBJECT。开CONFIG_DEBUG_KOBJECT_RELEASE这个选项会让 release 延迟执行专门用来暴露release 之后还有人访问的 UAF 问题排查悬空引用时非常有用。dmesg过滤dmesg | grep -i kobject能看到内核关于 kobject 的所有警告包括未初始化就 put、release 缺失等。这些手段配合起来大部分设备模型的问题都能定位。关键是要养成注册完立刻看/sys确认结构的习惯别等到出问题再回头查。8. 把三个 API 串成一条可复用的心智模型写到这里我想给这三个 API 一个统一的心智模型方便你在实际编码时快速判断该用哪个。把设备模型想象成一栋办公楼kobject是每个房间的门牌和钥匙引用计数决定谁能进、什么时候拆kset是楼层或部门把同类房间归到一起统一管理进出和广播ktype是房型模板决定这类房间配什么家具、怎么拆。你建一个新房间先选房型ktype再决定放哪层kset 或 parent最后挂牌kobject_init_and_add。拆房间时先交钥匙put再撤楼层unregister。这个类比不完美但足够帮你在写代码时快速定位职责。实际项目里我建议把创建 kset → 定义 ktype → 批量初始化成员封装成一个模块级的 init 函数销毁路径对称地封装成 exit 函数中间的错误处理用goto链统一收口。这样即使成员数量变化注册和销毁逻辑也不用改维护成本最低。最后分享一个我自己的检查清单每次写完涉及这三个 API 的代码提交前过一遍release 是否只做轻量释放、kobject_init_and_add失败是否 put、kset 销毁前成员是否清空、属性读写是否考虑了并发、sysfs 名字是否含非法字符。这五条覆盖了我这些年踩过的绝大多数坑希望对你有用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询