Wiki LogoWiki - The Power of Many

内存管理

从虚拟地址空间布局到物理内存分配算法, 系统性解析 Linux 内核的内存管理机制.

Linux 内存管理是一个极度精巧的子系统, 它的目标是在保证进程启动速度、运行效率的同时, 最大化利用物理内存资源. 理解内存管理不仅有助于排查 OOM (Out of Memory) 问题, 更是进行系统级调优的基础.


1. 虚拟内存与地址空间

Linux 并没有让进程直接访问物理内存, 面对每个进程提供了一个连续的、私有的虚拟地址空间.

1.1 为什么需要虚拟地址?

  1. 隔离性: 进程间无法直接访问彼此的地址空间, 提高了安全性.
  2. 连续性: 尽管物理内存可能支离破碎, 但虚拟内存让进程以为自己拥有连续的地址块.
  3. 扩展性: 虚拟内存可以大于物理内存 (通过分页交换到磁盘).

1.2 映射机制: MMU 与页表

虚拟地址到物理地址的转换由硬件 MMU (Memory Management Unit) 完成. 为了减少映射表的体积, 内核采用多级页表结构 (通常为 4 级或 5 级), 仅在真正分配内存时才建立对应的页表项.

1.3 缺页中断 (Page Fault): 内存分配的终点

Linux 采用延迟分配 (Lazy Allocation) 策略, malloc 仅修改进程的 VMA (Virtual Memory Area) 而不分配物理内存. 真正的分配发生在触发异常时:

  1. Minor Page Fault: 物理页已在内存中 (如共享库或 Page Cache), 仅需在当前进程页表中建立映射.
  2. Major Page Fault: 物理页不在内存中, 必须触发磁盘 I/O (从交换分区或文件系统) 读入数据.
  3. Invalid Fault: 访问了非法地址 (如 Null Pointer), 内核发送 SIGSEGV 信号终止进程.

2. 物理内存的组织结构

内核对物理内存的管理遵循 Node -> Zone -> Page 的层次结构.

2.1 NUMA 与 Node

在现代多路服务器中, 通常采用 NUMA (Non-Uniform Memory Access) 架构. 物理内存被划分为多个 Node, CPU 访问本地 Node 的内存速度远快于访问远程 Node.

2.2 Zone (管理区)

每个 Node 下又被划分为多个 Zone, 常见的包括:

  • ZONE_DMA: 供老旧硬件使用的低端地址空间.
  • ZONE_DMA32: 64 位系统中 4GB 以下的区域.
  • ZONE_NORMAL: 正常分配的内存区域.

2.3 Page (页)

内存管理的最小单位是 , 在 x86_64 架构下默认为 4KB.


3. 内存分配与碎片管理

内核的分配策略本质上是在满足随机大小申请的需求与抑制内存碎片 (Memory Fragmentation) 之间寻求平衡.

3.1 伙伴系统 (Buddy System) 与物理内存合并

  • 分配与分割: 伙伴系统将物理内存划分为 11 个阶 (Order), 每个阶包含 2^n 个连续页. 申请时, 若目标 Order 无空闲块, 则从高阶分割 (Split).
  • 回收与合并 (Merging): 当一个内存块被释放时, 内核会检查其 "伙伴" (相邻且同阶的块) 是否也为空闲. 若是, 则将其合并为高一阶的连续内存. 这种递归合并机制是维护整体连续性的核心.

3.2 内存碎片回避 (Anti-Fragmentation)

为了防止外部碎片 (External Fragmentation), 现代内核引入了 MIGRATETYPE 机制, 将物理内存按照 "迁移性" 分类管理:

  1. UNMOVABLE: 无法移动的页 (如内核栈、内核镜像). 这种页的碎片化对系统伤害最高.
  2. RECLAIMABLE: 可以回收但不能移动的页 (如 Filesystem Caches).
  3. MOVABLE: 可以随意移动的页 (如用户态匿名页).
  • 意义: 通过将不同迁移性质的页隔离在不同的 Pageblock 中, 即使 MOVABLE 区域支离破碎, 内核也可以通过 "搬家" (Compaction) 来重新整合大块内存, 而不会被 UNMOVABLE 的页“钉死”在中间.

3.3 Slab / Slub 分配器

  • 解决内部碎片 (Internal Fragmentation): 对于小对象申请, 伙伴系统的最小粒度 (4KB) 过大. Slab 分配器通过缓存 (Caches) 机制, 将页切割为固定大小的槽位 (Slots), 确保极高的利用率和 CPU 缓存命中率.

4. 统计指标与内存分析

在进行内核级调优或内存抖动排障时, 仅看 top 的内存列是极具欺骗性的. 必须从进程视角和系统视角两个维度展开.

4.1 进程视角: VSS, RSS, PSS 与 USS

由于内核支持内存共享 (Shared Libraries, Copy-on-Write Pages), 一个物理页可能同时映射到多个进程.

指标全称深度解析
VSSVirtual Set Size虚拟地址总量. 包含分配但未使用的 Page, 映射的文件, 链接库等. 对物理内存压力几乎无参考价值.
RSSResident Set Size驻留内存总量. 进程占用的所有物理页. 缺陷在于它会重复计算共享库, 多个进程相加会远超物理内存总和.
PSSProportional Set Size比例驻留内存. 最核心的指标. 将共享页的大小比例地分摊给所有映射进程. $PSS = USS + (Shared / Processes)$.
USSUnique Set Size独占驻留内存. 进程私有的物理内存. 如果杀死该进程, 系统能立刻回收的最小内存量.

4.2 匿名页 (Anonymous) vs 文件页 (File-backed)

理解回收逻辑的前提是区分页面的 "背景":

  • 匿名页 (Anonymous): 指堆 (Heap)、栈 (Stack)、数据段等没有物理文件对应的内存. 它们是进程的私有资产. 回收时必须将其写入 Swap 分区.
  • 文件页 (File-backed): 指程序代码段、共享库、或通过 mmap 映射的文件. 由于物理磁盘上有备份, 回收时 "Clean" 页可直接丢弃, "Dirty" 页则需通过 pdflush/writeback 写回.

4.3 系统视角: Buffers 与 Cached

free -m 命令中, 这两个概念常被混淆:

  • Buffers: 原始磁盘块的临时缓存 (Block I/O). 主要用于记录文件系统的元数据 (Metadata) 或直接磁盘访问 (Raw Disk I/O).
  • Cached (Page Cache): 物理文件的页缓存. 当你读取一个文件时, 内核会将其数据块缓存在内存中.
  • 核心逻辑: 只要内存不紧张, Linux 会竭尽全力扩大 Page Cache 的占比, 以减少磁盘访问消耗. 这正是 "Windows 会提示内存不足, 而 Linux 总是显示内存用光" 的哲学差异.

5. 内存回收机制 (Memory Reclamation)

Linux 的回收机制是一套复杂的优先级调度系统, 旨在通过牺牲一部分数据访问速度来换取操作系统的持续可用性.

5.1 内存回收的两条路径

内核根据压力的紧迫程度, 走两条截然不同的代码路径:

  1. 异步回收 (Background Reclamation / kswapd):
    • 触发条件: 剩余内存降至 low 水位线以下.
    • 特性: 独立的内核进程执行, 与当前进程异步, 不阻塞用户请求. 这是系统平稳运行时的首选方案.
  2. 直接回收 (Direct Reclamation):
    • 触发条件: 内存压力极大, 降至 min 水位线甚至分配失败.
    • 特性: 同步阻塞所有申请内存的线程, 强行执行回收逻辑. 这是导致应用死慢 (Stalling) 或长尾延迟的主要元凶.

5.2 内存整理 (Compaction)

当系统有足够的总内存但缺乏连续的大块内存时, 会启动 Compaction (内存紧缩):

  • 原理: 内核通过扫描区域, 将散落在末端的 Movable 页迁移到前端的空隙中, 从而在末端“合”出大块连续空间.
  • 应用: 这是满足大页 (HugePages) 申请和减少碎片的核心手段.

5.3 内存合并: KSM (Kernel Samepage Merging)

对于虚拟化或容器环境, 可能会有大量内容完全相同的页 (如多个 OS 镜像的重复代码段).

  • 机制: KSM 扫描物理内存, 发现内容一致的页时, 将其合并为单一的物理页, 并标记为 Copy-On-Write (COW).
  • 优势: 极大提升了内存密集型应用的整合率.

5.4 内存扫描核心: LRU 链表机制

Linux 的回收精度依赖于对页面活跃度的精准追踪, 这是通过 LRU (Least Recently Used) 算法的变体实现的.

5.4.1 五大 LRU 链表

内核在每个 Memory Node (及 Cgroup) 下维护了 5 组双向链表, 将页面的生命周期进行物理隔离:

  1. Inactive Anon: 长期未访问的匿名页 (Heap/Stack/Shared Memory), 是优先交换的目标.
  2. Active Anon: 最近被高频访问的匿名页.
  3. Inactive File: 长期未访问的文件缓存 (Page Cache), 优先丢弃.
  4. Active File: 热点文件数据, 受到内核保护.
  5. Unevictable: 被 mlock() 锁定的页, 不参与任何回收逻辑.

5.4.2 二位淘汰与晋升逻辑 (Two-Tier Strategy)

为了防止一次性的突发性大扫描 (如 cp 大文件) 冲垮缓存, 内核采用了复杂的二阶段判断:

  • Inactive -> Active (晋升): 只有当一个页在 Inactive 列表中被第二次访问时 (通过页表项的 Accessed 位标记), 它才会被移入 Active 链表.
  • Active -> Inactive (降级): 当 Inactive 扫描不足时, 内核会从 Active 链表末端剥离页面, 检查其引用位, 若引用位清零则降级至 Inactive.

5.4.3 现代进化: MGLRU (Multi-Gen LRU)

传统的 LRU 饱受 "扫描开销大" 和 "反馈滞后" 的诟病. 从内核 v6.1 开始, 引入了 MGLRU:

  • 多代管理: 将页面划分为多个 "代 (Generations)", 类似分代垃圾回收.
  • 性能提升: 通过更精细的年龄采样, 显著减少了页表遍历带来的内核态 CPU 消耗, 在高内存压力下能将系统响应速度提升高达 40%.

5.4.4 Swappiness 的数学本质

vm.swappiness 的值 (0-200) 是一个比率调节器, 用于在平衡扫描时计算 Anon_CostFile_Cost.

  • 公式推导: 扫描压力 = $f(priority, swappiness)$.
  • 策略选择: 增加该值会降低匿名页的虚拟回收成本, 强制内核在保留 Page Cache 的同时将更多堆栈数据推向 Swap 分区.

5.5 OOM Killer 决策链

当所有手段失效, out_of_memory() 函数将被调用:

  • oom_score 计算: (RSS + Swap) / Total_Memory * 10.
  • 保护机制: 系统进程或被标记为 oom_score_adj = -1000 的进程被豁免. 内核倾向于杀掉那个“内存占用率高且生存周期较短”的贪婪进程.

6. 高级内存特性

6.1 大页 (HugePages)

在处理数十 GB 甚至 TB 级别的内存时, 标准的 4KB 页会导致显著的性能瓶颈, 核心原因在于 TLB (Translation Lookaside Buffer) 的开销.

  • TLB 的角色: TLB 是 CPU 内部的一个高速缓存, 专门用于存放虚拟地址到物理地址的映射关系 (页表项). 由于 CPU 访问内存的速度远慢于寄存器, 如果每次内存操作都要去内存中查询页表, 性能将大幅下降. TLB 缓存了最近使用的映射, 使得地址转换可以在极短时间内完成 (TLB Hit).
  • 4KB 页的挑战: 对于一个 64GB 内存的数据库应用, 如果使用 4KB 页, 理论上需要维护 1600 万个页表项. 这个数量远远超过了 CPU TLB 的缓存容量, 导致频繁发生 TLB Miss. 此时 CPU 必须回退到慢速的内存页表查询 (Page Walk), 极大地增加了指令延迟.
  • HugePages 的优势: 通过将页大小提升至 2MB 甚至 1GB, 同样的内存容量所需的页表项数量减少了数百倍.
    1. 提高 TLB 命中率: 较少的条目意味着更多的映射可以驻留在 CPU 缓存中.
    2. 减少页表级数: 虚拟地址转换路径变短, 减少了 Page Walk 的深度.
    3. 减少管理开销: 降低了内存分配和回收时的锁竞争.

常见实现方式:

  • Static HugePages: 在系统启动时通过内核参数预留. 它们被锁定在物理内存中, 不会被交换 (Swap) 或回收, 适合数据库 (如 Oracle, PostgreSQL) 的共享内存段.
  • THP (Transparent HugePages): 内核在后台尝试自动合并相邻的小页. 虽然降低了配置门槛, 但在某些高度随机访问的场景下, 后台合并动作 (Compaction) 可能会引发不可监测的 CPU 突发性高负载.

6.2 交换分区 (Swap)

Swap 不仅仅是物理内存的备胎, 它更重要的意义在于将那些长期不使用的 "匿名页" 换出, 给活跃的 "文件页 (Page Cache)" 腾出空间, 从而提升整体 I/O 高效性.

6.3 零拷贝 (Zero-copy): 绕过用户态的中转站

传统的 I/O 需要在内核缓冲区与用户缓冲区之间进行多次数据拷贝 (Copying), 这会消耗大量 CPU 周期的上下文切换开销.

核心机制:

  1. mmap + write: 通过将内核缓冲区映射到用户空间, 减少了一次从内核态到用户态的拷贝, 但仍需一次系统调用.
  2. sendfile: 纯内核态操作. 数据直接从文件读取到 Page Cache, 随后由 DMA 直接搬运到网卡缓冲区. 实现了 Zero-CPU-Copy.
  3. splice / tee: 在两个文件描述符 (通常是 Pipe) 之间进行数据搬运, 仅移动映射而不移动数据本身.
  4. MSG_ZEROCOPY: 用于高性能网络, 配合硬件 DMA 允许应用程序在不拷贝数据的情况下发送报文, 极大地降低了内存总线带宽压力.

理解内存管理是一场关于 "空间换时间" 与 "精细平衡" 的权衡. 系统性能的优劣, 往往取决于内核如何在快速分配与有效回收之间维持那个动态的平衡点.

On this page