Linux内核之一条tcp到底占用多少内存

引言

在现代服务器环境中,内存管理是操作系统最核心也最复杂的子系统之一。随着多 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分配器:精装修队                                               │   │
│  │  → 包工头给了一整层楼(页面)                                       │   │
│  │  → 装修队隔成标准化的办公室(小对象)                               │   │
│  │  → 随时租给需要的公司(内核功能)                                   │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

0voice · GitHub

相关推荐
IT召唤狮1 小时前
【One-KVM】开源轻量级 IP-KVM 解决方案,无网远控免费平替 — BIOS 级远程控制
网络协议·tcp/ip·开源
肖坤超1 小时前
Ubuntu 26.04 完美安装和设置
linux·运维·ubuntu
Agent手记1 小时前
成品发货全流程自动化,落地实操与错发漏发规避方案 | 2026企业级Agent端到端落地指南
运维·人工智能·ai·自动化
杂家1 小时前
Docker 容器端口无法从外部访问
运维·服务器·docker·容器
骄傲的心别枯萎1 小时前
WireShark抓取rtsp包
网络·测试工具·wireshark
红茶要加冰1 小时前
二、shell中的变量
linux·运维·shell
189228048611 小时前
NV236美光MT29F32T08GWLBHD6-24TES:B
大数据·服务器·人工智能·科技·缓存
kyle~1 小时前
计算机网络---传输层
网络·计算机网络
杨云龙UP1 小时前
ODA/Oracle 19c CDB/PDB 环境下报错ORA-65162:common user密码过期问题排查与处理_2026-05-15
linux·运维·数据库·oracle·dba·db