引言
Linux 内核的内存管理子系统是整个系统性能的核心基石。从物理页面的分配到内核对象的缓存,从 NUMA 拓扑感知到 OOM 紧急回收,每一个机制都经过数十年的演进与打磨。本文将深入剖析这些核心机制,并结合生产环境的调优案例,帮助读者构建完整的知识体系。
一、物理内存模型与 ZONE 架构
1.1 物理内存的组织方式
Linux 内核将物理内存组织为 节点 (Node) → 区域 (Zone) → 页面 (Page) 的三层结构。在 64 位系统中,虽然不再需要 HIGHMem,但 ZONE 的概念仍然保留用于 DMA 兼容。
// 核心数据结构 (include/linux/mmzone.h)
struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; // 节点的内存区域
struct zonelist node_zonelists[MAX_ZONELISTS]; // 分配回退列表
int nr_zones; // 包含的区域数量
struct page *node_mem_map; // 页面描述符数组
unsigned long node_start_pfn; // 起始页帧号
unsigned long node_present_pages; // 存在的页面总数
unsigned long node_spanned_pages; // 跨度页面总数
int node_id; // NUMA 节点 ID
// ...
};
1.2 ZONE 划分与用途
| ZONE 类型 | 说明 | x86_64 典型范围 |
|---|---|---|
| ZONE_DMA | 兼容老设备 ISA DMA | 0-16MB |
| ZONE_DMA32 | 32位 DMA 设备 | 16MB-4GB |
| ZONE_NORMAL | 直接映射到内核空间 | 16MB->896MB |
| ZONE_HIGHMEM | 超出直接映射的区域 | 896MB+ (仅32位) |
| ZONE_MOVABLE | 可移动页面(热插拔) | 动态分配 |
在 64 位 x86_64 系统中,由于虚拟地址空间极其庞大(128TB 内核空间),ZONE_HIGHMEM 不再存在。全部物理内存线性映射到内核虚拟地址空间 (PAGE_OFFSET = 0xffff888000000000)。
1.3 页面描述符 struct page
系统中的每个物理页面都有一个对应的 struct page 描述符,这是内核内存管理最基本的原子单元:
struct page {
unsigned long flags; // 页面状态标志 (PG_locked, PG_active, PG_dirty 等)
union {
struct address_space *mapping; // 页面映射信息
void *s_mem; // slab 第一个对象指针
};
pgoff_t index; // 在映射中的偏移
unsigned long private; // 私有数据指针
atomic_t _refcount; // 引用计数
atomic_t _mapcount; // 映射计数
// 链表节点:伙伴系统的 free_list / LRU 链表 / slab 链表
struct {
union {
struct list_head lru;
struct list_head slab_list;
};
};
};
二、伙伴系统 (Buddy System) 深度剖析
2.1 算法原理
伙伴系统是内核物理页面分配的核心算法,由 Knowlton 于 1965 年提出。其核心思想是将可用内存分成不同 阶 (order) 的块组(order-N 块包含 2^N 个连续页面),分配时向上拆分,释放时与"伙伴"合并。
// mm/page_alloc.c 中的核心实现
struct free_area {
struct free_list free_list[MIGRATETYPE_TYPES]; // 按迁移类型分类
unsigned long nr_free; // 空闲页面总数
};
static inline struct page *__rmqueue(struct zone *zone, unsigned int order,
int migratetype)
{
struct page *page;
// 从请求阶开始向上查找
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
page = get_page_from_free_area(area, migratetype);
if (!page)
continue;
// 找到了大块,需要拆分(剥洋葱)
expand(zone, page, order, current_order, migratetype);
set_pcppage_migratetype(page, migratetype);
return page;
}
// 没有足够大的块,尝试直接回收/压缩
return NULL;
}
2.2 伙伴关系的判定规则
两个页面块成为"伙伴"需要同时满足三个条件:
- 大小相同:都是 order-N 块
- 物理连续:地址首尾相接
- 对齐:第 N 个块的起始地址必须是 2^(N+PAGE_SIZE) 的整数倍
伙伴关系的数学判定: 伙伴_pfn = pfn ^ (1 << order),即两个伙伴块只在第 order 位不同。
2.3 迁移类型碎片控制
伙伴系统长期运行后最大的问题是 外部碎片。内核引入了 页面迁移类型 分类来解决:
| 迁移类型 | 含义 | 回收特性 |
|---|---|---|
| MIGRATE_UNMOVABLE | 内核核心对象,无法迁移 | 几乎不回收 |
| MIGRATE_MOVABLE | 用户页面、可随意分配 | 容易回收 |
| MIGRATE_RECLAIMABLE | slab、kernel cache | 可收缩 |
| MIGRATE_ISOLATE | 预留/热插拔 | 隔离 |
| MIGRATE_CMA | 连续内存分配器 | 设备专用 |
2.4 per-cpu 页面缓存 (PCP)
为减少锁竞争,每个 CPU 维护一个本地单页缓存队列:
struct zone {
struct per_cpu_pages __percpu *per_cpu_pageset; // PCP 缓存
};
struct per_cpu_pages {
int count; // 本地缓存页面数
int high; // 高于此值时归还伙伴系统
int batch; // 每次添加的批量
struct list_head lists[MIGRATETYPE_TYPES]; // 按迁移类型分组的链表
};
三、Slab 分配器家族深度对比
3.1 Slab 的设计哲学
伙伴系统以 页 (4KB) 为粒度分配,但内核中大量对象的尺寸远小于一页。为减少内部碎片和 allocation/free 开销,引入了 Slab 分配器作为二级分配器。
Slab 的核心思想是 批量预分配 + CPU缓存:从伙伴系统获取整页,划分为多份等大对象,通过 free list 管理空闲对象指针。
3.2 SLUB (Unqueued Slab) — 当前默认
SLUB 是对 SLAB 的彻底简化,是现今 Linux 内核默认的 Slab 实现。核心改进:嵌入式 freelist + 去队列 + 原生 NUMA。
// SLUB 的核心对象管理 (mm/slub.c)
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // CPU 本地 slab
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA 节点级
unsigned int offset; // 空闲指针嵌入在对象内的偏移
unsigned int object_size; // 对象实际大小
unsigned int size; // 含元数据的总大小
unsigned int order; // 每个 slab 的页面阶
void (*ctor)(void *); // 对象构造函数
};
struct kmem_cache_cpu {
void **freelist; // 空闲对象指针链表 (头插法)
struct page *page; // 当前使用的 slab
struct page *partial; // 部分满 slab
};
SLUB 的关键优化点:
- 空闲指针嵌入:释放对象时直接将 freelist 指针写入对象内存
- CPU 本地优先:hot-path 完全无锁,每个 CPU 自有 freelist 和当前 page
- 着色 (Colouring):通过在 cache line 级别偏移减少 CPU cache 冲突
- CPU partial 链表:减少跨节点分配的开销
3.3 三大分配器性能对比
| 特性 | SLAB | SLUB | SLOB |
|---|---|---|---|
| 默认内核版本 | 2.6 早期 | 2.6.23 至今 | 嵌入式/特殊场景 |
| 代码复杂度 | 高 (~15K行) | 中 (~10K行) | 低 (~3K行) |
| 分配延迟 | ~20ns | ~5ns | ~50-200ns |
| NUMA 支持 | 一般 | 优秀 | 无 |
| 调试功能 | 有限 | SLUB_DEBUG 强 | 无 |
四、NUMA 内存分配策略
4.1 NUMA 拓扑基础
在现代多路服务器上,内存控制器分属不同 CPU Socket,形成非一致内存访问 (NUMA) 拓扑。远程访问的延迟通常是本地的 1.5-2 倍,带宽也受限于互联总线。
4.2 Linux NUMA 分配策略
| 策略 | 说明 | 典型场景 |
|---|---|---|
| MPOL_DEFAULT | 节点本地优先,fallback 到 zonelist | 大多数内核分配 |
| MPOL_PREFERRED | 首选某节点,本地无可用时回退 | 偏向但可容忍远程 |
| MPOL_BIND | 严格限定在指定节点集 | 实时/数据库 |
| MPOL_INTERLEAVE | 在节点间交叉分配 (round-robin) | 超大内存数据库 |
4.3 NUMA Balancing 自动均衡
Linux 4.7+ 引入了自动 NUMA 页面迁移,内核通过采样发现远程访问热点后,将页面迁移到访问它的 CPU 本地节点。
# 查看 NUMA 统计信息
numastat -c # 每个进程的本地/远程比例
numactl --hardware # 查看拓扑
numactl --interleave=all ./app # 交叉分配模式
# 开启/关闭 NUMA Balancing
echo 1 > /proc/sys/kernel/numa_balancing # 开启自动均衡
echo 0 > /proc/sys/kernel/numa_balancing # 关闭
五、页面回收与 Swap 机制
5.1 LRU 算法内核实现
内核维护两个 LRU 链表:Anonymous (匿名页) 和 File-backed (文件缓存页)。每种分活跃/非活跃两对。
enum lru_list {
LRU_INACTIVE_ANON = 0, // 不活跃匿名页 (最先回收)
LRU_ACTIVE_ANON = 1, // 活跃匿名页
LRU_INACTIVE_FILE = 2, // 不活跃文件缓存页
LRU_ACTIVE_FILE = 3, // 活跃文件缓存页
LRU_ISOLATED_ANON = 4, // 临时隔离
LRU_ISOLATED_FILE = 5, // 临时隔离
NR_LRU_LISTS = 6
};
5.2 直接回收与背景回收
- 直接回收 (Direct Reclaim):当分配时发现水位低于 min → 同步触发的回收,延迟大
- kswapd (背景回收守护进程):每个 NUMA Node 运行一个 kswapd,当 free_pages < high_wakeup 时被唤醒
- memcg 回收:当 cgroup 内存触及 limit 时触发
六、OOM Killer 深度解析
6.1 Out-Of-Memory 判定
在内核 5.x+ 中,out_of_memory() 在伙伴系统无法分配连续页并且直接回收也失败时被调用。
6.2 OOM Score 计算算法
oom_badness() 的任务是给每个进程打分,分数最高的被杀死:
- 基础分数:该进程占用的物理内存 / 系统总内存 (归一化到 0-1000)
- oom_score_adj 调整:用户可写,范围 ±1000
- 特殊保护:init/systemd 不杀 (adj = -1000)
# 查看各进程的 oom_score 和 oom_score_adj
ps aux | awk '{print $2}' | xargs -I{} cat /proc/{}/oom_score /proc/{}/oom_score_adj
# 手动触发 OOM (仅用于测试容器)
echo f > /proc/sysrq-trigger
dmesg | grep -i "killed process"
6.3 OOM 对容器的影响
容器环境下的 OOM 分为两种场景:
- Container OOM (cgroup 内存超限):触发的 killer 是 cgroup 内部的 oom killer,K8s 中 Pod 变为 OOMKilled 状态
- Host OOM (物理内存不足):宿主机的全局 OOM killer 触发,在所有进程(含容器)中选择得分最高者
七、高级内存管理特性
7.1 KSM (Kernel Samepage Merging)
KSM 是内核的去重机制,通过扫描页面内容,将内容相同的页面合并为一份只读副本。常用于 KVM 虚拟化环境。
# 启用 KSM
echo 1 > /sys/kernel/mm/ksm/run
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan # 每次扫描页数
echo 20 > /sys/kernel/mm/ksm/sleep_millisecs # 扫描间隔
7.2 Transparent Huge Pages (THP)
THP 是内核自动将连续的小页面合并为大页面的机制,减少 TLB 压力。
# THP 模式配置
echo always > /sys/kernel/mm/transparent_hugepage/enabled # 始终开启
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled # 按需 (默认)
echo never > /sys/kernel/mm/transparent_hugepage/enabled # 关闭
数据库使用 THP 的双刃剑效应:优点是 TLB miss 减少 8 倍、减少 page fault 次数;缺点是碎片化导致分配延迟毛刺、compaction 消耗 CPU。SRE 建议 MySQL/Oracle 关闭 THP,使用显式 hugepages。
八、内存控制器 (memcg) 与容器内存管理
8.1 cgroup v2 的层级保护
内存分配层级 (cgroup v2):
root (memory.max = 32G, memory.high = 29G, memory.low = 0)
│
├─ database (memory.max = 16G, memory.low = 8G)
│ ├─ mysql (memory.max = 12G)
│ └─ redis (memory.max = 4G, memory.min = 2G) ← 硬保护
│
├─ webserver (memory.max = 12G, memory.low = 4G)
│ ├─ nginx
│ └─ php-fpm
│
└─ system (memory.max = 4G, memory.min = 1G) ← 系统保护
优先级规则:
1. 触及 max → 触发 cgroup 内部 OOM
2. 触及 high → 主动回收 (不阻塞分配)
3. 分配时保证 min 不被挤占
4. 空闲内存按 weight 比例分配
8.2 K8s 内存资源模型
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
resources:
requests:
memory: "512Mi" # 调度依据, 对应 memory.low
limits:
memory: "1Gi" # 硬上限, 对应 memory.max (OOMKill)
九、生产环境实战调优
9.1 关键 sysctl 参数
# Swappiness (交换倾向)
# 数据库 (MySQL/PG): 1-10 (避免 swap 引起延迟风暴)
# 高性能应用: 0 (尽量在内存中, 用 OOM 替代 swap)
vm.swappiness = 10
# Dirty 写回策略
vm.dirty_ratio = 40
vm.dirty_background_ratio = 10
vm.dirty_expire_centisecs = 6000
# 过度分配策略
# Redis: 必须设为 1 (因 fork + COW)
vm.overcommit_memory = 1
# 最小保留内存 (8GB 内存通常设置 262144 即 256MB)
vm.min_free_kbytes = 262144
# 透明大页 (数据库推荐关闭)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 静态 HugePages (数据库常用)
echo 1024 > /proc/sys/vm/nr_hugepages
9.2 NUMA 拓扑感知的绑定实践
# 查看 NUMA 硬件拓扑
numactl --hardware
# MySQL 绑定 NUMA 实战
# 方法 A: numactl 启动 (简单有效)
numactl --cpunodebind=0 --membind=0 /usr/sbin/mysqld
# 方法 B: 在 my.cnf 中使用 (推荐)
# innodb_numa_interleave = ON
# 查看进程 NUMA 内存分布
numastat -p $(pidof mysqld)
9.3 内存泄漏检测与诊断
# 查看进程详细内存映射
cat /proc/$(pidof nginx)/smaps_rollup
# slabtop 实时 slab 监控
slabtop -o | head -20
# BPF/eBPF 追踪内存分配
funclatency -u kmem_cache_alloc # 分配延迟分布
memleak -p $(pidof my_app) # 泄漏检测
十、性能监控与排障全流程
10.1 内存问题排查决策树
收到告警: 系统内存不足 / OOM / 应用变慢
1. 系统级别快速诊断:
├─ free -h → 看 available, 不是 free
├─ dmesg | grep -i "out of memory" → 是否有 OOM
├─ vmstat 1 → 观察 si/so (swap in/out)
└─ sar -r 1 → 历史内存使用趋势
2. 进程级别分析:
├─ top -o %MEM → 占用内存最大的进程
├─ cat /proc/<pid>/status | grep -E "VmRSS|VmSwap"
└─ cat /proc/<pid>/smaps_rollup → PSS/USS/Anonymous
3. 内核级别排查:
├─ slabtop → 内核对象缓存异常
├─ cat /proc/meminfo | grep -E "Huge|Slab|Active|Inactive"
├─ cat /proc/zoneinfo → 每个 zone 的空闲页面分布
└─ numastat -p <pid> → NUMA 本地率
4. OOM 分析:
├─ dmesg | grep -A 30 "Out of memory"
└─ journalctl -k | grep -i "oom-killer"
5. 高级手段:
├─ bpftrace -e 'kprobe:out_of_memory { printf("OOM triggered by %s\n", comm); }'
└─ perf record -e page-faults -p <pid> -- sleep 30
10.2 生产环境黄金指标
# 建议采集的 Prometheus 指标:
# 系统内存
node_memory_MemAvailable_bytes # 最核心指标
node_memory_MemTotal_bytes
# 页面回收速率
node_vmstat_pgscan_kswapd # kswapd 扫描页面数 (增长 → 内存压力)
node_vmstat_pgscan_direct # 直接回收扫描 (增长 → 严重内存压力)
node_vmstat_oom_kill # OOM 计数
# 告警规则:
# 1. MemAvailable < 10% 总内存 → 警告
# 2. pgscan_direct/min > 100 → 内存压力严重
# 3. oom_kill counter 增加 → 紧急处理
总结
Linux 内核内存管理是一个历经数十年演进的精密系统。理解这些核心机制不仅有助于日常运维排障,更能指导应用架构和系统调优:
- 伙伴系统 提供物理页面的高效分配与回收,通过迁移类型分类缓解外部碎片
- SLUB 分配器 以极简设计实现极低的分配延迟,是内核对象的二级缓存核心
- NUMA 感知 在多路服务器上对性能影响可达 30%+,务必根据 workload 选择合适策略
- OOM Killer 作为最后手段,需通过
oom_score_adj和 cgroup 双重保护关键服务 - KSM/THP 分别在虚拟化和数据库场景有截然不同的配置决策
- memcg 是容器内存隔离的基础,requests/limits 映射到 cgroup 的 low/max
在生产实践中,建议采用 监控先行 → 参数调优 → 瓶颈分析 → 架构优化 的系统化方法,避免盲目修改 sysctl。
本文基于 Linux 6.x 内核源码分析,重点关注 x86_64 平台的实现细节,生产环境请以实际内核版本为准。

发表评论 取消回复