深入理解Linux内核--内存架构,页框分配器,性能优化

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                        │
└──────────────────────────────────────────────────────────────┘
相关推荐
Mortalbreeze1 小时前
深入理解 TCP 协议(二):TCP 可靠传输与高效通信机制详解
linux·服务器·网络·网络协议·tcp/ip
2401_868534782 小时前
企业信息安全建设
linux·网络协议·beautifulsoup
goyeer2 小时前
Liunx日志管理与journalctl
java·linux·运维·服务器·运维开发·信息化·信息化企业管理
酷可达拉斯2 小时前
自动化运维-Ansible Role综合应用案例-基于LNMP部署wordpress
linux·运维·服务器·自动化·ansible
拂拉氏2 小时前
【Linux】 编译与动静态库相关
linux·编译·动静态库
Lonely 净土10 小时前
Linux 运维文件写入操作
linux·运维·服务器·文件管理
吠品13 小时前
Wine 在 Linux 上运行 Windows 软件完整指南
java·linux·服务器
深念Y15 小时前
从 Windows 迁移到 Linux 开发环境的记录
linux·windows·链接·bun·ram·imdisk
知无不研16 小时前
Linux I/O复用之epoll
linux·服务器·epoll·socket编程