3. Linux内核源码结构与配置:从源码树到定制镜像

内核源码,说白了就是一座巨大的乐高城堡。我第一次接触Linux内核源码时,面对密密麻麻的目录,说实话有点懵。但后来我发现,只要掌握了它的骨架结构,再配合Kconfig和Makefile这两把钥匙,你就能轻松地裁剪、配置、编译出适合自己硬件平台的内核镜像。

这一章,我们就来拆解这座城堡。我会结合我这些年做过的项目,把内核源码的目录结构、配置机制、编译流程,还有设备树(DTS)的语法,掰开揉碎了讲给你听。

3.1 内核源码目录分析:骨架与血肉

先看看顶层目录。你下载一个主线内核,解压后第一眼看到的就是这些文件夹。它们分工明确,各司其职。

目录名 核心内容 我的经验备注
arch/ 体系结构相关代码(ARM、x86、RISC-V等) 移植时,这里是你最常待的地方
drivers/ 设备驱动(占源码一半以上) 写驱动,基本就在这个目录里转
kernel/ 核心调度、进程管理、信号等 一般不动,除非你在做深度优化
mm/ 内存管理 调优内存时才会进来看看
fs/ 文件系统(ext4、btrfs、NFS等) 加新文件系统支持时用
net/ 网络协议栈 网络驱动和协议都在这里
include/ 头文件(公共API定义) 找结构体定义,先来这里翻
Documentation/ 内核文档 遇到坑,先看这里,比百度靠谱
scripts/ 编译、配置、分析脚本 Kconfig解析、模块签名等工具

嗯,这里要注意。很多人一上来就扎进 drivers/ 里找代码,结果迷路了。我建议你先看 arch/ 目录,因为你的板子是什么CPU架构,决定了你从哪个入口开始。比如你做ARM64的板子,那 arch/arm64/ 就是你的主战场。

核心原则: 移植工作的第一步,不是写代码,而是找到正确的 arch/ 目录下的板级文件或设备树。

3.2 Kconfig与Makefile机制:配置的“语法”

内核的配置系统,说白了就是一套“菜单生成器”。Kconfig 定义菜单项,Makefile 根据菜单选择决定编译什么。这两兄弟配合得天衣无缝。

3.2.1 Kconfig:菜单的“菜谱”

每个驱动目录下都有一个 Kconfig 文件。它定义了哪些功能可以被选中。比如:

config SENSORS_LM75
    tristate "LM75 temperature sensor"
    depends on I2C
    help
      If you say yes here, support for LM75 temperature sensor will be included.

这里 tristate 表示有三种状态:编译进内核(y)、编译成模块(m)、不编译(n)。depends on I2C 表示这个驱动依赖I2C子系统,如果I2C没选,它就不会出现。

我的习惯: 在配置内核时,我一般先用 make menuconfig 做一次全量搜索(按 / 键),找到目标驱动的路径,再决定是编进内核还是模块。这样比瞎翻快得多。

3.2.2 Makefile:编译的“执行者”

Kconfig 定义完菜单,Makefile 就根据配置变量来编译。比如:

obj-$(CONFIG_SENSORS_LM75) += lm75.o

如果 CONFIG_SENSORS_LM75 被设为 y,则 lm75.o 会被编译进内核;如果设为 m,则编译成 lm75.ko 模块。

我曾经在一个项目中,因为 Kconfig 里写错了 depends on,导致驱动在菜单里死活不出现。排查了半天,最后发现是依赖的子系统名字拼错了。嗯,这种低级错误,犯过一次就记住了。

3.3 内核配置裁剪与编译:从通用到定制

拿到一个通用内核源码,直接编译出来的镜像可能几十兆,对于嵌入式设备来说,太大了。我们需要裁剪。

3.3.1 配置裁剪的步骤

  1. 选择默认配置: make ARCH=arm64 defconfig 或 make ARCH=arm64 xxx_defconfig(针对特定板子)。
  2. 手动微调: make ARCH=arm64 menuconfig,进入图形界面,去掉不需要的功能(比如蓝牙、Wi-Fi、多余的文件系统)。
  3. 保存配置: 退出时保存为 .config 文件。
  4. 备份配置: 把 .config 复制一份,命名为 my_defconfig,方便以后复用。

避坑指南: 我曾经在裁剪时,不小心把 CONFIG_DEVTMPFS 去掉了,结果系统启动后 /dev 目录是空的,设备节点一个都没有。折腾了一天才发现是这个问题。所以,裁剪时一定要保留基础的文件系统和设备管理选项。

3.3.2 编译内核

配置好了,编译就简单了:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j4

-j4 表示用4个线程并行编译,能快不少。编译完成后,会在 arch/arm64/boot/ 下生成 Image 或 zImage,这就是我们要的内核镜像。

3.4 内核设备树(DTS)语法与实战

设备树,是嵌入式Linux移植中最核心、也最容易出错的部分。它用来描述硬件资源,告诉内核:你的板子上有哪些外设、它们挂在哪里、中断号是多少。

3.4.1 DTS基础语法

一个简单的设备树节点长这样:

/dts-v1/;

/ {
    model = "MyBoard";
    compatible = "mycompany,myboard";

    chosen {
        stdout-path = &uart0;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  // 512MB内存
    };

    uart0: serial@10000000 {
        compatible = "ns16550";
        reg = <0x10000000 0x1000>;
        interrupts = <0 33 4>;
        clock-frequency = <24000000>;
    };
};

关键点解释:

  • /dts-v1/;:版本声明,必须放在第一行。
  • /:根节点,整个板子的描述从这里开始。
  • compatible:匹配字符串,内核通过它找到对应的驱动。
  • reg:地址和大小,格式是 <起始地址 长度>。
  • interrupts:中断描述,格式取决于中断控制器。

3.4.2 实战:添加一个I2C设备

假设你的板子上有一个I2C温度传感器(LM75),挂在I2C总线地址0x48上。你需要在设备树里这样描述:

&i2c0 {
    status = "okay";
    clock-frequency = <100000>;

    lm75@48 {
        compatible = "lm75";
        reg = <0x48>;
    };
};

这里 &i2c0 是引用I2C控制器的标签,status = "okay" 表示启用这个控制器。子节点 lm75@48 就是我们的传感器,reg = <0x48> 是它的I2C地址。

我的经验: 写设备树时,最容易犯的错误是地址写错或中断号不对。我一般会先看芯片的参考手册,确认外设的基地址和中断号,再对照内核里已有的类似板子的DTS文件来写。抄作业,但别抄错。

3.4.3 编译设备树

设备树源文件(.dts)需要编译成二进制(.dtb)才能被内核使用:

make ARCH=arm64 dtbs

或者单独编译:

dtc -I dts -O dtb -o myboard.dtb myboard.dts

生成的 .dtb 文件,要么和内核镜像一起打包,要么由bootloader单独加载。

3.5 知识结构图:内核配置与设备树流程

下面这张图,把从源码到最终启动的整个流程串起来了。你可以对照着看,每一步都对应我们刚才讲的内容。

内核配置与设备树编译流程 内核配置与设备树编译流程 Linux内核源码 设备树源文件(.dts) Kconfig/Makefile make menuconfig 配置裁剪 生成 .config 配置文件 make ARCH=arm64 编译内核 make dtbs 编译设备树 内核镜像 (Image/zImage) 设备树二进制 (.dtb) Bootloader加载 -> 内核启动 关键提示 • 裁剪时保留基础驱动 • DTS地址核对参考手册 • 备份 .config 到 defconfig

3.6 小结与避坑清单

这一章的内容,说白了就是三件事:看懂目录结构、学会配置裁剪、写对设备树。我最后给你列一个避坑清单,都是我在项目里踩过的雷:

  • 配置丢失: 每次修改配置后,记得 make savedefconfig 保存,否则下次 make distclean 就没了。
  • DTS编译错误: 语法错误最常见的是少分号或括号不匹配。用 dtc -I dts -O dtb -o /dev/null myboard.dts 只检查语法,不生成文件,快速排错。
  • 中断号不对: 设备树里的中断号,一定要和芯片手册里的GIC中断号对应。ARM64一般用 GIC_SPI 和 GIC_PPI 宏,别搞混。
  • 地址重叠: 两个外设的 reg 地址不能重叠,否则内核会报错。用 cat /proc/iomem 可以查看已占用的地址范围。