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:反向选择。比如某个驱动选中后,自动选上它依赖的另一个驱动。
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
配置系统的工作流程其实很简单:
- 读取各目录下的Kconfig文件,生成配置菜单
- 用户通过
make menuconfig等工具选择配置 - 保存为
.config文件 - 编译时,
.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,它有一套完整的流水线。我画了一张图,帮你理清整个过程:
整个流程中,我个人觉得最容易出问题的是依赖处理这一步。比如你改了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)全部去掉。
depends on 链上的一个中间选项。所以裁剪后一定要做一次全功能验证,别省这一步。