Linux驱动自动加载全解析:从udev、modprobe到设备树匹配
2026/9/15 5:59:43 网站建设 项目流程

做 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递归加载所有依赖模块。比如某个驱动依赖libphy,modprobe 会先把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

当串口线插上,内核发出MODALIAS=usb:v1A86p7523d0100dc00dsc00dp00icFFisc00ip00in00,modprobe 拿着它去扫描上面的规则,星号正好匹配后面的 d0100、dc00 等字段,于是成功定位到ch34x

PCI、I2C、SPI 等总线的匹配逻辑也大同小异,无非是struct pci_device_idstruct i2c_device_idstruct 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 myprobe,udev 收到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.alias

modinfo能看到模块自身携带的别名,grep能看到 depmod 在索引文件里生成的记录。如果 modules.alias 里查不到,那不管设备怎么插、怎么触发,modprobe 都不可能在文件里找到对应关系。这一步是极好的“快筛”,两秒钟就能排除一大半问题。

3.3 用真实设备做一次全链路验证

设备树里已经加上vendor,myprobe节点并重新烧录的情况下,启动后直接看加载状态:

sudo modprobe -v myprobe lsmod | grep myprobe dmesg | tail

但这里有个容易绕弯的地方:modprobe myprobe是手动加载,它验证的是“模块能装进去”,并没有验证“设备出现时会不会自动触发加载”。要验证自动加载,得让 udev 拿到真正的 modalias,再把触发链路跑通。

我给一个热插拔类设备的通用测试流程。先开监听窗口:

udevadm monitor

然后插上设备。如果看到类似MODALIAS=usb: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。所以你会发现,有些驱动即使没有真实的热插拔事件,只要模块安装正确、别名匹配得上,开机后依然能被自动加载。

调试冷启动问题时,最常用的工具是udevadmtriggersettle组合:

udevadm trigger udevadm settle

trigger让 udev 重新扫描 sysfs、为所有设备补发 uevent,settle则阻塞到事件队列处理完。如果某个模块始终不加载,可以手动 trigger 一下,观察它是否被加载。如果 trigger 后能加载,说明问题出在启动时序或 initramfs 内容;如果 trigger 后依然不行,基本就是别名或模块本身的问题。

4.3 什么时候该用 modules-load.d

还有一类模块,平时不会产生正确的 modalias,但产品又要求开机就必须加载,比如某些老式板级平台的 misc 设备。这时候可以在/etc/modules-load.d/下放一个.conf文件,每行写一个模块名:

# /etc/modules-load.d/myprobe.conf myprobe

systemd 在 boot 阶段会读取这个文件,直接modprobe对应模块。严格说起来这不算“按需自动加载”,而是“强制预加载”。在一些对启动时间敏感的产品里,预加载反而会增加启动耗时。更好的做法还是给模块补上正确的设备 ID 表或别名,让系统按需加载,这也是我在项目里更推荐的方向。

5. 常见问题排查与避坑实录

最后这部分是我花时间最多的地方。写驱动代码一般几个小时就能搞定,调自动加载问题却经常耗掉一两天。我把最常踩的几个坑整理成清单,以后遇到可以直接对照。

5.1 四种高频问题的速查表

现象常见原因处理办法
设备插上,/dev节点没出现驱动模块没加载,或 udev 规则缺失dmesg看设备是否识别,lsmod看模块状态,查/etc/udev/rules.d是否拦截了节点创建
modprobeModule 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。等你发现开机日志里只有模块加载失败、却看不到依赖模块失败的记录,就要怀疑是不是依赖模块连加载尝试都没发生过。

最后再分享一个经验:调试自动加载时,尽量别用insmodinsmod能绕过很多索引机制直接加载,这种做法虽然方便,但它掩盖了 depmod 和 modules.alias 层面的问题。我一直要求自己在开发阶段就用modprobe测试,因为你要交付的是一个“放上去就能自动加载”的驱动,而不是只在你自己机器上能跑起来的一堆.ko。等到交付前再做一次干净环境的全流程验证:新内核、新模块、depmod 更新、冷启动自动加载,全套跑通,才算是真的把这件事做完了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询