有一说一操作系统实验做到“Linux 内核编译及运行”这一步才算真正开始摸到操作系统的门槛。前面那些进程、线程、调度算法的实验大多还是在“用”操作系统而这个实验是在“改”操作系统。整条链路下来拿源码、给配置、触发编译、装模块、更新引导、重启验证每一步都有讲究任何一环出问题轻则编译失败重则开机直接进不了系统。我这次在虚拟机里完整跑了一遍这个实验用到的环境是某高校操作系统课程实验指导书里推荐的配置宿主机开 VMware Workstation客户机装 Ubuntu内核源码选的是当前稳定版的 Linux 主线内核。整个过程做下来大概花了一个下午加一个晚上中间踩了磁盘空间不足、编译选项冲突、重启后进不了新内核这几个典型的坑。这篇就把从零到一的操作记录、每一步的底层逻辑、以及踩坑之后的完整排查思路都写出来给后面做同样实验的同学当个参考也欢迎已经跑通的朋友一起交流。1. 实验目标与前置准备先搞清楚内核编译到底在“编”什么1.1 内核编译不是“编译一个程序”那么简单很多同学第一次看到内核编译的实验指导书第一反应是“这不就是 make 一下吗”实际上远不止。Linux 内核本身是一个宏内核但它不是一整个单一的可执行文件而是由核心映像vmlinuz、可加载内核模块.ko 文件、头文件、以及一堆构建期生成的中间文件共同组成的综合体。编译内核的本质是把成千上万个 C 源文件按照你给定的配置选项就是那个 .config 文件组合成一套可启动的系统核心。这套核心决定了操作系统能识别哪些硬件、支持哪些文件系统、启用了哪些调度策略、包含哪些安全模块。换句话说你在 menuconfig 里做的每一个选择最后都会直接影响你重启之后那个系统的“性格”。实验指导书要求“编译并能运行”深层目的是让我们理解这么几件事内核的构建体系Kconfig / Kbuild是如何工作的.config 配置选项如何决定内核的功能集合模块机制如何让内核功能可以按需加载、无需重编整个内核引导加载程序如何找到内核映像并完成启动理解了这几件事后面每一步操作才有意义而不是机械地敲命令。1.2 环境清单与磁盘空间预估我在实验里使用的环境如下仅供参考宿主机Windows 11VMware Workstation 17虚拟机某发行版 Linux内核版本 5.15分配 4 核 CPU、8GB 内存内核源码Linux 6.x 稳定版从内核官网下载的 tar.xz 源码包磁盘规划根分区分配 40GB 虚拟磁盘/boot 分区独立分配 1GB这里特别强调磁盘空间因为这是我整场实验第一个坑。首先内核源码包解压之后大概 1.3GB 左右如果你开启了 CONFIG_DEBUG_INFO 这种调试信息选项编译中间产物会膨胀得非常快/usr/src 目录下面最后可能吃掉 5 到 8GB 空间。再加上模块安装到 /lib/modules 下以及 /boot 下生成的 vmlinuz、initrd.img、System.map我建议虚拟机的根分区至少给到 20GB 以上/boot 分区独立给 500MB 以上如果磁盘宽裕直接给 40GB 根分区最省心。这也是为什么我一直建议用虚拟机而不是物理机做这个实验的原因。虚拟机可以提前打快照编译失败、引导损坏、重启起不来一键回滚心理压力小很多。物理机如果没做好引导备份一旦 grub 配置写错或者 initrd 生成失败运气不好就是开不了机处理起来非常麻烦。1.3 虚拟机里的“快照思维”要提前建立做内核编译这种高危实验强烈建议在动手之前先打一个干净快照。我的习惯是这样刚装完系统、能正常上网的时候打第一个快照叫“clean-base”安装完编译工具链build-essential、libncurses-dev、bc 等之后再打一个快照叫“with-toolchain”每次准备改引导配置之前手动再补一个快照这样做的原因是内核编译实验中很多问题不是“编译失败”这种温和错误而是“重启之后直接进不了系统”的严重故障。有了快照最坏情况也就是回到十分钟之前的状态重来一遍。没有快照你可能要在 grub 命令行里手忙脚乱地 rescue甚至重装系统。整个实验流程我梳理成了下面这张表方便你心里先有个全貌阶段关键动作产出物常见失败点源码准备下载、校验、解压内核源码树磁盘空间不足配置生成make menuconfig 或复制现有 config.config 文件配置依赖冲突、选项丢失编译make -jNbzImage、内核模块工具链缺失、磁盘写满模块安装make modules_install/lib/modules/版本号/目录权限不足内核安装make install/boot/vmlinuz、initrd.img、grub 菜单更新GRUB 配置失败重启验证重启选新内核并登录新内核运行环境进不去系统、无网卡、无模块2. 内核源码获取与配置策略从哪下载、怎么配置、为什么这样配2.1 源码下载与完整性校验内核源码的获取方式有很多常见的有三种直接从内核官网下载 tar.xz 包、用发行版仓库里的 linux-source 包、或者 git clone 官方仓库。我这次用的是官网下载 tar.xz 的方式。原因很简单实验报告需要体现“完整下载并编译官方主线内核”这个动作用发行版自带的 linux-source 包虽然省事但很多时候源码版本和当前系统内核版本几乎一样实验价值会打折扣。下载之后不要急着解压先做校验。tar.xz 包旁边一般有对应的 .tar.sign 签名文件正规做法是用 gpg 验证签名但对学生实验来说至少要做一次 sha256sum 校验确认下载的文件没有损坏。我当时没做这一步结果编译到一半报了一个奇怪的语法错误查了半天发现是源码包在下载过程中损坏了白费了一个小时血泪教训。解压的时候注意目录命名。内核源码编译有一个不那么起眼但很重要的规则源码路径里最好别有中文、空格、特殊字符否则会有一些 Makefile 层面的兼容问题。我习惯解压到 /usr/src 下命令大概是sudo tar -xvf linux-6.x.tar.xz -C /usr/src cd /usr/src/linux-6.x如果提示权限不够记得用 sudo或者在 /usr/src 下先授权给自己。2.2 为什么我建议复制当前 config而不是 make defconfig拿到源码后的第一件事不是编译而是生成配置文件。这个 .config 文件是内核编译的“总开关”里面每一行 CONFIG_XXXy/m/n 都对应一个功能模块的取舍。很多教程上来就说 make defconfig这个命令会生成一个默认配置理论上能编过但实际跑起来很可能缺驱动、缺功能尤其是在虚拟机环境里网卡驱动、图形支持、虚拟机工具模块可能都没编进去。更稳的做法是复制当前系统的配置文件作为起点cp /boot/config-$(uname -r) .config make olddefconfig这几行命令的逻辑是当前系统正在跑的内核它的配置一定是最适合当前硬件环境的。把它作为基线然后用 olddefconfig 把源码新增的配置项填成默认值既保留了系统原本能用的所有硬件支持又不会因为漏了新选项导致配置不完整。这里解释一下 olddefconfig 和 oldconfig 的区别。oldconfig 是逐条交互式询问新增选项会卡在终端里一条一条按回车olddefconfig 则是不询问直接把没显式设置的选项置为默认值。实验场景下直接用 olddefconfig 更顺滑不用守着终端按键。如果你对内核比较熟悉想顺手体验一下裁剪可以再执行make menuconfigmenuconfig 是基于 ncurses 的图形化配置界面按空格切换 y/m/ny 表示编译进内核m 表示编译成模块n 表示完全不编译。实验报告里如果能写清楚如何在 menuconfig 里添加对某个文件系统的支持比单纯贴命令更有说服力。2.3 menuconfig 里真正值得动一动的选项虽然复制当前配置之后不做修改也能编但我建议在 menuconfig 里至少做两三个有意义的改动这样实验报告才有“你主动配置过内核”的证据。比较自然的选择是调整 CPU 相关的优化选项。在 “Processor type and features” 里把 “Processor family” 选成和虚拟机 CPU 匹配的型号比如 Generic-x86-64 或者某个具体型号。这个选项会影响编译优化选得越准编译出的内核理论上执行效率越高。另一个值得动的是 “General setup” 里的 “Local version - append to kernel release”。在这里加一个自定义后缀比如 -exp这样编译安装之后uname -r 会显示类似 6.x.x-exp 的版本号。这个后缀最大的好处是你能一眼区分当前跑的是原版内核还是自己编译的内核也方便在实验中重复编译不同版本而不会互相覆盖。做完配置改动之后记得检查一下 .config 里的关键项。比如 CONFIG_MODULESy这是内核模块机制的总开关如果被设成 n后续 make modules_install 会直接失败。我见过有同学在 menuconfig 里手滑把模块支持关掉的编译全程都在报错所以这个点可以多留个心眼。3. 全量编译、模块安装与引导更新核心操作流程复盘3.1 并行编译参数与资源分配配置生成之后正式编译。直接裸跑 make 也能编但速度慢得让人崩溃尤其是现在内核动辄几万个源文件单线程编译可能需要两三个小时以上。所以要善用并行编译参数make -j$(nproc)nproc 会输出 CPU 逻辑核数-j 让 make 同时跑多个编译任务。我给虚拟机分配了 4 核所以这里实际执行的是 make -j4整个内核编译大约用了 25 到 30 分钟。如果你给虚拟机分配了 8 核时间能压缩到 15 分钟以内。但这里有个度。如果 -j 参数超过实际核数太多比如 4 核机器直接 -j16反而会因为频繁切换任务、内存吃紧导致编译变慢甚至触发 OOM。我一般遵循“逻辑核数乘以 1 到 1.5”这个经验值。编译过程中屏幕上会滚过大量信息大致长这样CC init/main.o CC arch/x86/entry/syscalls/syscall_64.o ... LD vmlinux OBJCOPY arch/x86/boot/bzImage看到 LD vmlinux 之后又出现 OBJCOPY arch/x86/boot/bzImage说明核心内核映像已经构建成功这是好消息。bzImage 是 x86 平台上实际参与引导的压缩内核映像后面 make install 主要就是把它安装到 /boot 目录。3.2 两次 make 之间的分工编译核心、编译模块、安装各司其职内核模块的编译和核心映像的编译是同时进行的都是在执行 make 的时候完成。但安装阶段一定要分两步而且要按顺序sudo make modules_install sudo make install第一步 modules_install 会把编译出来的 .ko 内核模块安装到 /lib/modules/内核版本/ 目录下。所有你配置为 m 的功能都会以模块文件的形式出现在这里。这个目录可以理解为内核功能的“外挂仓库”内核启动后再按需加载。第二步 make install 做的事情更多把 arch/x86/boot/bzImage 拷贝成 /boot/vmlinuz-版本号生成对应的 /boot/initrd.img-版本号初始 RAM 磁盘拷贝 System.map 和 .config 到 /boot 下自动执行 update-grub把新内核添加到 GRUB 菜单为什么要先装模块再 make install因为 initrd.img 的生成过程需要读取模块目录。如果你先执行 make install系统会尝试生成 initrd 但里面对应的新内核模块目录还不存在生成的 initrd 很可能缺关键的磁盘驱动模块重启时就会在挂载根文件系统这一环节卡住。顺序很重要先 modules_install再 install不要颠倒。3.3 make install 之后的引导更新逻辑make install 最后一步会调用 update-grub这个命令的本质是扫描 /boot 目录下所有内核映像生成 GRUB 菜单项。操作系统的 GRUB 工作流程其实是多级引导链条BIOS/UEFI 固件启动 → 读取磁盘开头引导代码 → 加载 GRUB 核心 → GRUB 读取 /boot/grub/grub.cfg → 按菜单加载 vmlinuz 和 initrd.img → 内核初始化 → 挂载根文件系统 → 启动 init 进程所以只要任何一个环节对不上就进不了系统。比如 /boot 下有了新的 vmlinuz但 initrd.img 没生成或者 grub.cfg 里没能扫描到新内核重启之后 GRUB 菜单里可能压根看不到新内核选项。实验过程中我建议在重启前先手动查看一下 /boot 目录ls -lh /boot正常情况下应该能看到 vmlinuz-版本号、initrd.img-版本号、System.map-版本号、config-版本号 四个文件齐全。如果缺哪个不要急着重启先把缺的补上否则大概率要在 GRUB rescue 里折腾。4. 新内核启动验证与信息读取如何确认你编出来的内核真的在跑4.1 从 uname 到 /proc验证内核版本编译安装完成重启虚拟机在 GRUB 菜单里选择新内核。如果一切顺利系统会正常登录。但“看起来正常”不算数要用几条命令验证当前跑的到底是不是你刚编译出来的内核。uname -r uname -v cat /proc/versionuname -r 显示内核版本号如果显示的是你配置时加的 Local version 后缀比如 6.x.x-exp说明新内核已经生效了。uname -v 会显示内核的编译时间如果你这次编译是在今天下午完成的这里显示的时间也对得上那就是没跑错。cat /proc/version 则给出更完整的信息包括编译器版本、构建主机信息。这里我特别想提醒一点如果 uname -r 显示的依然是系统自带的老版本号而你明明在 GRUB 菜单里选了新内核最常见的原因有两个一是没真正选到新内核默认启动项还是旧内核二是 make install 时 vmlinuz 没有被正确覆盖或命名。默认启动项的问题可以这样查grep menuentry /boot/grub/grub.cfg看看新内核的菜单项排在第几位然后确认 /etc/default/grub 里 GRUB_DEFAULT 指向的是不是它。如果希望新内核默认启动把 GRUB_DEFAULT 设为对应菜单项的编号比如 10 这种形式再执行 sudo update-grub。4.2 dmesg 日志里的启动痕迹验证新内核在跑除了 uname还有一条信息量更大的路子dmesg。dmesg 是内核环形缓冲区里面存着内核从启动到当前时刻打印的日志是理解系统启动过程的第一手资料。比如开机第一条日志类似dmesg | head -20你能看到 Linux version 那一行直接写明内核版本、编译者、编译器版本以及构建时间和构建主机。这和我前面说的 uname -v 是对应的。再往下翻dmesg 里还能看到内存检测信息Memory: ... availableCPU 型号与特性smpboot: Allowing 4 CPUsACPI 表解析结果各类设备驱动注册例如网卡、磁盘控制器、虚拟化相关驱动文件系统挂载信息实验报告里如果要求“截图证明内核编译成功并运行”uname -r 加 dmesg 头部信息就足够了。还可以结合 /proc/cmdline 看一下引导参数比如cat /proc/cmdline一般会看到 BOOT_IMAGE/boot/vmlinuz-版本号、ro、rootUUIDxxx 之类的内容。这行内容就是 GRUB 在启动内核时传给内核的命令行参数能帮你确认引导用的确实是新的 vmlinuz。4.3 模块目录与新内核的“指纹”一致性还有一个坑值得专门说一下就是模块目录和内核版本不一致的问题。Linux 在加载模块时会去 /lib/modules/$(uname -r)/ 这个特定目录下找 .ko 文件目录名必须和当前运行的内核版本完全一致。如果你编译安装完成之后ls /lib/modules/ 看到的目录名和 uname -r 对不上说明 modules_install 可能没成功或者你之前手动清理过。我用这条命令验证ls /lib/modules/ uname -r正常情况下会有至少两个目录一个是系统原装内核对应的版本一个是你新编译内核对应的版本。如果你发现新版本目录是空的或者目录根本不存在那就是 modules_install 出了问题需要重新执行。有了新内核版本目录之后还可以用 modinfo 检查某个具体模块有没有安装成功比如modinfo /lib/modules/$(uname -r)/kernel/fs/ext4/ext4.ko能正常输出模块描述、作者、依赖信息说明模块安装链路是通的。如果提示文件不存在说明你编译配置里那个文件系统被设成了内置y本来就编进内核了不在模块目录里这也是正常现象。5. 编译过程中最常见的四类问题排查链路5.1 磁盘空间告急在编译前就该做的检查我这次第一个踩的坑就是磁盘空间。编译到一半终端滚动刷出几百行类似 No space left on device 的错误然后整个编译进程退出人直接愣住。当时我的完整排查链路是这样的供后面的人参考。第一步先看文件系统使用率df -h结果发现根分区 / 已经到 100%/boot 也只剩几十 MB基本什么都写不进去了。第二步找出空间被什么吃了du -sh /usr/src/linux-6.x du -sh /lib/modules du -sh /boot一查发现源码树和模块目录占了大头其中源码树编译中间产物就有 4 个多 G。第三步根据具体情况处理。我的虚拟机磁盘才 20GB根分区确实不够最终解决方案是给虚拟机扩展虚拟磁盘到 40GB然后用 GParted 之类的工具把根分区扩大重新编译中间步骤颇多。事后复盘这个坑完全是可以在动手之前就避掉的。建议在解压源码之前先执行 df -h 看一下根分区剩余空间低于 15GB 就优先扩容。另外如果只是编译实验用不在源码树里反复折腾可以把这个选项关掉scripts/config --disable DEBUG_INFO关掉 debug info 之后整个编译产物体积能缩水一半以上实验场景完全够用。这也是我后来在报告里写“为了控制编译时间与磁盘占用关闭内核调试信息”这句话的由来。5.2 编译中断与报错先看配置依赖再谈工具链编译过程中报错是家常便饭但报错的原因绝大多数可以归为两类配置依赖冲突、工具链缺组件。我遇到过一次比较诡异的情况编译到某个驱动模块时报了 undefined reference 的错误看着像是代码问题实际上是因为某个内置符号被配置成了模块而模块又依赖了另一个没选的特性。这种问题的排查思路我总结成三步第一步先定位报错文件。在满屏的编译日志里往上翻找到包含 error: 的行定位到具体目录和源文件。第二步用带详细输出的方式重新编译make V1V1 会让 make 输出完整的编译命令行比如具体的 gcc 参数、头文件搜索路径、宏定义很多隐性问题能从这里看出来。第三步分析配置依赖。内核 Kconfig 体系里的选项之间有 strict 依赖关系你在 menuconfig 里把一个选项改成 m它依赖的另一个选项如果还是 n编译就可能挂掉。遇到这种最简单的办法是回到 menuconfig 里检查该选项的依赖提示把依赖项一并打开。还有一个更省力的兜底策略如果你改了不少配置越改越乱干脆重新复制当前系统的 config再 make olddefconfig放弃自定义改动以稳定为优先。实验的核心目标是“编出来并跑起来”不是“编出一个惊世骇俗的自定义内核”目标要分清。5.3 重启进不了新内核检查引导链条这个坑是内核编译实验里最刺激的。我第一次做实验时不巧新内核写进了 GRUB 菜单但重启后直接黑屏屏幕上只剩一个光标在闪系统完全起不来。当时的排查链路是这样的先在 GRUB 菜单里选择旧的 Linux 内核版本进入系统确保系统还能用然后开始分析新内核为什么起不来。GRUB 菜单里按 e 进入编辑模式在 linux 那一行末尾删掉 quiet splash加上一个空格再写上systemd.log_leveldebug或者干脆加一个 breaktop 之类的参数让 initramfs 阶段停下来看看卡在哪一步。实际跑起来之后发现是 initrd.img 的问题——启动时内核尝试挂载根文件系统但没有对应的块设备驱动盘符不存在自然挂不上根。根源就是 make install 生成 initrd 的时机不对。前面说过如果 modules_install 和 make install 顺序颠倒或者新内核模块目录缺失initrd 里就打包不进磁盘驱动模块。这个问题的根治方法sudo mkinitramfs -o /boot/initrd.img-版本号 版本号手动重新生成 initrd确认输出文件大小正常。如果 initrd 只有几十 KB几乎可以肯定里面是空的正常的内核 initrd 应该有几十 MB 的大小。处理完 initrd再执行 sudo update-grub重新生成引导菜单重启应该就能进新内核了。整个过程的核心就是引导是一个链条哪一环断了都会启动失败而 initrd 是最容易出问题的环节。5.4 新内核启动后没有网络驱动模块缺失的定位方法进得了系统但没网络这个问题出现的概率非常高。我身边有好几个同学都栽在这上面因为新配置复制了旧系统的 config 理论上不应该丢网卡驱动但如果你自己裁剪过内核配置或者用的是 make defconfig 起步网卡驱动极有可能没编进去。排查链路如下。首先确认网卡设备在系统里存不存在lspci | grep -i ethernet如果能看到网卡设备说明硬件在。然后看驱动加载情况lspci -k这一行会显示设备的 kernel driver in use 和 kernel modules。如果驱动显示为空白或者模块列表为空说明新内核里确实没有这个网卡的驱动。对于 VMware 虚拟机常见网卡驱动是 e1000、e1000e、vmxnet3。解决方法是回到源码目录menuconfig 里找到对应的驱动选项Device Drivers → Network device support → Ethernet driver support把对应驱动改成 y 或 m重新编译再走一遍模块安装和内核安装流程重启即可。如果不想重新编译整个内核还有一个临时方案从旧内核的模块目录里手动加载驱动modprobe e1000e前提是旧内核模块和新内核模块兼容。但实验场景下我更推荐直接重新编译一次反正也就二三十分钟顺便体验一下增量编译的速度——第一次全量编译可能要半小时第二次只编译改动过的部分几分钟就完成了。6. 实验之外内核编译能继续延伸的方向跑完一遍完整的内核编译和启动操作系统实验的基础目标就达成了。但如果不想让这个实验停留在“照着课件敲命令”的层面我建议在剩余时间里往这几个方向再走一步。第一个方向是内核模块编程。编译完内核之后你的系统已经有了完整的内核头文件和模块构建环境可以直接写一个最简单的 helloworld 内核模块用 Makefile 配合 Kbuild 编译然后 insmod/rmmod 加载卸载在 dmesg 里看到输出。这一步做出来“操作系统”在你眼里会从抽象概念变成一组可观察、可控制的内核机制。第二个方向是系统调用。实验指导书里如果后续有“添加一个系统调用”的实验这次内核编译就是最好的铺垫。改内核源码里 syscall 表加一个自己的调用号在用户态写个 C 程序触发它再通过 dmesg 验证调用是否进入内核整套链路能把操作系统从用户态到内核态的路径真正串起来。第三个方向是内核裁剪。如果对嵌入式感兴趣可以尝试用 make menuconfig 把大量设备驱动去掉只保留基本串口、RAM 盘、initramfs 支持编译一个极小的内核配合一个简单的 initramfs 启动到最小 shell。这个过程能让你直观感受到“配置选择如何改变内核体积与功能边界”。不管往哪个方向延伸这次编译实验积累下的经验和踩坑意识都会派上用场。最后再分享一个我自己的小习惯每次编译前把终端里敲过的命令同步记录到实验报告的草稿里每条命令旁边留一行写下“为什么这样写”编译完成后直接整理成文比事后回忆高效得多。