升级内核后网卡驱动消失排查实战:DKMS 重建原理、linux-headers 元包与 grub-reboot 自动回滚

内核一升级,网卡就没了

这是云主机上很典型的一次翻车:你执行了 apt upgrade,内核从 5.10.0-26 升到 5.10.0-28,重启之后 SSH 连不上。救援控制台里一片茫然——ip a 只有 lo,网卡设备整个消失了。dmesg 里既没有报错也没有网卡初始化日志,像是根本不存在这块设备。

原因通常不是驱动被删了,而是驱动是编译出来的,它只为旧内核编译过。内核升级后,旧编译产物对新内核无效,而系统初始化时又没法凭空变出新版驱动,于是网卡、RAID 卡控制器、虚拟化增强驱动这类"非主线随内核走"的模块就集体罢工了。

这个机制的名字叫 DKMS(Dynamic Kernel Module Support,动态内核模块支持)。理解它,就能避免每次内核升级都提心吊胆。

DKMS 到底做了什么

Linux 内核模块(.ko 文件)是二进制,它和内核版本强绑定。内核里的一些内部结构(比如函数签名、结构体布局)在不同版本间会变,模块加载时会校验版本符号,不匹配就拒绝加载。这就是为什么升级内核后,任何"厂商提供、需要单独编译"的驱动都必须重新编译。

DKMS 的作用就是把这个"重新编译"automate 化。它在系统里注册一个模块源码包,并挂钩到内核安装流程。当你安装一个新内核时,包管理器触发 DKMS hooks, DKMS 会在后台为新内核版本重新编译并安装该模块。整个过程应该是自动的、无感的。

既然自动,为什么还会翻车?因为 DKMS 的自动重建有三个前提,只要有一个不满足,重建就会静默失败:

  1. 内核头文件(headers)必须存在。没有 linux-headers-$(uname -r),编译器连内核的内部结构定义都拿不到,编译无从谈起。
  2. 编译工具链必须完整。DKMS 本身不装 gcc、make,它只调用。如果系统里缺少 build-essential 或者 dkms 包被误删,重建就会失败。
  3. 重建必须在重启前完成。DKMS 是在内核包安装时触发的。如果你装完内核立刻重启,而 DKMS 的构建任务恰好失败(或者还在排队),新内核启动时就是没有那个模块的状态。

最要命的是:三个前提的失败都不影响 apt upgrade 的返回码。命令成功了,系统告诉你一切正常,重启后才发现网卡没了。

升级内核的正确顺序(七步清单)

我已经把下面这套顺序固化成了习惯,执行内核升级前必走一遍。全部命令都在 sudo 权限下。

第一步,记录当前内核版本和已注册的 DKMS 模块。这是"后悔药"的基础——万一出事,你至少知道要恢复到哪个状态:

uname -r
dkms status

dkms status 会列出每个模块的当前状态。重点看有没有 installed 之外的条目——如果显示 built 但没有 installed,说明该模块存在版本不匹配的隐患,先处理它再升级内核。

第二步,确认头文件与新内核包是一体的。Debian/Ubuntu 上有元包能帮你保证这一点:

# 确保装了元包,内核升级时会自动带上对应的 headers
apt-get install -y linux-headers-amd64

# 或者按当前运行内核精确安装
apt-get install -y linux-headers-$(uname -r)

首选元包(linux-headers-amd64)的做法。元包会自动跟随新内核版本,不需要每次手动装;而 $(uname -r) 的写法装的是"当前运行内核"的头文件,对升级后的新内核无效。很多人升级后头文件缺失,就是因为一直用后一种写法。

第三步,确认 DKMS 和编译工具在。这两样是基础设施,缺了后面全白干:

dpkg -l dkms build-essential | grep -E '^ii' || apt-get install -y dkms build-essential

更完整一点,内核模块编译需要的包集合是 dkms build-essential linux-headers-$(uname -r) 三个,少哪个都会在某个模块上炸出来。

第四步,执行升级,但暂时不要重启。升级动作本身用这条:

apt-get update
apt-get upgrade

如果要升级发行版主版本(比如 Debian 11 到 12),用 apt-get dist-upgrade,它允许包管理器调整依赖关系。这一步执行完,先别急着 reboot——下两步才是关键。

第五步,立刻检查 DKMS 重建结果。这一步是整套流程的核心,也是绝大多数人跳过的一步:

dkms status

你要看的输出格式是 module/version, kernel-version, x86_64: installed。每个模块都应该针对新内核版本显示 installed。如果某个模块的行里新内核版本显示的是 build、built 甚至是空的,或者新内核版本压根没出现在列表里——那就是重建失败了,现在必须修,不能重启。

手工触发一次重建:

# 语法: dkms install <模块名>/<版本> -k <内核版本>
dkms install nvidia/535.104.05 -k $(ls /lib/modules | sort -V | tail -1)

里面那个 ls /lib/modules | sort -V | tail -1 是"取最新的内核版本号",-V 让 sort 按版本号语义排序而不是按字典序——不然后面会出 5.10.0-9 排在 5.10.0-28 后面的诡异结果。

第六步,验证新内核的模块文件真的落地了。dkms status 说 success 不代表文件一定在正确位置,直接去文件系统里确认:

K=$(ls /lib/modules | sort -V | tail -1)
ls -l /lib/modules/$K/updates/dkms/

DKMS 编译出的模块默认落在 /lib/modules/<版本>/updates/dkms/ 目录下。这个路径必须能看到你的 .ko 文件。空目录或者只有目录结构没有文件,是典型的"DKMS 报成功但实际没装"——通常因为模块名不匹配或者安装脚本里的路径写错了。

顺便刷新一下模块依赖表,确保 modprobe 能找到它:

depmod -a $(ls /lib/modules | sort -V | tail -1)

第七步,用 at 做定时回滚保险。这一步解释一下为什么值得做:重启后如果新内核起不来,你无法通过 SSH 回来下回滚指令——网络都没了。所以回滚指令必须在重启之前就安排好,由系统在本地自动执行。

# 安排 15 分钟后自动切回当前内核并重启
echo "grub-reboot 'Advanced options>Debian GNU/Linux, with Linux $(uname -r)' && reboot" | at now + 15 minutes
atq   # 确认任务已排队

# 现在重启去测新内核
reboot

这里用 grub-reboot 而不是改 GRUB_DEFAULT,是因为它只在下次启动生效一次,不用改配置文件也不用 update-grub,不会留下"下次开机又切回旧内核"的后患。重启后如果能正常上线,回到终端执行:

atrm $(atq | awk '{print $1}')   # 取消定时回滚

如果新内核真的有问题,15 分钟后服务器会自己切回旧内核重启,你又能连上了。代价是十五分钟的额外等待,收益是不需要开救援控制台。这个交换非常划算。

一个真实案例:一次"升级两次内核"的事故

有个朋友的机器装的是一块需要 DKMS 驱动的 RAID 卡。他执行 apt upgrade 时内核一次从 5.10.0-26 升到 5.10.0-28(中间跳过了 -27),升级过程中 apt 输出里有一段 dkms: building for 5.10.0-28 failed。他看到了没在意,直接重启。

结果:新内核启动,RAID 卡驱动缺失,数据盘全部不在,系统卡在紧急模式,只能进救援。

救援台上的排查路径值得记一下。首先确认模块是否存在:

ls /lib/modules/5.10.0-28-amd64/updates/dkms/

目录是空的。再去看 DKMS 的日志,这才是真正的根因所在:

cat /var/lib/dkms/*/*/build/make.log 2>/dev/null | tail -30

make.log 直接给出了编译错误:找不到某个内核头文件。再查包状态:

dpkg -l linux-headers-5.10.0-28-amd64

显示状态是 iU(已解包但未配置)——升级过程中头文件包配置失败,而 apt 在非交互模式下没有中断整个事务。修复很简单,把软件源访问修好(救援模式下网络往往是另一套配置),然后补上这一步:

apt-get -f install
apt-get install -y linux-headers-5.10.0-28-amd64
dkms autoinstall -k 5.10.0-28-amd64

dkms autoinstall -k <版本> 会针对指定内核版本重建所有已注册模块,比重敲每个模块名省事得多。重建成功后:

dkms status
ls /lib/modules/5.10.0-28-amd64/updates/dkms/

两个检查都过了才重启。这一次重启,RAID 卡正常识别,数据盘回来了。

这个案例最大的教训不是"要装头文件",而是DKMS = dkms + build-essential + linux-headers 三者齐全。linux-headers-amd64 这个元包必须提前装好,它会自动给每个新内核配齐对应头文件;否则每一次内核小版本升级,你都在赌头文件包恰好能成功配置。

长期策略:让内核升级不再需要人盯着

个人站长维护服务器,不可能每次都手工走七步。三个自动化手段能把风险压到最低。

一、限制内核升级频率。内核升级收益有限、风险不低,而自动安全更新非常适合内核——它们在后台悄悄换内核,却不会自动重启,风险会累积到下次你手动重启时集中爆发。unattended-upgrades 默认只装安全更新,可以接受,但要确保配套包也在更新范围内:

# 查看当前自动更新范围
cat /etc/apt/apt.conf.d/50unattended-upgrades | grep -A5 'Allowed-Origins'

更稳妥的做法是让内核升级仅在维护窗口进行,把 linux-image-* 加入黑名单,避免半夜自动换内核之后第二天重启踩坑。

二、加一个 DKMS 健康检查脚本。把它接到监控里,任何模块出现"未针对当前内核安装"的状态就告警:

#!/bin/bash
# /usr/local/bin/dkms-health.sh
K=$(uname -r)
fail=0
while read -r line; do
  mod=$(echo "$line" | awk '{print $1}')
  # 检查当前运行内核是否已安装
  dkms status "$mod" | grep -q "$K.*installed" || { echo "DKMS MISSING: $mod for $K"; fail=1; }
done < <(dkms status)
exit $fail

这个脚本的价值在于:它检查的是当前运行内核对应的模块状态,而不是"DKMS 有没有报错"。两者差别很大——DKMS 历史上成功过,不代表现在这个内核的模块就绪。

三、把自动重启关掉,由人决定时机。内核包安装后,needrestart 或者系统的提示可能建议立即重启。在服务器上,重启时机必须由你控制——确保 DKMS 重建已经完成、回滚保险已就位、并且你手边能访问救援控制台。检查是否有待重启标记:

[ -f /var/run/reboot-required ] && cat /var/run/reboot-required*

小结:内核升级的本质是"编译产物与内核的同步问题"

把这件事抽象一下:服务器上任何"需要为特定内核编译的模块",都存在与内核版本的耦合。内核升级本身很安全,危险的是耦合没有跟着更新、而你不知道。所以整篇的核心动作只有两个:

  • 重启前查 —— dkms status 加目录文件检查,确认新内核的模块都已就位;
  • 重启前留退路 —— 用 grub-reboot 加 at 安排一次性自动回滚。

这两个动作加起来不到两分钟,却能把"内核升级"从一件需要挑时间、守着做的高风险操作,变成一件随时可以做的常规维护。个人站长精力有限,这类"低成本换高确定性"的习惯,比学任何高级技巧都值。

Last modification:September 26th, 2026 at 12:25 pm

Leave a Comment