1. 模块加载顺序的核心逻辑内核怎么决定“谁先上”1.1 模块到底是什么为什么顺序会翻车很多刚接触Linux内核的人容易把“内核”想成一个单一庞大的程序但实际上现代Linux内核更像一个“核心调度台 一堆随时可插拔的零件”。这些零件就是内核模块以.ko文件存放在/lib/modules/$(uname -r)/目录下。比如你插一个U盘usb_storage模块被拉起来你换了一块网卡对应的驱动模块被拉起来。内核本身可以独立跑起来但缺了这些“零件”硬件就动不了。那顺序到底有多重要我举个很直观的例子有一块存储卡控制器它需要先有pci总线初始化再被ahci模块驱动识别如果ahci在PCI总线机制就绪之前加载驱动就会找不到设备返回No such device。还有的情况更隐蔽模块A提供符号给模块B使用如果B先加载A后加载B就会报Unknown symbol直接加载失败。这类错误在启动阶段尤其讨厌因为启动日志一闪而过等你想起来查系统已经进桌面了。所以模块加载顺序本质上是两件事第一依赖顺序也就是符号依赖B依赖A那A必须先上第二时序顺序也就是硬件初始化顺序总线必须先于设备控制器设备控制器必须先于具体驱动。前者由内核的符号解析机制决定后者更多靠配置和管理工具来约束。理解了这两个维度后面所有配置手段你都能看懂为什么这么设计。1.2 从按下电源到模块加载的完整链条先看完整启动链路不然说顺序都是纸上谈兵。按下电源后BIOS/UEFI 自检然后加载引导程序GRUB之类GRUB 把内核镜像vmlinuz和initramfs初始内存文件系统读进内存接着内核开始初始化核心子系统。注意这个阶段内核只带了一部分编译时内置的驱动真正让硬件跑起来的大量模块是在initramfs阶段加载的。initramfs相当于一个微型用户空间里面包含必要的驱动模块和加载工具如modprobe、udev。它存在的原因是根文件系统可能挂在SATA硬盘、NVMe固态、USB盘上而内核没有内置这些控制器驱动所以先靠 initramfs 把存储控制器驱动拉起来才能挂载真正的根文件系统。这就是为什么initramfs里的模块加载顺序极其关键——挂不上磁盘启动直接卡死或者掉进initramfsshell。挂载根文件系统后PID 1通常由systemd或init充当继续启动服务此时/lib/modules/下所有模块已经可用systemd-modules-load.service会读取静态配置将需要自动加载的模块加载进来。udev负责动态加载发现新硬件就按模块别名自动查找并加载对应驱动。这一整套链条中模块加载顺序受三个层面影响initramfs 内的启动脚本顺序、modprobe 的依赖解析和配置指令、以及运行时动态加载的触发时机。有个容易忽略的细节/lib/modules/$(uname -r)/modules.dep这个文件决定了模块间的依赖关系它由depmod命令生成。加载模块时modprobe会先读取这个文件按依赖顺序先把依赖项加载完再加载目标模块。所以单论符号依赖只要你及时跑过depmod大部分依赖问题内核自己就能处理。真正需要手工介入的是那些存在隐含时序约束或者资源抢占关系的模块。1.3 默认顺序的“隐藏痛点”理论上depmodmodprobe能解决依赖问题但实际工作中遇到的坑往往是“没有直接符号依赖但必须分先后”的情况。举个例子一个字符设备驱动和一个提供DMA内存池的驱动二者没有符号引用关系但字符设备在打开设备文件时需要DMA池已经初始化。这种状态依赖modprobe的静态依赖分析根本看不出来因为二者在modules.dep中没有任何关联。另一种常见痛点是模块被重复加载或者加载时机不对。比如某驱动模块加载时需要读取一个固件文件而固件由另一个模块负责初始化固件加载器二者没有直接依赖但如果前一个模块加载过快固件子系统还没准备好驱动加载虽然成功但硬件功能异常。更糟糕的是这类问题不是每次都复现具有随机性排查起来非常消耗时间。所以我一直觉得模块加载顺序的管理核心不在于“怎么让内核按顺序跑”而在于“怎么把那些只有你知道的隐含约束告诉内核”。这也是下面要讲的几种配置手段存在的真正意义。2. 优先级设置我常用的四种控制手段2.1 modprobe 配置与 softdep最稳的软件依赖控制modprobe的配置文件一般放在/etc/modprobe.d/下以.conf结尾。这个目录里有几种常用配置项alias、options、install、remove、blacklist和softdep。其中softdep就是专门用来描述“软依赖”的它不产生符号依赖而是纯粹的加载顺序控制。softdep语法很简洁softdep mod1 pre: mod2 post: mod3意思是在加载mod1之前先加载mod2在mod1加载完后再加载mod3。如果需要多个前置或后置模块用空格分隔。我在实际项目中常用softdep解决两类问题一是两个驱动存在时序约束但没有符号依赖二是需要在某模块加载前后执行额外操作。install指令更灵活它允许你指定一条命令替代默认的modprobe加载流程。比如install mydriver /sbin/modprobe --ignore-install dependency_drv /sbin/modprobe --ignore-install mydriver这条配置的意思是加载mydriver时先加载dependency_drv再加载mydriver。--ignore-install参数是为了防止递归执行install指令这是很多人容易踩的坑。用install可以实现比softdep更复杂的逻辑比如先检查条件再决定加载哪个变体驱动。我个人的建议是简单的顺序控制优先用softdep因为可读性好只有需要“判断、尝试、回退”这类逻辑时才用install。这里提一个注意点/etc/modprobe.d/的配置只影响modprobe和依赖modprobe的工具加载模块。如果模块是通过内核直接request_module自动加载的比如某些硬件热插拔事件配置依然有效因为request_module最终也会调用modprobe。但如果你在 initramfs 阶段手动用insmod加载模块insmod完全是另一条路它不会读/etc/modprobe.d/也不会处理依赖所以 initramfs 里有自己的一套模块配置体系这也是很多人配置了半天不生效的原因。2.2 initramfs 阶段挂载根文件系统前的生死线initramfs 阶段的模块加载顺序直接决定了你能不能挂上根分区。这一阶段我区分两个主流体系Debian/Ubuntu 系的initramfs-tools和 RHEL/Fedora 系的dracut它们俩的设计思路和配置方法完全不同。在 Ubuntu 下/etc/initramfs-tools/modules文件用来指定需要在 initramfs 阶段加载的模块列表每一行一个模块名支持附带模块参数# /etc/initramfs-tools/modules ahci sd_mod usb-storage修改完执行update-initramfs -u重新生成 initramfs 镜像。这个文件的加载顺序基本就是从上往下所以如果你有特别强的前置依赖可以把被依赖模块写在前面。不过要说明一点initramfs-tools的模块加载脚本本身会先跑一个modules文件列表再跑内置的udev事件处理很多时候设备节点不等人单纯调整文件顺序未必有效更可靠的方案是把前置模块编译进内核见 2.4 节。dracut体系则不同它通过dracut.conf和add_drivers模块名指定需要额外加入 initramfs 的驱动它自己会根据当前系统的模块依赖自动整理顺序# /etc/dracut.conf.d/my-drivers.conf add_drivers ahci raid0 最后执行dracut -f重新生成镜像。dracut的模块排序能力比我见过的其他 initramfs 方案都强它会上机模拟依赖关系但我还是遇到过自定义驱动和内置驱动的加载顺序冲突这种情况下我会在 dracut 的 mount hook 里手工指定modprobe命令这种方法有点粗暴但很有效。另外还要注意一个很多人忽略的问题initramfs是个临时环境它加载的模块目录在/lib/modules/下modprobe依赖的modules.dep文件也是镜像自带的。如果你在 initramfs 阶段手工添加了驱动模块一定要同时更新镜像里的modules.dep否则modprobe可能找不到依赖。update-initramfs -u和dracut -f会在生成镜像时自动处理但如果你手工改镜像内部文件就需要自己维护一致性这很容易出错反正我没少吃过这个亏。2.3 systemd-modules-load 与开机自动加载进入真正的用户空间后systemd会启动systemd-modules-load.service它读取三个目录下的.conf文件来加载模块/etc/modules-load.d/优先/run/modules-load.d/其次/usr/lib/modules-load.d/最后。配置文件内容很简单每行一个模块名# /etc/modules-load.d/br_netfilter.conf br_netfilter这个机制适合那些“无论有没有硬件开机都必须先加载”的内核模块。比如我在做容器网络隔离时经常需要br_netfilter、overlay这类模块在 Docker 启动前就绪直接写在这里最稳妥。但它只是“保证加载”和“保证加载时机”并不能完全解决模块间优先级问题。如果两个模块有先后约束你确实可以把它们按顺序写在同一个配置文件里systemd-modules-load会依次执行。不过它内部的实现是逐条调用modprobe而不是把整个列表一次加载所以这种“顺序敏感”的写法还是能生效的。只不过没有modprobe.d里的配置那么贴近内核依赖机制更像是一种外部约束。从我个人经验来看判断该用哪种方式有一个简单原则如果是内核符号依赖或硬件时序依赖用modprobe.d的softdep或install因为它在模块加载的最底层生效如果是业务层面的“这个模块必须在某个服务启动前就绪”用modules-load.d因为它的语义更贴近服务编排而且和其他 systemd 单元可以配合Before、After使用排查起来更直观。2.4 终极方案把关键模块编译进内核如果你被某个模块的加载顺序折磨得不行还有一个“一劳永逸”的办法直接把模块编进内核而不是编成.ko。编译时对应选项是y而不是m。编进内核的模块不再经过modprobe动态加载机制它的初始化顺序由内核启动时的initcalls机制决定整个模块的核心函数在内核启动早期被自动调用。这个方案的最大优势彻底绕开用户空间的模块加载顺序问题不依赖 initramfs 工具链不依赖 udev 事件。特别适合以下几种情况根文件系统驱动如根分区所在 SATA/NVMe 控制器的驱动、必须极早初始化的硬件如串口调试口驱动、以及那些你实在找不到加载时序问题根因的模块。但是代价也很明显内核镜像体积变大编译时间变长改模块代码必须重编内核才能生效对于树外驱动比如某些硬件厂商只提供.ko编译进内核还得先把源码放进内核树。所以我一般只建议把“最低限度的存储控制器 必要总线驱动”编进内核其他模块还是保留动态加载。还要纠正一个常见误区y不等于不执行初始化它只是把初始化函数的调用提前到内核启动阶段。内核对这类模块也有一套统一顺序主要靠module_init和arch_initcall、subsys_initcall、device_initcall等层级来调度。如果你需要的内核模块初始化时序本身异常把它编进内核并不能改变它和别的子系统的先后关系这一点一定要心里有数。3. 实操给一套真实服务器配置自动加载顺序3.1 先摸清现状看懂 modules.dep 和当前加载情况无论做任何修改第一步都是摸清系统当前的模块依赖和加载状态。因为盲改配置很容易让启动阶段雪上加霜下面这个顺序是我每次排查都要走的流程。先看内核版本和模块目录是否匹配uname -r ls /lib/modules/$(uname -r)/kernel/然后看modules.dep是否和当前内核一致。这个文件是depmod生成的如果手改过 /lib/modules 下的文件或者从别处拷贝过模块modules.dep很可能过期。重新生成一下总没错depmod -a查看当前已经加载的模块和它们的依赖树lsmod modinfo 模块名 modprobe -n -v 模块名 # 不实际加载只打印将要执行的加载动作modprobe -n -v这个组合非常实用它能让你看到这个模块会触发哪些依赖加载以及执行的先后顺序。我经常用它来验证softdep配置有没有生效。比如modprobe -n -v mydriver输出会展示类似insmod /lib/modules/.../dependency_drv.ko、insmod /lib/modules/.../mydriver.ko这样的顺序一目了然。如果输出顺序和预期不符就说明配置没被正确解析先检查文件名后缀是不是.conf、语法有没有写错以及目录权限会不会影响读取。对 initramfs 阶段的问题可以用lsinitramfsUbuntu或lsinitrdRHEL/Fedora查看镜像内部文件确认模块和依赖是否已经被包含在镜像中lsinitramfs /boot/initrd.img-$(uname -r) | grep mydriver lsinitrd /boot/initramfs-$(uname -r).img | grep mydriver3.2 配置一个带依赖顺序的驱动完整示例我拿一个实际场景来走一遍完整配置。假设我有一张硬件加速卡厂商提供了accel_drv驱动模块它需要先加载共享内存池模块shm_pool再加载设备树配置模块hw_config。这三个模块之间没有符号依赖但accel_drv加载时会尝试访问shm_pool分配的内存池如果shm_pool没就绪模块会加载成功但设备无法正常工作。第一步把模块安装到内核目录并更新依赖cp accel_drv.ko shm_pool.ko hw_config.ko /lib/modules/$(uname -r)/extra/ depmod -a注意我放到了extra/目录这是给第三方驱动预留的位置不会和内核自带模块冲突升级内核时也更容易识别。第二步写softdep配置cat /etc/modprobe.d/accel.conf EOF softdep accel_drv pre: shm_pool hw_config EOF第三步验证加载顺序modprobe -n -v accel_drv这里有个细节softdep指定的前置模块和modules.dep里解析出来的硬依赖会合并生效。如果accel_drv本身还依赖某个内核符号modprobe会先把符号依赖模块加载完然后按softdep的pre顺序加载前置模块最后加载目标模块。如果符号依赖模块和pre模块有冲突一般会报错并停止加载所以写softdep之前要先跑modprobe -n -v确认硬依赖的加载序列避免出现矛盾配置。3.3 使用 modules-load.d 实现开机自启如果目标不仅仅是“加载时序正确”还要求“开机立即加载”那就要结合modules-load.d来管了。继续用上面的场景。我不希望accel_drv等到硬件被发现才被 udev 自动加载因为设备的固件初始化流程要求驱动必须在网络服务启动前跑起来所以我把它写进/etc/modules-load.d/accel.conf# 开机即加载 accel_drv模块内部会自动触发前置依赖 accel_drv然后验证systemctl restart systemd-modules-load lsmod | grep accel但我得提醒一个非常容易踩的坑modules-load.d里的模块名加载时走的是modprobe它会尝试解析依赖和别名。如果模块不在系统标准的模块路径里或者没跑depmod -a这里就会加载失败而且 systemd 只是记录一条失败日志不会帮你排查原因。所以我在modules-load.d写任何模块前一定先手工执行一次modprobe 模块名确认成功后再写配置文件。另外如果你用的是/etc/modules老式 sysvinit 风格在 systemd 系统上它依然被兼容读取但我建议新环境一律用modules-load.d因为 systemd 对后者的错误处理、状态查询支持更好排查更容易。顺带说一句如果模块加载失败导致整个systemd-modules-load.service标记为失败后续模块可能不会被加载所以一个错误配置往往会连带影响一批模块这是很多运维同事碰上“开机后好几个模块都没加载”时没意识到的问题。3.4 验证配置是否生效配置完不能只看系统能不能开机还要确认模块真正的加载顺序。常用验证手段有几种dmesg | grep -E accel|shm_pool|hw_config看内核日志里模块初始化函数的调用顺序。注意modprobe加载模块时打印的 “Loading module” 信息不一定代表初始化完成最好看模块内部printk输出的时间戳。journalctl -b | grep -i modprobe看 systemd 日志里 modprobe 的执行记录。如果你在softdep或install指令里加了额外命令这些命令的 stdout/stderr 也会出现在日志中是比较可靠的验证途径。还有一个更底层的验证方式直接看/sys/module/模块名/initstate文件。比如cat /sys/module/accel_drv/initstate如果输出live说明模块加载成功如果显示coming说明正在初始化如果显示going说明正在卸载。这个文件在内核模块生命周期里实时变化脚本判断模块状态比lsmod更准确因为lsmod在模块初始化未完成时可能已经显示存在了。4. 问题排查与常见坑我的实战记录4.1 常见问题速查表我把这些年排查过的模块加载问题整理成了一张速查表遇到类似现象可以直接对照定位现象可能原因排查命令解决方案加载报Unknown symbolmodules.dep过期或依赖模块未加载dmesg | tail、modprobe -n -v 模块名执行depmod -a确认依赖模块加载成功后再加载本模块模块找不到No such file or directory模块路径不对、未安装或/lib/modules/$(uname -r)与当前内核不匹配modinfo 模块名、find /lib/modules -name *模块名*重新安装模块升级内核后重编驱动检查符号链接是否指向正确内核版本Initramfs 阶段挂不上根分区initramfs 缺少存储控制器驱动lsinitramfs查看镜像把驱动加入/etc/initramfs-tools/modulesUbuntu或add_driversdracut重建 initramfs硬件存在但驱动不加载udev 没有触发别名匹配或模块被 blacklistudevadm info、modprobe -n -v 模块名、grep -r blacklist /etc/modprobe.d/确认模块别名是否正确删除 blacklist 配置手动加载并观察 dmesg模块加载顺序不对业务异常隐式时序依赖未被描述journalctl -b | grep modprobe使用softdep描述软依赖或把前置模块放入modules-load.d模块加载成功后设备仍不可用模块参数未生效或固件未就绪cat /sys/module/模块名/parameters/*在/etc/modprobe.d/中写options 模块名 参数值重建 initramfs 并重启systemd-modules-load 服务失败某个模块配置加载失败连带其他模块没加载systemctl status systemd-modules-load手工逐个modprobe确认所有模块可加载修复后重启服务注意表中最后一行的情况特别容易误导人。systemd-modules-load 一旦失败很多新手以为是所有配置都没生效其实它只是按顺序加载到某一行失败了后面的模块没有继续执行。所以排查时一定要手工把modules-load.d里每个模块都试一遍定位到真正的失败项。4.2 没有日志的情况下怎么定位有些客户环境的系统连journald都没开或者启动日志根本没保存这时候排查模块问题就很痛苦。我遇到这种情况会优先靠启动时的输出信息或者干脆加一个串口调试输出。如果只是想确认模块有没有被系统识别到看/proc/modules和/sys/module就够了cat /proc/modules ls /sys/module | grep accel/proc/modules顺序就是模块加载顺序的实时快照从上到下就是加载顺序。在无日志环境里这是最直接的证据。但如果模块加载失败它在/proc/modules中不会出现只能靠外部观察设备是否工作、内核输出是否停住来推断卡在哪一步。我试过最笨但有效的办法改模块源码里的printk输出加上带时间戳的初始化进度打印然后重新编译模块在无日志环境里接串口观察。这个方法虽然原始但能精确定位模块内部卡在哪个函数。对厂商闭源驱动可以在加载前后分别抓/proc/iomem、/proc/interrupts的快照做对比也能推断出模块是否完成了资源申请。4.3 我踩过的几个真实坑第一个坑和blacklist有关。有次我在/etc/modprobe.d/blacklist.conf里屏蔽了一个冲突驱动但忘了那个模块还出现在modules-load.d里。结果就是systemd-modules-load尝试加载它时直接被 blacklist 拦截服务报错后续模块全部没加载。排查了很久才发现是两个配置文件互相打架。后来我给自己立了个规矩任何出现在modules-load.d里的模块都先全局搜索是否被blacklist或者install /bin/false之类的指令拦截。第二个坑是 initramfs 重建后模块依然缺失。我用 Ubuntu 时在/etc/initramfs-tools/modules里加了一个自定义驱动执行update-initramfs -u也成功了但重启后还是找不到设备。后来用lsinitramfs看镜像内容发现模块文件确实在但modules.dep里没有这个模块的依赖记录。原因是我把驱动放在/lib/modules/$(uname -r)/extra/后没有跑depmod -a而update-initramfs不会主动帮你做这件事。从那以后我在改 initramfs 之前一定先执行depmod -a这一条值得写进自己的运维检查清单。第三个坑最有代表性——临时升级内核后模块目录不一致。有些运维同学图省事直接在运行中的系统上编译了新内核但又没有同步编译第三方厂商模块。重启后uname -r变了系统自动加载模块时去的路径是/lib/modules/新版本/里面全是空的自然什么都加载不了。这种问题还特别隐蔽因为系统基本能起来只是硬件设备没了。遇到这种情况要么重新编译第三方模块并安装到新内核目录要么直接用modprobe指定旧版本模块路径临时救急但临时方案只能在测试环境用生产环境还是老老实实重编。最后再分享一个实用小技巧所有和模块加载顺序相关的配置改完之后统一在一条命令里完成“去旧缓存、重建依赖、重建 initramfs”避免因为配置顺序遗漏导致排查时多绕很多弯路depmod -a update-initramfs -u # Ubuntu/deepin 系 depmod -a dracut -f # RHEL/Fedora 系至于层叠的softdep配置我个人的态度是能简单就不要复杂。过度设计配置会让后续接手的人崩溃模块加载顺序的核心不是“把所有模块都锁死在某个顺序”而是“只约束那些真正有先后要求的模块”其余交给内核自身的依赖解析去处理。这是我在多次处理启动问题后最深的体会。
