Linux内核源码结构:从目录树到配置系统
大家好,我是你们的老朋友。今天咱们来聊聊Linux内核源码的结构。说实话,我第一次接触内核源码时,也被那庞大的目录树吓到了。但别担心,我带你一步步拆解,你会发现其实没那么复杂。
一、源码目录树解析
先看看内核源码的顶层目录。我习惯把它分成三类:核心代码、架构相关代码、以及工具和文档。
| 目录 | 作用 | 我的经验 |
|---|---|---|
| arch/ | 架构相关代码(x86、ARM、RISC-V等) | 移植驱动时,这里是最常打交道的目录 |
| drivers/ | 设备驱动代码 | 占了源码一半以上,我经常在这里找参考 |
| fs/ | 文件系统实现 | ext4、btrfs都在这里 |
| mm/ | 内存管理 | 调优时必看 |
| net/ | 网络协议栈 | TCP/IP、socket实现 |
| kernel/ | 核心调度、进程管理 | 系统调用的入口 |
| include/ | 头文件 | 找宏定义和结构体就来这 |
| scripts/ | 编译脚本、工具 | Kconfig解析器就在这里 |
嗯,这里要注意:arch/目录下每个子目录都代表一种CPU架构。比如arch/arm/、arch/x86/。我刚开始做驱动移植时,总以为驱动代码是通用的,结果在ARM平台上编译报错,后来才发现是没加架构相关的头文件。
二、关键子系统
内核有四大核心子系统,说白了就是内核的「四根柱子」。我一个个说。
1. 进程调度
进程调度负责决定哪个进程能跑在CPU上。代码在kernel/sched/目录下。我个人习惯先看core.c和fair.c,因为CFS(完全公平调度器)是默认调度器。
举个例子,当你写一个实时驱动时,可能需要把进程绑定到特定CPU上。这时候你会用到sched_setaffinity()。我在项目中遇到过,一个音频驱动因为没绑定CPU,导致中断响应延迟,声音断断续续。后来加了CPU亲和性设置,问题就解决了。
2. 内存管理
内存管理在mm/目录。核心是伙伴系统和slab分配器。我建议你重点关注mm/page_alloc.c和mm/slab.c。
为什么会这样?因为驱动开发中最常见的bug就是内存泄漏。我曾经调试过一个网卡驱动,每次收包都分配内存但不释放,跑了一天系统就OOM了。排查时发现是kmalloc和kfree没配对。
避坑指南:驱动中申请内存,一定要用GFP_KERNEL还是GFP_ATOMIC?在进程上下文中用GFP_KERNEL,在中断上下文中只能用GFP_ATOMIC。我曾经在中断处理函数里用了GFP_KERNEL,结果系统直接死锁了。
3. 文件系统
文件系统在fs/目录。VFS(虚拟文件系统)是核心抽象层。你写的驱动如果要暴露为设备文件,就得实现file_operations结构体。
我记得有一次,一个同事写了个字符设备驱动,open和release都正常,但read总是返回0。排查了半天,发现是file_operations里的read函数指针没赋值。嗯,这种低级错误其实很容易犯。
4. 网络栈
网络栈在net/目录。从socket层到TCP/UDP层,再到IP层和设备驱动层。如果你写网卡驱动,重点关注net/core/和net/ethernet/。
你想想看,一个数据包从网卡到应用层,要经过多少层?我数过,至少5层。每一层都有钩子函数,比如netfilter就在IP层做包过滤。我在做防火墙驱动时,就利用NF_HOOK宏来拦截数据包。
三、内核配置系统:Kconfig和Makefile
这部分是移植驱动的关键。说白了,你要让内核知道「有这个驱动存在」,并且「用户可以选择编译它」。
Kconfig:配置选项的定义
Kconfig文件定义了配置选项。比如你要添加一个驱动,就在对应目录的Kconfig里加一段:
config MY_DRIVER
tristate "My Awesome Driver"
depends on ARM
help
This is a sample driver for teaching.
Say Y here to compile it into kernel.
Say M to compile it as a module.
这里tristate表示三种状态:Y(编译进内核)、M(编译为模块)、N(不编译)。depends on ARM表示这个驱动只对ARM架构有效。
小技巧:我习惯在Kconfig里加default n,这样不会默认开启,避免编译时出问题。等测试通过了再改成default y。
Makefile:编译规则
Makefile告诉内核怎么编译你的驱动。比如:
obj-$(CONFIG_MY_DRIVER) += my_driver.o
如果CONFIG_MY_DRIVER被设置为y或m,就会编译my_driver.c。如果设置为n,就不编译。
嗯,这里有个坑:如果你的驱动由多个源文件组成,比如a.c和b.c,要写成:
my_driver-objs := a.o b.o
obj-$(CONFIG_MY_DRIVER) += my_driver.o
我曾经在这里卡了半天,因为只写了obj-y += a.o b.o,结果编译出来只有a.o,b.o根本没链接进去。
四、知识体系总览
下面这张图是我自己画的,帮你理清内核源码的整体结构。你看,从顶层目录到子系统,再到配置系统,是一条清晰的链路。
你看,从源码根目录出发,左边是四大子系统,右边是配置系统。配置系统通过Kconfig和Makefile,最终生成.config文件和内核模块。这个流程,说白了就是「配置-编译-生成」三步走。
五、实战建议
最后,我总结几条实战经验:
- 先看Kconfig:移植驱动前,先看看目标架构的Kconfig里有没有依赖选项。我遇到过,一个驱动依赖
CONFIG_PCI,但目标平台没开启PCI支持,编译直接报错。 - Makefile的obj-y和obj-m:调试阶段用
obj-m编译成模块,方便动态加载。稳定后再改成obj-y编译进内核。 - 多用
make menuconfig:图形化配置界面比手动改.config靠谱。我曾经手动改.config,结果漏了一个依赖选项,编译出来内核启动就panic。 - 关注
arch/目录:不同架构的启动流程、中断处理、内存映射都不一样。移植驱动时,一定要看对应架构的arch/xxx/include/头文件。
警告:不要直接修改include/linux/下的通用头文件!我见过有人为了省事,在include/linux/sched.h里加了自己的结构体,结果升级内核时全乱了。正确的做法是在驱动目录里定义自己的头文件。
好了,这一章就到这里。内核源码结构是基础,但也是最重要的。你把这些目录和子系统搞清楚了,后面写驱动、做移植就会顺手很多。
公众号:蓝海资料掘金营,微信deep3321