
自己动手编译一次 Linux 内核差不多是每个 Linux 使用者的“成人礼”。不只是为了在开机菜单里多一行内核版本号而是当你从uname -r看到一个带自定义后缀的版本字符串时那种“这台机器真正归我管”的掌控感是装多少个软件包都替代不了的。内核编译这件事听起来像硬核大佬的专利拆开看其实就是三件事拿到源码、配置功能、编译安装。但中间任何一个环节没处理好轻则编译报错反复折腾重则开机直接卡死或文件系统挂不上。这篇文章把我这几年编译内核踩过的坑、积累的经验和验证过的流程完整写出来从依赖安装、配置裁剪到引导更新和故障回退尽量让一个没编译过内核的人也能照着做成功。1. 为什么要自己编译内核不只是折腾1.1 内核编译到底解决什么问题很多人觉得发行版自带的内核够用了确实对普通桌面用户来说发行版内核是经过大量测试的稳定性和兼容性都有保障。但当你遇到下面这些情况时默认内核就不那么“够用”了新买的网卡或无线设备驱动没有编译进发行版内核模块插上没反应开发某个内核模块却始终加载失败原因多半是当前内核头文件版本和模块编译环境不一致或者你发现自己的服务器跑着大量线程默认调度参数怎么调都不理想想换一个更激进的调度策略。内核编译的实际意义是让你能按需定制操作系统的底层能力。它相当于把一辆“整车出厂”的发行版系统重新变成了“按自己零件目录组装”。需要哪些驱动就编哪些不需要的音频、蓝牙、老旧设备支持全部裁掉系统变得更精简、启动更快、内存占用更少。对嵌入式设备来说这就是刚需因为存储空间有限不可能像桌面系统那样塞一个几十 GB 的内核源码和模块树。1.2 哪些场景适合自己编译内核我整理了三个最常见的适用场景。第一类是硬件兼容需求比如新款笔记本的触控板、雷电接口、指纹传感器通常需要较新的内核版本才支持。你当然可以等发行版更新内核但如果你等不及自己编译新内核就是最直接的路径。第二类是性能与实时性调优比如音频制作、工业控制、高频交易模拟这类对延迟敏感的任务可以启用CONFIG_PREEMPT或打上实时补丁也可以调整时钟频率和调度器参数。第三类是安全加固和功能裁剪去掉不必要的驱动和协议栈支持关闭某些存在争议的调试接口减少攻击面这在做安全设备或内网服务器时很有用。还有一类不太起眼但很常见的场景做内核驱动开发或者跟着内核源码学习。你改了代码需要编译安装才能验证效果这时候掌握编译流程就是基本功。我自己最早编译内核就是为了把一个第三方驱动模块用上结果一入坑就不可自拔。1.3 版本选择、源码获取与“最小改动原则”内核版本选择上我建议遵循“够新但不盲目追新”的原则。长期支持版本LTS适合生产环境比如 6.6 LTS、6.12 LTS 这类维护周期长bug 修复稳定。最新的 mainline 版本适合尝鲜和开发测试但别指望它多稳定。新硬件强烈建议用比较新的稳定版或 LTS 版老硬件反而用太新内核可能出现回归问题这时选一个你熟悉的 LTS 版本更稳妥。源码获取有两个渠道一个是直接从内核官网下载 tar.xz 源码包另外一个是用 git 拉取仓库。# 方法一下载源码包并解压以 6.6 版本为例 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.x.tar.xz tar xf linux-6.6.x.tar.xz cd linux-6.6.x # 方法二git 浅克隆拉取指定 tag git clone --depth 1 --branch v6.6 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git这里有一个重要的原则第一次编译时别一上来就大刀阔斧地裁剪功能。最稳妥的思路是“基于发行版现有配置做小改动”——先复制当前系统正在使用的内核配置在此基础上做少量变更比如只改版本号后缀、添加某个驱动、调整某个参数确保能正常启动后再慢慢做减法。我见过太多人第一次编译就狠砍配置结果砍掉了一个不起眼的CONFIG_DEVTMPFS启动时连/dev都挂不出来屏幕上只剩一个闪烁的光标。2. 编译前的环境准备与工具链2.1 依赖包清单和安装命令内核编译本身依赖的工具其实不算多但缺一不可gcc负责 C 代码编译make负责自动化构建flex和bison是生成解析器的工具libncurses-dev用于menuconfig图形界面libssl-dev用于生成签名和内核模块dwarves则是生成 BTF 信息BPF 类型格式所需。不同发行版安装命令略有差别这里给出最常见的几类# Debian / Ubuntu 系 sudo apt install build-essential flex bison libncurses-dev libssl-dev libelf-dev dwarves bc # Arch / Manjaro 系 sudo pacman -S base-devel bc ncurses perl # Fedora / RHEL 系 sudo dnf install gcc make flex bison openssl-devel elfutils-libelf-devel ncurses-devel dwarves bc我踩过的一个典型坑是在 Debian 系上忘了装dwarves编译到 BTF 生成阶段直接报错pahole (pahole) is not available。当时以为只是警告结果make install走到一半失败排查了半天才发现是这个不起眼的小包导致的。所以别小看依赖清单哪个都不能省。2.2 磁盘空间、内存和编译并行度评估编译内核是典型的“空间大户”。一个干净的内核源码树解压后大约 1.5 GB编译过程中产生的中间文件会让整个目录膨胀到 10 GB 以上如果开启了CONFIG_DEBUG_INFO之类的调试选项体积会进一步膨胀。所以编译前先用df -h确认分区剩余空间留出至少 15 GB 比较舒服。如果空间紧张有两个对策一是关闭CONFIG_DEBUG_INFO能省掉一半以上的体积二是不要用 root 身份编译避免污染系统目录同时合理规划源码目录所在分区。内存和 CPU 核心数决定了你并行编译的强度。你可以在终端输入nproc查看核心数建议的并行参数是make -j$(nproc)或者略小于核心数。并行度不是越高越好我曾在一台 8 核 8 GB 内存的机器上尝试make -j16结果内存被瞬间打满编译进程被 OOM Killer 干掉。一般原则是每个编译任务大约吃 1 到 2 GB 内存8 GB 内存的机器用-j6或-j8比较安全。内存充足的话可以再用高一点的并行度。2.3 备份现有系统编译前最后一道保险说句老实话我自己早期编译内核时就没想过备份直到一次编译了一个无法启动的内核开机只到 GRUB 就死住才意识到这事的严重性。虽然可以进 recovery 模式回滚但那一通操作下来时间成本翻倍。所以我现在的习惯是弄一个备用启动项。Debian/Ubuntu 系的 GRUB 本身会保留所有已安装的内核所以只要你不主动清理旧的vmlinuz-*和initrd.img-*旧内核就还在可以在 GRUB 菜单里选择启动。这也是为什么我强烈建议安装完新内核后不要急着执行自动清理旧内核的命令。如果条件允许编译前还可以做一个根分区快照比如用 LVM 快照或文件系统快照工具这样即使新内核彻底崩了也能快速恢复到编译前的状态。对于生产服务器这条建议几乎等同于“保命”。测试环境无所谓但生产环境一定要留退路。3. 内核配置这是整个流程的灵魂3.1 配置方式的选型与对比内核功能的开关全部集中在.config文件里这个文件中每一行都是一个CONFIG_XXX配置项。配置这个文件的方式有好几种我按照推荐程度排个序方式命令特点与适用场景基于现有配置生成make olddefconfig复制一份已有.config新版内核新增的选项全部采用默认值快速且安全图形化菜单make menuconfig基于 ncurses 的 TUI 界面支持搜索和逐项选择适合细化调整最小化当前模块配置make localmodconfig只保留当前系统已加载模块对应选项体积最小但可能丢失未加载的驱动恢复默认架构配置make defconfig生成该架构下的默认配置适合从零开始或交叉编译纯命令行问答make oldconfig交互式询问每个新增选项没有图形界面适合远程终端对大多数人来说最好的起点是“把系统现有的/boot/config-$(uname -r)复制到源码目录下的.config然后执行make olddefconfig”。这样你的基础配置和发行版几乎一致踩坑概率最小。在此基础上再进menuconfig做你想要的小幅修改就能兼顾稳定和定制。3.2 参考现有配置站在发行版肩膀上为什么要强调复制发行版配置而不是从零开始因为发行版的内核配置是经过大批开发者测试过的里面包含了大量硬件驱动的成品选项。你自己憋着从defconfig起步很容易漏掉关键选项尤其是存储控制器、文件系统、USB 等基础支持。缺了什么启动时就是一堆kernel panic - not syncing: VFS: Unable to mount root fs之类的错误完全无从下手。复制配置的命令很简单cp /boot/config-$(uname -r) .config make olddefconfig如果你在某个发行版上同时想裁剪体积可以先复制配置然后在menuconfig里手动关闭不需要的选项。一个经验值是先能启动再谈精简。我见过有人把一个标准内核从模块全开裁剪到只剩几十 MB但那是经过数十次重启调试打磨出来的结果不是第一次就能做到的。3.3 常用配置项解析哪些选项值得关注在menuconfig里要关注的核心配置项我列几类最重要的。通用设置里CONFIG_LOCALVERSION可以设置自定义版本后缀比如设为-custom编译后uname -r会显示6.6.6-custom便于识别自己编译的内核CONFIG_DEBUG_INFO是调试信息开关日常使用建议关闭否则编译时间和产物体积都会暴涨。处理器类型和特性里CONFIG_MCORE2或CONFIG_GENERIC_CPU这类选项控制针对特定 CPU 的优化指令集如果你只在自己机器上跑可以选对型号让 CPU 指令集发挥更充分。文件系统部分要根据自己的根分区类型保留对应支持比如CONFIG_EXT4_FS、CONFIG_XFS_FS、CONFIG_BTRFS_FS如果根分区在 NVMe 固态硬盘上还要确认CONFIG_BLK_DEV_NVME是开启的。设备驱动里网卡、显卡驱动要格外小心。建议用lspci查看实际硬件再到menuconfig搜索对应的驱动名称把状态改为*编入内核或M编成模块。lsmod可以查看当前已加载的模块更直接地知道这台机器用到哪些驱动。网络方面如果要用 nftables 或 iptables记得确认CONFIG_NETFILTER相关选项打开。虚拟化场景下CONFIG_VIRTIO_PCI、CONFIG_VIRTIO_BLK、CONFIG_KVM_GUEST这些也最好保留。3.4 配置验证从 .config 到编译前的体检修改完配置不要立刻开始漫长编译先花几分钟做个体检。第一件事是确认符号链接和头文件没问题ls -l /usr/src/linux是否存在或者源码目录里有没有include/generated/autoconf.h。第二件事是查看.config中的关键选项确认没有误关grep -E CONFIG_EXT4_FS|CONFIG_BLK_DEV_NVME|CONFIG_DEVTMPFS|CONFIG_TMPFS .config第三件事如果是在虚拟化的云服务器上编译重点确认CONFIG_VIRTIO_BLKy和CONFIG_VIRTIO_PCIy因为云主机很多根设备走的是 virtio 驱动。很多人编译完重启后系统起不来就是栽在这种看不见的存储驱动上。还有一个小技巧编译前让源码目录保持干净执行make mrproper清理掉历史遗留文件注意这会删除.config如果已经配置好了就跳过这步。4. 编译与安装正式实操环节4.1 make 命令和 -j 参数的计算逻辑配置做完就可以进入真正的编译阶段了。编译本身分为三个子目标内核本体vmlinux/bzImage、内核模块.ko 文件、设备树等附加文件部分架构需要。通常一条完整的编译流程是这样make -j$(nproc) # 编译内核本体和模块 make modules_install # 安装模块到 /lib/modules/版本号/ make install # 安装内核到 /boot并触发引导更新关于-j参数的计算我提供一个比较好的经验公式。nproc返回逻辑核心数但并行编译要同时考虑内存。每增加一个并行任务大约需要 1 到 2 GB 内存所以更稳的做法是# 参考内存 8GB 且核心数大于 4用 -j4 或 -j6 # 内存 16GB 以上可以用满核心数 make -j$(nproc)我自己在 16 核 32 GB 的机器上编译默认配置内核用-j16大概 8 到 12 分钟。在老的 4 核笔记本上首次编译可能要 30 到 60 分钟这都是正常范围。如果只是为了测试配置是否正确可以先执行make -j$(nproc) bzImage只编译内核本体、不编译模块速度会快很多能快速暴露语法错误或配置问题。4.2 编译过程的常见状态解读编译时屏幕会滚动大量编译日志大多不会细看但有几个状态需要认识。如果看到scripts/Makefile.build之类后面跟着Error 1或者Error 2说明某个源文件编译失败需要向上翻日志找具体原因。常见原因包括依赖缺失提示fatal error: xxx.h: No such file or directory、语法问题一般少见、磁盘写满提示No space left on device。编译过程中偶尔会看到 warning 信息比如warning: ... will be removed in the future一般不影响结果可以忽略。但如果出现undefined reference这类链接错误通常是某个CONFIG选项开了一半导致的比如开了某个驱动却关闭了它依赖的底层符号。解决办法就是去menuconfig里检查对应的依赖关系一般依赖项会显示为***并锁定状态这时候要反向把依赖项勾上。编译结束时最后一行一般会有类似Kernel: arch/x86/boot/bzImage is ready的提示这意味着内核本体编译成功。注意看输出路径和大小然后就可以进行安装步骤了。4.3 安装模块与内核make install 的细节模块安装这条命令经常被忽略但它和内核本体同等重要。如果你的某个驱动是做成了模块标为M不执行make modules_install的话即使内核起来驱动也找不到。这条命令会把所有.ko文件复制到/lib/modules/版本号/目录这样后续加载模块才有据可依。在 Debian/Ubuntu 系上make install会自动完成几步把arch/x86/boot/bzImage复制成/boot/vmlinuz-版本号生成 initramfs然后更新 GRUB 菜单。但在某些发行版或某些版本配置下make install不会自动生成 initramfs只复制了内核镜像这个时候开机进入系统时可能因为缺少 initramfs 而无法挂载根文件系统。所以安装完以后我建议手动再执行一次sudo mkinitramfs -o /boot/initrd.img-6.6.6 6.6.6注意第二段的版本号要和make install安装的内核版本完全一致否则路径对不上。如果用的是 Fedora/RHEL 系对应的 initramfs 工具是dracut命令是dracut --force /boot/initramfs-6.6.6.img 6.6.6。4.4 更新引导让新内核出现在启动菜单安装完内核最后一步是更新引导程序。Debian/Ubuntu 系通常直接执行sudo update-grub它会扫描/boot目录下所有vmlinuz-*文件把对应的菜单项写进 GRUB 配置。Arch 系则用sudo grub-mkconfig -o /boot/grub/grub.cfg。更新完成后重启并按住 Shift 或 Esc 进入 GRUB 菜单选择 Advanced options下面应该能看到新版内核的启动项。建议第一选择还是先选“旧内核”做保守验证吗其实不用直接选新内核但如果失败了再回到 GRUB 选旧内核就行。这里有一个容易被忽略的问题/boot分区容量。EFI 引导分区一般只有 100 MB 到 500 MB每装一个内核就是 10 到 20 MBinitramfs 又是 50 到 100 MB装两三个内核就可能爆了。如果update-grub报了和写入有关的错误先df -h /boot看一眼剩余空间。平时最好定期清理老旧内核但注意先确认当前内核能正常工作再清理。5. 常见问题排查实录踩坑与填坑5.1 磁盘空间不足为什么内核越编越大这个问题几乎每个编译过内核的人都会遇到。现象是编译到一半终端弹出No space left on device然后编译中止。排查方法df -h /usr/src # 源码所在分区剩余空间 du -sh linux-6.6.x # 源码目录占了多少如果确认是空间不足清理策略有几个层级。最轻量级的是make clean会删除几乎所有编译产物但保留.config适合想重新编译的情况。更彻底的是make mrproper会把.config也删掉恢复源码树到刚解压的状态适合源码目录被搞乱时使用。还有一个技巧是使用make modules_prepare代替全量编译来验证配置它不会生成完整内核但能为模块编译准备好环境省时又省空间。5.2 依赖缺失与工具链问题在编译过程中最容易让新手困惑的错误信息是形如这样的/bin/sh: flex: command not found make[1]: flex: No such file or directory不要慌这类错误基本就是缺了某个依赖包。按2.1节列出的依赖清单逐个安装到位一般能解决。还有一类是版本过旧比如编译器gcc版本太低导致无法处理内核中较新的语法特性这时需要升级到发行版软件源里的新版 GCC。另一个隐蔽问题出在多版本 GCC 并存的环境如果系统里同时装了老版本和新版本建议在make前显式指定make CCgcc-12如果你使用 clang 而非 GCC内核也支持LLVM1参数但没有必要在首次编译时挑战自己的工具链。5.3 编译报错从报错信息反推 CONFIG 选项编译报错不可怕可怕的是不知道从哪排查。我的经验是先看最后的错误位置再往上报错前几行找线索。如果出现No rule to make target大概率是某个组件没有配置对应的CONFIG选项。比如编译某模块时报缺少某个头文件而这个头文件属于另一个驱动子系统的你就去menuconfig里搜索头文件对应的编译选项把它打开或改为模块。这个方法虽然原始但非常有效。还有一类常见错误是“符号重复定义”或“配置依赖不满足”这通常是你在.config里手动修改了某个CONFIG选项却没有运行make olddefconfig把依赖关系理顺。所以每次改完.config我建议都重新跑一遍make olddefconfig让 Kbuild 系统自动修正依赖关系。这个过程相当于给配置做一次自动体检。5.4 启动后的问题黑屏、模块缺失、卡在开机徽标编译安装成功不等于万事大吉真正的考验是重启。我把启动失败的现象整理成一个速查表方便你对照处理现象可能原因快速排查与处理直接卡在 GRUB 或黑屏无响应内核本身参数错误或驱动缺失在 GRUB 菜单按 e 编辑启动项删掉quiet和splash查看输出信息启动后停在VFS: Unable to mount root fs根文件系统对应驱动未编入或 initramfs 缺失确认CONFIG_EXT4_FS/CONFIG_XFS_FS/CONFIG_BLK_DEV_NVME等选项重新生成 initramfs登录后没有网络网卡驱动没启用或没编成模块lspci查看网卡型号dmesg挂在启动界面但不进入桌面显卡驱动异常更新内核后第三方显卡驱动NVIDIA 等需要重新安装必要时进入终端用 dkms 或官方驱动工具重装模块加载失败提示签名问题CONFIG_MODULE_SIG_FORCE导致强制校验重新配置时关闭模块签名强制或生成签名证书并完成签名启动失败时一个非常实用的技巧是在 GRUB 菜单高亮选择新内核后按e找到以linux开头的那一行在末尾加上single或emergency参数进入单用户模式或紧急模式这样即使系统服务起不来也能获得一个 root shell 来排查修复。5.5 回退方案如何安全地回到旧内核如果新内核折腾了半天还是起不来最稳的做法就是回到旧内核。在 GRUB 菜单的 Advanced options 子菜单中能看到之前旧内核的启动项选择旧内核正常进入系统即可。进入系统后卸载或清理新内核时要注意不要手动直接删除/boot下的文件就算完规范的清理方式是# 查看已安装内核包发行版管理的场景 dpkg --list | grep linux-image # 卸载指定版本Debian/Ubuntu 系 sudo apt remove --purge linux-image-6.6.6 linux-headers-6.6.6 # 重新生成引导菜单 sudo update-grub如果新内核是直接手动复制到/boot的没有通过发行版包管理器安装那删除对应vmlinuz-*、initrd.img-*、System.map-*以及/lib/modules/下对应目录再update-grub就行。注意千万别在删完内核后忘记更新 GRUB否则菜单里会残留无法启动的僵尸条目。6. 实操心得与最终建议6.1 我编译了这么多内核后的体会编译内核从第一次开始就会上瘾。但说实话我早期编译内核时走了不少弯路最大的弯路就是太追求“最精简”、“最优化”。有一次我把内核裁剪到编出来的bzImage不到 8 MB结果启动后没有 USB 键盘驱动只有 PS/2 接口能用物理机还能敲命令如果是远程服务器就彻底失联了。后来我学乖了新机器、新场景第一版永远是在发行版配置基础上做最微小改动先把系统完整跑起来再去逐项裁剪掉用不到的功能。这样虽然会多花一两轮编译时间但整体效率反而是最高的。我也越来越觉得编译内核最大的价值其实不是得到一个“更快”或“更小”的系统而是强迫你去理解操作系统的组件构成。原来那个df -h里的/boot分区为什么不能太小为什么uname -r和/lib/modules下的目录是对应的为什么 NVIDIA 驱动每次升级内核后都要重装一次——这些知识的碎片会在编译一次内核后全部串联起来形成完整的认知地图。6.2 一些提高效率的小技巧有几个提高效率的小技巧我在实际操作中反复验证过。第一每次编译完把.config备份一份到系统其他分区比如~/kernel-configs/config-$(date %F).config这样下次想调整时可以直接在此基础上改不用再从头配一遍。第二升级到新版本源码时把旧的.config复制过去运行make olddefconfig比在menuconfig里一个个选项重新勾选快得多而且遗漏风险小。第三编译时在命令前加time例如time make -j$(nproc)编译结束后会输出耗时这对判断机器性能或配置变更的影响很直观。还有一个小技巧几乎是“保命”级编译完先别急着把所有旧内核清理掉。至少保留最近两个版本的内核和对应的 initramfs等新内核稳定运行一两周、确认没有潜在问题后再清理旧内核也不迟。这在生产环境上尤其重要新内核即使能启动也可能在某天触发某个原来没暴露出来的驱动问题有旧内核做备份切换成本几乎为零。6.3 扩展方向从编译到内核源码阅读编译流程跑通之后你会发现内核源码已经不神秘了。下一步我建议往两个方向深入一是试着修改某个驱动或某个调度参数编译后对比差异培养“改代码验证”的实验习惯二是学会使用menuconfig里的搜索功能通过CONFIG_XXX反查源码的Kconfig文件和实现代码建立起“配置项到源码”的映射能力。这会对你后续做驱动开发、系统调优、嵌入式移植都非常有帮助。如果你是在做交叉编译比如给 ARM 开发板编译内核流程思想上完全一样区别在于需要先安装对应的交叉编译工具链并设置ARCHarm64和CROSS_COMPILEaarch64-linux-gnu-这两个环境变量。有了在 x86 上完整编译一次的经验你会发现交叉编译只是换了个工具链和架构前缀心态上会从容很多。最后分享一个我现在的习惯每个季度会固定编译一次新的 LTS 内核作为对这个季度系统环境的例行刷新。这个过程像给服务器做一次彻底的系统级体检能让我重新审视这台机器上到底跑着哪些驱动、哪些模块可以被裁剪比任何监控软件都更直观。也建议你编译一次内核试试那种从源码到启动菜单完全掌控的感觉值得体验一次。