ARTICLE · 1007044
内核都卸载了,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 的真实输出是这样的:

图 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
先排除人为因素
按照惯例,先把自己疑心的部分排除掉:
最后一个验证很关键:这不是灵异事件,是确定性逻辑。那么 DKMS 到底依据什么决定"要给哪些内核编译"?
3.2
DKMS 的"住户名单":/lib/modules
DKMS 是个用户态框架,它不查 dpkg 数据库,它眼中的"系统里有哪些内核",就是 /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:

图 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,包在安装/配置阶段执行的钩子):

图 6:真实文件内容。安装脚本在第一件事就把 autoinstall_all_kernels 设为 1。
这个变量是 DKMS 官方维护脚本(/usr/lib/dkms/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 来让所有内核都获得模块:

图 8:真实网页截图。Launchpad Bug 报告从反方向印证了这个开关的存在与默认值。
4.2
把整个决策链条画出来

图 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-pve5.2
复检三连确认修复
做完之后跑一遍"复检三连":

图 11:真实命令输出。dkms status 从 5 条变 1 条,linux-version list 只剩运行内核,DKMS 缓存从 110M 瘦身到 39M。
dkms status 只剩当前内核一条 installed、linux-version list 只剩一行、/var/lib/dkms 从 110M 瘦到 39M——三条全中,修复完成。当前内核的模块安然无恙,显卡虚拟化功能不受任何影响。
6
一键自动体检脚本
手不想碰键盘的读者,我把"体检 → 分类 → 清理 → 复核"整个流程封装成了三平台脚本。设计上遵循三条安全底线:
默认演练模式:不带参数只输出"将会做什么"的报告,落一份到 /tmp,人看过再动手; 绝不碰运行中内核:uname -r 命中的目录无条件保留,另设 dpkg -S 占用检查,任何仍被已安装软件包占有的目录一律跳过; 元包保护: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"fiWindows 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 时代的内核卫生守则
这次事件的教训可以浓缩成几条守则:
删内核要"镜像 + headers"成对删。headers 是 DKMS 误编译的土壤:只要图纸还在柜子里,下一次重装或升级 DKMS 驱动时,幽灵就会被再次唤醒。apt autoremove 之后,顺手 dpkg -l | grep headers 复查一眼。 装 DKMS 驱动前,想清楚要不要"全量编译"。如果你的机器常年只保留一个内核,autoinstall_all_kernels=1 纯属浪费;如果是新旧内核并存、等待重启验证的过渡期,这个设计反而是保障——重启回退旧内核时模块依然可用。 dkms status 要会读"沉默的浪费"。它只报告成功与失败,不报告"值不值得"。每台跑着 DKMS 驱动的机器都值得每月跑一次本文的体检脚本。 判断"内核是否存在"要以 /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