Linux内存管理深度实战:从伙伴系统到OOM治理的全方位解析

本文深入剖析Linux内核内存管理的完整技术栈,从物理页面分配器、虚拟内存管理、页面回收与交换、OOM Killer机制到cgroup内存控制,结合生产环境诊断工具与调优实践,为系统工程师提供全方位的内存管理知识体系。

一、内存管理子系统架构总览

Linux内存管理(MM)子系统是内核中最复杂的子系统之一,它负责CPU页表管理、物理内存分配、虚拟地址空间映射、页面回收与交换等核心功能。理解MM子系统的整体架构是排查内存相关问题的基石。

从用户态视角看,每个进程拥有独立的虚拟地址空间(x86_64下通常为128TB用户空间 + 128TB内核空间);从内核态视角,物理内存被划分为节点(NUMA node)→ Zone → Page Frame的三级层次。整个内存管理的核心数据流如下:

层级核心结构职责
NUMA Nodepglist_data隔离不同节点的内存资源
ZonezoneDMA/DMA32/NORMAL/HIGHMEM分区
Page Framestruct page物理页面的元数据描述

关键内核参数:/proc/meminfo、/proc/buddyinfo、/proc/pagetypeinfo、/proc/zoneinfo。生产环境中我们首先通过这些proc文件快速掌握系统内存全局状态。

二、伙伴系统(Buddy System):物理页面分配的核心

2.1 算法原理

伙伴系统解决的是外部碎片问题。它将所有空闲页面按阶(order,即2的幂次)分成11个链表(order 0 ~ 10),order N的块包含 2^N 个连续物理页。当申请 order N 的块时:

  1. 从 order N 的链表中查找,若存在直接分配
  2. 若不存在,向上查找高阶(order N+1, N+2 ...),找到后递归拆分
  3. 拆分后的"伙伴"(buddy)块放入低阶链表
  4. 释放时检查伙伴是否空闲,若空闲则合并升高阶

伙伴(buddy)的判定依据是物理地址:两个大小相同的块,若它们的起始地址只在第 N+1 位不同,则互为伙伴。这意味着伙伴块是连续的、地址对齐的。

2.2 核心数据结构

// mmzone.h
struct free_area {
    struct free_list free_list[MIGRATE_TYPES];
    unsigned long    nr_free;
};

struct zone {
    struct free_area free_area[MAX_ORDER];  // 11个阶的链表
    ...
};

free_list按迁移类型(migrate type)分组,这是Linux为解决碎片而引入的关键机制。

2.3 迁移类型(Migrate Types)

Linux为每个Zone的free_list按迁移类型分类,减少不同类型页面混合导致的无法合并问题:

迁移类型含义典型场景
MIGRATE_UNMOVABLE不可移动内核数据结构、页表
MIGRATE_MOVABLE可移动用户态内存、文件缓存
MIGRATE_RECLAIMABLE可回收不可移动内核缓存( dentries、inodes )
MIGRATE_HIGHATOMIC高阶预留GFP_ATOMIC紧急分配
MIGRATE_ISOLATE隔离使用内存热插拔
MIGRATE_CMA连续内存分配器视频/音频大段连续需求

2.4 页面分配流程(__alloc_pages)

  1. 分配标志检查:解析 gfp_mask(__GFP_HIGHMEM、__GFP_ATOMIC、__GFP_IO、__GFP_DIRECT_RECLAIM等)
  2. 快速路径:alloc_pages_fast_path 直接从PCP(per-cpu pageset)和伙伴系统分配
  3. 慢速路径:alloc_pages_slow_path 执行内存回收(page reclaim)、压缩 compaction、直接回收等
  4. OOM Killer:所有唤醒式回收均失败时触发OOM

2.5 碎片整理(Memory Compaction)

当高阶连续物理页不足时,kcompactd 内核线程被唤醒,扫描Zone中的MIGRATE_MOVABLE页面,将其迁移到一端,腾出连续大块物理内存。关键参数:

/proc/sys/vm/compact_memory         # 手动触发全局压缩
/proc/sys/vm/extfrag_threshold       # 外部碎片阈值
/proc/buddyinfo                      # 各阶空闲块数量

三、Slab分配器:内核小对象分配优化

3.1 为什么需要Slab

伙伴系统以页(4KB)为粒度分配,而内核中大量对象小于一页(如 task_struct ≈ 7KB,dentry ≈ 256B)。若直接用伙伴系统分配小对象,严重浪费内存且增加TLB压力。Slab分配器在伙伴系统之上构建对象缓存池,实现高效的细粒度分配与回收。

3.2 三代分配器

分配器内核版本特点
Slab2.1原始实现,SLAB_ALIGN,每CPU缓存
SLOB2.6极简实现,面向嵌入式
SLUB2.6.23+当前默认,放弃SLAB的复杂队列,简化元数据

SLUB作为当前默认分配器,核心思想是将元数据嵌入页面本身,利用 struct page 的 lru 链表部分来串联空闲对象。

3.3 SLUB核心实现

// SLUB的关键结构
struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;  // 每CPU快速分配
    struct kmem_cache_node *node[MAX_NUMNODES]; // 每Node半满/全满管理
    unsigned int object_size;         // 对象大小
    unsigned int size;           // 包含元数据的总大小
    unsigned int offset;         // 空闲指针偏移
    struct kmem_cache_order_objects oo; // 每slab的页数和对象数
    ...
};

分配优先级:CPU partial → Node partial → 新页分配。释放时优先放回CPU缓存,避免多CPU竞争。

3.4 Slab信息查看与调试

cat /proc/slabinfo                    # 所有缓存的活跃/总对象数、页数
slabtop                               # top式实时监控
cat /sys/kernel/slab/<cache>/aliases   # 该缓存的别名数
echo m > /proc/sysrq-trigger          # 触发Slab内存转储

生产经验:发现某缓存的num_objs远小于active_objs时,可能存在slab泄漏或缓存过大。/proc/slabinfo中的active_objs * obj_size与num_objs * obj_size对比可以计算浪费比例。

四、页表与虚拟内存映射

4.1 多级页表架构

x86_64 Linux采用四级页表(开启五级时为PGD→P4D→PUD→PMD→PTE),48位虚拟地址布局:

16 bits | 9 bits   | 9 bits   | 9 bits   | 9 bits    | 12 bits
  PGD    | P4D/PUD  | P4D/PUD  |   PMD    |   PTE    |  Offset

5级页表(LA57)扩展至57位:PGD→P4D→PUD→PMD→PTE→Offset,每级9位。

4.2 反向映射(Reverse Mapping)

页面回收时需要快速找到映射某物理页的所有PTE(反向映射)。Linux实现了两种机制:

  • 匿名页反向映射(anon_vma):每个匿名页通过 anon_vma 链表连接所有映射它的VMA,fork时通过链表复制共享关系
  • 文件页反向映射(address_space):通过基树(radix tree)或XArray直接查找

4.3 TLB管理

TLB(Translation Lookaside Buffer)缓存页表项,避免每次访存都走页表遍历。TLB miss的代价极高(数十至数百个CPU周期)。Linux对TLB的管理策略:

  • Lazy TLB:上下文切换时不刷新内核空间TLB
  • TLB Shootdown:修改页表后通过IPI通知其他CPU刷新TLB条目
  • PCID(Process Context ID):避免每次切换CR3时全量刷新TLB

五、页面回收(Page Reclaim)

5.1 LRU算法实现

Linux使用双链LRU(Two-list LRU)算法,将页面分为活跃(Active)和非活跃(Inactive)两个链表。页面回收时优先扫描非活跃链表。

// 主要LRU链表
enum lru_list {
    LRU_INACTIVE_ANON,   // 匿名页非活跃
    LRU_ACTIVE_ANON,     // 匿名页活跃
    LRU_INACTIVE_FILE,   // 文件页非活跃
    LRU_ACTIVE_FILE,     // 文件页活跃
    LRU_UNEVICTABLE,     // 不可回收(mlock等)
    NR_LRU_LISTS
};

页面活跃度通过引用位(reference bit)维护:被访问的页移到活跃链表尾部,长时间未访问的页降级到非活跃链表尾部。回收扫描器从非活跃链表头部开始回收。

5.2 kswapd 与直接回收

  • kswapd:后台内核线程,当free_page低于pages_high阈值时唤醒,异步回收直到高于pages_high。回收触发通过wakeup_kswapd()。
  • 直接回收(Direct Reclaim):分配路径发现内存不足时同步阻塞回收,严重损害延迟。触发条件: allocations 低于 pages_low 且 kswapd 忙不过来。

5.3 交换机制(Swapping)

匿名页(Anonymous Page)不像文件页那样可以直接写入底层存储,因此需要专用的Swap区域(分区或文件)。回收匿名页时:

  1. 选择非活跃匿名链表尾部的页面
  2. 分配swap slot,写入磁盘
  3. 更新PTE指向swap entry(编码type + offset到PTE)

关键参数:

/proc/sys/vm/swappiness          # 回收倾向(0=倾向文件,100=均衡,200=倾向匿名)
/proc/sys/vm/vfs_cache_pressure  # 文件缓存回收压力系数
/proc/sys/vm/min_free_kbytes      # 保留最小空闲内存

swappiness 默认60,数据库/Redis 环境常设为 0 或 1 以尽量保留文件缓存;容器中常将 swappiness 视为建议权重而非绝对比例。

5.4 页面压缩(zswap/zram/zsmalloc)

Linux提供了基于压缩的匿名页存储方案:

  • zswap:在 swap 路径中增加压缩缓存层,压缩后真正写出的页面更少
  • zram:创建块设备,匿名页直接存入内存中压缩存储,不写磁盘
  • zsmalloc:zram 的专用分配器,减小内部碎片

六、OOM Killer机制

6.1 触发条件

当所有唤醒式回收和压缩手段都失败,且分配请求不允许使用紧急预留(如 GFP_ATOMIC 的高水位检查也失败)时,内核选择杀害一个进程来释放内存。

6.2 OOM Badness评分算法

OOM Killer通过计算oom_score选择目标进程。评分公式:

points = total_vm (KB) * 1000 / total_ram
adj = oom_score_adj * total_ram / 1000
score = points + adj
  • 占用物理内存越多 → 分数越高(更容易被杀)
  • oom_score_adj 范围 -1000 ~ +1000,-1000 表示不可杀
  • root进程额外获得3%内存加分
  • 运行时间长的进程分数相对降低

相关接口:

/proc/<pid>/oom_score      # 当前得分
/proc/<pid>/oom_score_adj  # 调整值(写-1000可免杀)
/proc/<pid>/oom_adj        # 旧版接口

6.3 OOM Killer的演进

  • Early OOM:Linux 4.x前,用户态监控进程内存,滞后明显
  • ppressure_oom(PSI-based):基于PSI(Pressure Stall Information)内存压力提前触发
  • cgroup OOM:v2支持memory.pressure / memory.oom.group配置cgroup级别OOM策略
  • user reclaim + systemd-oomd:systemd实现的用户态OOM守护进程,更智能地选择目标

七、cgroup内存控制

cgroup v1 和 v2 提供精细的内存资源控制。生产环境重点配置:

7.1 v1内存子系统

/sys/fs/cgroup/memory/<cgroup>/memory.limit_in_bytes        # 硬上限
/sys/fs/cgroup/memory/<cgroup>/memory.soft_limit_in_bytes    # 软上限(不一定生效)
/sys/fs/cgroup/memory/<cgroup>/memory.swappiness            # 组级别swappiness
/sys/fs/cgroup/memory/<cgroup>/memory.oom_control           # OOM Killer开关
/sys/fs/cgroup/memory/<cgroup>/memory.kmem.limit_in_bytes   # 内核内存限制

7.2 v2统一控制

/sys/fs/cgroup/<cgroup>/memory.max          # 硬限制(max = 不限制)
/sys/fs/cgroup/<cgroup>/memory.high          # 触发主动回收的阈值
/sys/fs/cgroup/<cgroup>/memory.low           # 尽量保护不被回收
/sys/fs/cgroup/<cgroup>/memory.min           # 绝对保留(不会被全局回收器扫描)
/sys/fs/cgroup/<cgroup>/memory.oom.group     # 组级别OOM(一杀全杀)

v2相比v1的核心改进:memory.high 实现了渐进式的内存抑制——超过high就回收,超过max才OOM。这使得容器体验更平滑。

八、KSM与内存去重

KSM(Kernel Samepage Merging)内核线程扫描全系统页面,发现内容完全相同的页面时:

  1. 将多个PTE指向同一物理页
  2. 原页面标记为COW(Copy-On-Write),写入时再分裂
  3. 合并率 = 共享页面数 / 扫描页面数

适用场景:KVM虚拟化中多个相同OS实例、容器基础镜像。

/sys/kernel/mm/ksm/run         # 1=开启 0=暂停
/sys/kernel/mm/ksm/pages_to_scan
/sys/kernel/mm/ksm/sleep_millisecs
/sys/kernel/mm/ksm/pages_shared
/sys/kernel/mm/ksm/pages_sharing    # 已共享的物理页数
cat /sys/kernel/mm/ksm/pages_unshared # 已检测但内容不同的页

九、大页(Huge Pages)技术

9.1 HugeTLB 静态大页

预留固定大小的大页池(通常2MB或1GB),分配给明确请求的进程(如DPDK、QEMU、Databasae)。

/proc/sys/vm/nr_hugepages        # 大页总数(启动时预留)
/proc/sys/vm/nr_overcommit_hugepages # 超分配上限
cat /proc/meminfo | grep Huge     # 查看HugePages_Total/Free

大页直接操作PMD级页表(2MB)或PUD级页表(1GB),降低TLB miss率。

9.2 透明大页(THP)

THP是HugeTLB的动态版本,khugepaged内核线程自动合并连续4KB页为2MB大页。

/sys/kernel/mm/transparent_hugepage/enabled   # always/madvise/never
/sys/kernel/mm/transparent_hugepage/defrag    # 碎片整理策略

数据库场景(如Redis、PostgreSQL)通常建议关闭THP(设为never),因为延迟尖刺和内存膨胀的代价大于TLB收益。

十、NUMA内存策略

10.1 NUMA拓扑发现

numactl --hardware                # 查看NUMA拓扑
numastat                          # 各节点的内存分布统计
cat /sys/devices/system/node/node*/meminfo  # 单节点内存详情
cat /proc/<pid>/numa_maps         # 进程的NUMA策略与页面分布

10.2 内存分配策略

策略说明
MPOL_DEFAULT本地分配(LocalAlloc)
MPOL_BIND绑定到指定节点集合
MPOL_PREFERRED优先本地,允许回退
MPOL_INTERLEAVE交错分布到指定节点

通过 set_mempolicy() 系统调用或 numactl --membind=Node 命令行指定。

10.3 NUMA Balancing

Linux内核的自动NUMA均衡机制( numa_balancing 开关默认开启),通过采样缺页中断将页面迁回本地节点。

/proc/sys/kernel/numa_balancing          # 自动NUMA均衡开关
/proc/sys/kernel/numa_balancing_scan_delay_ms  # 扫描延迟
/proc/sys/kernel/numa_balancing_scan_period_max_ms # 扫描间隔

十一、内存诊断与监控工具

11.1 基础工具

# 整体内存概览
free -h
cat /proc/meminfo
vmstat 1                              # 虚拟内存统计
sar -r 1                              # 历史内存报告

# Slab
slabtop -o
cat /proc/slabinfo | sort -k4 -rn    # 按页数排序

# Buddy
cat /proc/buddyinfo                   # 各Zone各阶空闲块

# Page Table
cat /proc/<pid>/maps                  # 进程VMA列表
pmap -x <pid>                         # 详细RSS/PSS

# OOM相关
dmesg | grep -i "out of memory"       # OOM事件日志
journalctl -k | grep -i oom           # systemd journald版

# NUMA
numastat -p <pid>                     # 按进程统计
numastat -m                            # 按Zone统计

11.2 eBPF/BCC 内存追踪

# 页面分配追踪
funclatency mm_page_alloc                  # 页面分配延迟分布
stackcount mm_page_free                    # 页面释放调用链

# Slab分配追踪
slabratetop                                # Slab分配/释放速率
funclatency kmem_cache_alloc               # Slab分配延迟

# OOM追踪
trace 'oom_kill_process struct oom_control *oc' # OOM事件输出

# 缺页中断统计
funclatency handle_mm_fault                # 缺页延迟
biosnoop                                   # 磁盘交换IO

11.3 perf 内存分析

perf stat -e dTLB-load-misses,dTLB-store-misses -p <pid>
perf record -e page-faults -g -p <pid>    # 采样缺页调用栈
perf record -e kmem:kmem_cache_alloc -e kmem:kmem_cache_free

11.4 BPFTrace 脚本示例

// 追踪直接回收事件
kprobe:direct_reclaim_begin {
    @start[tid] = nsecs;
}
kprobe:direct_reclaim_end /@start[tid]/ {
    @lat_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}

// 追踪Slab分配
kprobe:kmem_cache_alloc {
    @alloc_bytes[comm, kstack(5)] = sum(arg1);
}

十二、生产环境调优实践

12.1 通用服务器调优

# 减少交换倾向
vm.swappiness = 10

# 增加文件缓存压力回收
vm.vfs_cache_pressure = 50

# 增加脏页刷新频率(减少突发的写峰值)
vm.dirty_ratio = 40
vm.dirty_background_ratio = 10
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500

# 防止意外OOM杀死系统关键进程
vm.panic_on_oom = 0
vm.oom_kill_allocating_task = 0

12.2 数据库/Redis优化

# 关闭THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 极低swappiness
vm.swappiness = 0                    # Redis官方建议

# 关闭NUMA均衡(若已绑核)
kernel.numa_balancing = 0

# 使用静态大页(防止TLB抖动)
vm.nr_hugepages = ...                # 按Redis内存配置

# 启用mlockall(Redis配置中设置)<怕冷

# cgroup限制
echo "8G" > /sys/fs/cgroup/memory/redis/memory.max
echo "0" > /sys/fs/cgroup/memory/redis/memory.swap.max

12.3 容器/云原生环境

# cgroup v2 内存配置
echo "1G" > /sys/fs/cgroup/workload/memory.max    # 硬上限
echo "800M" > /sys/fs/cgroup/workload/memory.high  # 主动回收触发
echo "200M" > /sys/fs/cgroup/workload/memory.min   # 绝对保留

# PSI监控
cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=123456
# full avg10=0.00 avg60=0.00 avg300=0.00 total=123456

# eBPF追踪容器内存
bpftrace -e 'cgroup:memory: { @[args->cgroup_path] = sum(args->nr_pages); }'

十三、前沿演进:Rust、BPF与内存管理

Linux内存管理领域正在经历两个重大变革:

  • Rust in MM:Linux 6.8+ 引入 Rust 支持,内存管理子系统是Rust重写的优先目标(Binder驱动已开始),减少use-after-free、double-free等内存安全问题
  • eBPF热力图:通过eBPF实时追踪页面分配分布、TLB miss热点,BCC/bpftrace已经是标配,新的memleak.bpf.c可自动发现内存泄漏
  • damon:Data Access MONitor,基于访问热力图的自动页面放置与回收策略,已在机器学习训练场景中显著提升性能

总结

Linux内存管理是一个多层次、多策略协同的复杂系统。物理层通过伙伴系统解决外部碎片,通过SLUB解决内部碎片;回收层通过双链LRU+Swap保证系统持续运行;控制层通过cgroup实现资源隔离;诊断层通过PSI、eBPF、perf提供全方位可观测性。

理解这些机制之后,我们面对内存相关故障时不再只是"加内存"三板斧,而是能够:精确定位症状(TLB miss高?直接回收慢?OOM评分异常?)→ 给出因果解释 → 实施有效调优。这就是深度理解内存管理的工程价值所在。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部