
做 Linux 驱动的人早晚都会被同一个问题卡住一两次明明驱动模块编译出来了也扔进了/lib/modules为什么插上设备以后系统就是不自动加载反过来为什么别人家板子上插个 CH340 串口线/dev/ttyUSB0自己就冒出来了答案不在于 insmod 和 rmmod 那两条命令而在于 Linux 内核态和用户态之间那套“设备事件通报、别名匹配、模块解析”的协作机制。这篇文章就围绕驱动自动加载来拆从设计原理讲到具体落地最后给出我踩过几次坑之后整理的排查清单。适合刚开始接触驱动开发的人也适合那些已经在调设备树、却对“模块为什么被加载”一知半解的工程师。1. 自动加载的完整链路从设备插入到 modprobe很多初学者会下意识认为内核发现自己有对应驱动模块所以主动去根文件系统里找.ko文件。这个直觉是错的内核既没有遍历目录的能力也不关心/lib/modules下放了什么。整个加载过程更像是一个“外包”流程内核负责通知用户态负责决策和执行。1.1 内核只做一件事把设备身份抛出来设备在电气上被识别之后内核的设备模型会为它创建一个struct device随后调用kobject_uevent_env()向用户态发送一条 uevent。这条 netlink 消息里最关键的内容就是MODALIAS你可以把它理解成设备的“身份证号”。对 USB 设备来说这个字符串长这样usb:v1A86p7523d0100dc00dsc00dp00icFFisc00ip00in00里面包含了厂商 ID、产品 ID、设备类别、接口类别等信息。对设备树节点来说MODALIAS则是类似of:NxxxTvendor,myprobe的形式。内核把身份信息广播出去之后自己的工作就结束了。它不会去判断“这个设备应该用哪个驱动”更不会主动加载模块。这个设计其实非常讲究。如果内核自己去做模块检索那就得把根文件系统路径、模块依赖关系、加载顺序这些策略全塞进内核一旦环境变得复杂比如模块放在 initramfs 里或者根文件系统还没挂载这套逻辑就会变得无比脆弱。把“谁去加载”交给用户态内核只保留最底层的通知机制职责边界清楚也方便用户态通过 udev 规则做各种自定义处理。1.2 udev 是传话人modprobe 才是执行者系统里收到这条 uevent 的通常是systemd-udevd或者传统 sysvinit 环境下的 udev。它先根据/etc/udev/rules.d里的规则处理设备节点名称、权限、符号链接同时注意到 uevent 里带着MODALIAS就会执行一个modprobe调用/sbin/modprobe -s -- $MODALIAS这里的-s表示静默模式错误输出走 syslog。也就是说udev 自己也不知道模块在哪儿它只是把内核给出的 modalias 字符串原封不动传给 modprobe由 modprobe 去解析。如果你在系统里执行过udevadm monitor就能亲眼看到这个过程。插上设备后屏幕上先出现内核发出的 KERNEL uevent紧接着出现 udev 处理后的 UDEV uevent再配合dmesg里驱动打印的日志一条完整的加载链就浮现出来了。1.3 modprobe 背后站着 depmod 生成的三件套modprobe 处理的是/lib/modules/$(uname -r)目录下的一组索引文件。每次安装新内核或新模块之后必须运行depmod -a它会在该目录下生成modules.alias保存模块与设备别名的对应关系modules.dep保存模块之间的依赖关系modules.symbols保存模块导出的符号与模块的对应关系modprobe 先拿着 modalias 去modules.alias里查匹配的模块名找到之后再根据modules.dep递归加载所有依赖模块。比如某个驱动依赖libphymodprobe 会先把libphy装好再装目标驱动。这一点对写过复杂驱动的人尤其重要如果你只insmod主模块却忘记它的依赖踩到的Unknown symbol错误极大概率就是这个原因。2. 自动加载的核心设计模块别名从哪来理解了链路之后最关键的环节就变成内核只给出了 modalias 字符串modprobe 凭什么知道这个字符串对应哪个.ko文件答案就在modules.alias。而modules.alias的每一条记录都来自驱动源码里的设备 ID 表。2.1 MODULE_DEVICE_TABLE 是如何变成别名的你在驱动源码里写设备 ID 表时一般会跟着写一行MODULE_DEVICE_TABLE(usb, my_usb_ids);这个宏本身不生成任何代码它只把my_usb_ids数组里的内容塞进模块文件的一个特殊 section 里。以后depmod解析.ko时会把这个 section 读出来逐条展开成人类可读的别名规则写进modules.alias。比如ch34x串口驱动的代码里有这么一段设备表包含厂商 ID0x1A86、产品 ID0x7523。depmod 扫描后会在modules.alias里生成类似这样的记录alias usb:v1A86p7523d*dc*dsc*dp*ic*isc*ip*in* ch34x当串口线插上内核发出MODALIASusb:v1A86p7523d0100dc00dsc00dp00icFFisc00ip00in00modprobe 拿着它去扫描上面的规则星号正好匹配后面的 d0100、dc00 等字段于是成功定位到ch34x。PCI、I2C、SPI 等总线的匹配逻辑也大同小异无非是struct pci_device_id、struct i2c_device_id、struct spi_device_id这些结构体配合对应的MODULE_DEVICE_TABLE(pci, ...)、MODULE_DEVICE_TABLE(i2c, ...)。写驱动的时候只要保证设备表字段填得正确、完整并且保留MODULE_DEVICE_TABLE这一行自动加载就有了第一层保障。2.2 平台总线下的特殊情况平台总线platform bus上没有真正的热插拔硬件事件设备要么由板级代码注册要么由设备树节点展开。它的 modalias 匹配规则也分两种情况。第一种是设备树匹配。驱动里写了of_match_table并且把 compatible 字符串写成了vendor,myprobe那么depmod会从MODULE_DEVICE_TABLE(of, ...)生成以of:N开头的别名udev 拿到设备树 modalias 后能准确命中。第二种是没有设备树的旧板子。平台设备的名字是板级代码里直接定的比如platform_add_devices传入一个名字myprobe。内核注册平台设备时它的 uevent 里不会有完整的设备 ID 表信息只会生成一个platform:myprobe这样的 modalias。这种情况下你会发现明明模块已经装进/lib/modules平台设备却没有绑定驱动。解决办法也很实在在驱动源码里显式声明别名MODULE_ALIAS(platform:myprobe);然后重新 depmod。这样modules.alias里就有了一条alias platform:myprobe myprobeudev 收到platform:myprobe后modprobe 就能找到模块。需要注意的是如果以后改用设备树这个手动别名不一定是坏事但最好用of_match_table生成的别名去匹配否则设备树 compatible 和平台名字不一致时会越弄越乱。2.3 内置驱动的别名也走同一套规则如果驱动直接编译进内核没有.ko自然会想内置驱动还需要别名吗答案是需要的尤其是系统用modules.builtin.modinfo告诉用户态“这个 alias 已经被内核内置了不用再去加载模块”。depmod 会把这个文件也读进索引里。当 udev 拿着MODALIAS去找 modprobe 时modprobe 看到 alias 被标记为 built-in就知道不需要也不应该再去加载外部模块。这个设计让用户态的加载逻辑保持一致无论驱动最终是模块还是内置modprobe 的查询路径都是通的。只不过内置驱动是在内核启动早期、由驱动模型自动完成匹配根本等不到模块加载这一步。3. 实操落地写一个能被自动加载的驱动理论讲再多不如直接上手跑一遍。我挑一个最典型的场景平台设备 设备树 compatible 匹配。这也是 ARM 板子上最常用的一套组合搞懂了它其他总线的操作都只是换表换宏。3.1 驱动代码侧该写什么下面是一个最小可用的示例驱动不绑定任何具体硬件probe 函数只打一条日志#include linux/module.h #include linux/platform_device.h #include linux/of.h static int myprobe_probe(struct platform_device *pdev) { pr_info(myprobe: driver matched and probe called\n); return 0; } static int myprobe_remove(struct platform_device *pdev) { pr_info(myprobe: device removed\n); return 0; } static const struct of_device_id myprobe_of_match[] { { .compatible vendor,myprobe }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myprobe_of_match); static struct platform_driver myprobe_driver { .probe myprobe_probe, .remove myprobe_remove, .driver { .name myprobe, .of_match_table myprobe_of_match, }, }; module_platform_driver(myprobe_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Auto-load demo driver);这里最重要的一行就是MODULE_DEVICE_TABLE(of, myprobe_of_match)。没有它depmod 无法生成设备树别名驱动就没办法被自动加载。很多人在网上抄驱动代码漏了这一行还能编译过但到了板子上怎么都不自动加载排查半天才发现这里缺了东西。module_platform_driver宏会帮你生成标准的 module_init 和 module_exit内部调platform_driver_register。probe 是否被调用取决于平台总线在注册设备、注册驱动时能否通过 compatible 字符串匹配上。3.2 编译、安装和 depmod 更新假设模块编译通过接下来这几条命令决定整个过程成不成make -C /lib/modules/$(uname -r)/build M$PWD modules sudo cp myprobe.ko /lib/modules/$(uname -r)/extra/ sudo depmod -a模块放在extra/目录下是发行版约定depmod默认会扫描这个目录。如果系统比较复杂在/etc/depmod.d里改过搜索路径那得自己确认扫描目录有没有包含 extra。depmod 跑完之后先别急着找设备用以下命令检查索引是否生成正确modinfo myprobe grep myprobe /lib/modules/$(uname -r)/modules.aliasmodinfo能看到模块自身携带的别名grep能看到 depmod 在索引文件里生成的记录。如果 modules.alias 里查不到那不管设备怎么插、怎么触发modprobe 都不可能在文件里找到对应关系。这一步是极好的“快筛”两秒钟就能排除一大半问题。3.3 用真实设备做一次全链路验证设备树里已经加上vendor,myprobe节点并重新烧录的情况下启动后直接看加载状态sudo modprobe -v myprobe lsmod | grep myprobe dmesg | tail但这里有个容易绕弯的地方modprobe myprobe是手动加载它验证的是“模块能装进去”并没有验证“设备出现时会不会自动触发加载”。要验证自动加载得让 udev 拿到真正的 modalias再把触发链路跑通。我给一个热插拔类设备的通用测试流程。先开监听窗口udevadm monitor然后插上设备。如果看到类似MODALIASusb:v1A86p7523d0100...的消息说明内核事件和 udev 处理的链路都已经通了。接着检查模块是否加载cat /sys/class/tty/ttyUSB0/device/modalias sudo modprobe -nv $(cat /sys/class/tty/ttyUSB0/device/modalias)第一条命令拿到设备对应的 modalias第二条用-n只做解析不实际加载能看到 modprobe 解析这条规则时对应的模块名。如果模块名正确再把-n去掉正常加载后lsmod里就会出现相应模块。对于平台设备这类没有真实热插拔的场景整套验证也可以靠盯启动日志来完成设备树节点注册后看对应驱动模块是否在 udev coldplug 阶段被加载。如果没自动加载先把设备树里的 compatible 和驱动里的字符串逐字符核一遍再回头看 modules.alias基本就能锁定问题。4. 开机启动场景下的自动加载设计驱动自动加载不只有“设备插入瞬间”这一种场景。很多产品上电启动时板子和外设早就焊在一起了设备从内核初始化的那一刻起就存在。这种冷启动下的自动加载逻辑会稍微绕一点。4.1 编译进内核还是编成模块这一步的选择直接影响自动加载的设计。编译进内核 (built-in)编译成模块 (module)匹配时机设备创建立即匹配无用户态依赖依赖 udev/modprobe或 initramfs 先加载文件大小内核镜像变大内核镜像小模块按需加载更新驱动必须重新编译内核单独编译.ko拷贝后 depmod 即可调试便利性打日志麻烦改参数还得重编rmmod后再modprobe迭代速度快适用场景根文件系统可能读不到、启动就要用的关键驱动大多数外设、可插拔设备启动时必须支持根文件系统的存储驱动、块设备驱动通常建议编译进内核否则内核从磁盘读根文件系统都成问题。而一般的串口、网卡、外设芯片编成模块更灵活。4.2 initramfs 与 coldplug 的关系如果你的驱动是模块而根文件系统在它下面才能读到那就必须先加载模块才能挂根文件系统。这种“先有鸡还是先有蛋”的矛盾靠 initramfs 机制解决。打包 initramfs 的时候会把目标驱动塞进去initramfs 阶段的 udev 会扫描已有的所有设备触发模块加载。系统切换到真正的根文件系统之后systemd-udevd 会重新执行一次类似动作把所有已经注册但还没加载驱动的设备再触发一遍这一步常被称为 coldplug。所以你会发现有些驱动即使没有真实的热插拔事件只要模块安装正确、别名匹配得上开机后依然能被自动加载。调试冷启动问题时最常用的工具是udevadm的trigger和settle组合udevadm trigger udevadm settletrigger让 udev 重新扫描 sysfs、为所有设备补发 ueventsettle则阻塞到事件队列处理完。如果某个模块始终不加载可以手动 trigger 一下观察它是否被加载。如果 trigger 后能加载说明问题出在启动时序或 initramfs 内容如果 trigger 后依然不行基本就是别名或模块本身的问题。4.3 什么时候该用 modules-load.d还有一类模块平时不会产生正确的 modalias但产品又要求开机就必须加载比如某些老式板级平台的 misc 设备。这时候可以在/etc/modules-load.d/下放一个.conf文件每行写一个模块名# /etc/modules-load.d/myprobe.conf myprobesystemd 在 boot 阶段会读取这个文件直接modprobe对应模块。严格说起来这不算“按需自动加载”而是“强制预加载”。在一些对启动时间敏感的产品里预加载反而会增加启动耗时。更好的做法还是给模块补上正确的设备 ID 表或别名让系统按需加载这也是我在项目里更推荐的方向。5. 常见问题排查与避坑实录最后这部分是我花时间最多的地方。写驱动代码一般几个小时就能搞定调自动加载问题却经常耗掉一两天。我把最常踩的几个坑整理成清单以后遇到可以直接对照。5.1 四种高频问题的速查表现象常见原因处理办法设备插上/dev节点没出现驱动模块没加载或 udev 规则缺失dmesg看设备是否识别lsmod看模块状态查/etc/udev/rules.d是否拦截了节点创建modprobe报Module xxx not found模块没安装到对应内核版本目录或者 depmod 没跑检查uname -r确认.ko在/lib/modules/$(uname -r)下重新depmod -a模块加载了lsmod能看到但 probe 没执行设备 ID 表不匹配或设备树 compatible 写错核对 modalias 与 modules.alias 规则核对设备树字符串加载时报Operation not permitted内核被 lockdown或者模块签名检查拦截dmesg查看具体锁定原因给模块签名或调整内核启动参数5.2 问题1插上 USB 设备后完全没有反应有次我调试一个 USB 串口芯片插上后dmesg只有 USB 枚举日志/dev下没有任何新节点。第一反应是驱动没装好但lsmod里查了一下连驱动影子都没有。再modprobe -v ch34x手动加载发现模块能正常装上设备节点也出来了。这就说明问题出在“自动触发”这一环而不是“模块能不能加载”这一环。我接下来运行udevadm monitor重新插拔发现内核 uevent 里有MODALIAS但 udev 处理链路没触发加载。最后查明是我的发行版用户态里/proc/sys/kernel/hotplug是空的而 udev 服务又没起来netlink 事件自然没人接收。这个排查顺序值得记住先看内核有没有识别设备再看用户态有没有收到事件最后才检查模块索引。5.3 问题2模块能加载probe 却不被调用平台设备驱动里最经典的错误是把of_match_table里的 compatible 写法搞错。设备树里写vendor,myprobe驱动里写vendor,myprobe1两个看起来差不多但内核匹配是严格字符串比较一个字符不对就不匹配。另一个不太容易想到的原因是设备树节点被另一个驱动先占用了。如果你看lsmod发现模块已经加载/sys/bus/platform/devices下也能看到设备节点但驱动就是没绑上那就去查一下这个设备的driver符号链接指向哪儿ls -l /sys/bus/platform/devices/*myprobe*/driver如果链接不存在说明确实没有驱动绑定如果链接指向别的驱动就说明你的驱动在匹配队列里排到竞争对手后面去了。这时候要回到设备树 compatible 的全局唯一性上去反思很多“probe 不执行”其实是被其他驱动截胡了。5.4 问题3冷启动阶段加载失败模块在热插拔测试时没问题但一开机就是加载不上这是我调试嵌入式设备时最头疼的。这大概率不是模块本身的问题而是模块依赖了另一个没进 initramfs 的模块。比如你的驱动依赖某个内核子系统而 initramfs 只打进了你的.ko没打进依赖项。排查这类问题要在 initramfs 生成阶段就确认依赖被包含。用modprobe --show-depends myprobe能直接列出模块依赖链打包脚本里根据这个列表把依赖一起塞进 initramfs。等你发现开机日志里只有模块加载失败、却看不到依赖模块失败的记录就要怀疑是不是依赖模块连加载尝试都没发生过。最后再分享一个经验调试自动加载时尽量别用insmod。insmod能绕过很多索引机制直接加载这种做法虽然方便但它掩盖了 depmod 和 modules.alias 层面的问题。我一直要求自己在开发阶段就用modprobe测试因为你要交付的是一个“放上去就能自动加载”的驱动而不是只在你自己机器上能跑起来的一堆.ko。等到交付前再做一次干净环境的全流程验证新内核、新模块、depmod 更新、冷启动自动加载全套跑通才算是真的把这件事做完了。