你是不是也遇到过这种情况:给乌班图(Ubuntu)装 NVIDIA 显卡驱动,进度条一路往前走,下载、解压、检查环境全都没问题,结果卡在 “Building kernel modules” 这一步,屏幕直接弹出一行 ERROR,末尾还不忘跟一句 “See ... for details.”。我第一次看到这个报错时也焦虑了大半天,疯狂翻资料,最后发现原因比想象中简单得多。这篇文章就是把 “Building kernel modules” 这个报错的来龙去脉、排查思路和修复操作完整捋一遍。你能获得三样东西:一是彻底搞懂这个过程到底在干什么,二是拿到一份可以直接照着执行的排查清单,三是弄清楚头文件缺失、编译器不匹配、安全启动拦截这三个最常出问题的点分别怎么处理。
先说点实在的:这类报错九成以上都不是显卡坏了,也不是驱动安装包损坏,而是当前系统的编译环境和内核状态没对上。只要按顺序排查,绝大多数人可以在半小时内解决。
1. 先搞清楚这个错误卡在了哪个环节
1.1 “Building kernel modules” 翻译成人话
NVIDIA 闭源驱动和普通 Windows 驱动最大的区别在于:它不是一套打包好的二进制文件直接塞进去,而是包含了一组需要针对当前 Linux 内核现场编译的内核模块。你在安装界面看到 “Building kernel modules” 这一行,其实就是安装器在调用 dkms(Dynamic Kernel Module Support)和 gcc,把nvidia.ko、nvidia-modeset.ko、nvidia-drm.ko、nvidia-uvm.ko这些文件按照当前内核的源码接口编译出来。
打个比方:内核像是住宅里的总电表箱,驱动模块就是适配某一个楼层配电箱的接线端子。每栋楼的电表箱图纸不一样,端子规格就得按当前楼栋的图纸现场加工。内核每次升级,相当于楼栋改造了一遍,配电箱里的东西必须重新按图施工。Ubuntu 给你报 “Building kernel modules” 失败,本质上就是说:这次施工没通过验收,图纸、工具或材料有问题。
这个阶段失败,跟前面下载、解压、环境检测全部通过并不矛盾。因为安装器在前面只检查了显卡型号、显卡 PCI 设备 ID、磁盘空间这类静态信息,而编译模块是第一次真正接触你的内核源代码和编译工具链。所以,很多环境下前面一路绿灯、到这里才翻车。
1.2 为什么偏偏在这一步失败,而不是更早
这一步之所以是“高危区”,是因为它同时依赖四个先决条件:
gcc:编译模块的编译器make:驱动构建系统的调度工具linux-headers-$(uname -r):当前运行内核的头文件,模块编译必须拿到对应版本的内核数据结构和函数声明- 内核构建配置:头文件包内提供的
.config或相关配置信息,决定了模块按什么规格编译
如果其中任何一个缺失、版本对不上,或者被安全策略挡住,模块就无法生成。再加上很多 Ubuntu 用户习惯直接从 NVIDIA 官网下载 runfile 安装,而这个安装器不会替你把系统依赖补齐,它默认你已经装好了编译环境——这就是为什么这么多人会撞上这个报错。
明白了这一点,接下来排查就有的放矢了。
2. 我这次翻车的完整排查过程
2.1 先翻日志,别凭感觉乱猜
报错界面通常会提示一行 “See /var/log/nvidia-installer.log for details.”。这句话不是客套,是你唯一可靠的线索来源。安装器的日志会完整记录它执行了哪些命令、在哪一步失败、编译器报了什么错。直接看日志尾部,最能说明问题。
sudo tail -n 100 /var/log/nvidia-installer.log我那次实际操作中,日志最后几行是这个样子的:
ERROR: Unable to build the NVIDIA kernel module. ERROR: Failed to run the kernel module build script. Please see the ERROR messages above for more information. ... gcc: fatal error: linux/bitsperlong.h: No such file or directory看到bitsperlong.h不存在,我心里就明白了一大半——这个头文件属于内核头文件包,系统里压根没有装对应版本的linux-headers。这类 “No such file or directory” 的错误,优先级最高的问题从来不是驱动本身,而是内核头文件缺失或版本错位。
如果日志末尾没有明显的编译错误,而是模块签名相关的内容,比如MODSIGN: module is not signed,那就要转向安全启动方向去排查。日志结构大致是:前面记录显卡 ID、驱动版本、内核版本、gcc 版本和 dkms 状态,尾部才是编译现场。所以别傻乎乎从头看,直接tail -n 100效率最高。
2.2 六个命令,一次拍完环境快照
看完了日志,我用下面这组命令给系统做了一次“体检”。每一条都有明确目的,不是瞎敲:
uname -r # 当前内核版本 dpkg -l | grep linux-headers # 头文件包是否安装 ls /usr/src/ # 头文件源码目录 gcc --version # 当前编译器版本 cat /proc/version | grep -o 'gcc version [0-9.]*' # 编译当前内核所用的 gcc 版本 mokutil --sb-state # Secure Boot 开关状态| 命令 | 想确认什么 | 预期正常状态 |
|---|---|---|
uname -r | 当前内核版本号 | 例如6.5.0-27-generic |
dpkg -l | grep linux-headers | 安装的头文件包版本 | 要和uname -r对应 |
ls /usr/src/ | 是否存在对应的头文件目录 | 有linux-headers-6.5.0-27-generic这类目录 |
gcc --version | 当前 gcc 主版本 | 例如gcc version 13.2.0 |
cat /proc/version | 内核编译时使用的 gcc | 与当前 gcc 大版本差距不超过 1~2 |
mokutil --sb-state | Secure Boot 是否启用 | SecureBoot enabled或disabled |
你的目标是:让第 1、2、3 项完全对上,第 4、5 项不要跨太大版本,第 6 项知道自己机器的安全启动策略。我当时就是第 2、3 项直接不满足——/usr/src下面空空如也,头文件根本没装。
2.3 排查思路:先排最常见,再考虑最隐蔽
遇到这个报错,按“头文件缺失 → gcc 版本差异 → Secure Boot 拦截 → 旧驱动残留冲突 → 内核配置异常”的顺序排查,可以省掉大量时间。头文件缺失是最常见的,因为很多教程只教你怎么装驱动,没人提醒你要同步核对头文件是否跟上内核版本。
特别是从旧版本 Ubuntu 跨版本升级过来的用户:系统内核升级了,但linux-headers没有自动装上,或者装的还是旧版本。这时候编译模块必然失败。我那次就是在一台从 20.04 升级到 22.04 的机器上遇到的,旧驱动触发 dkms 重建,但新内核的头文件根本没跟过来。
3. 真正的高频凶手只有三个
3.1 头文件缺失或版本错位:八成新手的第一个坑
内核头文件就是linux-headers-$(uname -r)这个包。它的作用是为编译内核模块提供当前内核的数据结构、宏定义和接口声明。没有它,gcc 连最基本的内核类型都找不到,编译自然会戛然而止。
判断方法很简单:先看uname -r,再去/usr/src里看有没有同名目录。如果两者对不上,直接安装:
sudo apt update sudo apt install linux-headers-$(uname -r)装完之后重新执行ls /usr/src,确认目录出现,再重装驱动。但有一个坑要注意:如果apt install linux-headers-$(uname -r)提示找不到包,说明你的内核版本不在当前软件源里。这在两种场景下常见:一是刚升级完内核但还没apt update;二是手动安装了某个特殊内核。前者执行apt update即可,后者建议直接装回官方内核,头文件齐备、稳定性也更好。
还有一个容易忽略的错位场景:uname -r显示的是6.5.0-27-generic,但/usr/src里躺着的是6.5.0-25-generic。这是因为系统更新了内核,却因为某些原因没有同步安装新头文件,或者头文件包安装失败。所以不要只看“有没有头文件”,还要看“版本是否完全一致”。版本不一致时,编译报错往往不直接说“版本不对”,而是会冒出一堆莫名其妙的fatal error,比如:
fatal error: linux/compiler_types.h: No such file or directory这种错误第一反应先查头文件版本,不要急着去怀疑驱动包。
3.2 gcc 版本与内核编译工具链差异过大
第二个容易翻车的地方是编译器版本。内核模块编译对 gcc 版本是有“容忍区间”的,相邻主版本一般没问题,但如果跨得太远,编译时可能出现各种奇怪的语法错误和宏展开失败。这时候安装器不会说“版本不兼容”,而是抛出一堆像是驱动源码本身有问题的报错,极具迷惑性。
判断依据在/proc/version里:
cat /proc/version正常输出会带上编译内核时的 gcc 版本信息,比如gcc version 12.3.0 (Ubuntu 12.3.0-1ubuntu1~22.04)。如果当前gcc --version是 13 或者 14,而编译内核用的是 12,大版本跨了 2 个,就可能出问题。我处理过不少案例,系统默认 gcc 是新版本,但内核是旧编译器编出来的,编译模块时出现可疑的Werror或者结构体初始化报错。
解决思路是不动系统全局 gcc,只给驱动安装过程指定编译器:
sudo apt install gcc-12 CC=gcc-12 sudo ./nvidia-installer如果是通过 DKMS 自动构建,可以临时设置环境变量让 dkms 使用指定编译器,或者直接修改/etc/dkms/dkms.conf里的 CC 设置,但多数场景下安装一个与内核匹配版本的 gcc 并临时指定,已经足够。不要粗暴删除新版 gcc,很多系统工具依赖它。
3.3 Secure Boot:模块明明编译成功,却被内核拒之门外
还有一种情况,日志尾部没有编译错误,反而出现模块签名相关字样:module verification failed: signature invalid或者MODSIGN: module is not signed。这是 Secure Boot 在拦路。
Ubuntu 在部分 UEFI 机器上默认开启安全启动,内核只加载受信任签名的模块。NVIDIA 的闭源驱动模块如果是从官网 runfile 安装的,通常没有预先签好名。编译过程完全正常,模块文件也生成了,但一加载就被拒。
先确认状态:
mokutil --sb-state如果输出SecureBoot enabled,你有两个方向。一是进 BIOS 关闭 Secure Boot,简单粗暴,适合自己可控的台式机;二是保留安全启动,用 MOK(Machine Owner Key)给自己的模块签名,适合某些无法关闭安全启动的机器。
MOK 签名的完整流程我整理过,核心四步:
# 1. 生成密钥对 sudo apt install mokutil openssl openssl req -new -x509 -newkey rsa:2048 \ -keyout MOK.priv -outform DER -out MOK.der \ -nodes -days 36500 -subj "/CN=Driver Signing Key" # 2. 导入公钥到 MOK 列表 sudo mokutil --import MOK.der # 3. 重启进入蓝色 MOK 管理界面,输入临时密码,选择 Enroll key # 注意:这个密码是刚才 mokutil --import 时让你设置的临时密码,不是 BIOS 密码 # 4. 用私钥给 NVIDIA 模块签名 sudo /usr/src/linux-headers-$(uname -r)/scripts/sign-file \ sha256 MOK.priv MOK.der \ /lib/modules/$(uname -r)/updates/dkms/nvidia.ko模块文件位置可能因安装方式不同而略有差异,find /lib/modules -name 'nvidia*.ko'可以帮你快速定位。有个细节:MOK 签名做完后最好重启验证一次,确认模块能正常加载。否则装了驱动但加载不了,nvidia-smi依然不存在,看起来像驱动没装上,实际上是被安全策略卡住了。
3.4 三种情况快速对照表
| 现象 | 日志关键字 | 高频程度 | 处理优先级 |
|---|---|---|---|
| 编译中途退出,缺少头文件 | fatal error: linux/*.h: No such file | 最高 | 先装linux-headers-$(uname -r) |
| 编译报出奇怪语法错误 | 无明确关键字,有很多类型/宏报错 | 中 | 对比 /proc/version 与当前 gcc |
| 编译成功但模块被拒绝加载 | module verification failed: signature invalid | 中 | 关闭 Secure Boot 或 MOK 签名 |
这三个原因占到了 “Building kernel modules” 报错的八成以上。剩下的两成,多半是旧驱动残留和内核配置异常,下面接着说。
4. 照着抄的操作:清理、禁用开源驱动、重新安装
4.1 让 nouveau 先退休,给新驱动腾地方
Ubuntu 默认加载的是开源显卡驱动 nouveau。它与 NVIDIA 闭源驱动水火不容,如果不提前禁用,即使模块编译成功,加载时也会被 nouveau 占用的显卡设备搞乱。这也是很多人装完驱动重启后黑屏、循环登录的一个重要原因。
先写一个黑名单文件:
echo -e "blacklist nouveau\noptions nouveau modeset=0" | sudo tee /etc/modprobe.d/blacklist-nouveau.conf然后重新生成 initramfs:
sudo update-initramfs -u到这里不要急着无缝衔接装驱动。我建议重启一次,确认系统在没有 nouveau 的情况下能正常启动进入桌面或文本界面。这一步看似多余,却能帮你把“驱动装不上”和“驱动装好但系统起不来”两类问题隔离开。如果重启后黑屏,先从引导参数里加nomodeset救急,再继续后面步骤。
4.2 给旧驱动做一次彻底“尸检”
如果你之前用 apt 装过 nvidia 相关包、或者用 runfile 装过,建议先彻底清干净,避免残留包参与新驱动的编译和加载。
sudo apt --purge remove '^nvidia-.*' '^libnvidia-.*' sudo apt autoremove --purge清理完检查一下是否还有残留:
dpkg -l | grep nvidia如果还有状态为iU、iF这类异常状态的包,执行sudo dpkg --configure -a修复或sudo dpkg --remove --force-remove-reinstreq <包名>强制移除。runfile 安装的驱动不会在 dpkg 列表里,如果之前用官方脚本装过,需要先跑一次:
sudo /usr/bin/nvidia-uninstall不建议手工乱删/usr/lib/nvidia和/opt/nvidia里的文件,容易留下半残状态。用官方卸载脚本或 apt purge 才是稳妥路径。
4.3 重新安装:三种方式怎么选
现在到了重装环节,选择方式很关键。我的建议顺序是:apt 仓库驱动 > 图形界面附加驱动 > 官方 runfile。
| 安装方式 | 优点 | 缺点 | 适合人群 |
|---|---|---|---|
sudo apt install nvidia-driver-550 | 依赖由 apt 管理,DKMS 自动重建,升级跟随系统 | 版本不一定最新 | 绝大多数桌面用户 |
| 系统设置里选 Additional Drivers | 图形化点选,最省心 | 本质上还是 apt,只是界面友好 | 新手最推荐 |
| 官方官网 runfile | 版本自由,可精确指定 | 需要手动处理依赖、签名、残留 | 有特定版本需求或数据中心场景 |
对于普通用户,我真的推荐 apt 仓库方式:
sudo apt update sudo apt install nvidia-driver-550 sudo reboot装完重启后,验证三个点:
nvidia-smi lsmod | grep nvidia dkms statusnvidia-smi能输出显卡信息和驱动版本号,说明驱动已经正常加载;dkms status会列出驱动模块与内核版本的映射,正常情况下显示为installed。
4.4 如果非要硬刚官方 runfile
跑官方 runfile 的安装器时,确实会遇到更多边界情况。如果前面排查完确认头文件、gcc 都没问题,只是 Secure Boot 拦着,可以加参数跳过部分检查:
sudo ./NVIDIA-Linux-x86_64-550.xx.run --no-cc-version-check这只是跳过 gcc 版本检查,不要指望它能解决所有问题。runfile 方式装完,模块默认放在/lib/modules/$(uname -r)/kernel/drivers/video/,签名和加载逻辑与 apt 方式一致,依然受 Secure Boot 约束。想省事的话,runfile 从来不是最优解,除非你明确知道自己在干什么。
5. 装好之后还会遇到的两种“假故障”
5.1 内核升级后 nvidia-smi 突然不见了
驱动装好后,隔一段时间系统内核升级,重启进入新内核,发现nvidia-smi没了,模块也没加载。这不是驱动坏了,而是旧内核的模块不会自动迁移到新内核,DKMS 需要在头文件就绪的情况下为新内核重建模块。
先检查:
dkms status如果状态显示某个内核版本下是built或缺失,再确认新内核头文件是否装了:
sudo apt install linux-headers-$(uname -r) sudo dkms install nvidia/550.xx --all有时只需要sudo dpkg --configure -a让 dkms 自动补建。这个问题的根本源头,还是内核升级时头文件没有同步到位,和前面聊的第 3 节是一个逻辑闭环。
5.2 BIOS 更新后安全启动密钥“失忆”
如果你的机器是通过 MOK 签名方式过安全启动的,BIOS 升级后可能发现驱动突然又加载失败了,日志里重新出现签名校验错误。这不是 MOK 密钥丢了,而是 BIOS 更新把密钥列表重置了。
解决办法是把之前生成的MOK.der、MOK.priv备份好,重新导入一次:
sudo mokutil --import MOK.der重启进蓝色 MOK 管理界面,重新 Enroll 一次,再对模块重新签名。所以从一开始就建议把密钥文件放到/root/mok/这种安全目录里,不要用完就删。很多人只记得签名、不记得备份,遇到 BIOS 升级就只能重新生成全套,麻烦不少。
5.3 我这些年养成的装机习惯
装新机器时,我会先把编译链和头文件一次配齐:sudo apt install build-essential dkms linux-headers-generic,然后再装驱动。这样把 “Building kernel modules” 的诱发因素提前掐灭。安装方式一律优先 apt 仓库,官方 runfile 只在有明确性能或版本诉求时使用。
另外,我习惯在装完驱动后做一次dkms status,把驱动版本和内核版本记下来。后续升级内核时,多看一眼有没有同步装头文件,就能避免绝大多数驱动失效问题。
到现在我反而觉得,看到 “Building kernel modules” 这个报错,不一定是坏事。它逼着你把内核依赖关系重新过一遍,而这套检查本身就是系统稳定运行的基本功。下次再遇到,先别急着换驱动版本,按日志、头文件、gcc、Secure Boot 的顺序过一遍,你会发现问题并没有想象中那么复杂——我自己装过的机器里,真正需要硬刚设备兼容性的,一只手都数得过来。