多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Linux驱动自动加载全解析:从内核模块到设备树实战

Linux驱动自动加载全解析:从内核模块到设备树实战 写 Linux 驱动躲不开一个问题设备一插上、系统一开机驱动是不是“自己就位”。很多朋友第一次做驱动开发都是先 insmod 一把看到 log 打印了再把模块拷到板子上。等真正要交付、断电重启验证的时候才发现每次都要手动敲命令一旦按下复位键之前写好的驱动像没存在过一样。这篇就是解决这个问题的把驱动自动加载的机制讲透再给出能直接用的方案。我会从内核模块机制讲到 udev、设备树、initramfs按照从原理到落地的顺序把整条链路理顺。适合已经写过至少一个字符设备驱动、想要让驱动真正“跑进系统”的开发者。1. 先把“加载”这件事本身搞明白1.1 模块不是“文件”是一段要注册的内核代码很多人会把.ko文件当成普通程序以为拷到某个目录里就算加载了其实完全不是一回事。.ko是 ELF 格式的内核模块文件但它不是被“运行”的而是被内核用init_module系统调用读进内核地址空间然后执行里面注册的初始化函数比如module_init指定的那个函数。我见过不少初学者在这块绕弯子觉得加载驱动像在 Windows 里双击安装包一样点两下就完事。Linux 下完全不一样驱动加载的本质是“把一段代码并进内核”它拥有内核权限访问内核符号表操作物理设备。这也是为什么内核模块要求 GPL 协议、要求签名校验、要求内核版本匹配——随便塞一段非法代码进内核态整个系统都可能直接崩溃。所以第一步你要理解一个基本事实驱动“加载”成功指的是内核完成了几件事——把模块二进制映射进内核空间解析符号依赖执行模块初始化函数把驱动的driver对象注册到对应总线上。这是一个动态的注册过程而不是单纯的文件操作。1.2 insmod 与 modprobe一步到位和自动排雷手动加载驱动时最常用的两个命令是 insmod 和 modprobe。insmod是最简单粗暴的路径指定.ko文件的完整路径内核直接加载。它不管依赖不给提示如果模块里引用了内核符号或者其他模块导出的符号加载时就会直接报unknown symbol然后加载失败。modprobe则聪明得多它会在/lib/modules/$(uname -r)/目录下寻找模块而且会先解析依赖关系把当前模块依赖的其他模块一起加载。比如你的驱动依赖内核里的i2c-coremodprobe 会检查依赖顺序先把i2c-core拉起来再加载你的模块。对比项insmodmodprobe路径指定必须指定绝对/相对路径只需要模块名依赖处理不处理缺符号就报错自动加载依赖模块配置支持不支持支持/etc/modprobe.d/配置底层依赖直接调用 init_module依赖 depmod 生成的 modules.dep这里有个容易忽略的关键点modprobe 之所以能处理依赖前提是系统里跑过depmod命令。depmod 会扫描/lib/modules/$(uname -r)/下的所有.ko文件分析里面的符号引用关系生成modules.dep和modules.alias等文件。如果没有执行 depmodmodprobe 大概率会告诉你 “module not found”哪怕你确信模块文件就在那个目录里。1.3 自动加载其实包含两个层面说到“驱动自动加载”严格来说要拆成两种情况第一层是系统启动阶段驱动随内核一起就位。比如开机后你ls /dev/能看到目标设备节点已经存在这就是启动加载。常见实现路径包括设备树匹配、platform 总线驱动注册、固定模块列表加载、initramfs 预加载等。第二层是设备热插拔阶段用户把 USB 设备插进电脑系统瞬间就识别出设备并完成驱动绑定。这一层主要靠内核的设备模型、总线匹配机制和用户态的 udev 协作实现。这两层虽然表现相似但底层机制完全不同。大部分人做嵌入式开发时遇到的问题集中在第一层——设备是焊在板子上的上电就在不需要热插拔只要求开机后驱动能自动匹配。而做 USB 外设开发、或者搞桌面部件的朋友则更需要理解第二层的热插拔触发链。2. 三条自动加载路径与背后原理2.1 Linux 设备模型device、driver、bus 三者如何碰撞要理解自动加载必须懂 Linux 的设备模型。先打个比方bus 就像是中介平台device 是求职者driver 是招聘岗位。系统里只要出现一个 device或者出现一个 driverbus 平台就会立刻做一次匹配尝试看看这个设备能不能跟某个驱动“对上眼”。这就是内核里核心的数据结构关系device描述硬件driver描述驱动能力bus负责匹配和绑定。总线在每次设备注册时都会遍历所有已注册的 driver调用总线的match函数驱动注册时同样会遍历所有设备尝试匹配。一旦匹配成功内核会调用驱动的probe函数probe 函数内部做硬件的初始化和设备节点的创建。对平台设备比如挂在 SoC 内部总线上的外设控制器来说匹配方式主要有这么几种按优先级排列驱动driver_override字段是否显式指定设备树节点的 compatible 与驱动的 of_match_table 是否匹配ACPI 表匹配驱动id_table中是否有匹配的设备 ID驱动 name 与设备 name 相同其中设备树匹配是嵌入式 Linux 中最常用的。内核在启动时会解析设备树把每一个节点注册为平台设备节点里的compatible属性就是设备的“身份证”。驱动侧则通过of_match_table声明自己支持哪些 compatible 字符串。以我常用的 demo 驱动为例static const struct of_device_id demo_of_match[] { { .compatible vendor,demo-device }, { }, }; MODULE_DEVICE_TABLE(of, demo_of_match);只要设备树里有vendor,demo-device这个 compatible系统启动时就会自动触发 probe。这里有个细节值得注意很多人在设备树里只写demo-device没有厂商前缀结果驱动怎么都配不上这就是没按设备树 binding 规范来写。虽然内核匹配时是纯字符串比较但带厂商前缀是内核社区的惯例一般格式是manufacturer,model。2.2 启动阶段的固定加载手动指定也要讲究方式如果说设备树匹配是“硬件存在 → 驱动自动出现”的被动式加载那么固定模块列表就是一种“主动点名”的加载方式。内核启动时用户空间的 init 进程会启动 udev设备管理器udev 会读取若干模块加载配置把指定的模块加载进内核。这个配置通常就在两个地方/etc/modules和/etc/modules-load.d/*.conf。/etc/modules是老式配置文件很多发行版还在用/etc/modules-load.d/是 systemd 时代推荐的目录方式每个配置文件一行一个模块名比如# /etc/modules-load.d/demo_drv.conf demo_drv系统启动时systemd 的systemd-modules-load.service会读取这个配置并执行 modprobe。这种方式的好处是简单直接、完全可控、不依赖设备树适合那些设备树暂不支持或者设备本身不具备热插拔属性的模块。但这种方式的缺点也很明显它是“无脑加载”不管硬件是否存在模块都会先进内核。如果硬件不存在模块 init 函数如果写了严格的资源检测可能会失败退出留下错误日志如果模块写法不规范甚至可能导致初始化时访问不存在的硬件寄存器直接挂掉。所以我在实际项目中通常只在设备树匹配暂时不可用或者模块加载本身没有副作用时才用这种方式。2.3 设备出现才加载MODULE_ALIAS udev 触发链热插拔场景下的自动加载依赖一条完整的触发链。以 USB 设备为例物理设备插上后USB 控制器会检测到连接内核的 USB core 枚举出新设备生成一个device对象并广播 uevent。udev 在用户态收到 uevent 后会从设备对象上读取 modalias 字符串然后根据这个字符串去模块别名数据库里找对应的模块。这里的关键就是MODULE_ALIAS。一个驱动如果支持某个厂商和产品 ID可以在代码里声明MODULE_ALIAS(usb:v1234p5678d*dc*dsc*dp*ic*isc*ip*in*);depmod 会把这些 alias 解析出来写入modules.alias文件。当 udev 拿到设备 modalias 后调用 modprobemodprobe 去modules.alias里根据通配符匹配模块名然后加载对应的驱动。这一整个链路合在一起才实现了“设备一插上驱动就自动加载”的完整效果。对于 PCI、SDIO、I2C 等不同总线原理类似只是 modalias 的字段格式不同。写设备驱动时MODULE_DEVICE_TABLE宏就是为了让 depmod 能从模块中提取设备表信息并生成 alias。这也是为什么我建议每个可热插拔驱动的id_table后面都跟着写一行MODULE_DEVICE_TABLE。static const struct usb_device_id demo_usb_ids[] { { USB_DEVICE(0x1234, 0x5678) }, { }, }; MODULE_DEVICE_TABLE(usb, demo_usb_ids); MODULE_ALIAS(usb:v1234p5678d*dc*dsc*dp*ic*isc*ip*in*);不过要小心MODULE_DEVICE_TABLE宏本身在编译时已经能产生 alias手动写MODULE_ALIAS往往是为了覆盖一些内核自动生成不了的场景比如平台设备或自定义总线设备。2.4 固件自动加载很多人忽略的另一类“加载”驱动自动加载话题里固件firmware自动加载经常被讲到一半就被跳过去但实际调试时它才是真正让人头大的部分。很多外设芯片内部还有一个独立的微控制器或者 DSP驱动需要把一段固件二进制上传给设备设备才能工作。这块固件一般放在/lib/firmware/目录下。驱动通过request_firmware()向内核申请固件内核发现文件不在内存里就会生成一个 uevent通知 udev 去加载固件文件加载成功后返回给驱动使用。这里有三个非常隐蔽的坑第一固件文件必须在/lib/firmware/或内核配置的CONFIG_EXTRA_FIRMWARE_DIR路径下文件名必须和 request_firmware 请求的完全一致一个字符都不能错。第二如果你的文件系统是 initramfs 启动模式而且固件在根文件系统分区的/lib/firmware里此时根分区还没挂载固件请求就会失败。这时候必须把固件也放进 initramfs。第三某些驱动用了request_firmware_nowait()异步请求它不阻塞驱动 probe所以你看 dmesg 时 probe 已经成功了但是设备其实一直在等待固件。这种异步加载的好处是驱动能快速返回坏处是调试时信息不直观容易误判。不管怎样固件自动加载其实是“用户态 udev 内核请求固件机制”配合完成的一个跨层流程跟之前的模块加载链很相似。理解了这个你再看自动加载就不只是“modprobe 一下”这么简单了。3. 动手实现让字符设备驱动开机自动加载3.1 写一个能跑起来的 platform 驱动光讲原理不行这里我直接放一个我在项目里常用的模板它包含 platform 驱动、设备树匹配、misc 设备注册三部分在很多内核版本上都能直接编译跑通。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/miscdevice.h #include linux/fs.h static int demo_dev_open(struct inode *inode, struct file *file) { return 0; } static const struct file_operations demo_dev_fops { .owner THIS_MODULE, .open demo_dev_open, }; static struct miscdevice demo_misc_device { .minor MISC_DYNAMIC_MINOR, .name demo_dev, .fops demo_dev_fops, }; static int demo_drv_probe(struct platform_device *pdev) { int ret; dev_info(pdev-dev, demo driver probe, device name: %s\n, pdev-name); ret misc_register(demo_misc_device); if (ret) { dev_err(pdev-dev, misc register failed: %d\n, ret); return ret; } return 0; } static int demo_drv_remove(struct platform_device *pdev) { misc_deregister(demo_misc_device); return 0; } static const struct of_device_id demo_of_match[] { { .compatible vendor,demo-device }, { }, }; MODULE_DEVICE_TABLE(of, demo_of_match); static struct platform_driver demo_platform_driver { .probe demo_drv_probe, .remove demo_drv_remove, .driver { .name demo_drv, .of_match_table demo_of_match, }, }; module_platform_driver(demo_platform_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name youexample.com); MODULE_DESCRIPTION(Demo driver for automatic loading);这里有个细节想提醒你misc_register是注册一个杂项设备注册成功后会自动生成/dev/demo_dev节点而且 misc 子系统会动态分配主设备号省去了手动分配设备号、创建 device class 的繁琐步骤。对驱动原型验证来说misc 设备是最省事的方式。module_platform_driver这个宏也很关键它把module_init和module_exit都封装好了等价于module_init(demo_platform_driver_init); module_exit(demo_platform_driver_exit);宏内部会生成两个静态函数init 时调用platform_driver_registerexit 时调用platform_driver_unregister。3.2 编译、部署与手动验证编译内核模块的 Makefile 极其固定不需要过多解释obj-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如果是在开发板的交叉编译环境需要设置 CROSS_COMPILE 和 ARCH 变量指向对应的工具链比如make ARCHarm CROSS_COMPILEarm-linux-gnueabihf-编译完成后把demo_drv.ko拷贝到目标机放到内核模块的标准目录sudo cp demo_drv.ko /lib/modules/$(uname -r)/extra/ sudo depmod -aextra目录是内核专门给第三方模块预留的位置depmod 会扫描整个/lib/modules/$(uname -r)/包括extra子目录。执行完 depmod 之后modprobe 才能找到这个模块。先手动验证一下模块能不能正确加载sudo modprobe demo_drv dmesg | tail ls -l /dev/demo_dev如果 dmesg 里看到demo driver probe, device name: demo_drv再看到/dev/demo_dev出现说明驱动本身工作正常。如果 dmesg 里没有 probe 日志但 modprobe 也没有报错说明模块 init 注册成功了但没有匹配到任何平台设备——这时候大概率是设备树方面的问题后面会专门讲。3.3 配置开机自加载驱动文件放进extra目录并 depmod 之后就可以配置开机自动加载了。最简单的办法是创建/etc/modules-load.d/demo_drv.confecho demo_drv | sudo tee /etc/modules-load.d/demo_drv.conf然后重启用lsmod | grep demo_drv检查。如果看到了模块说明配置生效。这里我要提醒一下模块加载成功后还需要确认 probe 是否执行了。模块注册成功不等于设备匹配成功也可能模块加载进去了但因为设备树 match 不到设备probe 根本不会被调用。所以在查看结果时我习惯用下面这个组合拳lsmod | grep demo_drv cat /proc/devices | grep demo ls -l /sys/bus/platform/drivers/demo_drv/ dmesg | grep demo这样能分辨模块加载、设备节点、驱动绑定、probe 日志四个层面的状态。3.4 设备树匹配的实战要点如果目标平台是嵌入式 ARM 板或者 RISC-V 板推荐用设备树匹配来代替 modules-load.d。设备树匹配的好处是设备在驱动就在设备不在驱动静默不加载干净利落。设备树节点写法demo_dev: demo-device { compatible vendor,demo-device; status okay; };这个节点放在哪个位置取决于你的设备实际挂在哪个总线上。如果是独立外设可以直接放在根节点下如果挂在某个 I2C 控制器上就应该放在对应 i2c 节点下并加reg和#address-cells等属性。DTS 编译后在 bootloader 里通过设备树传给内核。内核在启动阶段会遍历设备树凡是 compatible 和某个驱动 of_match_table 匹配的节点都会生成平台设备并触发 probe。这个过程完全不需要用户态参与属于“内核态自动加载”。日常调试时你会经常遇到一个现象dmesg 里完全看不到 probe 日志但模块加载也没报错。这时候我一般先检查两件事第一设备树里 compatible 的字符串是不是跟驱动of_match_table里的完全一致包括大小写和厂商前缀。一个最容易犯的错是重复定义节点导致内核只认第一个节点。第二确认设备树真正被打进内核并生效用下面的命令读取运行时的设备树状态ls /proc/device-tree/demo-device/ cat /proc/device-tree/demo-device/compatible如果/proc/device-tree/下根本没有这个节点说明 bootloader 传给内核的设备树里没有问题就出在 DTS 编译或 bootloader 配置环节而不是驱动本身。3.5 把驱动塞进 initramfs根文件系统还没挂载时怎么办有一个场景 modules-load.d 和设备树匹配都帮不上忙根文件系统还没挂载但驱动已经需要就位了尤其是根文件系统所在存储介质的驱动。拿最常见的 U 盘启动系统来举例内核要读取根文件系统但 U 盘对应的 USB 存储驱动还没有加载根本读不了根分区——典型的“先有鸡还是先有蛋”问题。这种场景的解法是 initramfs初始内存文件系统。initramfs 是一个小型文件系统镜像里面的文件被内核解压到内存中作为临时根文件系统。启动早期阶段内核先把 initramfs 中的驱动加载好然后挂载真正的根文件系统再切换到真正的根。把驱动加进 initramfs 的通用步骤如下以 Debian/Ubuntu 的 update-initramfs 为例echo demo_drv | sudo tee /etc/initramfs-tools/modules sudo update-initramfs -u这条命令会把/etc/initramfs-tools/modules中列出的模块打进 initramfs。之后可以用lsinitramfs检查lsinitramfs /boot/initrd.img-$(uname -r) | grep demo_drv对于使用mkinitcpio的 Arch 系发行版配置在/etc/mkinitcpio.conf的MODULES数组里MODULES(demo_drv) sudo mkinitcpio -P嵌入式环境则取决于你用的构建系统。Buildroot 可以在 kernel module 列表中指定模块Yocto 则通过 IMAGE_INSTALL 或 INITRAMFS 相关的变量处理。但思路是完全一致的把需要的 .ko 文件放进 initramfs并在 initramfs 的加载脚本里 modprobe 这些模块。实际项目里我把“自动加载”的解决方案按优先级排成这样设备树能匹配的平台设备 → 优先走设备树匹配普通外设模块无特殊时序要求 → modules-load.d 固定加载需要提前于根文件系统的模块 → initramfs 预加载热插拔 USB/PCI 设备 → MODULE_ALIAS udev 触发这样分层设计自动加载的问题基本能全部覆盖。4. 排查自动加载失败的实战记录4.1 问题定位思路先看哪个环节断了自动加载的链路比手动 insmod 长得多出问题时最关键的是先判断“断在哪一环”。我一般把这条链拆成四段模块文件层模块是否存在于标准目录、depmod 是否生成映射加载执行层modprobe 是否被调用加载时报什么错设备匹配层驱动是否成功注册设备是否成功注册两者是否都在同一总线用户态交互层设备节点是否创建权限是否正常相应服务有没有收到 uevent要快速定位最好用的就是dmesg加关键字过滤。如果 dmesg 里连模块 init 的信息都没有说明模块根本没被加载问题大概率在加载执行层往 modprobe、udev 方向查。如果模块加载了但没有 probe 的信息问题在设备匹配层往设备树、bus 匹配方向查。如果 probe 执行了但/dev下没有节点问题在设备模型层可能是 misc 注册失败或 device_create 位置不对。这里有一套我常用的排查组合直接复制到终端跑dmesg -w先开一个终端挂在日志输出上然后执行加载操作这样能实时看到内核产生的每一条日志比事后翻 dmesg 更直观。同时用udevadm monitor查看 uevent 事件udevadm monitor如果设备节点没有出现再检查 udev 规则是否生效。嵌入式系统里有时 udev 规则文件名称的优先级也很关键数字小的规则先执行ls /etc/udev/rules.d/4.2 高频问题速查表我把这些年遇到的高频问题整理成了表格每个问题都对应了排查方向和解决办法现象根本原因排查/解决办法modprobe 提示 module not founddepmod 没执行或模块不在标准目录先确认 .ko 在/lib/modules/$(uname -r)/extra/然后执行depmod -a模块加载成功但没有 probe 日志设备树 compatible 不匹配或设备未注册检查of_match_table和设备树 compatible用ls /proc/device-tree/确认节点生效insmod 时报 unknown symbol依赖模块未先加载改用 modprobe或者检查modules.dep是否生成modprobe 报 Operation not permittedSecure Boot/模块签名校验失败看 dmesg 中是否有 signature 相关报错可关闭 Secure Boot 或为模块签名/dev 下没有设备节点misc 注册失败或设备节点被 udev 规则隐藏查 dmesg 中 misc 注册返回值检查/etc/udev/rules.d/是否有相关规则固件请求失败/lib/firmware 下文件缺失或 initramfs 中缺乏固件用ls /lib/firmware/核对文件名用lsinitramfs检查 initramfs模块加载顺序错误模块间有依赖但 modules-load.d 配置了错误顺序用 modprobe 自动处理依赖或把依赖模块排在前面设备节点权限不足默认 root 权限普通用户打不开创建 udev 规则设置 group 和 mode比如MODE0666这些坑我基本都亲自踩过其中最阴险的是 Secure Boot 问题。有时候你在开发机上怎么测都正常换一台有 Secure Boot 的机器就加载失败而且报错信息被 systemd 日志吞掉只显示 modprobe 失败。这时候一定要去 dmesg 里找module verification failed或者signature关键词一眼就能确认。4.3 我常用的三层验证流程排查到最后还是要回到验证。我建议每次配置完自动加载都按下面三个层次逐一验证缺一不可。第一层模块已加载lsmod | grep demo_drv cat /proc/modules | grep demo_drv这一层确保模块二进制真的被内核接受并注册了。第二层驱动已绑定设备ls -l /sys/bus/platform/devices/ ls -l /sys/bus/platform/drivers/demo_drv/ cat /sys/bus/platform/devices/demo-device/uevent/sys/bus/platform/drivers/demo_drv/目录下如果出现了设备符号链接说明驱动与设备已经完成绑定probe 成功。第三层用户态设备节点可用ls -l /dev/demo_dev echo test /dev/demo_dev能写入说明设备节点正常驱动 open 函数工作正常。到这里一条完整的驱动自动加载链才算真正落地。三层都过了自动加载才是真正没问题了。很多朋友只看lsmod有模块就以为大功告成结果设备节点根本没生成业务程序跑起来才发现驱动是“空转”状态白高兴一场。最后聊聊我在这块的经验驱动自动加载的坑往往不在加载本身而在加载的“前置条件”。我见过太多案例驱动代码写得一点问题没有结果卡在设备树没更新、模块目录不对、initramfs 忘打补丁这些周边环节。所以每次交付前我都会在干净的开发板上完整做一次断电重启验证确认驱动在没有任何人为干预的情况下能够自动就位。还有一个小技巧想分享给你自动化验证时可以写一个启动检查脚本开机后自动检查模块、probe 日志和设备节点三样东西有任何一项缺失就输出明确的错误信息。这样出了问题能第一时间定位不用每次手忙脚乱地敲命令排查。另外如果你们项目里模块升级频繁记得维护好 depmod 和 initramfs 的自动更新流程否则很容易出现升级了 .ko 文件结果系统加载的还是旧模块版本的诡异情况。驱动自动加载这门手艺看着简单真正做扎实需要你把内核、设备树、文件系统、用户态服务这些层面全部串起来。
返回列表