摘要:本文面向嵌入式开发者,手把手还原从零构建 Linux 内核的完整流程。内容涵盖官方源码获取与版本选择、交叉编译环境搭建、内核目录结构解析、五大核心子系统架构、配置裁剪、编译与镜像生成、KBuild 机制分析、产物验证与清理,以及常见编译报错的排查思路。最后分享 ccache 加速、局部编译、精简内核体积等进阶优化技巧,帮助读者顺利生成适配自己硬件平台的定制内核。
关键词:Linux 内核;交叉编译;嵌入式开发;KBuild;menuconfig;设备树
目录
- ① 获取官方内核源码与版本选择指南
- ② 搭建交叉编译环境与工具链配置
- ③ 解析内核源代码目录布局与核心文件
- ④ 深入理解五大核心子系统功能架构
- ⑤ 配置内核选项与生成自定义配置文件
- ⑥ 执行内核编译流程与镜像生成步骤
- ⑦ 分析 KBuild 机制与顶层 Makefile 逻辑
- ⑧ 验证编译产物与清理目标文件方法
- ⑨ 解决常见编译报错与环境依赖问题
- ⑩ 优化编译策略与进阶定制技巧
很多嵌入式开发者在拿到新硬件平台时,最头疼的往往不是驱动怎么写,而是如何让操作系统真正跑起来。尤其是面对千差万别的 ARM、RISC-V 架构板卡,通用的发行版内核常常因为缺少特定的板级支持或驱动模块而无法启动。这时候,从源码开始构建一个量身定制的内核就成了必经之路。但这过程听起来就让人望而生畏:庞大的代码库、复杂的交叉编译环境、数不清的配置选项,随便一个环节出错就能让编译流程戛然而止。
其实,编译内核并没有想象中那么神秘,它更像是一次对操作系统内部结构的深度巡游。只要你理清了源码获取、环境搭建、配置裁剪到最终编译这条主线,很多看似复杂的问题都能迎刃而解。对于需要追求极致性能、精简系统体积,或者仅仅是想深入理解 Linux 运作机制的工程师来说,掌握这套流程是进阶的必修课。
本文将抛开枯燥的理论堆砌,直接还原一个真实的内核构建现场。我们会从官方源码的获取讲起,一步步拆解如何搭建高效的交叉编译环境,如何像搭积木一样通过配置选项组装出符合需求的核心系统,最后解决那些让人头秃的编译报错。无论你是第一次尝试编译内核,还是想在现有流程中寻找优化空间,希望接下来的实战经验能帮你少走弯路,顺利生成那个属于你的 vmlinuz。
① 获取官方内核源码与版本选择指南
一切工作的起点都是源码。对于生产环境或严肃的学习项目,强烈建议直接从 Linux 内核官方仓库获取代码,而不是随意下载第三方修改版。官方源码托管在 kernel.org 上,这里提供了完整的 Git 仓库镜像以及压缩后的 tarball 包。
1.1 如何选择合适的内核版本
选择版本是关键的第一步。内核版本分为稳定版(Stable)、长期支持版(LTS)和主线开发版(Mainline)。如果是为了嵌入式产品落地,务必选择 LTS 版本,这类版本会获得长达数年的安全补丁和维护支持,稳定性最有保障。你可以在官网的 "Releases" 页面查看当前的 LTS 列表,通常带有 [LTS] 标记。若只是为了学习最新特性,可以选择最新的 Stable 版本。避免使用过于陈旧的版本,以免缺失对新硬件的支持;也要谨慎追逐最新的 rc(release candidate)版本,除非你需要测试特定新功能,否则不稳定性可能会干扰学习进程。
版本命名规则速查:
| 版本类型 | 命名示例 | 适用场景 |
|---|---|---|
| 主线版(Mainline) | v6.7 |
尝鲜新特性,适合开发调试 |
| 稳定版(Stable) | v6.6.10 |
日常使用,bug 修复及时 |
| 长期支持版(LTS) | v6.6、v6.1 |
嵌入式产品落地,维护周期长 |
小贴士:对于嵌入式开发,推荐优先选择 LTS 版本。目前常见的 LTS 版本有
v6.6、v6.1、v5.15等,它们都提供数年的安全维护支持。
1.2 通过 Git 获取源码
下载时,推荐使用 git clone 方式,这样便于后续切换分支或查看提交历史:
git clone --depth 1 --branch v6.6.10 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
使用 --depth 1 参数可以只拉取最近的一次提交,大幅减少下载时间和本地占用空间,这对于初次尝试非常友好。如果网络环境受限,也可以直接下载 .tar.xz 压缩包并解压,效果是一样的。
国内加速镜像:如果 kernel.org 下载速度不理想,可以尝试使用国内镜像源,例如清华大学的镜像站:
git clone --depth 1 --branch v6.6.10 https://mirrors.tuna.tsinghua.edu.cn/git/linux-stable.git
1.3 通过压缩包获取源码
如果你不想使用 Git,也可以直接从官网下载 .tar.xz 压缩包:
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.10.tar.xz
tar -xf linux-6.6.10.tar.xz
cd linux-6.6.10
这种方式适合一次性使用,但后续想升级版本或查看提交历史就不太方便了。
1.4 验证源码完整性
无论通过哪种方式获取源码,都建议校验文件的完整性,防止下载过程中出现损坏。官方提供了对应的校验文件:
# 下载校验文件
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.10.tar.sign
# 校验 SHA256 哈希
sha256sum linux-6.6.10.tar.xz
将输出的哈希值与官网公布的 SHA256 值进行比对,一致则说明文件完整。
1.5 获取源码后的初步检查
拿到源码后,先做一个简单的检查,确认目录结构完整:
ls -la
cat Makefile | head -5
Makefile 开头的版本信息会显示当前内核版本号,例如 VERSION = 6、PATCHLEVEL = 6、SUBLEVEL = 10,确认与你下载的版本一致即可。
② 搭建交叉编译环境与工具链配置
在 x86 电脑上编译出能在 ARM 或 RISC-V 开发板上运行的内核,需要用到“交叉编译”技术。这意味着我们需要一套特殊的编译器工具链(Toolchain),它运行在宿主机上,但生成的机器码属于目标架构。
2.1 什么是交叉编译
简单来说,交叉编译就是在一种架构的电脑上,编译出能在另一种架构上运行的程序。以最常见的场景为例:你的开发机是 x86_64 架构,而目标开发板是 ARM64(如树莓派、RK3588 等)或 RISC-V,那么就需要一套针对目标架构的交叉工具链。这套工具链包含编译器(gcc)、汇编器(as)、链接器(ld)以及一系列辅助工具(binutils)。
2.2 通过包管理器安装工具链
现代 Linux 发行版通常可以通过包管理器直接安装预编译的工具链。例如在 Ubuntu 上,针对 ARM64 架构可以执行:
sudo apt-get update
sudo apt-get install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
常见架构对应的工具链包名:
| 目标架构 | Ubuntu 包名 | 编译器前缀 |
|---|---|---|
| ARM64(AArch64) | gcc-aarch64-linux-gnu |
aarch64-linux-gnu- |
| ARM(32 位) | gcc-arm-linux-gnueabihf |
arm-linux-gnueabihf- |
| RISC-V(64 位) | gcc-riscv64-linux-gnu |
riscv64-linux-gnu- |
| x86(32 位) | gcc-i686-linux-gnu |
i686-linux-gnu- |
小贴士:如果你的开发板是 ARM 32 位且带硬件浮点单元(FPU),请务必选择带
hf后缀的gnueabihf工具链,否则浮点运算性能会大打折扣。
安装完成后,需要确认编译器是否正常工作。输入 aarch64-linux-gnu-gcc --version 查看版本号。如果输出正常,说明基础环境已就绪。
2.3 使用厂商专用工具链
对于某些特定平台(如 STM32MP1、全志、瑞芯微等),厂商通常会提供经过验证的专用工具链。这类工具链往往针对特定芯片做了优化,能避免一些通用工具链的兼容性问题。获取方式一般是:
- 从厂商官网下载预编译的 SDK 工具链;
- 解压到本地目录(如
/opt/toolchains/); - 将工具链的
bin目录加入PATH环境变量。
2.4 使用 crosstool-ng 自行构建
如果通用工具链和厂商工具链都无法满足需求,还可以使用 crosstool-ng 从源码自行构建一套定制工具链。虽然过程稍显复杂,但灵活性最高,可以精确控制 GCC、Binutils、glibc 的版本组合:
# 安装 crosstool-ng
sudo apt-get install crosstool-ng
# 初始化配置
ct-ng aarch64-unknown-linux-gnu
# 开始构建(耗时较长,建议用多核加速)
ct-ng build -j$(nproc)
构建完成后,工具链会生成在 ~/x-tools/ 目录下,同样需要将其 bin 目录加入 PATH。
2.5 设置环境变量
无论使用哪种工具链,核心都在于正确设置环境变量。我们需要告诉内核构建系统(KBuild)使用哪个编译器。通常的做法是在终端导出 ARCH 和 CROSS_COMPILE 变量:
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
注意 CROSS_COMPILE 的值末尾必须带有一个连字符 -,因为构建系统会自动在其后拼接具体的工具名称(如 gcc、ld 等)。将这些配置写入 ~/.bashrc 或项目的 Makefile 中,可以避免每次手动输入的麻烦。
推荐做法:在项目根目录创建一个 env.sh 脚本,集中管理环境变量:
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
export PATH=/opt/toolchains/aarch64/bin:$PATH
每次开始编译前执行 source env.sh 即可,既清晰又不易出错。
2.6 验证工具链是否可用
环境配置完成后,建议先做一个快速验证,确保工具链能正常工作:
# 查看编译器版本
aarch64-linux-gnu-gcc --version
# 编写一个最简单的 C 程序并交叉编译
echo 'int main(){return 0;}' > test.c
aarch64-linux-gnu-gcc -o test test.c
# 查看生成文件的架构信息
file test
如果 file 命令输出显示 ELF 64-bit LSB executable, ARM aarch64,说明交叉编译环境已经就绪,可以放心进入下一步。
③ 解析内核源代码目录布局与核心文件
解压或克隆后的内核源码目录结构庞大,初看容易迷路。其实只要抓住几个关键目录,就能把握全局。下面我们先看一张整体结构图,再逐一拆解每个目录的职责。
3.1 顶层目录总览
进入源码根目录后,你会看到几十个文件夹。别被吓到,真正需要重点关注的其实只有下面这几个。下面这张图按「构建系统、核心代码、硬件相关、驱动与文件系统、辅助工具」五个维度做了归类,方便你建立整体印象:
linux-6.6.10/
├── arch/ # 架构相关代码(x86、arm、arm64、riscv 等)
├── block/ # 块设备层(SSD、硬盘的 I/O 调度)
├── crypto/ # 加密算法实现
├── drivers/ # 设备驱动(占源码一半以上)
├── fs/ # 文件系统(ext4、vfat、nfs 等)
├── include/ # 内核头文件
├── init/ # 内核初始化代码
├── ipc/ # 进程间通信(信号量、消息队列等)
├── kernel/ # 内核核心(调度、信号、时间管理)
├── lib/ # 内核通用库函数
├── mm/ # 内存管理
├── net/ # 网络协议栈
├── scripts/ # 构建辅助脚本
├── security/ # 安全模块(SELinux 等)
├── sound/ # 音频驱动
├── tools/ # 用户态辅助工具
├── usr/ # initramfs 相关
├── virt/ # 虚拟化支持(KVM 等)
├── Makefile # 顶层构建入口
├── Kconfig # 顶层配置菜单定义
└── .config # 编译配置(make menuconfig 生成)
按功能维度快速归类,帮你把这张图记进脑子里:
| 维度 | 目录 | 一句话职责 |
|---|---|---|
| 构建系统 | Makefile、Kconfig、scripts/ |
编译入口、配置菜单、构建脚本 |
| 核心代码 | kernel/、init/、mm/、ipc/ |
调度、启动、内存、进程间通信 |
| 硬件相关 | arch/、drivers/、sound/ |
架构代码、设备驱动、音频驱动 |
| 文件与网络 | fs/、net/、block/ |
文件系统、网络协议栈、块设备层 |
| 辅助与安全 | include/、lib/、security/、virt/、tools/、usr/ |
头文件、通用库、安全、虚拟化、工具、initramfs |
几个容易混淆的目录,提前帮你区分:
arch/与drivers/:arch/放的是「CPU 架构相关」的底层代码(如 ARM 的启动、页表、中断),drivers/放的是「具体外设」的驱动(如网卡、USB、GPU)。前者跟芯片型号强相关,后者跟外设型号强相关。kernel/与init/:init/只管「开机启动那一段」,跑完就基本不再参与;kernel/则是「系统运行期」的核心逻辑,进程调度、信号、时钟都在这里。fs/与block/:fs/是「文件系统」层(ext4、vfat 等),block/是「块设备」层(SSD、硬盘的 I/O 调度)。文件系统要读写数据,最终都要落到块设备层。
小贴士:初次接触时,建议先记住
arch/、drivers/、kernel/、mm/、fs/这五个目录,它们覆盖了 90% 以上的日常开发场景。其余目录等真正用到时再回头查,不必一开始就全部背下来。
3.2 核心目录逐一解析
根目录 Makefile 与 Kconfig
根目录下的 Makefile 是整个编译系统的入口,它定义了顶层的构建规则。当你执行 make 时,它负责读取 .config 文件、调用 KBuild 系统并协调各个子目录的编译。而 Kconfig 文件则定义了 menuconfig 中显示的菜单结构,两者配合构成了内核配置与编译的基础。
关键文件:
Makefile:顶层构建入口,定义了all、menuconfig、clean等目标,并负责递归调用各子目录的Makefile;Kconfig:顶层配置菜单定义,通过source指令把各子目录的Kconfig汇总成完整的配置树;.config:编译配置结果,由make menuconfig等命令生成,是编译时最重要的输入文件之一。
小贴士:当你修改了某个子系统的配置选项后,根目录的
Kconfig会自动感知并更新菜单。如果menuconfig里找不到某个选项,多半是它所在的Kconfig没有被正确source进来。
init/ —— 内核的起点
init/ 目录包含了系统启动时的初始化代码,是内核生命周期的起点。其中最重要的文件是 main.c,它包含了 start_kernel() 函数——这是内核启动后执行的第一个 C 函数。从 CPU 加电、引导加载程序(Bootloader)跳转到内核入口,到最终挂载根文件系统、启动第一个用户进程(init),整个流程都由这里的代码驱动。
关键文件:
main.c:包含start_kernel(),负责初始化内存、调度器、中断、VFS 等核心子系统;init_task.c:创建第一个内核线程init_task,它是所有进程的祖先;version.c:生成内核版本字符串(Linux version 6.6.10 ...),启动日志第一行就是它打印的;Kconfig:定义CONFIG_INITRAMFS_SOURCE等启动相关选项。
小贴士:如果你想在启动早期打印调试信息,可以在
start_kernel()中临时加printk,但要注意此时很多子系统还没初始化,能用的接口非常有限。
kernel/ —— 核心中的核心
kernel/ 目录存放着进程调度、信号处理、时间管理等最基础的逻辑。比如 sched/ 子目录实现了进程调度器,time/ 负责时钟与定时器管理,signal.c 处理进程信号。如果你对操作系统原理感兴趣,这里是最值得深入阅读的地方。
关键子目录与文件:
sched/:进程调度器,CFS(完全公平调度器)就在这里实现;time/:时钟与定时器管理,tick中断处理、hrtimer高精度定时器都在这里;signal.c:进程信号处理,kill()、signal()系统调用的内核实现;fork.c:进程创建,fork()、vfork()、clone()的实现;exit.c:进程退出与资源回收;module.c:内核模块的加载与卸载(insmod、rmmod的内核侧实现);power/:电源管理,suspend、resume流程。
小贴士:
kernel/是理解「操作系统原理」的最佳入口。建议从sched/core.c的schedule()函数读起,它是进程切换的核心。
arch/ —— 硬件相关的灵魂
硬件相关的代码主要分布在 arch/ 目录。这里按架构名称分成了 x86、arm、arm64、riscv 等子目录。每个架构目录下通常还有更细的分类:
arch/arm64/
├── boot/ # 启动相关(含 dts 设备树源文件)
├── configs/ # 该架构的默认配置文件
├── include/ # 架构相关的头文件
├── kernel/ # 架构相关的核心代码
├── lib/ # 架构相关的库函数
├── mm/ # 架构相关的内存管理
└── Makefile # 架构构建规则
关键子目录:
boot/:启动相关代码,dts/下存放设备树源文件(.dts),dts/按厂商分目录(如rockchip/、broadcom/);configs/:该架构的默认配置文件,如arm64/configs/defconfig、rockchip_defconfig;kernel/:架构相关的核心代码,如setup.c(CPU 初始化)、irq.c(中断控制器);mm/:架构相关的内存管理,如页表操作、TLB 刷新;include/:架构相关的头文件,如asm/目录下的汇编接口。
如果你在为特定板卡移植内核,大部分板级初始化代码(Board Support Package, BSP)都藏在这里的 boot/dts/(设备树)和 mach-xxx 文件夹中。
小贴士:交叉编译时,
ARCH变量决定了使用arch/下的哪个子目录。例如export ARCH=arm64后,构建系统只会编译arch/arm64/下的代码。
drivers/ —— 驱动的海洋
驱动程序占据了源码的半壁江山,全部位于 drivers/ 目录。这里按照设备类型细分,如 net/ 是网卡驱动,gpu/ 是显卡驱动,i2c/、spi/ 是总线驱动。当你的硬件无法工作时,大概率需要在这里寻找或编写对应的驱动模块。
常见子目录:
| 子目录 | 对应设备 | 典型文件 |
|---|---|---|
drivers/net/ |
网卡、无线 | ethernet/、wireless/ |
drivers/gpu/ |
显卡、GPU | drm/(Direct Rendering Manager) |
drivers/usb/ |
USB 控制器与设备 | host/、gadget/ |
drivers/i2c/ |
I2C 总线 | busses/、algos/ |
drivers/spi/ |
SPI 总线 | spi.c、spi-mem/ |
drivers/mmc/ |
SD/eMMC 存储 | core/、host/ |
drivers/input/ |
输入设备 | keyboard/、touchscreen/ |
drivers/char/ |
字符设备 | tty/、random.c |
drivers/mtd/ |
Flash 存储 | nand/、spi-nor/ |
drivers/pinctrl/ |
引脚控制 | pinctrl-rockchip.c 等 |
小贴士:驱动开发时,先到
drivers/下找同类型设备的驱动作为参考模板,比从零开始写要高效得多。例如写 SPI 驱动,先看drivers/spi/下已有的spi-xxx.c是怎么写的。
mm/ 与 fs/ —— 内存与文件系统
mm/ 负责内存管理,包括物理页分配、虚拟内存映射、页缓存等核心机制。fs/ 则是各种文件系统实现的所在地,ext4/、vfat/、nfs/ 等子目录分别对应不同的文件系统。如果你需要为嵌入式设备定制精简的文件系统支持,这里就是你要裁剪的地方。
mm/ 关键文件:
page_alloc.c:物理页分配器,alloc_pages()等核心接口;slab.c:slab 分配器,用于小块内存的高效分配;vmalloc.c:虚拟内存连续区域的分配;memory.c:缺页异常处理、内存映射;swap.c:交换(swap)机制。
fs/ 常见子目录:
| 子目录 | 文件系统 | 适用场景 |
|---|---|---|
fs/ext4/ |
ext4 | Linux 主流磁盘文件系统 |
fs/vfat/ |
vfat | U 盘、Windows 兼容分区 |
fs/squashfs/ |
squashfs | 只读压缩文件系统,适合嵌入式 rootfs |
fs/proc/ |
proc | 进程信息虚拟文件系统 |
fs/sysfs/ |
sysfs | 设备与驱动信息虚拟文件系统 |
fs/nfs/ |
nfs | 网络文件系统,适合开发调试 |
fs/overlayfs/ |
overlayfs | 容器镜像分层文件系统 |
小贴士:嵌入式裁剪时,
fs/是重点瘦身对象。只保留 rootfs 实际用到的文件系统(如ext4、squashfs、proc、sysfs、devtmpfs),其余全部关闭,能显著减小内核体积。
include/ —— 头文件的宝库
include/ 目录存放了所有头文件,定义了内核内部的数据结构和接口。其中 include/linux/ 是最常用的目录,包含了大量核心数据结构的定义(如 task_struct、file 等)。理解这些头文件,有助于在查阅文档或调试问题时快速定位代码位置。
关键子目录:
include/linux/:内核核心头文件,sched.h(进程描述符)、fs.h(文件系统)、mm.h(内存管理)等;include/asm-generic/:架构无关的汇编接口,各架构通过include/asm/覆盖;include/uapi/:用户态可见的接口头文件(系统调用、ioctl 定义等);include/dt-bindings/:设备树绑定常量定义(如 GPIO 编号、时钟 ID);include/net/:网络协议栈头文件;include/sound/、include/video/等:各子系统专用头文件。
高频使用的头文件:
| 头文件 | 内容 |
|---|---|
include/linux/sched.h |
task_struct 进程描述符、调度相关接口 |
include/linux/fs.h |
file、inode、super_block 等文件系统核心结构 |
include/linux/mm.h |
内存管理相关结构(page、vm_area_struct) |
include/linux/module.h |
内核模块开发必备(module_init、MODULE_LICENSE) |
include/linux/kernel.h |
printk、pr_info 等日志接口 |
include/linux/device.h |
设备模型核心结构(device、driver) |
小贴士:写内核模块时,
include/linux/module.h和include/linux/kernel.h是必引的头文件。遇到「找不到某个结构体定义」的编译错误,多半是漏了对应的头文件,用grep -r "struct xxx" include/linux/快速定位。3.3 快速定位代码的实用技巧
面对庞大的源码树,掌握几个快速定位的技巧能事半功倍:
# 在源码中搜索某个函数或宏的定义
grep -r "function_name" --include="*.c" --include="*.h" .
# 使用 ctags 建立索引,方便跳转
ctags -R .
vim -t function_name # 在 vim 中直接跳转到函数定义
# 查看某个配置选项在哪些文件中被引用
grep -r "CONFIG_XXX" .
# 使用 cscope 进行更强大的代码导航
cscope -R
小贴士:对于新手,强烈建议先花 10 分钟用
tree -L 2命令浏览一遍顶层目录结构,建立整体印象后再深入阅读具体代码,效率会高很多。
理解这些目录的职能,有助于在查阅文档或调试问题时快速定位代码位置。下一节我们将深入内核的五大核心子系统,看看它们是如何协同工作的。
④ 深入理解五大核心子系统功能架构
内核之所以强大,是因为它将复杂的功能模块化为了几个紧密协作的子系统。在定制内核前,理解这五大支柱非常有必要。下面我们逐一拆解每个子系统的职责、核心机制,以及它们在嵌入式开发中的实际意义。
4.1 进程管理(Process Management)
进程管理是内核的调度器,负责决定哪个进程在何时使用 CPU。它处理进程的创建、销毁、上下文切换以及优先级调度。没有它,多任务处理就无从谈起。
在 Linux 中,进程管理围绕几个核心概念展开:
- 进程与线程:进程是资源分配的基本单位,线程是调度的基本单位。一个进程可以包含多个线程,它们共享进程的地址空间和资源。
- 调度器(Scheduler):内核通过 CFS(完全公平调度器)等算法,在众多就绪进程中合理分配 CPU 时间片。它根据优先级、nice 值等因素决定下一个该运行哪个进程。
- 上下文切换:当 CPU 从一个进程切换到另一个进程时,需要保存当前进程的寄存器状态、栈指针等现场信息,再恢复目标进程的现场。这个过程由内核完成,对用户程序完全透明。
核心数据结构与关键文件:
进程管理的核心数据结构是 task_struct(进程描述符),它定义在 include/linux/sched.h 中,记录了进程的 PID、状态、优先级、内存映射、打开的文件、信号处理等信息。内核通过一个双向链表把所有进程的 task_struct 串起来,配合 kernel/sched/ 下的调度器代码,共同完成进程的全生命周期管理。
| 关键文件 | 职责 |
|---|---|
kernel/fork.c |
进程创建,fork()、vfork()、clone() 的实现 |
kernel/exit.c |
进程退出与资源回收 |
kernel/sched/core.c |
调度器核心,schedule() 函数所在 |
kernel/sched/fair.c |
CFS 完全公平调度器实现 |
kernel/signal.c |
进程信号处理 |
include/linux/sched.h |
task_struct 进程描述符定义 |
进程状态机:一个进程在生命周期中会经历多种状态,理解它们有助于排查「进程卡住」类问题:
创建(fork)→ 就绪(TASK_RUNNING)→ 运行(占用 CPU)
↑ ↓
└──── 唤醒 ← 睡眠(TASK_INTERRUPTIBLE / TASK_UNINTERRUPTIBLE)
TASK_RUNNING:就绪或正在运行;TASK_INTERRUPTIBLE:可中断睡眠,收到信号即被唤醒;TASK_UNINTERRUPTIBLE:不可中断睡眠,通常等待 I/O,ps中显示为D状态;TASK_STOPPED:被暂停(如SIGSTOP);TASK_ZOMBIE:僵尸进程,已退出但父进程尚未回收。
嵌入式视角:对于实时性要求高的场景(如电机控制、工业采集),可以启用 CONFIG_PREEMPT 或使用 PREEMPT_RT 补丁,让内核支持抢占式调度,降低任务响应延迟。此外,CONFIG_HZ 决定时钟中断频率(如 100/250/1000Hz),频率越高调度越精细,但 CPU 开销也越大,嵌入式设备需在实时性与功耗之间权衡。
小贴士:排查「进程卡在 D 状态」时,用
cat /proc/<pid>/stack查看内核栈,或echo w > /proc/sysrq-trigger触发一次调度器栈转储,能快速定位是哪个驱动或子系统阻塞了进程。
4.2 内存管理(Memory Management)
内存管理负责物理内存的分配与回收。物理内存是有限的资源,该子系统负责在多个进程间分配和回收内存,实现虚拟内存映射,并处理缺页中断。它还管理缓存机制,以提升 I/O 效率。
内存管理的核心机制包括:
- 虚拟内存:每个进程都拥有独立的虚拟地址空间,通过页表映射到物理内存。这样进程之间互不干扰,也方便了内存的按需分配。
- 页分配与回收:内核以页(通常 4KB)为单位管理物理内存。当内存不足时,通过页面回收、交换(swap)等机制释放空间。
- 页缓存(Page Cache):内核将磁盘文件的内容缓存在内存中,加速读写。这也是为什么反复读取同一个文件会越来越快的原因。
核心数据结构与关键文件:
内存管理的核心数据结构是 struct page(物理页描述符)和 struct vm_area_struct(进程虚拟地址空间中的一段连续区域,简称 VMA)。每个物理页对应一个 struct page,内核通过它跟踪页的状态(空闲、已分配、脏页等);每个进程的地址空间由若干 VMA 组成,mmap() 系统调用就是在进程地址空间中创建新的 VMA。
| 关键文件 | 职责 |
|---|---|
mm/page_alloc.c |
物理页分配器,alloc_pages() 等核心接口 |
mm/slab.c |
slab 分配器,用于小块内存的高效分配 |
mm/vmalloc.c |
虚拟内存连续区域的分配 |
mm/memory.c |
缺页异常处理、内存映射 |
mm/swap.c |
交换(swap)机制 |
include/linux/mm.h |
struct page、vm_area_struct 等核心结构定义 |
内存分配路径:一次典型的内存分配请求,从用户态到物理页的完整路径如下:
用户态 malloc() → 内核 mmap()/brk() → 分配 VMA(虚拟地址空间)
→ 首次访问触发缺页异常(缺页中断)
→ 内核通过 page_alloc 分配物理页
→ 建立页表映射 → 返回用户态继续执行
- 缺页异常(Page Fault):进程首次访问某段虚拟地址时,页表中还没有对应的物理页映射,CPU 触发缺页异常,内核在异常处理中分配物理页并建立映射。这是「按需分配」的核心机制。
- 写时复制(COW):
fork()创建子进程时,父子进程共享物理页,只有当某一方写入时才复制该页。这大幅减少了进程创建时的内存拷贝开销。
嵌入式视角:对于内存受限的设备,可以通过以下配置控制内存管理策略:
CONFIG_SLAB/CONFIG_SLUB:选择内核小块内存分配器。SLUB是当前默认且推荐的选择,性能更好、调试更简单;CONFIG_SWAP:若设备没有 swap 分区,务必关闭CONFIG_SWAP以节省内存开销;CONFIG_HIGHMEM:仅在 32 位系统且物理内存超过 1GB 时才需要关注,64 位系统无需配置;CONFIG_COMPACTION:开启内存碎片整理,对长时间运行的嵌入式设备有帮助。
小贴士:排查内存问题时,
cat /proc/meminfo查看内存分布,cat /proc/buddyinfo查看物理页碎片情况,cat /proc/slabinfo查看 slab 缓存占用。若发现MemAvailable长期偏低,优先检查是否有驱动存在内存泄漏。
4.3 虚拟文件系统(VFS)
虚拟文件系统(VFS)是内核中「统一文件访问」的抽象层。它屏蔽了底层不同文件系统(ext4、vfat、squashfs、proc 等)的差异,让用户态程序只需通过 open()、read()、write() 等标准系统调用,就能操作任意类型的文件系统,而无需关心底层实现。可以说,VFS 是连接「用户态文件操作」与「具体文件系统实现」之间的桥梁。
VFS 的核心机制包括:
- 统一接口:所有文件系统都向 VFS 注册一组标准操作函数(如
read、write、lookup),VFS 通过函数指针调用它们,实现「一套接口,多种实现」。 - 对象模型:VFS 用四种核心对象抽象文件系统——
super_block(超级块,描述整个文件系统)、inode(索引节点,描述一个文件/目录的元数据)、dentry(目录项,描述路径中的一个组成部分)、file(文件对象,描述一个已打开的文件实例)。 - 页缓存(Page Cache):VFS 层之上,文件读写会经过页缓存加速;VFS 之下,具体文件系统负责把数据落到块设备。
核心数据结构与关键文件:
VFS 的四种核心对象定义在 include/linux/fs.h 中,理解它们之间的关系是掌握 VFS 的关键:
| 对象 | 结构体 | 职责 | 生命周期 |
|---|---|---|---|
| 超级块 | struct super_block |
描述整个文件系统(挂载点、容量、根 inode) | 挂载时创建,卸载时销毁 |
| 索引节点 | struct inode |
描述一个文件/目录的元数据(权限、大小、时间戳) | 文件被访问时创建,缓存于内存 |
| 目录项 | struct dentry |
描述路径中的一个组成部分(如 /home/user 中的 home) |
路径解析时创建,缓存于 dcache |
| 文件对象 | struct file |
描述一个已打开的文件实例(当前读写位置、标志位) | open() 创建,close() 销毁 |
| 关键文件 | 职责 |
|---|---|
fs/namei.c |
路径解析核心,open() 系统调用的路径查找逻辑所在 |
fs/open.c |
open()、close() 等文件打开/关闭系统调用实现 |
fs/read_write.c |
read()、write() 系统调用实现 |
fs/super.c |
超级块管理,文件系统挂载/卸载的核心逻辑 |
fs/inode.c |
inode 的分配、缓存与回收 |
fs/dcache.c |
dentry 缓存(dcache)管理,加速路径解析 |
fs/file_table.c |
文件对象(struct file)的分配与回收 |
include/linux/fs.h |
VFS 四种核心对象及操作函数集定义 |
一次文件读写的完整路径:从用户态 read() 到磁盘数据的完整链路如下:
用户态 read() → VFS 层(fs/read_write.c)
→ 页缓存命中?是 → 直接返回数据
→ 否 → 调用具体文件系统的 read 操作(如 ext4_file_read_iter)
→ 落到块设备层(block/)→ 磁盘 I/O
→ 数据读入页缓存 → 返回用户态
- 路径解析:
open("/etc/config")时,内核通过fs/namei.c逐级查找etc、config对应的 dentry 与 inode,并优先命中 dcache 缓存以加速; - 挂载(mount):
mount系统调用通过fs/super.c创建super_block,并把该文件系统的根 inode 挂到挂载点目录上; - 页缓存:读写都优先经过页缓存,这也是「反复读同一文件越来越快」的原因。
嵌入式视角:VFS 是嵌入式裁剪的「双刃剑」。一方面,proc、sysfs、devtmpfs 这类虚拟文件系统是内核与用户态交互的窗口,必须保留;另一方面,fs/ 下大量真实文件系统(如 NTFS、XFS、Btrfs)若用不到,应在 menuconfig 中关闭以减小内核体积。此外,CONFIG_DEVTMPFS 与 CONFIG_PROC_FS、CONFIG_SYSFS 是嵌入式系统启动与调试的标配,建议保持开启。
小贴士:排查「文件系统挂载失败」时,先确认内核是否编译了对应文件系统支持(
CONFIG_EXT4_FS等),再用dmesg | tail查看挂载报错。若提示unknown filesystem type,多半是内核裁剪时把该文件系统关掉了。
4.4 网络子系统(Networking)
网络子系统负责数据包的收发、路由与协议解析,是 Linux 内核中最庞大、最复杂的子系统之一。从网卡驱动收到一个原始数据帧,到应用层 socket 读到数据,中间要经过协议栈的多层处理。理解它的分层结构,是排查网络问题、裁剪网络功能的基础。
网络子系统的核心机制包括:
- 协议栈分层:遵循 TCP/IP 四层模型,内核实现了链路层(
net/ethernet/)、网络层(net/ipv4/、net/ipv6/)、传输层(TCP/UDP)以及应用层接口(Socket)。每一层只与相邻层交互,职责清晰。 - Socket 抽象:
socket是用户态与内核网络栈之间的统一接口。用户程序通过socket()、bind()、listen()、accept()、send()、recv()等系统调用与内核交互,内核负责把数据在协议栈各层间传递。 - 网络设备层:
net_device结构体抽象了物理网卡或虚拟网卡(如eth0、lo、veth)。驱动通过netif_rx()把收到的数据帧送入协议栈,通过dev_queue_xmit()把要发送的数据交给网卡。 - NAPI 与中断收包:高负载下,网卡中断会频繁打断 CPU。NAPI(New API)机制将收包从「中断驱动」改为「轮询驱动」,大幅降低高吞吐场景下的 CPU 占用,是现代网卡驱动的标配。
核心数据结构与关键文件:
网络子系统的核心数据结构是 struct sk_buff(Socket Buffer),它贯穿整个协议栈,承载着每一层的数据。无论是收包还是发包,数据都以 sk_buff 的形式在各层间传递,各层通过操作它的头部指针来添加或剥离协议头。另一个核心结构是 struct net_device,它代表一个网络接口设备。
| 关键文件 | 职责 |
|---|---|
net/core/sock.c |
Socket 核心实现,struct sock 的管理 |
net/core/skbuff.c |
sk_buff 的分配、释放与操作 |
net/core/dev.c |
网络设备层核心,netif_rx()、dev_queue_xmit() |
net/ipv4/ip_input.c |
IPv4 收包处理,ip_rcv() |
net/ipv4/ip_output.c |
IPv4 发包处理,ip_output() |
net/ipv4/tcp.c |
TCP 协议实现 |
net/ipv4/udp.c |
UDP 协议实现 |
net/packet/af_packet.c |
原始套接字(raw socket),抓包工具的基础 |
include/linux/skbuff.h |
sk_buff 结构体定义 |
include/net/sock.h |
struct sock 套接字核心结构定义 |
一次数据收发的完整路径:以最常见的「浏览器访问网页」为例,数据包在内核中的旅程如下:
【收包方向】
网卡收到数据帧 → 驱动通过 NAPI 收包 → 构造 sk_buff
→ 链路层剥离以太网头(eth_type_trans)
→ 网络层 IP 处理(ip_rcv,查路由表)
→ 传输层 TCP 处理(tcp_v4_rcv,重组数据流)
→ Socket 层放入接收队列 → 用户态 recv() 读取
【发包方向】
用户态 send() → Socket 层构造 sk_buff
→ 传输层添加 TCP 头(tcp_sendmsg)
→ 网络层添加 IP 头并查路由(ip_output)
→ 链路层添加以太网头(dev_queue_xmit)
→ 驱动把 sk_buff 交给网卡 DMA 发出
嵌入式视角:网络子系统是裁剪的重点对象,因为协议栈默认开启的功能非常多,而嵌入式设备往往只需要其中一小部分:
- 按需裁剪协议:如果设备只需要 TCP/IP 通信,可以在
menuconfig的Networking support → Networking options中关闭IPv6、IPsec、Netfilter(防火墙)、Bluetooth、802.11(Wi-Fi)等用不到的协议,能显著减小内核体积; - 保留调试工具:建议保留
CONFIG_PACKET(原始套接字),它是tcpdump、ping等网络调试工具工作的基础; - 关注网络性能:对于高吞吐场景(如视频推流、网关设备),可开启
CONFIG_RPS、CONFIG_RFS等多队列优化选项,让多核 CPU 分担收包处理。
小贴士:排查网络问题时,
tcpdump -i eth0抓包是最直接的手段。如果抓不到任何包,问题多半在驱动或链路层;如果能抓到但应用层收不到,问题多半在协议栈或 Socket 层。另外,cat /proc/net/dev可以快速查看各网卡的收发统计,cat /proc/net/tcp能查看 TCP 连接状态。
4.5 设备驱动模型(Device Driver Model)
设备驱动模型是内核管理硬件设备与驱动之间关系的核心框架。它把「设备(Device)」「驱动(Driver)」「总线(Bus)」三者抽象成统一的对象,通过一套标准化的注册、匹配、绑定流程,让驱动开发变得规范而高效。理解这套模型,是编写和移植驱动的必修课。
三大核心对象:
- 设备(Device):描述一个具体的硬件实例,如「某块 I2C 温度传感器」「某个 USB 网卡」。它记录了设备的名称、资源(寄存器地址、中断号)、所属总线等信息,对应内核中的
struct device。 - 驱动(Driver):描述「如何操作某类设备」的代码逻辑,包含
probe()(探测并初始化设备)、remove()(卸载设备)、suspend()/resume()(电源管理)等回调函数,对应struct device_driver。 - 总线(Bus):设备与驱动之间的「桥梁」,负责把两者匹配起来。常见总线有
platform(平台总线)、i2c、spi、usb、pci等,对应struct bus_type。
设备与驱动的匹配流程:当内核启动或设备插入时,总线会遍历已注册的设备和驱动,通过 compatible 字符串、id_table 等方式进行匹配。一旦匹配成功,就调用驱动的 probe() 函数完成初始化:
设备注册(device_register)→ 加入总线设备链表
驱动注册(driver_register)→ 加入总线驱动链表
↓
总线匹配(bus_match)→ 匹配成功?
↓ 是
调用驱动 probe() → 初始化硬件、注册中断、创建设备节点
↓
设备可用(用户态通过 /dev/xxx 访问)
核心数据结构与关键文件:
设备模型的核心数据结构是 struct device(设备)和 struct device_driver(驱动),它们定义在 include/linux/device.h 中。而 platform_device 和 platform_driver 则是设备模型在「无真实总线」场景下的重要扩展——对于 SoC 内部集成的外设(如串口、GPIO、DMA 控制器),它们没有物理总线可挂,内核就用 platform 总线来承载。
| 关键文件 | 职责 |
|---|---|
drivers/base/core.c |
设备模型核心,设备注册、绑定、解绑 |
drivers/base/bus.c |
总线管理,设备与驱动的匹配逻辑 |
drivers/base/platform.c |
platform 总线与 platform 设备/驱动 |
drivers/base/dd.c |
设备与驱动的绑定(probe/remove 调用) |
include/linux/device.h |
struct device、struct device_driver 等核心结构定义 |
include/linux/platform_device.h |
platform_device、platform_driver 定义 |
设备树与驱动模型的衔接:在 ARM/ARM64 平台上,设备模型与设备树(Device Tree)紧密配合。设备树中的每个节点(如 uart0、i2c1)在启动时会被解析成对应的 platform_device,而驱动则通过 compatible 属性与设备节点匹配。这也是为什么驱动代码里常见 of_match_table:
static const struct of_device_id my_driver_of_match[] = {
{
.compatible = "vendor,my-device" },
{
}
};
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my-device",
.of_match_table = my_driver_of_match,
},
};
module_platform_driver(my_driver);
嵌入式视角:编写或移植驱动时,优先遵循设备模型而不是直接操作寄存器。好处有三:一是驱动与硬件解耦,换板卡只需改设备树;二是自动获得电源管理、热插拔、sysfs 暴露等能力;三是便于内核统一管理资源,避免冲突。新手写驱动,建议从 platform_driver 入手,配合设备树节点,这是当前 ARM 平台最主流的驱动形态。
小贴士:排查「驱动没生效」问题时,先看设备树节点是否与驱动的
compatible字符串一致,再看dmesg里有没有probe失败或-ENODEV报错。设备树里status = "disabled"的节点,驱动是永远不会被probe的。4.6 五大子系统如何协同工作
这五大子系统并非孤立存在,而是紧密协作的整体。以一次简单的文件读取为例:

- 用户程序调用
read(),先经过 VFS 统一接口; - VFS 查询 内存管理 的页缓存,若命中则直接返回,避免磁盘 I/O;
- 未命中时,VFS 调用具体文件系统(如 ext4)读取磁盘块;
- 最终由 设备驱动模型 中的块设备驱动与硬件交互,完成数据读取;
- 整个过程由 进程管理 调度,确保当前进程在等待 I/O 时能让出 CPU 给其他进程。
理解这五大子系统的协作关系,是深入内核开发和调试的基石。下一节我们将进入实战,学习如何配置内核选项并生成自定义配置文件。
⑤ 配置内核选项与生成自定义配置文件
内核配置决定了最终编译进去的功能模块,直接关系到内核的大小、性能和兼容性。配置工作主要通过交互式工具完成,最常用的是 make menuconfig。下面我们一步步拆解配置的完整流程。
5.1 配置前的准备工作
在执行配置命令前,确保已经设置了正确的 ARCH 和 CROSS_COMPILE 环境变量。如果你已经按第二节的方法创建了 env.sh,直接执行:
source env.sh
然后进入内核源码根目录,确认环境变量生效:
echo $ARCH # 应输出 arm64
echo $CROSS_COMPILE # 应输出 aarch64-linux-gnu-
小贴士:如果
ARCH没有设置,内核默认按当前宿主机架构(x86_64)配置,生成的配置和镜像都不适合你的开发板,务必先确认。
5.2 加载默认配置作为起点
对于初学者,最快的上手方式是加载一个默认配置作为基准。内核为每种架构都提供了默认配置文件,存放在 arch/<架构>/configs/ 目录下。
针对 ARM64 架构,可以运行:
make defconfig
这会生成一个适用于该架构的通用配置文件 .config。如果你使用的是厂商提供的开发板(如树莓派、RK3588 等),厂商通常会提供更贴近硬件的默认配置,例如:
# 以树莓派为例(具体文件名以厂商提供为准)
make bcm2711_defconfig
小贴士:可以先执行
ls arch/arm64/configs/查看有哪些现成的默认配置,优先选择与你的开发板最接近的那一个,能省去大量手动配置工作。
5.3 使用 menuconfig 进行可视化配置
在默认配置基础上,运行 make menuconfig 进行微调:
make menuconfig
这会弹出一个基于 ncurses 的文本图形界面。你可以像浏览菜单一样进入各个子系统,开启(Y)、关闭(N)或将功能编译为模块(M)。常用操作键:
- 方向键:在菜单中上下移动;
- Enter:进入子菜单或确认选择;
- Y:将该功能编译进内核(built-in);
- N:关闭该功能;
- M:编译为可加载模块(.ko);
- /:搜索某个配置选项(如输入
CONFIG_EXT4快速定位); - ?:查看当前选项的帮助说明;
- Esc Esc:返回上一级或退出。
小贴士:当不确定某个功能是否需要时,优先选择
M(模块)。这样即使暂时用不到,也不会让内核镜像变大,后续需要时还能动态加载。
5.4 配置过程中的关键检查点
配置过程中有几个关键点需要特别注意:
- 处理器类型:在 "Kernel Features" 或 "System Type" 中,必须选中与你开发板匹配的 CPU 型号。选错会导致内核无法启动或性能异常。
- 浮点运算:如果硬件支持 FPU 或 NEON,务必开启相关支持,否则数学运算会极慢。
- 文件系统:确保开启了 rootfs 所需的文件系统格式(如 EXT4、SquashFS),否则系统无法挂载根目录。
- 驱动模块:对于不确定是否需要的驱动,建议选择 "M"(模块),这样可以在系统运行时动态加载,避免内核镜像过大。
- 设备树支持:在 "Device Tree and Open Firmware support" 中确认已开启,这是 ARM 平台启动的必备选项。
5.5 保存配置与生成配置文件
配置完成后,选择 "Save" 保存,所有选择都会写入根目录的 .config 文件。这个文件非常重要,建议立即备份:
cp .config config_myboard_backup
下次编译时,可以直接复用这份配置:
cp config_myboard_backup .config
make olddefconfig # 根据新内核源码自动补齐缺失的选项
小贴士:
make olddefconfig会在保留你已有配置的基础上,自动为新增的内核选项填入默认值,避免因配置缺失导致编译报错。这是升级内核版本或复用旧配置时的推荐做法。
5.6 常用配置命令速查
除了 menuconfig,内核还提供了多种配置方式,适用于不同场景:
| 命令 | 用途 | 适用场景 |
|---|---|---|
make menuconfig |
交互式文本界面配置 | 日常手动配置,最常用 |
make defconfig |
加载架构默认配置 | 快速起步,作为基准 |
make olddefconfig |
基于现有 .config 补齐新选项 | 复用旧配置、升级内核 |
make tinyconfig |
生成最小化配置 | 精简内核体积,进阶优化 |
make savedefconfig |
精简 .config 为最小可读形式 | 整理配置、提交到版本库 |
配置完成后,就可以进入下一节,开始真正的内核编译了。
⑥ 执行内核编译流程与镜像生成步骤
配置就绪后,就可以开始真正的编译了。内核编译是一个计算密集型任务,耗时较长。为了充分利用多核 CPU 加速,建议使用 -j 参数指定并行作业数,通常设为 CPU 核心数加一。下面我们一步步拆解完整的编译流程。
6.1 编译前的最终检查
在按下回车开始编译之前,先花一分钟确认环境无误,避免编译到一半才发现问题:
# 确认架构与交叉编译器已设置
echo $ARCH
echo $CROSS_COMPILE
# 确认配置文件存在
ls -la .config
# 确认磁盘空间充足(内核编译通常需要 5~10GB)
df -h .
小贴士:如果
.config不存在,说明上一步配置没有保存成功,请回到第五节重新执行make menuconfig并保存。另外,编译过程会生成大量中间文件,务必确保源码目录所在分区有足够的剩余空间。
6.2 执行全量编译
确认无误后,执行编译命令:
make -j$(nproc)
$(nproc) 会自动获取当前 CPU 的核心数。如果你的机器内存较小(如 8GB 以下),建议适当降低并行度,例如 make -j4,避免内存耗尽导致编译进程被系统杀掉。
屏幕将滚动大量的编译信息。如果一切顺利,最终会在 arch/<架构>/boot/ 目录下生成内核镜像文件。对于 ARM64,通常是 Image 或 Image.gz;对于 x86,则是 bzImage。
小贴士:编译过程中如果看到
warning(警告)通常可以忽略,但出现error(错误)就必须停下来排查。遇到报错时,先看最顶部的错误信息,往往能直接定位问题所在。
6.3 编译设备树(DTB)
除了纯内核镜像,很多时候我们还需要设备树二进制文件(DTB)。它们通常位于 arch/<架构>/boot/dts/ 目录下,文件名与开发板型号对应。如果需要单独编译某个 DTB,可以指定目标:
make dtbs
如果只想编译特定开发板的设备树,可以直接指定文件名:
# 以树莓派 4 为例(具体文件名以你的开发板为准)
make dtbs/broadcom/bcm2711-rpi-4-b.dtb
小贴士:设备树文件(
.dts)位于arch/arm64/boot/dts/下,按厂商分目录存放。编译前可以先find arch/arm64/boot/dts -name "*.dts"搜索与你开发板匹配的文件名。
6.4 编译并安装内核模块
如果你在配置时选择了将部分功能编译为模块(.ko 文件),还需要执行安装命令将它们复制到指定目录:
make modules_install INSTALL_MOD_PATH=./output_modules
这一步会将所有 .ko 文件按目录结构整理到 ./output_modules 下,方便后续打包进根文件系统。查看一下生成结果:
find output_modules -name "*.ko" | head -20
小贴士:
INSTALL_MOD_PATH指定模块的安装根目录。对于嵌入式开发,通常把模块安装到目标根文件系统的对应位置,例如INSTALL_MOD_PATH=/path/to/rootfs,这样打包镜像时模块就已经就位。
6.5 编译产物一览
编译完成后,在源码根目录下会生成一系列产物,常见的有:
| 产物 | 位置 | 说明 |
|---|---|---|
| 内核镜像 | arch/arm64/boot/Image |
未压缩的内核镜像 |
| 压缩镜像 | arch/arm64/boot/Image.gz |
压缩后的内核镜像,体积更小 |
| 设备树 | arch/arm64/boot/dts/xxx.dtb |
各开发板的设备树二进制 |
| 内核模块 | output_modules/lib/modules/ |
编译为模块的驱动(.ko) |
| 符号表 | System.map |
内核符号地址映射,调试用 |
| 配置文件 | .config |
本次编译使用的配置 |
小贴士:
System.map和vmlinux(未压缩的完整内核)主要用于调试和内核开发,烧录时通常用不到,但建议保留,方便后续排查问题。
至此,编译产物的主体部分已经准备完毕。下一节我们将深入分析 KBuild 机制,理解 make 命令背后的构建逻辑。
⑦ 分析 KBuild 机制与顶层 Makefile 逻辑
在编译过程中,你可能好奇 make 命令到底是怎么知道如何编译成千上万个文件的?这归功于 Linux 内核独特的构建系统——KBuild。下面我们拆解它的核心机制,让你不仅会编译,还能看懂构建过程。
7.1 KBuild 的整体工作流程
KBuild 的核心逻辑集中在顶层 Makefile 中。当你运行 make 时,它大致经历以下几个阶段:

- 读取
.config:确定哪些功能被启用(y)、编译为模块(m)或关闭(n); - 加载架构 Makefile:根据
ARCH变量跳转到arch/arm64/Makefile,加载架构特有的编译标志和链接脚本; - 递归解析子目录:KBuild 会遍历所有子目录,收集各自的
Makefile中声明的编译目标; - 生成依赖文件:为每个目标生成
.cmd文件,记录编译命令和头文件依赖。
小贴士:理解这个流程后,你就明白为什么修改
.config后必须重新执行make,以及为什么切换架构(如从 arm64 换到 riscv)时要先make mrproper清理旧产物。
7.2 核心概念:obj-y 与 obj-m
KBuild 引入了两个重要的概念:obj-y 和 obj-m。在各级目录的 Makefile 中:
obj-y:列出的对象会被直接编译进内核镜像(built-in),例如obj-y += main.o表示main.c编译后链接进内核;obj-m:列出的则会被编译为独立模块(.ko),例如obj-m += mydriver.o表示mydriver.c编译为可加载模块。
这种机制使得内核具有极高的灵活性。以 drivers/net/ 下的 Makefile 为例:
# 编译进内核
obj-y += core.o
# 编译为模块
obj-m += my_net_driver.o
小贴士:当你编写自己的驱动时,只需在对应目录的
Makefile中按需添加obj-y或obj-m,再配合Kconfig中的config条目,就能让menuconfig识别并控制你的驱动。
7.3 依赖关系如何自动处理
KBuild 还自动处理了依赖关系。它生成的 .cmd 文件记录了每个目标文件的编译命令和依赖项。如果某个头文件被修改,KBuild 能精准地识别出哪些源文件需要重新编译,而不是盲目地全量重编,这在大型项目中极大地节省了时间。
你可以查看这些依赖文件:
# 查看某个目标文件的编译命令和依赖
cat drivers/net/core.o.cmd | head -20
小贴士:如果遇到“改了头文件但编译没生效”的诡异问题,多半是
.cmd依赖记录过期。此时执行make clean后重新编译即可强制刷新依赖。
7.4 常用 KBuild 目标速查
除了 make 全量编译,KBuild 还提供了许多实用目标:
| 目标 | 作用 | 典型场景 |
|---|---|---|
make |
全量编译内核镜像 | 日常编译 |
make modules |
只编译所有模块(.ko) | 单独构建模块 |
make modules_install |
安装模块到指定目录 | 打包 rootfs |
make dtbs |
编译设备树 | 生成 DTB 文件 |
make clean |
清理编译产物,保留配置 | 重新编译前 |
make mrproper |
彻底清理,含 .config | 切换架构/配置前 |
make help |
查看所有可用目标 | 快速了解构建系统 |
理解这一机制,有助于你在编写自定义驱动或修改内核源码时,正确编写自己的 Makefile。下一节我们将学习如何验证编译产物并清理目标文件。
⑧ 验证编译产物与清理目标文件方法
编译完成后,不要急着烧录,先验证产物的完整性。下面我们系统地检查镜像、模块、设备树等关键产物,并掌握 KBuild 提供的清理目标。
8.1 验证内核镜像
首先使用 file 命令检查生成的镜像格式是否正确:
file arch/arm64/boot/Image.gz
输出应显示为 gzip 压缩数据或 ELF 可执行文件,且架构标识与你预期一致。对于 ARM64,Image 是未压缩的 ELF 镜像,Image.gz 是 gzip 压缩后的版本。如果输出显示的是 x86 架构,说明 ARCH 环境变量可能没有正确设置。
还可以进一步检查镜像的版本信息:
# 查看内核版本字符串
strings arch/arm64/boot/Image | grep "Linux version" | head -1
小贴士:如果
file输出显示data而不是 ELF 或 gzip,说明镜像可能损坏或编译不完整,建议重新执行make编译。
8.2 验证设备树(DTB)
设备树文件同样需要验证。使用 dtc 工具可以反编译 DTB 检查其内容:
# 反编译 DTB 查看内容
dtc -I dtb -O dts arch/arm64/boot/dts/xxx.dtb | head -50
如果 dtc 未安装,可以通过包管理器安装:
sudo apt-get install device-tree-compiler
小贴士:反编译后应能看到
model、compatible等关键属性,确认与你开发板的型号匹配。如果设备树内容为空或报错,说明 DTB 编译有问题。
8.3 验证内核模块
检查 modules_install 输出的目录,确认 .ko 文件是否存在且符号表完整:
# 查看模块文件
find output_modules -name "*.ko" | head -10
# 检查模块的架构信息
file output_modules/lib/modules/*/kernel/drivers/net/*.ko | head -5
# 查看模块依赖关系
cat output_modules/lib/modules/*/modules.dep | head -10
modules.dep 文件记录了模块之间的依赖关系,如果该文件缺失,说明 depmod 步骤没有正确执行。
小贴士:如果某个
.ko文件的架构显示为 x86 而不是 ARM64,说明交叉编译环境配置有误,需要检查CROSS_COMPILE变量。
8.4 验证符号表与配置文件
System.map 记录了内核符号地址,是调试的重要依据。验证其完整性:
# 检查 System.map 是否存在且非空
ls -la System.map
wc -l System.map
# 检查某个关键符号是否在表中
grep " start_kernel" System.map
同时确认 .config 文件与本次编译一致:
# 查看配置中的关键选项
grep "CONFIG_ARM64" .config | head -5
小贴士:如果
System.map缺失,可以重新执行make生成。它对于内核调试(如使用 gdb 或分析 oops 信息)非常重要,建议保留。
8.5 KBuild 清理目标详解
在开发过程中,经常需要切换配置或清理环境。KBuild 提供了几个标准的清理目标:
| 清理目标 | 删除内容 | 保留内容 | 适用场景 |
|---|---|---|---|
make clean |
大多数生成的目标文件(.o、.ko) | .config 和少量辅助文件 |
日常重新编译 |
make mrproper |
所有生成文件,含 .config |
源码本身 | 切换架构、彻底重置 |
make distclean |
mrproper 内容 + 编辑器备份、补丁文件 |
源码本身 | 极少用到,发布前清理 |
# 日常清理:删除编译产物,保留配置
make clean
# 彻底清理:删除 .config 和所有生成文件
make mrproper
# 最彻底清理:在 mrproper 基础上删除备份和补丁
make distclean
小贴士:如果在修改配置后遇到奇怪的编译错误,尝试运行
make mrproper然后重新配置,往往能解决因残留旧对象文件导致的诡异问题。但注意mrproper会删除.config,执行前务必先备份你的配置文件。
8.6 清理与验证的完整流程
一个规范的开发流程应该是:验证 → 备份 → 清理 → 重新配置。以切换架构为例:
# 1. 备份当前配置
cp .config config_backup
# 2. 彻底清理旧产物
make mrproper
# 3. 恢复配置(或重新配置)
cp config_backup .config
make olddefconfig
# 4. 重新编译
make -j$(nproc)
小贴士:养成「先备份配置再清理」的习惯,能避免误删
.config后需要重新手动配置的麻烦。建议把config_backup命名为带日期和板卡型号的形式,如config_rk3588_20241014。
掌握这些验证与清理方法,能让你在开发过程中少走弯路。下一节我们将分析常见编译报错与环境依赖问题,帮你快速定位并解决编译失败。
⑨ 解决常见编译报错与环境依赖问题
编译内核很少能一次完美通过,遇到报错是常态。与其对着满屏的红色日志发愁,不如先掌握一套通用的排查思路:先看最顶部的第一条 error,再顺着它定位是环境问题、配置问题还是源码问题。下面我们把高频报错按类别拆开,逐一给出可落地的解决方案。
9.1 通用排查思路:三步定位法
遇到编译报错,先别急着搜网,按下面三步走,往往能快速缩小范围:
# 第一步:重新编译,把完整日志保存下来
make -j$(nproc) 2>&1 | tee build.log
# 第二步:只看 error 行,忽略海量 warning
grep -E "error:|Error [0-9]" build.log | head -20
# 第三步:定位到第一个 error 所在的文件和行号
grep -n "error:" build.log | head -1
小贴士:
tee build.log会把编译输出同时打印到屏幕并写入文件,方便反复检索。绝大多数报错都能在第一个error:处找到根因,后面的错误往往只是连锁反应。
9.2 工具链缺失或不匹配
典型报错:
aarch64-linux-gnu-gcc: command not found
/bin/sh: 1: flex: not found
原因分析:CROSS_COMPILE 前缀设置错误、工具链未加入 PATH,或宿主机缺少必要的编译工具。
解决方案:
# 确认工具链已安装
aarch64-linux-gnu-gcc --version
# 确认环境变量已生效
echo $CROSS_COMPILE # 应输出 aarch64-linux-gnu-
echo $ARCH # 应输出 arm64
# 若未生效,重新 source 环境脚本
source env.sh
如果 command not found 依旧存在,检查工具链的 bin 目录是否真的在 PATH 中:
which aarch64-linux-gnu-gcc
小贴士:在 64 位宿主机上编译某些较老的 32 位工具时,可能还需要安装 32 位兼容库:
sudo apt-get install lib32z1 lib32ncurses6 lib32stdc++6。若报错提示cannot execute binary file,多半就是这类问题。
9.3 依赖包不足
典型报错:
scripts/basic/fixdep: No such file or directory
HOSTCC scripts/basic/fixdep
/bin/sh: 1: bison: not found
原因分析:内核构建依赖 bc、bison、flex、libssl-dev、libelf-dev 等宿主工具。缺少它们时,构建脚本会在早期阶段直接失败。
解决方案:在 Ubuntu/Debian 上一次性补全:
sudo apt-get update
sudo apt-get install build-essential libssl-dev libelf-dev bc bison flex
其他发行版对应包名:
| 发行版 | 安装命令 |
|---|---|
| Ubuntu/Debian | sudo apt-get install build-essential libssl-dev libelf-dev bc bison flex |
| Fedora/RHEL | sudo dnf install gcc make openssl-devel elfutils-libelf-devel bc bison flex |
| Arch Linux | sudo pacman -S base-devel openssl elfutils bc bison flex |
小贴士:
libssl-dev缺失时,报错通常指向certs/或scripts/下的文件,提示找不到openssl/xxx.h。libelf-dev缺失时,报错通常指向scripts/mod/下的modpost工具。看到这类报错,优先补这两个包。
9.4 配置冲突与依赖缺失
典型报错:
drivers/net/xxx.o: undefined reference to `yyy_function`
原因分析:开启了某个驱动或功能,却忘记开启它依赖的总线支持、库函数或底层驱动,导致链接阶段找不到符号。
解决方案:
# 第一步:在源码中搜索缺失符号对应的配置
grep -r "yyy_function" --include="*.c" --include="*.h" .
# 第二步:在 menuconfig 中搜索相关配置项
make menuconfig
# 按 / 输入 CONFIG_XXX 搜索,找到后开启其依赖项
小贴士:
undefined reference to是链接错误,说明编译阶段已通过,问题出在配置裁剪过狠。遇到这类报错,优先检查报错文件所在目录的Kconfig,看它依赖哪些select或depends on的选项,逐一补全即可。
9.5 磁盘空间不足
典型报错:
No space left on device
gcc: fatal error: cannot write to output file
原因分析:内核编译会生成大量中间文件,完整编译通常需要 5~10GB 空间,加上模块和调试信息可能更多。
解决方案:
# 检查磁盘剩余空间
df -h .
# 清理 apt 缓存释放空间
sudo apt-get clean
# 若源码目录所在分区空间不足,可把编译输出重定向到其他分区
make O=/path/to/build_dir -j$(nproc)
小贴士:
make O=<目录>是内核的「分离编译目录」特性,可以把所有编译产物输出到指定目录,源码目录保持干净。这在磁盘空间紧张或需要多配置并行编译时非常实用。
9.6 内存不足导致编译进程被杀
典型报错:
Killed
gcc: internal compiler error: Killed (program cc1)
原因分析:并行编译任务过多,内存被耗尽,系统 OOM Killer 杀掉了编译进程。
解决方案:降低并行度,或限制单任务内存:
# 降低并行度(8GB 内存建议 -j4)
make -j4
# 或使用更保守的并行度
make -j2
小贴士:
-j$(nproc)在内存充足时最快,但内存小于 8GB 时容易触发 OOM。可以先free -h查看内存,再决定并行度。经验值:每 2GB 内存对应 1 个并行任务比较稳妥。
9.7 常见报错速查表
| 报错信息 | 可能原因 | 快速解决 |
|---|---|---|
command not found |
工具链未安装或 PATH 未配置 | 安装工具链,source env.sh |
bison: not found / flex: not found |
缺少宿主构建工具 | apt-get install bison flex |
openssl/xxx.h: No such file |
缺少 libssl-dev |
apt-get install libssl-dev |
elf.h: No such file |
缺少 libelf-dev |
apt-get install libelf-dev |
undefined reference to |
配置依赖缺失 | 在 menuconfig 中补全依赖项 |
No space left on device |
磁盘空间不足 | 清理磁盘或用 make O= 分离输出 |
Killed / internal compiler error |
内存不足,OOM | 降低 -j 并行度 |
arch/arm64/boot/Image 显示 x86 |
ARCH 未设置 |
export ARCH=arm64 |
.ko 文件架构为 x86 |
CROSS_COMPILE 未设置 |
export CROSS_COMPILE=aarch64-linux-gnu- |
小贴士:把这张表截图或收藏,遇到报错先对照排查。绝大多数新手报错都集中在「环境变量没设对」和「依赖包没装全」这两类,属于可以提前规避的低级问题。
掌握这些排查方法,编译报错就不再是拦路虎。下一节我们将分享 ccache 加速、局部编译等进阶优化技巧,帮你把编译效率再提升一个台阶。
⑩ 优化编译策略与进阶定制技巧
当你对内核编译流程已经驾轻就熟,下一步就是思考如何提升效率、精简产物、并让整个构建过程更自动化。本章将分享 ccache 加速、局部编译、分离编译目录、精简内核体积、自动化脚本等进阶技巧,帮你把编译效率再提升一个台阶。
10.1 使用 ccache 加速重复编译
在开发过程中,我们经常需要反复修改源码、微调配置后重新编译。如果每次都全量重编,等待时间会非常漫长。ccache(Compiler Cache)正是解决这一痛点的利器,它会缓存编译器的中间结果,当输入文件没有变化时直接复用缓存,速度提升可达数倍甚至十倍。
安装与配置:
# Ubuntu/Debian
sudo apt-get install ccache
# Fedora/RHEL
sudo dnf install ccache
安装完成后,需要让内核构建系统使用 ccache。推荐在 env.sh 中设置:
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
export CC="ccache aarch64-linux-gnu-gcc"
小贴士:也可以直接使用
ccache提供的编译器包装器。将工具链的bin目录软链接到 ccache 的包装目录,这样无需修改CC变量也能生效。不过对于初学者,直接在env.sh中设置CC是最简单直观的方式。
验证 ccache 是否生效:
# 查看缓存命中统计
ccache -s
# 第一次编译后查看命中率
make -j$(nproc)
ccache -s # 此时 cache miss 会增加
# 再次编译(未修改任何文件)
make -j$(nproc)
ccache -s # 此时 cache hit 应大幅增加,编译速度明显加快
小贴士:ccache 的默认缓存目录在
~/.cache/ccache,可以通过ccache -M 10G设置缓存上限(例如 10GB)。对于内核这种大型项目,建议至少分配 5~10GB 缓存空间。
10.2 局部编译:只编译你修改的部分
全量编译动辄十几分钟甚至更久,但如果只是修改了某个驱动或子系统,完全没必要重新编译整个内核。KBuild 提供了灵活的局部编译能力。
场景一:只编译某个驱动模块
# 进入驱动源码目录,单独编译该模块
make M=drivers/net/ethernet/intel/ -j$(nproc)
这会只编译 drivers/net/ethernet/intel/ 目录下的模块,生成对应的 .ko 文件,速度极快。
场景二:只编译某个子系统的目标文件
# 只编译 mm/ 目录下的目标文件
make mm/ -j$(nproc)
# 只编译某个具体文件
make mm/page_alloc.o
场景三:修改头文件后的增量编译
# 修改了 include/linux/sched.h 后,只需重新编译依赖它的文件
make -j$(nproc)
KBuild 会自动检测头文件依赖,只重新编译受影响的目标文件,这就是增量编译。
小贴士:局部编译前务必确认
.config已经生成。如果修改了Kconfig或.config,建议先执行make olddefconfig刷新配置,再局部编译,避免配置不一致导致的诡异报错。
10.3 分离编译目录:让源码目录保持干净
默认情况下,编译产物会散落在源码目录中,与源码混在一起。这不仅让 git status 变得混乱,也不利于多配置并行编译。内核提供了 O= 参数,将编译输出重定向到独立目录。
# 创建独立的编译输出目录
mkdir -p build/arm64
# 指定输出目录编译
make O=build/arm64 defconfig
make O=build/arm64 -j$(nproc)
分离编译的优势:
- 源码目录干净:
git status只显示源码改动,不会被编译产物干扰; - 多配置并行:可以在不同目录同时维护多个配置(如
build/arm64、build/riscv),互不干扰; - 磁盘管理方便:清理时只需删除
build/目录,源码目录不受影响。
小贴士:使用
O=后,所有产物(.config、Image、System.map、模块等)都在输出目录下。后续的make modules_install、make dtbs等命令也要带上O=build/arm64参数。
10.4 精简内核体积:为资源受限设备瘦身
对于 Flash 容量只有几十 MB 的嵌入式设备,内核体积至关重要。除了前文提到的 make tinyconfig,这里再分享几个实用的瘦身技巧。
技巧一:关闭调试与日志
# 在 menuconfig 中关闭以下选项
# CONFIG_DEBUG_KERNEL=n
# CONFIG_PRINTK=n(谨慎,会影响日志输出)
# CONFIG_KALLSYMS=n
技巧二:裁剪未用到的功能
# 在 menuconfig 中逐项检查
# 关闭未用到的文件系统(如 NTFS、XFS)
# 关闭未用到的网络协议(如 IPv6、IPsec)
# 关闭未用到的驱动(如 GPU、声卡)
技巧三:使用压缩内核镜像
# 生成压缩后的内核镜像
make Image.gz
对于 ARM64,Image.gz 通常比 Image 小 30%~50%,适合烧录到 Flash 中。
技巧四:使用 savedefconfig 精简配置
# 生成最小化的 .config 文件
make savedefconfig
defconfig 文件会精简为最小可读形式,只保留与默认值不同的选项,便于版本管理和后续维护。
小贴士:精简内核时务必保留 rootfs 所需的文件系统、启动必需的驱动和串口调试支持。建议每裁剪一项就编译验证一次,避免一次性裁剪过多导致无法启动。
10.5 自动化构建脚本:一键完成全流程
将获取源码、配置、编译、打包的过程固化为脚本,能显著提升开发效率和可重复性。下面是一个完整的自动化构建脚本示例:
#!/bin/bash
# build_kernel.sh - 一键构建内核
set -e # 任何错误立即退出
# 配置区
KERNEL_VERSION="v6.6.10"
ARCH="arm64"
CROSS_COMPILE="aarch64-linux-gnu-"
BUILD_DIR="build/arm64"
OUTPUT_DIR="output"
echo "=== 1. 准备环境 ==="
export ARCH=$ARCH
export CROSS_COMPILE=$CROSS_COMPILE
export CC="ccache ${CROSS_COMPILE}gcc"
echo "=== 2. 获取源码(如不存在) ==="
if [ ! -d "linux" ]; then
git clone --depth 1 --branch $KERNEL_VERSION \
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
fi
cd linux
echo "=== 3. 配置内核 ==="
mkdir -p $BUILD_DIR
if [ -f "config_myboard" ]; then
cp config_myboard $BUILD_DIR/.config
make O=$BUILD_DIR olddefconfig
else
make O=$BUILD_DIR defconfig
fi
echo "=== 4. 编译内核 ==="
make O=$BUILD_DIR -j$(nproc)
echo "=== 5. 编译设备树 ==="
make O=$BUILD_DIR dtbs
echo "=== 6. 编译并安装模块 ==="
mkdir -p ../$OUTPUT_DIR
make O=$BUILD_DIR modules_install INSTALL_MOD_PATH=../$OUTPUT_DIR
echo "=== 7. 收集产物 ==="
mkdir -p ../$OUTPUT_DIR/boot
cp $BUILD_DIR/arch/arm64/boot/Image.gz ../$OUTPUT_DIR/boot/
cp $BUILD_DIR/arch/arm64/boot/dts/*.dtb ../$OUTPUT_DIR/boot/
cp $BUILD_DIR/System.map ../$OUTPUT_DIR/
echo "=== 构建完成!产物位于 $OUTPUT_DIR/ ==="
小贴士:将脚本保存为
build_kernel.sh并赋予执行权限(chmod +x build_kernel.sh),每次构建只需执行./build_kernel.sh。配合 CI/CD 工具(如 Jenkins、GitLab CI),可以实现提交代码后自动构建固件。
10.6 进阶定制:编写自己的内核模块
当你对内核构建流程足够熟悉后,可以尝试编写自己的内核模块,将自定义功能以 .ko 形式加载。一个最简单的 Hello World 模块:
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Linux kernel!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Linux kernel!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello world module");
对应的 Makefile:
obj-m += hello.o
KERNEL_DIR := /path/to/your/kernel/source
ARCH := arm64
CROSS_COMPILE := aarch64-linux-gnu-
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
编译并加载:
# 编译模块
make
# 将模块复制到目标板并加载
scp hello.ko root@<board_ip>:/root/
ssh root@<board_ip> "insmod hello.ko && dmesg | tail -5"
小贴士:编写模块时,
KERNEL_DIR必须指向与目标板内核版本一致的源码目录,否则模块可能因版本不匹配而无法加载(invalid module format错误)。
10.7 优化策略速查表
| 优化手段 | 适用场景 | 关键命令/配置 |
|---|---|---|
| ccache 加速 | 反复修改源码重新编译 | export CC="ccache gcc" |
| 局部编译 | 只修改某个驱动/子系统 | make M=drivers/net/... |
| 分离编译目录 | 多配置并行、保持源码干净 | make O=build/arm64 |
| 精简内核体积 | Flash 容量受限的设备 | make tinyconfig、关闭调试选项 |
| 压缩镜像 | 减小烧录体积 | make Image.gz |
| 自动化脚本 | 重复构建、CI/CD 集成 | 编写 build_kernel.sh |
| 自定义模块 | 扩展内核功能 | obj-m += hello.o |
编译内核不仅是技术操作,更是理解操作系统设计哲学的过程。每一次配置的取舍,每一行代码的编译,都在加深你对计算机系统的认知。随着经验的积累,你将不再畏惧那庞大的源码树,而是能游刃有余地裁剪出最适合你硬件的那颗“心脏”。
⑪ 实战案例:从零构建 RK3588 定制内核
理论讲得再多,不如亲手跑通一次。下面我们以一块常见的 RK3588 开发板为例,把前面十章的知识串成一条完整的实战链路:从获取源码、搭建环境、配置裁剪,到编译镜像、验证产物、烧录启动。跟着做一遍,你就能真正掌握「从零构建定制内核」的完整流程。
11.1 案例背景与目标
硬件平台:RK3588 开发板(8 核 ARM64,含 Mali-G610 GPU、双千兆网卡、USB3.0 等外设)。
软件目标:
- 内核版本:
v6.6.10(LTS,稳定且对 RK3588 支持良好); - 裁剪掉用不到的驱动与协议,控制镜像体积在 30MB 以内;
- 保留串口调试、网络、USB、Mali GPU 等核心功能;
- 生成可烧录的
Image.gz+ 设备树rk3588-evb.dtb+ 内核模块。
最终产物:
| 产物 | 路径 | 说明 |
|---|---|---|
| 内核镜像 | build/arm64/arch/arm64/boot/Image.gz |
压缩内核,约 25MB |
| 设备树 | build/arm64/arch/arm64/boot/dts/rockchip/rk3588-evb.dtb |
板级设备树 |
| 内核模块 | output/lib/modules/6.6.10/ |
编译为模块的驱动 |
| 配置文件 | build/arm64/.config |
本次编译的配置 |
11.2 第一步:准备环境与获取源码
在 x86_64 的 Ubuntu 22.04 宿主机上操作。先安装依赖与交叉工具链:
# 安装宿主构建依赖
sudo apt-get update
sudo apt-get install build-essential libssl-dev libelf-dev bc bison flex device-tree-compiler
# 安装 ARM64 交叉工具链
sudo apt-get install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
# 安装 ccache 加速重复编译
sudo apt-get install ccache
创建项目目录并获取源码:
mkdir -p ~/rk3588-kernel && cd ~/rk3588-kernel
# 拉取 LTS 内核(浅克隆,节省时间)
git clone --depth 1 --branch v6.6.10 \
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
小贴士:如果
kernel.org下载慢,可改用清华镜像:https://mirrors.tuna.tsinghua.edu.cn/git/linux-stable.git。
11.3 第二步:配置环境变量
在项目根目录创建 env.sh,集中管理环境变量:
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
export CC="ccache aarch64-linux-gnu-gcc"
export PATH=/usr/bin:$PATH
加载并验证:
source env.sh
aarch64-linux-gnu-gcc --version # 确认工具链可用
echo $ARCH # 应输出 arm64
11.4 第三步:生成并裁剪配置
进入源码目录,先加载 RK3588 的默认配置(内核主线已包含 Rockchip 平台的 defconfig):
cd linux
# 查看可用的 rockchip 默认配置
ls arch/arm64/configs/ | grep rockchip
# 加载 RK3588 默认配置
make rockchip_defconfig
在默认配置基础上,用 menuconfig 做针对性裁剪:
make menuconfig
本次裁剪要点:
- 关闭用不到的协议:
Networking support → Networking options中关闭IPv6、IPsec、Netfilter(若不需要防火墙); - 关闭用不到的文件系统:
File systems中只保留ext4、vfat、squashfs、proc、sysfs、devtmpfs,其余全部关闭; - 关闭调试与日志:
Kernel hacking中关闭CONFIG_DEBUG_KERNEL、CONFIG_KALLSYMS; - 保留核心功能:确认
Device Tree and Open Firmware support、Serial port(串口调试)、USB、Mali GPU驱动均已开启。
保存配置并备份:
# 保存 .config 并备份
cp .config config_rk3588_backup
# 生成精简版配置便于版本管理
make savedefconfig
11.5 第四步:分离目录编译
使用 O= 分离编译目录,保持源码干净,也方便后续多配置并行:
mkdir -p build/arm64
# 将配置复制到输出目录
cp .config build/arm64/.config
# 编译内核镜像(8 核 CPU,内存 16GB,可用 -j8)
make O=build/arm64 -j8
编译完成后,检查产物:
ls -lh build/arm64/arch/arm64/boot/
# 应看到 Image 和 Image.gz
11.6 第五步:编译设备树与模块
单独编译 RK3588 的设备树:
make O=build/arm64 dtbs
ls build/arm64/arch/arm64/boot/dts/rockchip/rk3588-evb.dtb
编译并安装内核模块到独立目录:
mkdir -p ../output
make O=build/arm64 modules_install INSTALL_MOD_PATH=../output
11.7 第六步:验证产物
逐一验证镜像、设备树和模块的架构与完整性:
# 验证镜像架构
file build/arm64/arch/arm64/boot/Image.gz
# 应输出 gzip compressed data
# 验证设备树内容
dtc -I dtb -O dts build/arm64/arch/arm64/boot/dts/rockchip/rk3588-evb.dtb | head -20
# 应能看到 model = "Rockchip RK3588 EVB" 等关键属性
# 验证模块架构
file ../output/lib/modules/6.6.10/kernel/drivers/net/*.ko | head -3
# 应显示 ARM aarch64
11.8 第七步:烧录与启动验证
将产物打包并烧录到开发板:
# 打包产物
cd ~/rk3588-kernel
tar -czf rk3588_kernel_v6.6.10.tar.gz \
-C linux/build/arm64/arch/arm64/boot Image.gz \
-C linux/build/arm64/arch/arm64/boot/dts/rockchip rk3588-evb.dtb \
-C output lib/modules
烧录方式(二选一):
- SD 卡启动:将
Image.gz与rk3588-evb.dtb放入 SD 卡的 boot 分区,替换原有内核文件; - fastboot 烧录:连接开发板进入 fastboot 模式后执行:
fastboot flash boot Image.gz
fastboot flash dtb rk3588-evb.dtb
fastboot reboot
启动验证:
# 通过串口连接开发板,观察启动日志
dmesg | grep "Linux version"
# 应输出 Linux version 6.6.10
# 确认关键驱动加载成功
dmesg | grep -E "mali|gmac|usb"
小贴士:如果启动卡在某个驱动,先看串口日志最后一条
error或failed信息,再回到menuconfig检查对应配置。常见问题是设备树节点与驱动compatible字符串不匹配。
11.9 案例总结与复盘
通过这个实战案例,我们完整走通了「源码获取 → 环境搭建 → 配置裁剪 → 分离编译 → 产物验证 → 烧录启动」的全流程。回顾几个关键点:
- 版本选择:LTS 版本(
v6.6.10)是嵌入式落地的稳妥之选; - 环境变量:
ARCH与CROSS_COMPILE必须正确设置,否则产物架构会错; - 配置裁剪:先加载厂商 defconfig 再微调,比从零配置高效得多;
- 分离编译:
O=build/arm64让源码目录保持干净,多配置并行互不干扰; - 验证先行:烧录前用
file、dtc验证产物,能避免大量无效烧录。
至此,你已经掌握了从零构建定制内核的完整技能。无论是 RK3588 还是其他 ARM/RISC-V 平台,这套方法论都能直接复用。动手实践,是理解 Linux 内核最好的方式。