4. Linux内核源码结构:从目录到编译的完整链路

做驱动移植,第一件事不是写代码,而是搞懂内核源码长什么样。我见过不少新手,拿到内核源码包直接解压就开始改,结果连Kconfig和Makefile在哪都找不到。嗯,今天我们就从源码目录开始,把内核的配置和编译体系彻底捋一遍。

4.1 内核源码顶层目录:别被吓到

Linux内核源码几万个文件,乍一看确实头大。但你想想看,真正需要我们频繁打交道的目录其实就那么几个。我习惯把顶层目录分成三类:核心代码、驱动相关、构建工具。

目录 作用 驱动开发者关注度
arch/ 体系结构相关代码(ARM、x86、RISC-V等) ⭐⭐⭐⭐⭐
drivers/ 所有外设驱动(I2C、SPI、GPIO、USB…) ⭐⭐⭐⭐⭐
include/ 内核头文件,驱动API定义 ⭐⭐⭐⭐
kernel/ 核心调度、进程管理 ⭐⭐
mm/ 内存管理 ⭐
fs/ 文件系统实现 ⭐⭐
net/ 网络协议栈 ⭐⭐
scripts/ 编译脚本、配置工具 ⭐⭐⭐
Documentation/ 内核文档(建议常翻) ⭐⭐⭐
我的习惯:拿到一个新板子,先看 arch/arm/boot/dts/ 下的设备树文件,再看 drivers/ 下对应外设的驱动目录。这两个地方基本决定了你80%的工作量。

4.2 Kconfig与Makefile:内核的"菜单"和"菜谱"

Kconfig和Makefile是内核构建系统的两大支柱。说白了,Kconfig决定"要不要",Makefile决定"怎么编"。

4.2.1 Kconfig:配置菜单的语法

每个驱动目录下都有一个Kconfig文件,它定义了该驱动的配置选项。举个例子,我们看一个典型的GPIO驱动Kconfig片段:

config GPIO_PL061
    tristate "ARM PrimeCell PL061 GPIO support"
    depends on ARM_AMBA
    help
      Say yes here to support the ARM PrimeCell PL061 GPIO device.
      This driver can also be built as a module.

这里有几个关键字段:

  • tristate:三态选择——Y(编入内核)、M(编译为模块)、N(不编译)。驱动开发中,我建议新手先用M,调试方便。
  • depends on:依赖关系。如果依赖不满足,这个选项不会出现。
  • select:反向选择。比如某个驱动选中后,自动选上它依赖的另一个驱动。
我曾经踩过的坑:有一次移植I2C触摸屏驱动,Kconfig里写了 depends on I2C,但我忘了在配置内核时先打开I2C子系统。结果折腾了半天,菜单里死活看不到我的驱动选项。记住:依赖链必须完整。

4.2.2 Makefile:编译的"菜谱"

同目录下的Makefile告诉内核怎么编译这个驱动。还是以PL061为例:

obj-$(CONFIG_GPIO_PL061) += gpio-pl061.o

这行代码的意思是:如果 CONFIG_GPIO_PL061 被配置为Y或M,就把 gpio-pl061.c 编译进去。注意,obj-y 表示编入内核,obj-m 表示编译为模块,obj-n 表示不编译。

多文件驱动怎么办?比如一个驱动由 a.c 和 b.c 组成:

obj-$(CONFIG_MY_DRIVER) += my_driver.o
my_driver-objs := a.o b.o

嗯,这里要注意:my_driver-objs 这种写法在较新内核中已经逐渐被 my_driver-y 替代,但原理一样。

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

配置系统的工作流程其实很简单:

  1. 读取各目录下的Kconfig文件,生成配置菜单
  2. 用户通过 make menuconfig 等工具选择配置
  3. 保存为 .config 文件
  4. 编译时,.config 被转换为 include/generated/autoconf.h,供C代码使用

常用的配置命令:

命令 适用场景 特点
make menuconfig 最常用,字符终端 依赖ncurses库,交互友好
make xconfig 图形桌面环境 基于Qt,界面更直观
make defconfig 使用默认配置 快速生成,适合开发板
make savedefconfig 精简.config 只保留非默认选项,方便版本管理
实战技巧:我习惯先 make defconfig 生成基础配置,再用 make menuconfig 手动添加需要的驱动。最后用 make savedefconfig 生成精简配置,放到代码仓库里。这样既干净,又方便同事复现。

4.4 内核编译流程:从源码到镜像

内核编译不是简单的 gcc *.c,它有一套完整的流水线。我画了一张图,帮你理清整个过程:

Linux内核编译流程 1. 配置阶段 make menuconfig → .config 2. 依赖处理 make oldconfig / make silentoldconfig 3. 编译阶段 make → 编译C文件为.o 4. 链接阶段 链接为vmlinux / .ko模块 5. 打包生成 make zImage / uImage / dtbs 关键输出文件 • vmlinux:未压缩内核 • zImage:压缩内核镜像 • .ko:可加载内核模块 • .dtb:设备树二进制

整个流程中,我个人觉得最容易出问题的是依赖处理这一步。比如你改了Kconfig,但没执行 make oldconfig,新的配置选项可能不会被正确识别。我习惯每次修改Kconfig后,都跑一遍 make olddefconfig,确保配置一致性。

4.5 实战:添加一个自定义驱动到内核

光说不练假把式。我们走一遍完整流程,把一个小驱动加到内核里。

第一步:创建驱动文件

假设我们在 drivers/char/ 下新建一个 hello_drv.c:

#include <linux/module.h>
#include <linux/kernel.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, kernel!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel!\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

第二步:修改Kconfig

在 drivers/char/Kconfig 末尾添加:

config HELLO_DRV
    tristate "Hello World Driver"
    default n
    help
      A simple hello world driver for demonstration.

第三步:修改Makefile

在 drivers/char/Makefile 末尾添加:

obj-$(CONFIG_HELLO_DRV) += hello_drv.o

第四步:配置并编译

make menuconfig    # 找到 Device Drivers → Character devices → Hello World Driver,选M
make modules       # 编译模块
make modules_install  # 安装模块到 /lib/modules/
小提示:如果你只想编译单个模块,可以用 make M=drivers/char,这样只编译 drivers/char 目录下的代码,省时间。我调试驱动时经常这么干。

4.6 裁剪的艺术:只留你需要的

嵌入式设备资源有限,内核裁剪是必修课。说白了,就是把不需要的功能统统关掉。我总结了几条原则:

  • 按需配置:板子上没有的外设,驱动一律不选。比如你的板子没有PCIe,就把PCIe支持关掉。
  • 模块化:能用模块的就别编入内核。这样启动快,调试也方便。
  • 关闭调试选项:CONFIG_DEBUG_KERNEL 这类选项在发布版本中一定要关,它们会显著增加内核体积和运行开销。
  • 精简文件系统:不需要的文件系统类型(比如NTFS、XFS)全部去掉。
我曾经犯过的错:有一次给一个只有4MB Flash的设备裁剪内核,我关了所有能关的选项,结果启动时发现网卡驱动没编进去——因为我忘了 depends on 链上的一个中间选项。所以裁剪后一定要做一次全功能验证,别省这一步。