4. 内核编译实战:make命令详解、编译选项与多线程加速

内核配置完了,接下来就是编译。这一步看着简单,但坑不少。我见过不少新手在 make 这一步卡住,要么编出来的内核起不来,要么编译时间长得让人崩溃。今天咱们就把 make 的用法、编译目标、多线程加速这些事彻底讲透。

4.1 make 命令的基本用法

Linux 内核的编译系统基于 GNU Make。你想想看,几千个源文件,如果没有一套完善的构建系统,根本没法管理。内核的 Makefile 体系非常复杂,但咱们只需要掌握几个核心用法。

最基本的编译命令就一行:

make

不加任何参数时,make 会读取顶层 Makefile,然后根据 .config 中的配置,编译出默认目标。这个默认目标通常是 zImage 或 bzImage,取决于你的架构。

我个人习惯在第一次编译时,先执行 make help 看看当前架构支持哪些目标:

make help

输出会列出所有可用的编译目标,比如 zImage、uImage、modules、dtbs 等等。这个命令我几乎每次编译新内核都会跑一遍,确认一下目标平台支持什么。

小技巧: 如果你不确定当前内核版本,可以用 make kernelversion 查看。这个命令不会触发编译,只是读取顶层 Makefile 中的版本号。

4.2 编译目标详解:zImage、uImage、modules

不同的编译目标对应不同的输出产物。咱们一个一个说。

编译目标 输出文件 适用场景
zImage arch/arm/boot/zImage ARM 平台压缩内核镜像,体积较小
bzImage arch/x86/boot/bzImage x86 平台大内核镜像,实际也是压缩的
uImage arch/arm/boot/uImage U-Boot 引导的 ARM 内核镜像
Image arch/arm/boot/Image 未压缩的内核镜像,调试用
modules 分散在各目录的 .ko 文件 编译内核模块
dtbs arch/arm/boot/dts/*.dtb 编译设备树二进制文件

4.2.1 zImage 与 bzImage

zImage 和 bzImage 都是压缩后的内核镜像。名字里的 z 和 bz 其实跟压缩算法有关——zImage 用 gzip 压缩,bzImage 用 bzip2 压缩。不过现在内核默认都用 bzImage 了,因为它支持更大的内核。

在 x86 平台上,编译命令是:

make bzImage

在 ARM 平台上,通常用:

make zImage

4.2.2 uImage

uImage 是 U-Boot 专用的内核镜像格式。它在 zImage 前面加了一个 64 字节的 U-Boot 头部,包含镜像类型、加载地址、入口地址等信息。

编译 uImage 需要先安装 mkimage 工具:

sudo apt-get install u-boot-tools
make uImage

我曾经在一个项目里忘了装 mkimage,结果 make uImage 报错说找不到命令。折腾了半天才发现是工具链的问题。嗯,这种基础依赖最容易忽略。

4.2.3 modules 与 modules_install

内核模块的编译分两步:

make modules
make modules_install

make modules 编译所有配置为 M 的模块,生成 .ko 文件。make modules_install 把这些 .ko 文件安装到 /lib/modules/$(uname -r)/ 目录下。

注意: 如果你在交叉编译,make modules_install 会把模块安装到当前主机的 /lib/modules 下,而不是目标板的根文件系统。交叉编译时,建议手动指定安装路径:
make modules_install INSTALL_MOD_PATH=/path/to/rootfs

4.3 多线程编译与编译加速

内核编译是个耗时活。我第一次编译完整的内核,单线程跑了将近一个小时。后来学会了多线程编译,时间直接缩短到十几分钟。

4.3.1 -j 参数

-j 参数指定并行编译的线程数:

make -j4

这个 4 表示同时运行 4 个编译任务。一般建议设置为 CPU 核心数的 1.5 到 2 倍。比如 4 核 CPU,可以用 -j6 或 -j8。

我常用的命令是:

make -j$(nproc)

nproc 命令返回当前 CPU 的核心数。这样写的好处是,换到不同机器上编译时,不用手动改数字。

4.3.2 编译加速技巧

除了多线程,还有几个加速技巧值得掌握:

  • 只编译修改的部分: 内核 Makefile 会检测文件的时间戳,只重新编译有变化的文件。所以第一次编译慢,后续增量编译会快很多。
  • 使用 ccache: ccache 是一个编译器缓存工具。它会把编译结果缓存起来,下次编译同样的代码时直接命中缓存。
  • 跳过无关目标: 如果你只需要内核镜像,不需要模块,可以只编译 zImage 或 bzImage,跳过 modules。

ccache 的用法很简单:

sudo apt-get install ccache
export CC="ccache gcc"
make -j$(nproc)

或者直接修改内核 Makefile,把 CC 变量设成 ccache gcc。我个人习惯在 .bashrc 里加一行 export CC="ccache gcc",这样所有编译任务都能受益。

实战经验: 我在一个嵌入式项目里,每次修改驱动后都要重新编译内核。用了 ccache 之后,增量编译从 5 分钟降到了 30 秒。尤其是只改了一个 .c 文件时,ccache 几乎秒出结果。

4.4 完整编译流程示例

下面是一个完整的编译流程,从配置到安装:

# 1. 配置内核
make menuconfig

# 2. 编译内核镜像
make -j$(nproc) zImage

# 3. 编译设备树
make -j$(nproc) dtbs

# 4. 编译模块
make -j$(nproc) modules

# 5. 安装模块到目标根文件系统
make modules_install INSTALL_MOD_PATH=/path/to/rootfs

# 6. 安装内核镜像(可选)
cp arch/arm/boot/zImage /path/to/tftp/
cp arch/arm/boot/dts/your-board.dtb /path/to/tftp/

这个流程我用了很多年,基本没出过问题。唯一要注意的是,make modules_install 的路径一定要写对,否则模块装错地方,启动时内核找不到驱动。

4.5 内核编译知识体系

下面这张图总结了内核编译的核心流程和关键目标:

内核编译知识体系图 内核编译核心流程与目标 make menuconfig make zImage/bzImage make modules make dtbs arch/*/boot/zImage *.ko 模块文件 *.dtb 设备树 make modules_install 加速技巧:make -j$(nproc) | ccache | 增量编译 交叉编译时使用 INSTALL_MOD_PATH 指定安装路径

4.6 常见问题与避坑指南

编译过程中我遇到过不少问题,挑几个典型的说说:

  • 编译报错 "No rule to make target": 通常是 .config 配置有问题,或者某个依赖的库没装。先检查 .config 是否完整,再确认工具链和依赖库。
  • 编译到一半卡住: 可能是内存不足。多线程编译时,每个线程都会占用内存。如果内存不够,可以降低 -j 的数值。
  • uImage 编译失败: 确认是否安装了 u-boot-tools。我曾经在 Ubuntu 18.04 上遇到过 mkimage 版本不兼容的问题,升级工具包后解决。
  • 模块安装后不生效: 检查 INSTALL_MOD_PATH 是否正确,以及模块的 .ko 文件是否与内核版本匹配。
重要提醒: 编译内核时,尽量使用与目标平台匹配的交叉编译工具链。用宿主机的 gcc 编译 ARM 内核,出来的镜像肯定跑不起来。交叉编译时,需要设置 CROSS_COMPILE 变量:
make CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) zImage