操作系统课程第一个实验就是 Linux 内核编译及运行,这个实验放在很多高校的第一周,看起来就是“下载源码、make menuconfig、make、重启”,实际操作起来却能把人折腾到怀疑人生。这篇文章把我自己做这个实验的全过程、踩过的坑、排查思路都记录下来,希望能给准备做这个实验或者正在做的小伙伴一个完整的参考路径,也算是对实验一的一次复盘。
1. 实验目的与环境准备
1.1 实验目的:弄懂内核编译的全链路
很多同学一开始会疑惑:操作系统课为什么要从编译内核开始?直接用发行版不香吗?答案在于,这个实验真正要训练的不是“会敲命令”,而是理解操作系统的构建过程。
所谓内核编译,就是把一份 Linux 内核源码,通过工具链生成可执行的二进制内核镜像和大量可加载模块。这个过程涉及源码树的组织方式、Kconfig 配置体系、Makefile 构建逻辑、模块机制,以及 grub 引导流程。可以说,一个实验把操作系统课程里“进程管理、内存管理、文件系统、设备驱动”这些大概念背后的工程实现串起来了。
实验中你会看到,内核不是一整块巨大的程序,而是一个由上千个目录、几万个文件组成的工程集合。它的构建系统会按照 .config 文件里的配置项决定哪些功能编译进内核(<*>)、哪些编译成模块(<M>)、哪些干脆不编译(< >)。这和你平时写一个小项目直接 make all 完全是两码事,也正因如此,它才能成为操作系统课第一个实验的理想起点。
1.2 实验环境搭建
我是用虚拟机完成实验的,方案如下:
- 宿主机:普通 Windows 笔记本,开启硬件虚拟化
- 虚拟机:VMware Workstation,分配 4 核 CPU、8 GB 内存
- 虚拟磁盘:80 GB,动态分配
- 系统镜像:Ubuntu 22.04 LTS
这里特别提醒一下磁盘问题。内核源码解压后大约 1.2 GB,但编译过程中会产生大量的 .o 目标文件和临时文件,整个 /usr/src/linux 目录编译完能膨胀到 13~15 GB。如果虚拟机初始只给了 20 GB 系统盘,那基本必挂。建议至少给 60 GB,我甚至见过有人因为磁盘不够把实验拖了一周才做完。
进入系统后,先把基础工具链装上:
sudo apt update sudo apt install build-essential libncurses-dev libssl-dev flex bison bc dwarves这些包各司其职:
- build-essential:包含 gcc、make、ld 等编译必备工具
- libncurses-dev:提供 menuconfig 图形界面所依赖的 ncurses 库
- flex / bison:内核源码构建时需要生成的词法、语法分析工具
- libssl-dev:编译内核模块时需要的 OpenSSL 头文件
- bc:内核配置阶段计算依赖时用到的计算器工具
- dwarves:生成 BTF 信息,部分新内核默认启用这个依赖
还有一个比较容易忽略的点:编译工具版本要和内核版本匹配。Ubuntu 22.04 自带的 gcc 是 11.x,如果下载特别老的 Linux 内核(比如 4.x 时代的老版本),可能会遇到头文件不兼容、API 变更导致编译失败。所以我建议直接选 5.15.y 或者 6.1.y 这些 LTS 版本,社区文档多,遇到的问题网上也基本都有答案。
提示:做任何操作前,先用
uname -r看一下当前系统内核版本,然后选择一个比它新的内核来编译,避免出现版本倒退或者覆盖当前可用内核的风险。
2. 内核源码获取与内核配置
2.1 获取内核源码:三种方式怎么选
实验的第一步是拿到内核源码。我实践下来主要有三种方式,各自的使用场景不一样。
第一种,从 kernel.org 下载稳定版 tar.xz 源码包。这是最推荐的实验方式:
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.119.tar.xz tar -xf linux-6.1.119.tar.xz cd linux-6.1.119优点很直接:一条命令拿到完整源码,版本固定,不会因为网络拉取中断导致源码树不完整。缺点是下载要花一点时间,取决于网速。
第二种,用 git 克隆官方的 stable 仓库:
git clone --depth 1 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git加了--depth 1只拉取最新提交,能省不少流量。但要注意,浅克隆仓库里的版本号不像 tar 包那样固定在 6.1.119 这种 tag 上,实验时如果想对齐某个版本,还得手动 checkout 指定 tag,稍微麻烦一点。如果你后续想做内核开发、改代码提交,那 git 方式是必要的;单纯做实验,tar 包更省心。
第三种,用 apt 的源码管理机制从系统源里拉取,需要提前添加 deb-src 源。这种方式受发行版维护周期影响,拉到的可能不是最新版,而且配置起来比较繁琐,实验里我个人不推荐。
我实验用的是 Linux 6.1.119,这是 6.1 LTS 系列的一个版本。选它没有太多玄学,就是看重长期维护这个特点,出了问题也好查资料。
2.2 配置内核:menuconfig 的正确打开方式
拿到源码后,很多人第一反应就是直接 make。错,大错特错。编译前必须先配置内核,否则 make 会提示找不到 .config 文件,或者使用一个默认的不适合你环境的配置。
内核提供几档配置工具,我分别说下体验:
make config:命令行逐项问答式配置,几百个选项,全部答完大概需要一两个小时,非常折磨人。make menuconfig:基于 ncurses 的图形化菜单,上下键选择、空格切换、回车进入子菜单,也是实验中最推荐的一种。make defconfig:直接采用内核默认配置,省事但不对你当前系统做任何定制,适合快速验证流程,不适合做深入实验。make olddefconfig:基于现有 .config 生成新配置,通常在升级内核配置时用。
实验环境里,建议直接 menuconfig。但在此之前,先执行一次彻底清理:
make mrpropermake mrproper比make clean更彻底,它会清掉源码树中所有之前生成的配置、编译产物、依赖文件,保证你从一个干净的状态开始。我第一次没执行这一步,结果后面反复出现奇怪的头文件依赖错误,后来养成习惯,每次重新编译前先 mrproper。
执行:
make menuconfig进去之后,你会看到 General setup、Processor type and features、Power management、Device Drivers、File systems 等分类。重点理解这三个符号的含义:
<*>:编入内核,即直接整合到 vmlinuz 镜像里,开机常驻内存<M>:编译成模块,以 .ko 文件形式存在,需要时通过 modprobe/insmod 加载< >:不编译,完全不包含该功能
对于实验来说,你可以在 Device Drivers 下面找到块设备、网络设备、显卡驱动等选项,观察哪些是内建、哪些是模块。如果想明确看到效果,可以特意把一个驱动从<*>改成<M>,后面用 modprobe 手动加载,会非常有成就感。
配置完成后退出,会询问是否保存新配置。选 Yes,生成的配置文件叫 .config。可以查看验证:
grep CONFIG_BLK_DEV_SD .config这条命令能确认常见 SCSI 磁盘驱动是否被编译。
2.3 配置阶段容易掉进的坑
我在实验时遇到三个问题,提前帮大家避开。
第一个坑:menuconfig 打不开,提示缺少 ncurses。这纯粹是依赖安装不完整导致的,执行sudo apt install libncurses-dev即可解决。
第二个坑:.config 文件被覆盖或丢失。这是新手的重灾区。如果你先用了 defconfig,再打开 menuconfig 改了配置,但最后退出时没保存,那么你之前所有修改全部作废。我建议在改动前先留一份备份:
cp .config .config.bak第三个坑:新内核的 Kconfig 体系对配置依赖非常敏感,某些选项如果依赖其他被关闭的选项,会直接置灰无法勾选。这时候不要硬选,更不要手动往 .config 里强行加没被支持的 CONFIG_XX,否则编译时大概率出现找不到头文件或 undefined reference 的报错。
保险的做法是把当前系统内核的配置作为起点:
cp /boot/config-$(uname -r) .config然后基于这份配置增量修改,这样能最大限度保留对现有硬件的支持,避免把系统“配成一个启动不了的形态”。实验做完后我发现,很多同学出问题都是因为从零开始配内核,结果漏掉了自己虚拟机的磁盘控制器驱动、网卡驱动,重启后直接失联。
3. 内核编译、安装与引导
3.1 编译过程与关键产物解读
配置完成后,进入最耗时的阶段:编译。命令很简单:
make -j4这里的-j4表示 4 个并发编译任务。如果直接写-j$(nproc),系统自动用全部 CPU 线程数。但虚拟机上如果本身内存不大,并发开太高容易把内存吃满,出现 OOM(Out of Memory)。我的虚拟机分到 4 核,实测-j4比较稳,-j8虽然快一点,但内存占用会显著增加,还出现过一次编译崩溃。所以如果你不是专门为了拼速度,老老实实用 4 并发就好。
编译过程中,你会看到大量 C 源文件被编译成 .o,再被链接成各子系统的目标文件。重点关注这几个产物:
vmlinux:未压缩的内核镜像,带符号表,体积很大,通常位于源码根目录arch/x86/boot/bzImage:压缩后的内核镜像,是引导加载器真正加载的文件System.map:内核符号表,调试内核和模块时使用.ko模块文件:分散在各级目录中,后续会被安装到 /lib/modules
当终端里出现这句话时,说明内核本体编译成功:
Kernel: arch/x86/boot/bzImage is ready我第一次看到这行提示的时候还挺激动的,以为实验马上结束了,结果后面安装和引导还有一堆事。
关于编译时长,我的实测数据是:4 核虚拟机、8 GB 内存、默认配置基础上增加少量选项,编译完大约花了 35 分钟。如果配置里勾选了大量驱动、文件系统支持,或者把并发调低,耗时可能到 1 小时以上。建议实验安排时留足时间,别等到实验课快下课才开始编译。
3.2 模块安装与内核安装
编译完成后,先把模块安装到系统目录:
sudo make modules_install这一步会把所有 .ko 文件拷贝到/lib/modules/$(uname -r)/下。注意这里的$(uname -r)是你在内核源码顶层 Makefile 里设置的版本号,不是当前运行系统的版本号。在执行前,建议在源码顶层目录看一下版本变量:
make kernelrelease这个命令会打印出即将安装的内核版本,确保和你预期一致。
接下来安装内核镜像本身:
sudo make install这一步会执行几个动作:把 bzImage 拷贝为 /boot/vmlinuz-版本号,生成 /boot/initrd.img-版本号,拷贝 System.map 到 /boot,并自动调用 update-grub 更新引导菜单。如果你这个时候/boot分区空间不足,会直接报错中断,具体解决方式后面单独讲。
安装完成后检查两个目录,确认文件真的到位了:
ls -lh /boot/vmlinuz-* ls -lh /boot/initrd.img-*正常情况下,你应该能看到新版本号对应的 vmlinuz 和 initrd.img。
3.3 更新引导配置与重启验证
虽然 make install 大概率已经自动执行了 update-grub,但为保险起见,我习惯手动再执行一次:
sudo update-grub这个命令会扫描 /boot 目录,把所有可用内核写入 grub 菜单。执行后重启:
sudo reboot重启时,grub 菜单会列出多个内核版本,选择带新版本号的项进入。如果没看到选择菜单,按住 Shift 或按 Esc 进入 grub 菜单,具体按键取决于你的 grub 版本。
进入系统后,第一件事验证:
uname -r显示的结果如果是你编译的新内核版本,说明你已经成功让系统运行在一个“自己编译出来的内核”上了。这一步是整个实验最核心的验证点。
还可以更进一步:
dmesg | grep "Linux version"启动日志里会显示内核编译时间、使用的 gcc 版本、源码版本号,这些信息能辅助确认你启动的确实是刚刚编译的内核,而不仅仅是看起来版本号相同。
3.4 实测数据参考
我把个人实验的关键数据整理成一个表,方便后面同学对照:
| 项目 | 参考值 |
|---|---|
| 虚拟机配置 | 4 vCPU / 8 GB 内存 / 80 GB 动态磁盘 |
| 内核版本 | Linux 6.1.119 LTS |
| 配置方式 | 复制系统默认配置 + menuconfig 增量修改 |
| 编译命令 | make -j4 |
| 源码解压后体积 | 约 1.2 GB |
| 编译完成后源码目录体积 | 约 13~15 GB |
| 完整编译耗时 | 约 35 分钟 |
| 生成的 bzImage 大小 | 约 12~15 MB |
| /boot 新增占用 | 约 60 MB |
这些数值和具体配置选择、硬件性能有很大关系,只能作为参考方向,不必过分纠结具体数字。但磁盘空间和编译时间这两项,确实是最容易影响实验进度的变量。
4. 实验过程中遇到的坑与排查记录
这部分是整份实验报告里我觉得价值最高的地方。内核编译的报错五花八门,很多问题不亲手踩一遍,光看文档真的想象不到。
4.1 编译刚开始就报错:bison/flex 缺失
我自己第一次编译时,没有装 flex 和 bison。执行 make 后不久,终端直接报错:
/usr/bin/ld: scripts/mod/devicetable-offsets.s: No such file or directory yacc: command not found表面看是缺 yacc,实际是 bison 没装。补齐依赖之后,还要注意一个细节:如果编译中途失败,不要直接再跑一遍 make,最好先make clean然后重新编译,因为中途产生的某些生成文件可能不完整,直接复跑会引发奇奇怪怪的链接错误。
4.2 /boot 分区空间不足
我用的虚拟机当时 /boot 分区只有 1 GB,装过两次内核之后,空间消耗殆尽。make install到一半直接报:
No space left on device df -h /boot排查思路主要有三条:
- 清理旧内核文件,比如 /boot 下之前版本的 vmlinuz、initrd.img、System.map
- 删除 /boot 下不再使用的 abi-、config-文件
- 扩容 /boot 分区,但在虚拟机里操作比较麻烦,不建议实验中途做
我最终选择了删除旧的 vmlinuz-5.15 和对应 initrd,然后重新执行:
sudo make install sudo update-grub空间腾出来后安装顺利完成。这里给个建议:平时养成保持两个可用内核的习惯,别为了省空间把旧内核全删了,新内核一旦启动失败,你连退回的机会都没有。
4.3 启动卡在 initrd,进 BusyBox
这是整个实验里最折磨人的一个问题。编译和安装都很顺利,重启后却卡在 loading initial ramdisk,或者直接掉进 initramfs 的 BusyBox 界面,命令行前缀变成了(initramfs)。
这个问题的本质是:内核无法挂载根文件系统。大多数情况是因为根文件系统对应的驱动被配置成了模块(<M>),而不是内建(<*>)。系统启动早期,内核还没有加载模块的能力,如果磁盘控制器驱动、文件系统驱动不在内核镜像里,自然就找不到根分区。
排查手段是先在 initramfs 环境里看一下:
(initramfs) ls /lib/modules/$(uname -r)如果发现 ext4 或 xfs 这类文件系统驱动确实缺失,回到 menuconfig,把CONFIG_EXT4_FS这类关键选项从<M>改成<*>:
make menuconfig # 进入 File systems -> <*> The Extended 4 (ext4) filesystem make -j4 sudo make modules_install sudo make install sudo update-grub重新编译并安装后重启,问题通常能解决。这个坑特别容易出现在“从零配置内核”的同学身上,所以我前面才反复强调,最好基于系统自带 config 修改,不要自己从头配。
4.4 编译报错速查表
我一共整理了八类常见的编译安装问题,做成速查表:
| 报错现象 | 可能原因 | 解决措施 |
|---|---|---|
fatal error: libelf.h: No such file | 缺少 libelf 开发库 | sudo apt install libelf-dev |
yacc: command not found | 缺少 bison | 安装 bison 后make clean重编 |
/bin/sh: 1: bison: not found | 缺少 bison | 安装 bison 并重新配置 |
No rule to make target 'debian/certs/...' | 源码目录被污染 | make mrproper后重新配置 |
Unable to find the ncurses libraries | 缺少 libncurses-dev | 安装 ncurses 开发库后重开 menuconfig |
insufficient memory | 并发过高或内存不足 | 减少-j并发数或增加虚拟机内存 |
No space left on device | /boot 分区或根分区满 | 清理旧内核文件或扩容 |
gcc: error: elf.h: No such file | 缺少 libelf 头文件 | 安装 libelf-dev 并重新编译 |
这张表我实验时是贴在桌面上的,每遇到一个问题,就对应排查一遍,效率高很多。
4.5 实用的恢复技巧
实验里的保命技巧就一条:grub 菜单里保留一个旧内核作为保底。我图省事删过旧内核,结果新内核启动失败,那一刻真的欲哭无泪,最后只能拿 live CD 进去修。如果你正在做实验,无论如何都别急着删旧内核。等新内核连续重启两三次都稳定了,确认模块、驱动、网络都正常,再考虑清理工作。
另外,如果修改配置后想快速返回上一个可用状态,可以在 grub 菜单里选 Advanced options for Ubuntu,里面会列出所有已安装的内核版本,选旧版本进入即可。
5. 实验心得与进阶方向建议
5.1 给后面做实验的同学几条掏心窝的建议
首先,时间规划上,这个实验至少留两到三个晚上。第一晚理解源码结构和配置体系,第二晚集中编译安装,第三晚排查问题写报告。不要天真地以为“一节课就能搞定”,实际编译失败、重来、再失败是常态。
其次,实验报告不要只截图,要记录关键改动。我见过太多人实验做了,报告里却只写“用 menuconfig 配置”“make 编译”几句流水账。答辩时被问“你改了什么配置”“编译参数用的什么”“遇到过什么错”,直接哑口无言。我的做法是把 .config 里的关键修改项、make 命令的参数、每次报错的信息和解决过程都记录下来,这些才是实验报告真正的价值。
最后,不要害怕报错。内核编译的报错信息其实是很好的学习材料,它逼着你去查依赖关系、看源代码、理解构建系统。这一套流程走完,你再回去看操作系统教材里的“系统启动”“模块加载”这些章节,理解会完全不一样。
5.2 实验之后还能怎么玩
如果你对这个方向产生了兴趣,后续有几个很好的进阶点:
第一个,把某个驱动单独编译成模块,然后手动 insmod/modprobe 加载和卸载,观察 dmesg 里的驱动加载日志。这比单纯看教程理解“内核模块”更直观。
第二个,修改内核源码里的调度算法参数,比如 CFS 调度器的某些默认值,重新编译后跑一个 CPU 密集型程序,对比修改前后的运行表现。这个实验能让你对“操作系统调度”从抽象概念变成可触摸的工程实践。
第三个,尝试用 menuconfig 把内核裁剪到最小,做成一个精简内核。嵌入式开发里经常要这么干,把不需要的驱动、虚拟化支持、蓝牙、Wi-Fi 全裁掉,最终得到几 MB 大小的镜像。这个方向特别适合对嵌入式感兴趣的同学。
第四个,如果你愿意花点时间,可以顺着源码里的arch/x86/kernel、kernel/sched这些目录去读一部分源码,配合 System.map 和调试工具,看看系统调用到底是怎么从用户态走进内核态的。这一步走通之后,你对“操作系统”四个字的理解,会远超课本上的定义。
就我个人而言,这个实验最大的收获是第一次意识到:操作系统不是安装起来就完事的黑盒,它是有血有肉、可以被亲手修改和重建的工程系统。编译内核这件事,看起来只是课程的第一关,但它建立的“源码—配置—构建—部署—验证”的完整心智模型,后面读源码、写驱动、做系统优化都用得上。
如果你正在为这个实验头疼,别慌,按我记录的这些步骤一步步来,重点放在配置和排障上,最后一定能看到自己编译的内核成功启动。那个瞬间,你会觉得之前所有折腾都值了。