
1. 从 insmod 到开机自启驱动自动加载到底解决了什么问题如果你写过几个字符设备驱动大概率经历过这样的场景开发板上电系统起来你打开终端敲下insmod xxx.ko看到日志里刷出device registered然后才敢去写测试程序。这套流程在调试阶段一点毛病没有甚至很舒服因为每次改完代码重新编译rmmod再insmod一遍节奏完全可以自己掌控。但一旦驱动写完要交给别人用或者要部署到现场设备上问题就来了总不能要求每个使用设备的人都熟练地敲insmod、记模块名、查依赖顺序吧更重要的是系统一重启你手动加载的那些模块就全没了设备直接“失联”。这时候就需要把驱动做成开机自动加载或者更进一步让内核在发现对应硬件时自动把驱动匹配上。这篇文章就围绕“驱动自动加载”这个主题把我在实际项目里用过的几种方案、踩过的坑、排查思路都梳理一遍。内容定位在基础到进阶之间适合已经写过简单字符设备驱动、但对“怎么让它跑起来更省心”这件事还有点模糊的开发者。先强调一个前提自动加载不是某一条命令能解决的事它是一套组合拳。涉及驱动代码怎么写module_init和MODULE_DEVICE_TABLE、模块依赖怎么声明MODULE_DEPENDENCIES或者depmod生成的依赖关系、用户态怎么触发modprobe、udev、systemd、以及硬件平台怎么描述设备树、ACPI这四个层面。明白了这一点再看网上一堆零散的教程就不容易晕。2. 必须理解的内核模块加载机制2.1 模块加载的触发源不止一个要设计自动加载第一步得搞清楚驱动模块在内核里到底是怎么被“想起来”的。insmod只是最简单粗暴的一种不管三七二十一把.ko文件加载进内核执行模块里的init函数完事。它不解决依赖问题insmod a.ko时如果a依赖b得先手动把b加载进去。modprobe就不一样了它会把依赖关系一起处理掉。modprobe a的时候它会去查modules.dep这个文件由depmod命令生成如果发现a依赖b就先把b加载进来再加载a。但注意modprobe默认只在系统模块目录通常是/lib/modules/$(uname -r)/里找.ko你自己编译后放在/home/xxx/下的模块直接modprobe是找不到的。除了用户手动执行还有三种自动触发路径。第一种是硬件枚举触发。内核在启动过程中或者运行时发现有新的硬件设备出现比如 USB 设备插入、PCIe 设备枚举会向用户态的udev通过uevent发送消息udev根据设备信息去/lib/modules/$(uname -r)/modules.alias里找匹配的模块然后调用modprobe。这就是为什么你把 USB 转串口芯片插上去系统能自动加载ch341或cp210x驱动。第二种是文件系统或协议栈触发。比如你挂载一个ntfs格式的 U 盘内核的VFS层发现不认识这个文件系统会请求binfmt或者直接触发request_module(ntfs)之类的调用把对应模块加载进来。第三种是内核代码里显式调用request_module()。这种方式多半用于“按需加载”的场景比如某个驱动在probe时需要用到另一个子系统就主动请求加载。搞懂这些触发源你在设计自动加载方案时才能选对方向是用来解决“模块太多、依赖复杂”还是解决“插上设备就识别”还是解决“开机固定加载指定模块”。方向错了后面怎么做都不顺。2.2 module_init 与 module_exit 的隐藏细节所有驱动都必须实现module_init(xxx_init)和module_exit(xxx_exit)这两个宏看起来简单但有几个容易被忽略的细节。第一module_init不止是告诉内核“初始化函数是哪个”它还决定了这个驱动是编译成模块m还是编进内核镜像y。如果编进内核module_init注册的函数会在内核启动过程中被调用而且调用时机很早在initramfs挂载前后。这一点会影响驱动里能不能用某些依赖资源——比如你的初始化函数里面要分配内存这没问题但如果要访问某个需要稍后才会注册的子系统可能就会失败。第二如果一个模块同时声明了module_init和module_exit加载和卸载路径是完整的。如果你故意不写module_exit比如某些只加载、不卸载的驱动rmmod时会提示No such file or directory因为内核找不到卸载入口。当然也有合法的场景不写module_exit比如驱动要常驻内核但我是建议哪怕空实现也写上否则后续调试会恶心到自己。第三MODULE_LICENSE(GPL)不只是形式上的声明。如果你在驱动里用了不导出的内核符号或者用了某些依赖 GPL 的接口没有 GPL 声明的话编译时可能没问题但加载时会报Unknown symbol或者直接被拒绝加载。自动加载场景下这类问题往往要到设备插上去、udev尝试加载时才暴露排错成本更高。2.3 alias 机制驱动和设备的“身份证匹配”自动加载的关键在匹配而匹配的基础是MODULE_DEVICE_TABLE宏。这个宏的作用是导出一张设备 ID 表里面记录了该驱动支持哪些设备。这张表在编译时会生成一个独立的 sectiondepmod读取后生成modules.alias文件。举个例子你写一个 I2C 触摸屏驱动支持某厂商的型号代码里会有类似这样的声明static const struct i2c_device_id xxx_ts_id[] { { goodix-ts, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, xxx_ts_id);编译完执行depmod后modules.alias里会有一行类似于alias i2c:goodix-ts的记录。当 I2C 总线上枚举出地址和设备名匹配的节点时内核就能根据这个 alias 找到模块并加载。这张表也分类型常见的有i2c、spi、platform、usb、pci等。每种总线的匹配优先级和方式不太一样但核心思想相同驱动声明“我认识谁”总线枚举时报出“我是谁”两边对上了就自动加载。这也是设备树Device Tree能够工作的重要原因。3. 方案一基于 modprobe 和 depmod 的模块依赖自动加载3.1 从编译到 install 的标准流程假设你已经写好了一个驱动模块并且通过make编出了xxx.ko。要想实现modprobe自动加载得先把模块放到标准位置sudo make modules_install这一步会把.ko文件复制到/lib/modules/$(uname -r)/extra/或者对应内核版本的目录里并同时运行depmod更新依赖信息。不过实际项目里如果你是交叉编译给开发板用的就不能直接sudo make modules_install装到本机需要手动拷贝 在目标板上执行depmod。depmod做的事情简单说就是扫描模块目录下所有.ko解析里面的符号依赖和设备 ID 表生成下面这几个关键文件modules.dep模块依赖关系modules.alias设备别名映射modules.symbols模块导出的符号modules.builtin编译进内核的模块列表这几个文件是modprobe、udev自动加载的信息源。所以自动加载的第一步永远是确保目标板上的depmod跑对了。如果/lib/modules/$(uname -r)下面根本没有对应的.ko文件或者modules.dep是旧的再怎么折腾设备也不会自己起来。3.2 内核源码树与模块目录的匹配陷阱这里有个隐蔽的坑depmod并不是扫描/lib/modules下所有文件就能完事它依赖$(uname -r)这个版本号来定位模块目录。如果你手动编译的内核和当前运行的内核版本号不一致模块会装到另一个目录去modprobe就找不到。我在项目里就遇到过这么一次写了驱动编译完拷贝到板子上insmod能正常加载但modprobe始终报Module xxx not found。查了半天发现板子的/lib/modules/下有两个目录一个是 4.19.71一个是 4.19.71-g45d2b01带本地版本后缀编译内核时LOCALVERSION配置不一致。后来统一了.config里的CONFIG_LOCALVERSION重新编译模块问题才解决。所以做自动加载前先做好这两件事确认uname -r输出的版本号看看/lib/modules/下的目录名是否一致。编译模块时确认用的内核源码树就是当前运行内核对应的版本和配置最好用/lib/modules/$(uname -r)/build这个软链接来指定源码路径避免手动指定头文件的版本错乱。3.3 modules.dep 里描述了哪些依赖关系这里展开讲一下modules.dep的格式因为它直接决定了modprobe的行为。文件内容形如/lib/modules/5.10.0-rc7/extra/my_drv.ko: /lib/modules/5.10.0-rc7/extra/base_drv.ko冒号前面是目标模块冒号后面是它依赖的模块列表。depmod通过解析每个.ko的.modinfo段和未解析符号来自动生成这些关系。具体来说如果my_drv.ko里引用了base_drv.ko导出的符号比如调用了base_drv_init()或者使用了里面定义的全局变量depmod就会认为这俩存在依赖。所以编写驱动时如果你自己设计模块拆分为“基础模块 功能模块”一定要在功能模块源码里包含有真实引用的基础模块导出符号而不是只加一行depends注释。否则depmod没法自动建立依赖加载时就会因为符号未定义而失败。如果你想让依赖关系更明确也可以在源码中通过MODULE_SOFTDEP声明软依赖例如需要某个固件先加载或者用MODULE_ALIAS声明额外的别名。软依赖的好处是就算没有硬性符号依赖也能保证加载顺序正确。4. 方案二设备树与 platform 驱动的自动匹配4.1 platform 总线是“挂接”驱动的纽带在嵌入式 Linux 里很多设备不是 USB、PCIe 这种可枚举总线上的设备而是直接“长”在 SoC 内部或者板级电路上。这类设备没法自己报出“我是谁”内核采用了一种简单的办法把它们抽象成platform_device挂到platform总线上。platform_driver则通过platform_driver_register()注册到同一条总线上。总线的匹配逻辑是一套比较逻辑设备端有compatible字符串、name字段驱动端有of_match_table、id_table、driver.name。只要任意一组匹配上总线就会调用驱动的probe函数。这是内核里被千万个驱动验证过的成熟机制也是实现“设备树设置了节点、驱动就被自动加载”的根基。理解了这个过程就不难明白设备树里compatible属性的值必须和驱动代码里of_match_table的.compatible值完全一致一字不差。4.2 从设备树节点到 probe 的完整链路一个典型的设备树节点长这样/ { my_device: my_device0 { compatible myvendor,my-device; reg 0x0 0x100; interrupts 0x1f 8; }; };驱动侧对应的声明static const struct of_device_id my_of_match[] { { .compatible myvendor,my-device }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name my_device, .of_match_table my_of_match, }, }; module_platform_driver(my_driver);如果你把my_of_match[]声明成了static但没有加MODULE_DEVICE_TABLE(of, my_of_match)会发生什么编译能过加载能过但modprobe在系统启动的时候根本不知道这个驱动认识哪个设备。因为缺少这张表depmod生成modules.alias的时候没有对应记录udev就不会被触发去加载它。这就是很多新手最困惑的地方明明驱动写的没问题、设备树也有节点、insmod也能正常probe但“自动加载”就是不生效。原因往往就是漏了这一行MODULE_DEVICE_TABLE(of, my_of_match)。4.3 设备树匹配的其他细节匹配上之后probe函数里通常还需要获取设备树里的资源信息比如reg、interrupts、gpios等。这些通过device_property_read_u32()、platform_get_resource()、devm_gpiod_get()等 API 完成。自动加载成功后这些资源获取环节出错的话驱动会直接probe失败日志里能看到probe of my_device failed with error -XX。在实际项目中我还习惯在设备树节点里加一个status okay的属性。有些 SoC 的默认设备树比如从 SDK 里拷出来的会把用不到的节点设为status disabled如果你没注意到驱动加载了也 prob 不上因为设备端是禁用状态。排查这类问题时ls /sys/bus/platform/devices/看设备有没有注册ls /sys/bus/platform/drivers/my_device/看驱动有没有绑定通常一眼就能定位。5. 方案三systemd/init 脚本下的开机固定加载5.1 什么时候必须手动指定加载顺序依赖自动匹配不是万能的。有些驱动在硬件枚举之前就要就位有些驱动依赖特定的初始化顺序比如先加载某个自定义协议栈再挂载驱动还有些驱动压根不绑定具体设备纯软件驱动设备树和 alias 的匹配机制完全帮不上忙。这时候就需要在系统初始化阶段手动加载。最常见的方式是写一个 systemd service 单元放在/etc/systemd/system/下让它开机执行modprobe[Unit] DescriptionLoad my custom driver Beforenetwork.target [Service] Typeoneshot ExecStart/sbin/modprobe my_drv RemainAfterExityes [Install] WantedBymulti-user.target保存后执行sudo systemctl daemon-reload sudo systemctl enable my-driver.serviceBeforenetwork.target是特意加的因为我的驱动里有一个虚拟网卡接口必须在网络服务启动之前创建好否则网络配置会失败。这种“初始化顺序强相关”的场景靠MODULE_SOFTDEP已经不够了必须用 init 系统来管。5.2 旧式 init 脚本和 modules-load.d 的取舍如果你的目标系统还在用 SysV init比如某些老旧的 buildroot 系统可以创建一个/etc/init.d/Sxxmydrv脚本在start分支里执行modprobe在stop分支里执行rmmod并加上chmod x确保它能在开机阶段被rcS执行。还有一个更轻量的做法直接编辑/etc/modules-load.d/目录下新建一个.conf文件每行写一个模块名系统启动早期就会加载这些模块。这个方案适合“不需要调顺序、只需要固定加载”的简单场景。我的建议是能用 systemd service 就用 systemd service因为RemainAfterExityes可以让你用systemctl start/stop来控制运行状态调试方便很多。而/etc/modules-load.d/方式在出错时排查手段相对有限只能看dmesg。5.3 开机加载路径下依赖缺失的定位技巧在开机阶段modprobe可能因为查找路径不对、模块目录不完整、符号依赖不满足等原因失败。如果你发现在系统完全启动后手动执行modprobe能成功但开机期间加载失败多半是依赖的某个模块加载晚于你的模块。比如你的驱动依赖input子系统的某个模块而这个模块配置成了晚加载那么你在早期阶段强行modprobe它可能因为找不到符号而报错。这种问题可以通过两种方式规避让内核自己管理依赖也就是用modprobe它会自动处理依赖顺序。在 systemd service 里加Aftersystemd-modules-load.service把加载时机往后推。经验法则**尽量用系统原生的依赖机制而不是手动指定加载顺序。**手动指定顺序只是在掩盖设计上的缺陷。6. 常见问题与排查技巧实录6.1 问题速查表现象可能原因排查方法modprobe报Module not found模块不在标准目录find /lib/modules/$(uname -r) -name *.ko设备插上后dmesg无任何反应modules.alias未更新depmod -a后重插设备驱动能insmod但系统启动后设备不可用设备树节点status为disabled查设备树源文件或ls /sys/bus/platform/devices/probe报-ENODEVcompatible字符串不匹配对比设备树节点和驱动of_match_table自动加载后probe失败-EIO资源获取错误检查reg、interrupts定义开机报找不到符号依赖模块未先加载用modinfo xxx.ko查看依赖模块加载了但remove失败可能忘了配置module_exit查/sys/module/xxx/refcnt6.2 一个典型的 USB 驱动自动加载排查案例我之前做过一个 USB 设备驱动插上设备后系统没反应手动insmod却能工作。当时第一反应是modules.alias没更新但跑了depmod -a也没用。后来发现原因在驱动代码里的usb_device_id表。这个表里面需要包含设备的VID和PID我一开始是从原理图复制的数值结果大小端搞反了实际报告出来的 ID 和表里对不上udev匹配自然失败。解决方法是先插上设备执行lsusb查看真实的ID然后把usb_device_id表里的值改过来重新编译、depmod、插拔设备立刻就能自动加载了。这个案例给一个启发调试自动加载必须先确认设备上报的 ID 和驱动声明的一致而不是在modprobe和udev规则上瞎猜。6.3 实战经验在 sysfs 里验证驱动是否绑定成功不管是设备树匹配还是 USB 匹配驱动probe成功之后内核会在 sysfs 里留下痕迹。检查这些痕迹是最快的验证手段ls /sys/bus/platform/devices/ ls /sys/bus/platform/drivers/my_device/ cat /sys/bus/platform/devices/my_device/ueventuevent里的MODALIAS字段会直接告诉你设备的标识符。如果MODALIAS里没有内容说明设备端注册就有问题驱动再怎么写也匹配不上。如果MODALIAS正确但你发现/sys/module/下还是没有模块信息那就是用户态的udev规则或者modules.alias的问题。这套验证链路我几乎每个驱动都会走一遍它能把问题快速切分到“内核设备模型”和“用户态自动加载”两个层面排查效率比盲目翻日志高得多。7. 我个人的一点经验总结我最早接触驱动自动加载的时候也天真地以为只要把.ko拷到/lib/modules下就万事大吉结果被各种“为什么别人行我不行”折磨到怀疑人生。后来逐渐总结出一套固定的排查顺序看uname -r版本目录、跑depmod -a、查modules.alias、看MODALIAS、再看probe返回值。把这五步走完百分之八十的问题都能定位。另一个想强调的点是驱动自动加载设计应该在写驱动代码阶段就考虑而不是写完之后再补。比如MODULE_DEVICE_TABLE和of_match_table的声明一开始就写全后面省掉的排查时间不可估量。等到设备交付现场才想起没写 alias那才是真的叫天天不应。最后分享一个小技巧写驱动时尽量多用devm_开头的资源管理接口devm_kzalloc、devm_gpiod_get、devm_request_irq这样即使probe中途出错内核也会自动释放已申请的资源自动加载失败时不会把系统搞成半初始化状态。这一点在长期运行的嵌入式设备上尤其重要。希望你少踩坑。