Linux内核内存管理深度实战:从伙伴系统到SLAB分配器

引言

Linux内核的内存管理子系统是整个操作系统最核心、最复杂的组件之一。它不仅负责物理内存的分配与回收,还提供了抽象层使得用户空间的进程能够便捷地使用内存资源。本文将深入解析Linux内核内存管理的核心机制,包括伙伴系统(Buddy System)、SLAB/SLUB/SLOB分配器、虚拟内存管理以及页面回收策略,并通过实际案例和性能调优参数帮助读者全面理解这一关键子系统。

一、物理内存管理与伙伴系统

1.1 物理页面的组织

Linux内核将物理内存划分为固定大小的页面(通常为4KB)。每个物理页面由一个struct page结构体描述,所有页面的描述符存储在一个全局的mem_map数组中。系统启动时,物理内存被划分为多个节点(NUMA架构),每个节点又包含若干个内存域(Zone)。

主要内存域包括:

  • ZONE_DMA:0-16MB,用于DMA操作
  • ZONE_DMA32:16MB-4GB,32位DMA可访问区域
  • ZONE_NORMAL:直接映射到内核虚拟地址空间
  • ZONE_HIGHMEM:(仅32位系统)超过896MB的高端内存
  • ZONE_MOVABLE:可移动内存域,用于内存热插拔

1.2 伙伴系统算法

伙伴系统是物理内存分配的核心算法。它将空闲页面按大小分组,每组包含2的幂次个连续页面(order从0到11)。分配时,若请求大小无法精确匹配,则从更大的块中分割出一半作为伙伴(Buddy)。

// 伙伴系统核心数据结构
struct free_area {
    struct free_list free_list[MIGRATE_TYPES];
    unsigned long    nr_free;
};

// 分配页面的核心函数
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
    return __alloc_pages(gfp_mask, order, preferred_nid, NULL);
}

伙伴系统的关键操作:

  • 分裂(Split):当低阶free_list为空时,从高阶取一个块,分裂为两个伙伴
  • 合并(Coalesce):释放页面时,检查其伙伴是否也在空闲列表中,若是则合并
  • 页面迁移类型:MIGRATE_UNMOVABLE、MIGRATE_MOVABLE、MIGRATE_RECLAIMABLE等,减少碎片

1.3 GFP分配标志

内核分配页面时使用的GFP(Get Free Pages)标志控制分配行为:

  • GFP_KERNEL:标准内核分配,允许睡眠
  • GFP_ATOMIC:原子分配,不允许睡眠,用于中断上下文
  • GFP_NOIO:不允许启动I/O操作
  • GFP_NOFS:不允许递归文件系统调用
  • __GFP_HIGHMEM:允许从高端内存分配
  • __GFP_ZERO:分配后清零页面

二、SLAB分配器——内核对象的内存管理

2.1 为什么需要SLAB

伙伴系统以页面(4KB)为粒度分配,但内核中大量对象远小于一页(如task_struct约1.7KB、inode约600B)。频繁向伙伴系统申请整页会造成严重的内部碎片。SLAB分配器在伙伴系统之上构建了一个对象缓存层,实现细粒度、高效的内存分配。

2.2 SLAB核心概念

SLAB分配器由Jeff Bonwick在Solaris中发明,Linux实现了三种变体:

  • SLAB:经典实现,复杂但功能完整
  • SLUB:简化实现,现代Linux默认选项,更好的可扩展性
  • SLOB:极简实现,用于嵌入式系统

SLAB的核心数据结构关系:

kmem_cache (缓存)
  └── kmem_cache_node[NUMA节点]
        └── slab_list ( slab列表)
              └── slab (由1个或多个连续页面组成)
                    └── 对象槽位 (object slots)

2.3 SLUB分配器深入

SLUB是Linux内核当前默认的分配器,它简化了SLAB的设计,取消了复杂的着色和每CPU缓存层次:

// SLUB核心结构
struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  // 每CPU快速分配
    struct kmem_cache_node *node[MAX_NUMNODES]; // 每节点后备
    unsigned int offset;       // 下一个空闲对象的偏移
    unsigned int object_size;  // 对象实际大小
    unsigned int size;         // 含元数据的对象大小
    unsigned int order;        // 每次从伙伴系统申请的页面阶数
    // ...
};

SLUB的分配优先级:

  1. CPU部分(CPU Partial):当前CPU的空闲对象,无锁快速分配
  2. 每CPU缓存(Freelist):每CPU的空闲对象链表
  3. 节点部分(Node Partial):NUMA节点的部分空闲slab
  4. 伙伴系统(Buddy System):都没有时,向伙伴系统申请新页面

2.4 kmem_cache的使用

// 创建专用缓存
struct kmem_cache *my_cache = kmem_cache_create(
    "my_object_cache",      // 缓存名
    sizeof(my_object_t),    // 对象大小
    0,                      // 对齐
    SLAB_HWCACHE_ALIGN,     // 标志
    NULL                    // 构造函数
);

// 从缓存分配对象
my_object_t *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);

// 释放对象回缓存
kmem_cache_free(my_cache, obj);

// 销毁缓存
kmem_cache_destroy(my_cache);

三、虚拟内存管理与mmap

3.1 进程地址空间

每个进程拥有独立的虚拟地址空间(在x86_64上通常为128TB用户空间 + 128TB内核空间)。由struct mm_struct描述,通过VMA(Virtual Memory Area)树/区间树管理。

进程地址空间布局:
┌──────────────────────┐ 0x00007FFFFFFFFFFF (用户空间顶部)
│      栈 (Stack)       │ ↓ 向下增长
├──────────────────────┤ ← mmap 基址随机化
│      内存映射区域      │ (共享库、mmap匿名映射)
├──────────────────────┤
│      堆 (Heap)        │ ↑ 向上增长 (brk/mmap)
├──────────────────────┤ ← 数据段 (BSS/Data)
│      只读数据/代码     │ (Text段)
└──────────────────────┘ 0x0000000000400000 (典型ELF入口)

3.2 页表机制

Linux使用多级页表实现虚拟地址到物理地址的转换。x86_64架构通常使用4级页表(PML4/PDPT/PD/PT),ARMv8支持3-4级。

// 虚拟地址到物理地址转换流程
CR3寄存器
  → PML4表 (Page Map Level 4, 512项)
    → PDPT页目录指针表
      → PD页目录
        → PT页表
          → 物理页面基址 + 页内偏移

内核通过软件模拟的页表遍历函数(follow_page、follow_pfn)访问任意地址空间的页表。硬件通过MMU自动处理,配合TLB(Translation Lookaside Buffer)加速。

3.3 mmap系统调用

mmap是用户空间分配内存的核心系统调用,也可用于内存映射文件:

// mmap 参数详解
void *mmap(
    void *addr,          // 建议映射地址 (NULL = 内核选择)
    size_t length,       // 映射长度
    int prot,           // 保护标志: PROT_READ|PROT_WRITE|PROT_EXEC
    int flags,          // MAP_PRIVATE|MAP_SHARED|MAP_ANONYMOUS|MAP_HUGETLB
    int fd,             // 文件描述符 (匿名映射为-1)
    off_t offset        // 文件偏移
);

大页(HugePage/THP)支持:

  • 静态大页(HugePage):启动时预分配,2MB或1GB页面,减少TLB miss
  • 透明大页(THP):内核自动合并连续普通页面为大页

四、页面回收与OOM Killer

4.1 LRU链表机制

Linux使用改进的LRU(Least Recently Used)算法追踪页面的活跃度。每个内存维护两个LRU链表:活跃(Active)链表和非活跃(Inactive)链表。页面在两个链表间移动以模拟二次机会算法。

// LRU页面类型
enum lru_list {
    LRU_INACTIVE_ANON = 0,  // 非活跃匿名页
    LRU_ACTIVE_ANON,        // 活跃匿名页
    LRU_INACTIVE_FILE,      // 非活跃文件页
    LRU_ACTIVE_FILE,        // 活跃文件页
    LRU_UNEVICTABLE,        // 不可驱逐页
    NR_LRU_LISTS
};

4.2 Kswapd 与直接回收

当空闲页面低于阈值时,页面回收机制被触发:

  • kswapd:内核后台线程,异步扫描和回收页面
  • 直接回收(Direct Reclaim):当分配请求无法被满足时,调用者同步执行回收

回收优先级:

  1. 丢弃干净的缓存页(Buffer/Page Cache中的干净页)
  2. 将脏页写回磁盘(涉及I/O,代价较高)
  3. 将非活跃匿名页换出到交换分区(Swap Out)
  4. 驱逐活跃页面(代价最高)

4.3 OOM Killer

当系统内存极度匮乏,所有回收手段仍无法满足分配请求时,OOM Killer被触发。它选择一个进程终止以释放内存。

OOM评分机制 (oom_badness()):

// 评分因素 (简化)
score = total_vm  // 进程使用的虚拟内存总量
score *= 1000 >> oom_score_adj  // 调整系数
// 内核进程和init进程通常被豁免

通过设置/proc/[pid]/oom_score_adj可以调整进程被OOM Kill的优先级(-1000表示永不杀死)。

五、实战:内存调试工具与性能调优

5.1 内存泄漏检测

kmemleak是内核内置的动态内存泄漏检测工具:

// 启用kmemleak
echo scan > /sys/kernel/debug/kmemleak    // 触发扫描
cat /sys/kernel/debug/kmemleak             // 查看泄漏报告

// 标注不需要跟踪的分配
kmemleak_not_leak(ptr);
kmemleak_ignore(ptr);

用户空间内存泄漏使用Valgrind或AddressSanitizer(ASan):

# Valgrind检测
valgrind --leak-check=full --show-leak-kinds=all ./my_program

# GCC/Clang ASan
gcc -fsanitize=address -g -o my_program my_program.c

5.2 /proc与/sys内存参数调优

# 查看系统内存概览
cat /proc/meminfo
cat /proc/buddyinfo    # 伙伴系统状态
cat /proc/slabinfo     # SLAB分配器状态
cat /proc/vmallocinfo  # vmalloc分配区域

# 关键调优参数 (/proc/sys/vm/)
echo 10 > /proc/sys/vm/swappiness        # 降低swap倾向(默认60)
echo 1 > /proc/sys/vm/overcommit_memory   # 允许内存超分配
echo 50 > /proc/sys/vm/overcommit_ratio   # 可超分配比例
echo 50 > /proc/sys/vm/vfs_cache_pressure # 缓存回收压力(默认100)
echo always > /sys/kernel/mm/transparent_hugepage/enabled  # 启用THP
echo 3 > /proc/sys/vm/drop_caches         # 清理缓存

5.3 perf与ftrace分析内存行为

# 使用perf统计页面缺页率
perf stat -e page-faults,minor-faults,major-faults -p $(pidof my_app)

# ftrace追踪内存分配
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmem_cache_alloc/enable
cat /sys/kernel/debug/tracing/trace_pipe

# BPF工具:跟踪分配延迟、内存热点
bpftrace -e 'kprobe:__alloc_pages { @start[tid] = nsecs; }
             kretprobe:alloc_pages /@start[tid]/ {
               @lat_us = hist((nsecs - @start[tid]) / 1000);
               delete(@start[tid]);
             }'

六、NUMA感知内存分配

在NUMA(Non-Uniform Memory Access)架构下,CPU访问不同节点的内存延迟不同。内核提供NUMA感知的分配API:

// NUMA节点感知分配
struct page *alloc_pages_node(int nid, gfp_t gfp, unsigned int order);
void *kmalloc_node(size_t size, gfp_t flags, int nid);
void *kmem_cache_alloc_node(struct kmem_cache *cache, gfp_t flags, int nid);

// 设置进程内存策略
set_mempolicy(MPOL_BIND, &node_mask, MAX_NUMNODES);  // 绑定到特定节点
set_mempolicy(MPOL_PREFERRED, &node_mask, MAX_NUMNODES);  // 优先节点
mbind(addr, length, MPOL_BIND, &node_mask, MAX_NUMNODES, 0); // 绑定已有区域

查看NUMA布局:numactl --hardware和numastat -m可以提供详细的节点统计。

七、总结与展望

Linux内核内存管理子系统经历了数十年的演进,从最初的简单伙伴系统发展到现在高度优化的多层级架构。理解其工作原理对于内核开发者、系统性能调优工程师以及底层基础设施研发人员至关重要。

当前Linux内存管理的前沿方向包括:

  • 内存热插拔:支持在线添加/移除物理内存
  • CXL(Compute Express Link)内存扩展:新时代内存池化技术
  • eBPF内存分析:低开销的实时内存行为监控
  • 异步页面回收:减少直接回收带来的延迟尖刺
  • 内存分级管理:结合持久内存(PMem)和DRAM

掌握这些机制不仅有助于编写高性能的内核模块和驱动程序,也能帮助开发者在用户空间做出更明智的内存使用决策。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部