资讯动态

Linux设备模型核心:kobject、kset与ktype API详解

发布时间:2026/10/27 2:59:56 来源:尧图企业网站定制
1. 为什么这三个API值得单独拎出来讲搞Linux设备模型的人绕不开kset、kobject、ktype这三样东西。很多人第一次看内核源码里的驱动注册流程看到kobject_init_and_add、kset_create_and_add、ktype回调这些字眼脑子里是一团浆糊——明明device、driver、bus这些抽象已经够用了为什么底层还要再套一层kobject这三个API到底谁管谁、谁调谁、什么时候该用哪个我最初接触这块是在做一个自定义总线的字符设备驱动当时想让设备节点在/sys下呈现出清晰的层级结构结果被kobject的引用计数和ktype的release回调折腾了整整两天。后来把这三个API的关系彻底理清楚才发现它们其实是设备模型的地基kobject是对象本身kset是对象的集合ktype是集合里对象的“行为模板”。三者配合才撑起了/sys下那棵庞大的设备树。这篇内容适合两类人看一类是正在写驱动、需要在/sys下暴露自定义属性的开发者另一类是读内核源码时被kobject引用计数绕晕、想搞明白kobject_put到底什么时候触发释放的工程师。我会从设计思路讲到实操代码把每个API的参数含义、调用时机、踩坑点都摊开说尽量让你看完能直接照着写。需要先说明一点本文涉及的代码示例基于常见的Linux内核版本4.x到6.x区间的通用接口不同版本在细节上可能有微调比如kobject_init_and_add的返回值处理、kobj_type结构体成员的变化实际使用时请以你手上的内核头文件为准。下面所有示例都是模拟项目不涉及任何真实产品。2. 三个API的职责边界与设计思路拆解2.1 kobject设备模型的最小原子kobject是整个设备模型的基石你可以把它理解成“一个能被/sys看见的对象”。它本身不关心你是设备、驱动还是总线只负责几件事引用计数、父子层级关系、在sysfs里的名字、以及一个指向kobj_type的指针。结构体里最关键的几个成员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目录项 struct kref kref; // 引用计数 // ... 省略其他 };为什么要有引用计数因为/sys下的对象生命周期和用户态访问是异步的。用户态可能正在读某个属性文件而内核这边驱动已经决定卸载设备了。如果直接kfree用户态就会踩到野指针。kref的作用就是只要还有人持有引用比如正在读文件、或者父对象还挂着它就不真正释放等最后一个kobject_put调用时才走release流程。这里有个设计上的取舍值得说kobject的name和parent决定了它在sysfs里的路径但kobject本身并不直接创建目录目录的创建发生在kobject_add时。也就是说你可以先kobject_init把对象初始化好配好名字和父节点再择机add进去。这种“初始化”和“注册”分离的设计是为了让调用者有机会在对象真正暴露给用户态之前完成所有字段的填充避免出现半成品状态被访问。2.2 kset同类型对象的容器kset本质上是“一组kobject的集合”它自己内嵌了一个kobject注意是内嵌不是指针所以kset本身也能在/sys下有一个目录。这个设计很巧妙集合本身也是一个对象可以有自己的父节点、自己的属性文件。struct kset { struct list_head list; // 挂载的所有kobject spinlock_t list_lock; struct kobject kobj; // 内嵌的kobject const struct kset_uevent_ops *uevent_ops; // uevent处理 };kset存在的意义是什么如果只有kobject每个对象各自为政你就没法批量管理、没法统一在集合目录下放公共属性、也没法在集合层面做uevent过滤。举个例子一个自定义总线下的所有设备如果每个设备都单独挂到/sys/devices下层级会很乱用kset把它们收拢到一个/sys/mybus/devices/目录下既清晰又方便统一处理。kset和kobject的关系容易搞混的地方在于kset内嵌的kobj的kset指针指向自己吗不是。kset内嵌的kobj的kset成员通常为NULL或者指向父集合而集合里成员kobject的kset指针才指向这个kset。这个细节在写代码时如果搞反会导致kobject_add时找不到正确的父目录。2.3 ktype对象的行为模板kobj_type简称ktype定义了一类kobject的共性行为最核心的是release回调其次是sysfs_ops属性读写和default_attrs默认属性组。struct kobj_type { void (*release)(struct kobject *kobj); const struct sysfs_ops *sysfs_ops; const struct attribute_group **default_groups; // ... 新版本还有child_ns_type等 };为什么release必须由ktype提供因为kobject自己不知道它嵌在哪个更大的结构体里。比如一个struct my_device内嵌了kobject当引用计数归零时需要释放的是整个my_device而不是单独kfree那个kobject。release回调里通常用container_of反推出外层结构体再释放。这就是为什么内核文档反复强调没有提供release的ktype是不完整的kobject_put到零时会出问题。default_groups的作用是让一类对象自动带上相同的属性文件。比如所有my_device都该有id、status两个属性就把它们放进一个attribute_group挂到ktype上这样每次kobject_add时属性文件自动创建不用每个设备手动加。这个机制在写批量设备驱动时能省大量重复代码。三者的关系可以用一句话概括kobject是砖kset是把砖码成一摞的托盘ktype是规定这摞砖该怎么码、怎么拆的图纸。托盘本身也是一块特殊的砖内嵌kobj图纸可以给多摞砖共用。3. 核心API逐个拆解与参数实操要点3.1 kobject_init与kobject_init_and_add的区别kobject_init只做初始化把kref置1、设置ktype、初始化链表节点但不创建sysfs目录。kobject_init_and_add则是初始化加注册一步到位内部先调kobject_init再调kobject_add。int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype, struct kobject *parent, const char *fmt, ...);参数里parent为NULL时对象会挂到/sys根下实际是/sys/devices附近取决于内核配置一般不建议这么干容易污染顶层目录。fmt是可变参数用来拼名字比如kobject_init_and_add(dev-kobj, my_ktype, parent, dev%d, id)。实操要点kobject_init_and_add失败时内部已经调用了kobject_init所以失败后必须调kobject_put来释放引用否则引用计数泄漏。这一点很多人会漏因为直觉上“初始化失败应该不用清理”但内核的约定是只要kobject_init成功执行过就必须配对kobject_put。ret kobject_init_and_add(dev-kobj, my_ktype, parent, dev%d, id); if (ret) { kobject_put(dev-kobj); // 必须否则kref泄漏 return ret; }3.2 kobject_add的时机与父子关系单独用kobject_add的场景是你需要先初始化、填一些字段、甚至先挂属性再决定何时暴露。比如某些驱动会先kobject_init等硬件探测完成后再kobject_add。int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...);parent参数会覆盖kobj-parent。如果parent传NULL且kobj-parent也为NULL对象会挂到kset的kobj下如果kobj-kset非空否则挂到顶层。这个优先级顺序是显式parentkobj-parentkset-kobj 顶层。踩坑点如果kobj-kset指向一个还没kset_register的集合kobject_add会失败或者挂到错误位置。所以用kset时务必保证kset先注册。kset_create_and_add内部会完成注册手动构造kset的话要调kset_register。3.3 kset_create_and_add与手动kset_initkset_create_and_add是最省事的创建集合方式struct kset *kset_create_and_add(const char *name, const struct kset_uevent_ops *uevent_ops, struct kobject *parent_kobj);它内部kzalloc一个kset、kset_init、kset_register失败时自动清理。适合集合生命周期和模块生命周期一致的场景。手动方式则是自己分配kset通常内嵌在更大的结构体里然后void kset_init(struct kset *kset); int kset_register(struct kset *kset); void kset_unregister(struct kset *kset);手动方式的好处是kset可以内嵌和你的驱动私有数据放在一起省一次分配。但要注意kset_init只是初始化kset_register才真正把集合的kobj加到sysfs。kset_unregister会调kobject_put如果集合里还有成员没移除行为取决于内核版本新版本会警告。所以卸载顺序应该是先移除所有成员kobject再kset_unregister。3.4 kobject_put与release的触发链路kobject_put递减引用计数到零时调用ktype-release。但这里有个容易误解的点release被调用时sysfs目录已经被移除了吗答案是kobject_del负责移除sysfs目录kobject_put负责释放内存。两者是分开的。标准清理流程是kobject_del(dev-kobj); // 移除sysfs目录断开父子关系 kobject_put(dev-kobj); // 递减引用可能触发release如果你只调kobject_put不调kobject_del引用计数到零时release里如果直接kfreesysfs目录还在但底层对象没了用户态访问会出问题。所以内核在kobject_release里其实会检查并做清理但显式调用kobject_del是更清晰的做法也是内核文档推荐的顺序。release回调的典型写法static void my_dev_release(struct kobject *kobj) { struct my_device *dev container_of(kobj, struct my_device, kobj); kfree(dev); }注意container_of的第三个参数必须是内嵌的kobject成员名写错了会释放错地址这种bug很难查因为不一定立刻崩。4. 从零搭建一个带sysfs层级的模拟驱动4.1 场景设定与结构体设计假设我们要做一个模拟的“传感器集线器”一个myhub集合下挂若干mysensor设备每个设备在/sys/myhub/sensors/sensorN/下有id和value两个属性。这个场景足够小但覆盖了kset、kobject、ktype三者的完整配合。先设计结构体struct mysensor { struct kobject kobj; // 内嵌必须放第一个或配合container_of int id; int value; }; struct myhub { struct kset sensors_kset; // 内嵌集合 // 其他hub私有数据 };mysensor把kobj放第一个这样container_of偏移为0编译器优化后就是直接转换省一次减法。这是内核里常见的写法不是必须但推荐。4.2 ktype与属性组的定义先定义属性读写函数。sysfs_ops的show和store签名是固定的static ssize_t id_show(struct kobject *kobj, struct attribute *attr, char *buf) { struct mysensor *s container_of(kobj, struct mysensor, kobj); return sprintf(buf, %d\n, s-id); } static ssize_t value_show(struct kobject *kobj, struct attribute *attr, char *buf) { struct mysensor *s container_of(kobj, struct mysensor, kobj); return sprintf(buf, %d\n, s-value); } static ssize_t value_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count) { struct mysensor *s container_of(kobj, struct mysensor, kobj); int ret kstrtoint(buf, 10, s-value); return ret ? ret : count; }属性定义用ATTRIBUTE_GROUPS宏或者手动构造attribute_groupstatic struct attribute id_attr { .name id, .mode 0444, }; static struct attribute value_attr { .name value, .mode 0644, }; static struct attribute *mysensor_attrs[] { id_attr, value_attr, NULL, }; ATTRIBUTE_GROUPS(mysensor); // 生成mysensor_groupsATTRIBUTE_GROUPS(mysensor)会展开出一个mysensor_groups[]数组里面包含一个指向mysensor_attrs的attribute_group。这个宏省去了手写attribute_group结构体的麻烦但要注意它生成的数组名是name_groups在ktype里引用时别写错。ktype定义static void mysensor_release(struct kobject *kobj) { struct mysensor *s container_of(kobj, struct mysensor, kobj); kfree(s); } static struct kobj_type mysensor_ktype { .release mysensor_release, .default_groups mysensor_groups, };这里default_groups是较新内核的字段名老版本叫default_attrs直接是attribute **。如果你在4.x早期版本上编译报错检查一下头文件里kobj_type的成员名。4.3 集合创建与设备注册流程模块初始化时先创建ksetstatic struct myhub *hub; static int __init myhub_init(void) { int ret; hub kzalloc(sizeof(*hub), GFP_KERNEL); if (!hub) return -ENOMEM; hub-sensors_kset.kobj.parent NULL; // 挂到顶层或指定父节点 kset_init(hub-sensors_kset); hub-sensors_kset.kobj.ktype NULL; // 集合本身不需要自定义ktype ret kset_register(hub-sensors_kset); if (ret) { kfree(hub); return ret; } // 注册若干模拟设备 for (int i 0; i 3; i) mysensor_create(i); return 0; }注意kset_init之后kset_register之前可以设置kobj.parent和kobj.ktype。kset内嵌的kobj的ktype一般不需要自定义除非你想给集合目录本身加属性文件。如果设了ktype记得提供release否则kset_unregister时可能出问题。设备创建函数static int mysensor_create(int id) { struct mysensor *s; int ret; s kzalloc(sizeof(*s), GFP_KERNEL); if (!s) return -ENOMEM; s-id id; s-value id * 100; ret kobject_init_and_add(s-kobj, mysensor_ktype, hub-sensors_kset.kobj, sensor%d, id); if (ret) { kobject_put(s-kobj); return ret; } return 0; }这里parent传的是hub-sensors_kset.kobj所以设备目录会出现在/sys/.../sensors/sensor0/下。如果传NULLkobject_add会退而用s-kobj.kset但我们在kobject_init_and_add里没设kset所以必须显式传parent。这是新手常犯的错以为kobject_init_and_add会自动把对象挂到某个集合其实不会kset成员需要你手动赋值或者通过parent间接关联。4.4 卸载顺序与引用计数验证卸载时顺序很关键static void __exit myhub_exit(void) { // 先移除所有设备 // 实际项目中需要遍历集合链表这里简化为已知3个 for (int i 0; i 3; i) { struct mysensor *s find_sensor(i); // 模拟查找 if (s) { kobject_del(s-kobj); kobject_put(s-kobj); } } kset_unregister(hub-sensors_kset); kfree(hub); }kobject_del移除sysfs目录并断开父子关系kobject_put递减引用。如果某个设备还被用户态打开着属性文件kobject_put不会立刻触发release而是等文件关闭后的最后一次put。这就是引用计数保护生命周期的体现。验证引用计数是否泄漏可以在release里加打印或者用/sys/kernel/debug/kobject如果内核开了debugfs支持查看。更土的办法是反复加载卸载模块看/sys下目录是否残留、dmesg是否有kobject相关警告。内核在检测到kobject泄漏时通常会打印kobject: %s (%p): is not initialized, yet kobject_put() is being called之类的信息看到这类日志就要查是不是漏了kobject_put或者重复put。5. 常见问题排查与避坑经验实录5.1 release回调不触发或触发两次这是最高频的问题。不触发的原因通常是引用计数没归零可能某处kobject_get后忘了put或者kobject_init_and_add失败后没put导致初始引用泄漏。触发两次则通常是重复put比如kobject_del之后又调了一次kobject_put而kobject_del内部在某些版本里也会影响引用。排查思路在release里打印kobj-name和kref计数需要临时改内核或用kobject_get_path对比预期。更简单的办法是检查代码里所有kobject_get/kobject_put是否成对以及kobject_init_and_add的失败分支是否处理了put。注意kobject_init_and_add失败后必须kobject_put这是内核文档明确要求的不是可选项。5.2 sysfs目录创建了但属性文件缺失如果kobject_add成功、目录出现了但default_groups里的属性文件没生成先检查ktype是否在kobject_add之前就设置好了。kobject_init_and_add的第二个参数就是ktype如果传NULL属性组不会创建。另外检查ATTRIBUTE_GROUPS宏生成的数组名是否和ktype里引用的一致以及attribute的mode是否合法比如0644写成644会被当成八进制解析错误。还有一种情况属性文件创建失败但kobject_add返回成功。内核在属性组创建失败时通常会回滚并返回错误但某些老版本可能只打印警告。所以kobject_init_and_add返回0后最好实际ls一下目录确认属性文件在。5.3 kset_unregister时集合内还有成员新版本内核在kset_unregister时会检查list是否为空非空会打印警告甚至拒绝卸载。正确做法是先遍历kset-list对每个成员kobject_del加kobject_put再kset_unregister。遍历时注意加list_lock如果并发场景或者确保卸载时没有其他路径在往集合里加成员。一个实用的模式是给kset配一个uevent_ops在uevent回调里做过滤但这不是必须的。uevent_ops的filter可以用来阻止某些成员产生ueventuevent回调可以追加环境变量。如果不需要uevent传NULL即可。5.4 父子关系错乱导致目录嵌套异常kobject的parent决定了sysfs路径。如果发现设备目录挂到了意料之外的位置检查三处kobject_init_and_add的parent参数、kobj-parent字段、kobj-kset字段。优先级是显式parent最高。另外kset内嵌kobj的parent决定了集合目录的位置如果集合目录本身挂错了成员目录自然跟着错。一个隐蔽的坑kobject_add时如果parent为NULL且kset为NULL对象会挂到/sys下但内核可能因为权限或命名冲突失败。所以永远显式指定parent或kset不要依赖默认行为。问题现象可能原因排查动作release不触发引用计数泄漏检查所有get/put配对检查init_and_add失败分支release触发两次重复put检查del后是否又put检查错误路径属性文件缺失ktype为NULL或groups名错确认ktype在add前设置核对宏生成名集合卸载警告成员未清空先遍历list移除成员再unregister目录位置错parent/kset优先级搞混显式传parent检查kset注册顺序5.5 实操心得先跑通最小闭环再扩展我自己的习惯是先用一个kobject加一个ktype不挂kset确认/sys下目录和属性文件能正常出现、读写正常、卸载后目录消失、release打印一次。这个最小闭环跑通后再引入kset把对象收拢最后加多个设备验证遍历和批量卸载。这样每一步的问题都能快速定位不会一上来就被多层嵌套搞晕。另外调试阶段可以在release里加pr_info打印对象名配合dmesg -w实时看。等逻辑稳定了再删掉打印避免日志刷屏。还有个小技巧kobject的name在kobject_add之后不要随意改改了sysfs目录名不会跟着变会造成名字和目录不一致的诡异现象。6. 三个API的协作全景与扩展方向把前面的内容串起来看kobject负责“我是一个对象”kset负责“我们是一伙的”ktype负责“我们这伙人长什么样、怎么死”。写驱动时先想清楚你的对象需不需要在/sys下露面需要就用kobject露面时要不要归类要就用kset同类对象有没有公共属性和统一的释放逻辑有就用ktype。扩展方向上kobject还和sysfs_notify配合做用户态事件通知和uevent配合做热插拔事件上报。kset的uevent_ops可以精细控制哪些成员产生事件、事件里带什么环境变量。ktype在新版本内核里还支持child_ns_type用于命名空间相关的sysfs视图隔离。这些都属于进阶用法但底层还是这三个API的排列组合。我在实际项目里踩过最深的坑是kobject_put的调用时机有一次在错误处理路径里既调了kobject_del又调了kobject_put而正常路径只调了kobject_put结果错误路径下release被提前触发正常路径下目录残留。后来统一成“del和put永远成对出现在清理函数里错误路径只调清理函数”问题才根除。这个教训是把清理逻辑收敛到一个函数里别在多个分支里散着写put否则引用计数迟早对不上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑