Linux 内核内存管理是操作系统最复杂的子系统之一,直接关系到系统性能、稳定性和资源利用率。本文从内核源码级别剖析内存管理的核心机制,涵盖页表映射、Slab 分配器、伙伴系统、NUM OOM Killer、透明大页等核心话题,并结合容器化和云原生环境给出生产级调优方案。

1. 内存管理全景架构

Linux 内存管理采用分层架构,自底向上分为:物理内存层、内核空间层、用户空间层。每个进程拥有独立的虚拟地址空间,内核将物理内存抽象为 struct page 结构体,系统启动时每个物理页面对应一个 page 描述符。

// 核心数据结构:内存描述符
struct mm_struct {
    pgd_t *pgd;                 // 页全局目录(顶级页表)
    struct vm_area_struct *mmap; // VMA 链表
    atomic_t mm_users;          // 用户计数
    atomic_t mm_count;          // 引用计数
    unsigned long total_vm;     // 总虚拟页面数
    unsigned long locked_vm;    // 锁住不能换出的页面
    unsigned long pinned_vm;    // 永久锁住的页面
    unsigned long data_vm;      // 数据段页面
    unsigned long exec_vm;      // 代码段页面
    unsigned long stack_vm;     // 栈段页面
};

虚拟内存区域(VMA)是内存管理的基本单元,定义了一段连续虚拟地址空间的访问权限和保护属性。内核通过红黑树管理 VMA,支持 O(log n) 的查找效率。

2. 页表与 TLB 优化

四级页表架构(x86_64):PGD → P4D → PUD → PMD → PTE,对应 48 位虚拟地址空间(256TB)。5 级页表(LA57)扩展至 57 位地址空间(128PB)。

// 虚拟地址四级页表索引
// 63    48 47    39 38    30 29    21 20    12 11     0
// | PML4  |  PDPT  |  PD    |  PT    | 偏移  |
//   9bit    9bit    9bit    9bit    12bit

#define PML4_SHIFT  39
#define PDPT_SHIFT  30
#define PD_SHIFT    21
#define PT_SHIFT    12

// 各级索引计算
#define PML4_INDEX(addr)  (((addr) >> PML4_SHIFT) & 0x1FF)
#define PDPT_INDEX(addr)  (((addr) >> PDPT_SHIFT) & 0x1FF)
#define PD_INDEX(addr)    (((addr) >> PD_SHIFT) & 0x1FF)
#define PT_INDEX(addr)    (((addr) >> PT_SHIFT) & 0x1FF)

TLB(Translation Lookaside Buffer) 是页表的硬件缓存,现代 CPU 采用多级 TLB 设计:L1 ITLB/DTLB(通常 64-128 项)、L2 STLB(通常 512-2048 项)。TLB miss 会触发页表遍历,导致 30-100 个周期的惩罚。

透明大页(Transparent Huge Pages, THP) 通过 2MB 大页(或 1GB 巨页)减少 TLB miss 率:

// 查看 THP 状态
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never

// 查看大页使用情况
$ grep Huge /proc/meminfo
HugePages_Total:       0
HugePages_Free:        0
AnonHugePages:    491520 kB     // 当前使用 AnonHugePages
Hugepagesize:       2048 kB

// 数据库等内存密集型应用建议关闭 THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

注意:THP 在数据库场景(Redis/PostgreSQL/MySQL)中可能导致延迟抖动,因为 khugepaged 后台线程在碎片整理时会引入额外的 CPU 和内存开销。

3. 伙伴系统(Buddy System)

伙伴系统是物理内存分配的基础算法,将页面按阶(order)组织为 11 个空闲链表(order 0-10),分别对应 1, 2, 4, 8, ..., 1024 个连续页面(最大 4MB)。

// 核心操作:分配与释放
// alloc_pages(gfp_mask, order) → 分配 2^order 个页面
// __free_pages(page, order)     → 释放页面

// 伙伴查找:地址相差 2^order 的块为伙伴
static inline struct page *find_buddy_page(struct page *page, unsigned int order)
{
    unsigned long buddy_pfn = page_to_pfn(page) ^ (1UL << order);
    return pfn_to_page(buddy_pfn);
}

// 合并条件:
// 1. 两个块大小相同(同为 2^order)
// 2. 地址连续
// 3. 均为空闲状态
// 4. 合并后上级伙伴也空闲则递归合并

内存碎片 是伙伴系统的主要挑战。内核将页面分为三类迁移类型:

  • MIGRATE_UNMOVABLE:内核对象、页缓存(不可移动)
  • MIGRATE_MOVABLE:用户空间页面、可移动的文件缓存
  • MIGRATE_RECLAIMABLE:可回收的 slab 对象

内存碎片整理(Memory Compaction) 通过迁移可移动页面来创建更大的连续物理块:

// 触发碎片整理
echo 1 > /proc/sys/vm/compact_memory

// 查看碎片情况
$ cat /proc/buddyinfo
Node 0, zone      DMA      1    0    1    0    2    1    1    0    1    1    3
Node 0, zone    DMA32   6523  4523 3349 1502  607  247   95   40   10    3    2
Node 0, zone   Normal  34598 12041 5586 1423  408  113   28    7    2    1    0

// 高阶连续页面越少,碎片越严重

4. Slab 分配器

Slab 分配器建立在伙伴系统之上,解决内核对象频繁创建/销毁的内存碎片和缓存效率问题。Linux 支持三种变体:

分配器特点默认场景
Slab最早实现,slab 描述符复杂内核早期
Slob极简设计,代码少嵌入式/小内存
Slub优化调试、减少元数据服务器/桌面(默认)

Slab 的核心优势:

  • 对象缓存:相同大小的对象复用已分配的 slab,避免伙伴系统开销
  • 硬件缓存着色:不同 slab 偏移不同缓存行,减少 CPU 缓存冲突
  • per-cpu 缓存:CPU 本地空闲对象链表,无锁快速分配

// Slab 缓存创建与使用
struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,
                                     unsigned int align, slab_flags_t flags,
                                     void (*ctor)(void *));

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
void kmem_cache_free(struct kmem_cache *cachep, void *objp);

// 通用分配:kmalloc 基于预创建的 kmem_cache
void *kmalloc(size_t size, gfp_t flags);
// 内部查表选择最接近的 slab 缓存:
// 8, 16, 32, 64, 96, 128, 192, 256, 512, 1024, 2048, 4096, 8192 字节

Slub 调试功能:


// 开启 slab 调试(检测越界、use-after-free)
slab_debug=FPZ

// /proc/slabinfo 查看统计
$ cat /proc/slabinfo | head -10
slabinfo - version: 2.1
# name                 : tunables    : slabdata   
dentry             236784  237204     192   42    2 : tunables    0    0    0 : slabdata   5647   5647
inode_cache         53484   53776    648   52    8 : tunables    0    0    0 : slabdata   1034   1034
vm_area_struct      48978   49140    208   39    2 : tunables    0    0    0 : slabdata   1260   1260

5. OOM Killer 机制

当系统内存耗尽且无法通过回收获得足够内存时,内核触发 OOM (Out-Of-Memory) Killer,选择一个进程终止以释放内存。


// OOM 评分:每个进程都有 oom_score (0-1000)
// 评分越高越容易被 kill

// 手动调整 OOM 分数(-1000 表示不 kill)
echo -1000 > /proc/<pid>/oom_score_adj

// Docker 容器中常见操作:保护数据库进程
echo -1000 > /proc/$(pidof mysqld)/oom_score_adj

// 查看 OOM 评分
$ cat /proc/1/oom_score
0
$ cat /proc/1/oom_score_adj
-1000    // systemd 默认受保护

oom_score 计算公式:


// 基础分 = 物理内存使用占比(rss + swap + page_table + pte)
// oom_score = base_score + (oom_score_adj * 1000 / 1000)
// 限制在 [0, 1000] 范围内

OOM Killer 调优参数:


// 触发 OOM 前是否先尝试回收 (默认 1)
vm.panic_on_oom = 0          // 不 panic,触发 killer
// vm.panic_on_oom = 1       // 直接 panic(无 swap 场景)
// vm.panic_on_oom = 2       // 强制 panic

// overcommit 策略
vm.overcommit_memory = 0     // 启发式(默认)
// vm.overcommit_memory = 1  // 总是 overcommit
// vm.overcommit_memory = 2  // 严格检查,超过 CommitLimit 拒绝

vm.overcommit_ratio = 50     // 可用内存百分比

6. NUMA 架构感知优化

NUMA(Non-Uniform Memory Access)架构下,每个 CPU 节点有本地内存和远程内存。本地访问延迟低(~100ns),远程访问延迟高(~300ns+)。


// 查看 NUMA 拓扑
$ numactl --hardware
available: 2 nodes (0, 1)
node 0 cpus: 0 1 2 3 ... 31
node 0 size: 51551 MB
node 0 free: 12034 MB
node 1 cpus: 32 33 ... 63
node 1 size: 51551 MB
node 1 free: 38492 MB
node distances:
node   0   1
  0:  10  21  // 本地 10,远程 21(相对延迟)
  1:  21  10

NUMA 感知内核优化策略:

  • zone_reclaim_mode:是否优先回收/压缩本地节点内存
  • AutoNUMA Balancing:内核自动将访问频繁的页面迁移到本地节点
  • Per-Node Slab:Slab 分配器 per-node 缓存,避免跨节点分配

// 绑定进程到指定 NUMA 节点
numactl --cpunodebind=0 --membind=0 ./your_application

// MySQL 场景:绑定到 node 0,避免跨节点内存访问
numactl --cpunodebind=0 --membind=0 mysqld

// 查看进程 NUMA 策略
$ numastat -p $(pidof mysyql)
Per-node process memory (in MBs) for PID 12345
--------------------------------------------------------------------
Node 0    32456     // 节点 0 上分配 32GB
Node 1       12     // 节点 1 上几乎无分配
--------------------------------------------------------------------
Total     32468

7. 内存回收与交换

Linux 采用 LRU(Least Recently Used)变体算法回收内存,维护两类 LRU 链表:

  • Active:活跃页面,近期被访问过
  • Inactive:不活跃页面,是回收候选

页面在 Active 和 Inactive 之间移动(二次机会算法),最终被回收的候选是 Inactive 列表尾部的页面。


// 查看 LRU 统计
$ cat /proc/zoneinfo | grep -E "nr_|scr"
nr_free_pages 1203456
nr_zone_inactive_anon 234567
nr_zone_active_anon 1234567
nr_zone_inactive_file 1987654
nr_zone_active_file 8765432
nr_mlock 4567

// 触发手动回收
echo 3 > /proc/sys/vm/drop_caches
// 1: pagecache, 2: slab, 3: both

zswap / zram 压缩交换:将匿名页面压缩后存储在内存中,避免磁盘 I/O:


// 启用 zram 压缩设备
modprobe zram num_devices=1
echo 8G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon /dev/zram0 -p 32767

// zswap:在 swap 前加一层压缩缓存
echo 1 > /sys/module/zswap/parameters/enabled
echo zstd > /sys/module/zswap/parameters/compressor  // zstd 压缩比高
echo 20 > /sys/module/zswap/parameters/max_pool_percent  // 最大 20% 内存

8. 容器与云原生内存管理

cgroup v2 统一了内存资源管理,相比 v1 在统计准确性和一致性上有了显著改进:


// cgroup v2 内存控制
/sys/fs/cgroup/
├── memory.max          // 硬限制(等价于 old memory.limit_in_bytes)
├── memory.high         // 软限制,超过即开始回收
├── memory.low          // 保护性内存,尽量不被回收
├── memory.min          // 最低保证,即使全局内存不足也不回收
├── memory.swap.max     // swap+内存 总限制
├── memory.oom.group    // 组级 OOM:整个 cgroup 一起被 kill
└── memory.stat         // 详细统计

// Kubernetes 场景
resources:
  requests:
    memory: "4Gi"      // 对应 memory.min(最低保证)
  limits:
    memory: "8Gi"      // 对应 memory.max(硬限制)

K8s 内存工作的关键机制:

  • QOS Class:Guaranteed(req==limit)、Burstable(req<limit)、BestEffort(未设置)
  • OOM Score:根据 QOS 和内存使用率计算,BestEffort 最先被 kill
  • QoS Cgroup Hierarchy:kubepods → besteffort/burstable/guaranteed → pod

// 查看 Pod 实际 cgroup 内存限制
$ kubectl describe pod my-pod | grep -A2 Memory
$ cat /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podXXX.slice/memory.max
8589934592    // 8GB

// 监控容器内存使用
$ kubectl top pod my-pod --containers
POD          NAME    CPU(cores)   MEMORY(bytes)
my-pod       main    250m         2.1Gi     // 接近上限时 OOMKilled

9. 生产环境调优实战

高性能服务器推荐配置:


# /etc/sysctl.d/99-memory-tuning.conf

# 1. 关闭透明大页(数据库/Redis 场景)
echo never > /sys/kernel/mm/transparent_hugepage/enabled

# 2. 调整 swappiness(减少不必要的 swap)
vm.swappiness = 1              // 1-10,极低

# 3. 减少脏页刷新频率(降低写入峰值)
vm.dirty_ratio = 40             // 达到 40% 脏页才开始背景刷新
vm.dirty_background_ratio = 10  // 10% 时开始背景 flush
vm.dirty_expire_centisecs = 3000 // 脏页过期时间 30s
vm.dirty_writeback_centisecs = 500 // 刷新周期 5s

# 4. 内存 overcommit 策略
vm.overcommit_memory = 0       // 启发式
vm.overcommit_ratio = 80       // 可使用物理内存 80%

# 5. NUMA 优化
kernel.numa_balancing = 1     // 开启 AutoNUMA

# 6. OOM 保护关键服务
# 在服务启动脚本中:
#   echo -1000 > /proc/self/oom_score_adj

# 7. 网络高性能调优(注意与内存交互)
vm.min_free_kbytes = 262144    // 256MB 最小空闲(大内存系统建议 1-4GB)

场景化建议:

场景THPSwappiness关键参数
Redis/Memcachednever1大 page cache 预留、min_free_kbytes 调大
PostgreSQL/MySQLnever10shared_buffers 调大、overcommit=0
Kafka/消息队列madvise10dirty_ratio 调低加速刷盘
Java 应用(Hadoop/Flink)never1堆外内存监控、cgroup max 限制
Web 服务器(Nginx)always20tcp 内存自动调优、worker 并发

10. 性能诊断工具栈

从应用层到硬件层,完整的内存观测工具链:


// 1. 进程级内存查看
$ pmap -x 12345              // 详细内存映射
$ cat /proc/12345/smaps      // 每段 VMA 详细统计(Private/Rss/Pss)
$ cat /proc/12345/status | grep -i mem // 快速查看 VmRSS/VmSize

// 2. 系统级内存概览
$ free -h                     // 总内存/缓存/可用
$ vmstat 1                    // 实时内存事件(si/so 交换统计)
$ sar -r 1                    // 内存使用率历史
$ sar -B 1                    // 页面统计(每秒缺页等)
$ cat /proc/meminfo            // 完整内存信息

// 3. Slab 分析
$ slabtop -o                  // 实时 Slab 使用排名
$ cat /proc/slabinfo           // 详细统计
// sanalysis.py(slab 分配热点分析)

// 4. NUMA 分析
$ numastat -c                 // 按内核子系统统计 NUMA 命中
$ numastat -p <pid>           // 按进程查看 NUMA 分配

// 5. OOM 日志
$ dmesg | grep -i "out of memory"
$ journalctl -k | grep -i oom  // systemd 场景

// 6. BPF 高级追踪
// 追踪内存分配延迟
funclatency-bpfcc 'kmem_cache_alloc'
// 追踪页面回收
stackcount-bpfcc 'shrink_node'
// 追踪 OOM kill 事件
trace-bpfcc 'oom_kill_process'

// 7. perf 内存事件
$ perf stat -e dTLB-load-misses,dTLB-store-misses ./benchmark
$ perf mem record ./app       // 内存访问采样分析
$ perf mem report             // 生成热点报告

总结

Linux 内存管理是一个精密运转的生态系统。从底层的伙伴系统到上层的 Slab 分配器,从 NUMA 架构感知到 OOM 机制,每个环节都有其设计哲学和调优空间。在生产环境中,内存调优没有银弹,必须结合实际负载特性(数据库、缓存、消息队列、Java 应用等)和观测数据进行针对性优化。

核心原则:按需分配、就近访问、及时回收、防御性保护。掌握了这些机制,你就能在内存管理领域游刃有余。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论