引言
在现代服务器环境中,内存管理是操作系统最核心也最复杂的子系统之一。随着多 CPU、大内存架构的普及,Linux 内核设计了一套精密的分层内存管理体系来解决传统内存管理面临的挑战。本文将系统性地剖析 Linux 内核如何管理物理内存,从底层的 NUMA 架构到上层的 Slab 分配器,再到 TCP 连接的内存开销计算,帮助读者建立对这一关键系统机制的深入理解。
理解 Linux 内存管理机制对于高性能网络编程、性能优化、故障排查等场景具有重要的实际价值。无论是设计高并发服务器,还是排查内存泄漏问题,都需要扎实掌握这些底层知识。
第一章 核心背景:为什么内存管理需要这么复杂?
1.1 早期计算机的简单内存管理
在早期的计算机系统中,内存管理相对简单。就像一条长长的、连续的走廊,任何程序需要内存时就去申请一段连续的空间即可。那时的系统规模小、用户少,这种简单方式足够应付。
然而,随着计算机技术的飞速发展,特别是多处理器服务器和大规模内存的出现,这种简单的管理方式暴露出了严重的局限性。
1.2 现代服务器面临的两个核心问题
┌─────────────────────────────────────────────────────────────────────────────┐
│ 现代服务器内存管理面临的两大挑战 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 问题一:速度问题(NUMA架构带来的挑战) │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ CPU1 CPU2 │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌────────┐ ┌────────┐ │ │
│ │ │内存条A │ │内存条B │ │ │
│ │ │(本地) │ │(本地) │ │ │
│ │ └────────┘ └────────┘ │ │
│ │ │ │ │ │
│ │ └──────────┬───────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ QPI总线(跨CPU访问) │ │
│ │ │ │
│ │ 问题:CPU1访问内存条A很快,但访问内存条B需要经过QPI总线,速度慢很多 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 问题二:碎片化问题(内存碎片) │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 内存使用久了,会产生很多小空洞: │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ ■ ■ □ □ ■ ■ ■ □ □ □ ■ ■ □ □ ■ ■ ■ ■ □ □ ■ ■ □ □ ■ ■ □ │ │ │
│ │ │ ■ = 已占用 □ = 空闲但很小 │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 问题:总空闲内存可能很大(比如80%),但找不到一块连续的大内存 │ │
│ │ 结果:无法分配大对象,程序崩溃 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
1.3 Linux 的解决之道:分层管理体系
为了解决上述两个问题,Linux 内核设计了一套精妙的分层管理体系。这个体系就像管理一家超大型跨国公司,需要层层分工、各司其职:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Linux内存管理体系层级架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 第一层:Node(节点) → 解决"距离"问题,NUMA架构支持 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 第二层:Zone(区域) → 解决"兼容性"问题,适应不同硬件 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 第三层:Page(页面) → 内存的最小分配单位 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 第四层:Buddy System → 解决"碎片"问题,管理连续页面 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 第五层:Slab Allocator → 解决"小对象"频繁申请问题 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 物理内存 ──► Node ──► Zone ──► Buddy System ──► Slab ──► 内核对象 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
理解这个分层体系的关键在于:每一层都有其独特的职责和价值,上层依赖下层,下层为上层服务,共同构建起高效、可靠的内存管理系统。
第二章 第一层:Node(节点)------ 解决 "距离" 问题
2.1 NUMA 架构详解
NUMA(Non-Uniform Memory Access,非统一内存访问)是现代多处理器服务器的核心架构。在 NUMA 系统中,每个 CPU 都直连着几根专属的内存条,形成了多个独立的内存访问区域。
┌─────────────────────────────────────────────────────────────────────────────┐
│ NUMA架构示意图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ QPI总线/互联网络 │
│ │ │ │
│ ┌──────────────────┘ └──────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ CPU 0 │ │ CPU 1 │ │
│ │ Node 0 │ │ Node 1 │ │
│ │ ┌────┐ │ │ ┌────┐ │ │
│ │ │内存│ │ │ │内存│ │ │
│ │ │ 0 │ │ │ │ 1 │ │ │
│ │ └────┘ │ │ └────┘ │ │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ │ 快速(本地) │ │
│ │ 慢速(远程) │ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 访问速度对比: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ CPU 0 访问 Node 0 的内存(本地): 极快 ✓ │ │
│ │ CPU 0 访问 Node 1 的内存(远程): 较慢,需要经过QPI ✗ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2.2 Linux 如何管理 NUMA 节点
Linux 内核将 "一个 CPU + 它直连的内存" 定义为一个 Node(节点)。例如,如果一台服务器有两个 CPU 插槽,系统就会有 Node 0 和 Node 1 两个节点。
┌─────────────────────────────────────────────────────────────────────────────┐
│ 查看NUMA节点信息 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 命令:numactl --hardware │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ available: 2 nodes (node 0 and node 1) │ │
│ │ node 0 size: 16384 MB │ │
│ │ node 0 cpus: 0,1,2,3,4,5,6,7 │ │
│ │ node 1 size: 16384 MB │ │
│ │ node 1 cpus: 8,9,10,11,12,13,14,15 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 就近原则(Locality Policy): │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 内核的内存分配策略: │ │
│ │ • 尽量让进程在哪个Node的CPU上运行,就分配哪个Node的内存给它 │ │
│ │ • 这叫做"就近原则",最大程度减少跨节点访问 │ │
│ │ • 只有当本地节点内存不足时,才会考虑分配到远程节点 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2.3 Node 在性能优化中的意义
理解 NUMA 架构对于性能调优至关重要。在高并发场景下,如果进程的 CPU 亲和性和内存分配策略设置不当,会导致大量的远程内存访问,严重影响系统性能。
常见的 NUMA 优化策略包括:
- 使用 numactl 或 taskset 绑定进程到特定 CPU 和 Node
- 开启内核的自动 NUMA 平衡功能(numa_balancing)
- 合理规划进程分布,避免所有进程都挤在一个 Node 上
第三章 第二层:Zone(区域)------ 解决 "兼容性" 问题
3.1 为什么需要 Zone 划分?
并不是所有的内存都能被所有的硬件设备使用。这是一个历史遗留问题,需要从硬件发展的角度来理解。
早期的硬件设备(如显卡、网卡)相对 "笨拙",只能访问内存地址很低的那部分区域。为了保持向后兼容,Linux 把每个 Node 里的内存进一步切分成几个 Zone。
┌─────────────────────────────────────────────────────────────────────────────┐
│ Node内的Zone划分 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Node Memory │ │
│ │ ┌───────────┬───────────────────────────┬─────────────────────┐ │ │
│ │ │ │ │ │ │ │
│ │ │ ZONE_DMA │ ZONE_DMA32 │ ZONE_NORMAL │ │ │
│ │ │ │ │ │ │ │
│ │ │ 0-16MB │ 16MB-1GB │ 1GB以上 │ │ │
│ │ │ │ │ │ │ │
│ │ └───────────┴───────────────────────────┴─────────────────────┘ │ │
│ │ │ │
│ │ 地址从低到高 ──────────────────────────────────────────────► │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
3.2 各 Zone 详解
ZONE_DMA(0-16MB)
这是地址最低的一块区域,专门留给那些 "古老" 的 ISA 设备做 DMA(直接内存访问)用的。DMA 允许硬件设备直接读写内存而无需 CPU 介入,非常适合数据吞吐量大的设备。
┌─────────────────────────────────────────────────────────────────────────────┐
│ ZONE_DMA详解 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 什么是DMA? │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ DMA(Direct Memory Access,直接内存访问)是一种允许外设直接读写内存 │ │
│ │ 的技术,无需CPU介入,可以大大提高数据传输效率。 │ │
│ │ │ │
│ │ 为什么需要低地址? │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ 古老的ISA设备只能访问24位地址空间(即16MB以下的内存) │ │
│ │ 为了兼容性,这部分地址必须专门留给这些设备使用 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
ZONE_DMA32(16MB-1GB)
这个区域专门给 32 位设备使用。在实际场景中,如果你使用的是 64 位 CPU,但插了一块只能处理 32 位地址的老旧 PCI 设备,数据就必须放在这个区域。
ZONE_NORMAL(1GB 以上)
这是最大也是最重要的区域。Linux 内核自己主要使用这里的内存,普通应用程序的堆、栈等也都分配在这里。
┌─────────────────────────────────────────────────────────────────────────────┐
│ 查看系统Zone信息 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 命令:cat /proc/zoneinfo │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ Node 0, zone DMA16 pages: 4000 │ │
│ │ spanned: 16 │ │
│ │ present: 15 │ │
│ │ Node 0, zone DMA32 pages: 104456 │ │
│ │ Node 0, zone Normal pages: 327680 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
3.3 关于 ZONE_HIGHMEM 的说明
ZONE_HIGHMEM(高端内存)是 32 位时代的产物。在 32 位系统中,CPU 只能寻址 4GB 内存,但服务器的物理内存可能远超过这个数字。多出来的内存就叫做 "高端内存",需要特殊的映射机制才能访问。
在现代 64 位服务器上,地址空间极其充裕,ZONE_HIGHMEM 基本已经成为历史,不再需要关心。
第四章 第三层:Page(页面)------ 内存的最小单位
4.1 页面概念
内存不能随意切分,必须按固定大小切。这个固定大小的最小单位叫做页(Page)。在 x86 架构中,标准的页面大小是 4KB。
┌─────────────────────────────────────────────────────────────────────────────┐
│ 页面概念示意图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 物理内存 │ │
│ │ ┌────────┬────────┬────────┬────────┬────────┬────────┬────────┐ │ │
│ │ │ Page 0 │ Page 1 │ Page 2 │ Page 3 │ Page 4 │ Page 5 │ ... │ │ │
│ │ │ 4KB │ 4KB │ 4KB │ 4KB │ 4KB │ 4KB │ │ │ │
│ │ └────────┴────────┴────────┴────────┴────────┴────────┴────────┘ │ │
│ │ │ │
│ │ 内核管理内存,本质上就是在管理这几百万、几千万个4KB的小方块 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 页面大小配置: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ • 标准页面大小:4KB(大多数场景) │ │
│ │ • 大页面(HugePages):2MB或1GB(用于数据库等需要大内存的场景) │ │
│ │ • 查看命令:getconf PAGE_SIZE │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 页面是所有上层机制的基础
无论是 Buddy System 还是 Slab Allocator,它们的操作最终都是基于页面的。页面是物理内存管理的原子单位,不可再分。
内核会维护一个页面分配位图,记录每个页面的状态:是空闲还是已占用,属于哪个进程,用于什么目的等。这个位图是内核内存管理的基础数据结构。
第五章 第四层:伙伴系统(Buddy System)------ 解决碎片问题
5.1 为什么要伙伴系统?
让我们用一个生活化的例子来理解:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 伙伴系统的设计背景 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 场景:去银行取钱 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 如果柜员只有一种面额:100元纸币 │ │
│ │ │ │
│ │ 你要取10元 → 柜员给你100元 → 你剪碎 → 剩下90元浪费! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 场景:内存分配 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 如果只分配整页(4KB) │ │
│ │ │ │
│ │ 内核需要一个100字节的对象 → 分配4KB → 3900字节浪费(内部碎片) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 问题:外部碎片 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 当我们反复分配和释放不同大小的内存块时: │ │
│ │ │ │
│ │ 分配A(8KB) ─► 释放A ─► 分配B(4KB) ─► 释放B ─► 分配C(4KB) │ │
│ │ │ │
│ │ 结果:可能只剩下很多分散的4KB碎片,无法分配连续的大内存块 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.2 伙伴系统的工作原理
伙伴系统(Buddy System)是 Linux 内核解决内存碎片的核心算法。其核心思想是:以 2 的幂次方为单位管理空闲内存块。
┌─────────────────────────────────────────────────────────────────────────────┐
│ 伙伴系统分级结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 伙伴系统把空闲内存按大小分成11组链表(阶0到阶10): │ │
│ │ │ │
│ │ 阶0: 管理 4KB 的块(1个连续页面) │ │
│ │ 阶1: 管理 8KB 的块(2个连续页面) │ │
│ │ 阶2: 管理 16KB 的块(4个连续页面) │ │
│ │ 阶3: 管理 32KB 的块(8个连续页面) │ │
│ │ 阶4: 管理 64KB 的块(16个连续页面) │ │
│ │ 阶5: 管理 128KB 的块(32个连续页面) │ │
│ │ 阶6: 管理 256KB 的块(64个连续页面) │ │
│ │ 阶7: 管理 512KB 的块(128个连续页面) │ │
│ │ 阶8: 管理 1MB 的块(256个连续页面) │ │
│ │ 阶9: 管理 2MB 的块(512个连续页面) │ │
│ │ 阶10: 管理 4MB 的块(1024个连续页面) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.3 分配与释放过程详解
分配过程(Allocation)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 伙伴系统分配过程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 场景:程序需要申请 8KB 内存 │
│ │
│ Step 1: 查找合适大小 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 系统先看 8KB(第1阶)的链表里有没有空闲块: │ │
│ │ │ │
│ │ 第0阶链表:4KB ─► ┌───┐┌───┐┌───┐ 有很多块 │ │
│ │ 第1阶链表:8KB ─► ┌───┐ (空!) │ │
│ │ 第2阶链表:16KB ─► ┌───────┐ 有一些 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 2: 拆分(Split) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 8KB链表为空,去16KB链表找: │ │
│ │ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ 16KB │ │ │
│ │ │ (一个伙伴) │ │ │
│ │ └───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ 拆分! │ │
│ │ ┌───────────────────┐ │ │
│ │ │ │ │ │ │ │
│ │ │ 8KB │ 8KB │ │ ← "伙伴"(地址连续) │ │
│ │ │ A │ B │ │ │ │
│ │ └───────┴───────┴───┘ │ │
│ │ │ │
│ │ 把其中一个8KB(A)分配给程序 │ │
│ │ 另一个8KB(B)放入8KB空闲链表备用 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
释放过程(Deallocation)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 伙伴系统释放与合并 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 场景:程序释放刚才分配的8KB内存块A │
│ │
│ Step 1: 放入空闲链表 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ │ │ │ │ │
│ │ │ 8KB │ 8KB │ │ ← 块A刚释放,放入这里 │ │
│ │ │ A✗ │ B │ │ (块B正忙,不能合并) │ │
│ │ └───────┴───────┴───┘ │ │
│ │ │ │
│ │ 块A被标记为空闲,但块B还在使用,不能合并 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Step 2: 尝试合并(Buddy Merge) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 稍后,块B也被释放了: │ │
│ │ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ │ │ │
│ │ │ 8KB │ 8KB │ │ │
│ │ │ A✗ │ B✗ │ ← 两个伙伴都空闲! │ │
│ │ │ │ │ │
│ │ └───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ 合并! │ │
│ │ ┌───────────────────┐ │ │
│ │ │ 16KB │ ← 重新组成一个更大的块 │ │
│ │ │ (合并后的A+B) │ 放入第2阶链表 │ │
│ │ └───────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 伙伴合并的好处: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ • 大大减少外部碎片 │ │
│ │ • 保证总有大块连续内存可用 │ │
│ │ • 算法简单高效,复杂度为O(1) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.4 页面迁移类型(Migrate Types)
在伙伴系统的实现中,每个链表还会根据页面类型进一步分类,这主要是为了防止内存碎片化。
┌─────────────────────────────────────────────────────────────────────────────┐
│ 页面迁移类型分类 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 为什么要分类? │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 如果不同类型的页面随机穿插占用: │ │
│ │ │ │
│ │ [内核代码] [用户数据] [内核代码] [用户数据] [磁盘缓存] [用户数据] │ │
│ │ │ │
│ │ 结果:无法腾出大块连续空间给需要连续内存的场景(如DMA) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 页面分类: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ┌────────────────┬───────────────────────────────────────────┐ │ │
│ │ │ UNMOVABLE │ 不可移动 │ │ │
│ │ │ │ 内核代码、静态分配的数据结构 │ │ │
│ │ │ │ 钉死在当前位置,不能移动 │ │ │
│ │ ├────────────────┼───────────────────────────────────────────┤ │ │
│ │ │ RECLAIMABLE │ 可回收 │ │ │
│ │ │ │ 磁盘缓存、页面缓存 │ │ │
│ │ │ │ 可以回收再分配 │ │ │
│ │ ├────────────────┼───────────────────────────────────────────┤ │ │
│ │ │ MOVABLE │ 可移动 │ │ │
│ │ │ │ 用户态程序的内存页 │ │ │
│ │ │ │ 可以随时迁移到其他物理位置 │ │ │
│ │ └────────────────┴───────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 效果:同类型的内存聚集在一起,腾出大块空地 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
第六章 第五层:Slab 分配器 ------ 解决小对象频繁申请问题
6.1 为什么需要 Slab?
虽然伙伴系统解决了外部碎片问题,但它有两个明显的缺点:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 伙伴系统的局限性 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 问题1:最小分配单位太大 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 伙伴系统最小分配4KB(一个页面) │ │
│ │ │ │
│ │ 如果内核只需要存一个几十字节的小结构体(如TCP连接的状态标记) │ │
│ │ 分配4KB太浪费了! │ │
│ │ │ │
│ │ 浪费比例:几十字节 / 4KB ≈ 1%~2% 利用率 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 问题2:频繁初始化开销 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 内核对象(如文件对象、进程描述符)需要频繁创建和销毁 │ │
│ │ │ │
│ │ 每次都走伙伴系统申请 → 初始化 → 使用 → 释放 │ │
│ │ │ │
│ │ 这就像:为了买一瓶醋,专门开车去超市买一整箱 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6.2 Slab 分配器的工作原理
Slab 分配器构建在伙伴系统之上,充当 "中间商" 的角色:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Slab分配器原理 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Slab分配器架构 │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 应用程序/内核模块 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Slab分配器 │ │ │
│ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │
│ │ │ │ kmalloc- │ │ TCP │ │ inode │ │ │ │
│ │ │ │ 64 │ │ cache │ │ cache │ │ │ │
│ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 伙伴系统 │ │ │
│ │ │ (4KB页面) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Slab 的工作流程:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Slab分配器工作流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 向伙伴系统"进货" │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Slab向伙伴系统申请大块内存(比如1个页面,4KB) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 2: 切割成标准小块 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ 4KB 页面 │ │ │
│ │ │ │ │ │
│ │ │ ┌────┬────┬────┬────┬────┬────┬────┬────┐ │ │
│ │ │ │obj1│obj2│obj3│obj4│obj5│obj6│obj7│obj8│ ← 32个对象 │ │
│ │ │ │128B│128B│128B│128B│128B│128B│128B│128B│ 每块128字节 │ │
│ │ │ └────┴────┴────┴────┴────┴────┴────┴────┘ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 专门存放特定类型的对象(如TCP连接) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 3: 维护缓存池 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Slab维护多个链表: │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ empty │ │ partial │ │ full │ │ │
│ │ │(空链表) │ │(半满) │ │(满链表) │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ 所有块 部分块 所有块 │ │
│ │ 空闲 在使用 在使用 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 4: 快速分配与释放 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 内核需要新对象 ─► 直接从Slab拿一个空闲块 ─► 极快! │ │
│ │ │ │
│ │ 对象用完释放 ─► Slab不急着还给系统 ─► 留着下次直接用 │ │
│ │ │ │
│ │ 这就像"对象池"技术,复用对象,避免反复申请 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6.3 专用缓存与通用缓存
┌─────────────────────────────────────────────────────────────────────────────┐
│ Slab缓存类型 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 专用缓存(Specialized Caches) │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 针对特定内核对象,有专门的Slab缓存: │ │
│ │ │ │
│ │ ┌─────────────────┬───────────────────────────────────────────┐ │ │
│ │ │ 缓存名称 │ 用途 │ │ │
│ │ ├─────────────────┼───────────────────────────────────────────┤ │ │
│ │ │ TCP │ 存放 struct tcp_sock(TCP控制块) │ │ │
│ │ │ sock_inode_cache│ 存放 struct socket 和 inode │ │ │
│ │ │ dentry │ 存放目录项 │ │ │
│ │ │ filp │ 存放 struct file(文件对象) │ │ │
│ │ │ inode_cache │ 存放 struct inode │ │ │
│ │ └─────────────────┴───────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 通用缓存(General Purpose Caches) │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 对于不知道如何分类的小内存,有kmalloc系列: │ │
│ │ │ │
│ │ ┌─────────────────┬───────────────────────────────────────────┐ │ │
│ │ │ 缓存名称 │ 对象大小 │ │ │
│ │ ├─────────────────┼───────────────────────────────────────────┤ │ │
│ │ │ kmalloc-32 │ 32字节 │ │ │
│ │ │ kmalloc-64 │ 64字节 │ │ │
│ │ │ kmalloc-128 │ 128字节 │ │ │
│ │ │ kmalloc-256 │ 256字节 │ │ │
│ │ │ kmalloc-512 │ 512字节 │ │ │
│ │ │ kmalloc-1024 │ 1024字节 │ │ │
│ │ │ kmalloc-2048 │ 2048字节 │ │ │
│ │ └─────────────────┴───────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6.4 Slab 分配器的优势总结
┌─────────────────────────────────────────────────────────────────────────────┐
│ Slab分配器的核心优势 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 优势1:减少内部碎片 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 按需分配精确大小的对象 │ │
│ │ 而不是像伙伴系统那样按页分配 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 优势2:提高分配速度 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 对象预初始化并缓存 │ │
│ │ 分配时直接取现成的,不用反复初始化 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 优势3:对象复用 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 释放的对象不急着还给系统 │ │
│ │ 留着下次分配直接用 │ │
│ │ 减少向伙伴系统的申请次数 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 优势4:着色(Coloring) │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 不同Slab的相同对象,在CPU缓存中错开存放 │ │
│ │ 减少缓存冲突,提高命中率 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
第七章 TCP 连接的内核对象体系
7.1 TCP 连接涉及的四大核心对象
创建一个 TCP 连接时,内核会从不同的 Slab 缓存池中分配多个核心对象:
┌─────────────────────────────────────────────────────────────────────────────┐
│ TCP连接的四大核心对象 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. struct socket(来自 socket_alloc 缓存) │ │
│ │ 作用:给用户层看的接口,BSD Socket通用层 │ │
│ │ │ │
│ │ 2. struct sock(TCP层控制块,来自 tcp_sock 缓存) │ │
│ │ 作用:内核网络层真正干活的对象,存TCP状态、窗口、队列等 │ │
│ │ │ │
│ │ 3. struct file(来自 filp 缓存) │ │
│ │ 作用:Linux"一切皆文件",socket也必须有文件对象来管理 │ │
│ │ │ │
│ │ 4. struct dentry(来自 dentry_cache 缓存) │ │
│ │ 作用:目录项,把socket挂载到/proc/[pid]/fd/下 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
7.2 对象间的关联关系
内核通过指针把这些对象串联成一条完整的链路:
┌─────────────────────────────────────────────────────────────────────────────┐
│ TCP连接对象的关联链路 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 用户态 fd │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ struct file │ ← file->private_data ──────────────────────┐ │ │
│ │ │ (文件对象) │ │ │ │
│ │ └─────────────────┘ │ │ │
│ │ │ │ │ │
│ │ ▼ file->f_path.dentry │ │ │
│ │ ┌─────────────────┐ │ │ │
│ │ │ struct dentry │ (目录项,文件系统路径节点) │ │ │
│ │ └─────────────────┘ │ │ │
│ │ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ↑ │
│ socket->sk │
│ │ │
│ ┌───────────────────────────┴───────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ struct socket │ (套接字通用层) │ │
│ │ │ (BSDSocket) │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │ │
│ │ ▼ socket->sk │ │
│ │ ┌─────────────────┐ │ │
│ │ │ struct sock │ ← (TCP层真正的控制块) │ │
│ │ │ (TCP/IP核心) │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
7.3 指针关联详解
struct file ↔ struct socket
- 关联指针:
file->private_data - struct file 是内核文件系统的通用对象,用于对接用户态的文件描述符(fd)
- 当它代表一个 socket 时,private_data 指针指向对应的 struct socket 对象
- 用户调用 read (fd)/write (fd) 时,内核通过这个链路找到 socket
struct socket ↔ struct sock
- 关联指针:
socket->sk - struct socket 是文件系统和协议栈之间的 "中间层壳子"
- 真正干活的 TCP 控制块是 struct sock(实际是 struct tcp_sock)
- 所有协议栈操作最终都通过 socket->sk 调用到 struct sock
struct dentry ↔ struct file
- 关联指针:
file->f_path.dentry - struct dentry 代表文件系统中的路径节点
- socket 会被映射到 /proc/[pid]/fd/[fd 号]
- struct file 的 f_path 字段指向这个 dentry
第八章 TCP 连接创建流程详解
8.1 客户端主动创建流程
当客户端调用 socket () 函数时,内核发生了一连串精密的动作:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 客户端socket()创建流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 用户调用:socket(AF_INET, SOCK_STREAM, 0) │
│ │
│ │ │
│ ▼ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Step 1: 创建Socket对象 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ 调用:sock_alloc() │ │
│ │ │ │
│ │ 从 sock_inode_cache Slab缓存池中申请一个socket_alloc对象 │ │
│ │ 这个对象同时包含 struct socket(给用户用)和 struct inode │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Step 2: 创建Protocol对象 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ 调用:inet_create() → sk_alloc() │ │
│ │ │ │
│ │ 根据协议类型(TCP),从TCP的Slab缓存池申请struct tcp_sock对象 │ │
│ │ │ │
│ │ 内存魔法:tcp_sock的头部就是sock │ │
│ │ 申请了大的tcp_sock,可以当小的sock用(C语言继承) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Step 3: 创建File对象 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ 调用:sock_map_fd() → sock_alloc_file() │ │
│ │ │ │
│ │ 申请 struct dentry:从 dentry_cache 缓存池拿 │ │
│ │ 申请 struct file:从 filp_cache 缓存池拿 │ │
│ │ │ │
│ │ 串糖葫芦: │ │
│ │ file->private_data → socket │ │
│ │ socket->sk → sock │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ │
│ 结果:创建一条TCP连接,内核至少分配4个核心对象: │
│ • socket_alloc(约1.5KB,来自sock_inode_cache) │
│ • tcp_sock(约1.9KB,来自TCP cache) │
│ • dentry(约192B,来自dentry_cache) │
│ • file(约256B,来自filp_cache) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
8.2 服务端被动接受流程
服务端调用 accept () 时的流程与客户端不同,效率更高:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 服务端accept()流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 关键洞察:三次握手时已分配好 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 服务端在三次握手还没完成时(收到SYN包), │ │
│ │ 内核就已经分配好了 request_sock 和 tcp_sock! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ │ │
│ ▼ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ accept()阶段只需要: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 1. 从"全连接队列"取出已经建立好的连接 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 2. 分配新的 struct file(因为要交给用户进程用) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 3. 分配新的 struct socket(同样给用户进程用) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 4. 关联新file和socket到已有的tcp_sock │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 优势: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ accept()阶段不需要重新分配tcp_sock │ │
│ │ 大大减少了accept()的耗时 │ │
│ │ 提高了并发能力 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
8.3 connect/send 的完整调用链
┌─────────────────────────────────────────────────────────────────────────────┐
│ TCP连接操作的对象协同流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 阶段1:connect() │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 用户调用 connect(sockfd, ...) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 根据sockfd找到 struct file │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 通过 file->private_data 拿到 struct socket │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 通过 socket->sk 拿到 struct sock │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 调用 socket->ops->connect() → sock->sk_prot->connect() │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ TCP状态从 CLOSED 变为 SYN_SENT,发起三次握手 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 阶段2:send() │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 用户调用 send(sockfd, "hello", 5, 0) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 同样的查找链路:fd → file → socket → sock │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 把用户态的 "hello" 拷贝到 sock 的发送缓冲区 sk_write_queue │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ TCP协议栈根据滑动窗口、拥塞控制信息封装成包发送 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
第九章 TCP 连接内存消耗实测分析
9.1 实验准备
┌─────────────────────────────────────────────────────────────────────────────┐
│ 实验配置与准备 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 实验目标:精确测量建立50,000条TCP连接消耗的内核内存 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 关键内核参数调整: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 1. ip_local_port_range = 5000 65000 │ │
│ │ 原因:客户端要建立5万连接,需要超过5万个本地端口 │ │
│ │ │ │
│ │ 2. tcp_tw_reuse = 0 │ │
│ │ tcp_tw_recycle = 0 │ │
│ │ 原因:观察TIME_WAIT开销,必须关闭快速回收 │ │
│ │ │ │
│ │ 3. tcp_max_tw_buckets = 600000 │ │
│ │ 原因:限制系统能容纳的TIME_WAIT套接字数 │ │
│ │ │ │
│ │ 4. somaxconn = 1024 │ │
│ │ 原因:支持高并发建立连接 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 测量基准: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ 实验前记录 slabtop 和 cat /proc/meminfo 的初始数值 │ │
│ │ 例如:客户端初始 Slab 内存 = 39848 KB │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
9.2 ESTABLISHED 状态内存消耗
┌─────────────────────────────────────────────────────────────────────────────┐
│ ESTABLISHED状态实测数据 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 实验过程:客户端向服务端发起50,000条连接,状态全部ESTABLISHED │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 内存暴涨项(对比实验前后slabtop数据): │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ┌──────────────────┬──────────┬─────────────────────────────────┐ │ │
│ │ │ 对象 │ 大小 │ 说明 │ │ │
│ │ ├──────────────────┼──────────┼─────────────────────────────────┤ │ │
│ │ │ TCP │ 1.94 KB │ struct tcp_sock,TCP控制块 │ │ │
│ │ │ sock_inode_cache│ 0.62 KB │ struct socket + inode │ │ │
│ │ │ kmalloc-256 │ 0.25 KB │ struct file(文件对象) │ │ │
│ │ │ dentry │ 0.19 KB │ 目录项 │ │ │
│ │ │ kmalloc-64 │ 0.06 KB │ socket_wq/inet_bind_bucket │ │ │
│ │ ├──────────────────┼──────────┼─────────────────────────────────┤ │ │
│ │ │ 总计 │ ~3.06 KB │ │ │ │
│ │ └──────────────────┴──────────┴─────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 验证计算: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 理论计算:1.94 + 0.62 + 0.25 + 0.19 + 0.06 ≈ 3.06 KB │ │
│ │ │ │
│ │ 实际测量:(206896 - 39848) / 50000 ≈ 3.34 KB │ │
│ │ │ │
│ │ 结论:理论值(3.06KB)和实测值(3.34KB)非常接近! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ⚠️ 重要结论: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 在Linux下,维持一条TCP连接(ESTABLISHED状态) │ │
│ │ 内核大约消耗 3.3 KB 左右的内存 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
9.3 非 ESTABLISHED 状态的内存消耗
┌─────────────────────────────────────────────────────────────────────────────┐
│ 不同TCP状态的内存消耗对比 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 状态 核心对象 单连接内存 备注 │ │
│ │ ─────────────────────────────────────────────────────────────────│ │
│ │ │ │
│ │ ESTABLISHED tcp_sock, socket, ~3.3 KB 资源最全 │ │
│ │ file, dentry │ │
│ │ │ │
│ │ FIN_WAIT2 基础控制块 ~0.4 KB 大部分已释放│ │
│ │ │ │
│ │ TIME_WAIT tw_sock_TCP, dentry ~0.17 KB 非常轻量 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ TIME_WAIT详解: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 很多人担心服务器有几万个TIME_WAIT会把内存撑爆 │ │
│ │ 实际上TIME_WAIT非常轻量级: │ │
│ │ │ │
│ │ • TCP slab消失(回收了) │ │
│ │ • 只剩下 tw_sock_TCP(0.19KB) │ │
│ │ • 主要消耗的是"连接数名额",不是内存 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
9.4 实际应用计算
┌─────────────────────────────────────────────────────────────────────────────┐
│ 实际场景内存消耗计算 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 场景1:10万并发长连接 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 内存消耗 = 100,000 × 3.3 KB = 330 MB │ │
│ │ │ │
│ │ 看起来不多?继续看下一个例子: │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 场景2:100万并发长连接 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 内存消耗 = 1,000,000 × 3.3 KB ≈ 3.3 GB │ │
│ │ │ │
│ │ ⚠️ 注意: │ │
│ │ • 这是内核内存(Slab),无法通过swap交换到磁盘 │ │
│ │ • 必须用物理内存扛住 │ │
│ │ • 如果加上应用层缓冲区和实际数据,消耗会更大 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 关键启示: │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 1. 不要怕TIME_WAIT:每个才0.17KB,不会撑爆内存 │ │
│ │ │ │
│ │ 2. 真正的瓶颈在ESTABLISHED长连接数量 │ │
│ │ │ │
│ │ 3. 做C10K/C100K/C1M问题时,内存规划必须精确计算 │ │
│ │ │ │
│ │ 4. 监控/proc/slabinfo和slabtop是必要的运维手段 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
第十章 总结与最佳实践
10.1 内存管理体系全景图
┌─────────────────────────────────────────────────────────────────────────────┐
│ Linux内存管理完整架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 应用层需求 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ Slab分配器 ──► 管理内核对象(小对象,频繁申请) │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ 伙伴系统 ──► 管理连续页面(解决外部碎片) │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ 页面管理 ──► 4KB最小单位 │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ Zone区域 ──► ZONE_DMA / ZONE_NORMAL(硬件兼容性) │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ Node节点 ──► NUMA架构支持(解决访问距离) │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 物理内存 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
10.2 TCP 连接内存管理的启示
┌─────────────────────────────────────────────────────────────────────────────┐
│ TCP连接内存管理最佳实践 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 理解连接成本 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ • ESTABLISHED连接:约3.3KB/条 │ │
│ │ • TIME_WAIT连接:约0.17KB/条 │ │
│ │ • 不要害怕TIME_WAIT,它是轻量级的 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 2. 合理规划连接数量 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ • 100万连接 ≈ 3.3GB 内核内存 │ │
│ │ • 这是Slab内存,无法swap │ │
│ │ • 设计高并发系统时必须考虑在内 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 3. 监控与排查 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ • 使用slabtop观察内核缓存使用情况 │ │
│ │ • 查看/proc/slabinfo获取详细数据 │ │
│ │ • 关注Slab内存占总内存比例 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 4. 内核参数调优 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ • tcp_max_tw_buckets:限制TIME_WAIT数量 │ │
│ │ • tcp_tw_reuse:允许重用TIME_WAIT端口(客户端) │ │
│ │ • somaxconn:服务端全连接队列长度 │ │
│ │ • tcp_max_syn_backlog:半连接队列长度 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
10.3 核心概念回顾
理解 Linux 内存管理,可以用 "盖房子" 来类比:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 内存管理的"盖房子"类比 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Node(节点):选地盘 │ │
│ │ → 为了交通方便,选离原材料产地(CPU)最近的地皮 │ │
│ │ │ │
│ │ Zone(区域):分区域 │ │
│ │ → 有的地只能盖仓库(DMA),有的地可以盖高楼(Normal) │ │
│ │ │ │
│ │ 伙伴系统:包工头 │ │
│ │ → 要大块地给整块,要小块把大地切开 │ │
│ │ → 还回去时看看能不能拼回大块 │ │
│ │ │ │
│ │ Slab分配器:精装修队 │ │
│ │ → 包工头给了一整层楼(页面) │ │
│ │ → 装修队隔成标准化的办公室(小对象) │ │
│ │ → 随时租给需要的公司(内核功能) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘