3. Linux 内核源码结构:从目录到镜像的完整旅程

说实话,很多初学者拿到内核源码后,第一反应就是懵。几万个文件,几百个目录,从哪看起?我当年也经历过这个阶段。后来带项目时发现,搞懂内核源码结构,其实就是搞懂三件事:东西放哪、怎么配、怎么编。这一章我们就围绕这三件事展开。

本章核心脉络: 源码目录 → Kconfig/Makefile → 配置系统 → 编译构建。一条线串起从源码到内核镜像的完整流程。

3.1 内核源码顶层目录结构

先看顶层。你解压内核后,第一眼看到的目录大概有 20 多个。我习惯把它们分成三类:

类别 目录 一句话说明
架构相关 arch/ 每种 CPU 架构一个子目录,比如 arm/、arm64/、x86/
核心子系统 kernel/、mm/、fs/、net/、drivers/、ipc/ 进程调度、内存管理、文件系统、网络协议栈、驱动、进程间通信
基础设施 include/、init/、scripts/、tools/、Documentation/ 头文件、启动入口、构建脚本、用户工具、文档

这里我特别想提一下 arch/ 目录。做嵌入式 BSP 的朋友,大部分时间都在跟 arch/arm/ 或 arch/arm64/ 打交道。里面又细分了 boot/、configs/、kernel/、mach-xxx/、plat-xxx/ 等子目录。每个芯片厂商(TI、NXP、Rockchip、Allwinner 等)都有自己的 mach- 目录。

我的习惯: 接手一个新平台时,先看 arch/arm/configs/ 下的 defconfig 文件,再看 arch/arm/mach-xxx/ 下的板级初始化代码。这两块能帮你快速了解平台的基本配置。

3.2 Kconfig 与 Makefile:内核的"骨架"和"肌肉"

内核源码里,每个子目录下几乎都有两个文件:Kconfig 和 Makefile。我经常跟团队说:Kconfig 定义"有什么选项",Makefile 定义"怎么编译这些选项"。

3.2.1 Kconfig:配置选项的声明

Kconfig 的语法其实不复杂。看一个实际例子:

# drivers/i2c/Kconfig 中的一段
config I2C_CHARDEV
    tristate "I2C device interface"
    help
      Say Y here to enable the i2c-dev interface.
      This provides a character device for each I2C adapter.

这里 tristate 表示三种状态:Y(编入内核)、M(编译为模块)、N(不编译)。如果是 bool,就只有 Y 和 N 两种。

Kconfig 还支持 depends on、select、default 等关键字。我曾经踩过一个坑:在某个 BSP 项目中,我配置了 I2C_CHARDEV 为 Y,但忘了它 depends on I2C,而 I2C 被我设成了 N。结果编译时这个选项直接被忽略了。嗯,从那以后我每次配完都会跑一遍 make olddefconfig 来检查依赖。

3.2.2 Makefile:编译规则的编写

内核的 Makefile 使用了一种特殊的"目标-源文件"映射方式:

# drivers/i2c/Makefile
obj-$(CONFIG_I2C_CHARDEV) += i2c-dev.o
obj-$(CONFIG_I2C_ALGOBIT) += i2c-algo-bit.o

这里的 obj-$(CONFIG_I2C_CHARDEV) 会在配置时被展开。如果 CONFIG_I2C_CHARDEV=y,就变成 obj-y += i2c-dev.o,表示编入内核;如果是 m,就变成 obj-m,表示编译为模块。

注意: 内核 Makefile 里不要用通配符 *.o 来匹配所有源文件。每个文件都要显式列出。这是内核构建系统的硬性要求,也是很多新手容易犯的错误。

3.3 内核配置系统:从 menuconfig 到 .config

配置系统说白了就是一套"菜单-依赖-输出"的机制。流程如下:

  1. 读取 Kconfig 树:从顶层 Kconfig 开始,通过 source 语句递归包含所有子目录的 Kconfig
  2. 生成菜单界面:make menuconfig 调用 scripts/kconfig/mconf 显示交互菜单
  3. 用户选择:在菜单中勾选/取消选项
  4. 输出 .config:保存所有选项的最终值
  5. 生成 autoconf.h:include/generated/autoconf.h 供 C 源码使用

我个人最常用的配置命令:

命令 用途
make menuconfig 基于 ncurses 的文本菜单,最常用
make defconfig 使用架构默认配置(arch/xxx/configs/xxx_defconfig)
make olddefconfig 基于现有 .config,自动处理新增选项的默认值
make savedefconfig 从当前 .config 生成最小化 defconfig
避坑指南: 我曾经在项目交付前发现内核编译不过,查了半天发现是 .config 里有个选项被误改了。从那以后,我要求团队把 defconfig 纳入版本管理,每次修改配置后都用 make savedefconfig 更新 defconfig 文件。这样既能追踪变更,又能保证可复现。

3.4 内核编译与构建:从源码到镜像

编译内核,说白了就是三步:配置、编译、安装。但每一步都有不少门道。

3.4.1 标准编译流程

# 1. 配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig

# 2. 编译
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4

# 3. 安装模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_install INSTALL_MOD_PATH=/path/to/rootfs

# 4. 安装内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install

这里 -j4 表示用 4 个并行任务编译。我一般设成 CPU 核心数 + 1,比如 8 核机器用 -j9。

3.4.2 编译产物

编译完成后,主要产物在 arch/arm/boot/ 下:

  • zImage:压缩后的内核镜像,常用
  • uImage:带 U-Boot 头部的镜像
  • Image:未压缩的原始镜像
  • dtbs/:设备树二进制文件(.dtb)
关键点: 对于现代 ARM 平台,zImage + 设备树 .dtb 是标准组合。U-Boot 启动时先加载 zImage 到内存,再加载 dtb,然后 bootz 命令启动。

3.4.3 构建系统的"隐藏"机制

内核的构建系统基于 GNU Make,但做了大量封装。我简单说一下它的工作流:

  1. 顶层 Makefile 读取 .config,生成 include/config/auto.conf
  2. 递归进入每个子目录,读取 Makefile,根据 obj-y、obj-m 确定编译目标
  3. 每个 .c 文件编译成 .o 文件
  4. obj-y 的 .o 链接成 built-in.o(或 built-in.a)
  5. 所有 built-in.o 链接成 vmlinux(未压缩内核)
  6. vmlinux 被压缩并加上头部,生成 zImage

这个过程看起来简单,但实际调试时经常出问题。我记得有一次,我加了一个驱动文件,Makefile 里也写了 obj-y,但编译后内核里就是找不到这个驱动。折腾了半天,发现是 Kconfig 里 depends on 了一个没开启的选项,导致整个目录都没被编译。从那以后,我养成了一个习惯:每次加新文件,先 make 一遍确认编译通过,再提交代码。

3.5 本章知识结构图

下面这张图总结了内核源码结构的核心脉络:

Linux 内核源码结构知识图谱 Linux 内核源码结构知识图谱 内核源码树 目录结构 配置系统 编译构建 arch/ kernel/ mm/ fs/ drivers/ include/ init/ Kconfig Makefile menuconfig .config vmlinux zImage .ko 模块 .dtb 设备树 核心关系:Kconfig 定义选项 → menuconfig 生成 .config → Makefile 根据 .config 编译 最终产物:vmlinux(调试用)、zImage(启动用)、.ko(模块)、.dtb(设备树)

3.6 本章小结

这一章我们走了一遍内核源码的"骨架":从顶层目录结构,到 Kconfig/Makefile 的配合,再到配置系统和编译构建的完整流程。说白了,内核源码的组织方式就是一套"声明-配置-编译"的流水线。你只要掌握了这个流水线的每个环节,以后不管是移植新平台、添加新驱动,还是调试内核问题,都能快速定位到对应的文件和配置项。

实践建议: 找一块你熟悉的开发板,下载对应版本的内核源码,按本章的流程完整编译一遍。过程中遇到任何报错,先看 scripts/ 目录下的构建脚本,再看 Documentation/ 下的说明。自己动手踩一遍坑,比看十遍文档都管用。