第三章:Linux内核源码结构——内核源码目录树、Kconfig与Makefile、Exynos平台代码位置

各位同学,大家好。这一章我们聊聊Linux内核源码的结构。说实话,很多初学者一看到内核源码树就头大——几万个文件,几十个目录,从哪看起?我当年刚接触内核时也是这样,对着linux-3.5的目录发呆了好久。

但别怕。内核源码虽然庞大,但它的组织逻辑其实很清晰。说白了,就是一套「分层 + 模块化」的架构。你只要抓住几个关键目录,就能快速定位到你关心的代码。

3.1 内核源码目录树:从根目录说起

我们先从顶层看起。你下载一份内核源码,解压后第一眼看到的就是这些目录:

linux-5.10/
├── arch/          # 体系结构相关代码
├── block/         # 块设备子系统
├── crypto/        # 加密API
├── Documentation/ # 内核文档
├── drivers/       # 设备驱动
├── fs/            # 文件系统
├── include/       # 头文件
├── init/          # 内核初始化
├── ipc/           # 进程间通信
├── kernel/        # 内核核心
├── lib/           # 库函数
├── mm/            # 内存管理
├── net/           # 网络协议栈
├── scripts/       # 编译脚本
├── security/      # 安全模块
├── sound/         # 音频子系统
├── tools/         # 用户空间工具
└── usr/           # initramfs相关

嗯,这里我要强调一下:arch/ 和 drivers/ 这两个目录,是我们嵌入式驱动开发人员最常打交道的。为什么呢?

  • arch/:存放与CPU架构相关的代码。ARM、x86、RISC-V、MIPS等都在这里。Exynos平台属于ARM架构,所以它的平台代码在 arch/arm/ 下。
  • drivers/:存放各种外设驱动。I2C、SPI、GPIO、DMA、时钟、电源管理等,都在这里按子系统分类。
  • include/:内核头文件。你写驱动时经常要包含 #include <linux/xxx.h>,这些头文件就来自这里。

我个人习惯是,拿到一个新内核版本,先看 arch/drivers/ 这两个目录。其他的如 kernel/mm/fs/ 等,除非你要做内核核心开发,否则平时接触不多。

小技巧: 如果你只想看某个特定SoC的代码,比如Exynos 4412,可以直接在源码根目录下用 find 命令搜索:find . -name "*exynos*" -type f。这样能快速定位所有相关文件。

3.2 Kconfig与Makefile:内核的「配置-编译」体系

内核的配置和编译,靠的就是 KconfigMakefile 这两兄弟。我刚开始写驱动时,经常搞不清它们的关系。后来踩过几次坑,才真正理解。

Kconfig 负责「配置」——它定义了哪些功能可以开关,哪些驱动可以选。你运行 make menuconfig 时看到的菜单,就是由各个目录下的 Kconfig 文件生成的。

Makefile 负责「编译」——它根据 Kconfig 的配置结果(保存在 .config 文件中),决定哪些文件要编译进内核,哪些编译成模块。

举个例子,假设我们要在 Exynos 平台上添加一个 GPIO 按键驱动。Kconfig 和 Makefile 的写法是这样的:

Kconfig 示例

# drivers/input/keyboard/Kconfig
config KEYBOARD_EXYNOS_GPIO
    tristate "Exynos GPIO Keypad support"
    depends on ARCH_EXYNOS
    help
      Say Y here to enable GPIO-based keypad support for Exynos SoCs.
      This driver supports up to 8x8 matrix keypad.

这里 tristate 表示该驱动有三种状态:Y(编译进内核)、M(编译成模块)、N(不编译)。depends on ARCH_EXYNOS 表示这个驱动只对 Exynos 平台可见——你选别的平台,这个选项根本不会出现。

Makefile 示例

# drivers/input/keyboard/Makefile
obj-$(CONFIG_KEYBOARD_EXYNOS_GPIO) += exynos_gpio_keys.o

这一行的意思是:如果 CONFIG_KEYBOARD_EXYNOS_GPIO 被配置为 y,就把 exynos_gpio_keys.o 编译进内核;如果配置为 m,就编译成模块 exynos_gpio_keys.ko

避坑指南: 我曾经犯过一个低级错误——在 Kconfig 里写对了依赖,但 Makefile 里忘了加 obj-$(CONFIG_XXX) 这一行。结果配置菜单里能看到选项,编译时却找不到源文件。嗯,这种问题排查起来还挺费时间的。

3.3 Exynos平台代码位置:arch/arm/mach-exynos

好了,终于到了我们最关心的部分。Exynos 平台的代码主要分布在两个地方:

目录 内容 说明
arch/arm/mach-exynos/ 平台初始化代码 时钟、中断、GPIO、DMA等外设的初始化
arch/arm/boot/dts/ 设备树文件 硬件描述,如 exynos4412.dtsi
drivers/ 下各子系统 具体外设驱动 drivers/clk/samsung/ 下的时钟驱动

我们重点看 arch/arm/mach-exynos/ 这个目录。它里面通常包含以下文件:

arch/arm/mach-exynos/
├── common.c          # 通用初始化代码
├── pm.c              # 电源管理
├── sleep.S           # 休眠/唤醒汇编代码
├── platsmp.c         # 多核启动
├── headsmp.S         # 多核启动汇编
├── hotplug.c         # CPU热插拔
└── Kconfig           # 平台配置选项

我记得第一次看 common.c 时,里面一堆 map_ioinit_earlyinit_irq 之类的回调函数。说白了,这些就是内核在启动过程中,针对 Exynos 平台做的「定制化初始化」。

举个例子,Exynos 4412 的时钟初始化通常在 common.c 中调用:

// arch/arm/mach-exynos/common.c
static void __init exynos_init_early(void)
{
    exynos_map_io();
    exynos_init_clocks();
    exynos_init_uarts();
}

这里 exynos_init_clocks() 会去配置 PLL、设置各外设的时钟频率。你想想看,如果时钟配错了,整个系统可能连串口都打不开——我就在调试一个 I2C 驱动时遇到过,时钟频率不对导致通信时序全乱,折腾了两天才发现是平台初始化代码里一个宏定义错了。

注意: 从 Linux 4.x 开始,内核逐渐用设备树(Device Tree)替代了 mach-exynos 中的大量板级代码。如果你用的是较新版本的内核,mach-exynos/ 目录下的内容会少很多,大部分硬件描述都移到了 .dts 文件中。但 mach-exynos/ 仍然负责平台级别的初始化,比如 SMP 启动、电源管理等。

3.4 实战:如何找到Exynos 4412的GPIO控制代码

光说不练假把式。我们来做个小实战——找到 Exynos 4412 的 GPIO 控制代码。

  1. 先找设备树arch/arm/boot/dts/exynos4412.dtsi。这里面定义了 GPIO 控制器的寄存器基地址、中断号等。
  2. 再找驱动:GPIO 驱动在 drivers/gpio/gpio-samsung.c。这个文件包含了 Exynos 全系列 SoC 的 GPIO 操作函数。
  3. 最后看平台初始化arch/arm/mach-exynos/common.c 中会调用 samsung_gpio_init() 来注册 GPIO 控制器。

你看,通过这三个步骤,就能把「硬件描述 → 驱动实现 → 平台注册」这条链路串起来。我建议你拿到一块开发板后,也按这个思路去梳理——先找设备树,再找驱动,最后看平台初始化。这样整个代码脉络就清晰了。

个人经验: 调试 Exynos 驱动时,我经常在 arch/arm/mach-exynos/drivers/ 之间来回跳转。比如发现某个外设不工作,先看设备树里寄存器地址对不对,再看驱动里操作流程对不对,最后看平台初始化有没有遗漏。这三者缺一不可。

好了,这一章的内容就到这里。下一章我们会深入 Exynos 的时钟框架,看看 PLL 是怎么配置的,以及如何为你的外设驱动提供正确的时钟。到时候我会分享一个我调试 HDMI 时钟时遇到的坑——嗯,那个故事还挺有意思的。