摘要

本文记录了一台配备 NVIDIA RTX 3060、无法直接连接互联网的 Ubuntu 工作站在离线更新 NVIDIA 535 驱动时遇到的完整故障链。

驱动安装过程中,NVIDIA DKMS 模块已经针对当前内核成功编译,nvidia-smi 和 GPU 驱动在尚未断电的运行环境中也能正常使用。但独立的 /boot 分区只有约 94 MB,放不下当时生成的未压缩 initramfs。为把镜像放进 /boot,后来采用了在其他目录生成 initramfs、手工压缩并复制到 /boot 的办法。

在此之后,物理机所在环境发生了本次事件中唯一一次断电。机器重新上电后无法通过 SSH 连接。将物理机运回检查后,确认此前手工处理的 initrd 无法正常引导系统。结合故障发生顺序,最可能的原因是手工压缩得到的 initramfs 没有形成启动器能够正确识别和解析的有效镜像,或其内部归档本身并不完整

考虑到原系统已经同时存在 /boot 容量过小、离线软件包依赖混杂以及启动镜像可信度不足等问题,维护方最终选择备份业务文件,重装更新版本的 Ubuntu,并取消独立 /boot 分区,让 /boot 直接使用根文件系统的空间。


机器与任务背景

这台机器是一台离线 Ubuntu 工作站,硬件和当时的软件环境如下:

项目 信息
GPU NVIDIA GeForce RTX 3060
系统 Ubuntu 24.04 系列环境
当前内核 6.14.0-27-generic
目标驱动 NVIDIA 535 系列
DKMS 包版本 535.288.01-0ubuntu0.24.04.2
安装方式 在联网机器下载 .deb,复制到离线机器安装
/boot 独立分区,容量约 94 MB

驱动更新的目标,是让离线工作站能够正常识别 GPU 并运行依赖 CUDA/NVIDIA 驱动的程序。

由于机器无法直接联网,安装包先在另一台联网设备上准备,再复制到离线环境。实际安装时采用了较为直接的方式:

sudo dpkg -i *.deb

同一类操作曾在另一台机器上成功,因此(对方)最初认为可以复用。但两台机器的内核版本、现有软件包版本、图形栈状态和 /boot 剩余空间并不相同,最终结果也完全不同。


故障时间线

将离线包整体交给 dpkg 安装

(对方)复制到离线机器的目录中不只有 NVIDIA 驱动的核心软件包,还混有一部分更广泛的系统组件,例如:

  • NVIDIA 用户态库和 Xorg 驱动;
  • mesa-vulkan-driversmesa-libgallium
  • libdrm2
  • Wayland、Vulkan 相关包;
  • polkitdpkexec
  • 可能影响内核的 linux-headerslinux-imagelinux-modules 类软件包。

执行 dpkg -i *.deb 后,这些包被同时送入安装流程。由于 dpkg 本身不会像 apt 那样自动解决所有依赖关系,离线环境又无法即时下载缺失版本,系统出现了局部依赖不一致。

当时的错误涉及:

  • libnvidia-gl-535 未完成配置;
  • xserver-xorg-video-nvidia-535 未完成配置;
  • libnvidia-fbc1-535 未完成配置;
  • Mesa 组件与 libdrm2 版本不匹配;
  • pkexecpolkitd 版本不一致。

这使原本的“更新显卡驱动”扩大成了“NVIDIA 驱动、桌面图形栈和部分系统组件的混合升级”。

其实当时排查到这一步的时候就应该让对方加钱了的,不然也不会那么折磨自己


NVIDIA DKMS 模块编译成功

尽管用户态软件包的配置过程出现问题,nvidia-dkms-535 实际上已经成功针对当前内核:

6.14.0-27-generic

编译并安装了 NVIDIA 内核模块,包括:

nvidia.ko.zst
nvidia-modeset.ko.zst
nvidia-drm.ko.zst
nvidia-uvm.ko.zst
nvidia-peermem.ko.zst

模块被安装到:

/lib/modules/6.14.0-27-generic/updates/dkms/

这一步非常关键。它说明此次故障并不是简单的“驱动与 RTX 3060 不兼容”或“DKMS 无法编译”,而是发生在后续的启动镜像更新阶段。


/boot 空间不足,update-initramfs 失败

DKMS 完成后,软件包安装脚本需要更新 initramfs,将启动阶段可能需要的内核模块和脚本写入:

/boot/initrd.img-6.14.0-27-generic

但是,这台机器的 /boot 独立分区只有约 94 MB。生成新的 initrd 时不仅需要最终文件的空间,还可能同时保留旧镜像、临时文件和新镜像,因此实际空间需求通常高于最终 initrd 的大小。

安装过程最终出现:

No space left on device

于是形成了下面的状态:

NVIDIA DKMS 编译成功

内核模块已写入 /lib/modules

update-initramfs 写入 /boot 失败

NVIDIA 软件包后置脚本失败

dpkg、内核模块与启动镜像状态不一致

临时绕过 update-initramfs

由于每次执行:

sudo dpkg --configure -a

都会再次触发 update-initramfs,而 /boot 的容量问题尚未解决,因此当时曾临时阻止安装脚本自动调用 update-initramfs,希望先让 dpkg 完成其他软件包的配置。

这一做法可以暂时绕过安装脚本,但不能修复启动链。它可能让系统形成一种危险的“半完成状态”:

位置 可能状态
/lib/modules/6.14.0-27-generic/ 已有新 NVIDIA 模块
dpkg 数据库 部分包已经配置,部分包仍异常
/boot/initrd.img-* 仍是旧镜像、不完整镜像或手工处理镜像
当前运行内核 可能可以加载磁盘上的新模块
下一次启动 仍依赖未经可靠验证的 initrd

断电前,NVIDIA 驱动已经能够正常使用

在机器仍保持运行、尚未发生断电时,NVIDIA 驱动已经能够正常加载:

nvidia-smi

可以正常输出,GPU 驱动也能正常使用。这说明当时正在运行的内核能够找到并加载 /lib/modules 中的 NVIDIA 模块,用户态 NVIDIA 库也已经达到可用状态。

但这时机器并没有使用后来手工压缩的 initramfs 完成过一次重新启动。换句话说,已经验证的是“当前运行内核中的驱动”,尚未验证的是“下一次开机所需的启动镜像”。


未压缩的 initramfs 放不进 /boot

由于 /boot 容量不足,当时尝试在其他目录生成 initramfs。生成结果没有按预期压缩,未压缩镜像又无法放入约 94 MB 的 /boot,因此随后采用了手工 gzip 压缩、再复制到 /boot 的办法。

检查时,生成文件曾被 file 识别为 ASCII 类型。这里需要说明,initramfs 的内部通常是 cpio 归档,而 cpio newc 使用 ASCII 字段表示文件头。因此:

ASCII cpio archive

并不必然等于“普通文本”或“已经损坏”,它也可能只是一个尚未经过 gzip、zstd 等算法压缩的 cpio 归档。

真正需要关注的是归档能否被完整解析、是否包含启动所需的模块、外层压缩格式是否符合启动工具的预期,以及复制过程是否完整。手工执行:

gzip initrd.img

只能在已有归档外增加压缩层。如果内部 cpio 已经截断、缺少文件,或者最终文件名、压缩格式及 GRUB/initramfs 工具链之间不匹配,gzip 本身并不能把它变成可启动镜像。

当时使用过:

lsinitramfs /boot/initrd.img-6.14.0-27-generic

来检查归档结构,并遇到过归档非正常终止的迹象。

但即便完整解析,也只能证明归档结构大体正常,仍不能完全证明其中包含根文件系统、存储控制器、LVM/RAID、文件系统等启动所需模块。真正的启动验证最终仍需要重启或在受控环境中测试。


使用 MODULES=dep 缩小镜像

为了减小 initrd,还尝试过修改:

/etc/initramfs-tools/initramfs.conf

把:

MODULES=most

改为:

MODULES=dep

dep 是 dependency 的缩写,只收录生成镜像时根据当前硬件推断出的必要模块;most 则会包含更多常见的存储、文件系统和硬件模块。

MODULES=dep 确实可能显著减小镜像,但代价是降低硬件变化、磁盘迁移和异常救援时的兼容性。它可以缓解空间压力,却不能从根本上解决只有 94 MB 的 /boot 带来的长期维护风险。

这一步也可能是重启出问题的原因之一


一次断电后,机器无法通过 SSH 连接

在驱动已经正常使用、手工压缩的 initramfs 已经放入 /boot 之后,物理机所在环境发生了一次断电。这是本次事件中唯一一次断电。重新上电后,机器无法通过 SSH 连接。

当时只能观察到远程连接异常,无法立即区分以下情况:

  • 机器根本没有完成启动;
  • 系统进入 initramfs/emergency shell;
  • 根文件系统没有成功挂载;
  • 系统已经启动,但网络或 sshd 没有运行;
  • IP 地址或网络路径发生变化。

机器能够 ping 通也不能证明 Ubuntu 已经完整启动,因为 ICMP 应答、SSH 服务和用户空间启动是不同层次的问题;某个 IP 有响应也不能单独证明响应者就是目标系统。

由于现场缺少可用的带外管理入口,最终只能将物理机运回进行本地检查。


本地检查确认手工处理的 initrd 无法启动

将机器运回并接入本地显示和维护环境后,确认此前生成并手工压缩后放入 /boot 的 initrd 无法正常引导系统。

这解释了为什么断电前 nvidia-smi 能够正常工作,而断电后却无法重新启动:

  • 驱动运行时验证的是已经启动的内核和根文件系统中的模块;
  • 断电后的重新启动验证的是 GRUB、内核、initrd、存储驱动和根文件系统挂载这一整条启动链;
  • NVIDIA 驱动已经可用,并不能反向证明 initrd 一定完整、可启动。

这次断电并不是把一个已经验证可启动的系统突然“弄坏”,而是首次迫使机器使用手工压缩后的 initrd 完成完整启动,从而暴露了该镜像不能正常工作的隐患。

由于没有保留下足以唯一确定格式错误位置的完整启动日志,所以更严谨的结论是:手工压缩后的 initrd 已被现场确认无法启动;它没有被启动流程正确识别或解析,是结合时间线得到的最可能解释。


最终选择备份文件并重装系统

理论上仍可以通过 Live USB/DVD 挂载原系统、进入 chroot、修复软件包状态、重新生成 initramfs 并修复 GRUB。但维护方需要同时面对:

  • /boot 独立分区容量过小;
  • initrd 已经确认无法启动;
  • NVIDIA 与部分图形栈软件包版本混杂;
  • dpkg 曾经历临时绕过和未完成配置;
  • 即使修复当前镜像,未来内核或驱动升级仍可能再次耗尽 /boot
  • 离线环境使依赖修复和版本校验更加困难。

综合修复成本、未来维护成本和系统可信度后,对方最终选择:

  1. 备份原系统中的业务文件和数据;
  2. 重装更新版本的 Ubuntu;
  3. 不再创建独立 /boot 分区;
  4. /boot 作为根文件系统中的普通目录,直接使用根分区的可用空间。

这一选择没有继续保留原系统的包和配置状态,但换来了更清晰的软件包基线和更简单的磁盘布局,也从结构上消除了 94 MB 独立 /boot 对后续内核、initramfs 和 NVIDIA 驱动升级的限制。


根因分析

直接原因:initrd 无法正常启动

机器最后无法在断电后恢复服务,直接原因是此前生成、手工压缩并写入 /boot 的 initrd 不能正常完成启动。结合操作顺序,最可能的问题是该镜像没有被启动流程正确识别或解析,但缺少完整启动日志,无法进一步唯一确定是外层压缩格式、内部 cpio 结构、文件完整性还是启动配置不匹配。

首要基础问题:独立 /boot 容量过小

约 94 MB 的 /boot 已经难以容纳现代 Ubuntu 内核、initrd、GRUB 文件以及更新过程中的临时空间。即使只保留一个内核,也可能在重建镜像时触发空间不足。

放大因素:离线安装包范围失控

使用 dpkg -i *.deb 一次性安装整个目录,使 NVIDIA 驱动更新与 Mesa、Vulkan、Wayland、libdrm、polkit 甚至内核相关包混在一起,扩大了依赖冲突范围,也降低了对实际变更的可控性。

风险因素:绕过标准 initramfs 流程

临时阻止 update-initramfs、在其他目录生成镜像、手工压缩和复制等操作,在应急阶段各有用途,但本次手工压缩后的镜像没有在断电前完成启动验证,最终在唯一一次断电后的重新启动中暴露出无法启动的问题。

验证缺口:把运行时验证当成启动验证

nvidia-smi 正常只能证明当前会话中的驱动栈工作。它不能证明:

  • GRUB 指向正确内核和 initrd;
  • initrd 归档完整;
  • initrd 中包含必要的存储和文件系统模块;
  • 根文件系统可以在下一次冷启动时挂载;
  • 网络和 SSH 服务能够在重启后恢复。

安装 NVIDIA 驱动为什么会影响启动

安装 NVIDIA 驱动并不必然升级 Linux 内核,但通常会触发:

  1. 安装 NVIDIA 用户态库;
  2. 通过 DKMS 为当前内核编译模块;
  3. 执行 depmod 更新模块依赖;
  4. 调用 update-initramfs 更新启动镜像;
  5. 某些情况下更新 GRUB 配置。

因此:

不升级内核,不等于不更新 /boot;驱动模块编译成功,也不等于驱动安装和启动链已经完成闭环。

如果离线目录中还包含:

linux-image-*
linux-headers-*
linux-modules-*

那么安装过程还可能真正新增或更换内核。Wayland、Mesa、Vulkan 和 libdrm 等包则可能把一次驱动更新扩大为桌面图形栈升级。


为什么取消独立 /boot 分区

独立 /boot 并不是错误设计。在全盘加密、复杂存储结构、特定固件限制或集中运维规范下,它仍可能是必要的。但这台普通工作站的独立 /boot 只有约 94 MB,已经成为系统升级的固定瓶颈。

取消独立 /boot 后:

  • /boot 仍然存在,但只是根文件系统中的一个目录;
  • 内核和 initrd 可以使用根分区中更充足的空间;
  • 不再需要单独调整 /boot 分区大小;
  • 内核和驱动更新时更不容易因为几十或几百 MB 的固定上限失败;
  • 磁盘和挂载关系更简单,降低了后续维护复杂度。

需要强调的是,“取消 /boot 分区”不是删除 /boot 目录,而是不再为它设置单独的文件系统和挂载点。


重新执行推荐的流程

安装前确认系统基线

uname -r
lsb_release -a
df -h / /boot
lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINTS,UUID
dpkg --audit
dkms status

必须先确认:

  • 当前准确的 Ubuntu 版本和架构;
  • 正在运行的内核;
  • /boot 的总容量和剩余空间;
  • 是否已经存在未配置软件包;
  • 旧 NVIDIA 驱动和 DKMS 状态。

在相同发行版环境准备离线包

联网机器或容器环境应与离线机器保持相同的:

  • Ubuntu 发行版;
  • CPU 架构;
  • 软件源版本;
  • 目标驱动分支。

保留包清单和版本,不要把下载目录中所有历史 .deb 混在一起。安装时也应使用明确的软件包列表,而不是直接执行不受控的:

dpkg -i *.deb

不随意混入图形栈和内核包

除非依赖关系明确要求,否则不要因为名称与显卡相关,就同时升级:

  • Mesa;
  • Wayland;
  • Vulkan;
  • libdrm;
  • linux-image
  • linux-modules
  • 与当前内核无关的 linux-headers

为 initramfs 留出充足空间

驱动安装前先检查:

df -h /boot
du -h /boot/* | sort -h

需要考虑的不只是最终 initrd 大小,还包括旧镜像、临时文件和包管理器更新过程。对于新装系统,不应再为现代 Ubuntu 配置几十 MB 级别的独立 /boot


完成标准重建与一致性检查

sudo update-initramfs -u -k "$(uname -r)"
sudo update-grub
sudo dpkg --audit
dkms status
modinfo nvidia

完整解析 initrd:

lsinitramfs "/boot/initrd.img-$(uname -r)" >/dev/null
echo $?

把重启测试纳入验收

驱动更新后的验收不应止于:

nvidia-smi

还应包括:

  1. 确认当前内核与目标内核一致;
  2. 确认 initrd 可以完整解析;
  3. 确认 GRUB 配置已更新;
  4. 准备本地控制台、Live USB/DVD 或带外管理;
  5. 执行一次受控重启;
  6. 条件允许时执行一次完整关机再上电;
  7. 验证 SSH、网络、显示管理器和 nvidia-smi
  8. 确认业务程序实际调用 GPU。

Live 环境能够做什么

如果只是 initrd 或 GRUB 损坏,并不一定必须重装。Live USB/DVD 可以作为外挂维护环境,完成:

  • 挂载原根分区和 /boot
  • 检查或修复文件系统;
  • 绑定 /dev/proc/sys/run
  • chroot 进入原系统;
  • 修复 dpkg 状态;
  • 重新生成 initramfs;
  • 更新或重新安装 GRUB。

典型思路如下,实际设备名必须根据现场分区布局调整:

sudo mount /dev/ROOT_PARTITION /mnt
sudo mount /dev/BOOT_PARTITION /mnt/boot

for i in /dev /dev/pts /proc /sys /run; do
sudo mount --bind "$i" "/mnt$i"
done

sudo chroot /mnt
update-initramfs -u -k 6.14.0-27-generic
update-grub

本次最终没有继续修复旧系统,是因为重装更新版本 Ubuntu 并重新设计分区,比继续维护一个包状态复杂、/boot 结构先天受限的旧系统更可控。


最终结论

这次事件可以概括为三句话:

  1. NVIDIA 535 的 DKMS 模块确实编译成功,RTX 3060 也曾在运行中的系统里正常工作。
  2. 真正没有完成闭环的是手工压缩后的 initramfs 和启动链;唯一一次断电后的重新启动暴露了这个潜伏问题。
  3. 最终通过备份数据、重装更新版本 Ubuntu 并取消独立 /boot 分区,彻底移除了旧系统的结构性风险。

驱动更新不是看到 nvidia-smi 正常就结束。对 Linux 物理机而言,真正的完成标准应当是:软件包状态一致、内核模块正确、initrd 可用、GRUB 正确,并且机器能够在受控重启和完整断电后恢复网络、SSH 与 GPU 业务。