Linux 基础体系 · 第 31/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux 内核模块:加载、参数、依赖、签名、DKMS 和故障恢复
版本与范围: 面向现代主流 Linux 发行版;命令和路径以常见的 systemd、kmod、dracut 或 initramfs-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 设备事件和用户空间命名/权限]
这里有三个容易混淆的层次:
- 模块文件存在:例如
/lib/modules/$(uname -r)/kernel/drivers/.../e1000e.ko。 - 模块能够被内核接受:版本、配置、架构、签名和符号必须满足条件。
- 模块加载后设备可用:驱动还必须匹配设备、完成探测,并由设备模型和用户空间继续处理。
因此,“文件存在”不等于“可以加载”,“可以加载”也不等于“设备工作”。
二、内核模块的生命周期
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 加载和初始化不是同一个概念
模块文件被内核接受后,内核会执行模块的初始化入口。驱动通常在初始化函数中:
- 注册总线驱动或文件系统;
- 注册字符设备、块设备或网络设备;
- 声明支持的设备 ID;
- 创建 sysfs 属性;
- 请求中断、I/O 区域或 DMA 资源;
- 等待设备模型匹配具体硬件。
因此,下面两种情况必须区分:
modprobe e1000e
命令成功,说明模块装载和初始化过程成功返回,但不一定说明某块网卡已经成功工作。之后还要观察:
dmesg --ctime | tail -n 50
ip link
ls -l /sys/bus/pci/drivers/e1000e/
如果模块加载成功但没有设备,常见原因是:
- 硬件不存在或被固件禁用;
- 驱动没有声明该设备 ID;
- 设备已被另一个驱动绑定;
- PCI、USB 或 IOMMU 层初始化失败;
- 驱动探测阶段失败。
三、insmod、modprobe、rmmod 和 modinfo
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 工具集,通常会:
- 根据当前内核版本搜索
/lib/modules/$(uname -r)/; - 使用
modules.dep找到依赖; - 使用
modules.alias根据设备别名匹配模块; - 读取
/etc/modprobe.d/*.conf、发行版提供的配置目录; - 递归加载依赖;
- 把参数传给内核;
- 按逆依赖关系尝试卸载。
模块名中的连字符和下划线通常会被规范化。例如:
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 状态;signer、sig_id、sig_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)/ 后,如果没有运行 depmod,modprobe 可能找不到它,即使文件实际存在。
推荐的安装顺序是:
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 符号依赖的基本条件
模块可以导出符号,另一个模块可以引用这些符号。设模块 引用的符号集合为:
系统当前已导出的符号集合为 。要让 成功装入,至少需要:
如果某个符号不在 中,内核无法完成重定位,加载会失败。
若模块 导出 ,模块 导出 ,则加载 前至少要加载:
如果 又依赖 ,完整依赖闭包是:
modprobe A 会根据 modules.dep 递归处理这个闭包;insmod A.ko 通常不会自动完成这一步。
查看依赖:
modinfo -F depends e1000e
查看实际加载结果:
lsmod
lsmod 的 Used 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
能否运行时修改取决于参数声明和模块实现。常见情况有三种:
- 只读参数:只能在加载时设置;
- 可写参数:可以通过 sysfs 修改;
- 修改后不完整生效:参数变量改变了,但驱动只在初始化时读取一次。
因此,写入 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。
因此,启用模块签名时,内核会在加载阶段验证模块签名。签名不是加密,通常只保证:
- 模块内容没有被篡改;
- 模块由受信任的私钥对应方签发。
它不保证模块没有漏洞,也不保证驱动行为符合业务要求。
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)"
每一步的含义:
add:把源码版本登记到 DKMS;build:使用目标内核的构建目录和头文件编译;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 安装成功”至少要拆成四个问题:
- 是否对目标内核构建成功;
- 是否安装到目标内核的模块目录;
depmod是否生成索引;- 模块是否能够通过签名验证并在启动时加载。
检查:
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 三种不同的加载来源
模块可能由以下路径加载:
- 设备发现自动加载:内核产生设备事件,用户空间根据 alias 请求模块;
- 启动配置加载:initramfs 或 systemd 单元显式加载;
- 管理员手动加载:执行
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 卸载的条件
模块卸载通常要求:
引用可能来自:
- 另一个模块;
- 打开的设备文件;
- 已注册但尚未释放的网络、块设备或文件系统对象;
- 用户进程持有的文件描述符;
- 内核线程或异步工作;
- 设备仍然绑定在该驱动上。
查看:
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 中的临时用户空间。它需要完成:
- 加载存储控制器和文件系统模块;
- 发现根设备;
- 组装 RAID、LVM 或解锁加密卷;
- 挂载真实根文件系统;
- 将控制权交给真实根文件系统中的 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 黑名单的恢复风险
如果错误地把根存储驱动、文件系统模块或早期加密模块加入黑名单,系统可能无法启动。恢复时应:
- 从旧内核或救援介质进入;
- 删除或注释错误的
blacklist、install ... /bin/false、错误参数; - 重建目标 initramfs;
- 重新验证其中的模块和配置;
- 再次启动。
不要只修改真实根文件系统中的配置而忘记 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、/dev、ip link、lsblk等相应接口中。
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 status、modinfo、lsinitramfs/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 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux 中断与 Softirq:IRQ、Bottom Half、ksoftirqd 和延迟
- 下一篇:Linux 设备模型与 udev:设备号、sysfs、规则、热插拔和权限
- 延伸:Linux 启动完整流程:Firmware、Bootloader、Kernel、initramfs 与 systemd
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论