ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux设备驱动模型深度解析:总线、设备与驱动的底层协同

Linux设备驱动模型深度解析:总线、设备与驱动的底层协同 搞懂 Linux 设备驱动模型才算真正吃透内核底层以前我刚接触内核时最先啃的就是字符设备驱动。open、read、write 全搞明白register_chrdev 一调/dev 下面能看到设备节点我以为自己已经入门了。后来内核升级同样的驱动在新内核上出问题我才意识到自己根本没理解内核底层的组织方式。不是 API 变了而是设备、驱动、总线这三者之间的关系我没有真正建立起来。Linux 设备驱动模型就是内核里那套把设备、驱动、总线、类、属性全部串起来的管理框架。搞懂它你才能看懂设备树是怎么被解析的platform_driver 是怎么匹配到设备的sysfs 里的目录为什么长那样以及一个 USB 设备插上去之后内核到底做了多少事。这篇文章我从头到尾梳理一遍尽量用实际开发中的视角来讲不堆概念。1. 为什么设备驱动模型如此重要说白了设备驱动模型解决的是内核里“一堆硬件和一堆驱动怎么组织、怎么管理、怎么协同”的问题。没有这套模型之前驱动就是各自为政每个驱动自己注册一个设备号自己管理设备文件自己在 init 里做一堆初始化。早期内核确实就是这么干的设备数量少倒也凑合能用。但到了今天一台普通 PC 上光 PCI 设备就有几十个嵌入式 SoC 内部外设更是动辄上百个。如果还靠驱动各自为政系统启动时谁先加载、谁能用哪个设备、设备之间依赖关系怎么处理全是一团乱麻。设备驱动模型的出现就是内核的一次“治理结构升级”。1.1 设备驱动模型本质是对象化管理设备驱动模型的核心思想非常朴素把硬件设备、驱动、总线抽象成内核对象然后用一套统一规则管理它们。每个设备是一个 struct device每个驱动是一个 struct device_driver总线决定设备和驱动如何相互查找、匹配和通信。你可以把这套模型想象成一个大公司总线是猎头平台设备是挂在平台上的职位需求驱动是来应聘的候选人。猎头平台负责把匹配的职位和候选人撮合到一起一旦匹配成功就执行 onboarding——也就是驱动的 probe 函数。内核里 probe 的调用本质上就是职位的录用通知。这套对象化管理带来一个直接好处代码复用率大幅提升。一个 USB 驱动不需要关心设备是插在 xHCI 控制器还是 EHCI 控制器上只要总线层面把设备抽象成 struct usb_device驱动只需面向这个抽象对象编程。类似地一个 SPI 外设驱动也不用关心 SPI 控制器是哪个厂商的 IP平台层面的标准化已经把它屏蔽掉了。1.2 设备模型贯通上下层是内核的骨架设备驱动模型还有一个容易被忽略的作用它是内核里少有的、从上到下贯穿多个子系统的架构。设备模型之上有字符设备、块设备、网络设备、输入子系统、ALSA、DRM设备模型之下直接面对硬件资源、中断、DMA、时钟、电源管理旁边还挂着一个 sysfs 文件系统向用户空间暴露一切。也就是说设备模型不是一个孤立的子系统它是整个内核设备体系的“骨架”。理解了它你就能把零散的知识点串起来。比如一个 i2c 触摸屏驱动它注册为 i2c_driveri2c 子系统通过 bus 匹配让它 probe它内部又通过 input 子系统注册上报触摸事件同时它用 devm_ 系列接口管理电源、中断、GPIO 资源——这些操作全都建立在设备驱动模型的框架之上。所以我才说搞懂设备驱动模型才算真正吃透内核底层。它不是某个具体驱动类型的模板而是所有驱动都赖以生存的基础设施。2. 三大核心对象设备、驱动、总线设备驱动模型里最核心的三个数据结构struct bus_type、struct device、struct device_driver。理解这三者的关系就理解了整个模型的一半。剩下的一半是它们之间如何匹配、如何组织、如何暴露给用户空间。2.1 总线匹配器和通信通道总线在设备模型里不只是物理上的线路它首先是一个“匹配器”。每个注册到内核的总线比如 platform_bus_type、i2c_bus_type、pci_bus_type都有一套自己的匹配规则以及匹配成功之后需要执行的操作。看内核源码里的 struct bus_type 定义它包含 match、probe、remove、shutdown、suspend、resume 等回调函数。其中驱动开发者接触最多的是 match。以 platform 总线为例它的 match 函数会尝试多种匹配方式先看设备树 compatible 是否匹配再看 platform ID 表id_table再看驱动名和设备名是否一致。这里有个容易混淆的点总线的 probe 函数和设备驱动里的 probe 不是一回事。总线 probe 是总线层面拿到匹配结果后触发的总入口它负责调用真正 drv-probe。理论上一条总线可以挂很多驱动总线自己也有生命周期管理。把总线理解为“调度中心”更准确。2.2 设备和驱动数据和操作分离设备描述的是“有什么硬件”驱动描述的是“怎么操作硬件”。二者解耦是设备驱动模型设计上最漂亮的地方。struct device 里存放的是设备的硬件信息寄存器地址、中断号、时钟、GPIO、DMA 通道等struct device_driver 里存放的是操作这些硬件的逻辑init、read、write、ioctl、suspend、resume 等。设备与驱动分离带来一个直接好处同一个设备可以换不同的驱动来操作同一个驱动也能支持多个设备。比如 eMMC 控制器内核里有厂商原厂驱动也有社区的通用驱动二者并存通过设备树或平台数据去选择。设备节点就是“岗位需求”驱动就是“候选人”岗位和候选人自由组合系统才有弹性。实际操作中你会发现内核引导阶段会先枚举所有设备构建出完整的设备列表驱动是模块化加载的有些驱动编成了 module等系统起来之后才 insmod。设备早就等在那里了驱动什么时候来什么时候就触发匹配。如果匹配成功就调用驱动里的 probe 完成初始化。2.3 三类关系一对多、多对一、多对多设备和驱动的关系比想象中灵活。一个驱动可以支持多个设备——比如一个 GPIO 驱动支持 SoC 上几十个 GPIO controller一个设备也可以被多个驱动匹配——比如一个网络设备既能被厂商驱动匹配也能被通用驱动匹配。内核里靠优先级、依赖顺序和 subsystem 接口去约束最终选哪个驱动。这个特性在实际开发中通常以“面板驱动”“PHY 驱动”的形式出现。一个 LCD 屏既有 DRM 面板驱动也可能有 framebuffer 驱动二者公用同一套设备节点靠 Kconfig 参数决定编哪个进内核。设备模型本身不限制一方对多方的绑定关系这种灵活性也增加了理解难度——很多新手以为设备驱动是 1:1 的关系一遇到多对多就懵了。3. sysfs 与 kobject设备模型通向用户空间的大门设备模型不光是内核里的抽象它还通过 sysfs 把整个设备树“投射”到用户空间。这也是为什么你 ls /sys 能看到一排排目录每个目录对应一个设备或驱动。sysfs 的底层机制就是 kobject 和 kset。3.1 kobject一切设备对象的基类kobject 是设备模型里最小的“对象单位”struct kobject 里包含了名称、引用计数、parent 指针、ktype、sysfs_dentry 等字段。可以说设备模型里一切能在 sysfs 里看到的东西底层都是 kobject。device、driver、bus、class 内部都内嵌了一个 kobject。kobject 的作用有两个一是提供引用计数管理确保对象在使用的过程中不会被意外释放二是建立层次结构parent 指针决定了 sysfs 目录的父子关系。比如一个 USB 设备它的父对象是 USB 接口再往上是 USB 设备再往上是 USB 控制器——这条链在 sysfs 里就是 /sys/devices/pci0000:00/.../usb1/1-1/1-1:1.0 这样一串目录。引用计数这部分是驱动开发中“野指针”的高发地带。如果你在驱动里拿到了一个 device 指针想把它保存下来备用一定要用 get_device() 增加引用计数用完之后 put_device() 释放。否则设备一旦被拔出你的指针就成了悬空指针系统瞬间宕机也不是没可能。3.2 sysfs 属性节点的读写机制sysfs 目录下的文件就是设备模型暴露给用户空间的属性attribute。属性分为普通属性struct attribute和二进制属性struct bin_attribute。普通属性一般用于读写字符串比如某个设备的工作模式、版本号二进制属性用于读写大批量数据比如固件下载、寄存器 dump。属性文件的核心是 show 和 store 两个回调。用户 cat 一个属性文件内核就调用 show 回调把内核里的数据格式化成字符串返回给用户用户 echo 一个值进去内核就调用 store 回调解析字符串并写入硬件或驱动状态。这里有个经验不要在 store 回调里做耗时操作。sysfs 属性文件的读写是同步的调用者会阻塞在 read/write 系统调用上。如果你在 store 里做 msleep 或者大块内存操作用户空间会被长时间卡住体验非常糟糕。真要做事应该把工作交给 workqueue 或线程。3.3 class 的作用从设备号到设备节点的连接器class 可能是一开始最容易被忽略的部分但它在设备模型里的位置极其重要。class 是一种逻辑分组告诉用户空间的设备管理器如 udev这个设备属于什么类型并生成对应的设备节点。以 misc 设备为例misc_register() 内部会创建一个 misc class并在 /sys/class/misc/ 下按设备名创建目录。同时misc 子系统会动态分配一个 misc 设备号并调用 device_create() 把设备节点创建到 /dev/ 下。class 的存在让 udev 可以依据 /sys/class/xxx/ 的信息自动创建设备节点权限和命名规则都能灵活配置。反过来如果你在驱动里忘了创建 class即使注册了字符设备、申请了设备号/dev 下也不会自动出现设备节点。这种问题很隐蔽驱动加载没有报错但是应用层就是打不开设备文件。排查时一定要检查 class 是否创建成功、device_create 是否返回了错误指针。4. 设备树与 platform 总线嵌入式驱动的现代形态在老一点的内核版本里平台设备靠板级文件硬编码注册比如在 arch/arm/mach-xxx/ 下调用 platform_device_register()。这种方式维护成本极高换个型号的板子就要改一遍 C 代码。设备树出现之后平台设备的信息全部挪到了 dts 文件里内核启动时统一解析。4.1 设备树如何描述设备设备树Device TreeDT本质上是一种描述硬件拓扑的数据结构用节点node和属性property表示设备及其配置。每个节点都有 compatible 属性这是驱动匹配的关键。比如一个 I2C 触摸控制器在 dts 里会写成i2c1 { touchscreen38 { compatible edt,edt-ft5x06; reg 0x38; interrupt-parent gpio1; interrupts 13 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio5 12 GPIO_ACTIVE_LOW; }; };compatible 字符串的命名规则是“厂商,型号”内核匹配时优先查设备树 compatible再查驱动里的 of_match_table。总线枚举时会把 dts 里的节点转换成 struct device等待驱动来匹配。4.2 platform_driver 的完整生命流程platform 总线是设备模型里最常用的总线类型它处理的是一类“挂在 CPU 总线上、没有标准枚举机制的设备”——SoC 内部外设基本都是这个路数。所以重点看一个 platform 驱动的完整流程就能把整个模型串起来。驱动入口通常是 module_platform_driver(xxx_driver)这个宏展开后就是 module_init 里调用 platform_driver_register()。注册完成后platform 总线会拿着驱动里的 id_table 和 of_match_table去遍历总线上所有 device逐一尝试匹配。匹配成功后总线调用 platform_drv_probe()最终进入我们的 probe 函数。在 probe 里我们一般会做四件事获取并映射硬件资源devm_platform_ioremap_resource、申请中断devm_request_irq、初始化硬件状态、注册子设备或子接口比如注册 input_dev、mtd_info、net_device 等。驱动卸载时走 remove 回调常见坑点是顺序问题如果 remove 里先释放了中断又去操作硬件中断处理函数可能还在跑就会踩到空指针。比较稳妥的做法是先用 free_irq 确保中断不会再来再操作硬件和释放资源。4.3 设备树与 ACPI两种硬件描述方式嵌入式用设备树x86 PC 用 ACPI二者背后都是同一个设备模型在承载。ACPI 里的 _HID、_CID 就相当于设备树里的 compatibleACPI 表在固件阶段就被 BIOS 解析好了内核启动时直接读取。PCI 设备则通过 PCI 配置空间的 vendor ID、device ID 来匹配。这个共性说明了一件事设备驱动模型的匹配机制是有很强扩展性的。你可以在同一套框架上接入新总线类型定义任意匹配规则内核并不关心底下具体跑的是设备树还是 ACPI。总线 设备 驱动这个三角结构是内核所有设备管理的统一范式。5. 驱动资源管理devm_ 系列接口的巧妙设计probe 函数里最让开发者头疼的就是错误处理。每一行代码都可能失败一旦失败必须手动释放前面已经申请的资源。代码一长错误处理路径比主流程还复杂还容易漏释放。devm_ 系列的资源管理接口就是专门解决这个问题的。5.1 devm_ 接口为什么能自动释放devm_ 全称 is managed device resources。它在 struct device 内部维护了一个资源链表每次 devm_kzalloc() 成功就向链表挂一个节点记录资源类型、大小和释放函数。驱动 probe 成功或失败退出时内核统一释放链表里所有资源。这意味着只要你在 probe 里用 devm_ 接口申请资源就基本不用写错误处理路径。中断申请失败直接 return后面代码不执行资源链表会在设备移除或 probe 失败时自动清理。释放顺序也有讲究资源链表按申请顺序插入释放时按逆序进行避免了依赖关系导致的顺序问题。5.2 哪些场景必须手动释放devm_ 不是万能的。有些资源不走 device 生命周期管理比如你创建的 kernel thread、你注册的 sysfs 属性、你申请的 dma_buf都可能需要手动清理。还有一类情况你申请的资源不想跟随设备生命周期走而是在某个回调里提前释放那也建议手动处理。比较典型的是 request_threaded_irq 的线程化中断。devm_request_threaded_irq 虽然方便但在某些复杂场景下中断线程里可能还在使用其他子系统而设备移除时资源释放顺序未必满足你的需求。这种时候宁可手动 request_irq free_irq把释放时机掌握在自己手里。5.3 probe 失败时资源清理的两条路径probe 失败时内核会自动调用 devres_release_all()把该 device 上所有 devm_ 资源一次性释放。但这里有个容易被忽略的坑devm_ 资源释放时会调用对应的 release 回调如果你自定义了 release 函数需要时刻保证它是无副作用的、可以被重复执行安全的。否则设备热插拔时release 被调用两次可能直接导致系统崩溃。另外probe 里如果注册了子设备比如 mfd_add_devices子设备也有自己的生命周期。父设备的 devm_ 资源释放不会自动触发子设备的 remove。所以 mfd 子设备的清理一定要放在父设备的 remove 回调里由你自己来组织调用顺序。6. 从零写一个 platform 驱动看懂设备模型的落地理论说得再多不如实际写一个最小驱动来得直接。下面我用一个虚拟的、挂在平台总线上的 GPIO LED 驱动为例把刚才讲的那些概念全部落到代码里。这个例子不完整到可以直接编译但足够展示设备模型的核心工作流。6.1 驱动结构体从 platform_driver 到 file_operations一个 platform 驱动核心就是一个 struct platform_driver里面装着 probe、remove、id_table、driver 等字段。如果这个设备还要提供用户空间访问接口通常还会创建一个字符设备或者 misc 设备并在里面实现 file_operations。这就是热词里提到的“拦截 read write”的场景驱动通过 file_operations 接管用户空间对设备的读写请求。以下是一个最小驱动的骨架重点看设备模型部分的组织方式#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio/consumer.h #include linux/miscdevice.h #include linux/uaccess.h struct my_led_dev { struct gpio_desc *desc; struct miscdevice misc; }; static int my_led_open(struct inode *inode, struct file *file) { return 0; } static ssize_t my_led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { struct my_led_dev *led container_of(file-private_data, struct my_led_dev, misc); char kbuf[8]; int value; if (copy_from_user(kbuf, buf, min(count, sizeof(kbuf)))) return -EFAULT; if (kstrtoint(kbuf, 10, value)) return -EINVAL; gpiod_set_value(led-desc, value ? 1 : 0); return count; } static const struct file_operations my_led_fops { .owner THIS_MODULE, .open my_led_open, .write my_led_write, };这里没有 read 函数是因为 LED 设备只需要写。实际项目里read 一般用来读取设备状态或寄存器值write 用来下发命令。文件操作绑定在 miscdevice 上misc 子系统帮忙申请设备号和创建设备节点比裸写 char 设备省事很多。6.2 probe 里的资源申请与节点创建probe 函数是整个驱动的核心舞台。它做的事情很典型解析设备树获取 GPIO、注册 misc 设备、初始化硬件状态。注意我全部用了 devm_ 接口失败直接 return不需要手动清理。static int my_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct my_led_dev *led; struct gpio_desc *desc; int ret; led devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); if (!led) return -ENOMEM; desc devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(desc)) return PTR_ERR(desc); led-desc desc; led-misc.minor MISC_DYNAMIC_MINOR; led-misc.name myled; led-misc.fops my_led_fops; led-misc.parent dev; ret misc_register(led-misc); if (ret) return ret; platform_set_drvdata(pdev, led); dev_info(dev, myled probed\n); return 0; }注意这里 misc_register 不是 devm_ 接口所以如果后续还有别的初始化步骤失败需要手动 misc_deregister。更稳妥的做法是把 misc 注册放到 probe 的最后一步确保它之前的操作全部成功才创建设备节点。6.3 驱动注册宏与设备树匹配模块入口使用 module_platform_driver 宏配合 of_match_table 完成设备树匹配。设备树里 compatible 字符串必须与这里的 of_device_id 完全一致大小写和逗号都不能出错。static const struct of_device_id my_led_of_match[] { { .compatible vendor,myled }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match); static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name myled, .of_match_table my_led_of_match, }, }; module_platform_driver(my_led_driver); MODULE_LICENSE(GPL);设备树里对应节点只要写上 compatible vendor,myled内核枚举时就会自动绑定这个驱动。驱动加载完成之后你在 /sys/bus/platform/drivers/myled/ 下会看到一个符号链接指向设备节点这就是匹配成功的标志。6.4 热插拔场景下 remove 的执行时机remove 是 probe 的反过程负责释放所有资源、注销子设备、做必要的硬件关闭操作。我的建议是能被 devm_ 管理的资源全交给 devm_remove 里只做 devm_ 管不到的事比如 misc_deregister、某些子系统的手动注销。设备热插拔时内核会在所有对设备的引用全部释放后才调用 remove。如果没有引用设备拔掉后 remove 会立刻执行。这个时机很难从驱动里主动感知所以驱动不能假设 remove 一定在某个安全时刻被调用。严谨的做法是加一个状态标志位probe 置为 runningremove 清掉所有外部访问先检查这个标志位。7. 常见问题与排查技巧实录设备模型相关的坑我踩过不少很多问题不大但排查起来很费时间。我把最常见的几类问题整理成速查表方便大家遇到类似报错时能快速定位。7.1 驱动匹配失败probe 没有被调用驱动加载后没有任何 log/sys/bus/platform/devices/ 下能看到设备节点但驱动目录下空无一人。这种问题九成是 compatible 对不上。检查顺序确认设备树里节点的 compatible 字符串和驱动里的 of_match_table 完全一致确认驱动已经被编进内核或已经 insmod 成功lsmod 能看到确认设备树被正确编译并加载/proc/device-tree 下能找到对应节点确认没有多个驱动抢同一个设备或者驱动 name 冲突导致匹配错乱。设备树文件的语法问题也常见一个少了分号的错误会导致整个 nodes 无法解析驱动自然匹配不上。用 dtc 编译不报错并不代表语法绝对正确最好再检查一下 /sys/firmware/devicetree/base/ 下导出的树结构。7.2 probe 成功了但是设备节点没创建前面已经提过misc 设备注册成功但 /dev 下没有节点多半是 class 或 udev 的问题。手动执行cat /sys/class/misc/myled/dev看看主次设备号是否分配成功。如果设备号正常但 /dev 没有节点多半是 udev 规则没匹配上检查 /etc/udev/rules.d/ 下的规则。还有一种可能misc_register 的 parent 设置不对导致 sysfs 目录层级过深设备节点被创建到了非预期位置。这种情况ls /dev/ | grep myled找不到用find /sys -name myled反而能发现端倪。7.3 设备挂载在总线上但某个属性文件读写报错属性文件读写报错一般是 show 或 store 回调里出了问题。用 dmesg 查看内核日志如果有 BUG: unable to handle kernel paging request 之类的报错基本就是空指针或越界访问。排查方法在 show/store 函数里打印 dev 指针和私有数据指针确认它们没有被意外覆盖。buffer 大小也是个常见坑。sysfs 属性文件的 show 回调最多能写的 buffer 长度是 PAGE_SIZE也就是 4096 字节。如果你要输出的内容超过 4096必须截断或改用 bin_attribute。store 回调的 count 表示用户实际写入的字节数但用户可能带了一个换行符解析字符串时要处理掉否则 kstrtoint 会失败。7.4 设备树与驱动间的资源重叠冲突多个 dts 节点引用同一个寄存器区域或者中断号重复配置platform 子系统的 devm_platform_ioremap_resource 会返回 -EBUSY。这类问题排查时先看 dmesg 里有没有 resource busy 的提示然后检查每个节点的 reg、interrupts 是否与其他外设重叠。还有一种隐蔽场景GPIO 复用冲突。两个 dts 节点都定义了同一个 gpio 引脚第一个驱动 probe 时 gpiod_get 成功第二个驱动 probe 时返回 -EBUSY。这种问题从设备树语法上完全看不出来必须结合 SoC 的 pinmux 设置去排查或者在 dmesg 里搜 pinmux 关键字确认引脚被谁占用了。7.5 remove 里顺序颠倒导致的内核崩溃设备拔掉瞬间驱动崩溃十次里有八次是 remove 顺序写反了。比如先释放了中断然后调用 free_irq 的操作又触发中断处理函数或者先卸载了 sub-device再访问它内部的寄存器。安全做法是先移除对外接口misc_deregister、device_destroy再释放中断最后操作硬件。如果崩溃发生在 remove 的一半只有一份完整 log 才可能定位建议开了 panic_on_oops 再配合 KASAN 使用。KASAN 能精准报告 use-after-free 的位置是排查这类问题的一把好手。8. 设备模型里容易踩的 5 个细节坑最后聊几个藏在细节里的坑。这些坑不大但每一个都够你折腾半天。第一个是 device_create 的 dev_t 参数。如果你在杂乱的内核版本上移植驱动user space 的 mknod 或 udev 可能拿不到正确的设备号。多数情况下我们会故意传 0让内核自动分配但从老代码里复制过来的固定主设备号会导致设备节点与驱动不匹配。第二个是 module_platform_driver 宏展开后的 module_exit 行为。那是调用 platform_driver_unregister然后等所有设备 remove 完成。如果你的 remove 回调里有长时间阻塞操作rmmod 时系统会卡住看起来像死锁其实是在等设备释放。第三个是 platform_set_drvdata 和 platform_get_drvdata 的配对。很多新手在 probe 里 mid-way 用了 dev_set_drvdata到 remove 里又用 platform_get_drvdata 去取结果拿到 NULL。建议代码里统一用 platform_set_drvdata / platform_get_drvdata 配对不要混用。第四个是设备树节点里的 status 属性。默认 status okay如果你把它设成 disabled内核枚举时会跳过多达十几个设备。但某些 soc 级节点的 status 由 bootloader 修改不同启动模式下状态差异很大这会让驱动在 A 板子上正常、B 板子上完全失联。第五个是 of_match_table 的 .data 字段。它是一个 void*一般用来存放硬件版本差异表。但如果你没加 const在某些编译选项下会出现段错误。还有这个字段的匹配优先级很高它比 compatible 的名字更重要。9. 内核调试中那些不能省的检查真正干活的时候日志是最强的助手。设备模型相关的调试我一般从三个层面入手编译期看 Kconfig 和 Makefile运行期看 dmesg 和 /sys异常期看 ftrace 和 kprobe。9.1 用 ftrace 跟踪匹配与 probe 调用启动时如果加了 ftrace 的 event 开关就能追踪设备注册、驱动注册、匹配命中等环节的调用关系。platform 子系统有 tracepoint 可以直接看 probe 调用链。bus 的 match 和 probe 都在 event 里有记录打开之后能回答“这个驱动到底被调用了吗”这种问题。打开方式很简单# 在挂载了 tracefs 的系统上 echo 1 /sys/kernel/tracing/events/platform/enable cat /sys/kernel/tracing/trace_pipe能看到 platform_match、platform_drv_probe 之类的函数调用记录。如果什么都没打出来说明匹配根本没走到这一步问题大概率在 compatible 或设备树解析上。9.2 动态调试与 dev_info 的组合内核的 dynamic_debug 功能允许你在不重新编译的情况下打开源代码里所有 dev_info、dev_dbg 的输出。设备模型子系统本身也大量使用 dev_dbg 输出关键操作日志打开之后能看到的细节非常多。开启方式echo file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control通过这种方式可以在不重编内核的情况下观测 platform.c 内部的匹配逻辑对定位匹配失败非常有效。9.3 KASAN、UBSAN、LOCKDEP 在设备模型代码上的作用如果遇到的是内存损坏、死锁、竞态这类问题普通打印根本不够用。建议把 KASAN、UBSAN、LOCKDEP 全开。KASAN 能抓 use-after-free 和缓冲区越界UBSAN 能抓未定义行为LOCKDEP 能抓锁序问题。这三个工具在调试设备模型相关的复杂 bug 时几乎是必须开的。内核配置里搜 CONFIG_KASAN、CONFIG_UBSAN、CONFIG_PROVE_LOCKING全部打开然后在 KASAN 报告出现的时候backtrace 里能看到具体的释放函数和释放位置再结合设备模型的资源管理逻辑定位问题就非常快了。9.4 热插拔竞态的终极排查法延迟和压力测试热插拔 bug 最恶心的点是难以稳定复现。设备模型里 probe 和 remove 是并行的如果驱动里对共享资源的保护不充分很容易出现偶发崩溃。我的做法是写一个循环插入拔出的脚本配合在 remove 里加 msleep 人为扩大竞态窗口让问题更容易暴露。for i in $(seq 1 100); do echo 1-1 /sys/bus/usb/drivers/usb/unbind sleep 0.01 echo 1-1 /sys/bus/usb/drivers/usb/bind done同时开着 dmesg一旦看到 call trace 立刻保存。一般跑上几百轮大多数竞态问题都能现形。加了 LOCKDEP 之后哪怕只是锁序问题也能在第一次发生时就给出精确报告。我个人在实际操作中的一个体会是设备驱动模型这套东西你越是用“对象关系”的视角去看就越觉得内核设计得巧妙越是停留在“API 怎么调”的层面遇到问题就越容易抓瞎。它不像某个具体的网络协议栈那样功能明确它更像是一套组织规则把内核里上千个设备、上千个驱动安排得明明白白。真正写驱动写得多的老手往往不会觉得某段代码有多惊艳而是会感叹这层抽象把复杂度隔离得恰到好处。最后再分享一个小技巧看设备模型源码建议从 drivers/base/core.c 和 drivers/base/platform.c 这两个文件入手一个管通用对象生命周期一个管平台设备和驱动的互动。这两个文件吃透了内核里其他子系统I2C、SPI、PCI、USB的驱动模型代码读起来的思路是完全一致的。看到 bus_type 的一堆回调你不会再纠结它是什么你只会自然地想这里搞完 match该走 probe 了。
返回列表