十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Linux驱动自动加载实战:modprobe、udev与initramfs全解析

Linux驱动自动加载实战:modprobe、udev与initramfs全解析 说实话写《Linux 驱动基础》这个系列最开始规划时我只想聊聊字符设备驱动的框架怎么写、file_operations 怎么注册。可后来真到了把驱动从开发板搬到产品里跑才发现“驱动能不能自己乖乖加载”这个问题才是从 demo 到量产之间最容易翻车的一关。驱动自动加载听着就是个“开机自动 insmod”的小事实际做起来会牵扯到模块依赖、modprobe 机制、udev 设备节点、甚至 initramfs 里的备用驱动。一旦没理清你可能会遇到“板子重启后设备不见了”“插上 USB 设备系统毫无反应”“modprobe 报错 Unknown symbol”这类问题。这篇文章就把我踩过的坑和摸清的设计思路一次性讲透非常适合正在做嵌入式 Linux、或者第一次把驱动集成到系统里的朋友参考。1. 驱动自动加载到底在解决什么问题1.1 手动加载驱动的一天先说个场景。你在开发板上写了一个简单的按键驱动每次开机后手动运行insmod key_drv.ko mknod /dev/key_dev c 240 0第一次这么做觉得没啥但一旦反复重启、反复调试你就开始烦了为什么设备节点要手动建为什么依赖的另一个驱动必须先加载如果做产品总不能要求用户每次开机都敲一遍命令吧。自动加载的核心目的就是把“驱动模块加载 设备节点生成 依赖顺序处理”这三件事从人工操作变成系统机制自动完成。你不用关心驱动文件放在哪只要设备出现系统就能自己找到对应驱动并加载。1.2 自动加载的核心链路从开发者的视角看一个驱动从编译完成到能自动被系统启用大致要经过这么几个环节驱动编译成.ko文件安装到/lib/modules/$(uname -r)/对应目录下。depmod扫描模块信息生成modules.dep和modules.alias等索引文件。系统启动时systemd-modules-load或/etc/modules-load.d/中的配置按顺序加载指定模块。对于热插拔设备内核通过uevent通知udev由udev匹配规则后调用modprobe加载对应驱动。如果根文件系统本身依赖某个驱动例如磁盘控制器驱动、文件系统驱动那这个驱动还需要提前放入initramfs。这条链路里有内核态的机制也有用户态的守护进程参与。很多人只盯着.ko文件本身却忽略了depmod和udev规则所以才会出现“模块明明在系统却加载不了”的怪现象。2. 内核和用户态是怎么配合完成的2.1 模块机制insmod、modprobe与依赖解析先区分两个命令insmod和modprobe。insmod是最原始加载方式它只负责把一个.ko文件塞进内核不做依赖解析。比如你加载a.ko它依赖b.ko里的导出符号那你就得先把b.ko加载进去否则insmod a.ko会直接报Unknown symbol。modprobe则会聪明很多。它在加载模块前会查/lib/modules/$(uname -r)/modules.dep这个依赖关系文件自动先把依赖的模块加载好。所以日常调试也好、自动加载也好统一用modprobe准没错。模块依赖关系是depmod生成的。每当你新安装了一个模块或者更新了内核都要跑一次depmod -a只有依赖文件是最新的modprobe才能正确解析。很多新手会漏掉这一步结果换了新编译的驱动后modprobe还是加载了老版本的模块就是因为/lib/modules/...下的索引没刷新。2.2 udev与设备节点的自动创建内核加载了驱动只是让软件驱动和硬件设备建立了关联但用户空间程序要访问设备还需要/dev下的设备节点。设备节点就是文件系统里代表设备的一个特殊文件应用程序通过 open/read/write 这个文件来操作硬件。在早期 Linux 里设备节点需要手动mknod主设备号、次设备号都得自己填。现在基本都是udev在管理设备插入时内核发送ueventudev收到事件后根据/etc/udev/rules.d/下的规则创建设备节点并且可以设置权限、属组、软链接等。比如一个 USB 转串口芯片 CH340插入后想要自动创建/dev/ttyCH341USB0而不是默认的ttyUSB0可以写这样一条 udev 规则KERNELttyUSB*, ATTRS{idVendor}1a86, ATTRS{idProduct}7523, MODE0666, SYMLINKttyCH341USB0规则保存为/etc/udev/rules.d/99-ch340.rules然后执行udevadm control --reload-rules udevadm trigger很多时候大家发现“插上设备后没反应”不是驱动没加载而是 udev 规则没有刷新或者设备节点权限不够导致应用打不开。2.3 内核态主动请求加载request_module自动加载还有一个容易被忽略的内核机制request_module()。它运行在内核态作用是当某个子系统发现“硬件在这里但对应驱动还没加载”时主动触发用户态的modprobe来加载模块。最典型的例子是 USB 子系统。USB 设备插入后内核根据设备的 vendor ID 和 product ID 组成 modalias 字符串然后调用request_module(usb:v%04Xp%04X...)。modprobe根据这个字符串到modules.alias里查表找到匹配的驱动模块然后加载。所以如果你写的驱动支持 USB 设备记得在模块里声明MODULE_ALIAS(usb:v1A86p7523d*dc*dsc*dp*ic*isc*ip*);这样内核才能通过 modalias 自动找到你的驱动。同理PCI、I2C、SPI 等子系统也有类似的匹配机制只是字段格式不一样。3. 实操让一个驱动开机自动加载3.1 准备一个最小的字符设备驱动自动加载这件事最好拿一个干净的字符设备驱动来实验。我习惯先写一个什么都不做的框架#include linux/module.h #include linux/kernel.h #include linux/init.h #include linux/fs.h #include linux/uaccess.h #define DEVICE_NAME demo_drv #define MAJOR_NUM 240 static int demo_open(struct inode *inode, struct file *file) { printk(KERN_INFO demo_drv: open\n); return 0; } static struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, }; static int __init demo_init(void) { if (register_chrdev(MAJOR_NUM, DEVICE_NAME, demo_fops) 0) { printk(KERN_ERR demo_drv: register failed\n); return -1; } printk(KERN_INFO demo_drv: initialized\n); return 0; } static void __exit demo_exit(void) { unregister_chrdev(MAJOR_NUM, DEVICE_NAME); printk(KERN_INFO demo_drv: exited\n); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL); MODULE_ALIAS(dummy-demo);对应的 Makefileobj-m : demo_drv.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在源码目录执行make得到demo_drv.ko先用insmod或modprobe手动验证一次确保模块本身没有问题再谈自动加载。3.2 配置modprobe自动加载手动验证没问题后把模块安装到系统标准目录sudo cp demo_drv.ko /lib/modules/$(uname -r)/extra/ sudo depmod -a然后可以直接modprobe demo_drv验证因为depmod已经把模块索引建好了。如果希望系统开机时自动加载这个模块最通用的方法是创建/etc/modules-load.d/demo.confecho demo_drv | sudo tee /etc/modules-load.d/demo.conf这个目录下的每个文件都会在系统启动时被systemd-modules-load.service读取按行加载对应模块。加载的模块名不带.ko后缀因为这里指定的是模块名不是文件名。配置生效后可以手动执行一次sudo systemctl restart systemd-modules-load.service然后查看模块是否加载lsmod | grep demo_drv这里有个容易踩的坑modules-load.d里的模块名必须能被modprobe找到。也就是说depmod -a必须先执行过否则系统启动时根本不知道demo_drv对应哪个文件。3.3 使用udev规则按需加载开机自动加载适合固定硬件但如果你的驱动对应的是热插拔设备更好的做法是让驱动在设备插入时才加载。这样既节省资源也避免无关设备的误加载。拿 USB 串口芯片举例如果驱动模块叫ch34x设备插入时希望通过 udev 自动加载驱动可以在 udev 规则里写ACTIONadd, SUBSYSTEMusb, ATTRS{idVendor}1a86, ATTRS{idProduct}7523, RUN/sbin/modprobe ch34x规则文件放在/etc/udev/rules.d/99-ch34x.rules。modprobe路径最好写绝对路径因为 udev 执行环境的PATH很精简容易出现“命令找不到”的情况。如果驱动已经通过 modalias 匹配被内核自动加载了那 udev 里的RUN其实不是必须的。真正必须的往往是设备节点的创建和权限设置。所以更常见的规则写法是ACTIONadd, SUBSYSTEMtty, ATTRS{idVendor}1a86, ATTRS{idProduct}7523, MODE0666, SYMLINKarduino这里重点不是加载驱动而是给应用层提供一个稳定设备名。3.4 initramfs场景下的加载有一种特殊情况根文件系统所在的磁盘、或者根文件系统依赖的加密模块它们的驱动必须在 rootfs 挂载之前就加载。这种情况下驱动不能只放在/lib/modules下必须塞进initramfs。以 Ubuntu/Debian 系为例在/etc/initramfs-tools/modules里加上模块名demo_drv然后重新生成 initramfssudo update-initramfs -uRHEL/Fedora 系使用 dracut命令是sudo dracut --forceinitramfs 里的模块加载是顺序敏感的有些模块需要按依赖顺序来。比如 A 依赖 B如果 A 先加载就会报缺少符号。depmod会生成依赖顺序所以你要保证 initramfs 构建时也运行了depmod。调试 initramfs 里的模块加载是否成功可以在启动参数里加rd.shell或breakpre-mount进入 initramfs 的 shell 环境手动 modprobe 排查。这个方法帮我在真实项目里定位过好几次“磁盘找不到”的问题。4. 常见问题与排查技巧4.1 modprobe找不到模块这是最常遇到的问题modprobe demo_drv返回modprobe: FATAL: Module demo_drv not found in directory /lib/modules/...。先确认模块文件确实在标准目录下而且路径正确find /lib/modules/$(uname -r) -name demo_drv.ko再确认 depmod 是否已经刷新depmod -a modprobe demo_drv如果还不行直接用modinfo demo_drv看能不能识别到模块信息。modinfo找不到的话说明模块安装目录或者模块名有问题。还有一种情况是模块在/root或者其他非标准目录下那你只能通过insmod /path/to/demo_drv.ko加载或者把它复制到标准目录后再 depmod。4.2 模块签名校验失败如果你开启了 Secure Boot加载没有签名的模块会报类似 “Required key not available” 或 “Module verification failed” 的错误。insmod可能还能用但modprobe和开机自动加载肯定会失败。解决办法有两个要么用 MOKMachine Owner Key给模块签名要么在 BIOS 中关闭 Secure Boot。开发环境图省事通常会关掉但产品环境建议做正规签名。签名步骤大致是mokutil --import module-signing-key.der然后重启在 MOK 管理界面确认导入。这一步做完模块签名才被系统信任。4.3 udev规则没生效写完 udev 规则后如果发现设备节点没生成先用udevadm手动模拟测试udevadm test /sys/class/tty/ttyUSB0这个命令会输出 udev 实际执行的规则你就能看到自己写的那条规则有没有被匹配到。如果规则没跑检查三点规则文件后缀必须是.rules放在/etc/udev/rules.d/。规则文件里的匹配字段是否写对比如ATTRS和ATTR的区别很多设备信息在父设备上要用ATTRS在设备本身上用ATTR。执行了udevadm control --reload-rules没有。另外如果设备是在规则加载之前就插上的记得执行udevadm trigger重新触发一次事件。4.4 依赖顺序和循环依赖自动加载时modprobe会根据modules.dep处理依赖顺序但前提是模块间的符号依赖能被depmod正确识别。最典型的问题是模块 A 使用了模块 B 提供的EXPORT_SYMBOL符号但 B 没有安装到系统里。这时候depmod会提示 unresolved symbols或者modprobe A时提示 Unknown symbol。解决办法make modules_install depmod -a这两个命令最好连着跑。不要只 copy.ko文件因为符号信息同时需要.ko里的.symvers和模块目录下的索引配合。循环依赖比较少见但也遇到过。A 依赖 BB 又依赖 Amodprobe会卡住甚至死循环。遇到这种情况不要试图在模块层面解决把公共代码提取成一个独立的底层模块让 A 和 B 都依赖它从设计上打破循环才是正路。5. 一点个人经验驱动自动加载这套机制我在不同项目里用了很多遍最大的体会是开发阶段和量产阶段应该用不同的加载策略。开发阶段我习惯全部手动insmod因为模块改得频繁开机自动加载反而会加载到旧版本导致你改完代码发现行为没变化还以为没编对。等驱动稳定后再把它切换到自动加载模式。另一个建议是无论是用modules-load.d还是 udev 规则都一定要在模块名或规则里加上项目标识避免和别人写的规则冲突。我曾经在一个共享服务器上遇到过两个项目都定义了ttyUSB*的映射后加载的规则覆盖了前面的设备节点指到了完全不同的串口上排查了半天。最后分享一个小技巧每次编译并安装完模块后第一时间执行depmod -a然后再做任何 modprobe 或重启验证。这个习惯能帮你避开 90% 的“模块找不到”和“Unknown symbol”问题。自动加载不是玄学本质上就是“索引建好 事件匹配 模块名找得对”这三件事把每一步都验证一遍系统自然会在你需要的时候把驱动准备好。
返回列表