Skip to content

Latest commit

 

History

History
1911 lines (1520 loc) · 79.3 KB

File metadata and controls

1911 lines (1520 loc) · 79.3 KB

Linux 内核内存管理子系统深度解析

基于 Linux Kernel 源码深度分析(kernel 6.x mainline) 源码根目录:/Users/zt/src/linux-projects/linux


目录

  1. 内存管理总体架构
  2. 物理内存组织:NUMA → Zone → Page 三层模型
  3. struct page 与 struct folio 详解
  4. page 标志位系统
  5. 伙伴系统(Buddy Allocator)深度剖析
  6. SLUB 分配器
  7. 虚拟内存:mm_struct 与 VMA
  8. 页表管理
  9. 缺页异常处理链
  10. 内存回收子系统
  11. MGLRU:多代 LRU
  12. OOM Killer
  13. Swap 子系统
  14. 综合视角:各层协作

1. 内存管理总体架构

Linux 内存管理(Memory Management,MM)是内核中最庞大、最精密的子系统。它的核心任务可以用一句话概括:用有限的物理内存,为无数进程提供相互隔离、看似无限的虚拟地址空间

1.1 为什么需要如此复杂的内存管理?

理解 Linux MM 设计之前,先回答几个基础问题:

问题 1:为什么需要虚拟内存? 如果所有进程直接使用物理地址,进程 A 的错误指针会破坏进程 B 的数据。虚拟内存通过页表隔离每个进程的地址空间,使得每个进程都认为自己独占从 0 到最大值的地址范围。

问题 2:为什么需要页(Page)而不是字节级管理? 以字节为单位管理内存的元数据开销是灾难性的。4GB 内存若以字节管理,元数据本身就需要数 GB 空间。以 4KB 页为单位,元数据开销可控(每页约 64 字节的 struct page)。

问题 3:为什么 Linux 不用简单的 malloc/free? 内核需要应对多种场景:中断上下文(不能睡眠)、DMA 设备(需连续物理内存)、大量小对象(需 slab 分配器)、大对象(需 buddy 分配器)。一套机制无法满足所有场景。

1.2 子系统层次图

┌─────────────────────────────────────────────────────────────────┐
│                       用户空间应用程序                            │
│              malloc() / mmap() / brk()                          │
└─────────────────────────┬───────────────────────────────────────┘
                          │ syscall
┌─────────────────────────▼───────────────────────────────────────┐
│                      虚拟内存管理层                               │
│   mm_struct / vm_area_struct / Maple Tree / 缺页异常处理         │
│   mmap() → do_mmap() → vm_area_struct                           │
└─────┬───────────────────────────────────┬───────────────────────┘
      │                                   │
┌─────▼──────────────┐      ┌─────────────▼─────────────────────┐
│    页表管理层       │      │         内存分配层                  │
│  PGD→P4D→PUD→     │      │  kmalloc() → SLUB → buddy          │
│  PMD→PTE          │      │  alloc_pages() → buddy allocator   │
│  TLB shootdown    │      │  vmalloc() → 非连续物理内存          │
└─────┬──────────────┘      └─────────────┬─────────────────────┘
      │                                   │
┌─────▼───────────────────────────────────▼─────────────────────┐
│                      物理内存组织层                              │
│    NUMA Node (pglist_data)                                     │
│      └── Zone (struct zone): DMA / DMA32 / Normal / Movable   │
│            └── Page (struct page / folio)                      │
│                 free_area[0..10]: 伙伴系统空闲链表              │
└────────────────────────────────┬───────────────────────────────┘
                                 │
┌────────────────────────────────▼───────────────────────────────┐
│                      内存回收层                                  │
│    kswapd → 水位监控 → LRU/MGLRU 扫描 → shrink_folio_list()   │
│    OOM Killer → out_of_memory() → oom_kill_process()          │
│    Swap → swap_writepage() / swap_readpage()                   │
└────────────────────────────────────────────────────────────────┘

1.3 核心数据结构索引

数据结构 文件 职责
struct pglist_data include/linux/mmzone.h 代表一个 NUMA 节点
struct zone include/linux/mmzone.h:879 一个内存区域
struct page include/linux/mm_types.h:79 一个物理页框
struct folio include/linux/mm_types.h:401 复合页抽象
struct mm_struct include/linux/mm_types.h:1123 进程虚拟地址空间
struct vm_area_struct include/linux/mm_types.h:913 一段虚拟内存区域
struct kmem_cache mm/slab.h:197 SLUB 缓存描述符
struct kmem_cache_node mm/slub.c:430 per-node slab 链表
struct free_area include/linux/mmzone.h:138 伙伴系统每阶空闲区

2. 物理内存组织:NUMA → Zone → Page 三层模型

2.1 NUMA 节点(pglist_data)

现代服务器普遍采用 NUMA(Non-Uniform Memory Access)架构:每个 CPU socket 拥有本地内存,访问本地内存比跨 socket 访问快 1.5~4 倍。Linux 用 struct pglist_data(别名 pg_data_t)表示一个 NUMA 节点。

NUMA 架构示意(双 socket 服务器):
┌──────────────────────┐      ┌──────────────────────┐
│       Node 0         │      │       Node 1         │
│  CPU 0-15            │      │  CPU 16-31           │
│  本地内存 64GB       │══════│  本地内存 64GB       │
│  pglist_data [0]     │ QPI  │  pglist_data [1]     │
│  zones: DMA/Normal   │      │  zones: Normal       │
└──────────────────────┘      └──────────────────────┘
       ↑ 访问延迟 ~80ns              ↑ 跨节点访问 ~160ns

pglist_data 的关键字段:

// include/linux/mmzone.h(约 1100+ 行)
struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];  // 该节点所有区域
    struct zonelist node_zonelists[MAX_ZONELISTS]; // 分配回退列表
    int nr_zones;                          // 活跃区域数
    unsigned long node_start_pfn;         // 节点起始物理页号
    unsigned long node_present_pages;     // 实际存在的物理页数
    unsigned long node_spanned_pages;     // 跨度(含空洞)
    int node_id;                          // NUMA 节点 ID
    // kswapd 相关
    wait_queue_head_t kswapd_wait;
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd;           // 该节点的 kswapd 线程
    int kswapd_order;
    // ...
};

为什么 zonelist 要有回退机制? 当本地节点内存不足时,分配器按 zonelist 顺序尝试其他节点的 zone,这叫 NUMA 回退(fallback)。回退牺牲访问局部性,但保证了分配成功率。

2.2 内存区域(struct zone)

每个 NUMA 节点下的物理内存按用途划分为若干 zone。

// include/linux/mmzone.h:784
enum zone_type {
    ZONE_DMA,      // 低 16MB,兼容旧 ISA DMA 设备
    ZONE_DMA32,    // 低 4GB,32 位 DMA 设备
    ZONE_NORMAL,   // 内核可直接映射的内存(主力区)
    ZONE_HIGHMEM,  // 仅 32 位系统:高端内存,需 kmap 访问
    ZONE_MOVABLE,  // 可移动页,利于热插拔和大页分配
    ZONE_DEVICE,   // 设备持久内存(PMEM/DAX)
};

为什么要分 DMA/DMA32/Normal? 早期 ISA 总线的 DMA 控制器只有 24 位地址线,只能访问前 16MB 物理内存(ZONE_DMA)。PCI 32 位 DMA 只能访问前 4GB(ZONE_DMA32)。现代 64 位 DMA 设备可访问全部内存(ZONE_NORMAL)。内核通过 zone 分层确保 DMA 缓冲区始终落在正确的物理地址范围内。

struct zone 的关键字段(include/linux/mmzone.h:879):

struct zone {
    /* 水位线:控制内存回收触发时机 */
    unsigned long _watermark[NR_WMARK];    // line 883
    unsigned long watermark_boost;

    long lowmem_reserve[MAX_NR_ZONES];    // 低端内存保留

    struct per_cpu_pages __percpu *per_cpu_pageset;  // per-CPU 页缓存

    unsigned long zone_start_pfn;         // 区域起始 PFN

    atomic_long_t managed_pages;          // 伙伴系统管理的页数
    unsigned long spanned_pages;          // 含空洞的总跨度
    unsigned long present_pages;          // 实际存在的页数

    /* 核心:伙伴系统空闲链表数组 */
    struct free_area free_area[NR_PAGE_ORDERS];  // line 999

    spinlock_t lock;                      // 保护 free_area

    atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; // 统计计数器
} ____cacheline_internodealigned_in_smp;

缓存行对齐的意义____cacheline_internodealigned_in_smp 确保 zone 结构跨 CPU 缓存行对齐,避免不同 CPU 修改同一 zone 时产生 false sharing,这在 NUMA 多 socket 系统上至关重要。

2.3 水位线机制

水位线是 zone 内存管理的核心控制点(include/linux/mmzone.h:708):

enum zone_watermarks {
    WMARK_MIN,   // 最低水位:低于此紧急回收,阻塞分配者
    WMARK_LOW,   // 低水位:唤醒 kswapd 后台回收
    WMARK_HIGH,  // 高水位:kswapd 回收到此停止
    WMARK_PROMO, // 晋升水位:NUMA 页面晋升参考
    NR_WMARK
};
内存水位线示意图:

  free_pages
  ▲
  │████████████████████  高水位 (WMARK_HIGH)
  │                       kswapd 停止回收
  │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  低水位 (WMARK_LOW)
  │                       唤醒 kswapd
  │░░░░░░░░░░░░░░░░░░░░  最低水位 (WMARK_MIN)
  │                       直接回收,PF_MEMALLOC 特权分配
  └────────────────────────────────────► 时间

水位线由 /proc/sys/vm/min_free_kbytes 控制,计算公式大致为:

  • WMARK_MIN = min_free_kbytes / PAGE_SIZE(各 zone 按比例分配)
  • WMARK_LOW = WMARK_MIN * 5 / 4
  • WMARK_HIGH = WMARK_MIN * 3 / 2

2.4 三层层次关系图

System
├── NUMA Node 0 (pglist_data)
│   ├── ZONE_DMA (struct zone)
│   │   ├── free_area[0]: 1-page 空闲块链表 × MIGRATE_TYPES
│   │   ├── free_area[1]: 2-page 空闲块链表 × MIGRATE_TYPES
│   │   ├── ...
│   │   └── free_area[10]: 1024-page 空闲块链表 × MIGRATE_TYPES
│   ├── ZONE_DMA32 (struct zone)
│   └── ZONE_NORMAL (struct zone)
│       ├── per_cpu_pageset[CPU0]: PCP 缓存
│       ├── per_cpu_pageset[CPU1]: PCP 缓存
│       └── free_area[0..10]: 伙伴系统
│           每个 free_area 有 MIGRATE_TYPES 条链表:
│           [MIGRATE_UNMOVABLE] → page → page → ...
│           [MIGRATE_MOVABLE]   → page → page → ...
│           [MIGRATE_RECLAIMABLE] → page → ...
│
└── NUMA Node 1 (pglist_data)
    └── ZONE_NORMAL (struct zone)
        └── ...

3. struct page 与 struct folio 详解

3.1 struct page 的设计哲学

struct pageinclude/linux/mm_types.h:79)是内核中最重要的数据结构之一,每个物理页框对应一个。在 64 位系统上,它的大小约为 64 字节。

设计挑战:同一个物理页在不同时刻可能用于完全不同的目的(页缓存、匿名内存、slab 对象、页表、空闲页等),如何在 64 字节内兼容所有用途?答案是大量使用 union

// include/linux/mm_types.h:79
struct page {
    memdesc_flags_t flags;    // 原子标志位(zone/node 编码 + 状态标志)

    union {
        /* 页缓存 / 匿名页 */
        struct {
            union {
                struct list_head lru;      // LRU 链表节点
                struct list_head buddy_list; // 在伙伴系统中
                struct list_head pcp_list;   // 在 per-CPU 缓存中
                struct llist_node pcp_llist;
            };
            struct address_space *mapping; // 所属 address_space(文件映射)
                                           // 或 anon_vma(匿名映射,低位为1)
            union {
                pgoff_t __folio_index;     // 在文件中的偏移(页为单位)
                unsigned long share;       // fsdax 引用计数
            };
            unsigned long private;         // 页私有数据(buffer_head / swap entry / buddy order)
        };

        /* 网络 page_pool 页 */
        struct {
            unsigned long pp_magic;
            struct page_pool *pp;
            unsigned long dma_addr;
            atomic_long_t pp_ref_count;
        };

        /* 复合页的尾页 */
        struct {
            unsigned long compound_head;   // bit 0 = 1 表示这是尾页,其余位指向头页
        };

        /* RCU 释放 */
        struct rcu_head rcu_head;
    };

    union {
        unsigned int page_type;    // 类型页(typed folio)标识
        atomic_t _mapcount;        // 非类型页:被映射到用户页表的次数(初始化为 -1)
    };

    atomic_t _refcount;            // 引用计数(初始化为 1)

#ifdef CONFIG_MEMCG
    unsigned long memcg_data;      // Memory Cgroup 数据
#endif
} _struct_page_alignment;

mapping 字段的双重用途:这是一个经典的内核技巧。当页用于文件映射时,mapping 指向 struct address_space(低位为 0);当页是匿名页时,mapping 指向 struct anon_vma,并将最低位置 1 作为区分标志。调用者用 PageAnon(page) 宏检查。

private 字段的多义性

  • 伙伴系统中的空闲页:存储该块的 order(大小)
  • swapcache 页:存储 swp_entry_t(swap 槽位编码)
  • 文件系统页:存储 buffer_head *(缓冲头链表)
  • SLUB 页:存储 freelist 指针

3.2 struct folio:面向未来的复合页抽象

struct folioinclude/linux/mm_types.h:401)是 2021 年引入的新抽象,旨在统一处理大页(THP、hugetlb)和普通页。

引入 folio 的原因:大页(Transparent Huge Pages, THP)需要跨越多个连续 struct page,但很多代码用 struct page * 传递大页的头页,导致语义混乱——调用者不知道这个指针代表单个页还是整个大页。struct folio 明确表示"整个逻辑页"。

// include/linux/mm_types.h:401
struct folio {
    union {
        struct {
            memdesc_flags_t flags;         // 与 struct page.flags 对齐
            union {
                struct list_head lru;       // LRU 链表
                struct dev_pagemap *pgmap;  // ZONE_DEVICE
            };
            struct address_space *mapping; // 地址空间
            union {
                pgoff_t index;             // 在文件中的偏移
                unsigned long share;
            };
            union {
                void *private;             // 私有数据
                swp_entry_t swap;          // swap 入口
            };
            atomic_t _mapcount;            // 映射计数
            atomic_t _refcount;            // 引用计数
#ifdef CONFIG_MEMCG
            unsigned long memcg_data;
#endif
        };
        struct page page;                  // 与 struct page 兼容
    };

    /* 大 folio 特有字段(第 2 个 struct page 的位置)*/
    union {
        struct {
            unsigned long _flags_1;
            unsigned long _head_1;
            atomic_t _large_mapcount;      // 整体映射计数
            atomic_t _nr_pages_mapped;     // 已映射的子页数
            atomic_t _entire_mapcount;     // 整体完整映射计数
            atomic_t _pincount;            // DMA pin 计数
            mm_id_mapcount_t _mm_id_mapcount[2];
            union {
                mm_id_t _mm_id[2];
                unsigned long _mm_ids;
            };
        };
    };

    /* 第 3 个 struct page 位置 */
    union {
        struct {
            unsigned long _flags_2;
            unsigned long _head_2;
            struct list_head _deferred_list; // 延迟分裂列表
        };
    };
};

FOLIO_MATCH 静态断言include/linux/mm_types.h:508):确保 struct foliostruct page 的字段偏移完全对齐,使得 page_folio(page)folio_page(folio, 0) 零开销转换。

page_folio() 宏(include/linux/mm_types.h:306):

#define page_folio(p)  (_Generic((p),
    const struct page *: (const struct folio *)_compound_head(p),
    struct page *:       (struct folio *)_compound_head(p)))

这是 C11 泛型宏,编译期根据指针类型选择正确的 const 性,避免误用。

3.3 引用计数与生命周期

struct page 有两个引用计数:

计数器 访问方式 语义
_refcount page_ref_count() 页框本身的引用数(谁持有这个 page)
_mapcount page_mapcount() 被用户页表映射的次数,-1 表示未映射

_refcount 的典型来源

  • 伙伴系统分配:设为 1
  • 在 LRU 链表中:+1
  • 每次用户页表映射:通过 _mapcount 追踪(不增加 _refcount)
  • get_page() / put_page():显式持有

_refcount 归零时,页框被释放回伙伴系统。


4. page 标志位系统

4.1 flags 字段的布局

page->flags(实际类型 memdesc_flags_t,内含 unsigned long)是一个精心设计的复合字段:

64位 flags 字段布局(从高到低):

| SECTION | NODE | ZONE | LAST_CPUPID | KASAN_TAG | LRU_GEN | LRU_REFS | FLAGS |
  ↑高位                                                              ↑NR_PAGEFLAGS↑低位

FLAGS(低位)存储页状态标志
高位字段(ZONE/NODE 等)用于快速定位页所属的 zone 和 NUMA 节点

这个设计的精妙之处:给定一个 struct page *,不需要额外内存访问就能得知它属于哪个 NUMA 节点和哪个 zone(通过 page_zonenum()page_to_nid() 等函数)。

4.2 核心标志位详解

// include/linux/page-flags.h:93
enum pageflags {
    PG_locked,      // 页被锁定(I/O 进行中)。其他代码必须等待解锁
    PG_writeback,   // 页正在写回(writeback 进行中)
    PG_referenced,  // LRU 访问标记:最近被访问过(第二次机会)
    PG_uptodate,    // 页内容与磁盘一致(读取完成)
    PG_dirty,       // 页被修改,尚未写回磁盘
    PG_lru,         // 页在 LRU 链表中
    PG_head,        // 复合页头页标记(必须在 bit 6)
    PG_waiters,     // 有线程在等待此页解锁(与 PG_locked 同字节,bit 7)
    PG_active,      // 页在活跃 LRU 链表中
    PG_workingset,  // MGLRU:页属于工作集(被重新访问的 refault 页)
    PG_owner_priv_1,// 所有者私用(文件系统用作 PG_checked/PG_swapcache)
    PG_owner_2,     // 所有者私用(用作 PG_anon_exclusive)
    PG_arch_1,      // 架构特定标志(通常表示 dcache/icache flush 状态)
    PG_reserved,    // 保留页,不能被页分配器分配
    PG_private,     // 页有私有数据(buffer_heads 附着于此)
    PG_private_2,   // 用于 FS-Cache 本地缓存状态
    PG_reclaim,     // 页即将被回收(同时用作 PG_readahead)
    PG_swapbacked,  // 页由 RAM/swap 支撑(匿名页或 shmem)
    PG_unevictable, // 页不可驱逐(mlock'd 或 ramfs 等)
    PG_dropbehind,  // I/O 完成后丢弃(顺序读优化)
#ifdef CONFIG_MMU
    PG_mlocked,     // 页被 mlock() 锁定在内存中
#endif
#ifdef CONFIG_MEMORY_FAILURE
    PG_hwpoison,    // 硬件内存错误,绝对不能访问
#endif
};

PG_head 必须在 bit 6 的原因:compound 页的尾页用 compound_head 字段(以 bit 0 = 1 标记)来指向头页,而头页检测用 test_bit(PG_head, &page->flags)。bit 6 确保与某些架构的 atomics 操作兼容,且与 compound_head 的 bit 0 不冲突。

PG_waiters 必须与 PG_locked 在同一字节(bit 7)wake_up_page_bit() 在解锁时需要同时检查是否有等待者,两者在同一字节可以用单次原子操作同时检查,避免额外的内存屏障。

4.3 标志位别名

同一个 bit 可以有多个别名(取决于页的用途):

// include/linux/page-flags.h:132(部分)
PG_readahead = PG_reclaim,      // 预读控制
PG_swapcache = PG_owner_priv_1, // 页在 swap 缓存中
PG_anon_exclusive = PG_owner_2, // 匿名页排他所有权(COW 优化)
PG_mappedtodisk = PG_owner_2,   // 所有 buffer_heads 已映射
PG_fscache = PG_private_2,      // FS-Cache 缓存状态
PG_large_rmappable = PG_workingset, // 大页可反向映射标志

这种复用节省了宝贵的 bit,但需要调用者了解上下文。

4.4 访问标志位的 API

内核提供了一套统一的宏来访问标志位:

// 测试:PageXxx(page) → bool
PageLocked(page), PageDirty(page), PageAnon(page), ...

// 设置:SetPageXxx(page)
SetPageDirty(page), SetPageLocked(page), ...

// 清除:ClearPageXxx(page)
ClearPageDirty(page), ...

// 测试并设置(原子):TestSetPageXxx(page)
TestSetPageLocked(page), ...

// folio 版本:folio_test_xxx(folio)
folio_test_dirty(folio), folio_test_uptodate(folio), ...

5. 伙伴系统(Buddy Allocator)深度剖析

5.1 设计原理

伙伴系统(Buddy System)解决的核心问题:如何高效分配和回收不同大小的连续物理内存块,同时减少外部碎片?

核心思想:

  1. 所有内存块的大小都是 2 的幂次(order 0 = 1 页, order 1 = 2 页, ..., order 10 = 1024 页)
  2. 每个内存块都有一个"伙伴"(buddy):大小相同、物理地址仅在该 order 的最高位不同
  3. 分配时从空闲链表取,不够大则分裂;释放时与伙伴合并
伙伴系统内存分裂示意(分配 order-0 时从 order-2 分裂):

order-2 空闲块(4页):
[A   |B   |C   |D   ]

分裂为两个 order-1:
[A   |B   ] [C   |D   ]
↑ 取走一个     ↑ 这个放入 order-1 链表

第二个 order-1 继续分裂:
[C   ] [D   ]
↑ 返回给用户  ↑ 这个放入 order-0 链表

合并示意(释放 order-0 的 C 页):
[C   ] + [D   ] → [C   |D   ] (order-1)
[C   |D   ] + [A   |B   ] → [A   |B   |C   |D   ] (order-2)

为什么用 2 的幂次?

  • 判断伙伴只需 XOR 一个 bit:buddy_pfn = pfn ^ (1 << order)
  • 对齐检查用位运算:pfn & ((1 << order) - 1) == 0
  • 无需额外数据结构来定位伙伴,O(1) 时间找到伙伴

5.2 free_area 结构

// include/linux/mmzone.h:138
struct free_area {
    struct list_head free_list[MIGRATE_TYPES]; // 每种迁移类型一条链表
    unsigned long    nr_free;                  // 总空闲块数(所有 migrate type 之和)
};

zone 中有 NR_PAGE_ORDERS(= MAX_PAGE_ORDER + 1 = 11)个 free_area

// include/linux/mmzone.h:999
struct free_area free_area[NR_PAGE_ORDERS];
zone.free_area[] 结构:

free_area[0] (order-0, 4KB):
  .free_list[UNMOVABLE]  → page → page → NULL
  .free_list[MOVABLE]    → page → page → page → NULL
  .free_list[RECLAIMABLE]→ page → NULL
  .nr_free = 5

free_area[1] (order-1, 8KB):
  .free_list[UNMOVABLE]  → page-pair → NULL
  .free_list[MOVABLE]    → page-pair → page-pair → NULL
  .nr_free = 3

...

free_area[10] (order-10, 4MB):
  .free_list[MOVABLE]    → 4MB-block → NULL
  .nr_free = 1

5.3 MIGRATE_TYPE:迁移类型与反碎片化

// include/linux/mmzone.h:64
enum migratetype {
    MIGRATE_UNMOVABLE,   // 不可移动:内核数据、内核栈等
    MIGRATE_MOVABLE,     // 可移动:用户空间匿名页、文件页
    MIGRATE_RECLAIMABLE, // 可回收:page cache、slab 可回收部分
    MIGRATE_PCPTYPES,    // PCP 列表的类型数量(= 3)
    MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, // 高优先级原子分配保留
    MIGRATE_CMA,         // CMA(连续内存分配器)专用
    MIGRATE_ISOLATE,     // 隔离中(内存热插拔、CMA 迁移)
    MIGRATE_TYPES
};

为什么要区分迁移类型? 不可移动的内核对象(如 struct inode)一旦分配就固定在那里,随着时间推移会形成"钉子",让大块连续内存无法合并。通过将不可移动页与可移动页分离到不同链表,减少碎片化对大块分配的影响。

回退(fallback)机制:当目标迁移类型耗尽时,按预定顺序回退到其他类型:

// mm/page_alloc.c(回退顺序表,概念示意)
static int fallbacks[MIGRATE_TYPES][4] = {
    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, ... },
    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, ... },
    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE, ... },
};

5.4 分配路径:alloc_pages → get_page_from_freelist

分配调用链(以 alloc_pages(GFP_KERNEL, 0) 为例):

alloc_pages()                      [用户入口宏]
  └── __alloc_pages_noprof()        [mm/page_alloc.c:5279]
        ├── prepare_alloc_pages()   [构建 alloc_context]
        ├── get_page_from_freelist() [快路径:mm/page_alloc.c:3808]
        │     └── rmqueue()         [从 zone 取页]
        │           ├── rmqueue_pcplist() [per-CPU 缓存快路径]
        │           └── __rmqueue()       [从 free_area 取]
        │                 ├── __rmqueue_smallest() [按 order 查找]
        │                 └── __rmqueue_fallback() [迁移类型回退]
        │
        └── __alloc_pages_slowpath() [慢路径:mm/page_alloc.c:4710]
              ├── wake_all_kswapds()  [唤醒 kswapd]
              ├── get_page_from_freelist() [再次尝试]
              ├── __alloc_pages_direct_compact() [内存规整]
              ├── __alloc_pages_direct_reclaim() [直接回收]
              └── __alloc_pages_may_oom() [OOM 路径]

get_page_from_freelist()mm/page_alloc.c:3808)核心逻辑:

static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
                       const struct alloc_context *ac)
{
    struct zoneref *z;
    struct zone *zone;

retry:
    // 遍历 zonelist(按优先级:本地 zone → 远程 zone)
    for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx, ac->nodemask) {
        unsigned long mark;

        // 检查水位线
        mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
        if (!zone_watermark_fast(zone, order, mark, ...))
            continue;  // 水位不足,尝试下一个 zone

        // 水位满足,尝试从该 zone 分配
        page = rmqueue(ac->preferred_zoneref->zone, zone, order, ...);
        if (page)
            goto done;
    }
    // 所有 zone 都失败
    return NULL;
}

alloc_context 结构mm/internal.h:657)封装了一次分配请求的上下文:

struct alloc_context {
    struct zonelist *zonelist;       // 按优先级排序的 zone 列表
    nodemask_t *nodemask;            // NUMA 节点掩码约束
    struct zoneref *preferred_zoneref; // 首选 zone
    int migratetype;                 // 目标迁移类型
    enum zone_type highest_zoneidx;  // 允许的最高 zone
    bool spread_dirty_pages;         // 是否分散脏页到各节点
};

5.5 per-CPU 页缓存(PCP)

每个 CPU 在每个 zone 上维护一个小型页缓存(struct per_cpu_pagesinclude/linux/mmzone.h:744),避免频繁竞争 zone->lock:

struct per_cpu_pages {
    spinlock_t lock;      // 保护 lists
    int count;            // 当前缓存的页数
    int high;             // 上限:超过时批量归还给 buddy
    int batch;            // 批量操作大小
    struct list_head lists[NR_PCP_LISTS]; // 按 order 和 migratetype 组织
};

PCP 工作流程:

分配 order-0 页:
1. 检查 pcp->lists[migratetype] 是否有页
2. 有 → 直接从 pcp 取,无需持有 zone->lock
3. 没有 → 从 buddy 批量补充(batch 个页)到 pcp,再取

释放 order-0 页:
1. 放入 pcp->lists[migratetype]
2. 若 pcp->count > pcp->high → 批量归还给 buddy

5.6 回收路径:__free_pages → buddy 合并算法

释放调用链:

free_page() / put_page()
  └── free_frozen_pages() / __free_pages()
        └── free_unref_page() [order-0,经由 PCP]
              └── free_unref_page_commit()
                    → 放入 pcp 或直接归还 buddy

直接归还 buddy:
  └── __free_pages_core() / free_one_page()
        └── __free_one_page(page, pfn, zone, order, migratetype)
              ├── 循环尝试合并:
              │   buddy_pfn = pfn ^ (1 << order)  // 找伙伴
              │   buddy = page + (buddy_pfn - pfn) // 伙伴的 struct page
              │   if (PageBuddy(buddy) && buddy_order(buddy) == order):
              │       从链表移除伙伴
              │       合并:pfn &= ~(1 << order)
              │       order++
              │       若 order < MAX_PAGE_ORDER: 继续合并
              └── 将合并后的块加入 free_area[order].free_list[migratetype]

合并条件

  1. 伙伴页必须也在 buddy 系统中(PageBuddy(buddy) 为真)
  2. 伙伴的 order 必须相同
  3. 两者必须在同一个 zone
  4. 两者的 pfn 是正确的伙伴关系:pfn ^ (1 << order) == buddy_pfn

5.7 内存规整(Memory Compaction)

当高 order 分配失败但总 free pages 足够时,内核启动内存规整:将可移动页从内存低地址区迁移到高地址区,腾出连续的低地址内存供大块分配使用。这也是 MIGRATE_MOVABLE 存在的重要价值。

规整前:          规整后:
[K][.][U][K][U][.] → [K][K][U][U][.][.]
                               ↑ 合并为连续空闲块
K=内核不可移动  U=用户可移动  .=空闲

6. SLUB 分配器

6.1 为什么需要 Slab 分配器?

伙伴系统的最小分配单位是一个页(4KB)。但内核中大量对象远小于一个页(task_struct 约 7KB,inode 约 600 字节,dentry 约 200 字节)。如果每个对象都独占一个页,内部碎片(internal fragmentation)会极其严重。

Slab 分配器的核心思想:

  1. 按对象大小创建专用 kmem_cache(缓存池)
  2. 从伙伴系统批量获取若干页(称为 slab
  3. 将 slab 切分为固定大小的对象槽位
  4. 维护 freelist(空闲对象链表),实现 O(1) 分配和释放
  5. 对象构造缓存:对象释放后保留初始化状态,下次分配时无需重新初始化

Linux 历史上有三种 slab 实现(SLAB、SLUB、SLOB),当前主流是 SLUB(2007 年引入),以简洁著称。

6.2 SLUB 数据结构层次

kmem_cache(全局描述符)
├── cpu_sheaves(per-CPU 快路径): struct slub_percpu_sheaves __percpu *
│   ├── main:  struct slab_sheaf *(活跃 slab)
│   ├── spare: struct slab_sheaf *(备用 slab)
│   └── rcu_free: struct slab_sheaf *(kfree_rcu 批量释放)
│
└── node[MAX_NUMNODES]: struct kmem_cache_node *
    ├── partial:  struct list_head(部分满的 slab 链表)
    ├── nr_partial: unsigned long(partial slab 数量)
    └── barn: struct node_barn *(NUMA 优化的 slab 仓库)

kmem_cache 结构mm/slab.h:197):

struct kmem_cache {
    struct slub_percpu_sheaves __percpu *cpu_sheaves; // per-CPU slab
    slab_flags_t flags;          // SLAB_POISON / SLAB_RED_ZONE 等调试标志
    unsigned long min_partial;   // node->partial 链表的最小长度
    unsigned int size;           // 对象大小(含元数据对齐)
    unsigned int object_size;    // 原始对象大小(不含元数据)
    struct reciprocal_value reciprocal_size; // 除法优化:对象索引计算
    unsigned int offset;         // freelist 指针在对象内的偏移
    unsigned int sheaf_capacity; // 每个 sheaf 容纳的对象数
    struct kmem_cache_order_objects oo;  // 首选 slab 大小(order+objects)
    struct kmem_cache_order_objects min; // 最小 slab 大小(OOM 时降级)
    gfp_t allocflags;            // 分配 slab 页时的 gfp flags
    int refcount;                // 缓存引用计数(destroy 时检查)
    void (*ctor)(void *);        // 对象构造函数(初始化一次)
    unsigned int inuse;          // 对象内 metadata 的偏移
    unsigned int align;          // 对象对齐要求
    const char *name;            // 缓存名称(仅用于显示)
    struct list_head list;        // 全局 slab_caches 链表节点
    unsigned long random;         // SLAB_FREELIST_HARDENED 随机化密钥
    struct kmem_cache_node *node[MAX_NUMNODES]; // per-node 数据
};

per-CPU sheaves 结构mm/slub.c:420):

struct slub_percpu_sheaves {
    local_trylock_t lock;        // per-CPU 轻量级锁
    struct slab_sheaf *main;     // 活跃 slab sheaf,永不为 NULL
    struct slab_sheaf *spare;    // 备用 sheaf(空或满)
    struct slab_sheaf *rcu_free; // kfree_rcu 批量释放队列
};

per-node 结构mm/slub.c:430):

struct kmem_cache_node {
    spinlock_t list_lock;        // 保护 partial 链表
    unsigned long nr_partial;    // partial slab 数量
    struct list_head partial;    // 部分使用的 slab 链表
#ifdef CONFIG_SLUB_DEBUG
    atomic_long_t nr_slabs;      // 总 slab 数
    atomic_long_t total_objects; // 总对象数
    struct list_head full;       // 已满 slab(仅 debug 模式追踪)
#endif
    struct node_barn *barn;      // NUMA 优化仓库
};

6.3 SLUB 分配路径:快路径与慢路径

kmalloc(size, GFP_KERNEL)
  └── kmalloc_caches[type][index]          [按大小选择 cache]
        └── kmem_cache_alloc()
              └── slab_alloc_node()
                    ├── 快路径(per-CPU 无锁):
                    │   pcs = this_cpu_ptr(s->cpu_sheaves)
                    │   local_trylock(&pcs->lock)
                    │   object = slab_sheaf_alloc(pcs->main)
                    │   local_tryunlock(&pcs->lock)
                    │   return object                      ← 最快路径
                    │
                    └── 慢路径(需要补充 slab):
                          __slab_alloc()
                            ├── 尝试 spare sheaf
                            ├── 从 node->partial 链表取 slab
                            │   (需要 node->list_lock)
                            ├── 若 partial 为空,向 buddy 申请新 slab
                            │   new_slab() → allocate_slab()
                            │               → alloc_pages() [向伙伴系统申请]
                            └── 从新 slab 分配对象

freelist 硬化(SLAB_FREELIST_HARDENED):freelist 指针会用随机密钥(kmem_cache->random)进行 XOR 编码,防止 heap overflow 或 use-after-free 漏洞利用中修改 freelist 指针来控制程序流。

6.4 kmalloc size class 表

kmalloc() 根据请求大小选择合适的 kmem_cache:

// include/linux/slab.h:584
#define KMALLOC_SHIFT_HIGH  (PAGE_SHIFT + 1)  // = 13,即 8KB
#define KMALLOC_SHIFT_MAX   (MAX_PAGE_ORDER + PAGE_SHIFT) // = 22,即 4MB
#define KMALLOC_SHIFT_LOW   3                  // = 8 字节(最小)
kmalloc size class(典型配置,x86_64):

 请求大小范围    → 实际分配大小  → 对应 kmem_cache
  1  ~   8 字节 →   8 字节    → kmalloc-8
  9  ~  16 字节 →  16 字节    → kmalloc-16
 17  ~  32 字节 →  32 字节    → kmalloc-32
 33  ~  64 字节 →  64 字节    → kmalloc-64
 65  ~ 128 字节 → 128 字节    → kmalloc-128
129  ~ 256 字节 → 256 字节    → kmalloc-256
257  ~ 512 字节 → 512 字节    → kmalloc-512
513  ~ 1024字节 → 1024字节    → kmalloc-1k
1025 ~ 2048字节 → 2048字节    → kmalloc-2k
2049 ~ 4096字节 → 4096字节    → kmalloc-4k
4097 ~ 8192字节 → 8192字节    → kmalloc-8k(order-1 slab,直接分配)
> 8192 字节     → 向上取整到 2^n → 直接用 alloc_pages()

为什么在 8KB 以上直接用 alloc_pages?include/linux/slab.h:584的注释) SLUB 直接处理最大 order-1 页(PAGE_SIZE*2 = 8KB)的请求。更大的请求传递给页分配器,因为 slab 本身就是从页分配器获取的,没有必要维护更大的 slab cache。

kmalloc_type 区分KMALLOC_NORMAL / KMALLOC_DMA / KMALLOC_CGROUP 等):不同 GFP 标志会选择不同的 cache 集合,例如 GFP_DMA 分配来自 ZONE_DMA 的对象。

6.5 SLUB 释放路径

kfree(ptr)
  └── slab_free()
        ├── 快路径(对象属于当前 CPU 的 slab):
        │   pcs = this_cpu_ptr(s->cpu_sheaves)
        │   local_trylock(&pcs->lock)
        │   若对象属于 pcs->main 的 slab:
        │     直接放回 main->freelist(无锁,超快)
        │   local_tryunlock()
        │
        └── 慢路径:
              __slab_free(s, slab, object, ...)
                ├── 放回 slab 的 freelist
                ├── 若 slab 全空:
                │   discard_slab() → 归还给 buddy(若超出 min_partial)
                │   或加入 node->partial
                └── 若 slab 原来全满,现在有空闲:
                    加入 node->partial

7. 虚拟内存:mm_struct 与 VMA

7.1 进程地址空间:mm_struct

每个进程(除内核线程外)都有一个 struct mm_structinclude/linux/mm_types.h:1123)描述其虚拟地址空间:

struct mm_struct {
    struct {
        struct {
            atomic_t mm_count;       // 持有引用数(mmgrab/mmdrop)
        } ____cacheline_aligned_in_smp;

        struct maple_tree mm_mt;     // VMA 的 Maple Tree 索引(替代红黑树)

        unsigned long mmap_base;     // mmap 区域起始地址
        unsigned long task_size;     // 用户空间虚拟地址上限
        pgd_t *pgd;                  // 页全局目录(硬件页表根指针)

        atomic_t mm_users;           // 用户引用数(mmget/mmput)

        atomic_long_t pgtables_bytes; // 页表占用的内存(字节)
        int map_count;               // VMA 数量
        spinlock_t page_table_lock;  // 保护页表和部分计数器
        struct rw_semaphore mmap_lock; // 保护 VMA 链表/树的读写锁

        seqcount_t mm_lock_seq;      // per-VMA lock 序列号
    };
    // ... 更多字段(start_code, start_stack, exe_file 等)
};

mm_users vs mm_count 的区别

  • mm_users:真正使用这个 mm 的用户数(线程数 + get_user_pages 等持有者),归零时释放用户资源
  • mm_count:mm_struct 结构体本身的引用数(mm_users 计为 1),归零时释放 mm_struct 内存

线程(clone() 时共享 mm)共享同一个 mm_struct,mm_users 计数每个线程,而 mm_count 只计一次。

7.2 从红黑树到 Maple Tree

在 6.1 版本之前,VMA 由红黑树(rbtree)和链表双重索引:红黑树按地址范围快速查找,链表按地址顺序遍历。但维护两个数据结构带来了复杂性和锁竞争。

Linux 6.1(2022 年)引入 Maple Tree 替代红黑树,由 Oracle 开发,专为内核范围区间查找优化:

struct mm_struct {
    struct maple_tree mm_mt;  // 替代原来的 mm_rb + mmap 链表
};

Maple Tree 的优势

  1. B-tree 变体,节点存储多个区间,缓存行利用率更高(红黑树每节点只存一个值)
  2. 内置 RCU(Read-Copy-Update)支持,无锁读取 VMA
  3. 原生支持区间查询("找所有与 [addr, addr+len) 重叠的 VMA"),红黑树需要额外代码
  4. 支持 lockless 遍历,减少 mmap_lock 竞争

7.3 vm_area_struct:虚拟内存区域

VMA(include/linux/mm_types.h:913)描述进程地址空间中一段连续的虚拟内存区域,是进程内存映射的基本单元:

struct vm_area_struct {
    /* 第一缓存行:VMA 树遍历关键信息 */
    union {
        struct {
            unsigned long vm_start;  // 区域起始虚拟地址(含)
            unsigned long vm_end;    // 区域结束虚拟地址(不含)
        };
        freeptr_t vm_freeptr;        // SLAB_TYPESAFE_BY_RCU 时的空闲指针
    };

    struct mm_struct *vm_mm;         // 所属进程的 mm_struct
    pgprot_t vm_page_prot;           // 页保护属性(读/写/执行位)

    const vm_flags_t vm_flags;       // VM_READ / VM_WRITE / VM_EXEC / VM_SHARED 等

    /* 匿名页反向映射(RMAP)*/
    struct list_head anon_vma_chain; // 链入 anon_vma->rb_root
    struct anon_vma *anon_vma;       // 匿名内存 VMA

    /* 文件映射后端 */
    const struct vm_operations_struct *vm_ops; // mmap/fault/open/close 操作集
    unsigned long vm_pgoff;          // 在文件中的页偏移
    struct file *vm_file;            // 映射的文件(NULL 表示匿名映射)
    void *vm_private_data;           // 驱动私有数据

    /* per-VMA 锁(减少 mmap_lock 竞争)*/
    unsigned int vm_lock_seq;        // 锁序列号
    refcount_t vm_refcnt;            // VMA 引用计数

    /* 文件映射 rmap:interval tree 节点 */
    struct {
        struct rb_node rb;
        unsigned long rb_subtree_last;
    } shared;

#ifdef CONFIG_NUMA
    struct mempolicy *vm_policy;     // NUMA 分配策略
    struct vma_numab_state *numab_state; // NUMA 负载均衡状态
#endif
} __randomize_layout;

vm_flags 常见标志

标志 含义
VM_READ 可读
VM_WRITE 可写
VM_EXEC 可执行
VM_SHARED 共享映射(MAP_SHARED)
VM_MAYWRITE 可以设置 VM_WRITE(mprotect 使用)
VM_GROWSDOWN 向下生长(栈)
VM_LOCKED mlock() 锁定
VM_HUGETLB 使用 Huge Pages
VM_DONTCOPY fork 时不复制
VM_IO 设备 I/O 映射

7.4 典型进程地址空间布局(x86_64)

虚拟地址空间布局(x86_64,非 ASLR 简化版):

0xFFFFFFFFFFFFFFFF ┬─────────────────
                   │  内核空间(128TB)
0xFFFF800000000000 ┼─────────────────
      ...(不可用的规范形式漏洞)
0x00007FFFFFFFFFFF ┼─────────────────
                   │  用户栈(向下生长)
                   │  ↓
                   ├── mmap 区域(从高到低:MAP_FIXED、共享库、匿名映射)
                   │  ↓
                   ├── 堆(brk():向上生长)
                   │  ↑
                   ├── BSS 段(未初始化全局变量)
                   ├── Data 段(初始化全局变量)
                   ├── Text 段(代码)
0x0000000000400000 ┼─────────────────
                   │  NULL 保护区(不可访问)
0x0000000000000000 ┴─────────────────

7.5 mmap() 系统调用路径

用户调用 mmap(addr, len, prot, flags, fd, offset)

syscall → sys_mmap()
  └── ksys_mmap_pgoff()
        └── vm_mmap_pgoff()
              └── do_mmap()
                    ├── 参数验证(地址对齐、长度、权限)
                    ├── 寻找合适的空闲虚拟地址(若未指定 addr)
                    │   get_unmapped_area() → arch_get_unmapped_area()
                    ├── 创建 VMA:
                    │   vm_area_alloc(mm)           [从 slab 分配 vma]
                    │   vma->vm_start = addr
                    │   vma->vm_end = addr + len
                    │   vma->vm_flags = calc_vm_prot_bits(prot, flags)
                    │   vma->vm_file = file
                    │   vma->vm_pgoff = pgoff
                    ├── 若有文件:call_mmap(file, vma)
                    │   → file->f_op->mmap(file, vma) [驱动/fs 注册 vm_ops]
                    ├── 插入 Maple Tree:
                    │   vma_iter_store(&vmi, vma)    [插入 mm->mm_mt]
                    └── 更新 mm->map_count / mm->total_vm

关键设计mmap() 不立即建立物理内存映射,只创建 VMA 描述符(延迟分配策略)。物理内存在首次访问时通过缺页异常(page fault)分配。这是 demand paging 的核心。


8. 页表管理

8.1 多级页表结构

x86_64 Linux 支持四级或五级页表(取决于 CONFIG_X86_5LEVEL):

四级页表(48 位虚拟地址):

虚拟地址:[47:39]=PGD索引 [38:30]=PUD索引 [29:21]=PMD索引 [20:12]=PTE索引 [11:0]=页内偏移

CR3 寄存器
  └──► PGD(Page Global Directory,512 项)
         └──► PUD(Page Upper Directory,512 项)
                └──► PMD(Page Middle Directory,512 项)
                       └──► PTE(Page Table Entry,512 项)
                              └──► 物理页(4KB)

五级页表(57 位虚拟地址,加了 P4D 层):
CR3 → PGD → P4D → PUD → PMD → PTE → 物理页

每级页表项(Entry)的结构(以 PTE 为例):

PTE 格式(x86_64):

63  62:52  51:12  11  10   9   8   7   6   5   4   3   2   1   0
 ┌───┬────┬──────┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
 │NX │ OS │ PFN  │G │PS│D │A │UC│WT│U │W │P │
 └───┴────┴──────┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
  │         │        │  │  │  │  │  │  │  │  └─ Present
  │         │        │  │  │  │  │  │  │  └─── Writable
  │         │        │  │  │  │  │  │  └────── User/Supervisor
  │         │        │  │  │  │  │  └───────── Write-Through
  │         │        │  │  │  │  └──────────── Cache Disable
  │         │        │  │  │  └─────────────── Accessed(硬件置位)
  │         │        │  │  └────────────────── Dirty(硬件置位)
  │         │        │  └───────────────────── Page Size(PMD 时表示大页)
  │         │        └──────────────────────── Global(TLB 全局条目)
  │         └───────────────────────────────── 物理页帧号(PFN)
  └─────────────────────────────────────────── No-Execute

8.2 内核代码中的页表访问

// mm/memory.c:6372(在 __handle_mm_fault 中)
pgd = pgd_offset(mm, address);          // PGD:mm->pgd[PGD_INDEX(address)]
p4d = p4d_alloc(mm, pgd, address);      // P4D(五级页表用;四级时是 PGD 本身)
vmf.pud = pud_alloc(mm, p4d, address);  // PUD
vmf.pmd = pmd_alloc(mm, vmf.pud, address); // PMD
// 最终到 PTE 层由 handle_pte_fault 处理

每个 xxx_alloc() 函数:如果对应的页表页不存在,就从 ptdesc 分配新的页表页(来自 kmem_cache),并原子地插入父级页表项。

8.3 TLB Shootdown

修改页表后,TLB(Translation Lookaside Buffer)中缓存的旧翻译必须失效。在多处理器系统中,这需要通知所有 CPU,称为 TLB shootdown

TLB Shootdown 流程(以 munmap 为例):

CPU 0(执行 munmap):
1. 修改页表(清除 PTE)
2. 刷新本地 TLB:flush_tlb_range() / invlpg
3. 发送 IPI(处理器间中断)给其他所有 CPU
   → smp_call_function_many()
4. 等待其他 CPU 完成 TLB 刷新(可选:批量延迟处理)

其他 CPU 收到 IPI:
  → flush_tlb_func() [中断处理]
  → local_flush_tlb_range(info)
  → invlpg [x86 指令,使单个 TLB 条目无效]

mmu_gather 机制:为避免每次 PTE 修改都立即 shootdown(开销极大),Linux 用 struct mmu_gather(tlb.h)批量收集需要 shootdown 的 PTE,在 tlb_finish_mmu() 时统一执行一次 shootdown。这是 Linux 内存管理中典型的批量延迟优化。


9. 缺页异常处理链

9.1 总体流程

当 CPU 访问一个虚拟地址,且该地址对应的 PTE 不存在(Present 位 = 0)或权限不足时,CPU 触发缺页异常(Page Fault),进入内核的缺页处理路径:

CPU 触发 Page Fault
  └── arch 层(x86: arch/x86/mm/fault.c)
        └── do_page_fault() / exc_page_fault()
              └── handle_page_fault()
                    ├── 判断地址是否在有效的 VMA 内
                    │   find_vma(mm, address)
                    │   若不在 VMA → SIGSEGV
                    │
                    └── handle_mm_fault(vma, address, flags, regs)
                          └── __handle_mm_fault(vma, address, flags)
                                [mm/memory.c:6355]
                                ├── 建立 PGD/P4D/PUD/PMD(若不存在)
                                ├── 若是大页(THP):handle huge pmd/pud
                                └── handle_pte_fault(&vmf)
                                      [mm/memory.c:6273]
                                      ├── PTE 不存在 → do_pte_missing()
                                      │   ├── 匿名页 → do_anonymous_page()
                                      │   └── 文件页 → do_fault() / vm_ops->fault()
                                      ├── PTE 存在但不 present(swap)→ do_swap_page()
                                      ├── NUMA 提示页 → do_numa_page()
                                      └── 写保护 → do_wp_page()(COW)

9.2 匿名页缺页:do_anonymous_page()

当进程首次访问匿名映射区域(heap、stack、MAP_ANONYMOUS)时:

// mm/memory.c:5217
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct folio *folio;
    pte_t entry;

    // 读访问:映射零页(zero page),所有进程共享一个只读的全零物理页
    if (!(vmf->flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(vma->vm_mm)) {
        entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
                                      vma->vm_page_prot));
        // 建立 PTE 指向零页(只读)
        // → 写时才会真正分配物理页(COW)
        goto setpte;
    }

    // 写访问:分配真实的物理页
    folio = alloc_anon_folio(vmf);   // 调用 alloc_pages(GFP_HIGHUSER_MOVABLE)
    if (!folio) goto oom;

    __folio_mark_uptodate(folio);    // 标记内容有效(全零)
    entry = folio_mk_pte(folio, vma->vm_page_prot);
    if (vma->vm_flags & VM_WRITE)
        entry = pte_mkwrite(pte_mkdirty(entry), vma);

    // 设置 PTE,建立虚拟→物理映射
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
    // 将 folio 加入 LRU 链表
    folio_add_lru_vma(folio, vma);
    return 0;

oom:
    return VM_FAULT_OOM;
}

零页优化:读取新分配内存时,所有进程共享同一个物理零页(ZERO_PAGE()),完全无需分配。首次写入时,COW 机制才分配独立物理页。

9.3 写时复制(COW):do_wp_page()

COW(Copy-on-Write)是 fork() 高效实现的基础:

COW 触发场景(do_wp_page,mm/memory.c 约 3300 行):

场景:父进程 fork() 后写入共享页

1. fork() 时:子进程继承父进程所有 PTE,但将 PTE 设为只读
   parent PTE: PA → [PFN=0x1234, W=0](只读)
   child  PTE: PA → [PFN=0x1234, W=0](只读)

2. 子进程写入地址 PA:
   → 触发 Page Fault(写保护违例)
   → do_wp_page():
     a. 检查 folio 的 _refcount(是否只有我一个用户?)
        若唯一持有者(refcount == 1):直接 mkwrite,无需复制
     b. 否则:
        new_folio = alloc_folio(GFP_HIGHUSER_MOVABLE)
        copy_user_highpage(new_folio, old_folio, ...)  // 复制内容
        set_pte_at(..., new_pfn, writable_pte)        // 更新子进程 PTE
        folio_put(old_folio)                          // 释放对旧页的引用

3. 父进程的 PTE 仍指向旧页(不受影响)

PG_anon_exclusive 标志include/linux/page-flags.h:146):表示此匿名 folio 被某个 VMA 独占,不被任何其他 VMA 共享。do_wp_page() 检查此标志来决定是否真正需要复制,减少不必要的页复制。

9.4 文件页缺页

访问 mmap() 映射的文件区域:

do_fault() → do_read_fault() → do_cow_fault() → do_shared_fault()
                ↓
            __do_fault(vmf)
                ↓
            vm_ops->fault(vmf)   [文件系统实现,如 ext4_filemap_fault]
                ↓
            filemap_fault()      [通用文件缓存缺页]
                ├── 查找 page cache:find_get_page(mapping, index)
                ├── 若在 cache 中:直接建立 PTE 映射
                └── 若不在 cache:
                    folio_alloc(GFP_HIGHUSER_MOVABLE)
                    → add_to_page_cache_lru()
                    → submit_bio() [发起磁盘 I/O]
                    → wait_on_page_locked() [等待 I/O 完成]
                    → 建立 PTE 映射(VM_FAULT_MAJOR)

major fault vs minor fault

  • Minor fault(小缺页):物理页已在内存中(page cache 命中、COW 零页等),只需建立 PTE 映射,不需 I/O
  • Major fault(大缺页):物理页不在内存中,需要从磁盘读取,VM_FAULT_MAJOR 置位

handle_mm_fault()VM_FAULT_MAJOR 标志会更新 current->maj_flt 计数(mm/memory.c:6507),可通过 /proc/[pid]/stat 观察。


10. 内存回收子系统

10.1 回收总体架构

当空闲内存低于水位线时,内核必须回收内存:

内存压力来源:
    alloc_pages() 发现 free < WMARK_LOW
    ↓
    两条回收路径:

┌─────────────────────────────────┐
│    后台回收:kswapd              │
│    条件:free < WMARK_LOW        │
│    目标:free > WMARK_HIGH       │
│    行为:异步,不阻塞分配者       │
└────────────────┬────────────────┘
                 │
┌────────────────▼────────────────┐
│    直接回收:分配者自己回收       │
│    条件:kswapd 回收不够快        │
│    或 free < WMARK_MIN          │
│    行为:同步,阻塞当前进程       │
└─────────────────────────────────┘

kswapd:每个 NUMA 节点一个 kswapd 线程([kswapd0], [kswapd1]...),通过 pgdat->kswapd_wait 等待队列唤醒。

10.2 传统四链表 LRU

经典 LRU(include/linux/mmzone.h:316)将所有可回收页分为四个链表(加 unevictable):

enum lru_list {
    LRU_INACTIVE_ANON = 0,  // 不活跃匿名页:候选换出对象
    LRU_ACTIVE_ANON   = 1,  // 活跃匿名页:最近被访问的匿名页
    LRU_INACTIVE_FILE = 2,  // 不活跃文件页:候选丢弃/写回对象
    LRU_ACTIVE_FILE   = 3,  // 活跃文件页:最近被访问的文件页
    LRU_UNEVICTABLE   = 4,  // 不可驱逐(mlock/ramfs)
    NR_LRU_LISTS
};
LRU 四链表状态机:

新分配页 ──────────────────────→ ACTIVE_ANON / ACTIVE_FILE
                                    │
                               时间流逝/扫描
                                    │
                                    ▼
                              INACTIVE_ANON / INACTIVE_FILE
                               │              │
                  最近被访问(PG_referenced)   │
                               │              │ 回收扫描
                               ▼              ▼
                          升回 ACTIVE    shrink_folio_list()
                                              │
                                    ┌─────────┴──────────┐
                                    │                    │
                               文件页               匿名页
                               丢弃(clean)          换出 swap
                               或写回(dirty)

LRU 链表由 struct lruvec 管理(include/linux/mmzone.h:669):

struct lruvec {
    struct list_head lists[NR_LRU_LISTS]; // 五条 LRU 链表
    spinlock_t lru_lock;                  // 保护链表
    unsigned long anon_cost;              // 回收匿名页的历史代价
    unsigned long file_cost;              // 回收文件页的历史代价
    atomic_long_t nonresident_age;        // 非驻留年龄(workingset 检测)
    unsigned long refaults[ANON_AND_FILE]; // 重新 fault 统计
#ifdef CONFIG_LRU_GEN
    struct lru_gen_folio lrugen;          // MGLRU 数据
#endif
};

10.3 shrink_folio_list():核心回收逻辑

shrink_folio_list()mm/vmscan.c:1083)是内存回收的核心函数,处理一批待回收的 folio:

static unsigned int shrink_folio_list(struct list_head *folio_list,
        struct pglist_data *pgdat, struct scan_control *sc,
        struct reclaim_stat *stat, bool ignore_references,
        struct mem_cgroup *memcg)
{
    while (!list_empty(folio_list)) {
        folio = lru_to_folio(folio_list);  // 取出候选 folio
        folio_trylock(folio);               // 获取页锁

        // 检查是否正在 writeback(写回中的页不立即回收)
        if (folio_test_writeback(folio)) {
            // 等待写回完成(或放弃)
            goto keep_locked;
        }

        // 是否最近被访问过?(第二次机会算法)
        references = folio_check_references(folio, sc);
        if (references == FOLIOREF_ACTIVATE) {
            // 刚被访问,回到 active 链表
            goto activate_locked;
        }

        // 文件页且脏:需要写回
        if (folio_test_dirty(folio) && is_file_lru) {
            // pageout():提交写回 I/O
            if (pageout(folio, mapping) == PAGE_SUCCESS)
                goto keep; // 写回中,暂不回收
        }

        // 解除所有用户页表的映射
        if (folio_mapped(folio))
            unmap_folio(folio);  // → try_to_unmap() → RMAP 反向映射

        // 匿名页:写入 swap
        if (folio_test_anon(folio) && !folio_test_swapcache(folio)) {
            add_to_swap(folio);  // 分配 swap 槽位,写入 swap 设备
        }

        // 最终释放:从 page cache / swapcache 移除,归还给 buddy
        folio_free(folio);
        nr_reclaimed++;
    }
}

RMAP(Reverse Mapping)反向映射:给定一个 folio,找到所有映射了它的页表项,批量取消映射。实现在 mm/rmap.c,使用 anon_vma 链表(匿名页)和 address_space->i_mmap 区间树(文件页)。

10.4 回收扫描控制:scan_control

struct scan_control {
    unsigned long nr_to_scan;     // 本次需要扫描的页数
    unsigned long nr_reclaimed;   // 已回收的页数
    int order;                    // 触发回收的分配 order
    unsigned int priority;        // 扫描优先级(0 = 最高,DEF_PRIORITY = 12)
    gfp_t gfp_mask;              // 分配标志(约束可用回收手段)
    unsigned int may_writepage:1; // 是否允许写回脏页
    unsigned int may_unmap:1;     // 是否允许取消映射
    unsigned int may_swap:1;      // 是否允许换出匿名页
    // ...
};

priority 的含义:数值越小,扫描力度越大。shrink_inactive_list() 每次扫描 nr_to_scan >> priority 个页,priority 递减时扫描更多页。


11. MGLRU:多代 LRU

11.1 传统 LRU 的问题

传统四链表 LRU 的主要缺陷:

  1. 粗粒度:active/inactive 只有两代,无法区分"刚刚访问"和"很久前访问"
  2. 文件/匿名独立:比较文件页和匿名页的热度困难
  3. 扫描开销:需要获取 LRU 锁扫描链表,高并发下竞争激烈
  4. 页表感知:不直接感知 CPU 的访问位(Accessed bit),需要 rmap 逆向查找

11.2 MGLRU 设计思想

Multi-Gen LRU(MGLRU,CONFIG_LRU_GEN)由 Google 工程师 Yu Zhao 开发,Linux 6.1 合并主线。

核心思想:用"世代"(Generation)代替"活跃/不活跃"二分

// include/linux/mmzone.h:398
#define MIN_NR_GENS  2U   // 最少 2 个世代(维持第二次机会算法)
#define MAX_NR_GENS  4U   // 最多 4 个世代(需要 order_base_2(5) = 3 个 bit)
#define MAX_NR_TIERS 4U   // 每个世代内的访问层级

世代编号用 folio->flags 中的 LRU_GEN_MASK 字段存储(include/linux/mmzone.h:425)。

11.3 MGLRU 核心数据结构

// include/linux/mmzone.h:490
struct lru_gen_folio {
    unsigned long max_seq;           // 最年轻世代编号(单调递增)
    unsigned long min_seq[ANON_AND_FILE]; // 最老世代编号(anon/file 分开)
    unsigned long timestamps[MAX_NR_GENS]; // 各世代诞生时间(jiffies)

    // 多维 LRU 链表:[世代][anon/file][zone]
    struct list_head folios[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];

    // 各维度的页面数量
    long nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];

    // 统计(用于回收决策)
    unsigned long avg_refaulted[ANON_AND_FILE][MAX_NR_TIERS];
    unsigned long avg_total[ANON_AND_FILE][MAX_NR_TIERS];
    unsigned long protected[NR_HIST_GENS][ANON_AND_FILE][MAX_NR_TIERS];
    atomic_long_t evicted[NR_HIST_GENS][ANON_AND_FILE][MAX_NR_TIERS];
    atomic_long_t refaulted[NR_HIST_GENS][ANON_AND_FILE][MAX_NR_TIERS];
    bool enabled;
};

11.4 MGLRU 世代滑动窗口

MGLRU 世代滑动窗口示意(MAX_NR_GENS = 4):

时间轴 ────────────────────────────────────────►

世代 N+3 (最年轻, max_seq)
   └── 最近 fault 进来的页(新生代)

世代 N+2
   └── 上一轮 aging 后留下的页

世代 N+1
   └── 更老的页

世代 N (最老, min_seq)
   └── 候选驱逐(eviction 从这里取)

Aging(老化):
  max_seq++ → 新建最年轻世代
  老世代内的被访问页 → 晋升到年轻世代(folio_update_gen)

Eviction(驱逐):
  从 min_seq 世代取页 → 回收
  若该世代耗尽 → min_seq++

11.5 MGLRU Tier 与工作集保护

每个世代内部按 Tier 区分访问频次(include/linux/mmzone.h:401):

Tier 0: 通过页表 fault 进来,未被 fd 读取(PG_referenced 未设置)
Tier 1: 被文件描述符读取 1 次(PG_referenced 设置)
Tier 2: 文件 fd 读取 2 次(LRU_REFS_MASK 第 1 位)
Tier 3: 文件 fd 读取 4 次(PG_workingset 设置,热页保护)

MGLRU 通过 avg_refaulted / avg_total 统计各 Tier 的 refault 率,推断哪些 Tier 的页属于真实工作集,回收时优先保护工作集页。

11.6 页表扫描(MM Walk)

MGLRU 最关键的创新:直接扫描页表的 Accessed 位,无需 RMAP 逆向查找。

MGLRU MM Walk 流程:

lru_gen_mm_walk(后台 kswapd 触发):
1. 遍历 mm->mm_mt 中的所有 VMA
2. 对每个 VMA,遍历页表:walk_page_range()
3. 对每个已设置 Accessed 位的 PTE:
   a. 清除 Accessed 位
   b. folio_update_gen():将对应 folio 晋升到年轻世代
4. 使用 Bloom Filter 缓存非叶节点(PUD/PMD)
   避免重复扫描无访问的子树(mm/vmscan.c lru_gen_mm_state)

Bloom Filter 的使用(include/linux/mmzone.h:529):记录最近一轮扫描中发现有 Accessed 位的非叶页表节点,下轮扫描优先访问这些节点,跳过无访问的子树,大幅降低 MM Walk 开销。


12. OOM Killer

12.1 OOM 触发条件

当内存分配(__alloc_pages_slowpath())经历:

  1. 唤醒 kswapd,仍然失败
  2. 直接内存压缩(Compaction),仍然失败
  3. 直接内存回收(Reclaim),仍然失败

则进入 __alloc_pages_may_oom()out_of_memory()

12.2 out_of_memory() 核心流程

// mm/oom_kill.c:1119
bool out_of_memory(struct oom_control *oc)
{
    // 1. 通知链:允许驱动/应用释放内存
    blocking_notifier_call_chain(&oom_notify_list, 0, &freed);
    if (freed > 0) return true;  // 有内存释放,不杀进程

    // 2. 当前进程即将退出?让它先分配完内存然后退出
    if (task_will_free_mem(current)) {
        mark_oom_victim(current);
        queue_oom_reaper(current);
        return true;
    }

    // 3. sysctl_oom_kill_allocating_task:直接杀当前进程
    if (sysctl_oom_kill_allocating_task && ...)
        oom_kill_process(oc, "Out of memory (kill allocating task)");

    // 4. 选择"最坏"的进程
    select_bad_process(oc);  // → oom_evaluate_task() → oom_badness()

    // 5. 杀进程
    oom_kill_process(oc, "Out of memory");
    return !!oc->chosen;
}

12.3 oom_badness():进程评分

OOM 选择的依据是 oom_badness() 分数(mm/oom_kill.c 约 219 行):

oom_score = RSS + page_table_pages + swap_entries
           + 调整(oom_score_adj / 1000 * 总内存)

其中 oom_score_adj 范围:-1000 ~ +1000
  -1000:从不杀(OOM_SCORE_ADJ_MIN,如 init 进程)
     0 :默认,不偏向
  +1000:优先杀(适合沙箱进程)

为什么选 RSS(Resident Set Size)而不是虚拟地址空间? 虚拟地址可以很大(延迟分配),但 RSS 反映实际占用的物理内存。杀死 RSS 最大的进程能最快回收最多内存。

12.4 oom_kill_process()

// mm/oom_kill.c:1024
static void oom_kill_process(struct oom_control *oc, const char *message)
{
    // 打印 OOM 信息(dmesg 中的 "Out of memory: Kill process...")
    __oom_kill_process(victim, message);
    // 发送 SIGKILL
    // 设置 TIF_MEMDIE 标志,允许受害者从 PF_MEMALLOC 池分配内存(以便快速退出)
    // 唤醒 oom_reaper 内核线程(异步回收受害进程的内存)
}

oom_reaper(mm/oom_kill.c):在受害进程收到 SIGKILL 但可能还未退出时,oom_reaper 线程异步遍历受害者的匿名 VMA 并释放物理页,加速内存回收,避免系统长时间卡死。


13. Swap 子系统

13.1 Swap Entry 编码

Swap entry(include/linux/mm_types.h:285)是一个 unsigned long,编码了 swap 设备和槽位信息:

// include/linux/swapops.h:27
#define SWP_TYPE_SHIFT  (BITS_PER_XA_VALUE - MAX_SWAPFILES_SHIFT)
#define SWP_OFFSET_MASK ((1UL << SWP_TYPE_SHIFT) - 1)

// 编码:
// | swp_type (高位) | swp_offset (低位) |
//   MAX_SWAPFILES_SHIFT 位   SWP_TYPE_SHIFT 位
// include/linux/swapops.h:88
static inline swp_entry_t swp_entry(unsigned int type, pgoff_t offset)
{
    swp_entry_t ret;
    ret.val = (type << SWP_TYPE_SHIFT) | (offset & SWP_OFFSET_MASK);
    return ret;
}

// 解码:
static inline unsigned swp_type(swp_entry_t entry) {
    return (entry.val >> SWP_TYPE_SHIFT);  // line 98
}
static inline pgoff_t swp_offset(swp_entry_t entry) {
    return entry.val & SWP_OFFSET_MASK;    // line 107
}

当一个匿名页被换出到 swap 时,对应的 PTE 被替换为 swap entry:

PTE 换出前:[PFN=0x1234, Present=1, Dirty=1]
                   ↓ swap_out
PTE 换出后:[swp_type=0, swp_offset=42, Present=0]
                   ↓ 下次访问触发 page fault
              do_swap_page() 读取 swap,恢复 PTE

13.2 Swap 写出路径

匿名页换出(shrink_folio_list 中):

add_to_swap(folio)
  ├── get_swap_page()              [分配 swap 槽位]
  │   └── swap_alloc_cluster() 或 scan_swap_map()
  │       返回 swp_entry_t(type + offset)
  ├── add_to_swap_cache(folio, entry) [加入 swapcache]
  │   → radix tree / xarray 索引
  └── folio_mark_dirty(folio)      [标记需要写回]

swap_writepage(folio, wbc)
  └── swp_entry = folio->swap
      swap_info = get_swap_device(swp_entry)
      block_device = swap_info->bdev
      submit_bio(WRITE, ...)        [异步写入 swap 设备]

13.3 Swap 读入路径

缺页异常处理 swap page:

handle_pte_fault() → do_swap_page()
  ├── swp_entry = pte_to_swp_entry(orig_pte)
  ├── 查找 swapcache:find_get_page(swap_address_space, offset)
  │   ├── 命中:直接映射,无需 I/O(minor fault)
  │   └── 未命中:
  │       folio = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, addr)
  │       add_to_swap_cache(folio, swp_entry)
  │       swap_readpage(folio)        [发起读 I/O]
  │         └── submit_bio(READ, ...) → block layer
  │       wait_on_page_locked(folio) [等待 I/O 完成]
  ├── unmap_and_move():处理多个进程共享同一 swap entry
  ├── set_pte_at():恢复 PTE 指向物理页
  └── swap_free(swp_entry):释放 swap 槽位

13.4 Swap 设备管理

每个 swap 分区/文件由 struct swap_info_struct 管理,核心字段:

struct swap_info_struct {
    unsigned long flags;         // SWP_USED / SWP_WRITEOK 等
    signed short prio;           // 优先级(高优先级先用)
    struct block_device *bdev;   // swap 设备
    struct file *swap_file;      // swap 文件(若用文件作 swap)
    unsigned int max;            // swap 槽位总数
    unsigned char *swap_map;     // 槽位引用计数数组
    struct swap_cluster_info *cluster_info; // 集群管理(优化连续分配)
    unsigned long *frontswap_map; // frontswap(内存压缩)位图
};

Swap 优先级swapon -p 10 /dev/sdb 设置优先级 10,内核优先填满高优先级的 swap 设备,再使用低优先级的,实现 swap 分层(SSD + HDD)。


14. 综合视角:各层协作

14.1 一次 malloc() 的完整旅程

malloc(4096) 为例,追踪从用户空间到硬件的完整路径:

用户程序调用 malloc(4096)

  glibc malloc:
  ├── 若 brk 堆有空间:从 heap 切割返回(无系统调用)
  └── 若需要扩展堆:brk() 系统调用

  brk() → mm/mmap.c → do_brk_flags()
    └── vm_area_struct 扩展:vma->vm_end += PAGE_SIZE
        (此时无物理内存分配,PTE 尚未建立)

  程序写入 ptr[0] = 'X':

  CPU:虚拟地址 → 查 TLB → Miss → 查页表 → PTE 不存在
  CPU 触发 Page Fault(#PF 中断)

  x86 fault handler → handle_mm_fault()
  → __handle_mm_fault() → handle_pte_fault() → do_anonymous_page()
    ├── alloc_anon_folio(vmf)
    │   └── alloc_pages(GFP_HIGHUSER_MOVABLE, 0)
    │       └── __alloc_pages_noprof()
    │           └── get_page_from_freelist()
    │               └── rmqueue() → rmqueue_pcplist()
    │                   从 per_cpu_pages 取出一个 order-0 页
    │                   (若 pcp 为空,从 buddy 补充)
    │
    ├── 设置 PTE:set_pte_at(mm, addr, pte, entry)
    │   entry 包含:PFN | Present | Writable | User | Dirty
    │
    ├── 更新 TLB(flush_tlb_page)
    └── folio_add_lru(folio) → 加入 LRU_ACTIVE_ANON

  Page Fault 返回,用户程序继续执行
  ptr[0] = 'X' 写入成功(现在有物理页了)

14.2 内存压力下的协作

内存压力场景:系统内存严重不足

┌─────────────────────────────────────────────────────────┐
│  应用持续分配内存                                         │
│  free_pages < WMARK_LOW                                 │
└───────────────────────────┬─────────────────────────────┘
                            │ 唤醒 kswapd
┌───────────────────────────▼─────────────────────────────┐
│  kswapd 后台回收                                          │
│  kswapd_shrink_node() → shrink_node()                   │
│  → 扫描 LRU / MGLRU → shrink_folio_list()               │
│     ├── 文件页(clean)→ 直接丢弃                         │
│     ├── 文件页(dirty)→ 写回,稍后丢弃                   │
│     └── 匿名页 → 换出 swap(若 vm.swappiness > 0)       │
└───────────────────────────┬─────────────────────────────┘
                            │ 若回收仍不够
┌───────────────────────────▼─────────────────────────────┐
│  直接回收(分配者执行)                                    │
│  __alloc_pages_direct_reclaim()                         │
│  → try_to_free_pages() → shrink_zones()                 │
│  阻塞当前进程直到回收足够内存                              │
└───────────────────────────┬─────────────────────────────┘
                            │ 若仍然失败
┌───────────────────────────▼─────────────────────────────┐
│  OOM Killer                                             │
│  out_of_memory() → select_bad_process()                 │
│  → oom_kill_process() → SIGKILL 受害者                  │
│  → oom_reaper 异步回收内存                               │
└─────────────────────────────────────────────────────────┘

14.3 关键调优参数

参数 路径 说明
vm.min_free_kbytes /proc/sys/vm/ 控制水位线基准值
vm.swappiness /proc/sys/vm/ 0=避免 swap,100=积极 swap,默认 60
vm.dirty_ratio /proc/sys/vm/ 脏页占内存比例上限(触发同步写回)
vm.dirty_background_ratio /proc/sys/vm/ 后台写回触发阈值
vm.overcommit_memory /proc/sys/vm/ 0=启发式,1=总是允许,2=严格
/proc/sys/vm/nr_hugepages 预分配的 HugePage 数
/sys/kernel/mm/lru_gen/enabled MGLRU 开关
/proc/[pid]/oom_score_adj 进程 OOM 评分调整

14.4 性能观测工具

# 内存整体状态
cat /proc/meminfo
# MemTotal, MemFree, MemAvailable, Buffers, Cached
# SwapTotal, SwapFree
# AnonPages, Mapped, Shmem
# KReclaimable: 可回收内核内存(slab 可回收部分)

# 详细虚拟内存统计
cat /proc/vmstat
# pgfault: 总缺页次数(minor)
# pgmajfault: 大缺页次数(需要 I/O)
# pswpin/pswpout: swap 读写页数
# pgsteal_*: 各 LRU 回收页数
# pginodesteal: inode slab 回收

# SLUB 统计
cat /sys/kernel/slab/<cache_name>/stat

# 内存回收追踪
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_kswapd_wake/enable
cat /sys/kernel/debug/tracing/trace

# per-process 内存
cat /proc/[pid]/smaps_rollup
# Private_Clean, Private_Dirty, Shared_Clean, Shared_Dirty, Swap

# NUMA 统计
numactl --hardware
cat /proc/[pid]/numa_maps

# 伙伴系统状态
cat /proc/buddyinfo
# Node 0, zone Normal: 1 2 3 4 5 6 7 8 9 10 11
# 每列是该 order 的空闲块数

附录 A:关键源文件速查

功能 源文件
物理内存区域定义 include/linux/mmzone.h
page/folio 结构体 include/linux/mm_types.h
页标志位 include/linux/page-flags.h
伙伴分配器 mm/page_alloc.c
SLUB 分配器 mm/slub.c
SLUB 头文件 mm/slab.h
虚拟内存映射 mm/mmap.c
缺页处理 mm/memory.c
内存回收 mm/vmscan.c
RMAP 反向映射 mm/rmap.c
OOM Killer mm/oom_kill.c
Swap 管理 mm/swap_state.c, mm/swapfile.c
页表操作 mm/pgtable-generic.c, arch/x86/mm/
内存 compaction mm/compaction.c
大页 THP mm/huge_memory.c
kmalloc API include/linux/slab.h

附录 B:设计哲学总结

1. 延迟分配(Lazy Allocation)mmap() 不立即分配物理内存;fork() 不立即复制页面(COW)。代价是需要 page fault 处理,收益是避免浪费(程序可能不访问所有分配的内存)。

2. 分层缓存(Hierarchical Caching):buddy → pcp → SLUB freelist,每层减少下层锁竞争。类似 CPU 缓存层次,用局部性原理减少全局同步开销。

3. 批量操作(Batching):pcp 批量从 buddy 取页;mmu_gather 批量收集 TLB shootdown;kswapd 批量回收页面。减少系统调用和中断次数。

4. 无锁/读无锁(Lock-free / Read-mostly):RCU 保护 VMA 读取;per-CPU 数据无需全局锁;MGLRU 的 MM Walk 尽量无锁扫描页表。Linux 锁策略从粗粒度全局锁演进到精细的 per-CPU / per-VMA / per-folio 锁。

5. 与硬件协作(HW Cooperation):利用 CPU 自动置位的 Accessed/Dirty 位追踪页访问;利用 TLB 减少页表查找;利用 NUMA topology 优化数据局部性。

6. 渐进式降级(Graceful Degradation):水位线三档(MIN/LOW/HIGH)、OOM 逐步升级(释放→回收→杀进程→panic),在资源耗尽时尽可能维持系统可用性。


由 Claude Code 分析生成