Linux 基础体系 · 第 31/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 内核模块:加载、参数、依赖、签名、DKMS 和故障恢复

版本与范围: 面向现代主流 Linux 发行版;命令和路径以常见的 systemd、kmoddracutinitramfs-tools 环境为例,发行版差异、权限要求和生产风险会单独说明。


一、先建立整体模型:模块解决什么问题

Linux 内核模块(kernel module)是可以在内核运行期间动态装入或移除的内核代码。最常见的模块包括:

  • 存储控制器、网卡、USB、蓝牙等设备驱动;
  • 文件系统实现;
  • 加密、压缩、虚拟化等内核功能;
  • 网络过滤、审计和安全功能;
  • 第三方驱动,例如 GPU、ZFS、厂商网卡驱动。

一个模块通常是一个 .ko 文件。它不是普通用户态共享库:

  • .so 由动态链接器装入用户进程地址空间;
  • .ko 由内核装入内核地址空间;
  • .ko 必须匹配目标内核的构建接口、配置和符号;
  • 模块拥有内核级权限,错误代码可能直接导致内核崩溃或数据损坏。

模块加载的基本数据流如下:

flowchart TD
    A[用户命令 modprobe/insmod] --> B[kmod 工具]
    B --> C[/lib/modules/<release>/]
    C --> D[modules.dep、modules.alias、modules.symbols]
    B --> E[模块文件 .ko/.ko.xz/.ko.zst]
    E --> F[可选:签名校验]
    F --> G[内核模块装载器]
    G --> H[解析依赖和导出符号]
    H --> I[模块初始化函数]
    I --> J[/sys/module、/proc/modules、设备注册]
    J --> K[udev 设备事件和用户空间命名/权限]

这里有三个容易混淆的层次:

  1. 模块文件存在:例如 /lib/modules/$(uname -r)/kernel/drivers/.../e1000e.ko
  2. 模块能够被内核接受:版本、配置、架构、签名和符号必须满足条件。
  3. 模块加载后设备可用:驱动还必须匹配设备、完成探测,并由设备模型和用户空间继续处理。

因此,“文件存在”不等于“可以加载”,“可以加载”也不等于“设备工作”。


二、内核模块的生命周期

2.1 模块的几个关键状态

从运维角度,一个模块大致经历以下状态:

不存在
  ↓
已编译但未安装
  ↓
已安装到 /lib/modules/<release>/
  ↓
已建立 depmod 索引
  ↓
未加载
  ↓ modprobe/insmod
加载中
  ↓ 初始化成功
已加载、可使用
  ↓ 使用者释放、引用计数归零
卸载中
  ↓
已卸载

加载失败时,不一定会回到“未加载”状态。例如:

  • 内核已经分配了一部分资源,初始化后半段失败;
  • 依赖模块已成功加载,但目标模块失败;
  • 设备探测失败,但模块本身仍然留在内核中;
  • 模块被标记为不可卸载,或者被其他内核对象引用。

查看当前已加载模块:

lsmod
cat /proc/modules

lsmod 主要解析 /proc/modules,因此通常只能看到动态加载的模块。编译进内核的功能不会出现在这里。判断一个功能是否编译为内建功能,可以查看:

grep -E 'CONFIG_(E1000E|USB_STORAGE)=' /boot/config-$(uname -r)

典型结果:

CONFIG_E1000E=m
CONFIG_USB_STORAGE=y

含义是:

  • m:编译成模块,可通过 modprobe 加载;
  • y:直接编译进内核,不存在可独立加载的 .ko
  • 未设置或 # CONFIG_xxx is not set:该功能没有编译进内核。

对于当前内核对应的模块目录,还可以查看:

find /lib/modules/$(uname -r) -name 'e1000e.ko*'
grep -w e1000e /lib/modules/$(uname -r)/modules.builtin

如果某功能是 y,对它执行 modprobe 可能得到:

modprobe: FATAL: Module xxx not found in directory /lib/modules/...

这不一定表示功能不可用,而可能表示它已经在内核启动时存在。

2.2 加载和初始化不是同一个概念

模块文件被内核接受后,内核会执行模块的初始化入口。驱动通常在初始化函数中:

  1. 注册总线驱动或文件系统;
  2. 注册字符设备、块设备或网络设备;
  3. 声明支持的设备 ID;
  4. 创建 sysfs 属性;
  5. 请求中断、I/O 区域或 DMA 资源;
  6. 等待设备模型匹配具体硬件。

因此,下面两种情况必须区分:

modprobe e1000e

命令成功,说明模块装载和初始化过程成功返回,但不一定说明某块网卡已经成功工作。之后还要观察:

dmesg --ctime | tail -n 50
ip link
ls -l /sys/bus/pci/drivers/e1000e/

如果模块加载成功但没有设备,常见原因是:

  • 硬件不存在或被固件禁用;
  • 驱动没有声明该设备 ID;
  • 设备已被另一个驱动绑定;
  • PCI、USB 或 IOMMU 层初始化失败;
  • 驱动探测阶段失败。

三、insmodmodprobermmodmodinfo

3.1 insmod:直接装入指定文件

sudo insmod ./example.ko

insmod 直接把指定的模块文件交给内核。它不会像 modprobe 那样根据模块名自动搜索依赖并加载依赖模块。若 example.ko 依赖另一个模块导出的符号,而该依赖尚未加载,通常会失败:

insmod: ERROR: could not insert module ./example.ko: Unknown symbol ...

因此,insmod 适合:

  • 临时测试一个明确的模块文件;
  • 调试刚编译的模块;
  • 验证当前内核能否接受某个 .ko

它不适合常规系统管理,因为它绕开了模块目录索引、别名、配置文件和依赖处理。

3.2 modprobe:按模块名解析依赖和配置

sudo modprobe e1000e
sudo modprobe -r e1000e

modprobe 属于 kmod 工具集,通常会:

  1. 根据当前内核版本搜索 /lib/modules/$(uname -r)/
  2. 使用 modules.dep 找到依赖;
  3. 使用 modules.alias 根据设备别名匹配模块;
  4. 读取 /etc/modprobe.d/*.conf、发行版提供的配置目录;
  5. 递归加载依赖;
  6. 把参数传给内核;
  7. 按逆依赖关系尝试卸载。

模块名中的连字符和下划线通常会被规范化。例如:

modprobe foo-bar
modprobe foo_bar

在现代 kmod 环境中通常指向同一模块名,但配置文件和日志中应保持一致,避免人为混淆。

3.3 modinfo:查看模块元数据

modinfo e1000e

常见字段包括:

filename:       /lib/modules/.../e1000e.ko.xz
license:        GPL
description:    Intel(R) PRO/1000 Network Driver
author:         ...
alias:          pci:v00008086d000015F3...
depends:        ptp
retpoline:      Y
vermagic:       6.x.y-... SMP preempt mod_unload modversions

这些字段的含义不同:

  • filename:模块文件路径;
  • alias:设备匹配别名,udev、内核设备事件和 modprobe 可据此自动加载;
  • depends:模块依赖;
  • vermagic:编译该模块时记录的内核版本和部分构建特征;
  • license:模块声明的许可证,可能影响内核 taint 状态;
  • signersig_idsig_key:模块签名信息,若模块带签名通常可以看到。

查看指定文件而不是按模块名搜索:

modinfo ./example.ko

这对于 DKMS 生成的模块尤其有用。


四、模块搜索路径、depmod 和内核版本匹配

4.1 模块目录按内核版本隔离

模块通常安装在:

/lib/modules/<kernel-release>/

其中 <kernel-release> 必须与:

uname -r

的结果一致。

例如当前内核为:

6.8.0-31-generic

则系统会从:

/lib/modules/6.8.0-31-generic/

查找模块。

如果系统升级了内核但没有安装对应模块包,可能出现:

modprobe: FATAL: Module xxx not found in directory /lib/modules/6.8.0-31-generic

检查目录和内核版本:

uname -r
ls -ld /lib/modules/$(uname -r)
find /lib/modules/$(uname -r) -name 'xxx.ko*'

4.2 depmod 生成什么

depmod 根据已安装模块的 ELF 信息和模块元数据生成索引:

sudo depmod -a "$(uname -r)"

典型索引包括:

  • modules.dep:模块依赖关系;
  • modules.alias:设备别名到模块的映射;
  • modules.symbols:导出符号和模块的映射;
  • modules.builtin:编译进内核的功能;
  • 压缩版本的对应索引文件。

模块刚复制到 /lib/modules/$(uname -r)/ 后,如果没有运行 depmodmodprobe 可能找不到它,即使文件实际存在。

推荐的安装顺序是:

sudo install -D -m 0644 example.ko \
  /lib/modules/"$(uname -r)"/updates/example.ko
sudo depmod -a "$(uname -r)"
sudo modprobe example

这里假设 example.ko 已经针对当前内核正确编译,并且不需要额外签名。如果启用了强制模块签名,签名步骤必须发生在 modprobe 之前。

4.3 vermagic 不是完整的 ABI 保证

模块必须和内核匹配,但“匹配”不能简单理解为只看 vermagic

Linux 内核没有对第三方模块提供长期稳定的内部模块 ABI。内核内部数据结构、函数签名和符号语义可能随版本、配置和补丁改变。常见检查包括:

  • vermagic 是否匹配;
  • 是否启用 CONFIG_MODVERSIONS,以及符号 CRC 是否匹配;
  • CPU 架构是否匹配;
  • 内核配置是否满足模块需求;
  • 编译器、工具链和头文件是否与目标内核环境一致;
  • 模块是否针对正确的源码树和 Module.symvers 构建。

典型错误:

invalid module format

应立即查看内核日志:

dmesg --level=err,warn | tail -n 50

可能得到:

version magic '6.8.0 SMP mod_unload ...' should be '6.8.0-31-generic SMP mod_unload ...'

或者:

disagrees about version of symbol ...
Unknown symbol ...

--force-vermagic--force-modversion 可以绕过部分检查,但这只是把风险从“明确拒绝”变成“可能破坏内核”。生产环境不应把它当作修复手段。


五、依赖关系:模块为什么不能孤立加载

5.1 符号依赖的基本条件

模块可以导出符号,另一个模块可以引用这些符号。设模块 AA 引用的符号集合为:

R(A)={s1,s2,,sn}R(A)=\{s_1,s_2,\ldots,s_n\}

系统当前已导出的符号集合为 EE。要让 AA 成功装入,至少需要:

R(A)ER(A)\subseteq E

如果某个符号不在 EE 中,内核无法完成重定位,加载会失败。

若模块 BB 导出 s1s_1,模块 CC 导出 s2s_2,则加载 AA 前至少要加载:

deps(A)={B,C}\text{deps}(A)=\{B,C\}

如果 BB 又依赖 DD,完整依赖闭包是:

closure(A)={A,B,C,D}\text{closure}(A)=\{A,B,C,D\}

modprobe A 会根据 modules.dep 递归处理这个闭包;insmod A.ko 通常不会自动完成这一步。

查看依赖:

modinfo -F depends e1000e

查看实际加载结果:

lsmod

lsmodUsed by 列可以帮助判断卸载顺序,但它不是所有引用关系的完整语义表示。

5.2 循环依赖和软依赖

正常模块依赖应形成可处理的有向图。若出现:

A → B
B → A

说明模块设计、构建索引或版本组合存在问题,不能简单通过改变命令顺序解决。

软依赖(soft dependency)是“建议先加载或后加载某些模块”,它不一定是 ELF 符号依赖。配置示例:

softdep target pre: helper

含义是加载 target 前先尝试加载 helper。它适合表达初始化顺序偏好,但不能替代真实符号依赖。如果 target 实际引用 helper 的符号,正确做法是让构建系统生成真实依赖,而不是只写 softdep

5.3 设备别名和自动加载

设备驱动通常包含类似 PCI、USB 或平台设备别名:

alias: pci:v00008086d000015F3...

当内核发现设备时,会产生 uevent。用户空间的 udev/systemd-udev 监听事件并调用相应机制,modprobe 根据 modules.alias 找到驱动。

这解释了以下现象:

  • 插入 USB 网卡后,模块可能自动加载;
  • 手动加载模块和插拔设备的最终驱动绑定路径可能相同;
  • udev 负责设备节点、命名和权限,不负责替代内核模块本身;
  • /dev 中没有设备节点时,不能仅凭 modprobe 成功断定设备驱动完整工作。

可以观察内核设备事件:

udevadm monitor --kernel --udev

查看设备的驱动和模块信息:

udevadm info --query=all --path=/sys/class/net/eth0
readlink -f /sys/class/net/eth0/device/driver

六、模块参数:从命令行到 sysfs 的完整路径

6.1 参数的来源

模块参数可以来自几处:

sudo modprobe e1000e InterruptThrottleRate=3000

也可以写入配置文件:

# /etc/modprobe.d/e1000e.conf
options e1000e InterruptThrottleRate=3000

配置文件通常由 modprobe 读取。不同发行版还可能从 /usr/lib/modprobe.d//run/modprobe.d/ 等目录读取配置;具体优先级以本机 man modprobe.d 和发行版文档为准。

检查某个模块支持的参数:

modinfo -p e1000e

示例输出可能类似:

InterruptThrottleRate:Interrupt Throttling Rate (array of int)

参数名称、类型和默认值属于具体模块接口,不能根据其他驱动的习惯猜测。

6.2 参数何时生效

模块参数通常在模块装入时解析:

modprobe
  ↓
传递参数
  ↓
内核创建模块对象
  ↓
模块初始化函数读取参数
  ↓
注册设备或服务

因此,修改配置文件后,已加载模块不会自动重新初始化。通常需要:

sudo modprobe -r e1000e
sudo modprobe e1000e

但卸载驱动存在风险:

  • 正在使用的网络接口会中断;
  • 正在使用的存储设备可能无法卸载;
  • 图形驱动可能被显示服务器、桌面环境或 GPU 进程引用;
  • 模块引用计数不为零时会拒绝卸载。

强制卸载选项会放大风险,不应作为常规手段。

6.3 运行时参数与 sysfs

许多模块参数会出现在:

/sys/module/<module>/parameters/

例如:

ls /sys/module/e1000e/parameters/
cat /sys/module/e1000e/parameters/InterruptThrottleRate

能否运行时修改取决于参数声明和模块实现。常见情况有三种:

  1. 只读参数:只能在加载时设置;
  2. 可写参数:可以通过 sysfs 修改;
  3. 修改后不完整生效:参数变量改变了,但驱动只在初始化时读取一次。

因此,写入 sysfs 不等于设备行为一定立即改变:

echo 3000 | sudo tee /sys/module/e1000e/parameters/InterruptThrottleRate

操作前应确认:

test -w /sys/module/e1000e/parameters/InterruptThrottleRate

并查阅对应模块文档或源码。对生产网络、存储和安全模块进行参数修改,应先在维护窗口验证,因为错误值可能引起性能下降、设备失联或数据路径变化。

6.4 模块参数与内核启动参数不是一回事

编译为模块的功能,其参数通常通过 modprobe 传递;编译进内核的功能不能使用 modprobe 装入。内建功能的参数可能需要作为内核命令行参数传入:

foo.bar=value

例如系统启动链中的某些存储、IOMMU、早期控制台参数,必须在 bootloader 的内核命令行阶段生效。它们可能经过:

Firmware → Bootloader → Kernel command line → initramfs → systemd

如果设备根文件系统依赖某个驱动,该驱动可能必须在 initramfs 阶段加载,不能等到真实根文件系统挂载后再通过 /etc/modprobe.d/ 处理。修改配置后还要重建 initramfs:

# Debian/Ubuntu
sudo update-initramfs -u -k "$(uname -r)"

# Fedora/RHEL 系
sudo dracut -f

这也是“配置文件已经改了,但早期启动行为没有变化”的常见原因。


七、签名、Secure Boot 和信任链

7.1 为什么内核要验证模块

模块装入后拥有内核级权限。如果攻击者能把任意模块装入内核,就可能:

  • 隐藏进程、文件或网络连接;
  • 篡改系统调用和安全检查;
  • 读取或修改内核内存;
  • 持久化为 rootkit。

因此,启用模块签名时,内核会在加载阶段验证模块签名。签名不是加密,通常只保证:

  1. 模块内容没有被篡改;
  2. 模块由受信任的私钥对应方签发。

它不保证模块没有漏洞,也不保证驱动行为符合业务要求。

7.2 常见信任链

在启用 UEFI Secure Boot 的系统中,典型信任链是:

固件信任根
  ↓
shim/bootloader
  ↓
内核
  ↓
内核内置或平台提供的受信任证书
  ↓
模块签名证书
  ↓
模块 .ko

但具体链路取决于发行版、固件、shim、内核配置和是否启用 lockdown。Secure Boot 通常会促使系统启用更严格的内核完整性策略,但“Secure Boot 开启”与“所有发行版都用同一种模块验证策略”不是等价命题。

查看当前状态:

mokutil --sb-state
cat /sys/kernel/security/lockdown 2>/dev/null || true

查看模块是否带签名:

modinfo e1000e | grep -E '^(signer|sig_id|sig_key|sig_hashalgo):'

如果系统拒绝模块,查看:

dmesg | grep -iE 'module|secure|lockdown|signature|key'

常见错误包括:

Required key not available

或:

module verification failed: signature and/or required key missing

后者在某些配置中可能只是警告并导致内核 taint,而不是拒绝加载;是否拒绝取决于内核配置和安全策略,不能只凭这一行日志下结论。

7.3 手动签名的正确顺序

内核源码树通常提供:

scripts/sign-file

签名命令形式:

sudo scripts/sign-file sha256 private_key.pem public_cert.x509 example.ko

参数含义:

  • sha256:签名摘要算法;
  • private_key.pem:私钥;
  • public_cert.x509:证书;
  • example.ko:未压缩模块文件。

签名必须针对最终的模块内容。若模块已经压缩为 .ko.xz.ko.zst,通常应先解压、对 .ko 签名,再按发行版方式重新压缩并重新运行 depmod。直接对压缩包签名通常不是内核模块签名格式所要求的对象。

生成私钥时必须严格控制权限:

openssl req -new -x509 -newkey rsa:2048 \
  -keyout module-signing-key.pem \
  -outform DER \
  -out module-signing-cert.der \
  -nodes -days 3650 \
  -subj "/CN=Local Module Signing/"
chmod 600 module-signing-key.pem

生产环境不应把私钥随意放在普通构建机、软件包仓库或开发者工作站上。更稳妥的做法是使用发行版提供的 DKMS 签名集成、受控构建环境或硬件密钥管理。

7.4 MOK 与自签名证书

许多 Debian/Ubuntu 系环境使用 Machine Owner Key(MOK)让管理员将自有证书导入固件相关信任路径。典型流程可能是:

sudo mokutil --import module-signing-cert.der

然后在下一次启动时进入 MOK 管理界面确认。此流程具有明显的物理或控制台安全含义:

  • 导入证书通常需要重启;
  • 必须在预启动界面确认;
  • 忘记密码或导入错误证书可能需要恢复流程;
  • 证书一旦被信任,使用对应私钥签出的模块都可能被接受。

不同发行版的 MOK、内核 keyring 和 DKMS 集成不同,应以本机发行版文档为准。不要把“关闭 Secure Boot”作为默认修复方案;这会改变整条启动信任链,生产环境应先评估合规和攻击面。


八、DKMS:如何让第三方模块跟随内核升级

8.1 DKMS 解决的实际问题

DKMS(Dynamic Kernel Module Support)是一套用于管理第三方内核模块源码和构建产物的机制。它主要解决:

安装模块源码
  ↓
针对某个内核版本构建
  ↓
安装到该内核的模块目录
  ↓
内核升级
  ↓
针对新内核重新构建并安装

内核模块不是“编译一次、所有内核通用”的二进制。内核升级后,即使 ABI 看起来相近,也通常需要针对新内核重新编译。

DKMS 常见目录:

/usr/src/<name>-<version>/
/var/lib/dkms/<name>/<version>/<kernel-release>/
/lib/modules/<kernel-release>/updates/dkms/

实际路径和布局可能随发行版版本变化。

8.2 DKMS 的基本生命周期

以已包含 dkms.conf 的源码为例:

sudo dkms status

查看状态,例如:

example/1.2.3, 6.8.0-31-generic, x86_64: installed

常见手动流程:

sudo dkms add -m example -v 1.2.3
sudo dkms build -m example -v 1.2.3 -k "$(uname -r)"
sudo dkms install -m example -v 1.2.3 -k "$(uname -r)"

每一步的含义:

  1. add:把源码版本登记到 DKMS;
  2. build:使用目标内核的构建目录和头文件编译;
  3. install:把生成的模块安装到该内核的模块目录,并通常触发索引更新。

删除某个内核对应的安装结果:

sudo dkms remove -m example -v 1.2.3 -k 6.8.0-31-generic

删除所有内核对应结果:

sudo dkms remove -m example -v 1.2.3 --all

最后一个命令具有破坏性,会删除该版本模块的所有 DKMS 构建结果,执行前应确认当前运行内核不依赖它。

8.3 DKMS 构建需要什么

基本前置条件包括:

uname -r
ls -ld /lib/modules/$(uname -r)/build
gcc --version
make --version
dkms --version

/lib/modules/$(uname -r)/build 应指向当前内核对应的构建目录。Debian/Ubuntu 通常需要安装:

sudo apt install dkms build-essential linux-headers-$(uname -r)

Fedora/RHEL 系通常需要对应的:

sudo dnf install dkms gcc make kernel-devel-$(uname -r) kernel-headers

包名和是否需要完整内核源码树因发行版及内核包而异。仅安装“某个内核头文件”但版本不等于 uname -r,不能保证构建正确。

查看构建日志:

find /var/lib/dkms/example/1.2.3/ -type f -name make.log -print

常见失败原因:

  • 当前内核缺少匹配的 kernel-devel 或 headers;
  • 第三方源码使用了已删除或改变的内核内部 API;
  • 内核配置不满足模块;
  • 编译器或架构不匹配;
  • 模块源码没有适配当前内核;
  • Secure Boot 要求签名,但 DKMS 只完成了编译,没有完成签名和证书导入。

8.4 DKMS 不等于自动可用

“DKMS 安装成功”至少要拆成四个问题:

  1. 是否对目标内核构建成功;
  2. 是否安装到目标内核的模块目录;
  3. depmod 是否生成索引;
  4. 模块是否能够通过签名验证并在启动时加载。

检查:

dkms status
modinfo example
modprobe -n -v example
sudo modprobe example
lsmod | grep '^example'
dmesg --ctime | tail -n 50

modprobe -n -v 使用 dry-run 模式,通常只显示将要执行的动作,不真正加载模块。它适合先检查配置和依赖,但不能代替真正加载验证。

如果模块需要在 initramfs 阶段使用,例如根磁盘控制器、早期网络或加密存储驱动,安装后还要重建 initramfs:

# Debian/Ubuntu
sudo update-initramfs -u -k all

# Fedora/RHEL 系
sudo dracut --regenerate-all --force

是否自动重建由发行版安装脚本决定,不能假定所有 DKMS 包都相同。

8.5 DKMS 与内核升级的竞态

内核升级通常包含多个阶段:

新内核文件安装
  ↓
新内核模块包安装
  ↓
DKMS 针对新内核构建
  ↓
模块安装和 depmod
  ↓
initramfs 重建
  ↓
bootloader 可选内核更新
  ↓
重启

如果 DKMS 在新内核上构建失败,系统仍可能把新内核设为可启动内核,但第三方设备驱动缺失。升级后应检查:

dkms status
ls -l /lib/modules/<new-release>/updates/dkms/
lsinitramfs /boot/initrd.img-<new-release> | grep example  # Debian/Ubuntu
lsinitrd /boot/initramfs-<new-release>.img | grep example  # Fedora/RHEL

不要因为当前运行内核仍然正常,就认为新内核没有问题。真正的故障可能在下一次重启才出现。


九、模块配置、黑名单和自动加载策略

9.1 blacklist 的含义有限

配置:

# /etc/modprobe.d/blacklist-example.conf
blacklist example

通常表示:当用户空间根据设备别名自动请求模块时,不要自动加载它。它并不一定阻止:

sudo modprobe example

显式加载,也不一定阻止其他机制直接使用模块。

如果确实需要禁止模块,常见配置是:

install example /bin/false

但这种方式也只影响经由 modprobe 的路径,且可能破坏依赖它的其他设备。它还可能被 initramfs 中的旧配置覆盖或绕开。

修改后先验证:

modprobe -n -v example

如果输出类似:

/bin/false

说明当前 modprobe 配置会阻止显式加载。

9.2 配置变更可能只进入真实根文件系统

早期启动使用的是 initramfs 中的一份模块和配置副本。修改:

/etc/modprobe.d/*.conf

后,如果目标设备在 initramfs 阶段就需要该配置,必须重建 initramfs。否则可能出现:

  • 系统启动阶段仍然加载旧参数;
  • 黑名单在系统进入后才生效;
  • 根盘驱动在 initramfs 中缺失;
  • 手动检查 /etc/modprobe.d/ 看起来正确,但早期日志行为不变。

检查 initramfs 内容:

# Debian/Ubuntu
lsinitramfs /boot/initrd.img-"$(uname -r)" | grep -E 'modprobe|example'

# Fedora/RHEL 系
lsinitrd /boot/initramfs-"$(uname -r)".img | grep -E 'modprobe|example'

十、自动加载、手动加载与持久化加载

10.1 三种不同的加载来源

模块可能由以下路径加载:

  1. 设备发现自动加载:内核产生设备事件,用户空间根据 alias 请求模块;
  2. 启动配置加载:initramfs 或 systemd 单元显式加载;
  3. 管理员手动加载:执行 modprobe 或某些服务间接加载。

因此,执行:

sudo modprobe -r example

后模块又回来,未必是命令失败,可能是设备事件或服务再次请求了它。

追踪模块加载事件:

journalctl -k -b | grep -i example
udevadm monitor --kernel --udev

查看是否有服务显式配置:

grep -R --line-number --fixed-strings example \
  /etc/modules /etc/modules-load.d /usr/lib/modules-load.d \
  /etc/modprobe.d /usr/lib/modprobe.d 2>/dev/null

/etc/modules-load.d/*.conf 通常用于启动时按名称加载模块;modprobe.d 主要用于参数、别名、软依赖和黑名单。它们的职责不同。

10.2 卸载的条件

模块卸载通常要求:

引用计数=0\text{引用计数}=0

引用可能来自:

  • 另一个模块;
  • 打开的设备文件;
  • 已注册但尚未释放的网络、块设备或文件系统对象;
  • 用户进程持有的文件描述符;
  • 内核线程或异步工作;
  • 设备仍然绑定在该驱动上。

查看:

lsmod | grep '^example'
sudo modprobe -r example

失败时可能看到:

modprobe: FATAL: Module example is in use.

此时应查找使用者,而不是立即强制卸载:

lsof /dev/example0
fuser -v /dev/example0
findmnt
ip link

对网络、存储、GPU 和文件系统模块,强行卸载可能触发 use-after-free、内核崩溃或文件系统损坏。生产环境通常优先停用上层服务、解除设备绑定、安排重启,而不是使用强制选项。


十一、故障诊断:按照“文件—索引—内核—设备—启动”分层

模块故障不应只重复执行 modprobe。可以按以下因果路径检查。

11.1 第一层:确认运行内核和目标架构

uname -a
uname -r
uname -m

如果正在排查 DKMS 或手工编译模块,必须确认它针对的是当前运行内核,而不是磁盘上另一个版本。

11.2 第二层:模块是否存在且可识别

find /lib/modules/$(uname -r) -name 'example.ko*'
modinfo example

可能的结果:

  • find 找不到:没有安装、安装到了其他内核目录,或功能是内建;
  • modinfo 找不到:模块索引缺失、模块路径错误或模块确实不存在;
  • modinfo 能找到但 modprobe 失败:继续查签名、版本、依赖和初始化日志。

如果刚手工安装:

sudo depmod -a "$(uname -r)"

11.3 第三层:查看 modprobe 实际计划

modprobe -n -v example

如果输出包含:

insmod /lib/modules/.../helper.ko
insmod /lib/modules/.../example.ko option=value

说明依赖和参数配置至少被识别。若出现:

/bin/false

说明黑名单或 install 规则正在阻止加载。

11.4 第四层:执行加载并读取内核日志

sudo modprobe example
echo $?
dmesg --ctime | tail -n 100
journalctl -k -b --no-pager | tail -n 100

modprobe 返回码只能说明命令调用是否成功,具体原因要看内核日志。典型错误与方向:

日志表现 常见原因
Module not found 目标内核目录无模块、索引未更新、名称错误
Unknown symbol 依赖未加载、符号未导出、内核/模块版本不匹配
invalid module format vermagic、符号版本、架构或配置不匹配
Required key not available 签名缺失或签名证书不在信任链
Operation not permitted lockdown、权限、LSM 或安全策略阻止
Module is in use 引用计数不为零,无法卸载
加载成功但无设备 探测失败、设备 ID 不匹配、设备被其他驱动占用

11.5 第五层:确认设备是否绑定

以 PCI 设备为例:

lspci -nnk

重点观察:

Kernel driver in use: e1000e
Kernel modules: e1000e

两行含义不同:

  • Kernel modules:内核认为哪些模块可能支持该设备;
  • Kernel driver in use:当前实际绑定的是哪个驱动。

USB 设备可以使用:

lsusb
usb-devices

sysfs 是更接近内核设备模型的证据:

readlink -f /sys/bus/pci/devices/0000:03:00.0/driver
cat /sys/bus/pci/devices/0000:03:00.0/modalias

如果驱动模块已加载但没有绑定,下一步应查设备 ID、总线错误、资源冲突和设备是否已被其他驱动占用,而不是反复安装同一个 .ko


十二、启动故障:initramfs 中缺模块时会发生什么

根文件系统挂载前,内核通常先启动 initramfs 中的临时用户空间。它需要完成:

  1. 加载存储控制器和文件系统模块;
  2. 发现根设备;
  3. 组装 RAID、LVM 或解锁加密卷;
  4. 挂载真实根文件系统;
  5. 将控制权交给真实根文件系统中的 init/systemd。

如果根盘所需模块未被放入 initramfs,典型结果是:

ALERT! UUID=... does not exist.
Gave up waiting for root file system device.

或者进入 dracut emergency shell。

这时,真实根文件系统中的:

/lib/modules/<release>/

即使包含正确模块,也来不及使用,因为真实根文件系统还没有挂载。

12.1 从旧内核恢复

如果系统保留旧内核,最安全的恢复方式通常是从 bootloader 选择旧内核启动,然后:

uname -r
dkms status

修复目标新内核:

sudo dkms autoinstall -k <broken-kernel-release>

然后重建 initramfs:

# Debian/Ubuntu
sudo update-initramfs -u -k <broken-kernel-release>

# Fedora/RHEL 系
sudo dracut -f /boot/initramfs-<broken-kernel-release>.img \
  <broken-kernel-release>

最后检查其中是否包含目标模块:

# Debian/Ubuntu
lsinitramfs /boot/initrd.img-<broken-kernel-release> | grep example

# Fedora/RHEL 系
lsinitrd /boot/initramfs-<broken-kernel-release>.img | grep example

12.2 从救援介质 chroot 修复

如果无法从旧内核启动,可以使用安装介质或救援系统。假设真实根分区为 /dev/mapper/vg-root,启动后的挂载点为 /mnt

sudo mount /dev/mapper/vg-root /mnt
sudo mount /dev/nvme0n1p2 /mnt/boot
sudo mount /dev/nvme0n1p1 /mnt/boot/efi

再绑定必要的伪文件系统:

for i in /dev /dev/pts /proc /sys /run; do
    sudo mount --rbind "$i" "/mnt$i"
    sudo mount --make-rslave "/mnt$i"
done

进入系统:

sudo chroot /mnt /bin/bash

在 chroot 内检查:

uname -r
ls /lib/modules
dkms status

注意:chroot 不会改变正在运行的内核。此时 uname -r 显示的是救援系统内核,而不是磁盘上待修复的目标内核。因此 DKMS、depmod 和 initramfs 命令必须明确指定目标版本:

dkms autoinstall -k <target-release>
depmod -a <target-release>

# Debian/Ubuntu
update-initramfs -u -k <target-release>

# Fedora/RHEL 系
dracut -f /boot/initramfs-<target-release>.img <target-release>

修复后退出并卸载:

exit
sudo umount -R /mnt
sudo reboot

12.3 黑名单的恢复风险

如果错误地把根存储驱动、文件系统模块或早期加密模块加入黑名单,系统可能无法启动。恢复时应:

  1. 从旧内核或救援介质进入;
  2. 删除或注释错误的 blacklistinstall ... /bin/false、错误参数;
  3. 重建目标 initramfs;
  4. 重新验证其中的模块和配置;
  5. 再次启动。

不要只修改真实根文件系统中的配置而忘记 initramfs。否则同一错误会在下一次启动时复现。


十三、内核 taint:加载成功不等于风险消失

Linux 内核可以记录 taint 状态,表示当前内核处于某些非标准或高风险条件。例如:

  • 加载了非 GPL 或闭源模块;
  • 模块强制加载;
  • 模块签名验证失败但仍被允许加载;
  • 发生过硬件错误或内核警告;
  • 使用了某些 out-of-tree 模块。

查看:

cat /proc/sys/kernel/tainted
dmesg | grep -i taint

taint 不一定表示系统立即不可用,也不等于模块必然有问题。它的一个重要作用是告诉开发者:后续内核崩溃分析不能再假定系统只运行发行版内核代码。

因此,下面的结论都不成立:

  • modprobe 成功,所以驱动安全”;
  • “没有报错,所以模块一定正确”;
  • “模块签名通过,所以模块没有漏洞”;
  • “内核 taint,所以系统已经损坏”。

taint 是诊断和风险标记,不是单一故障结论。


十四、一个完整的第三方模块验证流程

以下流程适用于已经得到 example.ko 或 DKMS 包的场景。

14.1 确认目标内核

target="$(uname -r)"
echo "$target"
test -d "/lib/modules/$target" && echo "module tree exists"
test -e "/lib/modules/$target/build" && echo "build tree exists"

如果第二个测试失败,无法直接针对当前内核进行常规 DKMS 编译。

14.2 检查 ELF、架构和元数据

file ./example.ko
modinfo ./example.ko

应确认:

  • 架构与 uname -m 对应;
  • vermagic 与目标内核合理匹配;
  • depends 中的依赖可在目标模块树找到;
  • 如果启用了签名验证,存在 signer 等字段。

14.3 安装并建立索引

sudo install -D -m 0644 ./example.ko \
  "/lib/modules/$target/updates/example.ko"
sudo depmod -a "$target"
modinfo example

如果 modinfo example 仍找不到,优先检查文件路径、模块文件名、压缩格式和 depmod 是否针对正确版本执行。

14.4 先 dry-run,再真正加载

modprobe -n -v example
sudo modprobe example

预期结果不是固定文本,而是:

  • dry-run 能显示目标模块和依赖;
  • 真正加载返回成功;
  • lsmod 出现模块,或者它是内建功能;
  • dmesg 没有版本、签名、符号或探测错误;
  • 设备出现在 /sys/devip linklsblk 等相应接口中。

14.5 验证卸载和恢复

仅对测试设备执行:

sudo modprobe -r example
lsmod | grep '^example' || echo "example unloaded"

如果卸载失败,先调查使用者,不要直接强制卸载。对根磁盘、当前网络链路或显示服务器使用的模块,加载和卸载测试都可能导致系统失联,必须安排控制台或带外管理通道。


十五、常见误解和反例

误解一:把 .ko 复制到任意目录就能加载

反例:

cp example.ko /tmp/
sudo modprobe example

modprobe 默认不会把 /tmp 当作模块目录,也没有利用其中的文件建立依赖索引。正确做法是安装到目标内核的模块树,执行 depmod,或者在纯测试场景使用:

sudo insmod /tmp/example.ko

但后者仍不处理依赖,也不会建立持久化配置。

误解二:modprobe 找不到模块就是驱动没编译

反例:

modprobe usb_storage

如果 CONFIG_USB_STORAGE=y,USB 存储支持可能已经编译进内核,lsmod 不会显示它,modprobe 也找不到独立 .ko。必须结合:

grep CONFIG_USB_STORAGE /boot/config-"$(uname -r)"
grep -w usb_storage /lib/modules/"$(uname -r)"/modules.builtin

判断它是内建、模块还是未启用。

误解三:修改 modprobe.d 后立即生效

反例:

echo 'options example mode=1' | sudo tee /etc/modprobe.d/example.conf
cat /sys/module/example/parameters/mode

如果模块已经加载,配置不会自动重新执行。需要在安全条件下重新加载模块,或者重启。若模块位于 initramfs,还需要重建 initramfs。

误解四:DKMS 安装成功就一定能启动

反例:

DKMS build completed successfully

这只说明某次构建成功。仍可能存在:

  • 新内核实际使用了另一个模块目录;
  • initramfs 未更新;
  • 模块没有签名;
  • 证书没有进入内核信任链;
  • 驱动加载成功但没有绑定设备。

必须分别检查 dkms statusmodinfolsinitramfs/lsinitrd、签名字段、dmesg 和设备绑定状态。

误解五:强制绕过版本检查可以修好模块

--force-vermagic--force-modversion 只能绕过检查,不能修复二进制接口不兼容。若模块引用的结构体布局或函数语义已经改变,强行加载可能导致静默内存破坏。它最多适用于受控实验和明确知道兼容性的调试场景,不适用于生产恢复。

误解六:驱动加载成功但没有 /dev 节点,说明模块失败

设备节点的创建还涉及设备注册、sysfs、uevent、udev 规则和权限。模块可能已经成功加载,但:

  • 设备没有完成探测;
  • 设备属于网络或 sysfs 接口,不需要传统 /dev 节点;
  • udev 未运行或规则失败;
  • 设备节点被自定义规则改名;
  • 权限或 SELinux/AppArmor 策略阻止访问。

应同时检查:

lsmod
dmesg
find /sys -iname '*example*'
udevadm info --query=all --name=/dev/example0
ls -l /dev/example0

十六、生产环境的风险边界

内核模块变更比普通软件包变更更接近启动链和硬件数据路径,至少有以下风险:

  • 加载错误驱动可能抢占设备;
  • 卸载存储或文件系统模块可能导致数据损坏;
  • 修改网卡参数可能中断远程维护链路;
  • 禁用签名检查会削弱系统完整性;
  • DKMS 在内核升级时可能使新内核不可用;
  • 错误的 initramfs 配置可能让系统无法找到根文件系统;
  • 第三方模块可能触发内核崩溃、内存破坏或 taint。

变更前应至少保留:

uname -r
dkms status
ls -l /boot
ls -l /lib/modules
cp -a /etc/modprobe.d /root/modprobe.d.backup.$(date +%F)

并确保存在一种不依赖当前网络驱动的恢复路径,例如:

  • 云平台控制台;
  • IPMI、iDRAC、iLO 等带外管理;
  • 虚拟机串口或管理终端;
  • 可启动的旧内核;
  • 救援介质和已知可用的内核包。

在生产系统上,优先使用发行版打包的内核模块和 DKMS 包。必须使用第三方模块时,应把以下对象一起纳入版本管理和升级验证:

模块源码版本
内核版本
内核配置
编译工具链
模块签名证书
DKMS 配置
initramfs 内容
bootloader 启动项

十七、最终排查顺序

遇到模块问题时,可以把判断过程压缩为以下因果链:

当前运行的是哪个内核?
  ↓
该内核的模块目录是否存在?
  ↓
目标模块是否存在,depmod 索引是否更新?
  ↓
modinfo 显示的 vermagic、架构、依赖是否合理?
  ↓
modprobe dry-run 是否显示正确路径和参数?
  ↓
内核是否因签名、版本或符号拒绝?
  ↓
模块初始化是否成功?
  ↓
设备是否被该驱动实际绑定?
  ↓
udev 是否创建了正确的设备接口和权限?
  ↓
如果设备在早期启动需要,initramfs 是否包含正确模块和配置?

对应的最小命令集是:

uname -r
modinfo example
modprobe -n -v example
sudo modprobe example
dmesg --ctime | tail -n 100
lsmod | grep '^example'
lspci -nnk                    # PCI 设备时
lsusb                         # USB 设备时
dkms status                   # DKMS 模块时

这套顺序的核心是区分不同故障层次:文件发现问题由模块目录和 depmod 解释,依赖问题由符号和索引解释,安全拒绝由签名和 lockdown 解释,设备不可用由探测和绑定解释,启动失败则进一步涉及 initramfs、根设备和 bootloader。只有把这些层次分开,模块加载、参数、依赖、签名和 DKMS 的行为才能形成可验证的整体模型。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。