引言

内存管理是 Linux 子系统中最复杂、最核心的部分之一。它不仅负责在进程之间分配和回收物理内存,还实现了虚拟内存、页面交换、Slab 分配器等关键机制。本文将从内核源码的角度,深入剖析 Linux 内存管理子系统的架构与实现细节。

1. 物理内存模型

1.1 NUMA 与 UMA 架构

现代多核处理器普遍采用 NUMA(非统一内存访问)架构,每个 CPU 节点拥有本地内存,访问远程内存会有额外的延迟开销。Linux 内核通过 pg_data_t 结构体管理每个 NUMA 节点,其中包含该节点的内存区域(zone)、页面列表和交换信息。

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;
    ...
};

在 UMA(统一内存访问)系统中,内核只维护一个 contig_page_data,所有 CPU 看到的内存拓扑相同,但仍存在 DMA、Normal、HighMem 等内存区域的区别。

1.2 内存区域(Zones)

物理内存根据用途被划分为多个区域:

  • ZONE_DMA(0-16MB):用于老式 ISA 设备的 DMA 传输
  • ZONE_DMA32(16MB-4GB):32 位 DMA 设备的可寻址区域
  • ZONE_NORMAL(直接映射区):内核线性映射可直接访问的物理页面
  • ZONE_HIGHMEM(仅 32 位系统):超出内核线性映射范围的物理内存
  • ZONE_MOVABLE(可选):用于内存热插拔的可移动页面

2. 虚拟内存与地址空间

2.1 进程地址空间布局

在 x86_64 Linux 上,用户进程拥有 128TB 的虚拟地址空间(47 位有效地址)。地址空间按功能从低到高依次为:代码段(.text)、只读数据(.rodata)、数据段(.data/.bss)、堆(mmap 区域向上增长)、共享库映射区、栈(向下增长),以及最顶端的内核空间。

内核通过 struct mm_struct 结构体管理进程的地址空间,其中 mmap 字段指向进程的所有 VMA(Virtual Memory Area)红黑树根节点:

struct mm_struct {
    struct {
        struct rb_root rb;
        struct rb_node *rb_subtree_last;
    } mm_mt; /* VMA 红黑树 */
    unsigned long mmap_base;    /* mmap 区域基址 */
    unsigned long task_size;    /* 用户/内核地址分界 */
    pgd_t *pgd;                 /* 页全局目录 */
    atomic_t mm_users;          /* 活跃用户计数 */
    atomic_t mm_count;          /* 引用计数 */
    ...
};

2.2 页表机制

Linux 采用四级页表结构(x86_64 架构):PGD → P4D → PUD → PMD → PTE。每一级页表记录下一级的物理地址,最终在 PTE 级别完成虚拟地址到物理地址的映射。64 位系统下,页面大小为 4KB 时,每级页表使用 9 位索引(共 512 条目),四级即可映射 48 位虚拟地址(256TB)。

内核提供了 follow_page_mask()、walk_page_range() 等辅助函数完成页表遍历。TLB(Translation Lookaside Buffer)缓存最近使用的地址映射,是虚拟内存性能的硬件基础。

3. Slab 分配器

3.1 Slab 分配器原理

内核频繁分配和释放小对象(如 task_struct、inode、dentry 等),如果直接使用页面分配器(buddy system),会造成严重的内存碎片和分配延迟。Slab 分配器基于 Jeff Bonwick 的经典设计,解决了这个问题:

  1. 对象缓存(Cache):为同一类型对象创建专用缓存
  2. Slab:从 buddy system 获取一个或多个连续页面,划分为等大的对象槽位
  3. 状态跟踪:每个 slab 维护 free/used 对象列表,优先从 partial slab 分配
struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;    /* 每 CPU 快速分配路径 */
    struct kmem_cache_node *node[MAX_NUMNODES];  /* NUMA 节点管理 */
    unsigned int object_size;          /* 对象实际大小 */
    unsigned int size;                 /* 对齐后大小 */
    unsigned int offset;               /* 空闲指针偏移 */
    unsigned long flags;               /* SLAB 构造/析构标志 */
    const char *name;                  /* 缓存名称 */
    struct list_head list;             /* 全局链表 */
    ...
};

3.2 SLUB:新一代默认分配器

SLUB(Scribble-unrolled level block)是新版 Linux 内核中默认的 Slab 分配器,相比经典 SLAB,它简化了设计并提升了性能:

  • 移除了 per-CPU 队列,改用 per-CPU 指针直接嵌入空闲对象链表
  • 减少了元数据开销,每个 slab 的 page 结构复用 freelist 和 inuse 字段
  • 更好的 NUMA 亲和性和调试支持(如 red zone、poisoning)
  • 使用 kmem_cache_alloc() / kfree() 接口,用法简洁

3.3 SLOB:嵌入式场景的极简分配器

SLOB(Simple List Of Blocks)适用于内存极度受限的嵌入式系统。它采用简单的首次适应算法(first-fit),将空闲块用简单链表连接。虽然分配效率较低,但代码量极小、无 per-CPU 和元数据开销。

4. 页面分配器(Buddy System)

4.1 伙伴系统算法

伙伴系统是 Linux 内核物理内存分配的基石。它将物理页面按大小分为 11 个阶(order 0-10),分别管理 2^0 到 2^10 个连续页面。分配时从合适的阶开始,如果该阶没有空闲块,就向上借一个更大的块,将其分裂为两个伙伴(buddy)——这保证了物理地址按 2 的幂次对齐,合并时可以快速获取更大块。

/* 分配 2^order 个页面 */
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);

/* 释放页面到伙伴系统 */
void free_pages(unsigned long addr, unsigned int order);

4.2 GFP 标志

gfp_t 标志控制分配器的行为,关键标志包括:

  • GFP_KERNEL:标准内核分配,可能睡眠等待内存回收
  • GFP_ATOMIC:高优先级、不可睡眠(中断上下文使用)
  • GFP_HIGHUSER:用户页面,可回收
  • GFP_NOFS:不触发文件系统操作(避免递归)
  • __GFP_ZERO:分配清零页面(安全/密码学场景)
  • __GFP_NOWARN:分配失败不发出警告

4.3 Per-CPU 页面缓存(PCP)

为了避免全局伙伴系统锁竞争,内核为每个 CPU 维护一个 per-cpu 页面缓存。当需要分配单个页面(order 0)时,优先从 PCP 链表中取,完全不加锁。PCP 在空闲页面低于阈值时从 buddy system 批量获取补充。

5. 内存回收与交换

5.1 页面回收机制(kswapd)

当系统内存不足时,内核唤醒 kswapd 后台线程执行页面回收。回收主要针对两类页面:

  • 文件页(File Page):有后备存储(文件)的页面,可直接丢弃后重新读取
  • 匿名页(Anonymous Page):没有后备存储的页面(如堆、栈),必须写入 swap 区才能释放
  • 活跃LRU:最近访问过,暂不回收
  • 不活跃LRU:近期未访问,优先回收候选

这种双链表设计(active/inactive)使得 LRU 算法无需全局排序,只需在链表间移动即可完成近似 LRU 统计。

5.2 反向映射(Reverse Mapping)

回收一个物理页面时,需要找到所有映射了该页面的进程 PTE,将其标记为不存在。反向映射(rmap)建立了从物理页面到 VMA 链表的映射关系:

  • 文件页 rmap:通过 address_space 和 page->index 在 radix tree 中查找
  • 匿名页 rmap:通过 anon_vma 链表遍历所有关联的 VMA。KSM(Kernel Samepage Merging)也使用此机制合并相同内容的页面

5.3 OOM Killer

当内存耗尽且回收仍无法满足需求时,内核触发 OOM Killer。它会调用 oom_badness() 函数评估每个进程的"杀生分数",考虑因素包括:占用内存量、运行时间、UID、是否持有特殊权限等,最终杀死得分最高的进程。

现代内核支持 oom_score_adj 调整值,管理员可以为关键服务设置负值(避免被杀)或为批处理任务设置正值(优先被杀)。

6. 内存管理与性能优化

6.1 Transparent Huge Pages (THP)

透明大页技术自动将连续的普通页面合并为大页(2MB 或 1MB),减少 TLB 压力和页表遍历开销。khugepaged 后台线程扫描进程内存区域,将符合条件的连续 4KB 页面合并。对于数据库和科学计算等内存密集型应用,THP 可带来 10-25% 的性能提升。

6.2 内存压缩(zswap/zram)

  • zswap:在 swap 前对匿名页面进行压缩存储于内存缓存中,延迟或避免实际磁盘 I/O
  • zram:创建一个压缩的块设备作为 swap 设备,适合无磁盘的嵌入式环境或需要加速交换的场景

6.3 NUMA 调优

NUMA 架构下,内存分配的本地性对性能影响显著。策略包括:

  • numactl --interleave=all:在节点间交错分配
  • numactl --cpunodebind=0 --membind=0:绑定 CPU 和内存到同一节点
  • MADV_HUGEPAGE:madvise 提示使用大页

7. 调试与问题排查

内核提供丰富的工具和接口用于内存调试:

  • slabtop / /proc/slabinfo:查看 Slab 分配器状态
  • vmstat -s:内存使用速览
  • /proc/buddyinfo:伙伴系统各阶空闲块数量
  • /proc/<pid>/smaps:进程详细内存映射
  • kmemleak:内核内存泄漏检测工具
  • KASAN (Kernel Address Sanitizer):运行时检测越界访问和释放后重用
  • ftrace kprobe:追踪 __alloc_pages 等关键函数

结语

Linux 内存管理子系统的精巧设计,使其能在从嵌入式设备到超级计算机的各种场景中高效运转。理解 Slab 分配器、伙伴系统、LRU 回收机制以及反向映射等核心组成部分,不仅有助于定位性能瓶颈,更能启发我们在系统编程中做出更好的架构决策。建议读者结合最新内核源码(kernel.org)和 Brendan Gregg 的《Systems Performance》一书深入学习。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部