夜雨聆风学习资料网

ARTICLE · 1007044

内核都卸载了,DKMS 却还在为它们编译 110M 模块:揪出 i915-sriov-dkms 的“幽灵内核”

内核都卸载了,DKMS 却还在为它们编译 110M 模块:揪出 i915-sriov-dkms 的“幽灵内核”

先说结论

给 PVE 节点安装显卡虚拟化驱动 i915-sriov-dkms 时,dkms status 里冒出了 4 个"从没装过的内核版本"——每个都被完整编译了一份模块,磁盘上多吃了约 70MB,编译时间白白翻了五倍。而这些内核的镜像包,早在装驱动之前就被卸载干净了。

追凶的结论让人哭笑不得:这个驱动包自带的安装脚本里,赫然写着一行 export autoinstall_all_kernels=1——把 DKMS 的"给系统里所有内核各编译一份"开关拧到了最大。而 DKMS 判断"内核是否存在"的依据,不是 dpkg 数据库,而是 /lib/modules/ 下的目录名单加上对应 headers(内核头文件)是否还在。旧内核的镜像卸了,四套 headers 却原封不动地留在系统里,于是幽灵们拿到了"开工许可"。

修复只要三步:卸掉孤儿 headers → 清掉为幽灵内核编译的 DKMS 条目 → 删掉残留目录。本文完整复盘排查过程,并给出 Windows 11 / Ubuntu 26.04 / macOS 26 三个平台的一键体检脚本(默认演练模式),以及「人工执行」和「交给 Agent 自动配置」两种用法。

谁在给幽灵内核派活

图 1:PVE 主机装上一个驱动,DKMS 流水线却连夜给 4 个"幽灵内核"各交付了一套编译产物。

1

问题背景:给核显插上"一卡多虚"的翅膀

我的家庭实验室里有一台 PVE 物理节点,CPU 自带的 Intel 核显除了给控制台输出画面,大部分时间都在吃灰。为了让这颗核显物尽其用,我打算用 SR-IOV(单根 I/O 虚拟化)把它切成多个虚拟显卡(VF),直通给几台虚拟机分别用——硬件只有一份,算力大家分,这正是家庭服务器的经典玩法。

PVE 官方内核自带的 i915 驱动并不支持在消费级核显上开 SR-IOV,社区方案就是 strongtz 开发的 i915-sriov-dkms 项目:把主线内核的 i915 驱动打上 SR-IOV 补丁,做成 DKMS 模块发布(项目地址见文末参考资料)。当前的发布版支持 6.17.x ~ 7.0.x 内核,我的节点正好跑在 7.0.14-16-pve 上,版本完全匹配。

DKMS(Dynamic Kernel Module Support)简单说就是"内核模块的自动装修队":每次内核更新,它都会自动把第三方模块重新编译一遍,让模块始终匹配新内核。安装这类驱动通常就是 dpkg -i 一个 deb 包,装完 dkms status 里能看到 installed 就算万事大吉。

装完之后我习惯性瞄了一眼状态,然后愣住了。

2

问题表现:dkms status 里躺着一堆"查无此人"的内核

先把问题全景画出来,方便大家建立直觉:

问题全景

图 2:问题全景。系统里实际安装的内核镜像只有 7.0.14-16-pve 一个,DKMS 却交付了 5 份编译产物。

dkms status 的真实输出是这样的:

dkms status 里的四个幽灵内核

图 3:真实命令输出。四条 6.17.x 的条目状态都是 installed——编译成功、安装成功,但对应内核根本不存在。

疑点非常直白:这台机器上运行且仅安装了一个内核镜像 7.0.14-16-pve,DKMS 却为 4 个 6.17.x 版本也各编译了一份模块。更诡异的是,这几个版本的内核镜像早在装驱动之前就被我卸载了——升级到 7.0 系列后清理旧内核是标准操作,apt 里连一个 6.17 的 proxmox-kernel 包都不剩。

也就是说,DKMS 在给 4 个"幽灵内核"打工:编译它们永远用不上的模块。这既不是报错也不是警告,一切"看起来都很成功",只是磁盘在悄悄变胖(/var/lib/dkms 里躺着 110M,其中约 70M 属于幽灵),编译时间也翻了五倍。

3

问题分析:名单上有名字,图纸柜里有图纸

3.1

先排除人为因素

按照惯例,先把自己疑心的部分排除掉:

猜测
验证方式
结论
驱动包装错了版本?
核对 deb 包版本与 GitHub Release
排除,就是最新版
手动编译过残留?
/var/lib/dkms 只有本次安装的时间戳
排除
有人动过 dkms 配置?
/etc/dkms/framework.conf 干干净净
排除
DKMS 自己抽风?
干净虚拟机里复现,照样编出 4 份
实锤:确定性逻辑

最后一个验证很关键:这不是灵异事件,是确定性逻辑。那么 DKMS 到底依据什么决定"要给哪些内核编译"?

3.2

DKMS 的"住户名单":/lib/modules

DKMS 是个用户态框架,它不查 dpkg 数据库,它眼中的"系统里有哪些内核",就是 /lib/modules/ 目录下有哪些子目录——每个内核版本安装时会在这里拥有一个以版本号命名的目录,相当于小区的住户名单。列一下这份名单:

lib-modules 名单审计

图 4:真实命令输出。名单上有 8 个版本:4 个 6.17 带 headers(黄色),3 个 6.14 是连 headers 都没有的空目录,外加运行中的 7.0。

名单比想象的要长——除了运行中的 7.0.14-16-pve,还有 4 个 6.17 和 3 个 6.14。那 3 个 6.14 目录是历次升级留下的"空壳"(ls -A 数出来是 0 个文件),内核镜像卸载时没能把它们一并带走。

3.3

DKMS 的"图纸柜":headers

但"名单上有名字"只是必要条件。编译内核模块需要内核头文件(headers)——相当于按原图纸施工,没有图纸,装修队只能跳过这一户。检查方法就是看 /lib/modules/版本号/build 链接是否存在(这个链接由 headers 包创建)。而 headers 是不是还装着,得问 dpkg:

四套旧内核 headers 还在

图 5:真实命令输出(删去列)。内核镜像只剩 7.0 系列,但 4 套 6.17 的 headers 包原封不动地躺在系统里。

至此,对照组凑齐了,结论也就出来了:

  • 6.17 × 4:名单上有目录(升级后残留)+ 图纸柜里有 headers(一直没卸)→ DKMS 视为"合法住户",照单编译;
  • 6.14 × 3:名单上有空目录,但 headers 早已清掉 → DKMS 直接跳过,dkms status 里连一条记录都没有。

同样都是幽灵,待遇却天差地别,差就差在那一套"图纸"上。这也解释了图 3 里为什么偏偏是 4 个 6.17 而没有 6.14——DKMS 给幽灵编译的前提条件,是幽灵的 headers 还在系统里。

4

问题根因:一行 export 打开的"全量编译"开关

4.1

驱动包自带脚本里的那一行

名单和图纸只是"客观条件",还得有人下令开工。翻出这个 deb 包自带的安装脚本(postinst,包在安装/配置阶段执行的钩子):

postinst 里的开关

图 6:真实文件内容。安装脚本在第一件事就把 autoinstall_all_kernels 设为 1。

这个变量是 DKMS 官方维护脚本(/usr/lib/dkms/common.postinst)识别的开关。看看官方代码怎么解读它:

common.postinst 的判断逻辑

图 7:真实文件内容(节选)。变量一被导出,"只编当前内核"的默认分支整个被跳过。

语义翻译过来就是:不设这个变量时,DKMS 默认只给"当前运行内核 + 最新内核"编译;变量一旦被导出,改为遍历系统里的全部内核。而这正是 i915-sriov-dkms 打包者的选择——上游在 debian 打包脚本里显式加上了这行 export。

这个行为甚至有官方"病历"可查:Launchpad 上有一份针对 dkms 包的 Bug 报告(bugs.launchpad.net/bugs/2120641),标题就叫"DKMS only builds modules for the currently booted kernel version",报告里明确写道:默认行为源自 autoinstall_all_kernels 未设置,希望设为 yes 来让所有内核都获得模块:

Launchpad Bug 原始描述

图 8:真实网页截图。Launchpad Bug 报告从反方向印证了这个开关的存在与默认值。

4.2

把整个决策链条画出来

DKMS 决策流程

图 9:DKMS 在包安装阶段的决策流程。headers 是否在场,是幽灵能否被编译的分水岭。

根因一句话总结:i915-sriov-dkms 的安装脚本导出 autoinstall_all_kernels=1 → DKMS 遍历 /lib/modules/ 全部名单 → 4 个 6.17 幽灵"名单在册 + 图纸(headers)齐全" → 每人获得一份专属编译产物;6.14 幽灵因图纸缺失幸免。

4.3

打个比方:小区物业的装修队

如果觉得 DKMS、headers 这些词太抽象,用一个小区的比喻讲,小学生也能听懂:

小区物业装修队的比喻

图 10:小区比喻。名单 + 图纸缺一不可,而"楼还在不在"根本不在装修队的检查清单里。

  • DKMS 是小区物业的装修队,工作守则写着:照着「住户名单 + 图纸柜」,给每个有图纸的住户定制一套家具;
  • /lib/modules 是住户名单:内核装上时登记,卸载时本该划掉,但残留在所难免;
  • headers 是图纸柜:装了 headers = 图纸还在;镜像卸了图纸没卸 = 人走了图纸还压在柜子里;
  • autoinstall_all_kernels=1 是加班开关:正常物业只服务当前住着的一两户,打开开关后"名单上每一户都要做";
  • 编译产物是定制家具:6.17.13-2 这栋楼都拆了,家具照样按图纸做好,然后堆进物业仓库(磁盘)。

整个事件里没有谁是"坏蛋":上游想让所有已装内核启动时都能用上显卡虚拟化,逻辑自洽;DKMS 忠实执行名单,恪尽职守;真正的坑在于——装修队检查名单和图纸,却从不确认"这栋楼还存不存在"。

5

如何解决:把幽灵从名单上划掉

5.1

手动三步修复

思路顺着根因倒推就行,三步:

# 1) 卸掉孤儿 headers(镜像已卸载、headers 还在的,就是孤儿)apt-get purge -y proxmox-headers-6.17.13-2-pve proxmox-headers-6.17.13-21-pve \                 proxmox-headers-6.17.13-4-pve proxmox-headers-6.17.4-2-pve# 2) 清掉为幽灵内核编译的 DKMS 条目for k in 6.17.13-2-pve 6.17.13-21-pve 6.17.13-4-pve 6.17.4-2-pve; do  dkms remove i915-sriov-dkms/2026.08.12.1 -k $kdone# 3) 删掉 /lib/modules 下的残留空目录(确认对应内核包已不存在)rm -rf /lib/modules/6.17.13-2-pve /lib/modules/6.17.13-21-pve \       /lib/modules/6.17.13-4-pve /lib/modules/6.17.4-2-pve

5.2

复检三连确认修复

做完之后跑一遍"复检三连":

复检三连

图 11:真实命令输出。dkms status 从 5 条变 1 条,linux-version list 只剩运行内核,DKMS 缓存从 110M 瘦身到 39M。

dkms status 只剩当前内核一条 installed、linux-version list 只剩一行、/var/lib/dkms 从 110M 瘦到 39M——三条全中,修复完成。当前内核的模块安然无恙,显卡虚拟化功能不受任何影响。

6

一键自动体检脚本

手不想碰键盘的读者,我把"体检 → 分类 → 清理 → 复核"整个流程封装成了三平台脚本。设计上遵循三条安全底线:

  1. 默认演练模式:不带参数只输出"将会做什么"的报告,落一份到 /tmp,人看过再动手;
  2. 绝不碰运行中内核:uname -r 命中的目录无条件保留,另设 dpkg -S 占用检查,任何仍被已安装软件包占有的目录一律跳过;
  3. 元包保护:proxmox-headers-7.0 这类版本号不完整的元包不自动处理,宁可漏杀不可误杀。
脚本演练输出实录

图 12:真实演练输出(dry-run,有删节)。四个 6.17 幽灵被标记为 GHOST,运行内核被保护,元包被跳过。

6.1

方法一:人工一键执行

Ubuntu 26.04 / PVE 节点(fix-dkms-ghost-kernels.sh)——本机直接跑,或传 root@目标节点 远程体检另一台:

#!/usr/bin/env bash# fix-dkms-ghost-kernels.sh — 清理"幽灵内核"# 本机体检:   sudo bash fix-dkms-ghost-kernels.sh# 本机清理:   sudo bash fix-dkms-ghost-kernels.sh --apply# 远程体检:   bash fix-dkms-ghost-kernels.sh root@<目标节点IP># 远程清理:   bash fix-dkms-ghost-kernels.sh root@<目标节点IP> --applyset -euo pipefailTARGET=""; APPLY_ARG=""for a in "$@"; do  case "$a" in --apply) APPLY_ARG="--apply" ;; *) TARGET="$a" ;; esacdonePAYLOAD=$(cat <<'EOS'set -euo pipefailAPPLY=0[ "${1:-}" = "--apply" ] && APPLY=1REPORT="${TMPDIR:-/tmp}/dkms-ghost-kernels-$(date +%Y%m%d-%H%M%S).txt"log(){ printf '%s\n' "$*" | tee -a "$REPORT"; }RUNNING="$(uname -r)"log "== [1/6] DKMS 幽灵内核体检 $(date -Is) =="log "运行中内核: $RUNNING"INSTALLED_KERNELS="$(cd /boot 2>/dev/null && ls vmlinuz-* 2>/dev/null | sed 's/^vmlinuz-//' || true)"log "已安装内核镜像 (/boot/vmlinuz-*):"printf '%s\n' "$INSTALLED_KERNELS" | sed 's/^/  - /' | while read -r l; do log "$l"; donelog "== [2/6] dkms status 现状 =="dkms status 2>/dev/null | while IFS= read -r l; do log "$l"; donelog "== [3/6] 逐个分类 /lib/modules 下的内核目录 =="GHOST_DIRS=()for d in /lib/modules/*/; do  v="$(basename "$d")"  [ "$v" = "$RUNNING" ] && { log "KEEP  $v  (正在运行,绝不动它)"; continue; }  if printf '%s\n' "$INSTALLED_KERNELS" | grep -qxF "$v"; then    log "KEEP  $v  (内核镜像仍已安装)"; continue  fi  if dpkg -S "/lib/modules/$v" >/dev/null 2>&1; then    log "KEEP  $v  (仍有已安装的软件包占用,跳过)"; continue  fi  GHOST_DIRS+=("$v")  log "GHOST $v  (内核镜像已卸载,残留目录;文件数: $(find "$d" -type f 2>/dev/null | wc -l))"donelog "== [4/6] 需要清理的 DKMS 构建条目 =="while IFS= read -r line; do  [ -z "$line" ] && continue  kv="$(printf '%s' "$line" | awk -F', ' '/,/{print $2}' | cut -d, -f1)"  [ -z "$kv" ] && continue  [ "$kv" = "$RUNNING" ] && continue  printf '%s\n' "$INSTALLED_KERNELS" | grep -qxF "$kv" && continue  mod="$(printf '%s' "$line" | cut -d/ -f1)"  mv="$(printf '%s' "$line" | cut -d/ -f2 | cut -d, -f1)"  if [ "$APPLY" = 1 ]; then    log "CLEAN dkms remove $mod/$mv -k $kv"    dkms remove "$mod/$mv" -k "$kv" || log "  (移除失败,请手动检查)"  else    log "STALE $mod/$mv  for kernel $kv  (将执行: dkms remove)"  fidone < <(dkms status 2>/dev/null)log "== [5/6] 需要卸载的孤儿内核头文件包 =="ORPHANS=()for pkg in $(dpkg --list 2>/dev/null | awk '/^ii  (linux-headers|proxmox-headers|pve-headers)-[0-9]/{print $2}'); do  v="$(printf '%s' "$pkg" | sed -E 's/^(linux|proxmox|pve)-headers-//')"  printf '%s' "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+' || { log "SKIP  $pkg  (元包,不自动处理)"; continue; }  [ "$v" = "$RUNNING" ] && { log "SKIP  $pkg  (属于运行中内核)"; continue; }  [ -e "/boot/vmlinuz-$v" ] && { log "SKIP  $pkg  (内核镜像仍已安装)"; continue; }  ORPHANS+=("$pkg")  log "ORPHAN $pkg  (对应内核 $v 已卸载)"doneif [ "${#ORPHANS[@]}" -gt 0 ]; then  if [ "$APPLY" = 1 ]; then    DEBIAN_FRONTEND=noninteractive apt-get -y purge "${ORPHANS[@]}" || log "  (卸载失败,请手动检查)"  else    log "演练: apt-get -s purge ${ORPHANS[*]}"    apt-get -s purge "${ORPHANS[@]}" 2>/dev/null | grep -E '^(Remv|Purg)' | while IFS= read -r l; do log "  $l"; done || true  fielse  log "(没有孤儿头文件包,干净)"filog "== [6/6] 清理幽灵残留目录 =="for v in "${GHOST_DIRS[@]:-}"; do  [ -z "$v" ] && continue  if [ "$APPLY" = 1 ]; then    log "RMDIR /lib/modules/$v"; rm -rf "/lib/modules/$v"  else    log "演练: rm -rf /lib/modules/$v"  fidonelog "== 复核 =="log "dkms status:"; dkms status 2>/dev/null | sed 's/^/  /' | while IFS= read -r l; do log "$l"; donelog "/lib/modules:"; ls /lib/modules | sed 's/^/  /' | while IFS= read -r l; do log "$l"; donelog "DKMS 缓存占用: $(du -sh /var/lib/dkms 2>/dev/null | cut -f1)"[ "$APPLY" = 1 ] && log "完成。清理报告: $REPORT" \  || log "以上为演练结果(未做任何修改)。确认无误后加 --apply 重新执行。报告: $REPORT"EOS)if [ -n "$TARGET" ]; then  ssh "$TARGET" "bash -s $APPLY_ARG" <<<"$PAYLOAD"else  [ "$(id -u)" -eq 0 ] || { echo "本机清理需要 root 权限,请用 sudo 运行。"; exit 1; }  bash -s $APPLY_ARG <<<"$PAYLOAD"fi

Windows 11(Fix-DkmsGhostKernels.ps1)——用系统自带 OpenSSH 客户端远程体检/清理,PowerShell 里执行(加 -Apply 才真正清理)。特别说明:脚本刻意做成纯 ASCII——Windows PowerShell 5.1 会把无 BOM 的 UTF-8 脚本按本地代码页误读,中文字符串轻则乱码、重则解析报错:

# Fix-DkmsGhostKernels.ps1 - Check & clean "ghost kernels" on a remote node.# Dry run : .\Fix-DkmsGhostKernels.ps1 -Target "root@<server-ip>"# Apply   : .\Fix-DkmsGhostKernels.ps1 -Target "root@<server-ip>" -Applyparam(  [Parameter(Mandatory = $true)][string]$Target,  [int]$Port = 22,  [switch]$Apply)$payload = @'set -euo pipefailAPPLY=0[ "${1:-}" = "--apply" ] && APPLY=1RUNNING="$(uname -r)"echo "== [1/6] DKMS ghost-kernel health check =="echo "Running kernel: $RUNNING"INSTALLED_KERNELS="$(cd /boot 2>/dev/null && ls vmlinuz-* 2>/dev/null | sed 's/^vmlinuz-//' || true)"echo "Installed kernels: $INSTALLED_KERNELS"echo "== [2/6] dkms status =="dkms status 2>/dev/null || trueecho "== [3/6] Classifying /lib/modules entries =="GHOST_DIRS=()for d in /lib/modules/*/; do  v="$(basename "$d")"  [ "$v" = "$RUNNING" ] && { echo "KEEP  $v  (running kernel)"; continue; }  if printf '%s\n' "$INSTALLED_KERNELS" | grep -qxF "$v"; then    echo "KEEP  $v  (image still installed)"; continue  fi  if dpkg -S "/lib/modules/$v" >/dev/null 2>&1; then    echo "KEEP  $v  (owned by installed packages)"; continue  fi  GHOST_DIRS+=("$v")  echo "GHOST $v  (image uninstalled, leftover dir)"doneecho "== [4/6] DKMS builds for ghost kernels =="while IFS= read -r line; do  [ -z "$line" ] && continue  kv="$(printf '%s' "$line" | awk -F', ' '/,/{print $2}' | cut -d, -f1)"  [ -z "$kv" ] && continue  [ "$kv" = "$RUNNING" ] && continue  printf '%s\n' "$INSTALLED_KERNELS" | grep -qxF "$kv" && continue  mod="$(printf '%s' "$line" | cut -d/ -f1)"  mv="$(printf '%s' "$line" | cut -d/ -f2 | cut -d, -f1)"  if [ "$APPLY" = 1 ]; then    dkms remove "$mod/$mv" -k "$kv" && echo "CLEAN $mod/$mv -k $kv" \      || echo "FAILED $mod/$mv -k $kv"  else    echo "STALE $mod/$mv  (will run: dkms remove $mod/$mv -k $kv)"  fidone < <(dkms status 2>/dev/null)echo "== [5/6] Orphan kernel header packages =="ORPHANS=()for pkg in $(dpkg --list 2>/dev/null | awk '/^ii  (linux-headers|proxmox-headers|pve-headers)-[0-9]/{print $2}'); do  v="$(printf '%s' "$pkg" | sed -E 's/^(linux|proxmox|pve)-headers-//')"  printf '%s' "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+' || { echo "SKIP  $pkg (meta package)"; continue; }  [ "$v" = "$RUNNING" ] && { echo "SKIP  $pkg (running kernel)"; continue; }  [ -e "/boot/vmlinuz-$v" ] && { echo "SKIP  $pkg (image installed)"; continue; }  ORPHANS+=("$pkg"); echo "ORPHAN $pkg"doneif [ "${#ORPHANS[@]}" -gt 0 ]; then  if [ "$APPLY" = 1 ]; then    DEBIAN_FRONTEND=noninteractive apt-get -y purge "${ORPHANS[@]}" || echo "  (purge failed)"  else    echo "Dry-run: apt-get -s purge ${ORPHANS[*]}"    apt-get -s purge "${ORPHANS[@]}" 2>/dev/null | grep -E '^(Remv|Purg)' || true  fielse  echo "(no orphan header packages, clean)"fiecho "== [6/6] Removing ghost leftover dirs =="for v in "${GHOST_DIRS[@]:-}"; do  [ -z "$v" ] && continue  if [ "$APPLY" = 1 ]; then echo "RMDIR /lib/modules/$v"; rm -rf "/lib/modules/$v"  else echo "Dry-run: rm -rf /lib/modules/$v"; fidoneecho "== Verification =="dkms status 2>/dev/nullls /lib/modulesdu -sh /var/lib/dkms 2>/dev/null[ "$APPLY" = 1 ] && echo "Done." || echo "DRY RUN only. Re-run with -Apply to clean."'@$arg = ''if ($Apply) { $arg = '--apply' }$payload | ssh -p $Port $Target "bash -s $arg"if ($LASTEXITCODE -eq 0) {  Write-Host "Done. Please review the final 'Verification' block above." -ForegroundColor Green} else {  Write-Host "Failed. Check the SSH connection and the output above." -ForegroundColor Red}

macOS 26(fix-dkms-ghost-kernels-macos.zsh)——终端里执行(加 --apply 才真正清理),把下面内容存为 .zsh 文件运行:

#!/usr/bin/env zsh# 用法: bash fix-dkms-ghost-kernels-macos.zsh root@<目标节点IP> [--apply]set -euo pipefailTARGET="${1:?用法: bash fix-dkms-ghost-kernels-macos.zsh root@<目标节点IP> [--apply]}"APPLY_ARG="${2:-}"ssh "$TARGET" "bash -s $APPLY_ARG" <<'EOS'(与上面 Ubuntu 版 PAYLOAD 完全相同——把 fix-dkms-ghost-kernels.sh  中 EOS 之间的脚本原样粘贴到这里即可,逻辑完全一致)EOS

三个脚本执行完,核对输出末尾的「复核」段:dkms status 只剩运行内核、/lib/modules 只剩真实存在的内核,即为体检通过;确认无误后加 --apply / -Apply 重新执行一遍,就完成了清理。

6.2

方法二:Agent 自动配置

更省事的做法是把问题直接丢给 AI Agent(ZCode、Claude Code 等均可),你只需要提供 SSH 连接方式。下面这段提示词可以直接复制:

请帮我检查并清理一台 Proxmox VE / Ubuntu 服务器上的 DKMS"幽灵内核"——即内核镜像已卸载、但系统里仍残留其 DKMS 编译产物、孤儿 headers 包和 /lib/modules 残留目录。请严格按以下步骤操作:1. 通过 SSH 登录该节点(连接方式见会话上下文/我会另行提供);2. 记录现状:uname -r、dkms status、ls /lib/modules、   dpkg -l | grep -E 'linux-headers|proxmox-headers|pve-headers';3. 先以演练模式体检(不改任何东西):分类规则——uname -r 命中的目录无条件保留;   /boot/vmlinuz-<版本> 仍存在的保留;dpkg -S 显示仍被已安装包占用的目录跳过;   其余判定为幽灵;孤儿 headers = 包名对应内核无 vmlinuz 且非运行内核,   且版本号含三段数字(proxmox-headers-7.0 这类元包不得列入);4. 把演练报告给我看,明确列出:将 dkms remove 的条目、将 apt purge 的包、   将 rm -rf 的目录;等我确认后加 --apply 重新执行;5. 清理后复核:dkms status 应只剩已安装内核的条目;ls /lib/modules 应与   /boot/vmlinuz-* 及运行内核一致;du -sh /var/lib/dkms 记录瘦身效果。约束:绝不触碰运行中内核(uname -r);不执行 dist-upgrade/full-upgrade;不重启;不动当前内核的 headers 与模块;rm -rf 仅限 /lib/modules/<幽灵版本> 这一层目录。

Agent 跑完后,重点核对它给出的三样证据:演练报告里的 GHOST/ORPHAN 清单、清理后的 dkms status、以及 /lib/modules 与 /boot/vmlinuz-* 的一致性。

7

举一反三:DKMS 时代的内核卫生守则

这次事件的教训可以浓缩成几条守则:

  1. 删内核要"镜像 + headers"成对删。headers 是 DKMS 误编译的土壤:只要图纸还在柜子里,下一次重装或升级 DKMS 驱动时,幽灵就会被再次唤醒。apt autoremove 之后,顺手 dpkg -l | grep headers 复查一眼。
  2. 装 DKMS 驱动前,想清楚要不要"全量编译"。如果你的机器常年只保留一个内核,autoinstall_all_kernels=1 纯属浪费;如果是新旧内核并存、等待重启验证的过渡期,这个设计反而是保障——重启回退旧内核时模块依然可用。
  3. dkms status 要会读"沉默的浪费"。它只报告成功与失败,不报告"值不值得"。每台跑着 DKMS 驱动的机器都值得每月跑一次本文的体检脚本。
  4. 判断"内核是否存在"要以 /boot/vmlinuz- 和 dpkg 为准*,而不是 /lib/modules 的目录——后者是"名单",会有残页;前者是"门牌",拆楼必摘牌。

8

Q&A:你可能还想问

Q1:为幽灵内核编译出来的模块,有什么实际危害?

A:功能上零危害——幽灵内核永远不会启动,模块永远不会被加载。危害都在资源侧:安装时多等几趟编译(update-initramfs -u -k all 也会为每个内核目录跑一遍)、磁盘多占几十 MB、dkms status 里多一堆干扰视线的条目。属于"不致命但膈应人"的慢性浪费。

Q2:为什么 6.14 的空目录没被编译?

A:因为编译需要 headers(/lib/modules/版本/build 链接)。6.14 的 headers 当年清干净了,只剩目录空壳,DKMS 想编也无从下手。这恰好从反面验证了根因:名单 + 图纸,缺一不可。

Q3:这些幽灵模块会拖慢开机吗?

A:不会。开机只加载运行内核的模块,幽灵的模块在它自己的目录里睡大觉。唯一的间接影响是 initramfs 更新变慢——那也只发生在安装/卸载包的那几十秒。

Q4:上游为什么要设计成"给所有内核编译"?这不是 bug 吗?

A:不是 bug,是取舍。i915-sriov 这类驱动属于"启动即必需":如果新旧内核并存、你重启回退到旧内核,没有模块显卡虚拟化就直接失效。全量编译保证了任何已安装内核启动时都可用。Debian 默认(只编当前 + 最新内核)是另一种取舍,Launchpad 上同样有人提需求希望默认全量。两种策略没有对错,只有场景。

Q5:以后升级内核还会复发吗?

A:只要"删镜像留 headers"的习惯还在,就会复发——下次重装或升级 i915-sriov-dkms 时,残留 headers 的幽灵会再次被全量编译。根治方法是让"镜像 + headers"同进同出,然后定期跑一遍本文的体检脚本,一分钟解决。

Q6:我就是不想要全量编译,能从根上关掉吗?

A:可以绕过 deb 包的 postinst,手动注册 DKMS:把源码目录放到 /usr/src/i915-sriov-dkms-版本号 后执行 dkms add 和 dkms install -m i915-sriov-dkms -v 版本号 -k $(uname -r),只给当前内核编译。但要注意:后续每次内核升级后需要自己补编译(这正是 deb 包 + 全量编译帮你自动做的事)。取舍归你自己。

9

参考资料

  • strongtz/i915-sriov-dkms(本文主角,i915/xe 驱动的 SR-IOV 社区移植版)—— github.com/strongtz/i915-sriov-dkms
  • Launchpad Bug 2120641:DKMS only builds modules for the currently booted kernel version —— bugs.launchpad.net/bugs/2120641
  • Debian Wiki:DkmsPackaging(DKMS 打包与 common.postinst 约定)—— wiki.debian.org/DkmsPackaging
  • dkms(8) 手册页(Debian)—— manpages.debian.org
  • 本文博客版(含全部配图原图):blog.margrop.net

相关学习资料