
上次聊内核源码结构的时候不少朋友在评论区问能不能直接把自己编译的内核装到机器里用起来说实话读代码和用上自己编译的内核是完全不同的两个体验。前者是“看懂”后者是“折腾懂”。编译内核这件事入门门槛其实不算高但坑是真的多——我前后踩过的、帮别人排查过的加起来能写一篇长文。这篇文章就完整走一遍从源码下载、内核配置、编译、安装到引导菜单出现新内核条目的全过程用 6.12 LTS 版本做演示。适合两类人一是为了解实际驱动问题无线网卡、触摸板、新版显卡驱动要求较新内核而被迫动手的二是想真正理解内核构建流程的开发者。物理机和 WSL2 的操作流程基本一致差异点我会单独标注。1. 编译内核前先想清楚这三件事1.1 编译内核到底在“编译”什么先明确一件事内核编译的产物不只是那个几兆字节的内核镜像文件。完整的内核构建会产生三类东西压缩后的内核本体x86 平台为arch/x86/boot/bzImage大量的内核模块.ko文件分散在源码树各个目录中System.map、Module.symvers等辅助文件排查问题时有用日常使用感知不强很多人以为编译内核就是“make 一下出来一个文件”其实模块才是大头。现代发行版内核的模块数量动辄上万个绝大部分驱动、文件系统、网络协议都是以模块形式加载的。你编译时如果没把某个硬件对应的驱动编进去或者编成了模块却没有安装到对应目录启动新内核后硬件就会“消失”。这也是后面排错最常见的根源。1.2 你会得到一个什么样的内核编译出来的是“你自己配置的内核”。同一个源码包不同人编出来可能差异巨大。你可以只保留自己硬件需要的驱动把无关的东西全砍掉得到一个启动飞快、体积很小的内核也可以保持和发行版几乎一致的配置只是升级内核版本。但也要泼一盆冷水如果你只是想解决某个软件需要高版本内核的问题而你的发行版仓库里已经有新内核包直接装官方包往往更省事。自己编译内核的正确使用场景是硬件太新发行版内核还没来得及开启对应驱动需要开启某些发行版默认关闭的实验性特性修改内核源码做学习或二次开发精简内核以减少攻击面和启动时间如果以上一个都不沾这篇教程可以当知识储备看不必实际动手。1.3 时间成本与机器要求编译时间取决于机器性能。以 6.12 这种体量的内核源码为例8 核 16 线程的 CPU、内存 16GB 的前提下完整编译含模块大约需要 20 到 40 分钟。4 核老机器可能要一个半小时以上。磁盘空间方面源码解压后约 1.5GB编译过程会产生大量中间文件完整构建后源码目录可能要占到 8 到 10GB。另外/boot分区至少预留 200MB最好有 1GB 以上——很多发行版默认/boot只有几百兆装上几个旧内核之后再装新内核直接写满。内存方面并行编译时每个编译任务吃几百兆内存很常见8 核并行建议至少 8GB 可用内存。内存不足时编译过程会出现 OOM内存耗尽被杀进程的错误表现形式往往是随机某个.c文件编译时报错重试又能过这种最恼人。2. 环境准备与源码获取2.1 磁盘空间与分区检查动工之前先检查磁盘。两条命令df -h /boot df -h /usr/src/usr/src只是我建议的解压位置你放在~/kernel-build这类用户目录下也可以。重点看/boot剩余空间如果小于 200MB建议先清理旧内核或者直接把/boot挂在空间更大的分区上。我见过不止一个人编译花了半小时结果make install时/boot写满了grub 更新失败新内核也没装上去还得回头清理。2.2 安装编译依赖不同发行版依赖包名差异很大这一步最容易被忽略。Debian/Ubuntu 系执行sudo apt update sudo apt install build-essential libncurses-dev flex bison libssl-dev libelf-dev dwarves bc rsync cpioFedora/RHEL 系执行sudo dnf install gcc make flex bison openssl-devel elfutils-libelf-devel dwarves bc rsync cpio ncurses-devel perl逐个说一下这些包的作用。build-essential提供 gcc 和 makeflex和bison是内核解析配置文件和设备树时用到的词法/语法分析器libssl-dev是内核开启模块签名和某些加密特性后编译必需的libelf-dev用于生成内核镜像的 ELF 信息dwarves提供pahole工具新版内核默认开启的 BTF 调试信息CONFIG_DEBUG_INFO_BTF依赖它少了它会在链接阶段报错。bc是内核构建脚本里做数值计算用的某些平台不装会报奇怪的语法错误。2.3 下载并校验内核源码从内核官网https://www.kernel.org下载长期支持版本。选版本有个小建议不要追最新选 LTS 版本更适合作为自编译内核的基线因为 bug 修复周期长社区资料也全。下载命令cd ~ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.17.tar.xz下载完做一下 SHA256 校验官网每个下载链接旁边都给了校验和。这一步不要省内核源码被投毒的事虽说概率极低但校验一下成本几乎为零sha256sum linux-6.12.17.tar.xz确认校验和一致后解压tar -xf linux-6.12.17.tar.xz cd linux-6.12.17如果你的网络环境不方便直接访问官网发行版源里也有内核源码包Debian 的linux-source包、Fedora 的kernel.src.rpm但从官网下载 tar.xz 是最干净、最不容易被发行版补丁干扰的方式。3. 内核配置决定“你的内核”长什么样的那一步3.1 配置从哪里来配置内核是整条链路里最需要动脑子的环节也是新人最容易懵的地方。内核有几万个配置项自己从头配不现实。最稳妥的起点是用当前运行内核的配置作为基底。Debian/Ubuntu 系的运行配置文件在/boot/config-$(uname -r)Fedora 系可能在/boot/config-*或/lib/modules/$(uname -r)/config。把它复制到源码目录cp /boot/config-$(uname -r) .config光复制还不够你的发行版内核配置基于某个旧版本源码里的新配置项没有值。执行make olddefconfig这一步会把所有缺失的配置项自动填入默认值。对于大多数配置项“默认值”就是编译时最安全的选项。olddefconfig和oldconfig的区别在于oldconfig会逐个询问你新配置项的选择几百上千个问题能问到你崩溃olddefconfig直接采用内核推荐的默认答案干净利落。如果你完全不想继承发行版配置想从一个干净的最小配置起步可以运行make defconfig或者针对特定硬件做精简make localmodconfiglocalmodconfig会分析当前系统已加载的模块只保留这些模块对应的配置项。这样配置出来的内核和当前机器硬件高度匹配编译快、体积小但换一台机器大概率起不来。作为学习可以试试日常使用还是建议以发行版配置为底。3.2 关键配置项新手必须检查的几项拿到.config后用grep或make menuconfig检查以下配置项。配置项作用建议CONFIG_MODULE_SIG模块签名校验发行版默认开启自编译模块加载可能失败建议关闭或自签密钥CONFIG_DEBUG_INFO生成调试信息开启后编译时间翻倍、磁盘占用暴涨学习调试可保留纯使用建议关闭CONFIG_DEBUG_INFO_BTFBTF 调试格式需要pahole工具不开会导致部分工具无法工作但编译时间也明显拉长CONFIG_LOCALVERSION内核版本后缀强烈建议改为自定义字符串避免和发行版内核版本号撞车CONFIG_X86_X2APIC现代 CPU 特性默认开启无需手动改CONFIG_PREEMPT内核抢占模型桌面用推荐CONFIG_PREEMPT全抢占服务器可按需选择最关键的坑是模块签名。现在主流发行版都启用了模块签名机制如果你从发行版配置起步自编译出来的模块可能因为签名不匹配而拒绝加载。最直接的解决办法是关掉强制签名scripts/config --disable MODULE_SIG scripts/config --disable MODULE_SIG_FORCE想研究签名机制的可以暂时保留但新手学习阶段建议关闭否则后续自己写的模块、第三方驱动模块加载时会出现Required key not available的报错排查起来很折磨人。3.3 用 menuconfig 做可视化微调命令行改完配置强烈建议再用make menuconfig打开图形配置界面看一眼至少熟悉一下内核配置的交互方式make menuconfig这是个基于 ncurses 的文本界面方向键移动回车进入子菜单y编入内核、m编成模块、n禁用/键可以搜索配置项。搜索功能非常实用比如你想确认某个配置项叫什么名字敲/输入关键词就能看到对应的路径和当前状态。有个小技巧在menuconfig里改配置保存时它会更新.config文件同时生成大量.*.cmd和缓存的依赖文件。如果你在命令行用scripts/config改了配置之后再进menuconfig没看到生效别慌检查一下是不是多个终端同时操作导致写冲突。3.4 配置文件的版本管理.config是你这次编译的“配方”强烈建议多留几份备份。我的习惯是cp .config ~/kernel-configs/config-6.12.17-mybuild为什么要这样因为编译过程中如果改了配置或者同一份源码多次编译.config会被覆盖。等你折腾完想回头对比“上次到底开了什么选项”时没有备份就只能后悔。更讲究的做法是把.config纳入 git 管理后面我还会细说。4. 开始编译命令背后的参数逻辑4.1 并行度的科学选择配置完成后编译命令的核心是并行度参数make -j$(nproc)nproc返回的是逻辑核心数-j$(nproc)就是让每个逻辑核都跑一个编译任务。这个选择在小机器上没问题但有一个隐蔽的陷阱每个编译任务的内存开销不是固定的某些文件编译时内存飙得很高。如果你的机器是 8 核但只有 8GB 内存-j8有可能触发 OOM。我的经验公式并行度 逻辑核数但当内存小于 2GB/核 时把并行度砍半。例如 4 核 4GB 内存的机器用make -j2反而比make -j4稳定。如果你不想赌内存够不够可以直接限制编译内存压力make -j$(nproc) 21 | tee build.log把编译日志写入文件出错时不用翻终端历史记录。这一步很值得养成习惯编译内核的报错信息往往有几百行日志里定位error:关键字比肉眼盯屏幕高效得多。4.2 编译进度与常见异常编译过程会不断刷屏每一行都是编译某个.c文件的 gcc 命令。你不需要看懂每一行但需要知道哪些现象是正常的哪些是需要干预的。提前说一下常见报错的处置思路详细的排查链路放在后面。yylloc相关错误老版本内核源码碰到新版本 gcc 时的经典错误一般是flex/bison生成的解析器代码与 gcc 新版本不兼容。解决办法是升级内核源码版本或者给HOSTCFLAGS加上-Wno-error之类的参数绕过去。BTF: .tmp_vmlinux.btf: pahole (pahole) is not available前面提到的dwarves包没装补装即可。No space left on device磁盘写满没有任何技巧腾空间。随机某个文件编译时报错、重试又能过大概率内存不足触发 OOM降并行度。正常编译过程的最后阶段会输出类似Kernel: arch/x86/boot/bzImage is ready看到这行说明内核本体编译成功了。紧接着会自动编译模块模块很多会持续一段时间。整个过程没有报错最后回到 shell 提示符说明编译阶段完成。4.3 模块签名问题的预防如果你没有在配置阶段关闭模块签名编译后期可能遇到签名工具的问题。具体表现是编译完模块后签名步骤报错提示找不到签名密钥或sign-file工具。处理方式有两个一是回到配置阶段禁用签名重新编译二是生成自己的签名密钥配置CONFIG_MODULE_SIG_KEY指向密钥文件。对新手来说关闭签名是最省心的路径。内核模块签名本质上是防止未授权模块进入内核的校验机制学习阶段先不碰它等后面理解了加载机制再研究不迟。5. 安装让新内核出现在引导菜单里5.1 安装模块编译完成后第一件事是安装模块不是直接装内核镜像。模块安装命令sudo make modules_install这条命令会把编译好的所有.ko文件安装到/lib/modules/6.12.17-custom/目录下目录名取决于你配的版本号。这一步本质上是把“零件”放到内核运行时能找到的标准位置虽然make install有时也会隐式触发模块安装但显式执行一次能避免部分工具链行为差异带来的遗漏。安装模块时加一个环境变量能省不少磁盘空间sudo make modules_install INSTALL_MOD_STRIP1INSTALL_MOD_STRIP1表示安装时剥离模块的调试符号。模块的调试符号占了很大体积生产环境用不上建议开启。如果你打算后续用gdb或perf调试内核模块就别加这个参数。5.2 安装内核镜像与 System.map模块装好后安装内核本体sudo make install这条命令在不同发行版上的行为有差异。在 Debian/Ubuntu 系make install会调用/sbin/installkernel它会复制arch/x86/boot/bzImage到/boot/vmlinuz-6.12.17-custom复制System.map到对应文件自动生成initrd.img通过update-initramfs自动运行update-grub更新引导菜单在 Fedora/RHEL 系机制类似但走的是kernel-install或new-kernel-pkg。如果你用的是 Arch 这类系统可能没有自动安装脚本需要手动复制内核镜像sudo cp arch/x86/boot/bzImage /boot/vmlinuz-linux-custom sudo cp System.map /boot/System.map-linux-custom这里要特别留意新版内核源码树里已经没有System.map文件了但源码根目录下会生成同名文件。检查一下如果有就复制。5.3 更新引导器并检查菜单Debian/Ubuntu 系执行make install后会自动跑update-grub但如果你改了/etc/default/grub或想手动刷新可以重跑sudo update-grubFedora 系对应sudo grub2-mkconfig -o /boot/grub2/grub.cfg更新完检查一下新内核是否进了引导菜单grep menuentry /boot/grub/grub.cfg能看到类似Ubuntuwith Linux 6.12.17-custom的条目。确认存在后再重启。如果你的机器用的是 systemd-boot很多 Arch/定制系统流程完全不同需要手动把内核镜像复制到 EFI 分区并在 loader 配置里新增一条启动项。这个场景比较复杂篇幅有限不展开。用 GRUB 的读者按上面的步骤走就行。5.4 WSL2 用户特别说明WSL2 支持自定义内核但安装机制完全不同。首先不用管 grub 和/bootWSL2 的引导由 Windows 侧配置你需要在 Windows 用户目录下新建.wslconfig文件把编译好的内核镜像bzImage复制到 Windows 的文件系统例如C:\Users\你的用户名\kernel\bzImage在.wslconfig里写[wsl2] kernelC:\\Users\\你的用户名\\kernel\\bzImage然后在 PowerShell 里执行wsl --shutdown重启 WSL再进入发行版后执行uname -r验证。WSL2 不需要 initramfs不需要更新引导比物理机简单很多。6. 我踩过的坑编译启动全链路排查实录6.1 编译期磁盘写满与工具链版本不匹配先说一个真实案例。有次我在只有 120GB 硬盘的笔记本上编译 6.6 内核看着源码目录越来越大但没当回事。编译到模块阶段报错No space left on device一开始以为只是偶发清理了点临时文件继续反复折腾了三次才意识到是源码目录本身已经占满了磁盘。最后删掉了几个不需要的内核源码目录才算编译完。这类问题有更好的前置检查手段du -sh linux-6.12.17 df -h .编译开始前确认源码所在目录有 10GB 以上可用空间。如果你开了CONFIG_DEBUG_INFO这个数字直接翻倍到 20GB 以上。磁盘不够时别说make clean了连make mrproper彻底清理编译产物都可能因为空间不足跑不完。gcc 新版本和旧内核源码的兼容性问题也很常见。yylloc报错出现在老内核4.x 时代配新 gcc 编译时本质是flex生成的yyloc全局变量在 gcc 高版本下不再隐式声明触发了编译错误。6.x 版本的内核几乎没有这个问题如果非要用老内核临时规避手段是make HOSTCFLAGS-Wno-errorimplicit-function-declaration -j$(nproc)6.2 启动期VFS 挂载根文件系统失败这是内核编译新手最容易遇到的启动故障现象是Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)我总结了一条完整的排查链路按顺序走大多数情况能定位问题。第一步确认 initramfs 是否加载。屏幕上如果出现Loading initial ramdisk说明 initramfs 存在且被 grub 找到了如果直接跳过说明 initrd 没有正确生成或引导配置里少了initrd行。检查/boot目录ls -lh /boot/initrd.img-6.12.17-custom没有这个文件就去检查上一步make install是否真正执行了 initramfs 生成脚本。Debian/Ubuntu 系手动补生成sudo update-initramfs -c -k 6.12.17-custom第二步检查根文件系统驱动是否编入内核。这个坑很隐蔽如果你用发行版配置起步文件系统是编成模块的。正常情况下 initramfs 会装载包含这些模块但当你自定义配置时可能不小心把相关模块漏掉了。确认方法zgrep CONFIG_EXT4_FS /proc/config.gz 2/dev/null || grep CONFIG_EXT4_FS /boot/config-$(uname -r)如果是m说明 ext4 是以模块形式存在必须确保 initramfs 里包含了它。查看 initramfs 内容lsinitramfs /boot/initrd.img-6.12.17-custom | grep ext4没有输出就说明 initramfs 缺少文件系统模块需要回配置阶段把CONFIG_EXT4_FS改为y直接编入内核重新编译。这是新手最推荐的保险做法核心文件系统驱动直接编入内核不依赖 initramfs。第三步检查root引导参数。grub 菜单里按e查看启动参数确认rootUUIDxxx指向的确实是根分区。这个环节的问题通常不是因为你编译内核而是你恰好用了不正确的分区。对照blkid确认UUID一致即可。第四步如果以上都没问题考虑模块版本不匹配。你升级了新内核但/lib/modules/新版本号目录不存在或内容不完整。检查ls /lib/modules/6.12.17-custom模块目录缺失意味着模块安装步骤出了问题重新执行make modules_install后再试。这套链路帮我解决过不下十次“编译完起不来”的问题。核心逻辑就一句话内核启动到最后一步需要能挂载根文件系统而这条路依赖内核本体、initramfs、文件系统驱动、引导参数四个环节逐个排查即可。6.3 版本号撞车的隐藏坑如果你编译时没改CONFIG_LOCALVERSION编译出的内核版本号可能恰好和发行版现有的旧内核相同。比如运行中的内核是6.8.0-48-generic你编译官方 6.8 源码版本号可能变成6.8.0这就会引起一系列诡异现象。最典型的是make install时新内核直接覆盖了同名旧内核的/boot文件grub 菜单里的旧条目也被覆盖。但/lib/modules下的目录也同名旧模块会被新模块覆盖或混合极易出现模块与内核镜像不匹配的启动故障。解决方式就是在编译前配置CONFIG_LOCALVERSIONscripts/config --set-str CONFIG_LOCALVERSION -mybuild这样版本号变成6.12.17-mybuild和系统自带内核彻底区分开。即便出问题旧内核依然安全重启时用 grub 高级选项选回旧内核即可。6.4 删除旧内核的正确操作自编译内核如果不用了删干净需要三处文件/boot下的镜像文件、/lib/modules/对应版本目录、grub 菜单条目。Debian/Ubuntu 系用包管理器处理起来更安全sudo apt remove --purge linux-image-6.12.17-mybuild但自编译内核通常不是用dpkg安装的没有软件包记录需要手动删sudo rm -f /boot/vmlinuz-6.12.17-mybuild sudo rm -f /boot/initrd.img-6.12.17-mybuild sudo rm -f /boot/System.map-6.12.17-mybuild sudo rm -rf /lib/modules/6.12.17-mybuild sudo update-grub手动删除时先后顺序有讲究先删/boot文件再删模块目录最后更新 grub。如果先把 grub 更新了菜单里虽然还有指向同名文件的条目但文件已经没了下次启动必然进恢复模式。7. 进阶玩法把内核编译变成高效工作流7.1 配置增量管理当你编译内核的次数多起来之后最痛苦的不是编译本身而是“这次我到底改了哪些配置”。单纯靠.config对比不够直观我建议把整个内核源码目录变成 git 仓库每次编译前把.config提交一次git init git add .config git commit -m config: 6.12.17 baseline后面每次调整配置后git diff .config就能清楚看到改动。配合scripts/config的命令行操作整个流程可以脚本化scripts/config --disable DEBUG_INFO scripts/config --set-str LOCALVERSION -mybuild make olddefconfig这样配置过程可以被记录、被复现比在 menuconfig 里手动点来点去可靠得多。7.2 用 ccache 加速重复编译如果你会频繁改动源码并重新编译推荐启用ccache。它缓存编译过的 C 文件结果第二次编译时未改动的文件直接命中缓存整体提速非常明显。安装启用sudo apt install ccache export CCccache gcc make -j$(nproc)实测小改动后重新编译时间能从几十分钟降到几分钟。代价是需要额外几 GB 磁盘给缓存但绝对值。7.3 我的个人习惯编译内核做了这么多年我的流程基本固化成了这样下载源码、校验、复制当前运行配置、改LOCALVERSION、关闭签名和调试信息、make olddefconfig、make menuconfig快速扫一遍关键项、备份.config到独立目录、make -j$(nproc)、make modules_install、make install、检查 grub 菜单、重启验证。这套流程中间省略了很多摸爬滚打但每一步背后都对应着真实踩过的坑。如果你问我编译内核最值钱的收获是什么我会说不是那个加快启动速度或省了 200MB 内存的内核而是当你亲手把一个从源码构建的完整操作系统内核跑起来、并且能独立排查启动故障之后你对 Linux 系统整体的理解会上一个台阶。这个经验无法速成只能靠一次一次编译、启动、排错来积累。