返回文章

WRITING

第七章 总结与设计思想

第七章 总结与设计思想

7.1 五大子系统核心机制对比

子系统

核心算法/机制

核心数据结构

关键源码目录

设计目标

内存管理

Buddy 系统、SLUB、COW、LRU 回收、缺页异常

struct page、mm_struct、vm_area_struct、zone

mm/

高效利用有限物理内存,提供隔离的虚拟地址空间

进程管理

fork 写时复制、上下文切换、中断/异常/系统调用

task_struct、thread_info、pt_regs

kernel/

创建和管理执行流,实现多任务和资源隔离

调度

CFS 虚拟运行时间、调度类、红黑树、负载均衡

sched_entity、cfs_rq、rq、sched_class

kernel/sched/

公平高效地分配 CPU 时间,兼顾响应性和吞吐量

存储

VFS 多态、Page Cache、write-back、IO 调度、blk-mq

bio、inode、file、address_space、super_block

block/ fs/

在慢速块设备上提供高效、可靠、统一的文件抽象

网络

NAPI 中断轮询、TCP 滑动窗口/拥塞控制、GRO/TSO、qdisc

sk_buff、sock、napi_struct、net_device

net/

在不可靠网络上提供可靠高效的通信,分层解耦

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、SLUB kmem_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 基础阶段

  1. 理解操作系统原理:先学习操作系统的基本概念(进程、线程、虚拟内存、文件系统、网络协议),推荐书籍:

    • 《操作系统导论》(Operating Systems: Three Easy Pieces):通俗易懂,适合入门。

    • 《深入理解计算机系统》(CSAPP):从程序员角度理解计算机系统。

    • 《现代操作系统》(Modern Operating Systems):经典教材,全面系统。

  2. 阅读核心数据结构:从 task_struct、struct page、sk_buff、bio、inode 等核心结构入手,理解每个字段的含义和设计原因。

  3. 跟踪系统调用路径:选择几个常见的系统调用(fork、read、write、send、recv),从系统调用入口跟踪到内核实现,理解完整的调用链。这是串联各子系统的最佳方式。

7.3.2 进阶阶段

  1. 深入一个子系统:选择一个感兴趣的子系统(如内存管理或网络),深入阅读核心源码,理解关键算法的实现细节。

  2. 使用调试工具:

    • ftrace:内核内置的函数跟踪工具,可以动态跟踪函数调用和延迟。

    • perf:性能分析工具,可以分析 CPU 占用、缓存命中率、函数调用图。

    • bcc/BPF:灵活的动态跟踪工具,可以编写 eBPF 程序在内核中收集数据,无需重新编译内核。

    • crash/gdb:内核崩溃转储分析和调试。

  3. 阅读文档和社区资源:

    • 内核源码 Documentation/ 目录:包含大量子系统文档。

    • kernelnewbies.org:内核新手社区,有大量教程和文章。

    • lwn.net:Linux 内核新闻和技术文章,很多内核特性的设计思路都在 LWN 上有详细介绍。

    • Linux Insides(github.com/0xAX/linux-insides):优秀的内核学习资源。

7.3.3 实践阶段

  1. 编译和运行自定义内核:下载内核源码,配置、编译、安装,在虚拟机或测试机器上运行,体验内核开发的基本流程。

  2. 编写内核模块:编写简单的内核模块(字符设备、proc 文件、sysfs 属性),理解内核编程模型。

  3. 修改和测试:尝试修改内核参数(sysctl)、调度器、IO 调度器、拥塞控制算法,观察系统行为变化。

  4. 参与社区:阅读内核邮件列表(LKML),尝试修复简单的 bug 或改进文档,向上游提交补丁。

7.3.4 善用内核自带文档(Documentation/)

内核源码自带的 Documentation/ 是和代码同步维护的一手资料,通常比二手博客更可靠,建议结合本文按图索骥:

主题

文档路径

CFS 设计与调度类

Documentation/scheduler/sched-design-CFS.rst

nice 值设计

Documentation/scheduler/sched-nice-design.rst

CFS 带宽控制

Documentation/scheduler/sched-bwc.rst

调度域 / CPU 容量 / PELT / Deadline

sched-domains.rst、sched-capacity.rst、sched-pelt.c、sched-deadline.rst(均在 Documentation/scheduler/)

内存概念总览

Documentation/admin-guide/mm/concepts.rst

大页 THP / HugeTLB / NUMA

Documentation/admin-guide/mm/transhuge.rst、hugetlbpage.rst、numa_memory_policy.rst

内存与脏页等可调参数

Documentation/admin-guide/sysctl/vm.rst

收包扩展 RSS/RPS/RFS/XPS

Documentation/networking/scaling.rst

网络参数 / 分段卸载 / 多队列

Documentation/networking/ip-sysctl.rst、segmentation-offloads.rst、multiqueue.rst

中断、工作队列、内存分配 API

Documentation/core-api/、Documentation/core-api/irq/

全部内核启动参数

Documentation/admin-guide/kernel-parameters.txt

读这些文档时再回头对照本文和源码,能把"为什么这样设计"和"代码具体怎么实现"对应起来。

7.4 结语

Linux 内核代码规模大、参与者多,运行场景覆盖嵌入式设备到大型服务器。读懂它的内部结构,最直接的用处是写应用程序时能判断系统调用、IO、网络路径上的开销在哪里;也可以借此观察一个大型 C 项目如何在性能和可靠性约束下,用分层、函数表、每 CPU 数据结构等手段控制复杂度。

本文基于 Linux 5.15.220,梳理了五个子系统的实现。内核一直在演进,版本之间字段、函数名和默认参数会有差异,但分层结构和基本机制相对稳定。入门时先建立整体框架,再针对具体子系统对照更新的源码深入,比逐行通读更现实。


本文基于 Linux Kernel 5.15.220 源码分析,所有引用的文件路径、数据结构和函数名均来自该版本源码。内核源码已下载并解压至 D:\k\linux-5.15.220\,可供查阅验证。

文章目录

DISCUSSION

评论

0 条已通过评论

留下评论

评论提交后需要审核。邮箱仅用于联系,不会公开。