TT Lab
开始
学习 学习路径 课程

闭网环境的 GPU 驱动搬入与安装

比驱动更早出问题的:内核头文件

在 TT Lab 中继续学习

一句话总结

导入驱动时最容易出问题的不是驱动本身,而是内核头文件。DKMS 用与正在运行的内核版本完全一致的构建树来构建模块,导入的头文件哪怕只差一个字符,也会在开始编译之前就停下来。如果服务器启用了 Secure Boot,导入流程还要包括为构建出的模块签名,并把对应的密钥登记到固件中。

为什么需要它

前面的几个模块用假的 .deb 练习了驱动的依赖关系和安装顺序,因为真正的 NVIDIA 驱动不能再分发。但现场的事故往往出在这之后。“驱动软件包装好了,可是 nvidia-smi 报 couldn't communicate with the NVIDIA driver”——这说明内核模块没有构建出来。顺着原因查下去,要么是没有头文件,要么是下载的头文件是 linux-headers-generic(指向最新内核的元软件包),与正在运行的内核不一致,要么是模块已经构建出来,但 Secure Boot 拒绝了未签名的模块。

工作原理

DKMS 的规则。 按 DKMS 文档,源码要放在 /usr/src/<PACKAGE_NAME>-<PACKAGE_VERSION>/ 中,其中的 dkms.conf 必须包含 PACKAGE_NAME 和 PACKAGE_VERSION。如果有多个模块,需要 BUILT_MODULE_NAME[#](不带 .ko),DEST_MODULE_LOCATION[#] 必须以 /kernel 开头,但在 Ubuntu、Fedora、RHEL 等发行版上会被忽略(模块会放到发行版规定的位置)。设置 AUTOINSTALL="yes" 后,新内核装入时 autoinstall 会重新构建。如果不写 MAKE[#],就会使用内核构建树的 make,并附带 KERNELRELEASE。CLEAN 现在会被 DKMS 警告不要使用。

命令的顺序。 dkms add 登记源码,dkms build -k <커널> 在 /var/lib/dkms/<이름>/<버전>/<커널>/<아키텍처>/module/ 中构建模块,dkms install 再把模块移到内核模块树中。目标内核的头文件不存在时,会像文档中写的那样,以 Your kernel headers for kernel <버전> cannot be found at /lib/modules/<버전>/build or /lib/modules/<버전>/source 停止(dkms 源码中的退出码是 21,但本实验 VM 的 dkms 3.0.11 让整条命令以 1 结束——脚本应判断是否不为 0,而不是比较具体的码值)。dkms status 的输出格式因版本而略有不同——较新的版本是 이름/버전, 커널, 아키텍처: installed。

头文件必须与内核字符串完全一致。 Ubuntu 的头文件分为 linux-headers-<uname -r>(按 flavor 区分)和它所依赖的 linux-headers-<버전>(公共部分)。导入清单必须从 uname -r 开始列。linux-headers-generic 很方便,但它会跟随下载当天的最新内核,所以很容易与隔离网络中服务器的内核错位。

vermagic。 模块中带有一个 vermagic 字符串,记录它是为哪个内核构建的,可以用 modinfo -F vermagic 查看。为其他内核构建的模块无法加载。modprobe 会按 depmod 生成的列表连同依赖模块一起加载,而 insmod 只加载一个文件。

Secure Boot 与 MOK。 按 Ubuntu 文档,在启用 Secure Boot 的系统上,DKMS 会用 MOK(Machine Owner Key)自动为新模块签名。密钥是 /var/lib/shim-signed/mok/MOK.priv 和 MOK.der,如果不存在就会创建并发出登记请求。登记要在下次启动时的 MokManager 界面中输入密码才算完成——也就是说,对于只能远程访问的隔离网络服务器,必须把控制台操作排进导入日程。mokutil --sb-state 查看状态,mokutil --import 为 DER 密钥创建登记请求。在强制模式下,未签名的模块会被 Key was rejected by service 拒绝;在不强制的内核上,会留下 module verification failed: signature and/or required key missing - tainting kernel 并照常加载。

在现场相遇的样子

我们在这台实验 VM(Ubuntu 24.04,内核 6.8.0-139-generic,dkms 3.0.11)上实际测量过。没有头文件时,用不存在的内核去构建,会输出 Error! Your kernel headers for kernel 6.8.0-9999-generic cannot be found at ...,下一行是 Please install the linux-headers-6.8.0-9999-generic package or use the --kernelsourcedir option。用导入的头文件构建出的模块被压缩后放到了 /lib/modules/6.8.0-139-generic/updates/dkms/airgap_hello.ko.zst。有意思的是签名。这台 VM 无法使用 Secure Boot(mokutil --sb-state 显示 EFI variables are not supported on this system),DKMS 仍然为模块签了名,modinfo -F signer 的结果是 <호스트이름> Secure Boot Module Signature key。然而加载时,内核留下了 module verification failed: signature and/or required key missing - tainting kernel。原因是签名虽然存在,但那把密钥不在内核信任的密钥列表中——如果是强制模式,这里就会被拒绝,这也是 MOK 登记要放进导入流程的原因。

如果隔离网络 GPU 服务器的内核在两次导入之间被更新,驱动本身没有变,但从下次启动起模块就不见了。即使 AUTOINSTALL 想用新内核重新构建,该内核的头文件也没有被导入。内核软件包和它的头文件必须始终在同一个导入包中一起带入。本实验的 VM 无法开启 Secure Boot,所以签名步骤只通过记录来确认——这一局限也写在了实验说明中。另外,这台 VM 的云镜像通过 linux-headers-virtual 一开始就装好了正在运行的内核(测量当天是 6.8.0-139-generic)的头文件。隔离网络中的服务器通常不是这样,所以实验准备步骤删除了这些头文件,让你从“没有头文件的服务器”开始。

下一项实验要做什么

确认正在运行的内核,不安装、只下载该内核的两个头文件软件包,并生成哈希清单。封锁外部访问后,用导入的 .deb 安装头文件,再把一个 20 行的 GPL 模块用 DKMS 依次 add、build、install,并带参数加载。记录 vermagic、签名和 Secure Boot 状态,再用没有头文件的内核去构建,留存 DKMS 的错误输出作为证据。

参考文档:dkms(8) · Ubuntu — UEFI Secure Boot · mokutil(1) · Kernel module signing · modprobe(8)