☰
DKMS幽灵内核清理指南:状态残留排查与根治顺序
2026/10/7 10:13:08 网站建设 项目流程

那天晚上我只是想顺手清掉机器上几个用不到的旧内核,结果在dkms status里看到一件让我后背发凉的事:uname -r明明是 6.8.0-31-generic,6.5 的内核镜像也早就被apt purge干掉了,但 i915-sriov-dkms 对6.5.0-41-generic的状态仍然显示 installed。再顺手du -sh /var/lib/dkms/i915-sriov-dkms/*一看,好家伙,光是这一个模块的历史构建残留就有 110M,而且每次系统更新触发 DKMS autoinstall 时,它都会“温柔”地再编译一遍这些已经不存在的内核。这类账面上还活着、实际已经没有对应内核文件的编译残留,就是我标题里说的“幽灵内核”。

这篇文章不只是一次清理记录。我会把 DKMS 的状态机制、排查路径、根因、干净清理的顺序,以及以后怎么避免再犯同样的错误,全部讲清楚。如果你正在 Ubuntu/Debian 系上用 DKMS 管理内核模块,尤其是搞 i915-sriov 这类需要随内核重编的重型驱动,或者只是心疼自己每次升级都要被白白的编译时间折磨,那这篇应该能帮上忙。

1. 先看懂 DKMS 的“账本”在哪里:三个目录把模块状态记得明明白白

1.1 源码、构建树、安装点:DKMS 眼里的三类目录

DKMS 不像你在/lib/modules/$(uname -r)/kernel里看到的那些随内核一起打包的模块,它是独立管理的第三方模块。它要在内核版本变化后自动重编模块,就必须有一本自己的“账本”,记录每个模块版本为哪些内核编译过、编译产物装到了哪里。这本账本主要落在三个目录上。

第一个是源码目录,通常在/usr/src/i915-sriov-dkms-<version>。我这边装的是社区打补丁版 i915,dkms add的时候会把源码解压到这里,DKMS 编译前就去里面读dkms.conf,确认模块名、版本号、构建命令。这个目录原则上不要手动画,它只是素材库。

第二个是构建状态目录,也就是/var/lib/dkms/i915-sriov-dkms/<version>/。这是 DKMS 真正的“内部工作台”,里面会按内核版本分出子目录,每个内核版本都有自己的一套 build 中间产物和 module 产物。我看过里面结构,每个内核版本下面又按架构分目录,最终module/里放编译好的.ko,build/里是大量.o文件。这就是 110M 的主要来源。

第三个是安装点目录,/lib/modules/<kernel-version>/updates/dkms/。dkms install会把编译好的模块复制到这里,之后modprobe i915执行时,系统会优先从updates/dkms里找模块,而不是去内核自带的路径里找,这样补丁版 i915 才能覆盖上游驱动。

搞清这三个目录的关系后,再解释“幽灵内核”就很简单:DKMS 判断一个内核版本是否还需要编译模块,依据的是/lib/modules/<version>/build这个链接是否存在,加上状态库里有没有对应记录,而不是看这个内核能不能启动、在不在/boot下。这个认知差异是后续一切问题的根源。

1.2 i915 这种大模块为什么会攒出 110M

很多人第一次看到 110M 这个数字会吓一跳,觉得一个驱动哪有这么大。其实 i915 本来就不是那种几百 K 的小模块。它是 Intel 核显驱动,上游源码体量很大,要支持从老 Ivy Bridge 到最新 Arc 的一堆 GPU,编译出来一个 i915.ko 就有几十 MB,中间产物.o更是成百上千个。

而 SR-IOV 版本又是在这个大驱动上打补丁重编,所以每次编译都相当重。如果状态库里同时挂着三四个内核版本的记录,每个版本一份独立的构建目录,那 110M 一点不夸张。我用个生活类比你就懂了:相当于你为一把早就换掉的旧门锁配了三把备用钥匙,钥匙主人早就不在了,但备用钥匙还一直躺在抽屉里占地方,每来一个新的家人(新内核),你还会重新配一遍。

2. 现场勘察:从 dkms status 到目录,把幽灵内核一个个挖出来

2.1 第一步:先盘点“活着”的内核

排查的第一步永远是搞清楚这台机器现在到底有哪些内核。先跑这两个命令:

uname -r ls -1 /lib/modules/

uname -r是当前正在跑的内核,ls /lib/modules是系统里所有还留有模块目录的“候选内核”。一台干净点的机器输出大概长这样:

6.8.0-31-generic 6.5.0-41-generic 6.5.0-42-generic 6.8.0-31-generic

注意,第二行和第三行里可能就混着幽灵项。判断它是不是幽灵,光看ls还不够,要用包管理器确认这些内核版本是否真的有对应的linux-image包:

dpkg -l | grep -E 'linux-image|linux-headers'

如果dpkg -l里找不到linux-image-6.5.0-41-generic,但/lib/modules/6.5.0-41-generic还在,那这就是一个没有“宿主”的残留目录。这本身就是线索。

2.2 第二步:看看 DKMS 的账本上都有谁

接着看 DKMS 的状态:

dkms status

输出大概是这种格式:

i915-sriov-dkms/20240927, 6.5.0-41-generic, x86_64: installed i915-sriov-dkms/20240927, 6.5.0-42-generic, x86_64: built i915-sriov-dkms/20240927, 6.8.0-31-generic, x86_64: installed

每一行代表:模块版本、针对的内核版本、架构、状态。installed表示模块已经装进了/lib/modules/<ver>/updates/dkms,built表示只编译过但没安装。现在把这个列表和第一步的/lib/modules对照,凡是 DKMS 状态里有、但/lib/modules下已经没有对应目录(或者目录里连build链接都不完整)的,基本就是幽灵项。

2.3 第三步:顺着目录坐实证据

光看还不行,必须把目录级的证据挖出来。我用这三条命令把现场固定下来:

du -sh /var/lib/dkms/i915-sriov-dkms/*/ find /var/lib/dkms/i915-sriov-dkms -maxdepth 2 -type d ls -l /lib/modules/6.5.0-41-generic/build

du -sh直接看出每个内核版本构建目录占多大,110M 就是这么一处处堆出来的。find看结构里有多少个内核版本子目录。ls -l能确认/lib/modules/<ver>/build这个符号链接还在不在、指向哪里。正常情况它应该指向/usr/src/linux-headers-<ver>,如果指向的目录已经不存在了,那么这个“内核”在 DKMS 眼里就是一个只能报错的重编译对象。

在我那次排查里,最直观的证据就是对账结果:

  • /lib/modules下压根没有6.5.0-41-generic目录了
  • 但/var/lib/dkms/i915-sriov-dkms/<version>/6.5.0-41-generic/完整无损
  • dkms status里还是installed状态

三个事实合起来,“幽灵内核”基本就实锤了。

3. 根因复盘:卸载内核时为什么状态没跟着清掉

3.1 卸载动作的钩子断了

按正常流程,在 Debian/Ubuntu 系里卸载linux-image-<版本>时,包管理器会触发/etc/kernel/下的 hook 脚本,DKMS 也是通过这种 hook 接到“某内核被移除”的通知,然后自动把该版本的模块记录清掉。这条链路只要完整,理论上不会出现幽灵内核。

但现实里钩子很容易断。最常见的情况是你直接在 shell 里执行了rm -rf /lib/modules/6.5.0-41-generic,觉得只要把目录删了就算清干净了。实际上包管理器对内核包的记录还在,DKMS 状态库里那条记录也毫发无伤。更糟糕的是,过段时间你装了新内核,dkms autoinstall把账本里所有内核版本都过一遍,发现那个 6.5 的构建目录还在,就继续编译,结果因为/lib/modules/<ver>/build已经没了而报错,白白浪费时间还要吓你一跳。

另一种典型情况是 hook 脚本依赖顺序问题。比如你先手动卸载了 dkms 包,再卸载内核,自然的,内核卸载时没人通知 DKMS 了。等以后重新装 dkms 和 i915-sriov-dkms,它扫描已有内核目录时如果碰到残留记录,又会把它们重新纳入构建范围。

3.2 只删 image 不删 headers:内核在 DKMS 眼里就是“活的”

这是我认为最隐蔽的一点,值得单独拿出来说。很多人的清理习惯是只卸linux-image-xxx,因为觉得“反正启动不了的内核留着占 /boot 空间”。但他们往往会留下linux-headers-xxx,因为怕哪天要用。

问题就出在这里。DKMS 编译模块时根本不需要vmlinuz,也不需要这个内核真的能引导。它只需要/lib/modules/<版本>/build这个符号链接能用,而build又指向/usr/src/linux-headers-<版本>。只要 headers 包还在,DKMS 就有完整的头文件、配置和Module.symvers来编模块。

换句话说,你删了 image 但留着 headers,在 DKMS 的视角里这个内核“软件环境”依然是完整的、值得服务的。于是每次 autoinstall 触发,它都会任劳任怨地把这个版本重新编译一遍,编译完了装进/lib/modules/<版本>/updates/dkms,甚至还会顺手写进当时的 initramfs 里。最后你开机从 6.8 跑起来,根本用不上这份模块,但它确实占了磁盘、花了编译时间。

3.3 dkms remove 和 autoinstall 的状态机认知差异

还有一层认知差异会让问题雪上加霜。很多人以为dkms autoinstall只会处理当前正在运行的内核,其实在常见配置下,它会遍历/var/lib/dkms状态库里记录到的全部内核版本。这就好像你明明今天只开了其中一辆车出门,但车库管理员把两辆车都保养了一遍——因为他台账上两辆车都在。

正确做法应该是,删除某个内核版本前,先单独对这个版本执行dkms remove --all -k <版本>,把它的模块记录、编译产物全部拆掉,再去卸载 image 和 headers。顺序一旦反了,DKMS 就像一个记忆力极好的管家:主人搬走了,他还天天打扫空房间。

4. 动手修复:按规范拆掉幽灵内核,释放 110M 构建残留

4.1 先试标准摘除命令

清理的第一步是让 DKMS 自己“体面地删除”。针对每个要清理的内核版本,指定-k参数精确摘除:

sudo dkms remove -m i915-sriov-dkms -v <version> -k 6.5.0-41-generic

这里<version>换成dkms status里显示的版本号,比如20240927。这条命令会删除/var/lib/dkms/i915-sriov-dkms/<version>/6.5.0-41-generic下的构建记录,并尝试把/lib/modules/6.5.0-41-generic/updates/dkms/i915.ko等已安装文件摘掉。

如果内核目录已经不完整,或者状态记录已经损坏,dkms remove可能会报类似 “Module ... is not registered” 的错误。这种情况不用慌,说明状态里已经没有对应子项可删,或者目录内容不完整。标准命令走不通,就走下面兜底。

4.2 手动拆状态的兜底操作

兜底其实是对准账本做一次“外科手术”。直接删掉/var/lib/dkms下对应版本的残留子目录:

sudo rm -rf /var/lib/dkms/i915-sriov-dkms/<version>/6.5.0-41-generic

这条命令只删一个特定内核版本的构建产物,不动其他内核版本,不影响 DKMS 整体状态。跑完后dkms status里关于这个内核版本的一行会消失。要注意,千万别手滑变成sudo rm -rf /var/lib/dkms,那是删掉全部模块账本,等于把整个 DKMS 仓库端了,之后所有模块记录都要重新构建。

如果/lib/modules/6.5.0-41-generic整个目录都是残留的(dpkg -l里已经查不到对应 image 和 headers),也可以连根删:

sudo rm -rf /lib/modules/6.5.0-41-generic

如果连/usr/src下对应的旧 headers 目录也确认没用了,再把 headers 包一起 purge 掉:

sudo apt purge -y linux-headers-6.5.0-41-generic linux-image-6.5.0-41-generic

多一句嘴:purge 时如果系统提示某些包不存在,就只保留实际存在的包名,别硬凑一个不存在的包名进去导致命令返回非零。

4.3 验证与恢复 initramfs

清理完,别忘了更新 initramfs。特别是 i915 这类驱动很早就被 initramfs 加载,模块路径如果变了,不更新的话会影响后续开机的模块装载一致性。执行:

sudo update-initramfs -u

不想全量更新的话,可以指定版本:

sudo update-initramfs -u -k 6.8.0-31-generic

最后重新核对三处:

dkms status ls -1 /lib/modules/ du -sh /var/lib/dkms/i915-sriov-dkms

dkms status里幽灵项消失,/lib/modules下只剩真实安装的内核目录,du显示的大小从 110M 掉到几十 M,这一趟修复就算闭环了。

5. 以后的防丢魂操作:删内核前先断 DKMS 的“念想”

5.1 正确摘除一棵内核的安全顺序

吃一堑长一智,我现在清理内核的标准动作是固定的:先让 DKMS 忘记这个内核,再卸载内核镜像和头文件,最后统一更新 initramfs。每一步都不要省,顺序更不能乱。

步骤动作意图
1sudo dkms remove --all -k <版本>清掉该版本所有模块记录,释放构建目录
2sudo apt purge linux-image-<版本> linux-headers-<版本>删除内核镜像与头文件,让/lib/modules/<版本>/build链接失效
3复查dkms status与ls /lib/modules确保没有残留子项和幽灵目录
4sudo update-initramfs -u让当前内核的 initramfs 回归干净状态

注意第 1 步用的是--all,意思是把这个内核版本下的所有 DKMS 模块一起摘掉,而不是只摘 i915-sriov-dkms。如果你机器上还挂着一堆别的 DKMS 模块,一次摘干净最省事。

5.2 顺手写一个清理脚本

因为这种清理以后难免会再遇到,我干脆写了个小脚本放在本地,每次要删旧内核就跑一下。核心逻辑不复杂:

#!/bin/bash # 用法: ./kernel_cleanup.sh <kernel-version> KVER="$1" if [ -z "$KVER" ]; then echo "用法: $0 <kernel-version>" exit 1 fi CURRENT=$(uname -r) if [ "$KVER" = "$CURRENT" ]; then echo "不能删除当前正在运行的内核: $CURRENT" exit 2 fi # 1. 摘除该内核版本下所有 DKMS 模块记录 for mod in $(dkms status | awk -v k="$KVER" '$2 ~ k {print $1}' | sort -u); do echo "=> dkms remove $mod -k $KVER" sudo dkms remove "$mod" -k "$KVER" || sudo rm -rf "/var/lib/dkms/${mod%/*}/${mod#*/}/$KVER" done # 2. 卸载内核镜像与头文件 sudo apt purge -y "linux-image-$KVER" "linux-headers-$KVER" \ "linux-modules-$KVER" "linux-modules-extra-$KVER" 2>/dev/null || true # 3. 清理 /lib/modules 下的残留目录 if [ -d "/lib/modules/$KVER" ]; then sudo rm -rf "/lib/modules/$KVER" fi # 4. 更新 initramfs sudo update-initramfs -u echo "完成: $KVER 已清理"

脚本里第 1 步的|| sudo rm -rf是兜底逻辑:如果dkms remove因为状态损坏失败,就直接删对应版本的状态目录。第 2 步加了2>/dev/null || true,是因为某些内核版可能没有linux-modules-extra-xxx这个包,忽略不存在的包名不会让脚本中断。第 3 步在整个流程最后做,因为默认情况下 apt purge 会自己删/lib/modules,只有 purge 失败或包不存在时才需要手动兜底。

5.3 对 i915-sriov-dkms 的日常维护提醒

最后说几个针对这类“重型 DKMS 模块”的维护心得。装了 i915-sriov-dkms 之后,每次内核升级都意味着一次大编译,这是不可避免的,但我们可以避免重复劳动。维护习惯上我给三条建议:

第一条,升级内核后先看编译日志。/var/lib/dkms/i915-sriov-dkms/<version>/.../make.log里记录了完整编译输出,如果模块没装上,先去日志里找根因,不要反复dkms autoinstall硬试。很多问题其实是linux-headers-新版本没装导致的,装好头文件再编译一次就能过。

第二条,只为你真正会用的内核保留状态。如果你长期保留两三个内核做回滚,那么 DKMS 里有两三份构建树是正常的,各占几十 M,不算浪费。但如果某个内核已经确定不再回滚使用,就按 5.1 的顺序把它连头带状态一起清理,不然每个新内核进来都会额外加重编译负担。

第三条,真要弃用 SR-IOV 时,卸载顺序别马虎。先关闭虚拟机里正引用的 VF 设备,再dkms remove --all把 i915-sriov-dkms 全部摘掉,然后 purge 掉源码包,最后update-initramfs -u。如果不更新 initramfs,旧模块的信息可能还留在 initramfs 里,重启后系统会尝试加载一个已经不存在的模块,轻则报错,重则影响正常桌面启动。

我在实际维护里还有个习惯:每次跑apt upgrade之前,先花十秒钟过一遍dkms status。只要看到某个内核版本既不在ls /lib/modules里、也不在当前uname -r中,我就顺手按上面的步骤把它清了。这个习惯以前能帮我省下十几分钟的编译时间,现在也推荐给你——毕竟把时间花在真正服务自己的内核上,才不算浪费。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询