内核源码结构解析:从顶层目录到核心代码

拿到一份 Linux 内核源码,第一反应是什么?我当年第一次解压完,看着几百兆的文件目录,说实话有点懵。但别怕,内核的目录结构其实很有章法。今天我们就把它拆开来看,搞清楚每个目录是干什么的。

顶层目录结构:一眼看穿内核的骨架

先看根目录下都有什么。我习惯用 ls -l 先扫一遍,重点关注这几个:

目录/文件 作用 我的备注
arch/ 体系结构相关代码 移植内核时主要改这里
drivers/ 设备驱动 最大目录,占一半以上代码量
init/ 内核初始化 启动入口,我调过很多次
kernel/ 核心子系统 调度、锁、信号量都在这里
mm/ 内存管理 页分配、SLAB、VMA
fs/ 文件系统 VFS 层 + 具体文件系统
net/ 网络协议栈 TCP/IP 实现
include/ 头文件 全局 API 定义
scripts/ 编译脚本 Kconfig、Makefile 辅助
Documentation/ 文档 有些过时,但值得翻

嗯,这里要注意:arch/ 和 drivers/ 是裁剪的重点。你想想看,嵌入式设备不需要 x86 的 ACPI 驱动,也不需要服务器上的 RAID 卡驱动。把这些去掉,内核体积能小一大截。

我的习惯:每次拿到新板子,先跑一遍 make menuconfig,然后对照 arch/ 和 drivers/ 目录,把不需要的模块一个个关掉。别偷懒,这一步省不了。

arch/ 目录详解:体系结构的灵魂

arch/ 下面按 CPU 架构分目录。我做过 ARM 和 RISC-V 的项目,最常打交道的就是 arch/arm/ 和 arch/riscv/。来看看里面有什么:

arch/arm/
├── boot/          # 启动镜像、压缩、DTS
├── configs/       # 默认配置文件
├── include/       # 架构相关头文件
├── kernel/        # 架构核心代码(中断、时钟、SMP)
├── lib/           # 优化过的库函数(memcpy、strlen)
├── mach-*/        # 具体 SoC 平台代码
├── mm/            # 内存管理(页表、TLB)
└── Kconfig        # 架构配置选项

我个人觉得,mach-* 目录是移植的关键。每个 mach- 对应一个 SoC 系列,比如 mach-s3c24xx、mach-omap2。里面放的是板级初始化代码、GPIO 配置、时钟设置这些硬件相关的东西。

举个例子,我在调试一块 i.MX6 板子时,发现串口不工作。查了半天,最后定位到 arch/arm/mach-imx/ 下的时钟初始化代码里,某个 PLL 配置不对。这种问题,不熟悉 arch/ 结构根本无从下手。

避坑指南:我曾经在裁剪内核时,把 arch/arm/mach-* 里某个不用的平台目录整个删了。结果编译报错,因为 Kconfig 里还有依赖。记住:删目录前先检查 Kconfig 和 Makefile,别蛮干。

drivers/ 目录概览:驱动的大杂烩

drivers/ 是内核里最大的目录,没有之一。我统计过,5.10 版本里 drivers/ 占了将近 60% 的代码量。里面按设备类型分子目录:

子目录 包含内容 裁剪建议
tty/ 串口、控制台 保留,调试用
gpio/ GPIO 控制器 按需保留
i2c/ I2C 总线 传感器常用,别删
spi/ SPI 总线 显示屏、ADC 常用
mmc/ SD/MMC 卡 文件系统挂载需要
net/ 网卡驱动 按硬件型号选
usb/ USB 主机/设备 嵌入式常裁剪掉
video/ 显示驱动 有屏幕才保留
sound/ 音频驱动 按需
input/ 输入设备 触摸屏、按键

说白了,裁剪驱动就是做减法。你的板子上没有 USB WiFi 模块,那把 drivers/net/wireless/ 里那些 Broadcom、Realtek 的驱动全关掉。没有音频需求,drivers/sound/ 直接不编译。

我记得有一次帮客户优化启动时间,发现内核加载了 30 多个驱动,但实际硬件只有 5 个外设。剩下的全是内核自带的通用驱动,板子上根本没那些芯片。关掉之后,内核从 4.2MB 缩到 1.8MB,启动快了将近 1 秒。

关键思路:裁剪不是盲目删代码,而是理解你的硬件需要什么。把 drivers/ 想象成一个工具箱,你只需要拿出自己用的那几把扳手,其他的放回去。

init/ 与 kernel/ 目录:内核的起点和心脏

这两个目录虽然代码量不大,但地位特殊。init/ 是内核的入口,kernel/ 是核心调度器所在。

init/ 目录:一切从这里开始

init/
├── main.c          # 内核 C 语言入口(start_kernel)
├── initramfs.c     # 初始 RAM 文件系统
├── Kconfig         # 初始化相关配置
├── Makefile        # 编译规则
└── version.c       # 版本字符串

init/main.c 里的 start_kernel() 是内核启动的第一个 C 函数。我调试启动流程时,经常在这里加 printk 打印,看内核执行到哪一步挂掉的。嗯,这招虽然土,但很管用。

start_kernel 的执行顺序大致是:

  1. 关中断、设置栈指针
  2. 初始化内存管理(mm_init)
  3. 初始化调度器(sched_init)
  4. 初始化中断(init_IRQ)
  5. 初始化定时器(time_init)
  6. 初始化控制台(console_init)
  7. 创建 init 进程(kernel_thread)
  8. 启动用户空间(run_init_process)

你想想看,如果内核在 console_init 之前就崩溃了,你连打印都看不到。这时候只能靠 JTAG 或者早期串口输出来定位。我踩过这个坑,后来学乖了,在 arch/arm/kernel/ 里提前初始化串口。

kernel/ 目录:内核的核心机制

kernel/ 下面放的是与体系结构无关的核心代码:

文件/子目录 功能
sched/ 进程调度(CFS、RT)
locking/ 锁机制(spinlock、mutex)
irq/ 中断子系统
time/ 定时器、时间管理
power/ 电源管理(suspend、resume)
printk/ 内核日志
module.c 模块加载/卸载
kallsyms.c 符号表
panic.c 内核崩溃处理

这里我特别想提一下 kernel/panic.c。调试的时候,内核 panic 了怎么办?默认行为是打印一堆信息然后死在那。但嵌入式设备往往没有显示器,你只能靠串口看日志。我建议在裁剪时保留 CONFIG_PANIC_TIMEOUT,设置一个超时重启,别让设备死在那没人管。

调试技巧:在 kernel/printk/printk.c 里可以调整日志级别。我习惯把 console_loglevel 设成 15,这样所有 printk 都能输出到串口,方便排查早期启动问题。

知识结构图:内核源码目录关系

下面这张图帮你理清各个目录之间的关系:

内核源码目录结构关系图 Linux 内核源码目录结构关系 Linux 内核源码根目录 arch/ drivers/ init/ kernel/ arm/ riscv/ x86/ mips/ tty/ gpio/ i2c/ spi/ main.c (start_kernel) initramfs.c sched/ irq/ locking/ printk/ 裁剪重点:arch/ 和 drivers/ 占代码量最大,是裁剪的主战场 init/ 和 kernel/ 代码量小但关键,一般不动

这张图把内核源码的目录关系画清楚了。从上往下看,根目录分出四大块:arch/ 管硬件体系、drivers/ 管外设驱动、init/ 管启动流程、kernel/ 管核心机制。裁剪的时候,你主要在前两个目录里做减法。

总结一下:理解内核源码结构,是裁剪和调试的第一步。arch/ 决定你的内核能在什么 CPU 上跑,drivers/ 决定你的内核能驱动什么外设,init/ 决定内核怎么启动,kernel/ 决定内核怎么运行。把这四个目录搞明白,后面裁剪和调试就有了地图。