做完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依赖哪些其他.ko
- modules.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,生成类似这样的alias:
alias 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 debug=1 # 重新定义模块名 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-device@0 { compatible = "myvendor,mydevice"; reg = <0x0 0x100>; };驱动侧则声明自己支持哪些compatible:
static 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设备表为例,编译后的模块里会有一段类似这样的记录:
alias=of: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事件。当设备被添加、移除、改变状态时,内核会发送uevent,udev根据/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_COMPILE:
make ARCH=arm CROSS_COMPILE=arm-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 found | depmod没跑或模块没放对目录 | 确认模块在/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/modules | Secure 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列表。这样后续每一次改动,都能第一时间暴露自动加载链路上的问题,而不是等到设备到客户现场才翻车。