WRITING
第七章 总结与设计思想
第七章 总结与设计思想
7.1 五大子系统核心机制对比
子系统 | 核心算法/机制 | 核心数据结构 | 关键源码目录 | 设计目标 |
|---|---|---|---|---|
内存管理 | Buddy 系统、SLUB、COW、LRU 回收、缺页异常 |
|
| 高效利用有限物理内存,提供隔离的虚拟地址空间 |
进程管理 | fork 写时复制、上下文切换、中断/异常/系统调用 |
|
| 创建和管理执行流,实现多任务和资源隔离 |
调度 | CFS 虚拟运行时间、调度类、红黑树、负载均衡 |
|
| 公平高效地分配 CPU 时间,兼顾响应性和吞吐量 |
存储 | VFS 多态、Page Cache、write-back、IO 调度、blk-mq |
|
| 在慢速块设备上提供高效、可靠、统一的文件抽象 |
网络 | NAPI 中断轮询、TCP 滑动窗口/拥塞控制、GRO/TSO、qdisc |
|
| 在不可靠网络上提供可靠高效的通信,分层解耦 |
7.2 贯穿五个子系统的共性设计
把五个子系统放在一起看,会反复遇到下面这些设计手法:
7.2.1 分层抽象与面向对象
分层:每个子系统都通过分层隔离复杂度。内存:虚拟地址空间 → 页表 → 物理页 → 小对象;存储:VFS → 文件系统 → 页缓存 → 块层 → 驱动;网络:Socket → 传输层 → 网络层 → 链路层 → 驱动。上层只依赖下层的抽象接口,不关心具体实现。
面向对象的函数表:内核大量使用函数指针表实现多态:
sched_class(调度类)、file_operations(文件操作)、inode_operations(inode 操作)、vm_operations_struct(VMA 操作)、address_space_operations(地址空间操作)、net_device_ops(网卡操作)。具体实现通过填充函数表来提供自己的行为,上层通过函数表调用,实现"一套接口,多种实现"。容器与继承:通过
container_of宏从嵌入的基类对象找到派生类对象(如从sched_entity找到task_struct,从inode找到ext4_inode_info),实现了 C 语言的面向对象继承。
7.2.2 懒惰与写时复制(Lazy & COW)
能推迟的工作就推迟,能不复制就不复制:
fork COW:fork 不复制物理页,只复制页表并标记只读,写入时才复制。
零页共享:新匿名页映射到全局只读零页,写入时才分配实际页。
惰性页表分配:页表本身按需分配,不访问的区域不分配页表页。
write-back:写文件只写页缓存标记脏页,异步回写磁盘,不等待磁盘。
延迟分配:ext4 写文件时不立即分配磁盘块,回写时才分配,提高连续性。
懒 TLB:切换到内核线程时不刷新 TLB,因为内核空间页表不变。
延迟 FPU 保存:不使用浮点就不保存/恢复 FPU 状态。
这些懒惰优化的共同目标是:避免不必要的工作,将开销推迟到真正需要时,甚至永远不发生(如进程退出了还没写入的页就不需要分配)。
7.2.3 缓存与批处理
为了减少慢速操作(磁盘、网络、锁竞争)的开销,内核大量使用缓存和批处理:
Page Cache:缓存磁盘文件内容,读命中避免磁盘 IO,写合并批量回写。
SLUB per-CPU 缓存:小对象分配从 per-CPU 缓存取,避免 buddy 锁竞争。
Buddy pcp 缓存:单页分配从 per-CPU 页缓存取,批量填充和归还。
dentry/inode 缓存:缓存目录项和 inode,避免重复磁盘读取和路径解析。
NAPI 批量收包:软中断中批量处理数据包,减少中断数量。
预读(read-ahead):顺序读时提前读取后续页面,提高命中率。
预写(write-back):脏页批量回写,合并相邻 IO。
GRO/TSO:收包合并小包、发包分片由硬件完成,减少 per-packet 协议栈开销。
缓存的本质是利用局部性原理(时间局部性和空间局部性),将慢速设备的数据缓存到快速设备(内存/CPU缓存),将频繁的小操作合并为批量操作。
7.2.4 每 CPU 数据结构(Per-CPU)
在多 CPU 系统中,全局锁是性能瓶颈。内核大量使用 per-CPU 数据结构,每个 CPU 只访问自己的数据,避免锁竞争:
调度:每 CPU 运行队列
rq。内存:每 CPU 页缓存
per_cpu_pageset、SLUBkmem_cache_cpu。网络:每 CPU
softnet_data(NAPI poll_list、backlog、统计)。统计:大量计数器使用 per-CPU 计数,最后汇总,避免原子操作开销。
per-CPU 的代价是内存开销(每个 CPU 一份数据)和数据一致性问题(需要特殊的同步机制,如 smp_call_function、RCU),但在多核系统中性能收益远大于代价。
7.2.5 水位线、阈值与预算控制
内核通过各种水位线和阈值来控制系统资源的使用,避免某个子系统占用过多资源导致其他子系统饿死:
内存:zone 水位线(min/low/high)控制回收时机,dirty_ratio 控制脏页比例,OOM 阈值。
调度:sched_latency(调度延迟)、sched_min_granularity(最小运行粒度)、wakeup_granularity(唤醒粒度)。
网络:netdev_budget(收包包数预算)、netdev_budget_usecs(收包时间预算)、tcp_mem(TCP 内存阈值)。
存储:dirty_expire_interval(脏页过期时间)、dirty_writeback_interval(回写周期)。
这些阈值保证了系统在极端情况下的稳定性和公平性,避免一个子系统的异常影响整个系统。
7.2.6 RCU 与无锁读
RCU(Read-Copy-Update)是 Linux 内核广泛使用的同步机制,适用于读多写少的场景:
读者无需加锁,直接读取(无锁读,性能极高)。
写者复制数据、修改副本、原子替换指针,然后等待所有读者完成后释放旧数据。
广泛用于:路由表、文件系统挂载点、网络协议栈、dentry 缓存等。
RCU 是内核在多核时代提高读路径性能的关键技术。
7.3 学习路径建议
对于想继续学习 Linux 内核的读者,建议按下面的路径推进:
7.3.1 基础阶段
理解操作系统原理:先学习操作系统的基本概念(进程、线程、虚拟内存、文件系统、网络协议),推荐书籍:
《操作系统导论》(Operating Systems: Three Easy Pieces):通俗易懂,适合入门。
《深入理解计算机系统》(CSAPP):从程序员角度理解计算机系统。
《现代操作系统》(Modern Operating Systems):经典教材,全面系统。
阅读核心数据结构:从
task_struct、struct page、sk_buff、bio、inode等核心结构入手,理解每个字段的含义和设计原因。跟踪系统调用路径:选择几个常见的系统调用(
fork、read、write、send、recv),从系统调用入口跟踪到内核实现,理解完整的调用链。这是串联各子系统的最佳方式。
7.3.2 进阶阶段
深入一个子系统:选择一个感兴趣的子系统(如内存管理或网络),深入阅读核心源码,理解关键算法的实现细节。
使用调试工具:
ftrace:内核内置的函数跟踪工具,可以动态跟踪函数调用和延迟。
perf:性能分析工具,可以分析 CPU 占用、缓存命中率、函数调用图。
bcc/BPF:灵活的动态跟踪工具,可以编写 eBPF 程序在内核中收集数据,无需重新编译内核。
crash/gdb:内核崩溃转储分析和调试。
阅读文档和社区资源:
内核源码
Documentation/目录:包含大量子系统文档。kernelnewbies.org:内核新手社区,有大量教程和文章。lwn.net:Linux 内核新闻和技术文章,很多内核特性的设计思路都在 LWN 上有详细介绍。Linux Insides(github.com/0xAX/linux-insides):优秀的内核学习资源。
7.3.3 实践阶段
编译和运行自定义内核:下载内核源码,配置、编译、安装,在虚拟机或测试机器上运行,体验内核开发的基本流程。
编写内核模块:编写简单的内核模块(字符设备、proc 文件、sysfs 属性),理解内核编程模型。
修改和测试:尝试修改内核参数(sysctl)、调度器、IO 调度器、拥塞控制算法,观察系统行为变化。
参与社区:阅读内核邮件列表(LKML),尝试修复简单的 bug 或改进文档,向上游提交补丁。
7.3.4 善用内核自带文档(Documentation/)
内核源码自带的 Documentation/ 是和代码同步维护的一手资料,通常比二手博客更可靠,建议结合本文按图索骥:
主题 | 文档路径 |
|---|---|
CFS 设计与调度类 |
|
nice 值设计 |
|
CFS 带宽控制 |
|
调度域 / CPU 容量 / PELT / Deadline |
|
内存概念总览 |
|
大页 THP / HugeTLB / NUMA |
|
内存与脏页等可调参数 |
|
收包扩展 RSS/RPS/RFS/XPS |
|
网络参数 / 分段卸载 / 多队列 |
|
中断、工作队列、内存分配 API |
|
全部内核启动参数 |
|
读这些文档时再回头对照本文和源码,能把"为什么这样设计"和"代码具体怎么实现"对应起来。
7.4 结语
Linux 内核代码规模大、参与者多,运行场景覆盖嵌入式设备到大型服务器。读懂它的内部结构,最直接的用处是写应用程序时能判断系统调用、IO、网络路径上的开销在哪里;也可以借此观察一个大型 C 项目如何在性能和可靠性约束下,用分层、函数表、每 CPU 数据结构等手段控制复杂度。
本文基于 Linux 5.15.220,梳理了五个子系统的实现。内核一直在演进,版本之间字段、函数名和默认参数会有差异,但分层结构和基本机制相对稳定。入门时先建立整体框架,再针对具体子系统对照更新的源码深入,比逐行通读更现实。
本文基于 Linux Kernel 5.15.220 源码分析,所有引用的文件路径、数据结构和函数名均来自该版本源码。内核源码已下载并解压至
D:\k\linux-5.15.220\,可供查阅验证。
DISCUSSION
评论