资讯动态

Linux驱动开机自动加载全解析:从modprobe到设备树与udev

发布时间:2026/9/20 16:09:03 来源:尧图企业网站定制
1. 从“手动 insmod”到“开机自启”驱动自动加载到底解决了什么问题做过 Linux 驱动开发的人基本都经历过这样的阶段写了一个简单的字符设备驱动insmod之后dmesg看到register_chrdev成功mknod创建设备节点然后跑 userspace 程序验证 read/write一切正常。这时候你会觉得驱动开发也不过如此。但一旦你把模块拷到目标板子上断电重启准备正式跑功能的时候问题来了——驱动没了。每次开机都要手动insmod、mknod如果是调试阶段还能忍可一旦进入量产或者交给现场工程师部署这种手动流程就完全不可接受了。你不可能要求运维人员在设备每次重启后都去敲那几条命令更不可能让他们去记你的驱动依赖了哪个设备树节点、主设备号是多少、需要创建设备节点还是靠 udev 自动创建。驱动自动加载的本质是把告诉内核加载哪个模块这件事从人为操作变成系统机制。这里要先分清楚两件经常被混为一谈的事模块的自动加载和模块的自动挂载。前者是内核或内核配套机制在系统启动或设备出现时自动将模块插入内核后者是模块加载成功后文件系统层把设备节点或挂载点准备好。本文重点讲前者但两者在实际部署中经常要一起配合否则会出现“模块加载了但设备节点不存在”的尴尬局面。2. 自动加载的底层支撑modules.dep、modprobe 与内核模块机制的配合要谈自动加载绕不开三个核心角色modprobe、modules.dep和内核的模块符号解析机制。2.1 modprobe 不是 insmod 的简单替代很多初学者会有一个误解觉得modprobe无非就是insmod加了一个自动解析依赖的功能所以实际使用中甚至有人图省事直接用insmod加载所有模块。这种理解在简单场景下问题不大但在真实项目中会坑到你。insmod是一个纯粹的系统调用封装它把指定的.ko文件加载进内核不做任何依赖解析。如果你的驱动 A 依赖驱动 B 导出的符号而 B 还没加载insmod就会直接报Unknown symbol错误。modprobe则完全不同它会读取/lib/modules/$(uname -r)/modules.dep这个依赖关系文件自动先把依赖的模块全部加载好再加载目标模块。这个区别在自动加载场景中是致命的自动加载通常由 udev 或内核的 request_module 机制触发触发时内核只会给你一个模块名不会给你完整路径更不会帮你处理依赖顺序。所以任何依赖 insmod 的自动加载方案都是脆弱的正确做法是让modprobe全权接管。2.2 modules.dep 是怎么生成的modules.dep文件不是手写的而是由depmod命令扫描/lib/modules/$(uname -r)/目录下所有.ko文件后生成的。它会解析每个模块的__module_parm段、.modinfo段以及未解析符号表最终形成模块之间的依赖关系树。举个实际例子。你写了一个平台驱动my_platform_driver.ko它调用了regmap_init_i2c这个符号来自regmap-i2c模块。depmod会扫描到my_platform_driver.ko中有一个未解析的符号regmap_init_i2c然后在其他.ko中找到导出该符号的模块最后在modules.dep中生成一行/kernel/drivers/misc/my_platform_driver.ko: /kernel/drivers/base/regmap/regmap-i2c.ko看到这里你就明白了modprobe my_platform_driver在执行时会先解析这一行把冒号后面的依赖模块全部加载最后才加载目标模块。注意depmod必须在内核编译完成、模块安装到/lib/modules/$(uname -r)/之后运行。很多新手在开发机上交叉编译完模块直接拷贝到板子上的/lib/modules/目录却忘了跑depmod结果modprobe一直报module not found排查半天发现是依赖文件没生成。2.3 内核的 request_module 机制自动加载还有一个隐藏角色就是内核的request_module()函数。当内核在运行时发现需要一个模块但模块未加载时会调用这个函数通过用户空间的modprobe来请求加载。最典型的就是文件系统驱动。比如你插入一个 U 盘VFS 层识别到分区表类型是 vfat但内核里没有 vfat 模块此时内核会调用request_module(vfat)用户空间的modprobe收到请求后去加载 vfat.ko。这就是一种完全由内核触发的自动加载不需要任何用户态配置前提是内核配置了CONFIG_MODULE和CONFIG_MODVERSIONS同时/proc/sys/kernel/modprobe指向了正确的modprobe路径。对驱动开发者来说这个机制也有用。比如你在驱动里调用了crypto_alloc_shash申请一个哈希算法但这个算法是以模块形式编译的内核会自动通过request_module去加载对应模块。如果你希望自己的驱动在加载时能触发某个管理模块的加载也可以通过调用request_module或依赖其导出符号来实现。3. 模块加载的两种自动触发路径静态配置法和运行时热插拔法自动加载的实现路径从触发时机上可以分成两大类。理解清楚这两类的区别你才能在具体场景中做对选择。3.1 静态配置法开机时按固定顺序加载静态配置法的核心思路是在系统初始化阶段根据预设的模块列表和顺序将模块加载进内核。这种方法不关心硬件什么时候出现它假设驱动对应的硬件在系统启动时就已经就绪或者说即使硬件没就绪模块先加载也没关系后续通过 driver match 机制去匹配设备。具体实现方式有三种第一种是在/etc/modules文件中逐行列出模块名。Debian/Ubuntu 系的发行版在初始化时会读取这个文件并加载其中的模块。这个方案最简单适合模块之间没有复杂依赖或者依赖已经通过modules.dep解析好的场景。第二种是在/etc/modules-load.d/目录下创建.conf文件。这是 systemd 系发行版的标准做法。systemd-modules-load.service在系统启动早期会扫描/etc/modules-load.d/、/run/modules-load.d/、/usr/lib/modules-load.d/三个目录加载其中列出的模块。每个.conf文件一行一个模块名注释以#开头。第三种是利用内核启动参数modules...或者设备树中配置的模块加载策略。这种方法比较特殊一般用于嵌入式平台的特殊启动流程比如 bootloader 直接给内核传递模块名列表。对于这三种方法我得说一句静态配置法适合那些“必须最早加载、不依赖硬件枚举结果”的驱动比如 GPIO 控制器驱动、中断控制器驱动、基础总线驱动。如果你的驱动是 PCIe 或 USB 设备驱动静态配置法虽然也能用但并不优雅因为你可能会在设备还没出现时加载驱动然后驱动 probe 失败等到设备真正接入时反而不会再次触发 probe。3.2 热插拔触发法设备出现时才加载热插拔触发法更符合现代硬件的实际情况。所谓热插拔不一定是指物理上能带电插拔而是指设备可能在系统启动后的任意时刻出现或消失。实际上大多数总线设备在启动时也是通过 hotplug 机制完成枚举的只是这一过程发生得非常早让人感觉不到热插拔的存在。核心机制是 udev 和内核的 uevent 通知。当总线驱动程序检测到新设备时内核会生成一个 uevent并通过 netlink socket 发送给用户空间的 udev 守护进程。udev 根据预设的规则决定如何处理这个事件其中就包括调用modprobe加载对应的驱动模块。这里有一个关键点多数情况下你不需要自己写 udev 规则来加载模块因为内核的 modalias 机制已经帮你做了这件事。每个设备在 uevent 中会携带一个 modalias 字符串它描述了设备的总线类型、厂商 ID、设备 ID、类别等信息。举个例子一个 USB 设备可能生成这样的 modaliasusb:v1234p5678d0100dc00dsc00dp00ic08isc06ip50udev 收到这个字符串后会去/lib/modules/$(uname -r)/modules.alias文件中查找匹配项找到对应的模块名然后调用modprobe加载。这个modules.alias文件同样是depmod生成的它把模块中声明支持的设备 ID 和 modalias 模式关联起来。所以如果你的驱动是通过MODULE_DEVICE_TABLE宏声明了设备 ID 列表并且用MODULE_ALIAS生成了对应的别名那么自动加载大多数情况下是“自动”的你要做的只是把模块放到正确位置并跑一次depmod。4. 实操记录在 imx6ull 板卡上实现自研 GPIO 按键驱动的开机自动加载理论讲多了容易飘还是来一个实际项目记录。我之前在一款基于 imx6ull 的板卡上调试过一个 GPIO 按键驱动要求系统开机后驱动自动加载按键事件通过 input 子系统上报。整个过程踩了好几个坑正好把完整链路拆给你们看。4.1 驱动的声明部分是怎么写的为了让自动加载机制能够识别驱动创建模块时必须在驱动代码里做好两件关键声明第一是模块作者和描述信息这个比较简单MODULE_LICENSE(GPL); MODULE_AUTHOR(yourname); MODULE_DESCRIPTION(GPIO Key Driver for imx6ull); MODULE_VERSION(1.0);第二是设备 ID 表这个是自动加载的“索引”。对于 GPIO 按键这种非热插拔设备我没有用 platform driver 设备树匹配因为设备树匹配依赖内核把设备节点和驱动进行绑定。实际使用中我直接把它写成了一个 platform driver并借助设备树中的compatible属性实现自动 probe。static const struct of_device_id gpio_key_of_match[] { { .compatible mycompany,gpio-key }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, gpio_key_of_match); static struct platform_driver gpio_key_driver { .probe gpio_key_probe, .remove gpio_key_remove, .driver { .name gpio_key, .of_match_table gpio_key_of_match, }, }; module_platform_driver(gpio_key_driver);MODULE_DEVICE_TABLE(of, ...)这行非常关键。它告诉depmod这个驱动支持 compatible 属性为mycompany,gpio-key的设备。depmod会根据这个声明生成modules.alias中的of:NmycompanyTNULLCmycompany,gpio-key条目。当内核启动时解析设备树发现有节点 compatible 匹配这个字符串就会通过request_module触发模块自动加载。不用任何/etc/modules配置也不用写 udev 规则驱动会在设备树解析阶段被自动拉起来。4.2 编译安装与 depmod 的处理交叉编译好之后模块文件是gpio_key.ko。把模块拷贝到板子上位置放在/lib/modules/$(uname -r)/extra/目录下这是因为标准内核模块都放在这个路径下的kernel/子目录中自定义模块统一放extra/便于管理。然后执行depmod -a这会在/lib/modules/$(uname -r)/下重新生成modules.dep、modules.alias、modules.symbols等文件。随后你可以用modprobe gpio_key验证或者直接重启系统看是否自动加载。这里有一个细节值得单独说如果你在开发机上交叉编译模块一定要用与目标板相同的内核版本头文件进行编译并且depmod必须在目标板文件系统上执行。如果你在 x86 开发机上直接跑depmod -a它不会去分析 ARM 的.ko文件或者分析出来的路径完全是 x86 模块路径拷贝到板子上后 modprobe 会一脸懵。4.3 设备树节点的匹配过程设备树里我加了一个节点gpio-key { compatible mycompany,gpio-key; pinctrl-names default; pinctrl-0 pinctrl_gpio_key; gpios gpio5 1 GPIO_ACTIVE_LOW; interrupt-parent gpio5; interrupts 1 IRQ_TYPE_EDGE_BOTH; status okay; };内核在启动过程中会遍历设备树对每个compatible属性为mycompany,gpio-key的节点向 platform bus 注册一个 platform device。注册的时候bus 会尝试匹配已经加载的 platform driver如果找不到匹配的驱动而内核又开启了CONFIG_MODULES就会根据modules.alias中的 of 匹配规则触发modprobe。如果你的模块没有用MODULE_DEVICE_TABLE(of, ...)声明那么即使设备树里写了完全一致的compatible内核也无法通过 modalias 机制找到你的模块名。这种情况下你只能在/etc/modules里静态加载模块虽然驱动也能工作但失去了“按需加载”的优雅性而且如果设备树节点不存在驱动也会做一次无意义的 probe。4.4 排查自动加载失败的一次完整链路第一次做的时候我遇到了自动加载不生效的情况。重启后lsmod看不到gpio_keydmesg里也没有驱动注册的日志。我不是直接上手改配置而是按链路一步步排查。先确认模块本身能不能手动加载modprobe gpio_key结果报错modprobe: module gpio_key not found in modules.dep。这说明depmod没有识别到这个模块。检查后才发现我把.ko拷贝到了/lib/modules/5.4.47/目录下但板子实际运行的内核版本查出来是5.4.47-g23f5d53。内核版本带了后缀而 /lib/modules 下并没有5.4.47-g23f5d53这个目录。处理方法是把模块放到正确的内核版本目录下KVER$(uname -r) cp gpio_key.ko /lib/modules/${KVER}/extra/ depmod -a再次modprobe gpio_key成功。接下来重启测试发现驱动还是没有自动加载。这次dmesg能看到设备树节点被注册为 platform device但没有任何 probe 日志。这说明问题出在 modalias 匹配上。检查modules.aliasgrep gpio_key /lib/modules/$(uname -r)/modules.alias输出为空。再检查是否是因为编译模块时没有安装到开发环境的/lib/modules/$(uname -r)/目录下导致 module 符号表没有生成实际上depmod只在拷贝模块之后的那个文件系统上把of_device_id表解析出来。我这边的板子文件系统是小概率的只读 rootfs第一次拷贝后没成功写入depmod扫到的不是新模块。确认写入成功后重新跑depmod -a grep gpio_key /lib/modules/$(uname -r)/modules.alias这次看到了of:NmycompanyTNULLCmycompany,gpio-key。重启后lsmod和dmesg都正常了。5. modules.alias 背后的匹配规则为什么有些驱动自动加载成功有些失败不少人在这一步会卡住自己的驱动已经MODULE_DEVICE_TABLE了depmod也跑了modules.alias里也有条目了但自动加载还是失败。这时候你需要搞清楚modules.alias里的 modalias 模式是怎么和设备的 modalias 做匹配的。5.1 alias 格式与设备 modalias 字段模块别名格式是由内核的file2alias.c工具在编译时生成的不同总线有不同的格式。PCI 设备的 alias 格式是pci:v00008086d00008C46sv0000...USB 设备是usb:v...d...dc...platform 设备走的是 of 匹配则用of:N...T...C...格式。以 of 匹配为例alias 中N后面是节点名T后面是device_typeC后面是 compatible 字符串。内核在生成平台设备 modalias 时会构造一个类似的字符串然后与模块 alias 做精确或通配符匹配。X86 平台上大家可能更熟悉的是 PCI 设备的动态加载。比如一颗 Intel 网卡lspci 看到 vendor0x8086 device0x10D3那么内核会生成pci:v00008086d000010D3...的 modalias然后匹配 modules.alias 里对应模块。对应模块的 alias 格式由网卡驱动中的MODULE_DEVICE_TABLE(pci, igb_pci_tbl)来生成。如果你的驱动是多个设备的驱动alias 会生成多条这没问题depmod会如实反映。真正容易出问题的是有些厂商 SDK 里的驱动代码MODULE_DEVICE_TABLE声明的是of表但驱动自身的 probe 逻辑写的是 ACPI 匹配或 i2c 设备匹配导致 alias 与设备实际 modalias 类型对不上。比如一个 I2C 触摸屏驱动正确做法是用MODULE_DEVICE_TABLE(i2c, ...)如果你误写成of在 ACPI 平台上自动加载就永远不会触发。5.2 通配符与精确匹配的优先级modules.alias文件里有时候能看到*通配符这些通配符来自驱动中声明的较宽泛的设备 ID 匹配。举例来说aliasusb:v*p*d*dc*dsc*dp*ic08isc06ip50*这表示一个 USB 存储类的驱动匹配所有符合 mass storage 类别的设备不限制具体的厂商 ID 和设备 ID。当内核为一个 U 盘生成精确 modalias 时这个通配符条目能够匹配。但如果系统里同时存在一个精确到 vendor 和 device 的存储驱动内核会优先选择精确匹配的那个。这个优先级是系统级的不取决于模块名而是取决于 alias 模式字符串的匹配长度。实际项目里最让人困惑的现象是一个设备明明有两个驱动都能匹配但自动加载只加载了其中一个另一个要靠手动insmod才工作。这往往是modules.alias中两者都匹配但 udev 的规则只触发了其中一个驱动名。这种时候不要怀疑系统坏了去udevadm monitor抓 uevent再配合udevadm info查看设备属性基本几轮下来就能定位原因。5.3 为什么有些模块放到了/lib/modules仍然不识别还有一种很常见的现象模块放在/lib/modules/$(uname -r)/下但modprobe说找不到。除了前面提到过的内核版本目录不匹配还有两个原因值得排查。第一个是模块文件权限或符号链接问题。.ko文件权限太严格比如 600 权限depmod可能直接跳过它导致生成的依赖文件里没有记录。Yocto 工程打包 rootfs 时特别容易出现这种问题。解决方法很简单确保模块文件至少是 644 权限且所在目录可读可执行。第二个是模块的 vermagic 与当前内核版本不匹配。如果你在一个内核版本上编译模块却尝试在高版本内核上加载insmod可能提示Invalid module formatmodprobe则会跳过模块并输出错误。检查方式modinfo gpio_key.ko查看vermagic字段和uname -r输出的字符串做对比。如果开发板内核开启CONFIG_MODVERSIONS还需要保证模块的 CRC 符号版本一致否则会出现Unknown symbol或version magic报错。这种问题常见于手动编译驱动而开发板内核是厂商提供的 prebuilt 内核。6. 嵌入式 Yocto/Buildroot 系统中驱动自动加载的工程化配置做项目最后免不了要量产驱动自动加载不能只在开发板手动环境里验证还得落到实际镜像中。如果是用 Yocto 或 Buildroot 构建系统驱动模块的打包方式直接决定了自动加载是否能生效。6.1 内核模块的安装位置和打包方式在 Yocto 中你通常会把自研驱动作为独立内核模块 recipe 加入镜像安装路径默认是/lib/modules/${KERNEL_VERSION}/extra/kernel-modules 包会包含所有内核模块但如果你只打包自己需要的模块可以用KERNEL_MODULE_AUTOLOAD变量把模块加入自动加载列表。在 Yocto 的 recipe 中这样写KERNEL_MODULE_AUTOLOAD gpio_key这个变量的作用是生成一个/etc/modules-load.d/gpio_key.conf文件内容就是模块名。它可以保证 systemd 启动早期加载模块适用于那些不能依赖设备热插拔、必须在 userspace 起来之前就位的内核模块。Buildroot 则是在 kernel 配置段选择BR2_LINUX_KERNEL_CUSTOM_TARBALL后通过BR2_LINUX_KERNEL_MODULES和 target finalize 脚本实现模块安装。Buildroot 内建了对/etc/modules的支持你可以把模块名写进/etc/modules文件。6.2 设备树随 rootfs 一起部署时要注意的配套问题只把模块放进去还不够如果设备树节点没同步更新驱动加载了也不会 probe。这里有一个常见的坑你改了设备树 dts编译出新 dtb放在/boot分区但没更新 rootfs 里modules.alias对应的设备信息。内核启动时设备树用的还是旧 dtb于是新 compatible 不匹配模块自然不加载。所以完整流程应该是更新 dts - 编译 dtb - 将 dtb 替换到 boot 分区 - 同步将模块拷贝到 rootfs - 跑 depmod - 检查 modules.alias。这四个环节少任何一个自动加载都可能静默失败。我在一个项目里就是因为只换了 dtb 没更新 rootfs设备节点连注册都没有折腾了一个下午才反应过来。另外如果你的系统是 initramfs 引导的模块可能需要在 initramfs 里提前加载而不是等 rootfs 挂载完之后再加载。这时要用到initramfs-tools的 hook 配置或者 Yocto 的INITRAMFS_IMAGE机制把需要的模块提前打进 initramfs。特别是那些负责挂载 rootfs 的存储控制器驱动、文件系统驱动必须进 initramfs否则在根文件系统还没有挂载前系统根本没有能力从 rootfs 读取模块。6.3 模块签名与 Secure Boot 对自动加载的影响现代嵌入式平台和部分服务器平台开启了 Secure Boot 或模块签名验证。如果内核配置了CONFIG_MODULE_SIG_FORCE所有未签名模块都不能加载自动加载自然也就失效。这种情况的特征是手动insmod时报Required key not availabledmesg里有模块签名校验失败的信息。解决方法是使用内核构建时生成的签名证书对模块签名/usr/src/linux-headers-$(uname -r)/scripts/sign-file sha256 \ signing_key.priv signing_key.x509 gpio_key.ko这里要注意sign-file脚本需要内核源码目录中scripts目录提供运行环境最好是内核版本对应的构建环境。如果你既不打算启用签名验证又确实是桌面系统的开发环境建议在编译内核时保持CONFIG_MODULE_SIG_FORCE为关闭状态。7. 关于实际调试中常见问题的回溯与避坑清单为了让大家调试时少走弯路我把前面分散在各个环节的常见问题集中整理成一张表方便对照排查。症状直接原因快速确认方法修复方案modprobe 找不到模块模块目录与 uname -r 不一致uname -r与ls /lib/modules/拷贝到正确目录并重新 depmodmodules.alias 无条目模块没有 MODULE_DEVICE_TABLEmodinfo查看 alias 字段补全设备 ID 表重新编译模块设备树有节点但驱动不 probe驱动与设备不匹配检查 of_match_table compatible对比 dts 与驱动中的 compatible 字符串手动加载成功但开机不自启没有配置自动加载触发源systemctl status systemd-modules-load添加 modules-load.d 配置或 udev 规则开机启动时依赖模块未加载依赖顺序不对modprobe --show-depends指定显式依赖或把依赖模块放进 modules-load.d 前部模块版本魔术字不匹配内核暴版本有变更modinfo查看 vermagic用目标内核源码重新编译模块Secure Boot 拒绝加载未签名或证书不受信任dmesg搜索 module_sig签名或关闭强制验证这张表不能覆盖所有情况但覆盖了我在实际项目中高频踩到的九成问题。另外有两个习惯强烈建议养成。第一个习惯是每次改完模块或设备树后都要同时确认modules.dep、modules.alias、modules.symbols三个文件是否更新。单独depmod -a会重新生成全部但如果你只是拷贝了新模块跑depmod -a之后一定要顺手modinfo验证。第二个习惯是在驱动代码的init函数里加上明确的日志输出包括模块名、版本号、设备树 compatible 等关键信息。这样自动加载成功后你通过dmesg能一眼看出驱动是哪个时机加载的是从设备树触发的还是通过 modules-load.d 触发的。如果日志里出现两行相同 init说明模块可能因为 alias 匹配和静态加载列表被加载了两次这种重复加载虽然不致命但容易让内存资源或中断注册出现隐藏问题。最后再分享一个小技巧。调试自动加载时可以用下面的命令实时捕捉系统解析 uevent 和调用 modprobe 的过程udevadm monitor --property --kernel同时在另一个终端执行触发动作比如重新扫描总线echo 1 /sys/bus/platform/devices/gpio_key/uevent如果你配置正确应该能看到 udev 收到 uevent然后出现 modprobe 调用日志。如果 uevent 都收了但 modprobe 没有执行说明 modules.alias 匹配失败如果 modprobe 执行了但模块加载失败则问题在模块本身或依赖上。这条链路捋顺了驱动自动加载就算真正掌握在手里了。

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

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

免费获取报价