Linux开机自动加载驱动模块:modprobe、udev与initramfs解析

发布时间:2026/9/30 8:03:08
Linux开机自动加载驱动模块:modprobe、udev与initramfs解析 前几天帮人看一台跑隧道业务的机器重启之后接口起不来登上去lsmod | grep tun光秃秃的手动modprobe tun立马恢复。问题不在命令本身而在于重启之后没有任何东西替它执行那条命令。Linux 启动的时候自动加载驱动模块听起来就是往配置文件里写一行模块名的事但真正落到生产机器上踩的坑散落在好几个完全不同的层面谁负责加载、在启动的哪个阶段加载、加载失败之后你能不能第一时间看见。我在 Debian、Ubuntu、CentOS、Rocky 这几类系统上都反复折腾过这一套有些坑是所有发行版通用的有些则是发行版自己的历史包袱。这篇就把整条链路从头到尾拆一遍从手动加载和开机加载的本质差别讲起一直讲到配置写完还是不生效时该怎么一步步定位刚接触驱动模块的人照着做也能跑通。1. 开机没加载模块的现场三种自动加载根本不是一回事1.1 从 lsmod 和 dmesg 开始的第一轮确认发现模块没上来的第一个动作永远是lsmod它读的是/proc/modules能看到当前内核里已经加载的模块名、占用内存大小、被引用次数以及依赖它的模块列表。这里有个容易误判的点如果某个驱动是编译进内核镜像的built-in你在lsmod里永远看不到它因为 built-in 的代码在内核初始化阶段就已经在内核地址空间里了跟模块这套机制没关系。所以只靠lsmod判断驱动有没有工作是不完整的正确做法是配合/sys/module/目录一起看lsmod | grep -w tun ls /sys/module/ | grep -i tun dmesg | grep -i tun/sys/module/下每一个目录都对应一个已经初始化的模块或者 built-in 组件只要目录在说明代码已经注册进内核。dmesg则是判断加载之后有没有真正 probe 成功的关键模块被modprobe拉起来只是把它塞进内核并执行了 init 函数驱动能不能绑定到具体硬件还得看 probe 的结果。很多人的困惑就卡在这一步lsmod里明明有模块设备节点却不存在一看dmesg才发现 probe 返回了-ENODEV或者被-EPROBE_DEFER挂起重试了。我一般会把排查顺序固定成三步lsmod确认模块有没有在内核里dmesg确认 probe 有没有成功/sys/module/name/parameters/确认参数是不是按预期传进去了。这三步跑完问题基本能定到具体环节。1.2 udev 热插拔、modules-load、initramfs 三条路径的边界开机自动加载这句话其实被三种机制共享它们触发的时间点和触发条件完全不同混在一起想就永远理不清。第一种是udev 的自动加载。内核在探测到硬件时会把这个设备的总线类型、厂商 ID、设备 ID 拼接成一个MODALIAS字符串通过 netlink 发一个 uevent 到用户空间。udev 收到之后去查modules.alias文件找到匹配的模块名就调用modprobe把它拉起来。这条路径是设备驱动设备的逻辑只有匹配上 alias 才会触发服务器上那些靠 ACPI 上报、没有实际热插拔事件的板载设备有时候就走不通。第二种是显式列表加载也就是 systemd 的modules-load.d机制Debian 老系统里的/etc/modules属于同一类。它不关心有没有硬件开机就按名单一个个modprobe适合那些没有设备别名、或者你就是要它无条件存在的模块比如tun、nf_conntrack、br_netfilter、vfat这类纯软件功能模块。第三种是initramfs 阶段加载。根文件系统还没挂载之前内核只能用内存里那个极小的根文件系统也就是 initramfs。如果你的根分区在 NVMe 上、在 LVM 里、在加密卷里那对应的驱动必须先被塞进 initramfs否则内核连根都找不到直接 kernel panic。这个阶段和前面两个阶段的时间差可能是几百毫秒也可能是几秒取决于硬件初始化速度。提示判断一个模块到底该走哪条路最直接的问题是它需不需要在根文件系统挂载前就工作。需要走 initramfs不需要但当设备出现时应该自动上走 udev不需要但必须常驻走 modules-load.d。2. modprobe 和 insmod 的差别决定了配置该往哪里写2.1 insmod 只认路径modprobe 会替你算依赖insmod是最原始的工具参数就是.ko文件的完整路径比如insmod /lib/modules/$(uname -r)/kernel/drivers/net/tun.ko。它做的事情很少读文件、检查格式、调用 init_module 系统调用。它完全不处理依赖如果这个模块依赖别的模块你得自己按顺序先 insmod 那些依赖。modprobe是上层封装它只接收模块名不需要路径也不需要.ko后缀然后去/lib/modules/$(uname -r)/下查modules.dep把依赖树算出来按正确顺序依次加载。它还负责读取/etc/modprobe.d/下的配置处理 alias、options、blacklist、install 这些指令。两者最本质的差别在于modprobe是按名字和策略加载insmod是按文件加载。开机自动加载这件事绝不可能是insmod干的因为路径会因为内核版本变化而变所有系统级机制用的都是modprobe。所以凡是和开机加载相关的配置写的都是模块名不带路径也不带后缀这一点写错了系统连报错都懒得给你。# 正确写模块名 echo tun /etc/modules-load.d/tun.conf # 错误写路径systemd 会找不到这个模块名然后失败 echo /lib/modules/5.15.0-91-generic/kernel/drivers/net/tun.ko /etc/modules-load.d/tun.conf顺带说一个容易被忽略的点模块名里如果有连字符和短横线的区别比如nf_conntrack和nf-conntrackmodprobe内部会做一定的转换容错但配置文件里最好还是按modinfo输出的name字段原样写不要自己想当然。2.2 depmod 生成的 modules.dep 和 modules.alias 是整套机制的地基在/lib/modules/$(uname -r)/下面有这么几个文件modules.dep、modules.dep.bin、modules.alias、modules.alias.bin、modules.symbols它们是depmod命令扫描整个目录下所有.ko之后生成的索引。modules.dep记录的是依赖关系每一行的格式是模块文件路径: 它依赖的模块路径列表。modprobe加载一个模块时先在这里查它的依赖把依赖树做一次拓扑排序然后从叶子节点开始往上加载。modules.alias记录的是设备匹配规则来自驱动源码里MODULE_DEVICE_TABLE宏展开出的字符串格式是alias 匹配模式 模块名。udev 能自动加载模块靠的就是这张表。这两个文件的意义在于它们是从 .ko 文件本身的内容推导出来的不是你手写的。所以当你自己编译了一个驱动模块用insmod挂上去能工作用modprobe却报 module not found八成就是没有跑depmodmodules.dep里没有这个模块的条目。正确流程是# 把编译好的 .ko 拷贝到内核模块目录或者 make modules_install cp mydrv.ko /lib/modules/$(uname -r)/extra/ # 重建索引 depmod -a # 此时 modprobe 才能按名字找到它 modprobe mydrv内核升级之后/lib/modules/新版本/是新的空目录如果第三方模块没有跟着重新编译并depmod开机加载就会失败。这是自研驱动在自动加载上最常见的翻车原因尤其是那些用 DKMS 管理的模块DKMS 在重建时会自己跑 depmod但如果重建失败索引就停在旧状态。2.3 手动能加载不等于开机也能加载这一点值得单独说因为它造成的困惑最多。你在 shell 里modprobe foo成功不代表开机那一刻也成功原因主要有三个。第一个是加载顺序和依赖链的差异。你手动加载时前面的依赖可能已经因为别的原因被加载过了modprobe只需要处理剩下的部分。开机时环境是干净的如果依赖链里某一环因为硬件没就绪而 probe 失败modprobe可能仍然返回成功模块被加载了但设备没起来于是你以为模块加载了实际功能不可用。第二个是配置文件被modprobe.d改写。/etc/modprobe.d/里可能有blacklist foo或者install foo /bin/false这类规则手动modprobe foo时你可能用了-i忽略配置或者干脆是加载的别名而 systemd 走的路径会严格执行这些规则结果就是手动成功开机失败。排查时用modprobe -n -v foo干跑一遍它会把你即将执行的每一步打印出来包括要不要读哪个配置文件、解析成哪个模块这是定位配置冲突最快的手段。第三个是加载时机的差异。开机早期很多子系统还没初始化完成驱动初始化函数里的注册回调可能返回-EPROBE_DEFER内核会把它放进一个延迟重试队列等它依赖的资源就绪后再试。如果资源一直没就绪模块就永远停在那里。手动加载时系统已经跑完了全套初始化流程自然不会遇到这个问题。3. systemd 体系下的标准答案modules-load.d3.1 /etc/modules-load.d/*.conf 的文件格式与命名习惯这是目前所有使用 systemd 的发行版上最正规的做法。目录是/etc/modules-load.d/里面放.conf结尾的文本文件格式极简每行一个模块名空行和以#开头的行被忽略。# /etc/modules-load.d/storage-extra.conf # 用于承载存储相关的附加模块 vfat exfat dm_crypt目录的读取顺序是/usr/lib/modules-load.d/、/run/modules-load.d/、/etc/modules-load.d/同名文件后面的会覆盖前面的/etc优先级最高。这跟 systemd 其他配置目录的约定一致包管理器装进来的默认配置放/usr/lib管理员自己改的放/etc两边不打架。文件名我建议按用途分组而不是按模块名一个一个建文件。见过有人给每个模块单独建一个.conf结果/etc/modules-load.d/里躺着四五十个小文件后来要排查哪个模块是哪个业务加的翻半天。按功能分文件比如k8s-node.conf、storage.conf、security.conf配合文件里的注释半年后还能看懂。注意这个目录里的加载是按文件名字典序、文件内按行序串行执行的但整个 service 是并行的模块本身如果有顺序要求不要靠文件命名去赌用softdep或者干脆写进 initramfs 更稳妥。3.2 Debian 系的 /etc/modules 是怎么被兼容进来的Debian 和 Ubuntu 历史上用的是/etc/modules这个文件格式和现在的.conf一样一行一个模块名。systemd 时代这个文件仍然被保留为兼容入口由kmod包提供的那套机制接管。在同时存在/etc/modules和/etc/modules-load.d/的系统上两边的内容都会被处理所以在 Debian 上你可能会发现我只改了/etc/modules模块也上来了或者反过来我在/etc/modules-load.d/里加了怎么好像有两个地方都有配置。我的建议很明确新系统一律用/etc/modules-load.d/不要往/etc/modules里加东西。原因是前者是 systemd 的跨发行版标准迁移到 RHEL 系或者容器基础镜像时不用重新适应后者是 Debian 的历史遗留未来哪天被彻底废弃你的配置就成了孤儿。如果接手一台老机器发现/etc/modules里有内容迁移的时候注意把里面的模块名完整抄过去别漏了带注释的边界行。3.3 systemd-modules-load.service 报错时的定位手法配置写完之后别急着重启机器先用 systemd 的单元状态和日志确认# 查看服务状态 systemctl status systemd-modules-load.service # 只看本次启动的日志带时间戳 journalctl -b -u systemd-modules-load.service --no-pager # 手动触发一次看即时输出 systemctl restart systemd-modules-load.service journalctl -b -u systemd-modules-load.service -n 50服务失败时日志里会直接点名是哪个文件、哪一行、哪个模块加载失败了比如 Failed to find module exfat 或者 Module foo is blacklisted。这里有个细节systemd-modules-load遇到加载失败的模块会继续处理后面的模块最后返回一个非零状态码所以在 status 里看到failed并不代表所有模块都没上得逐条看日志确认。还有一种情况是服务显示 active模块却不在lsmod里。这通常是因为模块被编译进了内核modprobe对 built-in 模块的处理是静默成功——它发现/sys/module/foo已经存在就直接返回 0什么都不做。这种情况你用journalctl是看不到任何线索的只能靠modinfo foo看它有没有filename字段来判断。4. udev 自动加载那条链设备一插模块自己就上来了4.1 MODALIAS 是怎么和 modules.alias 对上的内核探测到设备后会调用总线的uevent回调生成一个环境变量MODALIAS内容形如pci:v00008086d000015B8sv00001028sd00000700bc02sc00i00。这串东西是总线名加一系列key:value对的拼接比如v是 vendor idd是 device idsv/sd是 subsystem vendor/devicebc/sc/i是 class、subclass、interface。驱动的源码里如果有MODULE_DEVICE_TABLE(pci, my_pci_id_table)这样的声明编译时就会在.ko里生成一段 alias 信息depmod扫描时把它抽出来写进modules.alias格式大致是alias pci:v00008086d000015B8sv*sd*bc*sc*i* my_driverudev 收到 uevent 之后拿MODALIAS的值去modules.alias里做匹配匹配上就modprobe那个模块。整个链路里*是通配符v和d之外的字段如果驱动没声明就用*兜底所以匹配逻辑是比较宽松的。手工验证这条链最直接的方法是# 查看某设备的 modalias cat /sys/bus/pci/devices/0000:00:1f.6/modalias # 用 modprobe 按 alias 匹配不会真的加载只显示匹配结果 modprobe -R pci:v00008086d000015B8sv*sd*bc*sc*i* # 或者更精确地直接用设备路径触发 udevadm test /sys/class/net/eth0udevadm test会把 udev 处理这个设备的完整过程打印出来包括它查了哪些规则、算了什么 alias、最终要不要加载模块。这是排查设备插上了模块为什么不自动加载的第一工具比猜快得多。4.2 自研驱动补 alias 的实操写法自己写的驱动如果要享受 udev 自动加载必须在源码里声明设备表否则depmod抽不出 aliasmodules.alias里没有条目udev 自然匹配不上。以平台设备或者字符设备为例很多时候驱动并不挂靠在标准总线上那就没有现成的MODULE_DEVICE_TABLE可用这时候有两个办法。第一个是在modprobe.d里手写 alias 规则# /etc/modprobe.d/mydrv.conf alias char-major-240 mydrv alias my-special-device mydrvmodprobe在解析模块名时会先查 alias 表把my-special-device解析成mydrv然后加载它。配合 udev 规则可以在设备出现时主动触发# /etc/udev/rules.d/99-mydrv.rules ACTIONadd, SUBSYSTEMmisc, KERNELmydrv, RUN/sbin/modprobe mydrv第二个办法是驱动注册 misc 设备或 platform driver 时用MODULE_ALIAS(misc:mydrv)显式声明 alias这样depmod之后modules.alias里就会有对应条目udev 的通用规则就能处理不需要额外写 udev 规则。需要提醒的是udev 规则里执行RUN是在设备事件处理过程中同步跑的如果modprobe耗时太长会阻塞 udev 事件队列进而影响其他设备的事件处理。对于大型驱动更推荐用ENV{MODALIAS}的方式让 udev 走标准加载路径而不是自己RUN。4.3 冷插拔设备开机加载失败的典型原因冷插拔设备指的是开机时就已经在的总线设备比如板载网卡、主板上的 SATA 控制器。这些设备在系统启动时会被总线枚举理论上也应该产生 uevent但在实际系统里很多机器上的 ACPI 枚举出来的设备并不会发完整的 hotplug uevent或者 udev 在早期 initramfs 阶段处理了一次、切到真实根之后就不再重复处理。这就导致一个很典型的场景某个板载设备对应的模块用 udev 规则依赖ACTIONadd永远触发不了因为那个 add 事件在 initramfs 里就已经发生过了。解决办法通常有两种一是把模块写进modules-load.d让它无条件加载二是把RUN/sbin/modprobe mydrv改成不带条件或者匹配SUBSYSTEM的通配规则让它在冷插拔场景下也能被触发。另外内核参数udev.log_leveldebug可以在排查 udev 早期行为时打开详细日志但这个参数对启动速度有影响排完记得去掉。5. initramfs 阶段根文件系统还没挂谁来加载模块5.1 哪些模块必须在 initramfs 里就位initramfs 是内核启动时加载到内存里的一个微型根文件系统里面的内容通常是一个 cpio 归档包含必要的/bin、/sbin、内核模块和一套启动脚本。它的使命很短找到真正的根文件系统、挂载它、然后把控制权交出去。如果根文件系统所在的存储设备依赖某个驱动才能访问那这个驱动就必须在 initramfs 里。典型的需要进 initramfs 的模块包括NVMe 控制器驱动、RAID 控制器驱动、LVM 的dm-mod、设备映射相关模块、文件系统模块ext4、xfs、btrfs、加密卷相关的dm-crypt和加密算法模块、以及某些网络启动场景下的网卡驱动。判断方法很实在看当前系统启动时lsinitramfs列出来的模块列表对照lsmod找出你在用但没进 initramfs 的模块。如果某个模块是根分区挂载的必经之路却没进去那这台机器一旦内核更新、initramfs 重建之后就可能起不来。# Debian/Ubuntu 查看 initramfs 内容 lsinitramfs /boot/initrd.img-$(uname -r) | grep \.ko # RHEL 系 lsinitrd /boot/initramfs-$(uname -r).img | grep \.ko5.2 Debian 与 RHEL 两条更新路径的差异Debian 和 Ubuntu 用的是initramfs-tools配置入口是/etc/initramfs-tools/modules文件一行一个模块名。改完之后必须跑update-initramfs -u重新生成当前内核的 initramfs否则改了等于没改。还有/etc/initramfs-tools/conf.d/可以放一些额外配置比如MODULESmost还是dep前者会把大部分模块都塞进去后者只塞探测到的必需模块。生产服务器为了稳一般选most代价是 initramfs 体积大一些。RHEL、CentOS、Rocky 这些用的是dracut配置入口是/etc/dracut.conf.d/下的.conf文件用force_drivers mydrv 强制加入模块或者用add_drivers mydrv 。改完跑dracut -f重建当前内核的 initramfs也可以指定输出文件。dracut 的模块化程度比 initramfs-tools 高它自己有一套 dracut module 机制但对我们来说用 force_drivers 就够了。发行版家族工具配置文件重建命令查看内容Debian/Ubuntuinitramfs-tools/etc/initramfs-tools/modulesupdate-initramfs -ulsinitramfsRHEL/CentOS/Rockydracut/etc/dracut.conf.d/*.confdracut -flsinitrd通用临时内核参数无重新引导无5.3 改了配置忘记更新 initramfs 的经典翻车这是我最想强调的坑因为它造成的后果是机器直接起不来而不是某个功能不可用。流程是这样的你在/etc/initramfs-tools/modules里加了一个模块觉得配置改完了然后过几天内核升级包管理器自动重建 initramfs这次重建会把你的配置带进去但如果你的配置本身写错了模块名或者模块依赖没满足重建时可能只打印一个 warning 就过去了生成的 initramfs 在启动时找不到那个模块于是卡在挂载根文件系统那一步。更隐蔽的一种情况是你手动改了配置但没重建 initramfs然后因为别的原因手动执行了dracut -f或update-initramfs -u把错误配置一起烧进去了重启之后才发现起不来。所以在生产机器上改 initramfs 相关配置我固定会做三件事改之前备份当前能启动的 initramfs 文件改之后在测试机或者同一批的某台机器上先验证一遍然后用lsinitramfs确认模块真的在归档里。恢复手段也要提前准备好手边常备一个 U 盘启动盘能进救援模式挂载根分区把 initramfs 换回备份文件或者临时在引导菜单里加rd.break/breakpremount打断启动流程进去检查。这些操作平时用不上真出事的时候能救命。6. modprobe.d 里的参数、黑名单与顺序控制6.1 options 传参与 softdep 的用法/etc/modprobe.d/目录下可以放任意.conf文件modprobe每次执行都会读取它们。常用指令有四种alias定义别名映射options给模块传参数blacklist屏蔽模块install/remove用自定义命令替换默认的加载和卸载动作。options最常用格式是options 模块名 参数名值 [...]。比如给网卡驱动指定多队列参数# /etc/modprobe.d/net-tuning.conf options ixgbe RSS8,8 options nf_conntrack hashsize65536模块参数同样可以通过内核命令行传给 built-in 或者早期加载的模块格式是模块名.参数名值这一点在调试时很有用因为内核命令行优先于modprobe.d。softdep用来声明模块之间的软依赖让modprobe在加载某个模块前先加载指定的前置模块可以带pre:和post:两种前缀softdep mydrv pre: crc32c_generic post: mydrv_helper这跟硬依赖的区别在于软依赖不会写进modules.dep只在按名字加载时生效适合那些依赖关系不是编译期决定的场景。用它的代价是隐式别人看代码找不到这条关系所以我一般只在硬依赖解决不了的时候才用。6.2 blacklist 到底能不能彻底禁用blacklist指令的效果被很多人高估了。它的真实作用是在 alias 解析阶段屏蔽这个模块阻止硬件自动匹配把它拉起来。也就是说它挡的是 udev 那条自动加载路径不挡显式的modprobe 模块名。# 只是阻止自动加载 blacklist pcspkr # 这才是真正的拒绝加载任何路径都挡 install pcspkr /bin/false所以如果你发现有些系统上明明写了 blacklist模块还是出现在lsmod里不用惊讶——很可能是另一个模块通过硬依赖把它拉起来了或者某个脚本显式加载了它。真正要禁掉用install 模块 /bin/true返回成功但不做事或者/bin/false返回失败再配合blacklist双管齐下。反过来如果某个模块被发行版默认 blacklist 了你想恢复可以在/etc/modprobe.d/里加一个优先级更高的文件用alias重新映射回来或者直接显式加载。还有一个细节/etc/modprobe.d/的读取顺序是按文件名字典序后读的覆盖先读的文件名用两位数数字前缀是常见做法比如10-blacklist.conf、50-custom.conf、99-override.conf这样意图一目了然。6.3 模块签名与安全启动导致的静默失败开了 Secure Boot 的机器上内核只允许加载带有效签名的模块。发行版自带的模块有签名第三方编译的模块包括 DKMS 编译出来的默认没有加载时会失败日志里通常能看到类似 Loading of unsigned module is rejected 或者 Required key not available 的字样。# 查看模块签名信息 modinfo mydrv | grep -E sig_id|signer|sig_key # 查看内核日志里的拒绝信息 dmesg | grep -i -E reject|signature|key这类失败最麻烦的地方是它在modprobe的返回码层面可能被处理得不够直白尤其是走 udev 自动加载的时候失败信息只在dmesg里一闪而过你在journalctl -u systemd-modules-load里可能什么都看不到。排查时的第一动作永远是dmesg | tail -50另外journalctl -k -b也能看到内核环形缓冲区的完整内容。处理办法有几个方向给模块做签名并把公钥导入内核的信任链需要提前在 Secure Boot 的 key 管理里登记机器重启后进 firmware 界面操作关闭 Secure Boot或者把驱动改成 built-in 编进内核。生产环境里我一般选择第一条因为关 Secure Boot 涉及安全策略改内核配置又会影响升级流程。7. 自研驱动做成模块还是编进内核一次项目里的取舍与验证清单7.1 built-in 与 module 的初始化时机差异编进内核的驱动在do_initcalls阶段执行按core_initcall、postcore_initcall、arch_initcall、subsys_initcall、device_initcall这几个等级从高到低跑。模块的 init 函数则统一在device_initcall之后被调用也就是说模块的初始化整体晚于 built-in。这个差异带来的实际影响是如果 A 驱动是 built-inB 驱动是模块且 B 依赖 A 提供的符号或服务那 B 在加载时 A 一定已经就绪顺序天然正确。反过来如果 A 是模块、B 是 built-inB 在初始化时可能找不到 A需要靠-EPROBE_DEFER来延迟重试。所以对于启动早期就必须可用、且被其他组件依赖的驱动built-in 是更省心的选择。代价是 built-in 不可卸载、增大内核镜像、参数只能通过内核命令行传、调试时改一行要重编整个内核或者说至少重编并重启。在快速迭代阶段模块化的开发体验好太多所以常见做法是开发期用模块产品化之后再决定要不要改 built-in。7.2 上线前我固定要跑的验证清单自研驱动从手动能加载走到开机稳定加载中间还有一段路。我把每次交付前固定要跑的检查列成清单按顺序执行基本不会漏modinfo mydrv确认filename存在、depends符合预期、alias里包含你声明的设备匹配串。把.ko放到/lib/modules/$(uname -r)/extra/或者更规范的子目录执行depmod -a然后grep mydrv /lib/modules/$(uname -r)/modules.dep确认索引里有它。modprobe -n -v mydrv干跑确认加载路径上没有黑名单或 install 规则拦截。modprobe mydrv真实加载lsmod确认在内核里ls /sys/module/mydrv/parameters/确认参数目录存在。dmesg | tail -30看 probe 结果重点确认没有-ENODEV、-EIO、-EPROBE_DEFER这类返回。把模块名写进/etc/modules-load.d/systemctl restart systemd-modules-load.servicejournalctl -b -u systemd-modules-load.service确认没有报错。如果需要早期加载写进 initramfs 配置重建后lsinitramfs确认归档里有这个.ko。真正的重启验证至少两次一次普通重启确认冷启动行为一次systemctl soft-reboot或者带kexec的重启确认热路径行为一致。第 8 步我见过太多人跳过结果在测试机上一切都好上生产之后因为 BIOS 设置不同、设备枚举顺序不同模块加载失败。冷启动和热重启在硬件枚举上是有差别的尤其是那些依赖 PCI 枚举顺序、依赖固件加载的驱动两次重启都跑一遍才算稳。最后分享一个我自己用了很久的小习惯在/etc/modules-load.d/的每个文件头写清楚谁加的、什么时候加的、解决什么问题并且在驱动的加载验证脚本里把dmesg的关键行抓出来存到/var/log/下的独立文件。模块加载失败最讨厌的地方是它经常静默通过有日志可查后面接手的人能省几个小时。