内核模块加载不上:六类原因与三条管控线

从一次 modprobe 失败说起

上周给一台新装的 Debian 12 加 BBR,按老习惯写了 /etc/modules-load.d/bbr.conf,重启后 sysctl net.ipv4.tcp_congestion_control 显示还是 cubic。modprobe tcp_bbr 倒是能手动加载成功,但开机就是不自动加载。查了半天才发现:我把模块名写成了 bbr,正确的名字是 tcp_bbr——/etc/modules-load.d 里的加载失败是静默的,不报错、不进 journal 的 error 级别,只在 debug 级别的日志里留一行。

这件事让我意识到,内核模块这块的东西零散、文档少、失败还没声音,但它们和日常建站运维关系比想象中大得多:网卡驱动、文件系统、防火墙 conntrack 表、加密算法、Docker 的 overlay2 和 br_netfilter,全都靠模块。这篇文章把内核模块从"怎么加载"到"为什么加载不上"再到"怎么管控"讲一遍。

模块从哪里来:三条加载路径

首先要建立一个基本认识:内核模块不是"装上就有"的,它需要显式加载。Linux 上的加载有三条路径,优先级和时机都不同。

路径一:开机自启清单 /etc/modules-load.d/

这是 systemd 提供的方式,最推荐。写法极其简单,一个文件名对应一组模块:

# /etc/modules-load.d/site.conf
tcp_bbr
br_netfilter
nf_conntrack
overlay

注意几点:

  • 文件名任意,但必须以 .conf 结尾,否则被忽略;
  • 每行一个模块,不带参数(要参数得用 modprobe.d,见下);
  • 模块名是下划线形式,不是文件名里的横线(nf_conntrack 而非 nf-conntrack);
  • 这个文件只负责"开机加载",不会自动解决依赖——但 modprobe 本身会,所以你不必把依赖也列上。

老写法 /etc/modules 仍然可用(Debian 系兼容),但 systemd 系统上优先用 modules-load.d。

路径二:按需自动加载(udev + alias)

大部分模块其实不需要你手动写清单。内核的模块自动加载机制是这样的:

  1. 设备出现(插网卡、挂载某文件系统)→ 内核发出 uevent;
  2. udev 根据设备的 modalias 查 /lib/modules/$(uname -r)/modules.alias;
  3. 找到对应模块名,调用 modprobe 加载;

所以"插上一块网卡驱动自动就有了"并不是魔法。判断某个模块是不是走这条路,可以查 alias:

modinfo -F alias nf_conntrack
modinfo -F filename tcp_bbr

反过来说,如果你遇到"某个模块怎么都不自动加载",先查它有没有 alias。没有 alias 的模块(比如 tcp_bbr 这种纯算法模块)永远不会被 udev 触发,只能靠清单或者显式 modprobe。这就是我上面踩的那个坑的根因。

路径三:程序运行时显式加载

Docker 启动时会自己 modprobe overlay、br_netfilter;iptables 会加载 nf_conntrack、xt_conntrack;这种由程序触发的加载,配置写在 /etc/modprobe.d/*.conf 里,比如禁用 IPv6 时常见的:

# /etc/modprobe.d/disable-ipv6.conf
options ipv6 disable=1

但要注意,options 只是传参数,并不会阻止模块被加载。真要屏蔽,得用 install ... /bin/true 或者给模块改名:

# 真正屏蔽:让加载动作变成"什么都不做"
install dccp /bin/true
blacklist dccp

blacklist 与 install /bin/true 的区别是常考的点:blacklist 只阻止自动加载,手动 modprobe dccp 仍然能加载;install xxx /bin/true 则连手动加载也变成空操作。安全加固场景要两个一起写。

查:怎么确认一个模块到底加载了没有

我见过太多人用错命令。四个命令各管一件事:

lsmod | grep bbr                 # 已加载的模块(读 /proc/modules)
modinfo tcp_bbr                  # 模块是否存在、路径、依赖、参数
modprobe --dry-run --verbose tcp_bbr   # 试着解析依赖链,看会动哪些模块
cat /proc/modules | head         # lsmod 的原始来源

三个关键点:

  • lsmod 只列出可卸载的模块。内核编译进去的(builtin)不会出现在 lsmod 里,但它确实在。查内置模块用 grep CONFIG_XFS_FS /boot/config-$(uname -r),值为 =y 是内置、=m 是模块、不出现是没编译。
  • modinfo 找不到模块时会报 Module not found,这时先确认 uname -r 与 /lib/modules/ 下的目录名是否一致。内核升级后没重启,uname -r 还是旧版本,但 modprobe 走的是新版的 depmod 缓存——这类"版本错位"是新机器上模块加载失败的头号原因。
  • --dry-run 特别有用:它打印"如果要加载这个模块,会先加载哪些",不改动任何状态。排查依赖冲突时先跑它,比直接 modprobe 安全。

读:modinfo 输出的六个字段

modinfo 的输出字段不多,但每个都有用:

filename:       /lib/modules/6.1.0-31-amd64/kernel/net/ipv4/tcp_bbr.ko
license:        Dual BSD/GPL
depends:        tcp_congestion_ops
retpoline:      Y
name:           tcp_bbr
vermagic:       6.1.0-31-amd64 SMP preempt mod_unload modversions
  • depends:这一栏空着说明没有依赖,非空说明必须先加载后面的模块。modprobe 会自动处理,但 insmod 不会——insmod 是"我只加载这一个"的低层命令,日常永远用 modprobe。
  • vermagic:这段字符串必须与运行内核完全一致,否则加载会被拒(报 Invalid module format)。它包含内核版本、SMP、preempt、modversions 四项。自己编译的模块装到错版本内核上,报错就是这句。
  • retpoline:现代内核的预测执行缓解标记,Y 是正常值。
  • parm / alias:modinfo -F parm <mod> 列出可调参数,-F alias 列设备匹配规则。

模块参数怎么看当前生效值?在 /sys/module/<name>/parameters/ 下:

cat /sys/module/nf_conntrack/parameters/hashsize
ls /sys/module/overlay/parameters/

这个是排查性能问题的宝地。比如 conntrack 表满导致丢包(nf_conntrack: table full, dropping packet),调的重点之一就是 hashsize,改完要重新加载模块才生效。

六个"模块加载不上"的真实原因

原因一:模块名写错,或者用了别名

这是最常见的。bbr vs tcp_bbr、connlimit vs xt_connlimit、nat vs iptable_nat。判断方法:modinfo <name> 报 not found,但 ls /lib/modules/$(uname -r)/kernel/ 下能找到——说明路径对、名字错,用 find /lib/modules/$(uname -r) -name '*bbr*' 反查。

原因二:内核版本与模块目录不匹配

uname -r
ls /lib/modules/

两个版本不一致意味着你现在跑的内核的模块目录不见了,或者新的在、旧的跑了。典型场景:apt 装了新内核但没重启,然后手动 modprobe 某个在新内核里才能用的模块。对策:升级内核后尽快重启,或者把 /etc/modules-load.d 的生效时机留给下次启动。

原因三:依赖缺失,报 Unknown symbol

modprobe nf_nat_sip
# modprobe: ERROR: could not insert 'nf_nat_sip': Unknown symbol in module

意思是这个模块引用了某个符号,但那个符号所在的模块没加载。dmesg | tail -20 会打出具体是哪个符号。对策:先用 modprobe --dry-run -v 看它打算加载什么;如果 dry-run 显示的依赖链是全的还失败,那就是 vermagic 或配置不匹配。

原因四:Secure Boot 拒绝加载未签名的第三方模块

这个坑在云主机上少见,在有 UEFI 的实体机上非常常见。症状:modprobe: ERROR: could not insert 'xxx': Operation not permitted,同时 dmesg 里有 Loading of unsigned module is rejected。查状态:

mokutil --sb-state        # 输出 SecureBoot enabled 就是开启状态

三条路:关闭 Secure Boot、给模块签名并注册 MOK(mokutil --import 需要重启进蓝色界面确认)、或者装发行版签过名的版本(比如竖排的 dkms 包)。个人站长如果不是自己编译模块,基本只会碰到第三条——装的是第三方源里的驱动包,它没被官方签名。

原因五:模块被 blacklist 了你自己不知道

排查顺序应该是先看有没有被屏蔽:

grep -r "blacklist" /etc/modprobe.d/ /lib/modprobe.d/ /run/modprobe.d/ | grep <模块名>

注意路径有三个,只查 /etc/modprobe.d 会漏掉发行版自带的屏蔽规则(比如 Ubuntu 默认屏蔽 dccp、sctp、rds、tipc 这些有历史 CVE 的模块,文件在 /etc/modprobe.d/blacklist-*.conf,由某个包安装)。

原因六:内存不足,或者模块已在使用中

rmmod overlay
# rmmod: ERROR: Module overlay is in use

卸载时报 in use,说明有引用计数没归零。lsmod 的 Used by 列会显示出谁在用,但常见情况是 Docker 的挂载点还在——哪怕容器停了,只要 /var/lib/docker/overlay2/ 下有活跃挂载,overlay 模块就摘不掉。正确顺序:停 docker、确认 findmnt | grep overlay2 为空、再 rmmod。

管:给模块划三条线

日常运维里,模块管控我只做三件事,成本很低:

线一:黑名单锁定不用的协议模块

# /etc/modprobe.d/zz-hardening.conf
install dccp /bin/true
install sctp /bin/true
install rds /bin/true
install tipc /bin/true
install cramfs /bin/true
install freevxfs /bin/true
install jffs2 /bin/true
install hfs /bin/true
install hfsplus /bin/true

这一组是常见的历史 CVE 来源,个人站 99% 用不到。写完跑 update-initramfs -u(Debian),否则早期启动阶段仍可能被加载。特别注意:这些文件必须放在 /etc/modprobe.d/ 且以 .conf 结尾,以 .conf.bak 结尾会被静默忽略——我第一版就犯了这个错。

线二:开机清单保持最小

cat /etc/modules-load.d/*.conf
lsmod | wc -l

清单里只留真正需要显式加载的(BBR、br_netfilter、overlay)。数量越少,开机越快、出错面越小。看当前已加载多少个模块:我的一台干净 Debian 12 是 40 个左右,如果某个站点的机器是 90+,值得看看多出来的是什么(通常是没用的文件系统模块或某些厂商 agent 带的)。

线三:每周记录一次差异

#!/bin/bash
# /root/bin/mod-diff.sh
set -euo pipefail
NOW=/var/log/modules.list
[ -f "$NOW.prev" ] || touch "$NOW.prev"
lsmod | awk 'NR>1{print $1}' | sort > "$NOW"
if ! diff -q "$NOW.prev" "$NOW" >/dev/null; then
  echo "模块清单变化:"
  diff "$NOW.prev" "$NOW" || true
fi
mv "$NOW" "$NOW.prev"

这个脚本能抓到"某天多了个模块"这种关键线索。上一次我靠它发现某个来路不明的模块在凌晨被加载,顺着 modinfo -F filename 找到路径不在 /lib/modules 下,最后确认是一段恶意脚本在装 rootkit。

一个容易忽略的连带问题:initramfs

如果你把自己写的模块屏蔽配置改在 /etc/modprobe.d/,但模块在 initramfs 阶段就已经被加载了,那么屏蔽不会生效。原因是 initramfs 是一个独立的 cpio 镜像,里面有自己的一份 modprobe.d 副本。所以每次改完黑名单都要:

update-initramfs -u -k all
# 检查现有 initramfs 里是否含该配置
lsinitramfs /boot/initrd.img-$(uname -r) | grep modprobe.d

同理,如果你是想让某个模块在早期启动就能用(比如某些存储控制器驱动),必须把它写进 /etc/initramfs-tools/modules,光写 modules-load.d 是不够的——那时候根文件系统都还没挂上。

小结

内核模块这块东西看着底层,但日常遇到的问题其实高度集中在几句话上:模块名对不对、内核版本对不对、有没有被 blacklist、要不要过 Secure Boot、改完有没有更新 initramfs。把这五条按顺序过一遍,90% 的加载失败都能在两分钟内定位。

而真正值得花时间的是"管控"那三条线:黑名单、开机清单最小化、周度差异记录。它们不是为了解决今天的故障,而是为了让明天的故障有一个明确的起点。

Last modification:October 22nd, 2026 at 10:23 pm

Leave a Comment