ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux驱动开机自动加载全解析:modprobe、设备树与initramfs实战

Linux驱动开机自动加载全解析:modprobe、设备树与initramfs实战 做完Linux驱动开发的朋友应该都经历过这个场景调试阶段手动insmod解决一切感觉万事大吉结果产品准备量产了才发现驱动必须在系统上电、根文件系统挂载之后自动加载板子一开机设备就得能直接工作。这时候再回头看insmod只是最简单的触发按钮要让驱动在正确的时间、按正确的顺序、被正确的机制加载起来背后那条链路比刚开始想象的要长不少。这篇文章是“Linux驱动基础”系列的第二篇专门把自动加载这条链路拆开揉碎讲一遍驱动模块的两种形态怎么选、modprobe背后的依赖解析机制、设备树和udev到底在中间扮演什么角色、initramfs又是怎么回事以及完整跑一遍“开机即加载”的实战流程。适合已经能写出hello world级别字符设备驱动、但没怎么研究过模块加载机制的朋友内容偏嵌入式Linux也覆盖PC发行版场景。1. 驱动加载的两种形态编进内核还是做成模块1.1 built-in方式的基本逻辑驱动代码有两种归宿编译进内核镜像built-in或者编译成独立的.ko模块loadable module。这两种形态在开发阶段没有太大差别反正都能跑但一旦涉及到“自动加载”它们的启动时机完全不是一个量级。编译进内核的驱动加载时机由内核初始化流程里的initcall机制接管。Linux内核在启动时会按照预先定义的初始化级别依次调用各个驱动的init函数比如pure_initcall、core_initcall、postcore_initcall、arch_initcall、subsys_initcall、fs_initcall、device_initcall等。模块化的驱动一般走的是module_init映射到device_initcall这个级别而编译进内核的驱动可以在更早的core级别就完成初始化。这意味着什么如果某个驱动是系统启动阶段就必须存在的比如存储控制器、根文件系统所在设备的驱动、串口console驱动那就必须编进内核或者放进initramfs里。这属于启动早期依赖靠内核自己解决的。反过来如果驱动面对的是启动后期才枚举到的设备比如USB外设、SD卡、普通I2C传感器做成模块就完全没问题。实际项目里最常见的坑是有人把文件系统驱动的模块放在根文件系统里结果系统启动时内核发现根设备不可用直接kernel panic因为根文件系统都挂不上模块根本无从加载。这种问题一旦出现新手往往一脸懵。1.2 模块化形态的“身份证”与生命周期编译成模块的驱动产出一个.ko文件。这个文件不是普通的二进制它内部包含了一个名为.modinfo的section里面记录了模块名称、描述、作者、许可证、依赖关系、设备ID匹配表等信息。内核和用户态工具modinfo、depmod全靠这个section来识别模块。打个比方.modinfo就是模块的身份证和简历。驱动加载前modprobe会先读这份简历判断这个模块依赖哪些其他模块、需要哪些符号、能匹配哪些设备。模块的生命周期也比built-in驱动灵活很多加载时执行module_init指定的init函数卸载时执行module_exit指定的exit函数。加载后模块会在/sys/module/目录下出现一个同名目录里面可以暴露参数parameters子目录和状态信息。这种灵活性是built-in驱动不具备的也正因为这种灵活性自动加载机制才有了发挥空间。1.3 Kconfig与Makefile如何决定最终形态决定驱动最终是y还是m靠的是Kconfig和Makefile的配合。Kconfig里定义一个配置项比如config MY_DEVICE_DRIVER tristate My device driver support depends on OF default mtristate表示三态n不编译、y编进内核、m编成模块。Makefile里对应obj-$(CONFIG_MY_DEVICE_DRIVER) my_driver.o配置为y时obj-y会让驱动编进内核配置为m时obj-m会让驱动编成.ko文件。这里有一个容易忽略的点编进内核的驱动如果还想同时存在模块版本是不行的一个源文件只能二选一。但实际开发中经常遇到“先编成模块调试最后量产时编进内核”的情况所以代码里尽量别依赖只有模块才有的特性比如module_param虽然built-in也能用但某些模块独有的宏在built-in模式下行为会发生变化。2. modprobe自动加载的核心机制2.1 insmod和modprobe一字之差天壤之别很多教程上来就教insmod加载驱动这个命令确实简单给它一个路径它就能把模块加载进内核。但insmod有一个致命弱点它不解析依赖关系。如果你的驱动依赖其他模块比如依赖一个通用的regmap框架模块insmod不会自动帮你先把依赖的模块加载上加载失败就报错还得自己手动去把依赖链上的模块一个一个按顺序insmod。modprobe则完全不同。它工作的核心逻辑有两块依赖解析和别名匹配。依赖解析靠的是depmod生成的modules.dep文件别名匹配靠的是modules.alias文件。modprobe在加载模块前会先检查这个模块依赖谁然后递归地先把依赖模块全部加载好最后再加载目标模块。实际使用中永远优先用modprobe哪怕只是加载一个看起来没有依赖的模块。因为你可能不知道这个模块在某个内核配置下悄悄依赖了别的模块modprobe会帮你处理insmod不会。2.2 depmod自动加载的幕后索引工depmod这个命令很多人只知道“编译完跑一下”但它是整个自动加载机制的地基。depmod会扫描/lib/modules/$(uname -r)/目录下的所有.ko文件分析每个模块的.modinfo信息然后生成几个关键文件modules.dep模块依赖关系列表记录每个.ko依赖哪些其他.komodules.alias模块别名列表记录每个模块能匹配哪些设备modules.symbols模块提供的符号表记录每个导出符号由哪个模块提供modules.builtin内建模块的对应关系用于处理模块和内建设备的匹配这几个文件就是modprobe工作的依据。系统安装内核或者安装新模块后必须重新运行depmod更新索引否则modprobe根本不知道新模块的存在。我见过太多“驱动模块拷贝进/lib/modules/了modprobe却提示找不到模块”的案例原因就是忘了跑depmod。这个操作一句话就能解决但一旦漏了排查起来确实费时间。2.3 内核主动请求模块request_module与alias匹配自动加载最神奇的部分在于很多时候根本不需要用户手动敲modprobe内核发现新设备后会自己发起模块加载请求。这个过程的入口在内核的kernel/kmod.c里核心函数是request_module()。当总线比如Platform Bus、PCI Bus、USB Bus枚举到一个新设备时驱动程序核心会根据设备的标识符生成一个“看起来像模块名”的字符串然后调用request_module去尝试加载对应模块。就拿PCI设备举例驱动里会写一个MODULE_DEVICE_TABLE(pci, xxx_ids)这样的设备ID表编译后depmod从.modinfo中提取这些ID生成类似这样的aliasalias pci:v00008086d000015B4sv000017AAsd0000225Ebc*sc*i*系统里插入PCI设备后内核读取设备的vendor ID、device ID等字段生成对应的modprobe请求modprobe去modules.alias里查询匹配项找到对应的.ko文件然后加载。整个过程用户完全没有参与感插上设备驱动自动生效这就是Linux“即插即用”的基础。在嵌入式场景里这个机制对应的是设备树和Platform Bus。设备树节点指定compatible字符串驱动里通过MODULE_DEVICE_TABLE(of, xxx_of_match)声明自己能匹配哪些compatible系统启动时设备树节点被解析成platform_device然后总线匹配机制会触发模块加载。2.4 /etc/modprobe.d干预与定制加载行为虽然自动加载已经足够智能但实际场景中总有需要干预的地方。比如某个模块和硬件冲突想让系统别自动加载它或者某个模块加载时需要传参数希望开机时就带上指定参数。这些配置统一放在/etc/modprobe.d/目录下发行版和嵌入式系统可能还默认读取/lib/modprobe.d/。常见的配置项# 黑名单禁止自动加载某个模块 blacklist my_driver # 加载模块时附带参数 options my_driver debug1 # 重新定义模块名 alias my_driver_alias my_driver # 加载/卸载某个模块时执行自定义脚本 install my_driver /sbin/modprobe --ignore-install my_driver /usr/bin/my_setup.sh这里重点提醒一下blacklist的局限性。blacklist只对modprobe的自动加载生效如果模块已经被编进内核或者被其他模块显式依赖黑名单拦不住。另外如果使用了install指令说明对模块加载流程做了深度定制排查问题时一定要检查这些配置否则会非常困惑。3. 设备树、udev和initramfs自动加载中的三个关键角色3.1 设备树compatible匹配如何触发模块加载现代嵌入式Linux设备树几乎是标配。每个设备节点通过compatible属性声明自己是什么设备比如my_dev: my-device0 { compatible myvendor,mydevice; reg 0x0 0x100; };驱动侧则声明自己支持哪些compatiblestatic const struct of_device_id my_of_match[] { { .compatible myvendor,mydevice }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver { .probe my_probe, .driver { .name my_device, .of_match_table my_of_match, }, }; module_platform_driver(my_driver);系统启动时设备树被解析成platform_device总线上的driver_override和of_match_table匹配机制开始工作。如果驱动还没加载内核会尝试通过request_module自动加载如果驱动已经是built-in或者已经加载匹配成功后直接调用probe函数。这里有个非常容易踩的坑compatible的厂商前缀必须和设备树里完全一致大小写、下划线、逗号都不能错。很多人调试时发现驱动没被加载调了半天最终发现只是compatible字符串多了一个空格或者大小写不一致非常浪费时间的教训。3.2 MODULE_DEVICE_TABLE的幕后工作MODULE_DEVICE_TABLE这个宏值得单独讲一讲。它做的事情本质上是把一张设备ID表嵌入到模块的.modinfo section里并载明这是一张什么类型的表。以of设备表为例编译后的模块里会有一段类似这样的记录aliasof:Nmy-deviceT(NULL)Cmyvendor,mydevicedepmod扫描到这条alias后把它写进modules.alias。后续内核在总线匹配过程中如果发现设备树节点的compatible和这个alias模式匹配就会发起modprobe请求modprobe通过modules.alias反查找到对应的.ko文件并加载。类似的还有USB、PCI、I2C、SPI等总线的设备表。写法都是同一个套路定义一个ID表结构体数组用MODULE_DEVICE_TABLE宏声明类型最后把表指针赋值给驱动结构体。调试自动加载问题时第一步就是确认模块里是否生成了预期的alias。用modinfo命令查看modinfo my_driver.ko如果alias条目缺失自动加载大概率不工作因为内核不知道这个模块能匹配哪个设备。3.3 udev用户态的设备管家这里还需要理解一个角色。设备树把设备交给内核内核把设备枚举出来但用户空间怎么知道设备出现了设备节点在哪里创建权限怎么设置这些事一个叫udev的守护进程负责。udev通过netlink socket监听内核发出的uevent事件。当设备被添加、移除、改变状态时内核会发送ueventudev根据/etc/udev/rules.d/里的规则来决定怎么处理。自动加载驱动这件事上udev能做的其实不多因为设备驱动匹配这块主要是内核和modprobe完成的。但udev有几种典型的辅助场景设备节点创建后设置自定义权限和属组让普通用户也能访问设备出现后触发额外的用户态动作比如同步某种配置通过RUN指令调用脚本比如加载固件、设置硬件参数特定设备需要特殊驱动时可以用udev规则来绕过内核自动匹配强制加载指定模块对于嵌入式开发者来说尤其需要注意热插拔和冷插拔的差异。系统启动时设备已经接好的情况叫冷插拔内核会先枚举设备、生成uevent此时udev可能还没起来所以冷插拔设备的用户态处理会延后到udev启动后统一补齐而热插拔是设备在系统运行中插入udev能及时响应。驱动自动加载在这两种场景下都可能触发但时序不同排查时要注意这个先后关系。3.4 initramfs启动早期的“临时根文件系统”前面提到根文件系统不可用时会panic但很多场景又不允许把所有驱动都编进内核。这时候initramfs就派上用场了。initramfs本质是一个小型的cpio归档镜像里面包含必要的内核模块、udev或busybox mdev、初始化脚本以及一个最小的/dev、/proc、/sys目录结构。系统启动时内核先解压initramfs到内存把它作为临时根文件系统然后执行其中的/init脚本。脚本负责加载必要的驱动模块让真正的根设备可用然后switch_root切到真正的根文件系统继续启动流程。驱动模块要进入initramfs需要通过initramfs的构建工具配置。不同平台用的工具不同常见的有dracut、initramfs-tools、buildroot等。以initramfs-tools为例可以在/etc/initramfs-tools/modules里列出需要预先加载的模块# 需要在initramfs阶段加载的模块 my_store_controller my_network_driver然后重新生成initramfs镜像。实际项目里什么时候该编进内核、什么时候放initramfs、什么时候依赖根文件系统里的modprobe自动加载这里有一套决策逻辑场景推荐方式理由根文件系统所在设备驱动编进内核或放initramfs根设备不可用则系统无法启动存储控制器、文件系统驱动编进内核或放initramfs启动早期必须可用平台核心外设串口、时钟、中断控制器编进内核启动流程极早期依赖非关键外设WiFi、蓝牙、传感器等模块自动加载灵活性好可热插拔节省内存调试阶段用的驱动模块方便反复加载卸载、调整参数4. 实操全流程让自定义驱动开机自动加载4.1 准备一个最小可用的platform驱动为了把上面讲的原理串起来这里准备一个最小的虚拟platform设备驱动。场景很简单设备树里声明一个设备节点驱动模块不提前加载系统启动后设备树节点被解析内核自动找到并加载我们的驱动然后绑定设备。驱动源码如下#include linux/module.h #include linux/platform_device.h #include linux/of.h static int my_demo_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; dev_info(pdev-dev, my_demo_probe called\n); res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (res) { base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); dev_info(pdev-dev, reg mapped at %p\n, base); } return 0; } static int my_demo_remove(struct platform_device *pdev) { dev_info(pdev-dev, my_demo_remove called\n); return 0; } static const struct of_device_id my_demo_of_match[] { { .compatible myvendor,my-demo-device }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_demo_of_match); static struct platform_driver my_demo_driver { .probe my_demo_probe, .remove my_demo_remove, .driver { .name my_demo_device, .of_match_table my_demo_of_match, }, }; module_platform_driver(my_demo_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Demo driver for auto-load test);几个地方需要解释一下。module_platform_driver是一个封装宏等价于在init函数里调用platform_driver_register在exit函数里调用platform_driver_unregister省去了手动写module_init和module_exit的模板代码。of_match_table指定驱动支持的设备树compatible集合MODULE_DEVICE_TABLE把这张表写进模块的.modinfo。4.2 编译环境的搭建与Makefile编译模块需要一个完整的内核构建目录推荐直接在目标平台同版本的内核源码树上编或者使用目标系统/lib/modules/$(uname -r)/build目录。最简单的Makefile如下obj-m : my_demo.o KERNELDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean如果是交叉编译加上ARCH和CROSS_COMPILEmake ARCHarm CROSS_COMPILEarm-linux-gnueabihf-编译完成后目录下会出现my_demo.ko。在拷贝到目标板之前先在本机用modinfo看一眼modinfo my_demo.ko输出中应该能看到类似filename: my_demo.ko alias: of:Nmy-demo-deviceT(NULL)Cmyvendor,my-demo-device description: Demo driver for auto-load test license: GPL这个alias就是我们前面反复强调的关键。只要alias存在后面内核才能通过设备树节点反向找到这个模块。4.3 拷贝模块、更新depmod索引模块编译好只是开始要让modprobe认识它必须把它放到内核模块的标准搜索路径最常见的是sudo cp my_demo.ko /lib/modules/$(uname -r)/extra/ sudo depmod -adepmod -a会扫描/lib/modules/$(uname -r)/下的所有模块重新生成modules.dep和modules.alias。确认索引文件里已经包含我们的模块grep my_demo /lib/modules/$(uname -r)/modules.alias grep my_demo /lib/modules/$(uname -r)/modules.dep如果modules.alias里能看到那行of:Nmy-demo-device...的条目整个方案已经成功了一大半。此时可以先手动验证一下sudo modprobe my_demo dmesg | taildmesg里应该能看到“my_demo_probe called”之类的输出。如果设备树节点还不存在probe不会立刻调用但模块会加载成功。加载完可以用lsmod确认lsmod | grep my_demo这个阶段能正常出结果说明模块本身没问题、依赖没问题、索引也建好了。4.4 设备树节点设计与系统启动验证现在到最关键的一步让设备树告诉内核“有一个设备长这样请找对应驱动”。在设备树源文件里找到合适的骨架位置添加节点/ { my-demo { compatible myvendor,my-demo-device; reg 0x0 0x100; }; };有的平台需要在某个bus节点下有的根节点下也可以。关键是compatible务必和驱动里of_device_id表中的完全一致。改完设备树重新编译设备树二进制dtb烧写到板子上重启。启动后内核在解析设备树时看到compatible为“myvendor,my-demo-device”的设备节点会在platform bus上注册一个platform_device。驱动匹配时发现my_demo模块还没加载于是内核发起request_module调用modprobe。modprobe去modules.alias里查询找到对应的my_demo.ko加载模块模块注册platform_driver总线立即匹配到刚注册的设备调用probe函数。整个链路里我们没有手动执行任何insmod。验证是否成功很简单lsmod | grep my_demo dmesg | grep my_demo ls -l /sys/bus/platform/devices/my-demodmesg里如果能同时看到类似my_demo: loading out-of-tree module taints kernel. my_demo_driver my-demo: my_demo_probe called说明从“设备树节点解析”到“自动加载模块”再到“驱动绑定设备”全链路已经通了。4.5 如果平台没有设备树module alias的降级方案并不是所有Linux系统都有设备树x86平台和部分老式ARM平台用的是ACPI或者传统platform_device注册方式。这种情况下驱动自动加载仍然可以实现但触发点不同。一种做法是直接声明一个MODULE_ALIAS让modprobe能在没有设备树匹配的情况下识别模块。比如MODULE_ALIAS(my-demo-device);然后通过modprobe.conf或udev规则在设备出现时触发加载。更多见的做法是使用模块的软依赖或其他总线的设备ID表PCI、USB等PCI和USB设备的自动加载几乎全依赖MODULE_DEVICE_TABLE生成的alias和设备树没有直接关系。对于纯platform设备但没有设备树的情况通常需要在板级代码里注册platform_device然后驱动的.name和设备的.name一致系统启动时也能匹配。不过这种方式现在已经不推荐新项目强烈建议设备树。5. 自动加载问题排查与踩坑实录5.1 典型故障速查表这一节把实际开发中经常遇到的自动加载失败场景整理成表格每个都对应明确的排查方向现象可能原因排查方法modprobe提示Module not founddepmod没跑或模块没放对目录确认模块在/lib/modules/$(uname -r)/下执行depmod -a设备树节点存在但驱动没加载compatible不匹配或alias缺失对比modinfo输出和设备树compatible确认MODULE_DEVICE_TABLE存在手动modprobe成功但开机不加载没有设备触发request_module检查设备树节点是否真的被内核解析在/sys/bus/platform/devices/下确认驱动加载了但probe没被调用of_match_table和compatible不匹配用dmesg查驱动注册情况确认of_device_id表内容启动早期根设备挂载失败必要驱动没进内核和initramfs把驱动加进initramfs或编进内核模块加载时提示Unknown symbol依赖模块没先加载用modprobe代替insmod检查modules.dep驱动在initramfs阶段不生效initramfs里没包含驱动和依赖检查initramfs模块列表重新生成镜像插上设备但没有任何加载尝试内核request_module被禁用或usermode helper异常检查/proc/sys/kernel/modprobe确认modprobe路径升级内核后自动加载失效新内核模块目录清空重新编译安装模块重新depmod无权限修改/lib/modulesSecure Boot锁定了模块签名检查内核模块签名要求为模块签名或关闭强制校验5.2 真实案例一次“驱动神秘消失”的排查过程有次在板子上调试一款触摸屏驱动设备树节点明明加了compatible也对模块也编译了然后reboot后系统里怎么都找不到这个驱动。dmesg里没有任何报错/sys/bus/platform/devices/下也没有对应设备。整个排查过程有点绕这里拆开说。先查设备树有没有问题。用udevadm和dtc分别确认dtb确实烧录进去了反编译dtb设备树节点存在。然后查模块索引modules.alias里确实有对应条目。问题看起来不在设备树和模块侧。后来进系统后手动modprobe发现驱动能加载probe也调用成功。这就奇怪了手动能触发说明设备和驱动是匹配的。最后把注意力放到启动日志上发现这个模块在启动阶段曾有加载失败的记录原因是initramfs阶段系统尝试加载它但该模块依赖的另一个模块当时还没就绪加载失败后续整个加载请求被放弃。这个问题表面上神秘实际是模块依赖时序问题。解决办法有三个一是把依赖模块也放进initramfs并保证加载顺序二是在initramfs阶段不加载这个驱动让系统切根后再自动加载三是直接把依赖模块编进内核。最终选择让initramfs提前加载依赖模块问题解决。这个案例的教训是自动加载失败不一定发生在你盯着dmesg看的那一刻很多失败发生在initramfs早期日志可能被覆盖或过滤掉了。排查时要把启动全阶段日志都收集齐尤其不要忽略initramfs阶段的日志。5.3 几个提升排障效率的小技巧自动加载问题排查起来容易瞎忙活原因是链路太长每个环节都可能出问题。这里分享几个亲测好用的技巧。第一个技巧是分层验证。把“设备存在”“模块可加载”“索引正确”“匹配成功”分成四个独立环节逐个确认。设备存在由设备树和/sys目录验证模块可加载由modprobe手动加载验证索引正确由grep modules.alias验证匹配成功由dmesg的probe日志验证。任何一环不过关就只攻那一环别来回折腾。第二个技巧是善用modprobe的verbose模式。加载模块时加-v参数modprobe会把依赖解析过程、模块查找路径全部打出来modprobe -v my_demo它显示的信息比dmesg更直观尤其适合判断是不是依赖解析时出了问题。第三个技巧是修改request_module的usermode helper路径。如果系统里modprobe路径不对内核的自动加载请求会静默失败。检查一下cat /proc/sys/kernel/modprobe正常情况下应该输出modprobe的绝对路径。如果这个值是空的或者指向不存在的文件自动加载必然失败。第四个技巧是量产阶段建议在每次文件系统构建脚本里强制更新模块索引并自动检查关键模块的alias是否出现在modules.alias中。比如depmod -a grep -q myvendor,my-demo-device /lib/modules/$(uname -r)/modules.alias \ || echo module alias missing, auto-load will fail把这一步写进CI或者打包脚本能在问题影响现场之前就暴露出来。结语的一点个人体会自动加载这套机制刚接触时觉得无非就是“开机跑一下modprobe”真正深入之后才发现它串联了编译系统、模块机制、设备树、内核总线和用户态工具好几个层次。我自己最早调试一个SPI屏幕驱动时也遇到过设备树节点明明加了、驱动就是没加载的情况把从设备树到modules.alias的每一条链路都翻了一遍才意识到是depmod没有刷新索引。自那以后我再也不敢小看这个“基础”话题——基础不代表简单而是意味着它支撑着上面所有复杂功能的地基。最后再分享一个小技巧产品量产阶段不要依赖开发板上那种“反正我能手动加载”的习惯。从第一次提交驱动代码开始就把自动加载配置一起提交到工程里包括设备树dts、modprobe配置、initramfs列表。这样后续每一次改动都能第一时间暴露自动加载链路上的问题而不是等到设备到客户现场才翻车。
返回列表