引言

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 DMA0-16MB
ZONE_DMA3232位 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_RECLAIMABLEslab、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 三大分配器性能对比

特性SLABSLUBSLOB
默认内核版本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 平台的实现细节,生产环境请以实际内核版本为准。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部