一、物理内存基石:Buddy System 分配器

1.1 核心思想

Linux 内核以页面(Page,通常 4KB)为最小单位管理物理内存。Buddy System(伙伴系统)将所有空闲页面组织为 11 个链表,分别管理 1、2、4、8、...、1024 个连续页面块(即 order 0~10)。

// mm/page_alloc.c 核心数据结构
struct free_area {
    struct free_list free_list[MIGRATE_TYPES];
    unsigned long    nr_free;
};

当请求 2 个页面但 order-1 链表为空时,内核从 order-2 链表中取出一个 4 页块,拆分一半分配,另一半留在 order-1 链表——这便是“伙伴”名称的由来。

1.2 迁移类型(Migrate Types)

为对抗物理碎片化,Buddy System 按页面可迁移性分类:

MIGRATE_UNMOVABLE:内核数据结构、页表项、不可移动的 slab 页面。

MIGRATE_MOVABLE:用户空间页面、文件缓存、可重分配的 slab 缓存。

MIGRATE_RECLAIMABLE:文件系统元数据、可回收的 inode/dentry 缓存。

这种分离确保了在需要整合连续物理内存时,只需移动 MOVABLE 类型页面。

1.3 Per-CPU 热页缓存(PCP)

多核场景下 Buddy System 的全局锁成为瓶颈,因此每个 CPU 维护独立的热页缓存队列:

// include/linux/mmzone.h
struct per_cpu_pageset {
    struct per_cpu_pages pcp;  // 冷热页面_batch管理
    s8 expire;
    u16 count;   // 单CPU缓存页面数
} ____cacheline_aligned_in_smp;

单 CPU 分配优先走 PCP 链表,低于 high watermark 时从 Buddy System 批量补充,高于 max 时归还。这对单线程顺序分配场景极为高效。

二、内核对象分配:Slab 分配器家族

2.1 Slab 的设计哲学

Buddy System 最小分配单位为 4KB 页,但内核频繁创建销毁的对象往往只有几百字节(task_struct ~1.7KB、inode ~600B)。于是 Slab 分配器作为建立在 Buddy System 之上的对象缓存层出现。

Slab 将一页(或连续多页)划分为若干等长 slot,每个 slot 存放一个内核对象,通过 free list 跟踪空闲位置。

2.2 Slab、SLOB、SLUB 三代演化

Linux 内核经历了三代 Slab 实现:

Slab(1994 Linux 1.0 继承 UV):每个 slab 维护本地 free list,对象分空闲/部分/满三种状态。硬件缓存友好但管理开销大。

SLOB(Simple List of Blocks):为嵌入式设备设计,用 first-fit 算法在空闲页面中线性搜索。代码约 1500 行但碎片严重,仅用于 CONFIG_SLOB 编译选项。

SLUB(Unqueued Slab,2.6.23 成为默认):现今 Linux 默认分配器。每个 CPU 维护自己的 partial 页面链表,消除了 Slab 中的共享锁。对象释放时直接放回原 slab 的 free list,利用硬件缓存局部性。

2.3 Slab 的 CPU 本地缓存机制

// mm/slub.c 简化结构
struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;  // per-cpu 本地缓存
    struct kmem_cache_node *node[MAX_NUMNODES];  // per-node partial
    unsigned int object_size;   // 对象实际大小
    unsigned int size;          // 含元数据的对象大小
    unsigned int offset;        // free pointer 偏移
    struct kmem_cache_order_objects oo;  // 最佳order
}

SLUB 的 CPU 本地缓存中,free list 通过嵌入指针实现——空闲 slot 内存直接存储下一个空闲 slot 地址。对象分配本质是从 per-cpu page 上 pop 一个指针,O(1) 完成。

三、虚拟内存映射:页表与 VMA

3.1 四级页表架构

Linux 在 x86-64 上采用四级页表(PML4→PDPT→PD→PT),虚拟地址划分为:

47                            39         30         21         12            0
|        PML4 index (9bit)    | PDPT(9) |  PD(9)  |  PT(9)  | offset(12) |
|————————— 39bit —————————————|

每级页表 512 项(4KB 页 / 8B 项),覆盖 256TB 地址空间。当应用使用超过 48bit 地址时(Intel 5-level page table / LA57),扩展为五级页表覆盖 128PB。

3.2 TLB 与 ASID/PCID

地址转换的性能关键在 TLB(Translation Lookaside Buffer),它是页表的硬件缓存。TLB miss 需四次内存 walk,代价极高。

内核通过以下技术减小 TLB 刷新频率:

  • ASID/PCID:TLB 项标记进程标识,进程切换无需 flush TLB
  • Lazy TLB:内核线程借用上一个进程的页表,避免刷新
  • Huge Pages / THP:使用 2MB/1GB 大页减小 TLB miss(4KB 页需 512K 项覆盖 256GB,2MB 页仅需 131K 项)

3.3 VMA(Virtual Memory Area)

用户进程的虚拟地址空间由红黑树组织的 VMA 区间表示,每个 VMA 描述一段连续区域的权限、后备存储和 flags:

// include/linux/mm_types.h
struct vm_area_struct {
    unsigned long vm_start, vm_end;    // 区间范围
    struct mm_struct *vm_mm;            // 所属 mm
    pgprot_t vm_page_prot;              // 访问权限
    unsigned long vm_flags;             // READ/WRITE/EXEC/SHARED
    const struct vm_operations_struct *vm_ops;  // fault/ioctl handler
    struct file *vm_file;               // 映射文件(若有)
    unsigned long vm_pgoff;             // 文件偏移(页对齐)
};

四、页面回收与 OOM 决策

4.1 LRU 算法与双链表

内核使用双链表 LRU 算法追踪页面活跃度:

Active List:近期访问过的页面

Inactive List:长时间未访问的候选回收页面

每次页面访问通过 PG_accessed 硬件标志(x86)或软件模拟将其从 inactive 提升到 active。回收时从 inactive 链表尾部逐出。匿名页面(匿名映射)在 swap 被逐出前会先进入 swap cache。

4.2 kswapd 与 Direct Reclaim

后台守护进程kswapd在每个 NUMA node 上运行,当空闲页面低于 low watermark 时异步唤醒,从 inactive 链表回收页面直到 high watermark。

当应用突发内存分配且 kswapd 来不及回收时,触发Direct Reclaim(直接回收)——分配路径同步进行页面回收。Direct Realloc 严重影响应用延迟,这也是“慢路径”分配(alloc_pages(GFP_KERNEL))的核心痛点。

4.3 OOM Killer 的评分逻辑

当所有回收手段耗尽仍无法满足分配请求时,触发 OOM Killer。它通过 oom_badness() 为每个进程评分:

// mm/oom_kill.c 简化逻辑
totalpages = total_ram + total_swap;
points = (get_mm_rss(mm) + get_mm_counter(mm, MM_SWAPENTS) + mm->nr_ptes) / totalpoints *= 1000;
oom_score = points + (oom_score_adj * totalpages / 1000);

评分因子:RSS + swap 占用 + PTE 页表占比(比值基准),加上 /proc/<pid>/oom_score_adj 手动调整。内核线程(PF_NO_SETUID 标记)和 init 进程的 oom_score_adj 设为 -1000 免疫。

4.4 memcg OOM vs 全局 OOM

当限制在 cgroup v1 memory.limit_in_bytes 或 v2 memory.max 内时,优先触发 memcg OOM——仅杀死该 cgroup 内的进程,不影响其他 cgroup。memcg OOM 评分仅基于组内 RSS,不参考全局内存压力。

五、内存碎片整理与 THP

5.1 Anti-Fragmentation 机制

Buddy System 的迁移类型分类已对抗外部碎片。内核还提供 memory compaction 主动整理碎片:

// mm/page_alloc.c → compact_zone()
// 1. 扫描 zone,分离 movable 页面到临时链表
// 2. 调用 page migration 将 movable 页面搬离
// 3. 原区域合并为更大连续块

压实由 /proc/sys/vm/compact_memory 触发或由 khugepaged(针对 THP)按需发起。

5.2 Transparent Huge Pages(THP)

THP 由 khugepaged 后台守护进程合并 4KB 基础页为 2MB 大页:

// 查看 THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled  # always/madvise/never

THP 适合内存数据库(Redis/PostgreSQL)和大内存应用,但其延迟 spike(合页时的 stop-the-world 停顿)使其不适合延迟敏感型服务(如交易系统)。生产建议:延迟敏感型服务设为 madvise,仅对明确调用 MADV_HUGEPAGE 的映射启用。

5.3 KSM(Kernel Samepage Merging)

KSM 扫描内存中内容相同的页面合并为一例(COW 共享),减少冗余内存占用。适用于 KVM 虚拟化环境(多个 VM 运行相同 OS 内核)。

// /sys/kernel/mm/ksm/ 控制接口
pages_to_scan     // 每次扫描页数
sleep_millisecs   // 扫描间隔
pages_shared      // 当前共享页数量

KSM 的扫描开销随内存增大而增长,大规模部署时 CPU 占用可达 3%~10%,需权衡收益。

六、生产环境调优实战

6.1 关键 sysctl 参数

# 减少 swap 倾向(默认60,0=积极swap)
vm.swappiness = 10

# 脏页刷新策略(CentOS7+默认)
vm.dirty_ratio = 40           # 单文件脏页占总内存%时同步写
vm.dirty_background_ratio = 10 # pdflush/flush 后台写回阈值

# 内存分配水位(per-zone 的 min/low/high)
vm.min_free_kbytes = 262144   # 256MB emergency 保留

# OOM 行为调整
vm.panic_on_oom = 0           # OOM时触发 kernel panic(配合kdump调试)
vm.overcommit_memory = 2      # 严格 overcommit,拒绝超过 CommitLimit 的分配

6.2 监控与诊断工具

vmstat -SM 1:查看 si/so(swap in/out)、free、buff、cache 实时变化

slabtop:观察 slab 缓存实时分配(如 dentry/inode_cache 是否无限膨胀)

numastat -p <pid>:某进程在各 NUMA 节点的内存分布

bpftrace -e kswapd:...:追踪 kswapd 唤醒频率与回收效率

echo m > /proc/sysrq-trigger:dump 当前内存信息到 dmesg,包含各 zone 水位、OOM 评分快照

6.3 NUMA 亲和性优化

多槽服务器中本地节点内存访问延迟比跨槽低~30%。关键优化:

# 查看 NUMA 拓扑
numactl --hardware

# 绑定应用到本地节点
numactl --cpunodebind=0 --membind=0 /path/to/app

# 或 taskset CPU 亲和 + mbind 内存策略
numactl --physcpubind=0-31 --membind=0 ./app

数据库和 Redis 强烈建议开启 numad 或使用 taskset + mbind,避免跨槽抖动引发 P99 毛刺。

七、性能调优实战案例

案例1:Redis P99 延迟抖动:诊断发现 khugepaged 周期性合页导致 200ms+ 停顿。修复:echo never > /sys/kernel/mm/transparent_hugepage/enabled,P99 稳定在 800μs。

案例2:容器化应用频繁 OOMKilled:limit=4GB 但应用峰值 RSS 达 4.3GB。修复:提高 limit 至 6GB + 配置 JVM -XX:MaxRAMPercentage=75 预留 off-heap 空间;同时设置 oom_score_adj=-500 保护关键 sidecar。

案例3:MySQL 查询偶发超时:numastat 显示 80% 内存在 node1 但 mysqld 绑定在 node0。修复后延迟下降 40%。

八、总结

Linux 内核内存管理是一个精密的多层系统:物理分配器 Buddy System 管理连续页面,Slab 层(SLUB)优化小对象分配,VMA 和 TLB 支撑虚拟内存映射,LRU/kswapd/OOM Killer 构成回收与决策链路。理解这些机制及其参数接口,才能在生产环境中做出正确的内存调优决策。下次我们将进入文件系统与 VFS 层的深度实战分析。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部