1.页框
1.1.NUMA 节点与物理内存组织
1.1.1.NUMA 架构
在 NUMA(Non-Uniform Memory Access)系统中,CPU 和内存被划分为多个节点(Node)。每个节点有自己的本地内存,访问本地内存比访问远程节点内存快得多。
内核用 struct pglist_data(简称 pgdat)表示一个 NUMA 节点:
cpp
// include/linux/mmzone.h
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; // 该节点包含的内存区域
struct zonelist node_zonelists[MAX_ZONELISTS]; // 分配时的 fallback 列表
unsigned long node_start_pfn; // 节点起始页框号
unsigned long node_present_pages; // 实际存在的页数
unsigned long node_spanned_pages; // 包含空洞的总跨度
int node_id; // NUMA 节点 ID
wait_queue_head_t kswapd_wait; // kswapd 等待队列
int kswapd_order; // kswapd 当前回收的 order
enum zone_type kswapd_highest_zoneidx; // kswapd 回收的最高 zone
atomic_t kswapd_failures; // 连续回收失败次数
// ...
} pg_data_t;
每个节点包含若干 Zone(内存区域),分配内存时优先从本地节点的 Zone 分配,本地不足时才 fallback 到远程节点。
1.2.内存区域(Zone)------ 不同性质的物理内存
1.2.1.Zone 类型
Linux 将物理内存按地址范围和硬件限制划分为不同性质的 Zone:
| Zone 类型 | 说明 | 典型范围 |
|---|---|---|
| ZONE_DMA | 用于 legacy ISA 设备的 DMA,必须位于物理内存低 16MB 内 | < 16MB |
| ZONE_DMA32 | 32位设备可 DMA 的内存,x86-64 上通常 < 4GB | < 4GB |
| ZONE_NORMAL | 可直接内核映射的内存(线性映射区) | 因架构而异 |
| ZONE_HIGHMEM | 32位系统中内核无法直接线性映射的内存(需临时映射) | > 896MB (x86) |
| ZONE_MOVABLE | 用于可移动内存(如 CMA、内存热插拔预留) | 动态 |
| ZONE_DEVICE | 用于设备内存(如 PMEM、GPU 显存) | 设备特定 |
在 64 位系统上通常只有 ZONE_DMA32 和 ZONE_NORMAL。
1.2.2.Zone 的核心结构
cpp
struct zone {
unsigned long watermark[NR_WMARK]; // min/low/high/promo 水位线
unsigned long nr_reserved_highatomic; // 高阶原子分配预留
struct per_cpu_pages __percpu *per_cpu_pageset; // 每CPU缓存
struct free_area free_area[NR_PAGE_ORDERS]; // 伙伴系统空闲链表
unsigned long zone_start_pfn;
unsigned long managed_pages; // 伙伴系统管理的页数
unsigned long present_pages;
unsigned long spanned_pages;
const char *name;
// ...
};
1.3.伙伴系统(Buddy System)------ 页框分配核心
1.3.1.基本思想
伙伴系统将空闲页框按 2^order 个连续页 的块进行管理(order 从 0 到 10,对应 1~1024 页)。
cpp
order 0: 1 page (4KB)
order 1: 2 pages (8KB)
order 2: 4 pages (16KB)
...
order 10: 1024 pages (4MB)
1.3.2.核心数据结构
cpp
struct free_area {
struct list_head free_list[MIGRATE_TYPES]; // 按迁移类型分链表
unsigned long nr_free; // 该 order 空闲块数
};
1.3.3.迁移类型(Migrate Types)------ 反碎片机制
内核将页按可移动性分类,避免不可移动页夹杂在可移动页中间导致碎片:
| 迁移类型 | 用途 |
|---|---|
| MIGRATE_UNMOVABLE | 内核分配(如 kmalloc 大对象),不可迁移 |
| MIGRATE_MOVABLE | 用户态匿名页/文件页,可迁移/交换 |
| MIGRATE_RECLAIMABLE | 可回收缓存(如 slab、文件页缓存) |
| MIGRATE_HIGHATOMIC | 高优先级原子分配预留 |
| MIGRATE_CMA | 连续内存分配器区域 |
| MIGRATE_ISOLATE | 隔离中的页(迁移/热插拔) |
分配时优先从对应迁移类型的链表中取页,该类型不足时可以steal(降级)其他类型的页块。
1.3.4.分配与释放操作
- 分配:找到满足要求的最小 order 的空闲块,如果 order 过大则分裂(split)为更小的块,返回所需部分,剩余部分挂到对应 order 的空闲链表。
- 释放:检查相邻页框是否为空闲且同 order,若是则合并(coalesce)为更大的块,递归向上合并。
1.4.每 CPU 页框缓存(Per-CPU Page Cache, PCP)
1.4.1.为什么需要 PCP?
伙伴系统的全局链表需要加锁(zone->lock),频繁的单页分配会导致锁竞争。PCP 为每个 CPU 维护一个本地缓存,批量从伙伴系统取页/归还页,大幅减少锁竞争。
1.4.2.PCP 结构
cpp
struct per_cpu_pages {
int count; // 当前缓存中的页数
int high; // 缓存上限,超过则归还一批到伙伴系统
int batch; // 每次批量取/还的数量
struct list_head lists[NR_PCP_LISTS]; // 热页/冷页链表
};
1.4.3.热页 vs 冷页
- 热页(Hot Pages):最近被使用过,CPU 缓存中可能还有数据,适合快速重用。
- 冷页(Cold Pages):刚从伙伴系统取来或准备归还,CPU 缓存中无有效数据,适合 DMA 等场景。
分配时优先从 PCP 的热页链表取,order ≤ 3 的分配通常走 PCP 快速路径。
1.5.水位线(Watermarks)与回收机制
1.5.1.四条水位线
每个 Zone 有四条水位线控制内存回收行为:
cpp
enum zone_watermarks {
WMARK_MIN, // 最低水位,低于此触发直接回收/Direct Reclaim
WMARK_LOW, // 低水位,低于此唤醒 kswapd 后台回收
WMARK_HIGH, // 高水位,kswapd 回收的目标
WMARK_PROMO, // 用于分层内存(CXL/NUMA)页提升
NR_WMARK
};
1.5.2.水位线计算
水位线由 vm.min_free_kbytes 和 vm.watermark_scale_factor 推导:
cpp
watermark[min] = min_free_kbytes 按 zone 比例分摊
watermark[low] = min * (5/4) // 约 1.25x min
watermark[high] = min * (3/2) // 约 1.5x min
1.5.3.三级回收策略
cpp
┌─────────────────────────────────────────────────────────────┐
│ Free > high │ 健康状态,无需回收,kswapd 休眠 │
├─────────────────────────────────────────────────────────────┤
│ low < Free < high │ 压力初现,唤醒 kswapd 后台异步回收 │
├─────────────────────────────────────────────────────────────┤
│ min < Free < low │ 压力较大,分配进程可能进入 Direct │
│ │ Reclaim,同步回收,阻塞当前进程 │
├─────────────────────────────────────────────────────────────┤
│ Free < min │ 极度紧张,原子分配可能失败,持续 │
│ │ Direct Reclaim 后可能触发 OOM Killer │
└─────────────────────────────────────────────────────────────┘
1.5.4.kswapd 与 Direct Reclaim
-
kswapd:每个 NUMA 节点的后台内核线程。当 free pages < low 时唤醒,扫描 LRU 链表回收页,直到达到 high 水位。回收内容包括:
- 干净文件页:直接丢弃
- 脏文件页:写回后丢弃
- 匿名页:换出到 swap(如有)
- Slab 缓存:通过 shrinker 收缩
-
Direct Reclaim:当 free pages < min 时,当前分配进程自己执行回收,同步阻塞,导致延迟抖动。若回收失败,进入 OOM。
1.6.完整页框分配流程
1.6.1.入口函数
cpp
struct page *alloc_pages(gfp_t gfp, unsigned int order)
└── __alloc_pages(gfp, order, preferred_nid, nodemask)
└── __alloc_pages_nodemask(gfp, order, nid, nodemask)
└── __alloc_pages(gfp, order, nid, nodemask)
├── get_page_from_freelist() // Fast Path
└── __alloc_pages_slowpath() // Slow Path
1.6.2.Fast Path(快速路径)
cpp
1. 根据 gfp 标志确定首选 Zone 和 fallback 顺序
2. 检查目标 Zone 的水位是否满足 (zone_watermark_ok)
3. 若满足:
a. order == 0 且 <= 3:走 rmqueue_pcplist(),从 PCP 缓存取页
b. 否则:走 __rmqueue(),从伙伴系统 free_area 取页
- __rmqueue_smallest():从低 order 到高 order 遍历
- 找到后 del_page_from_free_list(),必要时 expand() 分裂
4. 返回 page
1.6.3.Slow Path(慢速路径)
当 Fast Path 失败(所有 Zone 水位不足)时进入:
cpp
1. 唤醒 kswapd(如果未唤醒)
2. 尝试直接回收(Direct Reclaim):
- shrink_node() 扫描 LRU
- shrink_slab() 收缩缓存
- 可能触发 compaction(内存规整)以获取连续页
3. 再次尝试 get_page_from_freelist()
4. 若仍失败,考虑 OOM Killer(除非 __GFP_RETRY_MAYFAIL 等标志)
1.6.4.分配标志(GFP)对路径的影响
| GFP 标志 | 行为 |
|---|---|
GFP_ATOMIC |
不允许睡眠,不走 Direct Reclaim,可能失败 |
GFP_KERNEL |
标准路径,允许 Direct Reclaim 和 IO |
GFP_HIGHUSER |
用户态分配,优先从 ZONE_HIGHMEM |
GFP_DMA / GFP_DMA32 |
限制在对应 Zone |
__GFP_RETRY_MAYFAIL |
尽力回收但不触发 OOM |
__GFP_NOFAIL |
不允许失败,极端情况下可能死等 |
1.7.性能优化与调优
1.7.1.监控与诊断
cpp
# 查看伙伴系统状态(各 order 空闲块数)
cat /proc/buddyinfo
# 查看 Zone 水位线和统计
cat /proc/zoneinfo | grep -E "pages free|min|low|high|managed"
# 查看迁移类型分布
cat /proc/pagetypeinfo
# 查看回收统计
cat /proc/vmstat | grep -E "pgscan|pgsteal|allocstall|oom_kill"
1.7.2.关键调优参数
| 参数 | 作用 | 建议 |
|---|---|---|
vm.min_free_kbytes |
控制 min 水位线基准,影响三级回收触发时机 | 内存大时可适当增大(如 64MB~128MB),避免频繁 Direct Reclaim |
vm.watermark_scale_factor |
控制 low/high 与 min 的间距(默认 10,即 0.1%) | 内存压力大时增大,让 kswapd 更早介入 |
vm.swappiness |
控制文件页回收 vs 匿名页换出的倾向(0-200) | 数据库等延迟敏感场景设为 10-30;需要积极换出可设 100+(MGLRU) |
vm.zone_reclaim_mode |
NUMA 节点内存不足时是否回收本地节点 | 本地内存远快于远程时设为 1 |
vm.numa_balancing |
自动 NUMA 页迁移平衡 | 大多数场景保持开启(1) |
1.7.3.避免 Direct Reclaim 延迟
Direct Reclaim 是性能杀手,会导致分配进程同步阻塞。优化方向:
- 保证 kswapd 能提前工作:增大 min_free_kbytes 和 watermark_scale_factor,确保 free 降到 low 之前 kswapd 已被唤醒。
- 使用 Swap:即使少量 swap 也能让匿名页有回收出路,避免 OOM。
- 内存 Cgroup 隔离:限制单个容器/服务的内存上限,避免其耗尽系统内存导致全局 Direct Reclaim。
- 调整 oom_score_adj:保护关键进程不被 OOM Kill。
- 使用 MADV_HUGEPAGE / THP:减少高阶分配频率,降低伙伴系统分裂压力和碎片化。
1.7.4NUMA 优化
- numactl / libnuma:绑定进程到特定节点,减少跨节点访问。
- 自动 NUMA 平衡(numa_balancing=1):内核自动将页迁移到访问它的 CPU 所在节点。
- 分层内存(Memory Tiering):利用 WMARK_PROMO 和 CXL/PMEM 作为慢速层,热页保留在 DRAM。
1.7.5.碎片化治理
- 内存规整(Compaction):/proc/sys/vm/compact_memory 手动触发,或依赖 kcompactd 后台规整。
- CMA(Contiguous Memory Allocator):预留一块可移动内存区域,供驱动申请连续物理内存。
- 避免长期持有不可移动页:如减少内核大对象 kmalloc,改用 vmalloc(虚拟连续但物理不连续)。
1.8.总结图
cpp
┌──────────────────────────────────────────────────────────────┐
│ 用户/内核内存请求 │
│ alloc_pages(gfp, order) │
└──────────────────────────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ 1. 确定 NUMA 节点和 Zone 优先级(本地优先 → fallback) │
└──────────────────────────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ 2. FAST PATH: get_page_from_freelist() │
│ ├── 检查 zone_watermark_ok() │
│ ├── order 0~3 → PCP 缓存(rmqueue_pcplist) │
│ └── 高 order → 伙伴系统(__rmqueue → expand) │
└──────────────────────────┬───────────────────────────────────┘
│ 失败
▼
┌──────────────────────────────────────────────────────────────┐
│ 3. SLOW PATH: __alloc_pages_slowpath() │
│ ├── 唤醒 kswapd(free < low) │
│ ├── Direct Reclaim(free < min)→ 扫描 LRU、shrink slab │
│ ├── Compaction(内存规整) │
│ └── 重试分配 → 仍失败 → OOM Killer │
└──────────────────────────────────────────────────────────────┘