1. 为什么我要开这个 Linux 内核专栏
搞了十多年底层开发,从裸机驱动到内核模块,从设备树到内存管理,踩过的坑比写过的代码行数还多。身边总有朋友问我:内核这东西到底该怎么入门?市面上的资料要么是教科书式的理论堆砌,要么是零散的博客片段,看完之后脑子里全是碎片,串不起来。这个专栏就是来解决这个问题的——它不是一本电子书,也不是官方文档的翻译,而是一个一线从业者把自己反复啃内核源码、调驱动、排查panic的实战经验,按知识依赖关系重新组织出来的学习路径。
整个专栏的核心目标很明确:帮你建立一套完整的Linux内核知识体系,从进程调度到内存管理,从文件系统到设备驱动,从中断处理到并发同步,每一块都讲清楚“它为什么这么设计”“代码在哪里”“实际调试时怎么下手”。适合谁看?如果你是刚接触内核的嵌入式工程师、想从应用层下沉到系统层的后端开发、或者准备面试内核相关岗位的求职者,这个专栏就是为你写的。如果你已经能独立写内核模块,但总觉得知识点是散的,那这里的内容也能帮你把脉络理清楚。
我写这个总目录,不只是列个清单,而是要把每个专题之间的依赖关系、学习顺序、以及每个阶段该动手做什么实验,全部交代清楚。你可以把它当成一张地图,按图索骥,少走弯路。
2. 专栏整体设计与学习路径拆解
2.1 为什么按“从使用到原理再到实战”三层递进
很多内核教程一上来就讲task_struct结构体,讲调度算法,结果读者连怎么编译一个内核模块都没搞明白,直接劝退。我设计的路径是三层递进:第一层是“会用”,能编译模块、能看proc文件、能用ftrace抓函数调用;第二层是“懂原理”,理解每个子系统的设计动机和核心数据结构;第三层是“能实战”,自己写驱动、改调度参数、排查内存泄漏。这个顺序不是拍脑袋定的,而是根据认知负荷理论来的——先建立感性认识,再深入抽象逻辑,最后通过动手固化理解。
具体来说,专栏会分成六个大模块:基础环境与工具链、进程管理与调度、内存管理、文件系统与IO、设备驱动与中断、并发与同步。每个模块内部再按“概念引入→核心数据结构→关键代码路径→调试手段→实战案例”五步展开。这样安排的好处是,你学完一个模块就能立刻上手做实验,而不是攒了一堆理论不知道往哪用。
2.2 模块之间的依赖关系与学习顺序
这六个模块不是孤立的。进程管理是入口,因为内核最核心的任务就是调度进程;内存管理是进程管理的基础,没有虚拟内存就没法谈进程隔离;文件系统和设备驱动是进程与外部世界交互的桥梁;并发与同步则贯穿所有模块,因为内核本身就是个并发环境。所以我的建议顺序是:先学基础环境和工具链,然后进程管理,接着内存管理,再文件系统,然后设备驱动,最后并发与同步。但并发与同步其实可以穿插在前面每个模块里讲,因为自旋锁、信号量这些东西在每个子系统里都会出现。
注意:不要试图跳过基础环境直接看内存管理,因为你连内核编译和模块加载都没跑通,看代码就是看天书。我见过太多人卡在这一步。
2.3 每个专题的篇幅分配与深度预期
整个专栏预计二十篇左右,每篇控制在五千到八千字,配三到五个可复现的实验。基础环境两篇,进程管理四篇,内存管理四篇,文件系统三篇,设备驱动四篇,并发与同步三篇。每篇的深度预期是:读完能给别人讲清楚这个子系统的核心机制,能独立完成配套实验,遇到常见问题能自己排查。不追求覆盖所有边角料,但核心路径必须讲透。
3. 核心模块内容详解与实操要点
3.1 基础环境与工具链:别急着看代码,先把家伙什备齐
这个模块包含两篇:内核源码获取与编译、调试工具链搭建。很多人觉得编译内核是运维的事,跟内核开发没关系,这个想法大错特错。你如果不自己编译一次内核,就永远不知道make menuconfig里那些选项对应哪些代码,也不知道vmlinux和bzImage的区别。我的建议是找一个稳定版本,比如5.15或者6.1,在虚拟机里完整编译一遍,开启CONFIG_DEBUG_INFO和CONFIG_GDB_SCRIPTS,为后续调试做准备。
调试工具链这块,重点讲三个东西:printk和dynamic_debug、ftrace、kprobe。printk是最原始的调试手段,但很多人不知道pr_debug和dev_dbg可以通过dynamic_debug动态开关,不用重新编译内核。ftrace能抓函数调用图和中断延迟,kprobe能在任意指令地址插桩。这三个工具组合起来,基本能解决百分之八十的调试场景。
实操心得:编译内核时用
make -j$(nproc)能大幅缩短时间,但第一次编译建议先make defconfig再make menuconfig微调,不要直接用发行版配置,否则模块太多编译慢且干扰学习。
3.2 进程管理与调度:理解内核的“大脑”
进程管理是内核最核心的部分。这个模块会从task_struct讲起,但不会一上来就贴结构体定义,而是先问你一个问题:内核怎么表示一个进程?为什么需要thread_info和task_struct分离?然后引出current宏的实现,再讲进程创建fork和clone的区别,接着讲调度器的发展历史——从O(n)到O(1)再到CFS,每个调度器解决了什么问题。最后讲上下文切换的底层实现,包括switch_to宏和栈切换。
调度这块,CFS是重点。我会详细讲vruntime的计算、红黑树的使用、sched_entity和cfs_rq的关系。然后讲调度组和带宽控制,这部分在容器场景下特别重要。实验部分会让你写一个内核模块,遍历所有进程并打印它们的vruntime,直观感受调度器的行为。
注意:遍历进程列表必须用
for_each_process宏,并且要加RCU读锁,否则会panic。这个坑我踩过不止一次。
3.3 内存管理:从页表到slab分配器
内存管理是内核里最复杂也最迷人的部分。这个模块从物理内存管理讲起,讲memblock和buddy分配器,然后讲虚拟内存和页表,包括四级页表PGD/PUD/PMD/PTE的遍历过程。接着讲vmalloc和kmalloc的区别,为什么kmalloc有大小限制,vmalloc为什么慢。然后讲slab分配器和kmem_cache,以及kmalloc底层的slab实现。
页回收和交换是另一个重点。我会讲LRU链表、kswapd内核线程、直接回收和内存压缩。实验部分会让你写一个模块,统计当前系统的slab使用情况,并尝试创建一个自定义的kmem_cache。
实操心得:用
/proc/meminfo和/proc/slabinfo观察内存状态时,注意Slab和SReclaimable的区别,前者包含不可回收的slab,后者是可回收的。排查内存泄漏时,先看SUnreclaim是否持续增长。
3.4 文件系统与IO:VFS是抽象的艺术
文件系统模块从VFS讲起,讲super_block、inode、dentry、file四个核心对象的关系。然后讲路径查找path_lookup和dentry缓存。接着讲具体文件系统,以ext4为例讲日志和延迟分配,以proc和sysfs为例讲伪文件系统的实现。最后讲块IO层,包括bio结构、IO调度器和blk-mq多队列。
实验部分会让你写一个简单的字符设备驱动,实现open/read/write/release,并注册到/dev下。然后写一个proc文件,通过seq_file接口输出内核数据。
注意:写字符设备驱动时,
copy_to_user和copy_from_user必须检查返回值,否则用户态传个非法指针进来,内核直接oops。
3.5 设备驱动与中断:从设备树到中断处理
设备驱动模块从设备树讲起,讲compatible属性、reg属性、中断控制器的interrupt-parent和interrupts。然后讲平台设备驱动模型,platform_driver和platform_device的匹配过程。接着讲字符设备和杂项设备,以及file_operations的各个回调。中断部分讲上半部和下半部,request_irq、threaded_irq、软中断、tasklet和工作队列的区别与使用场景。
实验部分会让你写一个平台设备驱动,通过设备树匹配,申请GPIO和中断,实现一个按键中断处理程序,并用devm_系列函数管理资源。
实操心得:用
devm_request_irq代替request_irq能自动释放中断,避免卸载模块时忘记free_irq导致中断风暴。这个习惯能省很多事。
3.6 并发与同步:内核里的锁与无锁编程
并发与同步模块讲原子操作、自旋锁、读写锁、顺序锁、RCU、信号量、互斥体、完成量。重点讲每种锁的适用场景和死锁风险。比如自旋锁不能睡眠,所以在中断上下文只能用自旋锁;信号量可以睡眠,所以只能在进程上下文用。RCU适合读多写少的场景,但写者需要等待宽限期。
实验部分会让你写一个模块,创建多个内核线程竞争同一个共享变量,分别用自旋锁和互斥体保护,观察性能差异和lockdep的检测结果。
注意:开启
CONFIG_PROVE_LOCKING和CONFIG_DEBUG_ATOMIC_SLEEP能帮你发现潜在的锁问题,但会降低性能,只在调试时开启。
4. 实操环境搭建与第一个内核模块
4.1 虚拟机配置与内核源码准备
我推荐用QEMU加Buildroot或者直接用Ubuntu虚拟机。内存至少4G,磁盘至少40G,因为内核源码编译后加上调试信息能占十几个G。源码从内核官网下载,解压到/usr/src或者家目录。编译前先安装依赖:gcc、make、flex、bison、libssl-dev、libelf-dev、bc。这些包缺一个都会导致编译失败,而且报错信息不一定直观。
配置内核时,先用make defconfig生成默认配置,然后make menuconfig开启CONFIG_DEBUG_INFO、CONFIG_GDB_SCRIPTS、CONFIG_DEBUG_FS、CONFIG_DYNAMIC_DEBUG。如果要做驱动实验,还要确保CONFIG_MODULES是开启的。编译用make -j$(nproc),第一次编译大概二十分钟到一小时,取决于机器性能。
4.2 编写并加载第一个内核模块
第一个模块就写经典的Hello World。代码结构是module_init和module_exit两个宏,分别指定加载和卸载函数。加载函数里用pr_info打印日志,卸载函数里也打印一条。Makefile用内核提供的kbuild体系,指定obj-m和内核源码路径。
编译命令是make -C /lib/modules/$(uname -r)/build M=$(pwd) modules。加载用insmod hello.ko,查看日志用dmesg | tail。卸载用rmmod hello。如果加载失败,常见原因是内核版本不匹配或者模块签名问题。可以先用modinfo hello.ko看依赖和签名信息。
实操心得:如果
insmod报Invalid module format,先检查uname -r和编译时用的内核版本是否一致。用modprobe代替insmod能自动处理依赖,但需要模块在/lib/modules下。
4.3 用ftrace追踪内核函数调用
ftrace的使用分几步:先挂载tracefs,通常在/sys/kernel/tracing。然后设置current_tracer为function_graph,设置set_ftrace_filter指定要追踪的函数,比如do_sys_open。然后echo 1 > tracing_on开始追踪,执行一个打开文件的操作,再echo 0 > tracing_on停止。最后cat trace看调用图。
这个工具能直观展示内核函数的调用层次和耗时,对理解代码路径特别有帮助。比如你想知道open系统调用到底走了哪些函数,用ftrace一抓就清楚了。
5. 常见问题与排查技巧实录
5.1 内核编译与模块加载问题速查
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 编译报错缺少头文件 | 依赖包未安装 | 安装libssl-dev、libelf-dev、flex、bison |
insmod报Invalid module format | 内核版本不匹配 | 检查uname -r与编译版本 |
| 模块加载后无日志 | printk级别太低 | 检查/proc/sys/kernel/printk,用dmesg -n 8 |
rmmod报Module is in use | 模块被引用 | lsmod看引用计数,检查是否打开了设备文件 |
| 内核panic后无输出 | 串口未配置 | 用QEMU的-nographic或配置earlyprintk |
5.2 调试内核oops的通用思路
内核oops信息里最关键的是RIP寄存器的值和调用栈。先用addr2line -e vmlinux <地址>定位到源码行。如果地址是模块里的,用gdb加载模块的.ko文件再查。调用栈从下往上读,最上面是出错点,下面是调用者。常见原因包括空指针解引用、野指针、栈溢出、睡眠在原子上下文。用CONFIG_DEBUG_INFO和CONFIG_KALLSYMS能大幅提高可读性。
注意:oops后内核可能处于不稳定状态,不要继续操作,先保存日志再重启。用
panic_on_oops可以让系统直接重启,避免状态污染。
5.3 内存泄漏与死锁的排查手段
内存泄漏用kmemleak,开启CONFIG_DEBUG_KMEMLEAK,然后echo scan > /sys/kernel/debug/kmemleak触发扫描,cat kmemleak看报告。死锁用lockdep,开启CONFIG_PROVE_LOCKING,如果发生死锁,dmesg里会打印详细的锁依赖图。另外CONFIG_DEBUG_ATOMIC_SLEEP能检测在原子上下文睡眠的问题。
我个人的习惯是,写任何内核代码之前先把这几个调试选项打开,虽然性能有损失,但能提前发现很多问题。等代码稳定了再关掉。
6. 后续内容规划与个人建议
这个专栏后续还会覆盖更多专题,比如网络子系统、安全模块、虚拟化与容器底层、实时内核补丁等。但那些属于进阶内容,建议先把前面六个模块吃透。每篇文章我都会尽量配可复现的实验,代码会放在公开仓库里,方便对照。
我个人在实际操作中的体会是,内核学习最忌讳只看不练。你看十遍task_struct的定义,不如自己写个模块打印一次current->pid。另外,不要怕犯错,内核panic是常态,关键是学会从oops信息里找线索。我刚开始写驱动的时候,一个空指针解引用查了一整天,后来发现是platform_get_resource返回NULL没检查。这种坑踩过一次就记住了。
最后再分享一个小技巧:用git bisect定位内核回归问题特别有效。如果你发现某个功能在新版本内核上坏了,但不知道是哪个提交引入的,用git bisect二分查找,配合一个自动化的测试脚本,很快就能定位到问题提交。这个技能在跟进主线内核时非常实用。