内核源码结构解析:从顶层目录到核心代码
拿到一份 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/ 结构根本无从下手。
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 秒。
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 的执行顺序大致是:
- 关中断、设置栈指针
- 初始化内存管理(mm_init)
- 初始化调度器(sched_init)
- 初始化中断(init_IRQ)
- 初始化定时器(time_init)
- 初始化控制台(console_init)
- 创建 init 进程(kernel_thread)
- 启动用户空间(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 都能输出到串口,方便排查早期启动问题。
知识结构图:内核源码目录关系
下面这张图帮你理清各个目录之间的关系:
这张图把内核源码的目录关系画清楚了。从上往下看,根目录分出四大块:arch/ 管硬件体系、drivers/ 管外设驱动、init/ 管启动流程、kernel/ 管核心机制。裁剪的时候,你主要在前两个目录里做减法。