引言

Linux 内核的内存管理是操作系统最核心的子系统之一,它直接决定了系统的稳定性、性能和资源利用率。从底层物理页框的分配与回收,到上层 Slab 小对象缓存机制,再到 OOM(Out-Of-Memory)杀手在极端压力下的决策逻辑,每一个环节都经过了数十年的工程打磨。本文将深入剖析 Linux 内存管理的核心机制,涵盖伙伴系统、Slab/Slub 分配器、LRU 页面回收、OOM Killer 决策流程、透明大页(THP)、cgroup 内存控制以及 eBPF 内存追踪等实战技术。

一、物理内存管理:伙伴系统与 Per-CPU 缓存

1.1 伙伴系统(Buddy System)

Linux 内核以物理页框(Page Frame)作为基本管理单位,通常大小为 4KB。伙伴系统负责组织所有物理内存,支持以 2 的幂次(order)分配连续页框。每个 NUMA 节点维护 11 个空闲区域(free_area),分别对应 order-0 到 order-10(即 1 页到 1024 页/4MB)。

核心机制:

  • 分裂:当低阶空闲链表无空闲块但高阶链表有可用块时,递归将大块对半分裂。
  • 合并:当相邻的伙伴块同时被释放时,自动合并为更大的块并移向高阶链表。
  • 水位线:每个 Zone 维护 min/low/high 三条水位线,决定页面回收的触发时机。

1.2 Per-CPU Page Frame Cache

为减少多核竞争伙伴系统锁的开销,每个 CPU 维护一个单页框本地缓存(pcp-1 到 pcp-16)。当申请单页时优先从 PCP 获取,批量补充/回收的阈值由 /proc/sys/vm/percpu_pagelist_fraction 控制。PCP 极大提升了高频小分配场景的吞吐量。

1.3 Zone 与 NUMA 感知

传统 x86 将物理内存划分为 DMA(0-16MB)、DMA32(16MB-4GB)、Normal(4GB 以上)三个 Zone。在 NUMA 架构下,每个节点内部仍保留 Zone 层级。分配优先级遵循 alloc_pages() 的 GFP 标志位链:先从本地节点的合适 Zone 分配,失败时按 fallback 列表向远程节点推进。

查看伙伴系统状态:

cat /proc/buddyinfo
  Node 0, zone      DMA      1      0      0      0      1      1      1      0      1      1      3
  Node 0, zone    DMA32  6596   3765   1045    278     35      5      0      0      0      0      0
  Node 0, zone   Normal 73580  68095  48253  22148  6828   724    78     3      1      0      0

# 查看 Zone 水位线
cat /proc/zoneinfo | grep -E "Node|zone|min|low|high" | head -20

二、Slab/Slub 分配器:小对象高效复用

2.1 三层架构

Slab 系列分配器建立在伙伴系统之上,专门优化内核中频繁分配释放的固定大小对象(如 task_struct、inode、dentry 等):

  • kmem_cache:每种对象类型对应一个 cache,内部维护本地/部分/全满三个 Slab 列表。
  • kmalloc:基于一组预定义大小的通用 kmem_cache(如 8/16/32/64/96/128/...2048 字节)实现。
  • Slub 默认替代 Slab:从 2.6.23 起为默认分配器,设计更简洁,碎片率更低。

2.2 对象生命周期

每个 Slab 由一个或多个连续页框构成,内部等分槽位存放对象。分配时优先使用 CPU 本地缓存(避免锁竞争),其次从 partial Slab 取空闲槽位;若 partial 耗尽则向伙伴系统申请新 Slab。释放时对象回到本地缓存或直接归还 Slab,当 Slab 全部空闲时归还伙伴系统。

2.3 实战:查看与调优 Slab

# 查看 Slab 使用总量
slabtop

# 查看每个 cache 的详细统计
cat /proc/slabinfo
kmalloc-64           10224   5112   64   63    1 : tunables  ...
kmalloc-128          15420   7710  128   32    1 : tunables  ...
task_struct           1200    600  7104   5    8 : tunables  ...
dentry               31824  31240  192   21    1 : tunables  ...

# Slab 内存占用量
grep Slab /proc/meminfo
Slab:            1238976 kB

# 手动清理可回收 Slab(谨慎使用)
echo 3 > /proc/sys/vm/drop_caches  # 清理 pagecache + slab

Slub 碎片调优参数:

/proc/sys/vm/min_partial  # partial 链表中保留的最少页数
/sys/kernel/slab/*/order   # 每个 cache 的 slab 阶数
/sys/kernel/slab/*/cpu_partial  # 每个 CPU 本地缓存对象上限

三、页面回收与 LRU 算法

3.1 双 LRU 链表机制

Linux 从 2.6 起采用活跃/非活跃双链表(Active/Inactive LRU)设计,每个 NUMA 节点维护 5 类 LRU 链表:

  • Anonymous:匿名页(进程堆、栈)
  • File-backed:文件映射页(共享库、mmap)
  • SWAP/DMA:与硬件直接相关的分类

页面首先进入 Active 链表尾部。如果在下次扫描前被访问(硬件缺页标记 PG_referenced),则晋升到 Active 头部;否则降级到 Inactive。当内存紧张时,kswapd 将 Inactive 链表尾部的页面回收:匿名页写入 Swap,文件页直接丢弃。

3.2 水位线与回收触发

当空闲页低于 wmark_low 时 kswapd 开始后台回收;低于 wmark_min(min_free_kbytes)时进入直接回收(Direct Reclaim),申请进程同步阻塞等待回收完成——这是系统卡顿的常见根因。

# 查看当前水位线
cat /proc/zoneinfo | grep -A2 "pages free"

# 调整 min_free_kbytes(建议按总内存 0.5%~1%)
sysctl -w vm.min_free_kbytes=1048576  # 1GB 系统建议 64MB

# kswapd 回收的统计
grep -E "pgsteal|pgscan" /proc/vmstat
  pgscan_kswapd_normal  1234567
  pgsteal_kswapd_normal  987654

3.3 Swap 与 Swappiness

vm.swappiness(范围 0-200)控制匿名页与文件页回收的比例偏好。默认值 60 意味着二者权重相当。在数据库等场景中设为 1-10 可显著减少 Swap 抖动(但不会完全禁用 Swap,仅在 OOM 前仍可能触发)。

# 临时设置
sysctl -w vm.swappiness=10

# 永久设置
echo "vm.swappiness=10" >> /etc/sysctl.d/99-sysctl.conf

# 查看 Swap 使用
free -h
swapon --show
vmstat 1 5  # 观察 si/so 是否频繁非零

四、OOM Killer:内存耗尽时的最后防线

4.1 触发条件

当系统所有内存(包括 Swap)耗尽、直接回收无法满足需求、且所有 Zone 都低于 min 水位线时,内核调用 out_of_memory() 选择牺牲进程。panic_on_oom 控制是否在 OOM 时直接触发 kernel panic(常用于关键生产环境)。

4.2 OOM Score 计算

内核通过 oom_badness() 为每个进程计算 OOM Score:

  • 比例因子:进程使用的物理内存(含 RSS + Swap)占总内存的比例
  • 调整参数:oom_score_adj(范围 -1000 ~ +1000,-1000 表示永不杀死)
  • 特权进程、内核线程(PF_NO_SETAFFINITY)通常获得 1/32 的评分折扣
# 查看进程 OOM 评分
cat /proc/1/oom_score          # 当前分数
cat /proc/1/oom_score_adj      # 调整值(默认 0)
echo -1000 > /proc/1/oom_score_adj  # 保护 systemd 不被 OOM 杀死

# 快速查看最容易被杀死的进程
ps aux --sort=-%mem | head -10
  USER  PID %CPU %MEM    VSZ   RSS TTY  STAT START   TIME COMMAND
  mysql 1234 2.1 85.5 12.3g 11.8g ?    Ssl  10:00 234:56 /usr/sbin/mysqld

4.3 OOM 事件后的分析

OOM Killer 触发的消息会写入 dmesg,包含:触发进程、被杀死进程、释放内存量以及各 Zone 的内存水位状态。这些信息是排查内存瓶颈的关键线索。

dmesg | grep -i "out of memory"
  [123456.789] Out of memory: Killed process 5678 (java) total-vm:12345678kB
  [123456.789] oom_memcg: memcg=/user.slice, memory: usage 8589934592, limit 8589934592

五、透明大页(THP)

透明大页将 2MB 或 1GB 的巨页透明化,减少 TLB Miss 并提升内存访问连续性。内核后台线程 khugepaged 负责在适当时机将普通页集合并为巨页。

# 查看 THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled   # [madvise|always|never]
cat /sys/kernel/mm/transparent_hugepage/defrag     # 碎片整理策略

# 建议:数据库场景设为 madvise,通过 madvise() 按需使用
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

# 查看 HugePages 使用
grep -i huge /proc/meminfo
  HugePages_Total:      64
  HugePages_Free:       32
  Hugepagesize:       2048 kB

实战建议:虚拟化/数据库等延迟敏感场景适合 always;内存碎片化严重或需要精细控制的场景建议 madvise 或 never。

六、cgroup 内存控制

cgroup v1/v2 提供层级化内存资源隔离,是现代容器内存管理的基石:

  • memory.limit_in_bytes:硬限制,超出即触发 OOM 或阻塞
  • memory.soft_limit_in_bytes:软限制,仅在全局内存紧张时生效
  • memory.kmem.limit_in_bytes:限制内核内存(Slab 等)
  • memory.swappiness:cgroup 级 swappiness 偏好
# cgroup v2 内存控制(systemd)
systemctl set-parameter  MemoryMax=2G
systemctl set-parameter  MemoryHigh=1.5G  # 软限制

# 查看容器内存统计
cat /sys/fs/cgroup/memory/docker/.scope/memory.current
cat /sys/fs/cgroup/memory/docker/.scope/memory.stat
  anon  536870912        # 匿名页占用 (512MB)
  file  1073741824       # 文件页占用 (1GB)
  slab  268435456        # Slab 占用 (256MB)
  pgfault  9876543       # 缺页异常总数
  oom_kill  0            # OOM 触发次数

七、eBPF 内存追踪

eBPF 提供对内存子系统的无损观测能力,覆盖从页分配到 OOM 的完整链路:

7.1 关键 Tracepoint

  • kmem:mm_page_alloc:每次伙伴系统分配物理页
  • kmem:mm_page_free:页框归还伙伴系统
  • vmscan:mm_vmscan_kswapd_wake:kswapd 唤醒事件
  • oom:mark_victim:OOM Killer 选定牺牲进程

7.2 BCC/bpftrace 实战

# 实时监控 >4 阶的大块分配
bpftrace -e ' tracepoint:kmem:mm_page_alloc /args->order > 4/ { 
  printf("order=%d gfp=%x comm=%s\n", args->order, args->gfp_flags, comm); 
}'

# 统计每个进程的 RSS 变化(每 1 秒)
bpftrace -e ' kprobe:do_anonymous_page { 
  @rss[comm] = sum(arg1); 
} interval:s:1 { print(@rss); clear(@rss); }'

# 监控 Direct Reclaim(高延迟根因)
bpftrace -e ' tracepoint:vmscan:mm_vmscan_direct_reclaim_begin { 
  @start[tid] = nsecs; 
} tracepoint:vmscan:mm_vmscan_direct_reclaim_end /@start[tid]/ {
  @lat[comm] = hist(nsecs - @start[tid]); delete(@start[tid]); 
}'

7.3 perf 内存剖析

perf record -e kmem:mm_page_alloc -g -p $(pidof myapp)
perf report --sort=comm,dso

# 实时监控缺页异常率
perf stat -e page-faults,dTLB-load-misses -p 1234 5

八、性能调优全景

不同场景下的推荐配置:

# 1. 高吞吐服务器(数据库/缓存)
vm.swappiness=1
vm.dirty_ratio=10
vm.dirty_background_ratio=5
vm.min_free_kbytes=262144  # 256MB on 128GB
sysctl -w kernel.mm.transparent_hugepage.enabled=madvise

# 2. 桌面/开发环境(交互响应优先)
vm.swappiness=60
vm.dirty_ratio=20
vm.dirty_background_ratio=10
kernel.mm.transparent_hugepage.enabled=always

# 3. 嵌入式/低内存设备
vm.swappiness=100
vm.overcommit_memory=0
vm.min_free_kbytes=4096
kernel.mm.transparent_hugepage.enabled=never

OOM 综合防护策略:

  • 关键服务(DB、MQ)设置 oom_score_adj=-1000
  • 通过 cgroup MemoryMax 限制非关键容器
  • 部署 earlyoom/usermode OOM 守护进程提前介入
  • 监控 pgscan_kswapd/pgscan_direct 比值,提前预警

总结

Linux 内存管理是一个高度层次化的工程杰作:伙伴系统确保物理页框的高效组织,Slub 分配器让内核对象分配零碎片化,LRU 与 kswapd 实现优雅的渐进式回收,OOM Killer 在极端情况下做出最小代价的止损决策。掌握这些机制不仅能帮助我们排查内存泄漏、OOM 突刺等生产问题,更能指导我们在容器化、NUMA 以及性能敏感场景下做出正确的全局配置选择。借助 eBPF 等现代观测工具,内存问题不再是"黑盒",而是完全可量化、可追踪、可优化的透明系统。

点赞(0) 打赏

评论列表 共有 0 条评论

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

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部