资讯详情

Linux内核设备模型全解析:kobject、sysfs与驱动绑定机制

📅 2026/9/25 13:19:12 | 华诺云谱 👁 阅读
Linux内核设备模型全解析:kobject、sysfs与驱动绑定机制
1. 为什么Linux内核的设备模型值得单独写一篇先说个亲身经历。我刚入行做嵌入式驱动开发那会儿最崩溃的不是看不懂字符设备驱动怎么写而是每次要理解一段代码都会碰到一堆绕不开的名词kobject、kset、ktype、bus、class、uevent……每个概念单独搜都能看到不少文章但串在一起就完全懵了——它们到底谁管谁和我的驱动又有什么关系直到后来硬着头皮啃了好几遍《Linux设备驱动程序》和内核源码又实际踩过几次因为没搞懂设备模型而半夜上线、第二天被业务方找上门的坑才真正意识到设备模型是整个Linux内核里极少见的、几乎撑起所有硬件相关代码的公共骨架。从你插上一个USB鼠标到手机里的传感器上报数据到服务器上热插拔一块NVMe盘背后全是这套模型在工作。这篇东西是想给那些已经有了Unix/Linux基础、却一直对内核设备模型觉得隔着一层纸的朋友看的。我会把设备和驱动的关系、kobject和sysfs那套抽象体系、平台设备和设备树的衔接逻辑从它为什么要这样设计讲起再落到我怎么用它写代码、怎么用它排查问题。写内核文章最怕的就是字都认识连起来不知道在说什么。所以我尽量用真问题、真代码、真场景来讲不堆术语。2. 内核目录下万物皆对象kobject、kset与ktype的第一性认识2.1 kobject是什么为什么万物皆对象在这里成立Linux内核用C语言写的但设备模型这部分代码处处透着面向对象的味道。struct kobject就是那个根类——它相当于一个顶层基类被嵌入到几乎所有的设备模型相关结构体里。看一眼定义不同内核版本略有差异但核心字段稳定struct kobject { const char *name; // 对象名字sysfs里的目录名 struct list_head entry; // 挂在kset中的链表节点 struct kobject *parent; // 对象在层次结构中的父节点 struct kset *kset; // 对象所属的集合 struct kobj_type *ktype; // 对象的行为类属性、release等 struct kernfs_node *sd; // sysfs目录项表示在/sys里的存在感 struct kref kref; // 引用计数内核态对象的生死关键 unsigned int state_initialized:1; unsigned int state_in_sysfs:1; unsigned int state_add_uevent_sent:1; unsigned int state_remove_uevent_sent:1; unsigned int uevent_suppress:1; };说实话第一次看到这个结构体时我也震惊一个对象竟然靠嵌入而不是继承来实现复用。C语言没有继承所以内核的做法是在你自定义的大结构体里把struct kobject作为第一个或某个成员嵌进去比如后面要去解析的struct device和struct device_driver都是这么干的。面向对象的好处在这里非常明显内核里所有设备模型相关的对象都具有统一的形态和统一的共性操作——它们都能被放到sysfs里显示、都能有属性文件、都能被引用计数管理、都可以被释放操作回收。你就把kobject想象成所有内核通知栏公告牌上的一个钩子什么对象要上墙公告就先得装一个它。2.2 引用计数kref对象生命周期管理的核心struct kref是一个极其精巧的小结构本质就一个原子引用计数struct kref { atomic_t refcount; };而围绕它有一堆操作kref_init()把计数置1kref_get()增加引用kref_put()减少引用并在计数归零时调用release函数。为什么要这么讲究因为内核里设备、驱动、总线这些对象都有多个不同路径的持有者驱动核心持有它、sysfs的目录项持有它、某个正在进行的IO操作也可能持有它。你没法确定最后一个使用者是谁于是只好用计数的方式管理。举个例子一个USB设备插入之后struct usb_device的kobject会被sysfs、usb核心、可能还在阻塞状态等待其上报的某个URB回调共同引用。如果代码里提前释放了device对象但URB回调还没跑完一访问就是野指针直接内核崩溃。相反如果不释放设备拔了内存还在就是内核对象泄漏。所有内核开发者都该养成条件反射get必须配putinit必须配release的实现。从实践角度看引用计数引出的一个经典坑是在release回调里不能依赖任何同样受引用计数保护的其他对象因为你的对象被释放时其他对象可能早就没了。这个我后文讲probe失败时会再次提到。2.3 kset对象的容器也是热插拔事件的广播员struct kset可以简单理解为一组kobject的集合。它在设备模型里有两个作用一是把每个kobject用链表串起来方便内核遍历相同类型的对象二是提供了一组操作接口uevent_ops用于在对象添加或移除时向用户空间发送热插拔事件。struct kset { struct list_head list; // 该集合下的所有kobject链表 struct kobject kobj; // kset本身也是一个kobject所以可以嵌套 const struct kset_uevent_ops *uevent_ops; };注意一个细节kset自己内部包含一个kobject这意味着集合本身也能挂到另一个集合下面形成层次。而不是把容器和被容器分成两种完全不相干的东西这种设计在映射到sysfs时特别自然——一个目录既可以是它父目录里的条目也是它自己子目录的根。那ktype呢它定义了某个kobject的行为默认的attribute集合、show/store操作、以及最关键的release回调。同一个kset里通常共享同一个ktype但并不是强制的。用画图类比的话kobject是一个树上的节点kset是一根树枝把同类节点串在一起ktype是树种的基因决定节点怎么展示、怎么被移除这三个概念是后面所有设备模型话题的基石。你只要理解设备也是一个挂在sysfs里的kobject它属于某个kset如devices并且通过ktype定义了它的属性展示方式后续就好办多了。3. sysfs让枯燥的内核对象从幕后走到台前3.1 sysfs是什么它是怎么把kobject投影出来的说设备模型必然绕不开sysfs。Linux在2.6内核引入了sysfs挂载在/sys目录把内核里设备模型的层次关系、属性信息原原本本地暴露给用户态。这样做的最大价值是把一个平时只在内核内存里运转的设备关系网变成了一件你可以用ls、cat直接观察的实体。在sysfs中一个kobject就是一个目录一个attribute就是一个文件。文件读取走show()回调文件写入走store()回调。所以实际上一行cat /sys/class/net/eth0/address内核里跑的正是网卡驱动的某个函数把MAC地址填到你看到的输出缓冲区里。拿我调试过的一个RTC驱动举例设备树里挂了一个外部RTC芯片驱动加载后/sys/class/rtc/rtc0下面会出现date、time、wakealarm等属性文件。用户在应用层cat /sys/class/rtc/rtc0/date走的链路在sysfs层面就是VFS layer - kernfs (sysfs文件系统) - attribute .show回调 - 驱动自定义读函数这也是为什么说一切皆文件在Linux里不是一句口号——内核对象都能通过文件系统的形式暴露给用户空间。3.2 一个驱动属性从定义到上墙的完整过程写一个简单的属性核心结构不用多两个回调足矣static ssize_t my_attr_show(struct device *dev, struct device_attribute *attr, char *buf) { return sysfs_emit(buf, %d\n, some_device_state); } static ssize_t my_attr_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { int val; if (sscanf(buf, %d, val) ! 1) return -EINVAL; some_device_state val; return count; } static DEVICE_ATTR_RO(my_attr); // 只读属性 static DEVICE_ATTR_RW(my_attr); // 如果要可读可写就换这个这里DEVICE_ATTR_RO宏展开后本质上构造了一个struct device_attribute并且自动把回调绑定到我们写的my_attr_show上。之后在设备的probe函数里调用device_create_file(dev, dev_attr_my_attr);或者用sysfs_create_group()批量创建。当设备注册时/sys/devices/platform/.../my_attr这个文件就出现了。我见过不少初学者会把show回调写成sprintf(buf, ...)—— 这在旧内核没问题但新内核里推荐用sysfs_emit()因为它能保证不再发生缓冲区越界而且自动处理页边界情况。这属于那种不踩一次就记不住的细节我早期在新内核上调试时曾经因为用sprintf把一个超过PAGE_SIZE的内容写进属性结果读出来的数据被截断看起来玄学实际就是缓冲区上限。3.3 sysfs的目录层次设计从devices、driver到class走进任何一台Linux机器/sys底下最关键的几个目录是/sys/devices所有真实设备的全局层次按物理拓扑排布如PCI总线域/设备/功能号/sys/bus按总线类型组织设备与驱动比如pci、usb、platform/sys/class按功能分类的设备视图比如网络接口、块设备、RTC等/sys/block、/sys/firmware、/sys/module等按具体子系统划分的内容。同一个设备对象会在多个目录下同时出现。这就是设备模型里一个很重要的设计思想同一个对象可以从不同视角被观察。物理视角devices、拓扑视角bus、功能视角class各有各的用处——应用层看功能内核管理看物理驱动绑定时看总线。比如你的显卡是PCI设备它在/sys/devices/pci0000:00/0000:00:02.0下存在也在/sys/bus/pci/devices/0000:00:02.0下存在这两个路径通常是指向同一对象的符号链接同时如果它绑定了某个drm驱动还会在/sys/class/drm/card0暴露功能入口。理解了这个多视图设计以后排查为什么这个设备怎么没有出现在某个目录的问题就简单了。4. device、driver与bus的三角关系以及probe是怎么发生的4.1 三个核心结构体的定位与真实语义Linux设备模型中真正的主角不是kobject而是围绕它构建的三个大结构struct device、struct device_driver、struct bus_type。struct device描述一个物理设备或者逻辑设备。它保存了设备名称、父设备、总线、电源管理状态、资源、设备树节点等大量信息struct device_driver描述一段能驱动某类设备的代码。它提供probe、remove、shutdown、suspend/resume等回调接口还知道自己适用的设备ID表struct bus_type描述一种通信总线。它定义了匹配规则、u事件处理、电源管理、DMA、热插拔等机制。内核里可以说任何孤立的设备都没有意义一个设备必须知道自己挂在哪条总线上一个驱动也必须声明自己适配哪条总线。这条总线的存在把设备和驱动从各自独立的状态变成了有可能绑定的状态。4.2 设备驱动匹配的三种主要机制总线的存在最终是为了配对。我以最典型的platform总线、i2c总线、pci总线来举例说明匹配机制三种不同思路第一种id_table 硬匹配驱动声明一个ID表列出它支持的所有设备名/ID。设备侧也有自己的名字或ID。当总线进行match时查表对比命中即匹配。i2c设备最典型static const struct i2c_device_id xxx_id[] { { bme280, 0 }, { bmp280, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, xxx_id);第二种device tree 兼容性匹配设备树节点里写compatible bosch,bme280;驱动里写of_device_id的.compatible字段。总线在match时优先查设备树匹配static const struct of_device_id xxx_of_match[] { { .compatible bosch,bme280, }, { } }; MODULE_DEVICE_TABLE(of, xxx_of_match);第三种设备主动创建设备名匹配有些总线没有严格ID表比如platform总线有一条兜底逻辑如果设备名device.name和驱动名driver.name完全一致也能匹配上。这就是为什么很多平台驱动里platform_driver.name xxx_platform会恰好对应设备树节点中被解析出来的xxx_platform设备名。4.3 probe流程是人手交接的过程当匹配成功后总线会调用驱动里的probe()把struct device *dev传进去。驱动在这个函数里完成的事情相当于锅炉工接到锅炉房钥匙后的第一步巡检从设备节点/资源表中获取IO地址、中断号、时钟频率等资源申请IO内存、注册中断、初始化硬件初始化各种子结构如输入设备、网络设备、字符设备框架创建一个或多个struct device的从设备进一步暴露功能如果失败返回错误码驱动核心会把匹配关系回滚设备状态变成 unbound。每个总线的match和probe逻辑略有区别比如PCI总线的match会看vendor/device IDUSB会看interface class但整体哲学是统一的总线和驱动核心只负责撮合具体硬件怎么初始化完全交给驱动的probe。这里有个值得记住的调试要点设备匹配失败时/sys/bus/xxx/devices/下设备还在但在si_driver这个符号链接或驱动目录里没有对应驱动。而驱动加载失败时/sys/bus/xxx/drivers/下的驱动没有绑定任何设备。用ls -l一看便知比在内核日志里翻天覆地找错误要快得多。4.4 一张流程图式的对应关系非图表用列表形式列清楚为了把上面读起来比较散的关系压实我直接列一份最常见的配对状态清单设备存在驱动已加载匹配成功/sys/bus/platform/devices/xxx/driver指向驱动的符号链接设备存在驱动已加载匹配失败/sys/bus/platform/devices/xxx/driver不存在但driver_override属性可以强行指定驱动设备存在驱动未加载驱动目录里没有对应条目lsmod也看不到模块设备未创建驱动却已加载说明设备树或ACPI没有声明该设备或设备枚举尚未完成。在上面基础上再配合dmesg | grep xxx、/proc/device-tree和udevadm info大多数绑定问题都能在分钟级定位。5. 设备模型的入口从注册一个platform_driver开始5.1 为什么平台设备是嵌入式/非PCI设备的事实标准现代Linux内核里你写的大部分字符设备驱动、传感器驱动、GPIO驱动其实都是挂在platform总线上的。因为PCI、USB这些总线都有硬件探测机制枚举是硬件帮你做的而SoC内部的许多设备UART、I2C控制器等没有类似标准发现协议只能靠编译时固件描述或设备树描述。platform总线就是对这些无硬件枚举能力设备的一种抽象本质上它是个软件总线设备列表由系统初始化时注册的设备树节点或者板级初始化代码生成。举个例子在设备树里写mydevice: mydevice1c00000 { compatible myvendor,mydevice; reg 0x01c00000 0x1000; interrupts 0 45 4; };内核启动时of_platform_default_populate_init()会遍历设备树把这个节点变成一个platform_device注册到内核总线里。随后platform_bus_type.match会和你的驱动去对表匹配成功就probe。5.2 手写一个最简platform驱动骨架为了让上面的机制不提虚的给你一个最简驱动骨架这个骨架我自己每次新板子起笔都是拿它改的#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int ret; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); devm_platform_ioremap_resource(pdev); /* 等价做法返回映射后的虚拟地址 */ ret devm_request_irq(pdev-dev, platform_get_irq(pdev, 0), my_isr, 0, mydevice, my_data); if (ret 0) return ret; dev_info(pdev-dev, probe ok, base%px\n, base); return 0; } static void my_remove(struct platform_device *pdev) { dev_info(pdev-dev, remove\n); } static const struct of_device_id my_of_match[] { { .compatible myvendor,mydevice, }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name mydevice, .of_match_table my_of_match, }, }; module_platform_driver(my_driver); MODULE_LICENSE(GPL);有几个点我特别说一下。devm_前缀的函数device managed是设备模型送给驱动开发者最大的礼物。用了它你就不用在remove或错误分支里手工释放资源——内核会在设备unbind时自动帮你释放。这个设计大幅降低了资源泄漏概率。真实项目里我几乎不再手动request_regionioremaprelease_region那套老组合全都交给devm_系列。platform_get_resource(pdev, IORESOURCE_MEM, 0)是从哪里来的资源它的来源是设备树里的reg属性。而platform_get_irq(pdev, 0)对应的是设备树的interrupts属性。也就是说资源和中断不是驱动自己编的而是由设备节点提供驱动为消费者。5.3 注册宏module_platform_driver到底做了什么这个宏展开后做了三件事定义一个platform_driver实例即你填写的那个结构体定义module_init调用的注册函数platform_driver_register(你的驱动结构体);定义module_exit调用的注销函数platform_driver_unregister(你的驱动结构体);手动拆开会发现它跟你写的static int __init xxx_init(void) { ... } static void __exit xxx_exit(void) { ... } module_init(xxx_init); module_exit(xxx_exit);是完全一致的。用宏可以减少模板代码也让驱动挂载/卸载逻辑一目了然。6. class、devtmpfs和uevent设备模型与用户空间的最后一公里6.1 从内核设备到/dev/xxx设备节点是怎么来的设备模型不只是内核里的抽象它必须能支撑用户空间的日常使用。/dev/下面那些节点就是通过设备模型 devtmpfsudev三者协作完成的。内核设备在注册时如果所属的子系统使用了device_create()之类的接口会自动在 devtmpfs 里创建设备节点。class的核心作用就是把一类具有相同功能的设备组织起来并分配设备号。例如static struct class *my_class; static int __init my_init(void) { my_class class_create(myclass); device_create(my_class, dev, MKDEV(major, minor), NULL, mydev%d, minor); }这样用户空间立刻能看到/dev/mydev0不需要额外脚本。在新内核里class_create的接口有变化它简化成传入owner参数即可my_class class_create(myclass);6.2 uevent与热插拔内核怎么通知用户空间的另一条线是uevent。当一个设备从内核里加入或者移除时设备模型会生成一条环境事件包含ACTIONadd或ACTIONremove、DEVPATH、SUBSYSTEM等键值对。用户空间的udevdsystemd-udevd收到之后按照/usr/lib/udev/rules.d和/etc/udev/rules.d里的规则执行任务。这就解释了为什么你插上一个U盘/dev/sda会自动出现为什么拔掉之后它又能自动消失。实际上这块逻辑是USB子系统创建usb_device- 驱动核心检测到新增 - 生成uevent - udev处理 - 根据规则创建设备节点或者启动挂载。调试这类问题时udevadm monitor是你的得力工具它能实时打印内核发出的uevent。6.3 设备模型在电源管理里的隐蔽角色设备模型的另一个重要作用时刻你可能没留意过——系统挂起/唤醒suspend/resume。内核在休眠时并不是随便谁想睡就睡而是按照设备模型里的拓扑次序逐级处理先挂起子设备再挂起父设备唤醒时顺序相反。这个行为是通过struct dev_pm_ops挂到总线和驱动上的。你在driver结构体里填的.pm my_pm_ops会在设备模型历遍时被调用。很多嵌入式项目里出现的睡眠后醒不来某个设备唤醒时没恢复寄存器最后查下来都是设备树里电源域层级和驱动pm回调不太对。这也是设备模型抽象的一个隐藏价值它让所有设备在电源状态迁移时都遵循统一框架驱动开发者只需要在框架里填写自己的回调。7. 实战排错设备模型问题排查的三板斧7.1 第一板斧查ls /sys与符号链接设备模型调试时我最先做的永远是看三个地方/sys/bus/bus/devices/下面有没有设备/sys/bus/bus/drivers/下面有没有驱动设备的driver符号链接是否指向对应驱动如果设备目录存在但driver指向不存在说明match失败接下来要审视设备树 compatible 是否和驱动的of_match_table完全一致——注意一旦有大小写差异、多余空格、换行都匹配不上。7.2 第二板斧在驱动加载路径上埋trace设备模型框架本身不太需要调试但驱动绑定过程需要。我在probe前后加上打印dev_info(pdev-dev, myprobe enter\n); ... dev_info(pdev-dev, myprobe leave success\n);再配dynamic_debug或直接看dmesg就能知道是入口都没进去多半是match没成功还是进去后某个资源获取失败多半是设备树资源不对。如果还不够用ftrace或者tracefs里的events/device/dev_add等tracepoint能拿到更底层的设备模型事件流比如设备在哪一步创建、哪个驱动绑定被拒绝。7.3 第三板斧/proc/device-tree与实际的硬件地址比对设备树与设备模型衔接出问题时我喜欢直接看运行时的设备树子目录ls /proc/device-tree/ocp/mydevice/ cat /proc/device-tree/ocp/mydevice/compatible cat /proc/device-tree/ocp/mydevice/reg如果这些内容和你写进驱动of_match_table里的不一样优先修正设备树而不是改驱动。改驱动去适配一个错误描述硬件的设备树属于错误的源头还没解决就先去捂住出水口后续隐患很大。8. 从设备模型看Linux内核设计的几层深意8.1 为什么要有这么复杂的抽象层直接操作硬件不好吗这个问题我早年问过自己。后来写了不少驱动维护过几套板级SDK之后想明白了如果每个驱动都直接操作寄存器、直接管理自己的生命周期、直接分配设备号那么内核里的每个驱动就是一座孤岛。系统集成的时候孤儿设备没人管电源、热插拔没法通知用户态、多个驱动抢同样设备号、设备节点混乱——这些问题会爆炸性地增加。设备模型的存在是为了把硬件驱动代码和公用的系统管理能力解耦。前者是产品相关、五花八门后者是通用能力、人人需要。可以说设备模型就是这些通用能力的制度化基础设施。8.2 引用计数、sysfs、uevent三者的共同哲学把设备模型的几个要素放在一起看能看到一个统一的哲学内核里发生的每一件事都要以可观察、可追溯、可管理的方式呈现出来。引用计数保证可安全释放sysfs保证可查看、可配置uevent保证可感知状态的变更总线加驱动架构保证可按统一规则绑定。这对我们写业务代码也有借鉴意义——模块与模块之间永远要有一层清晰的公共协议层而不是让每个业务方直接互相调用。Linux内核用了几十年时间证明这种底盘式的抽象设计是应对复杂度最有用的武器。8.3 掌握设备模型的价值从被框架限制到驾驭框架说句实在话如果你只是能用框架写一个hello驱动设备模型对你来说可能只是仪式感。但一旦你要做以下任何一件事——多个时钟域设备的电源管理、背光调节与设备树绑定的资源协调、子系统之间异步热插拔的竞态规避、在sysfs上给用户态开调试口——你就离不开设备模型的理解。我见过不少工程师背熟了platform_driver模板却不知道它的probe为什么被调、失败后为什么devm资源还能被释放、设备节点怎么从 sysfs 里消失。结果遇到真正难一点的问题就无从下手。把设备模型这层关系想明白了你就是从模板使用者变成了框架理解者。9. 一些从实践中来、书本上少写的话最后说点写代码之外的题外话。设备模型设计得很优雅但它是几十年来无数硬件、无数驱动磨合出来的结果。你读它的时候不要指望每个 API 的引入都像教科书那样富有浪漫色彩。很多函数是新版本为了修某个bug而加的很多字段是为了兼容某个特定厂商的硬件而留的。所以看源码时务必带上git blame或者版本历史你会有一种打通任督二脉的感觉。我自己在调试一个触摸屏驱动的经历里最有价值的一课是设备树里的 status disabled 会把节点彻底藏起来但 sysfs 里仍然会看到总线目录下有残留设备名只是没有在/sys/devices/platform/里展开成真实设备。这种既有又没有的状态如果不理解设备模型的“设备纳入、驱动绑定”两阶段会绕很久。再分享一个小习惯我每次接手一个新板子会先做一次系统性的设备模型盘点把所有在跑的设备在 /sys 下的路径和绑定的驱动都记录下来作为板级调试的基线。后面一旦出现某个外设不工作只需对比基线就能快速锁定是设备树没声明、驱动没绑定、还是硬件电源没就绪。设备模型不是内核里最容易懂的部分但绝对是最值得花时间理解的部分之一。它像一张组织结构图把硬件、驱动、用户空间、电源管理全捏合在一起。把这张图刻进脑子里以后遇到 Linux 里任何为何我的设备没反应的问题你前进的方向都会清晰很多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑