资讯动态

Linux设备驱动模型全解析:从device、driver、bus到probe机制

发布时间:2026/9/18 16:56:42 来源:尧图企业网站定制
搞懂 Linux 设备驱动模型才算真正吃透内核底层我最早接触 Linux 驱动开发的时候犯过一个很典型的错误以为写驱动就是实现 file_operations、注册字符设备、然后在 /dev 下生成一个节点。照着网上的 demo 码确实能跑led 也能点亮串口也能收发。但只要一涉及到 USB 热插拔、电源管理、设备树、模块自动加载……这套“裸奔式”写法马上就露馅了。直到后来把设备驱动模型Device Driver Model完整啃了一遍我才意识到Linux 内核的驱动框架根本不是我之前理解的那个结构。设备驱动模型可以说是整个 Linux 内核驱动体系的“地基”。它决定了设备怎么被枚举、驱动怎么被匹配、用户空间怎么看到硬件、系统休眠唤醒时怎么管理设备甚至决定了一块板子从开机到系统起来驱动是如何一步步被找到并激活的。如果你只是停留在写文件操作函数的层面那么一旦遇到设备树、总线、热插拔、电源域这些问题就会觉得内核不可理喻。这篇文章我想用实际项目里的视角把设备驱动模型的几个核心对象、匹配机制、sysfs 表现、以及常见坑位完整梳理一遍。适合正在学嵌入式 Linux 驱动、准备看内核源码、或者已经写驱动但被 probe 不执行这类问题折磨过的人。1. 设备驱动模型到底在解决什么问题1.1 早期内核的驱动方式乱在哪Linux 早期不是没有驱动模型而是驱动和具体硬件绑得太死。比如某个驱动写出来里面直接写死 IO 地址、中断号、时钟频率然后在内核启动时按固定顺序初始化。你要换一颗 SoC、换一块板子就得重新改驱动、重新编译内核。USB 热插拔出现之后这种办法彻底扛不住了。设备随时插上、随时拔掉你不可能提前在内核里静态枚举一堆设备同时电源管理也要求内核能随时知道某个设备在不在、有没有驱动绑定、能不能安全关掉。旧模型还有一个问题设备、驱动、总线之间没有清晰的抽象导致内核里大量代码在“猜”硬件关系。比如一个 I2C 触摸屏它挂在哪条 I2C 总线上、地址是多少、有没有中断这些信息要么写死在驱动里要么靠各种全局变量传递。想让驱动具备可移植性几乎做不到。1.2 新模型的核心把设备和驱动彻底解耦设备驱动模型的核心思路一句话就能讲清楚硬件是硬件逻辑是逻辑总线是媒人。device 代表真实存在的硬件实体比如某颗 I2C 触摸屏芯片、某个 PCIe 网卡、某块 platform 设备。driver 代表能够操作某一类硬件的软件逻辑比如通用的 e1000 网卡驱动、通用的 ili9341 屏幕驱动。bus 负责把 device 和 driver 撮合在一起通过 match 方法判断“这个设备能不能用这个驱动”。class 负责从用户空间的角度给设备归类比如所有网络接口都会出现在 /sys/class/net不管你底层是 USB 还是 PCIe。通过这套抽象内核可以做到同一份驱动代码支持多颗芯片或者同一颗芯片在不同的总线上被不同驱动接管。设备树本质上也是在描述 device 的拓扑和属性让总线层在启动时把设备一个个注册进来再交给匹配机制去找到合适的驱动。这套模型另一个隐藏价值是生命周期管理。设备和驱动的引用计数、绑定关系、移除动作都有了一套标准化流程。模块卸载顺序、设备拔除时的清理回调、电源状态切换都是基于这套模型做出来的。没有这个模型Linux 的电源管理框架、内核的 sysfs 体系、udev 自动加载模块的能力全都要塌掉。所以可以把设备驱动模型看成内核为驱动开发者提供的一套“框架标准”。驱动写的本质上是在做填空题你要申明支持哪些设备、匹配时做什么、probe 成功之后怎么初始化、remove 时怎么清理。剩下什么时候调用你、怎么找到你都是内核在完成。2. 模型里的关键角色device、driver、bus、class 与内核对象2.1 kobject一切对象的底座设备驱动模型之所以能统一管理设备、驱动、总线是因为底层有一个最小的公共祖先struct kobject。它包含引用计数、对象名、父对象指针、sysfs 目录的 dentry 等基本要素。你可以把 kobject 理解成一个“可以被内核追踪的目录项”。每个注册到内核里的 device 或 driver本质上都是一个 kobject它们会在 sysfs 中表现为一个目录。而struct kset则是一组 kobject 的集合比如devices这个 kset 下面放的就是所有 device 对象。内核通过 kobject 实现了统一的生命周期谁引用、谁释放不再靠人为约定而是通过kobject_get/kobject_put配合引用计数来管理。实际写驱动时你可能不会直接操作 kobject但它一直在背后起作用。比如你给设备注册一个 sysfs 属性文件属性文件的创建和销毁依赖的其实就是这个设备 kobject 的引用计数。一旦你生命周期没搞对模块卸载时崩溃、sysfs 属性悬空这些问题就全冒出来了。2.2 device 与 driver硬件实体和软件逻辑的分工struct device是整个模型里最重要的数据结构之一。它描述了一个硬件设备在内核中的抽象节点包括父设备、总线类型、电源管理状态、DMA 掩码、NUMA 节点等信息。注意它不描述设备的具体工作方式具体工作方式由 driver 和其私有数据结构完成。struct device_driver则代表驱动逻辑。它包含 name、owner、probe、remove、shutdown、suspend、resume 等回调。内核通过driver_register()把驱动加入总线随后总线会根据 driver 声明的匹配规则去遍历已经注册的 device如果匹配成功就调用 driver 的 probe。这里有非常多新手搞混的一点device 和 driver 不是“包含”关系而是“绑定”关系。driver 不是 device 的一部分device 也不是 driver 的实例。它们更像是两个独立个体由总线撮合在一起后才产生一个“bound”状态。只有完成绑定内核才会把 device 真正交给 driver 去操作。2.3 bus撮合的逻辑核心总线 bus 在内核里不完全对应物理线路。它更多是一个“抽象分类器”比如 platform_bus_type 把 SoC 内部各种集成外设归类为平台设备i2c_bus_type 管理所有 I2C 控制器和挂载在其上的设备。每个总线对象里都有一份自己的 device 链表和 driver 链表以及 match、uevent、probe 等回调。当新的 device 注册时总线会遍历自己链表上的所有 driver调用 match 方法尝试配对。反之当新的 driver 注册时也会遍历所有 device 做同样的尝试。这套双向遍历机制保证了注册顺序不影响最终绑定结果不管是设备先来还是驱动先来总能有机会匹配上这就是为什么我们经常看到设备树里声明了 node但驱动后加载也能正常 probe。2.4 class用户空间看到的设备视角class 在设备驱动模型中很容易被忽略但它是用户空间工具能够工作的前提。struct class提供了一种按照功能而不是硬件类型组织的视图比如/sys/class/net下能看到 eth0、wlan0底层可能是 PCIe 网卡、USB 网卡或者虚拟设备。更重要的价值在于class 是设备节点自动创建的机制基础。class 接口提供class_create()和device_create()前者创建了一个类后者在类下创建一个设备并且会自动生成 dev 节点。配合udev内核就可以在设备注册时自动在 /dev 下生成对应节点。没有 class 的话设备节点就只能手动 mknod这在现代 Linux 上基本不可接受。2.5 引用与生命周期防止崩溃的防线设备驱动模型里引用计数不是单纯的计数是为内核提供一套“谁在用我”的管理机制。比如一个 USB 网卡插在系统上网络子系统持有 net_device 的引用USB 子系统持有 usb_interface 的设备引用。如果用户在拔掉网卡的同时还在后台跑 iperf内核不会立刻释放设备内存而是通过引用计数延后释放直到所有消费者都放掉引用。实际工程中这类问题最常见的高发区是 sysfs 属性回调。用户程序正在读取某个属性文件驱动这时执行 remove如果设备内存直接被释放那这个 read 回调就会访问到已释放的内存出现典型的 UAFuse-after-free崩溃。解决方案就是利用 device 内部的引用计数保护属性文件或者用 cdev 的索引机制做安全释放。这些细节只要深度看过设备模型源码都能理解是为什么。3. 匹配机制与 probe 流程从注册到绑定内核做了什么3.1 先有 device 还是先有 driver结果都一样总线上的匹配逻辑是设备模型最精妙的部分。对于一个总线类型当内核调用device_add()把设备挂入总线时会执行bus_probe_device()它会调用总线的device_attach()去做一次匹配。同样地driver_register()也会调用bus_add_driver()去为它匹配已有设备。所以无论谁先注册最终都会出现两种路径device 先注册内核遍历该总线的 driver 链表逐个尝试 match匹配成功则 probe。driver 先注册内核遍历该总线的 device 链表逐个尝试 match匹配成功则 probe。这个设计对做嵌入式开发尤其友好。因为设备树下的 platform_device 是内核在初始化时批量注册的而驱动可能是模块方式后加载的如果模型不支持后到的驱动主动找设备那所有驱动都只能编进内核了。3.2 match 方法多种匹配方式各有取舍不同总线的 match 方法实现不一样但基本不外乎以下几种通过 id_table 匹配大多数设备驱动会定义一个of_match_table或者是id_table里面列出了支持的 compatible 字符串或厂商设备 ID。通过设备名匹配platform 总线驱动可以设置driver.name如果与 device 的 name 相同也能匹配。在设备树时代这种方法已经不推荐但老代码里依然常见。通过设备树 compatible 匹配device_node 里的 compatible 属性会在设备注册时被解析出来总线用这个字符串去 id_table 里匹配。通过 ACPI 匹配x86 平台上的常见方式类似设备树 compatible 的存在。match 一旦成功总线会认定该 driver 可以为这个 device 服务随后就进入 probe 环节。要注意的是match 成功不等于绑定立即完成中间还有 driver 的probe返回值决定最终成败。如果 probe 返回错误码内核认为绑定失败甚至可能回滚之前分配的资源。3.3 probe驱动的真正入口也是初始化的大本营当设备与驱动配对成功内核会调用really_probe()。这里面会做一连串事情先把设备挂到驱动的设备链表中然后调用驱动里的probe回调。如果 probe 成功设备状态被标记为 onlinedriver_bound 被调用uevent 被发送到用户空间sysfs 里的driver符号链接也会被创建。probe 函数是驱动开发者最常打交道的入口它要做的事情也是固定的套路从 platform_data 或设备树里解析硬件资源地址、中断、时钟、GPIO申请 IO 内存、注册中断号、初始化硬件注册各种子系统接口比如字符设备、net_device、input_device、regmap设置电源管理回调使设备支持运行时休眠。需要注意probe 里如果分配资源后注册失败必须做回滚。内核对 probe 的失败处理并不宽容只是把设备状态切回未绑定状态至于你 probe 里自己申请的内存、irq、regmap全部需要你自己释放。一旦遗漏就会出现内存泄漏、中断号被占用等隐蔽问题。3.4 一个完整实例从 platform_device 到 platform_driverplatform 总线是嵌入式开发里最常遇到的总线类型。它把所有 SoC 内部集成外设统一当作 platform 设备来管理。比如一个 UART 控制器不通过设备树描述的时候可以用 platform_device_register 直接注册设备树模式下内核会根据根节点下 compatible 字符串自动生成 platform_device。驱动侧需要定义一个struct platform_driver填充 probe、remove、shutdown 等回调并在driver字段里填上of_match_table。模块加载时调用platform_driver_register()内核就会遍历已注册的 platform_device 列表用设备树的 compatible 去和of_match_table匹配匹配成功即调用 probe。这个流程配合设备树后板级代码里几乎不再写平台设备的注册逻辑了全部由内核框架在启动时自动完成。带来的好处是“一套内核适配多块板卡”只要设备树改一改驱动通通不用重编。这也就是为什么现代嵌入式 Linux 开发中设备树能力几乎是驱动工程师的必修课。4. 从代码层面看设备模型的落地怎么在你的驱动里用好这套框架4.1 基本驱动骨架platform_driver 的固定套路实际的驱动代码里很多新人刚上手就想直接研究各种复杂框架这没必要。先搭好最标准的 platform_driver 骨架比看多少篇文档都管用。下面这段就是嵌入式 Linux 驱动最典型的初始化结构static int my_probe(struct platform_device *pdev) { struct resource *res; struct my_dev *dev; int ret; dev devm_kzalloc(pdev-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -EINVAL; dev-regs devm_ioremap_resource(pdev-dev, res); if (IS_ERR(dev-regs)) return PTR_ERR(dev-regs); platform_set_drvdata(pdev, dev); ret misc_register(dev-miscdev); if (ret) return ret; return 0; } static int my_remove(struct platform_device *pdev) { struct my_dev *dev platform_get_drvdata(pdev); misc_deregister(dev-miscdev); return 0; } static const struct of_device_id my_of_match[] { { .compatible vendor,my-device, }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name my_device, .of_match_table my_of_match, .owner THIS_MODULE, }, }; module_platform_driver(my_driver); MODULE_LICENSE(GPL);这里面有几个值得注意的点使用devm_系列的资源管理函数可以省去大量错误处理。probe 里如果注册失败由 devm 分配的内存、ioremap 的映射都会自动释放。很多老代码还在手动释放 IO 内存完全没必要了。platform_get_resource()从设备树或 platform_data 中拿资源。如果你是设备树启动它会解析 reg 属性如果是最老的板级代码它解析 platform_device 里预填的 resource。MODULE_DEVICE_TABLE非常关键。它生成模块的别名信息udev/modprobe 才能根据设备树里的 compatible 自动加载对应驱动模块。不写这行设备树声明了、驱动也编译了modprobe 可能也识别不了。4.2 不只是 platform 总线i2c、spi、regmap 这些子系统怎么复用模型设备模型的可复用性体现在它不是一个孤立的框架而是所有子系统的基础。以 I2C 为例写一个 I2C 触摸屏驱动核心结构是struct i2c_driver里面也有 probe、remove、id_table。当你拿到一个合法 I2C 地址驱动注册后i2c_bus_type 会遍历 I2C 子系统里已有的设备读取设备树里该节点的地址信息匹配i2c_device_id或者匹配设备树的 compatible。成功之后probe 里才能用i2c_get_clientdata()拿到设备对象。SPI 子系统也走同样的套路。只不过 SPI 设备没有地址的说法更多的是片选 CS 序号。而在 regmap 抽象出来之后I2C、SPI、MMIO 这些接口在上层驱动看来都差别不大了。你 probe 里只需要用devm_regmap_init_i2c()或devm_regmap_init_spi()创建 regmap后续用 regmap_read/regmap_write 操作寄存器。这种结构带来的直接好处是底层总线换了驱动上层代码基本不用动。比如一个音频编解码芯片挂在 I2C 上时用 regmap_i2c挂在 SPI 上时用 regmap_spi而 ALSA 相关的控制和寄存器操作代码能完全复用。4.3 设备模型与 sysfs为什么你能在 /sys 下看到那么多目录sysfs 是设备模型对外窗口。设备模型注册一个 device就会在 /sys/devices/ 下创建对应目录如果挂到某个 class又会在 /sys/class/ 下创建符号链接。你可以通过读 /sys 下的文件来判断一个设备当前是什么状态、有没有驱动绑定、支持哪些特性。调试驱动时我最常用到这几个路径/sys/bus/platform/drivers/每个注册的驱动在这里有一个目录里面有它绑定的设备符号链接。/sys/bus/platform/devices/所有平台设备列表。如果你发现设备树里声明的节点没有出现在这里说明设备注册阶段失败或者说该节点不受 platform 总线管理。/sys/class/xxx/按功能分类的设备节点比如 /sys/class/gpio、/sys/class/net、/sys/class/input。配合 udev这是 /dev 节点自动创建的来源之一。如果你想在驱动里给设备添加自定义信息也可以利用DEVICE_ATTR宏创建属性文件放到设备的 sysfs 目录下。这样用户空间可以直接通过读写文件的方式调试设备状态省去一辈子在 debugfs 里翻的功夫。当然 debugfs 是另一套机制也是调试内核的利器。4.4 设备树与设备模型的配合老一代板级文件为什么被抛弃在老版本内核里platform_device 都是由板级代码也就是 arch/arm/mach-xxx/ 下的文件手动注册的。每换一块板子都要改 C 文件、加 resource、改 platform_data重新编译内核极其痛苦。设备树的出现彻底改变了这个流程。硬件拓扑在 dts 文件里描述compatible、reg、interrupts、clocks、resets 等属性被内核解析后会由of_platform_populate()自动创建 platform_device 并注册到 platform 总线上。驱动侧只需要声明 of_match_table剩下的匹配流程交给内核。这也带来了一个实际工作习惯的变化排查驱动问题时往往先看 dts。probe 没调用先看 compatible 有没有对上。ioremap 失败先看 reg 地址和长度有没有写对。中断申请不到先看 interrupts 属性与中断控制器的对应关系。设备树是设备模型在这个时代的核心输入源之一所以把它单独拿出来讲是为了强调设备模型不是一套只能通过 API 理解的抽象概念它是驱动开发日常工作的落地基础。5. 常见问题与排查技巧那些年踩过的设备模型的大坑5.1 probe 不执行或不调用先别怀疑驱动代码我在好几个项目里遇到同一种情况驱动编译成 .koinsmod 成功但 log 里就是没有 probe 输出。排查的时候第一件事不是看驱动 probe 里写了什么而是看设备树节点有没有生成 platform_device。最直接的检查路径ls /sys/bus/platform/devices/看你的设备树节点对应的设备目录是否存在。如果不存在用下面命令检查设备树里节点状态cat /proc/device-tree/your-node-path/status cat /proc/device-tree/your-node-path/compatiblestatus 如果不是 ok 或者 compatible 和 of_match_table 对应不上probe 就不可能执行。此外还要确认驱动有没有加载到正确的总线上。可以这样查看ls /sys/bus/platform/drivers/你的驱动名/如果驱动目录存在里面却没有设备的符号链接说明它们还没有匹配成功。这时候要多问一句驱动模块有没有被 udev 在设备注册后自动加载如果驱动被延迟到设备注册之后才 insmod也是能匹配上的但前提是 MODULE_DEVICE_TABLE 和 alias 信息正确。5.2 sysfs 属性文件读写崩溃生命周期没有保护好很多自己写 sysfs 属性的驱动会直接给DEVICE_ATTR的 show/store 回调里传设备的私有数据结构。听起来没问题但一旦设备被移除或者驱动卸载用户空间还持有该属性文件描述符这时候读取就会访问野指针。正确的做法是不要裸传指针而是通过struct device *到platform_get_drvdata来获取私有数据同时利用内核的引用计数机制。更稳的是配合 cdev 和 index 做延迟释放或者至少在被移除时保证不会有其他线程正在访问属性回调。动手排查时如果碰到读取属性直接死机或 oops看一眼堆栈里是不是有 sysfs 的 show/store 函数。如果是优先检查私有数据生命周期和 module 的引用计数。别非要在设备模型上花太久很多这类问题本质是模块卸载时代码路径没有完全 null 掉。5.3 driver 与 device 的 name 到底匹配的是谁还有一种常见误操作是驱动里只设置了 driver.name没有 of_match_table。在设备树模式下platform_device 的名称是设备树节点的 node name未必和你想的驱动名字一样。你本意是匹配结果是完全匹配不上。设备树模式下优先级最高的是 compatible 匹配其次是 id_table最后才是 name 匹配。所以如果你的板子采用设备树启动不要指望把 driver.name 改成节点名就能万事大吉老老实实把 of_match_table 配上才是正确方案。另外多说一句老式板级代码直接注册 platform_device 时device 名字就是注册时指定的 name。这种写法在设备树时代基本绝迹。你要是还在用这类老代码只能说要么项目很老要么教材太旧。5.4 模块卸载崩溃remove 回调与引用计数的连带关系模块卸载时内核会先调用驱动绑定的所有设备的 remove 回调再检查设备是否还被其他模块引用。如果设备还挂在使用链表中比如 misc 设备或者 miscdevice 的引用数不为 0卸载就会出现 “module is in use” 之类的拒绝。而如果你强行走 rmmod -f后果可能就是设备节点悬空、用户空间访问崩溃、甚至是内核直接 oops。正确做法是先在用户空间关闭所有文件描述符再卸载模块。如果想做得优雅可以借助 class 和 devnode 回调机制在 remove 时告知用户空间设备已消失。通过 udev 的基础上也可以自动清理 /dev 节点。这里也提一下 devm 资源管理的价值。如果你用 devm_kzalloc、devm_ioremap_resource、devm_request_irq那么 remove 时会自动释放这些资源移除顺序严格按申请顺序反向执行省去大量手动释放代码也减少了遗漏释放带来的互相踩踏。5.5 大量设备下的匹配性能不是玄学是算法问题设备少的时候总线遍历匹配的代价可忽略不计。但到服务器级别一个总线上挂着几十个设备、几十个驱动每次注册新设备都要全量遍历那就不划算了。现代内核在 model 和 bus 匹配上做过很多优化比如利用 idr、hash list 来加速同时也引入了 device_links 实现更精细的依赖关系管理。实际工作中设备模型的性能问题很少是瓶颈。真正要注意的是设备探测顺序带来的依赖问题。如果一个设备的 probe 依赖另一个驱动先完成光靠总线匹配顺序是不够的需要利用 device links 或者 deferred probe 机制。内核有deferred_probe机制当设备因为依赖资源没准备好而 probe 失败时会在后面稍后的时机重新尝试。这也是为什么 dmesg 里能看到类似 “deferring probe” 的原因遇到这个别慌是内核在等依赖。6. 学习设备模型的几个实操建议如果让我重新走一遍学内核驱动的路我会建议从一条比“写字符设备点灯”更有价值的路线入手。点灯只能让你学会怎么用 API不能让你看懂内核。真正值得的是去啃一下drivers/base/core.c、drivers/base/dd.c、drivers/base/bus.c这三个文件。它们不依赖具体硬件纯粹是设备模型的实现代码量适中、逻辑清晰是理解整套机制最好的入口。看代码时可以顺着一条线走“设备注册 → 加入总线 → 匹配驱动 → 调用 probe”。你会发现很多你以为很神奇的东西其实都是很朴素的链表遍历和回调分发。真的不用怕源码内核的注释比大部分培训机构的课件都良心。另外配置内核时如果有条件可以打开CONFIG_DEBUG_DRIVER和CONFIG_DEBUG_DEVRES。前者会打印大量设备模型匹配和 probe 流程的调试信息后者能看到 devm 资源分配和释放记录。这两个开关在真正排查设备模型问题时比任何 IDE 都顶用。最后再分享一个小技巧遇到设备模型相关的问题多去 /sys 和 /proc 下面翻一翻。它们不是给普通用户看的“花架子”而是内核设备模型里所有对象最直接的现实投影。学会从 sysfs 反推设备树、反推驱动绑定状态你排查问题的速度会甩开只看 dmesg 的人一大截。我在实际项目里吃过最多的亏就是没把“设备和驱动的关系”想清楚总以为驱动注册了就一定会被调用。直到有一次排查一个 USB 摄像头驱动怎么调参数都不 probe最后发现是 USB 匹配 id_table 写错了一个厂商 ID。那一次之后我彻底明白了设备驱动模型不是抽象的理论它是实实在在决定驱动生死的裁判。吃透这一层再看内核其它子系统心里会踏实很多。

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

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

免费获取报价