从一次 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)
大部分模块其实不需要你手动写清单。内核的模块自动加载机制是这样的:
- 设备出现(插网卡、挂载某文件系统)→ 内核发出 uevent;
- udev 根据设备的 modalias 查
/lib/modules/$(uname -r)/modules.alias; - 找到对应模块名,调用
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 dccpblacklist 与 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% 的加载失败都能在两分钟内定位。
而真正值得花时间的是"管控"那三条线:黑名单、开机清单最小化、周度差异记录。它们不是为了解决今天的故障,而是为了让明天的故障有一个明确的起点。