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)
场景化建议:
| 场景 | THP | Swappiness | 关键参数 |
|---|---|---|---|
| Redis/Memcached | never | 1 | 大 page cache 预留、min_free_kbytes 调大 |
| PostgreSQL/MySQL | never | 10 | shared_buffers 调大、overcommit=0 |
| Kafka/消息队列 | madvise | 10 | dirty_ratio 调低加速刷盘 |
| Java 应用(Hadoop/Flink) | never | 1 | 堆外内存监控、cgroup max 限制 |
| Web 服务器(Nginx) | always | 20 | tcp 内存自动调优、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 应用等)和观测数据进行针对性优化。
核心原则:按需分配、就近访问、及时回收、防御性保护。掌握了这些机制,你就能在内存管理领域游刃有余。

发表评论 取消回复