FEATURED · 精选文章

Linux驱动自动加载机制全解析:udev、modprobe与设备树协同

发布时间 / 2026/9/20 6:35:26
来源 / 创域科博编辑部
栏目 / 资讯中心
Linux驱动自动加载机制全解析:udev、modprobe与设备树协同 1. 为什么能 insmod和能自动加载是两件事很多朋友做 Linux 驱动开发的第一步是从 insmod 开始的写一个 hello 驱动编译出 .koinsmod hello.kodmesg 里看到输出然后rmmod卸载觉得驱动开发不过如此。但真正到项目里需求往往不是手动加载一下看看效果而是设备一接上驱动自己就工作开机之后驱动不用人管。也就是这篇要聊的核心话题Linux 驱动的自动加载设计与实现。我最早接触这个概念时也走过弯路。当时给一块 ARM 板子调一个 SPI 触摸屏驱动insmod 之后一切正常触摸坐标也准。可产品要交付的是开机就能用我一开始的方案是在系统启动脚本里加一行 insmod结果发现经常出现模块加载了但设备 probe 没有被调用的怪现象。后来才明白自动加载不是简单地把 insmod 挪到开机脚本里而是一整套内核-用户空间-模块三方配合的机制。所以这篇把自动加载这件事拆开讲清楚覆盖这样几个问题内核怎么知道来了一个设备又是怎么把需求传递给用户空间的udev/systemd 如何拿到设备信息最终调用 modprobe 找到正确的模块模块里的 MODULE_DEVICE_TABLE、MODULE_ALIAS 到底做了什么设备树 compatible 匹配和自动加载之间的关系最后给一份完整的可复现示例和常见排查手段。这篇更适合已经会写字符设备驱动、但对驱动怎么被系统自动拉起来还比较模糊的人。如果你刚接触 Linux 驱动建议先把 file_operations、内核模块的 init/exit 函数和设备号分配这些基础过一遍再看这篇会顺畅很多。2. 自动加载的完整链路从设备插入到模块载入2.1 内核首先发出设备事件要理解自动加载必须先理解 Linux 内核的设备模型。简单说内核里存在总线的概念比如 platform 总线、USB 总线、PCI 总线、I2C 总线。设备接入或注册时总线子系统会调用device_add()这一步不仅是把设备挂进内核的设备树里还会在 sysfs 里创建对应的目录结构同时向用户空间发一个热插拔事件。这个事件在 Linux 里叫 uevent。内核通过kobject_uevent()发送一组环境变量用户空间拿到之后就能知道发生了什么。最关键的环境变量是MODALIAS它相当于内核给用户空间的一个设备身份证号。举个例子当某个 platform 设备注册时内核可能发出这样一组环境变量ACTIONadd DEVPATH/devices/platform/my_device SUBSYSTEMplatform MODALIASplatform:my_device用户空间想要驱动它就必须找到支持platform:my_device这个 MODALIAS 的驱动模块。如果设备挂在 USB 总线上MODALIAS 会更复杂长这样MODALIASusb:v1D6Bp0102d0005dc00dsc00dp00ic08isc06ip02in00这个字符串把 USB 设备的 vendor ID、product ID、设备类、接口类等信息全部编码进去了。后面你会看到模块里的 MODULE_DEVICE_TABLE 会生成同样格式的别名两边对上自动加载才能成立。2.2 用户空间如何响应 uevent内核把 uevent 发出去之后用户空间必须有程序接收并处理。在现代 Linux 系统里这个角色由 systemd-udevd 承担也就是我们常说的 udev。udev 通过 netlink socket 监听内核事件一旦收到 ACTIONadd 事件它会读取 MODALIAS然后在规则库中匹配。匹配结果有两种有对应的 udev 规则就按规则执行指定动作没有显式规则udev 会调用内置的 kmod 工具把 MODALIAS 交给 modprobe。所以这里要明确一个认知自动加载的决策者是用户空间的 udev modprobe而不是内核本身。内核只负责描述我发现了什么设备真正去查模块、加载模块的是用户空间。我常在调试时用命令直接观察这一过程。在终端执行udevadm monitor --kernel --property然后在另一个终端让设备插入或注册就能看到完整的环境变量。比如注册一个名为 my_device 的 platform 设备能看到上面提到的那组 MODALIAS。这一步是后续排查自动加载问题的基础。2.3 modprobe 如何找到该加载的模块modprobe 和 insmod 最大的区别在于 modprobe 不是直接加载一个文件而是根据模块名或模块别名去查询映射表。这张映射表由 depmod 工具扫描内核模块目录后生成具体在/lib/modules/$(uname -r)/modules.alias /lib/modules/$(uname -r)/modules.depmodules.dep 记录模块依赖关系modules.alias 则记录了每个模块声明的别名与模块文件名的对应关系。例如一行记录可能是alias platform:my_device my_driverudev 把 MODALIAS 传给 modprobe 后modprobe 在这个文件里按别名查找找到 my_driver然后处理依赖并加载。所以整个过程可以概括为设备注册 - 内核发送 uevent (MODALIAS) - udev 捕获事件 - 调用 modprobe - 查询 modules.alias - 加载对应 .ko - 模块 init 函数执行 - 驱动注册/设备 probe这里有一个常见误区很多人以为把 .ko 放到 /lib/modules 下就能自动加载。实际还要执行depmod -a生成映射关系否则 modprobe 根本不知道这个模块对应哪个别名。3. 驱动端匹配总线、设备、驱动三者如何对上号3.1 bus_type 的 match 是所有匹配的总闸前面讲了模块被加载但模块被加载了并不代表驱动一定能工作。驱动代码通常通过platform_driver_register()注册到总线之后总线需要判断这个驱动能不能驱动那台设备。判断的工作在每种总线的match函数里完成。拿最常用的 platform 总线举例它的匹配逻辑大致是如果设备有 of_node 设备树节点优先用设备的 compatible 属性和驱动of_match_table里的 compatible 匹配如果没有设备树尝试用platform_driver的 id_table 匹配设备 name再用驱动的driver.name直接匹配设备 name。这个顺序对后面写代码影响很大。如果你给 platform_driver 同时写了of_match_table和id_table且设备树里 compatible 匹配不上驱动还是有机会通过 id_table 或 name 匹配上。我见过一个项目里驱动作者图省事只用driver.name直接匹配比如static struct platform_driver my_driver { .probe my_probe, .driver { .name my-device, }, };当时能用是因为设备树节点里没有 compatible设备 name 恰好也是 my-device。这种写法在 prototype 阶段没问题但可维护性很差。设备树节点一改名或者想在一个内核里支持多个同型号设备这种裸 name 匹配就撑不住了。3.2 MODULE_DEVICE_TABLE 到底做了什么模块要支持自动加载必须让 modprobe 知道这个模块能支持哪些设备。最简单的办法是写 MODULE_ALIAS但那样手动维护很麻烦。更规范的做法是用MODULE_DEVICE_TABLE宏。static const struct platform_device_id my_platform_ids[] { { my-device, 0 }, { }, }; MODULE_DEVICE_TABLE(platform, my_platform_ids);这个宏做的事情是把 id_table 里的 device id 放入内核模块的一个特殊 section__mod_device_table。编译时modpost 工具读取这段内容为每一项生成对应的 MODULE_ALIAS写进模块的 .mod.c 文件里。最终在模块的 modinfo 信息里能看到modinfo my_driver.ko alias: platform:my-device这就是自动加载的驱动器端身份证。不光是 platformUSB、PCI、I2C 等总线都有对应的 MODULE_DEVICE_TABLE 用法。比如 USBstatic const struct usb_device_id my_usb_ids[] { { USB_DEVICE(0x1234, 0x5678) }, { }, }; MODULE_DEVICE_TABLE(usb, my_usb_ids);编译后 modinfo 里会出现一长串类似usb:v...p...的 alias正好和设备插入时内核发送的 USB MODALIAS 格式对齐。3.3 只写 of_match_table 不能保证自动加载这里必须专门提醒一个容易踩的点。很多人写设备树驱动时只写static const struct of_device_id my_of_match[] { { .compatible vendor,my-device, }, { }, }; static struct platform_driver my_driver { .probe my_probe, .driver { .name my-device, .of_match_table my_of_match, }, };这种写法可以让驱动在已经加载的情况下正确匹配到设备树节点。但它只解决了匹配问题没有解决自动加载问题。要让设备树里的设备自动触发模块加载必须额外加一行MODULE_DEVICE_TABLE(of, my_of_match);或者手动声明 MODULE_ALIAS。modpost 会根据MODULE_DEVICE_TABLE(of, ...)生成类似of:N...T...的别名。我见过不少同事在这上面卡了一整天驱动明明手动加载就能 probe但一拔一插就不行。查来查去就是少了这一行宏。4. 设备树 compatible 与 of 匹配的联动4.1 嵌入式场景设备节点先于驱动存在在嵌入式 Linux 里设备树不仅描述硬件拓扑也承担着设备注册的职责。设备树里每一个带compatible属性的节点在内核启动过程中都可能被解析成一个 platform_device 注册到 platform 总线上。比如设备树里有这样一个节点my_device: my-device40000000 { compatible vendor,my-device; reg 0x40000000 0x1000; interrupts 17; };内核启动后在/sys/devices/platform/下能看到对应的设备同时内核会为它发送 uevent。由于该节点有 compatible通常 MODALIAS 会是这种格式MODALIASof:Nmy-deviceTNULLCvendor,my-device注意这里的N后跟的是设备树节点的 nameC后跟的是 compatible 字符串。这个格式很重要因为在 modules.alias 里你看到的 of 别名也长这样。4.2 当 MODALIAS 是 of 格式时模块那边怎么对应假设模块里写了MODULE_DEVICE_TABLE(of, my_of_match);depmod 之后grep my-device /lib/modules/$(uname -r)/modules.alias的结果类似alias of:Nmy-deviceT*Cvendor,my-device* my_driverudev 收到设备的 of 格式 MODALIAS 后把它交给 modprobemodprobe 在 modules.alias 里找到这一行加载 my_driver。这就完成了设备树设备 - 模块自动加载的完整闭环。这里有一个值得说透的技术点为什么of别名里有N和C两个字段。N对应节点 nameC对应 compatible。modpost 在生成 of 别名时会把这两部分都保留匹配的优先级会以 compatible 为主节点 name 作为辅助约束。所以设备树节点 name 改了问题不大只要 compatible 没变alias 中间用通配符也能匹配上。4.3 实际项目中该用哪种匹配策略我个人的建议是新板子的 Linux 驱动以设备树 compatible 匹配为主of_match_table里必须写MODULE_DEVICE_TABLE(of, ...)也必须写。如果驱动需要考虑兼容老内核或非设备树环境额外维护一份platform_device_id作为兜底。尽量不要只依赖driver.name做平台匹配。除非你有充分理由比如某些无设备树的内核模块。下表总结三种匹配方式在不同场景下的作用匹配方式运行时判断自动加载使用场景of_match_table设备树 compatible 匹配依赖 MODULE_DEVICE_TABLE(of)标准设备树平台id_table设备 name 匹配依靠 MODULE_DEVICE_TABLE(platform)非设备树/老平台driver.name设备 name 直接匹配需额外 MODULE_ALIAS 或 platform 别名尽量避免长期使用5. 写一个自动加载的完整驱动从代码到设备树再到验证5.1 驱动代码骨架下面给一个可以直接编译测试的示例功能不需要太多重点展示自动加载所需的全部要素。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/err.h static int my_auto_probe(struct platform_device *pdev) { struct device *dev pdev-dev; dev_info(dev, my_auto: probe success\n); return 0; } static void my_auto_remove(struct platform_device *pdev) { struct device *dev pdev-dev; dev_info(dev, my_auto: remove called\n); } static const struct of_device_id my_auto_of_match[] { { .compatible vendor,my-auto-device, }, { }, }; static const struct platform_device_id my_auto_platform_ids[] { { my-auto-device, 0 }, { }, }; static struct platform_driver my_auto_driver { .probe my_auto_probe, .remove my_auto_remove, .driver { .name my-auto-device, .of_match_table my_auto_of_match, }, .id_table my_auto_platform_ids, }; module_platform_driver(my_auto_driver); MODULE_DEVICE_TABLE(of, my_auto_of_match); MODULE_DEVICE_TABLE(platform, my_auto_platform_ids); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A demo driver for auto load test);这里要解释几个关键点module_platform_driver()是 platform_driver_register 和 platform_driver_unregister 的封装用起来最省事。MODULE_DEVICE_TABLE(of, ...)必须写在of_match_table定义之后两者配合才能生成 of 别名。我同时写了 id_table 和 of_match_table是刻意为之。如果设备树匹配失败platform 总线还能通过 id_table 兜底这对运行期调试有帮助。5.2 Makefile 与安装步骤驱动编译还是老套路假设你的内核源码树在 /lib/modules/$(uname -r)/build 可访问obj-m : my_auto.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean编译完成后把 .ko 放到内核模块目录并执行 depmod这是很多新手容易漏的一步sudo cp my_auto.ko /lib/modules/$(uname -r)/extra/ sudo depmod -a执行完 depmod立刻检查模块的 alias 信息modinfo my_auto.ko可以看到类似输出alias: of:Nmy-auto-deviceT*Cvendor,my-auto-device* alias: platform:my-auto-device再到 modules.alias 里确认一下grep my-auto /lib/modules/$(uname -r)/modules.alias如果这两处都没有对应 alias说明模块编译时 MODULE_DEVICE_TABLE 没生成成功自动加载一定不会生效。5.3 设备节点与触发验证驱动侧准备好后需要一台设备来触发自动加载。如果你有一块开发板直接改设备树增加一个自己的测试节点my_auto_test { compatible vendor,my-auto-device; status okay; };然后重新编译设备树并启动。启动后观察 dmesg如果看到类似下面的日志说明自动加载和匹配都成功了my_auto: probe success没有开发板时也可以先在普通 PC 上验证。你需要这几种手段配合手动向 sysfs 里注册一个 platform 设备。用以下命令模拟设备插入echo my-auto-device /sys/bus/platform/devices/auto_test/driver_override echo auto_test /sys/bus/platform/drivers_probe或者用 udevadm monitor 观察事件看内核是否发出了 MODALIASudevadm monitor --kernel --property用 udevadm trigger 重新触发已注册设备的 ueventudevadm trigger --typedevices --subsystem-matchplatform如果一切配置正确触发后模块会被自动加载lsmod能看到 my_auto/proc/modules里也有记录。5.4 开机常驻与事件触发的场景选择到这里还要区分一个容易被混淆的点自动加载分为设备事件触发加载和开机无条件加载两种场景。设备事件触发加载依赖 udev 捕获 uevent 后调用 modprobe。适合 USB、PCI、SDIO 这类可热插拔设备以及启动后期才注册的 platform 设备。开机无条件加载通过 systemd 的 modules-load.d 机制开机时按模块名强制加载。适合那些不依赖具体设备事件、只是必须常驻的模块或者一些没有设备树节点的早期驱动。例如# /etc/modules-load.d/my-auto.conf my_auto这样 systemd-modules-load 服务启动时就会自动执行modprobe my_auto。但要注意这种用法只是加载模块 不保证设备一定 probe。如果设备还没准备好模块加载后 probe 也会失败或者等待设备后续出现时才由总线匹配触发。实践中的选择依据很简单如果是通用总线设备优先靠 udev 事件自动加载如果是板级特定设备可以两条路都留但要以设备树匹配为主。6. 自动加载失败排查最常见的原因与定位方法论6.1 能手动 modprobe 却不会自动加载先查 MODALIAS不管问题多玄排查顺序都建议从内核发出的 MODALIAS 开始。命令是udevadm monitor --kernel --property然后触发设备注册观察有没有 MODALIAS 发出。如果根本没有 MODALIAS那可能是总线 uevent 生成逻辑的问题或者设备根本没有成功注册如果 MODALIAS 有就看它是of:...还是platform:...还是usb:...然后拿这个字符串去 modules.alias 里反向搜索。grep MODALIAS 中的核心字段 /lib/modules/$(uname -r)/modules.alias能搜到但模块没加载重点查 udev 和 modprobe 的配置搜不到问题基本定位在模块侧 alias 没生成。6.2 alias 没生成通常是什么原因最容易出现的几个原因按概率排序忘了写MODULE_DEVICE_TABLE特别是对设备树只有 of_match_table 没有 of 表的情况of_match_table里的 compatible 字符串与设备树节点里的 compatible 不一致大小写、标点、厂商前缀写错的我都见过模块编译时字符编码问题比如从网页复制代码时隐含了全角逗号depmod 之后没有重新生成映射alias 文件是旧的。排查手段很直接。modinfo my_auto.ko看 alias再和设备实际 MODALIAS 对比。平台设备匹配还曾遇到过一个现象id_table 里写的是 my-auto-device但设备树节点里的 compatible 不匹配导致运行期只能依赖 platform name 匹配。这种问题从 alias 上看不出来必须确认总线的 match 顺序。6.3 busybox 环境下 mdev 不会自动调 modprobe嵌入式产品里有个特别常见的坑文件系统用 busybox设备管理用 mdev然后发现设备插上后驱动不自动加载。原因很简单mdev 默认不做 MODALIAS 到 modprobe 的自动映射。它只能根据配置去创建 /dev 节点不会像 systemd-udevd 那样主动完成设备事件 - modprobe的转发。如果项目里只能用 mdev又想要自动加载常见做法有几种在 mdev.conf 里针对特定子系统写触发动作动作里调用 modprobe在系统的热插拔脚本里自己解析 MODALIAS 并调用 modprobe干脆用一个独立的轻量 udev 替代 mdev或者直接上 systemd。我在一个 MIPS 平台的小路由器上试过自己写热插拔脚本思路是#!/bin/sh if [ -n $MODALIAS ]; then /sbin/modprobe $MODALIAS fi监听内核发来的 uevent 环境变量解析出 MODALIAS然后就调用 modprobe。这种方式能跑但健壮性和 systemd-udevd 没法比只适合资源极度受限的场景。6.4 depmod、文件系统与 initramfs 的边界问题最后一个典型坑来自文件系统本身。自动加载依赖 modprobe 在 /lib/modules/$(uname -r)/ 目录下查找信息。如果这个目录里的模块不完整或者根文件系统是只读的depmod 生成的缓存不是最新都会导致自动加载失效。还有一个很容易被忽略的场景initramfs 只在启动早期提供模块但真正的根文件系统挂载后/lib/modules 里的内容和 initramfs 里的可能不一致。启动过程中驱动加载失败不一定能直观发现因为 initramfs 阶段的日志可能被 logrotate 清掉了。排查怀疑到 initramfs 时解包看模块清单是必要的lsinitramfs /boot/initrd.img-$(uname -r) | grep my_auto有朋友会遇到这样的怪问题qemu 里模拟自动加载正常放到真实板子上却不加载。最后查出来是在 initramfs 阶段内核还访问不到存储设备模块放在 rootfs 深处udev 处理事件时模块还没准备好。解决方案是在 initramfs 里提前放入目标模块或让 udev 服务等待文件系统挂载完成。6.5 排查思路小结现象优先怀疑第一步操作手动加载正常自动加载无反应udev / mdev 环境问题udevadm monitor 看 MODALIAS有 MODALIASmodprobe 找不到模块depmod 缓存或模块路径grep modules.alias模块加载了但 probe 没调用匹配表或设备树 compatible对比 of_match_table 与设备节点开机早期偶发不加载initramfs 或缺模块目录lsinitramfs 确认模块清单一些零碎但重要的经验最后补充几点代码层面之外、实操中很容易忽略的经验。第一模块里尽量把MODULE_ALIAS写得保守一点还是精确一点我的建议是platform 设备可以额外写一行人能读懂的别名比如MODULE_ALIAS(platform:my-auto-device);这在通过modprobe my-auto-device这种按名查找场景下非常有用。虽然 MODULE_DEVICE_TABLE 会生成 alias但那通常是给 uevent 的 MODALIAS 精确匹配用的手写一条platform:别名有时候能节省不少排查时间。第二验证自动加载时别急着把设备插入物理接口。先在 sysfs 层面用echo设备名、或 udevadm trigger 去伪造设备事件把验证过程从硬件上剥离开能更快定位是设备侧问题还是驱动侧问题。第三driver_remove回调写成 void 还是 int 的坑不同内核版本确实不一样老内核里platform_driver.remove返回 int新内核改成 void。我提供的示例是按新内核接口写的如果你在旧内核上编译报错把 remove 回调签名的 int 返回值去掉或加上即可。我到现在写项目驱动仍会默认坚持一条原则驱动不仅能 insmod 工作还要验证拔插自动加载。这两步一旦都通过才算真正交付了一个可用的驱动。自动加载不是高深魔法它就是内核设备模型、用户空间工具链和模块自身信息三者的协同把这条链路在自己代码里过一遍以后遇到不加载的问题排查思路就会清晰很多。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻