ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

DKMS幽灵内核清理指南:状态残留排查与根治顺序

DKMS幽灵内核清理指南:状态残留排查与根治顺序 那天晚上我只是想顺手清掉机器上几个用不到的旧内核结果在dkms status里看到一件让我后背发凉的事uname -r明明是 6.8.0-31-generic6.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。我这边装的是社区打补丁版 i915dkms add的时候会把源码解压到这里DKMS 编译前就去里面读dkms.conf确认模块名、版本号、构建命令。这个目录原则上不要手动画它只是素材库。第二个是构建状态目录也就是/var/lib/dkms/i915-sriov-dkms/version/。这是 DKMS 真正的“内部工作台”里面会按内核版本分出子目录每个内核版本都有自己的一套 build 中间产物和 module 产物。我看过里面结构每个内核版本下面又按架构分目录最终module/里放编译好的.kobuild/里是大量.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/dkmsbuilt表示只编译过但没安装。现在把这个列表和第一步的/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/builddu -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-dkmsdkms 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中我就顺手按上面的步骤把它清了。这个习惯以前能帮我省下十几分钟的编译时间现在也推荐给你——毕竟把时间花在真正服务自己的内核上才不算浪费。
返回列表