Skip to content

Latest commit

 

History

History
3019 lines (2390 loc) · 105 KB

File metadata and controls

3019 lines (2390 loc) · 105 KB

Linux 内核内存规整(Memory Compaction)与碎片整理机制深度分析

目录

  1. 问题背景:内存碎片化
  2. 伙伴系统与迁移类型分组
  3. pageblock_flags:页块标志位图
  4. compact_control:规整控制结构
  5. 两个扫描器:核心设计
  6. compact_zone() 核心流程
  7. 页面迁移引擎
  8. kcompactd 守护线程
  9. 主动规整(Proactive Compaction)
  10. 直接规整(Direct Compaction)
  11. THP 与规整的协作
  12. CMA:连续内存分配器
  13. 规整延迟机制
  14. /proc 接口解读
  15. 性能调优与 sysctl 参数
  16. 整体架构图
  17. 碎片化量化:fragmentation_index 深度分析
  18. 可移动页面判定机制
  19. isolate_migratepages_block 逐页隔离详解
  20. migrate_pages() 迁移引擎实现细节
  21. fast_find_migrateblock 快速搜索优化
  22. capture_control:零拷贝页面捕获机制
  23. compaction_suitable 与水位检查
  24. skip 机制的完整生命周期
  25. Huge Page 的 compaction 支持
  26. NUMA 感知的 compaction
  27. 调试接口深度解析
  28. compaction_free 与 compaction_alloc 的对称设计
  29. 内存热插拔与 compaction 的交互
  30. 性能分析与常见问题诊断

1. 问题背景:内存碎片化

1.1 外部碎片 vs 内部碎片

**外部碎片(External Fragmentation)**是 Linux 内存管理中内存规整主要针对的问题。即使系统中存在大量空闲内存,这些页面却可能分散在物理地址空间的各个角落,导致无法满足高阶(high-order)连续页面的分配请求。

**内部碎片(Internal Fragmentation)**则是指已分配给某个对象的内存块中,有一部分实际上并未被有效利用——例如分配了 128 字节,但实际只用了 70 字节,浪费了 58 字节。内核的 SLAB/SLUB 分配器在一定程度上会产生内部碎片,但这不是内存规整的目标。

外部碎片示意图:

物理地址 ->  0   4K  8K  12K 16K 20K 24K 28K 32K
            +---+---+---+---+---+---+---+---+---+
            |用 |空 |用 |空 |用 |空 |用 |空 |用 |
            +---+---+---+---+---+---+---+---+---+
            ^       ^       ^       ^
            各 4KB 空闲页面无法合并成连续 8KB

规整之后:

物理地址 ->  0   4K  8K  12K 16K 20K 24K 28K 32K
            +---+---+---+---+---+---+---+---+---+
            |用 |用 |用 |用 |空 |空 |空 |空 |用 |
            +---+---+---+---+---+---+---+---+---+
                                ^               ^
                          连续 16KB 空闲,可满足高阶分配

1.2 为什么需要连续物理内存

以下场景强制要求物理连续内存:

  • 透明大页(THP):分配 2MB(order-9)的大页,需要 512 个连续 4KB 页面
  • DMA 传输:部分设备驱动不支持 scatter-gather,要求 DMA buffer 物理连续
  • CMA 区域:Camera、GPU 等设备驱动需要从预留的连续内存区域分配

1.3 碎片化评分计算

内核在 mm/vmstat.c:1130 中定义了 extfrag_for_order() 函数来量化碎片程度:

// mm/vmstat.c:1130
unsigned int extfrag_for_order(struct zone *zone, unsigned int order)
{
    struct contig_page_info info;

    fill_contig_page_info(zone, order, &info);
    if (info.free_pages == 0)
        return 0;

    return div_u64((info.free_pages -
            (info.free_blocks_suitable << order)) * 100,
            info.free_pages);
}

该函数返回 [0, 100] 的值,代表无法满足指定阶数分配的空闲页面比例。值越高说明碎片越严重。


2. 伙伴系统与迁移类型分组

2.1 迁移类型枚举

Linux 伙伴系统(Buddy System)不仅按照页面阶数(order)组织空闲页,还按照**迁移类型(migratetype)**进行分组。这是减少碎片化的关键设计。定义在 include/linux/mmzone.h:64

// include/linux/mmzone.h:64
enum migratetype {
    MIGRATE_UNMOVABLE,     // 不可迁移:内核代码、内核数据结构
    MIGRATE_MOVABLE,       // 可迁移:用户态匿名页、文件页(通过 rmap)
    MIGRATE_RECLAIMABLE,   // 可回收:文件系统缓存、slab 可回收部分
    MIGRATE_PCPTYPES,      // PCP 列表使用的类型数量
    MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,  // 高原子分配预留
#ifdef CONFIG_CMA
    MIGRATE_CMA,           // CMA 区域专用
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,       // 正在被隔离,不可分配
#endif
    MIGRATE_TYPES
};

2.2 free_area 结构

每个 zone 的伙伴系统由 free_area 数组组成,每个阶数(0 到 MAX_PAGE_ORDER=10)有一个 free_area,其中又按迁移类型分为多个链表(include/linux/mmzone.h:138):

// include/linux/mmzone.h:138
struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];  // 按迁移类型分组的空闲链表
    unsigned long       nr_free;                    // 总空闲页块数
};

这意味着每个 zone 有 NR_PAGE_ORDERS * MIGRATE_TYPES 个空闲链表。

2.3 迁移类型回退机制(Fallback)

当某种迁移类型没有空闲页时,分配器会从其他类型"借用"(steal)页块。回退顺序定义在 mm/page_alloc.c:1951

// mm/page_alloc.c:1951
static int fallbacks[MIGRATE_PCPTYPES][MIGRATE_PCPTYPES - 1] = {
    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE   },
    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE },
    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE   },
};

这个回退机制是产生碎片的根本原因:当 UNMOVABLE 类型从 MOVABLE 页块中借用页面后,这个页块就被"污染"了——即使将来大部分页面空出来,也因混有不可迁移的页面而无法形成连续大页块。规整的目标正是修复这种污染。


3. pageblock_flags:页块标志位图

3.1 基本概念

pageblock 是内存管理中的重要粒度单位,其大小为 pageblock_nr_pages(通常等于 HPAGE_PMD_ORDER 即 512 页 = 2MB,由 include/linux/pageblock-flags.h:66 定义)。

// include/linux/pageblock-flags.h:64
#elif defined(CONFIG_TRANSPARENT_HUGEPAGE)
#define pageblock_order  MIN_T(unsigned int, HPAGE_PMD_ORDER, PAGE_BLOCK_MAX_ORDER)

每个 pageblock 都有一个标志位组,定义在 include/linux/pageblock-flags.h:17

// include/linux/pageblock-flags.h:17
enum pageblock_bits {
    PB_migrate_0,      // 迁移类型位 0
    PB_migrate_1,      // 迁移类型位 1
    PB_migrate_2,      // 迁移类型位 2
    PB_compact_skip,   // 规整跳过标志:若置位,规整会跳过此页块

#ifdef CONFIG_MEMORY_ISOLATION
    PB_migrate_isolate,  // 隔离标志:与迁移类型分离存储
#endif
    __NR_PAGEBLOCK_BITS
};

#define NR_PAGEBLOCK_BITS (roundup_pow_of_two(__NR_PAGEBLOCK_BITS))
#define MIGRATETYPE_MASK (BIT(PB_migrate_0)|BIT(PB_migrate_1)|BIT(PB_migrate_2))

3.2 紧凑跳过标志(PB_compact_skip)

PB_compact_skip 是规整性能优化的关键。规整算法扫描页块时,如果之前对该页块的扫描没有发现可迁移的候选页,就会设置此标志位,下次扫描直接跳过:

// include/linux/pageblock-flags.h:95
#ifdef CONFIG_COMPACTION
#define get_pageblock_skip(page) \
    get_pfnblock_bit(page, page_to_pfn(page), PB_compact_skip)
#define clear_pageblock_skip(page) \
    clear_pfnblock_bit(page, page_to_pfn(page), PB_compact_skip)
#define set_pageblock_skip(page) \
    set_pfnblock_bit(page, page_to_pfn(page), PB_compact_skip)

每个 pageblock 的标志位存储在一个集中的 bitmap 中,每个 pageblock 占用 NR_PAGEBLOCK_BITS 位(通常 8 位,经 BUILD_BUG_ONmm/page_alloc.c:396 处断言)。


4. compact_control:规整控制结构

规整的整个执行过程围绕 struct compact_control 展开,定义在 mm/internal.h:950

// mm/internal.h:950
struct compact_control {
    struct list_head freepages[NR_PAGE_ORDERS]; // 按阶数组织的空闲页链表(迁移目标)
    struct list_head migratepages;              // 待迁移页链表(迁移源)
    unsigned int nr_freepages;                  // 已隔离的空闲页数量
    unsigned int nr_migratepages;               // 待迁移页数量
    unsigned long free_pfn;                     // 空闲页扫描器当前位置(高地址向低扫)
    unsigned long migrate_pfn;                  // 迁移页扫描器当前位置(低地址向高扫)
    unsigned long fast_start_pfn;              // 快速搜索的起始 PFN
    struct zone *zone;                          // 当前规整的内存区域
    unsigned long total_migrate_scanned;        // 统计:扫描的迁移候选页数
    unsigned long total_free_scanned;           // 统计:扫描的空闲页数
    unsigned short fast_search_fail;            // 快速搜索失败次数
    short search_order;                         // 快速搜索的起始阶数
    const gfp_t gfp_mask;                      // 分配请求的 GFP 标志
    int order;                                  // 目标分配阶数
    int migratetype;                            // 目标迁移类型
    const unsigned int alloc_flags;            // 分配标志
    const int highest_zoneidx;                 // 最高允许的 zone 索引
    enum migrate_mode mode;                    // MIGRATE_ASYNC / MIGRATE_SYNC_LIGHT / MIGRATE_SYNC
    bool ignore_skip_hint;                     // 忽略 PB_compact_skip 标志,强制扫描
    bool no_set_skip_hint;                     // 不设置 skip 标志(CMA 使用)
    bool ignore_block_suitable;                // 忽略页块适合性检查
    bool direct_compaction;                    // true = 直接规整,false = kcompactd
    bool proactive_compaction;                 // kcompactd 主动规整模式
    bool whole_zone;                           // 是否扫描整个 zone
    bool contended;                            // 是否遭遇锁竞争
    bool finish_pageblock;                     // 完成当前页块的扫描
    bool alloc_contig;                         // alloc_contig_range() 调用的规整
};

zone 中也缓存了规整扫描位置,以便下次规整时不必从头开始(include/linux/mmzone.h:1028):

// include/linux/mmzone.h:1028
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
    unsigned long compact_cached_free_pfn;                // 空闲扫描器缓存位置
    unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC]; // 迁移扫描器缓存(异步/同步)
    unsigned long compact_init_migrate_pfn;
    unsigned long compact_init_free_pfn;
#endif
    unsigned int compact_considered;     // 延迟计数器
    unsigned int compact_defer_shift;    // 延迟级别
    int compact_order_failed;            // 最小失败阶数

5. 两个扫描器:核心设计

内存规整算法的核心是两个反向扫描器的设计,这是整个机制的精华所在。

5.1 架构图

Zone 地址空间
+------------------------------------------------------------------------+
| low_pfn (zone_start_pfn)                         high_pfn (zone_end)  |
+------------------------------------------------------------------------+
  ^                                                               ^
  |                                                               |
  +--- 迁移扫描器 (migrate scanner) --->       <-- 空闲扫描器 ---+
       从低地址向高地址扫描                        从高地址向低地址扫描
       寻找:可迁移的已分配页                       寻找:空闲页(迁移目标)
       (LRU 页、MOVABLE 页)                        (PageBuddy 页)


规整过程:

低地址                                                         高地址
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|U |U |M |M |F |M |F |M |M |F |F |F |F |U |U |F |F |F |F |F |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  <------ migrate scanner ------->      <-- free scanner ----->

U = 不可迁移已分配页  M = 可迁移已分配页  F = 空闲页

迁移扫描器找到 M 页,空闲扫描器找到 F 页,将 M 迁移到 F 处

规整后:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|U |U |F |F |F |F |F |F |F |F |F |U |U |M |M |M |M |M |M |M |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
          ^                       ^
          连续空闲区域              已迁移的页

两个扫描器在中间相遇时,规整结束(compact_scanners_met())

5.2 迁移扫描器:isolate_migratepages()

迁移扫描器从 zone 的低地址端向高地址端扫描,寻找可以迁移的已分配页面。入口函数是 mm/compaction.c:2045isolate_migratepages()

// mm/compaction.c:2045
static isolate_migrate_t isolate_migratepages(struct compact_control *cc)
{
    unsigned long block_start_pfn;
    unsigned long block_end_pfn;
    unsigned long low_pfn;
    // ...

    // 尝试快速搜索:从空闲链表中找到部分空闲的页块作为迁移源
    low_pfn = fast_find_migrateblock(cc);
    block_start_pfn = pageblock_start_pfn(low_pfn);
    // ...

    // 逐 pageblock 扫描,直到遇到空闲扫描器
    for (; block_end_pfn <= cc->free_pfn; ...) {
        // 检查 PB_compact_skip 标志,跳过最近扫描过且无收获的页块
        if (!isolation_suitable(cc, page))
            continue;

        // 检查页块的迁移类型是否合适
        if (!suitable_migration_source(cc, page)) {
            update_cached_migrate(cc, block_end_pfn);
            continue;
        }

        // 执行实际的页面隔离
        if (isolate_migratepages_block(cc, low_pfn, block_end_pfn, isolate_mode))
            return ISOLATE_ABORT;
        // ...
    }
}

isolate_migratepages_block()mm/compaction.c:836)是核心隔离函数,它逐页扫描页块内的页面:

  • 跳过 PageBuddy 页(空闲页)
  • 跳过 PageHuge 页(除非是 CMA 模式)
  • 跳过大于目标阶数的复合页(skip_isolation_on_order()
  • PageLRU 页调用 folio_isolate_lru() 从 LRU 链表隔离
  • page_has_movable_ops() 的页调用 isolate_movable_ops_page()

每次周期性(每 COMPACT_CLUSTER_MAX = 32 页)释放 lruvec 锁并检查信号,避免长时间持锁。

5.3 空闲页扫描器:isolate_freepages_block()

空闲扫描器从 zone 的高地址端向低地址端扫描,寻找空闲页作为迁移目标。定义在 mm/compaction.c:556isolate_freepages_block()

// mm/compaction.c:556
static unsigned long isolate_freepages_block(struct compact_control *cc,
                unsigned long *start_pfn,
                unsigned long end_pfn,
                struct list_head *freelist,
                unsigned int stride,
                bool strict)
{
    // 异步模式下以 COMPACT_CLUSTER_MAX 为步长跳跃扫描(更快但粗糙)
    if (strict)
        stride = 1;  // 严格模式(CMA)步长为 1

    for (; blockpfn < end_pfn; blockpfn += stride, page += stride) {
        // 定期释放锁
        if (!(blockpfn % COMPACT_CLUSTER_MAX) && compact_unlock_should_abort(...))
            break;

        // 跳过复合页
        if (PageCompound(page)) { ... goto isolate_fail; }

        // 检查是否为伙伴系统中的空闲页
        if (!PageBuddy(page))
            goto isolate_fail;

        // 加锁后再次确认,并调用 __isolate_free_page()
        order = buddy_order(page);
        isolated = __isolate_free_page(page, order);
        // ...
        list_add_tail(&page->lru, &freelist[order]);  // 加入按阶数组织的链表
    }
}

两个扫描器在 compact_scanners_met() 时相遇(mm/compaction.c:1418):

// mm/compaction.c:1418
static inline bool compact_scanners_met(struct compact_control *cc)
{
    return (cc->free_pfn >> pageblock_order)
        <= (cc->migrate_pfn >> pageblock_order);
}

注意:此处比较的是 pageblock 粒度(右移 pageblock_order 位),而不是页面粒度,这样可以避免边界条件下的误判。


6. compact_zone() 核心流程

compact_zone() 是内存规整的主控函数,定义在 mm/compaction.c:2511。以下是完整的执行流程分析:

6.1 流程图

compact_zone(cc, capc)
    |
    v
初始化:清零计数器,初始化 freepages/migratepages 链表
    |
    v
确定 migratetype(从 gfp_mask 推导)
    |
    v
compaction_suit_allocation_order() -- 快速检查是否需要规整
    |    (不需要?直接返回 COMPACT_SKIPPED/SUCCESS)
    v
检查是否需要重置 skip 标志(compaction_restarting())
    |    (如果之前多次失败,调用 __reset_isolation_suitable())
    v
设置扫描起始位置:
    migrate_pfn = compact_cached_migrate_pfn[sync]  (低地址端)
    free_pfn    = compact_cached_free_pfn           (高地址端)
    |
    v
lru_add_drain()  -- 刷新本 CPU 的 LRU 缓冲区
    |
    v
主循环:while (compact_finished(cc) == COMPACT_CONTINUE)
    |
    +-- 1. isolate_migratepages(cc)  -- 迁移扫描器
    |       返回 ISOLATE_ABORT -> 回滚,返回 COMPACT_CONTENDED
    |       返回 ISOLATE_NONE  -> 跳过,检查是否需要 drain
    |       返回 ISOLATE_SUCCESS -> 继续迁移
    |
    +-- 2. migrate_pages(&cc->migratepages,
    |                    compaction_alloc,  -- 从 free scanner 分配
    |                    compaction_free,   -- 回收未迁移的页
    |                    cc->mode, MR_COMPACTION)
    |       (核心调用:将隔离的页迁移到新位置)
    |
    +-- 3. 处理迁移结果:
    |       err == 0: 成功,继续下轮
    |       err == -ENOMEM: 内存不足,可能终止
    |       err != 0: 将未迁移的页放回(putback_movable_pages)
    |
    +-- 4. check_drain:
    |       如果迁移扫描器越过上次迁移的 order-aligned 边界,
    |       调用 lru_add_drain_cpu_zone() 刷新 PCP 列表,
    |       允许 buddy 页面合并
    |
    v
out:
    释放未用的空闲页(release_free_list -> __free_pages)
    更新 compact_cached_free_pfn
    统计 COMPACTMIGRATE_SCANNED / COMPACTFREE_SCANNED
    |
    v
返回 compact_result

6.2 compact_finished() 终止条件

__compact_finished()mm/compaction.c:2234)决定何时停止规整:

// mm/compaction.c:2234
static enum compact_result __compact_finished(struct compact_control *cc)
{
    // 条件1:两个扫描器相遇 -> COMPACT_COMPLETE 或 COMPACT_PARTIAL_SKIPPED
    if (compact_scanners_met(cc)) {
        reset_cached_positions(cc->zone);
        if (cc->whole_zone)
            return COMPACT_COMPLETE;
        else
            return COMPACT_PARTIAL_SKIPPED;
    }

    // 条件2:主动规整 -> 检查碎片评分是否降到低水位以下
    if (cc->proactive_compaction) {
        score = fragmentation_score_zone(cc->zone);
        wmark_low = fragmentation_score_wmark(true);
        return (score > wmark_low) ? COMPACT_CONTINUE : COMPACT_SUCCESS;
    }

    // 条件3:全量规整(order == -1)-> 继续
    if (is_via_compact_memory(cc->order))
        return COMPACT_CONTINUE;

    // 条件4:直接规整 -> 检查目标 migratetype 的 freelist 是否有合适的页
    for (order = cc->order; order < NR_PAGE_ORDERS; order++) {
        struct free_area *area = &cc->zone->free_area[order];
        if (!free_area_empty(area, migratetype))
            return COMPACT_SUCCESS;  // 已有足够空闲页,分配应该成功
#ifdef CONFIG_CMA
        if (migratetype == MIGRATE_MOVABLE &&
            !free_area_empty(area, MIGRATE_CMA))
            return COMPACT_SUCCESS;
#endif
        if (find_suitable_fallback(area, order, migratetype, true) >= 0)
            return COMPACT_SUCCESS;
    }

    return COMPACT_NO_SUITABLE_PAGE;  // 转换为 COMPACT_CONTINUE
}

6.3 compaction_alloc() 与 capture_control

直接规整有一个优化机制:通过 struct capture_control 在中断上下文直接捕获刚释放的页面。compact_zone_order()capture_control 注册到 current->capture_control,当 buddy 系统释放页面时(free_pages_prepare()),如果发现 current->capture_control 且页面满足要求,就直接将该页给到正在等待的规整操作,无需经过完整的 freelist。

6.4 check_drain:PCP 列表刷新

在主循环末尾的 check_drain 阶段(mm/compaction.c:2699),内核检查迁移扫描器是否已越过上次迁移位置的 order 对齐边界:

// mm/compaction.c:2707
if (cc->order > 0 && last_migrated_pfn) {
    unsigned long current_block_start =
        block_start_pfn(cc->migrate_pfn, cc->order);

    if (last_migrated_pfn < current_block_start) {
        lru_add_drain_cpu_zone(cc->zone);
        /* No more flushing until we migrate again */
        last_migrated_pfn = 0;
    }
}

这样做的目的是:页面迁移完成后,原来的物理页被释放,但可能仍然停留在 PCP(Per-CPU Pages)列表中,需要通过 lru_add_drain_cpu_zone() 将其真正归还给伙伴系统,让 buddy 合并算法有机会形成更大的连续块。


7. 页面迁移引擎

7.1 migrate_pages() 核心接口

页面迁移由 mm/migrate.c 实现。compact_zone() 中调用的核心接口是(mm/compaction.c:2647):

// mm/compaction.c:2647
err = migrate_pages(&cc->migratepages, compaction_alloc,
        compaction_free, (unsigned long)cc, cc->mode,
        MR_COMPACTION, &nr_succeeded);
  • compaction_alloc:从空闲扫描器收集的 freepages 链表中取页(migration target)
  • compaction_free:将迁移失败的源页放回原处
  • cc->mode:控制同步/异步行为
  • MR_COMPACTION:迁移原因,用于统计

7.2 迁移模式

迁移模式(enum migrate_mode)决定了规整的积极程度:

MIGRATE_ASYNC       -- 异步模式(最宽松)
    - 跳过正在回写(writeback)的脏页
    - trylock 而非阻塞等锁
    - 遇到锁竞争立即放弃(cc->contended = true)
    - 用于首次直接规整尝试

MIGRATE_SYNC_LIGHT  -- 轻同步模式
    - 等待脏页回写完成,但不等待 unmap
    - kcompactd 和中等优先级直接规整使用

MIGRATE_SYNC        -- 完全同步模式(最激进)
    - 等待所有条件满足
    - 用于最后手段(COMPACT_PRIO_SYNC_FULL)

7.3 可迁移页面的类型

迁移引擎支持多种页面类型(mm/migrate.c):

  • LRU 页面:匿名页和文件缓存页,通过 rmap 修改页表指向新物理页
  • movable_ops 页面:通过注册 struct movable_operations 的页面(balloon pages、offline pages、zsmalloc 页等)
  • 大页(HugeTLB):CMA 模式下可以溶解大页后迁移其组成部分
  • THP(透明大页):作为复合页整体迁移

8. kcompactd 守护线程

8.1 线程创建

每个 NUMA 节点有一个 kcompactd 内核线程,在 kcompactd_run() 中创建(mm/compaction.c:3237):

// mm/compaction.c:3237
void __meminit kcompactd_run(int nid)
{
    pg_data_t *pgdat = NODE_DATA(nid);

    if (pgdat->kcompactd)
        return;

    pgdat->kcompactd = kthread_create_on_node(kcompactd, pgdat, nid,
                                               "kcompactd%d", nid);
    if (IS_ERR(pgdat->kcompactd)) {
        pr_err("Failed to start kcompactd on node %d\n", nid);
        pgdat->kcompactd = NULL;
    } else {
        wake_up_process(pgdat->kcompactd);
    }
}

线程的 PF 标志设置了 PF_KCOMPACTDmm/compaction.c:3171),可通过 current_is_kcompactd() 检测(include/linux/compaction.h:83)。

8.2 主循环(kcompactd,mm/compaction.c:3165)

// mm/compaction.c:3165
static int kcompactd(void *p)
{
    pg_data_t *pgdat = (pg_data_t *)p;
    // 碎片检查周期:500ms(HPAGE_FRAG_CHECK_INTERVAL_MSEC)
    long default_timeout = msecs_to_jiffies(HPAGE_FRAG_CHECK_INTERVAL_MSEC);
    long timeout = default_timeout;

    current->flags |= PF_KCOMPACTD;
    set_freezable();  // 支持系统冻结(suspend)

    pgdat->kcompactd_max_order = 0;
    pgdat->kcompactd_highest_zoneidx = pgdat->nr_zones - 1;

    while (!kthread_should_stop()) {
        // 若 proactiveness=0,关闭定期检查(无限等待)
        if (!sysctl_compaction_proactiveness)
            timeout = MAX_SCHEDULE_TIMEOUT;

        // 等待唤醒或超时
        if (wait_event_freezable_timeout(pgdat->kcompactd_wait,
            kcompactd_work_requested(pgdat), timeout) &&
            !pgdat->proactive_compact_trigger) {
            // 被显式唤醒(非超时)-> 响应式规整
            psi_memstall_enter(&pflags);
            kcompactd_do_work(pgdat);
            psi_memstall_leave(&pflags);
            timeout = default_timeout;
            continue;
        }

        // 超时或主动触发 -> 主动规整
        timeout = default_timeout;
        if (should_proactive_compact_node(pgdat)) {
            prev_score = fragmentation_score_node(pgdat);
            compact_node(pgdat, true);  // proactive=true
            score = fragmentation_score_node(pgdat);
            // 若无进展,大幅延长下次检查间隔(最多 64 倍)
            if (unlikely(score >= prev_score))
                timeout = default_timeout << COMPACT_MAX_DEFER_SHIFT;
        }
        // 清除手动触发标志
        if (unlikely(pgdat->proactive_compact_trigger))
            pgdat->proactive_compact_trigger = false;
    }
}

8.3 触发条件(wakeup_kcompactd)

kcompactd 有两种唤醒路径:

路径1:kswapd 唤醒(vmscan.c:7151, 7214, 7397)

// mm/vmscan.c:7151 - kswapd 进入睡眠前,若内存水位改善
wakeup_kcompactd(pgdat, pageblock_order, highest_zoneidx);

// mm/vmscan.c:7214 - kswapd 已回收内存,通知 kcompactd 整理
wakeup_kcompactd(pgdat, alloc_order, highest_zoneidx);

// mm/vmscan.c:7397 - 内存不足但无法直接回收时
wakeup_kcompactd(pgdat, order, highest_zoneidx);

路径2:wakeup_kcompactd() 主体逻辑(mm/compaction.c:3135)

// mm/compaction.c:3135
void wakeup_kcompactd(pg_data_t *pgdat, int order, int highest_zoneidx)
{
    if (!order) return;  // order=0 不需要规整

    // 记录目标阶数和 zone
    if (pgdat->kcompactd_max_order < order)
        pgdat->kcompactd_max_order = order;
    if (pgdat->kcompactd_highest_zoneidx > highest_zoneidx)
        pgdat->kcompactd_highest_zoneidx = highest_zoneidx;

    if (!wq_has_sleeper(&pgdat->kcompactd_wait)) return;
    if (!kcompactd_node_suitable(pgdat)) return;  // zone 已满足水位

    wake_up_interruptible(&pgdat->kcompactd_wait);
}

8.4 kcompactd_do_work(响应式规整,mm/compaction.c:3055)

响应式规整使用 MIGRATE_SYNC_LIGHT 模式(比直接规整更温和),对每个 zone 调用 compact_zone()

// mm/compaction.c:3063
struct compact_control cc = {
    .order = pgdat->kcompactd_max_order,
    .search_order = pgdat->kcompactd_max_order,
    .highest_zoneidx = pgdat->kcompactd_highest_zoneidx,
    .mode = MIGRATE_SYNC_LIGHT,
    .ignore_skip_hint = false,
    .gfp_mask = GFP_KERNEL,
    .alloc_flags = defrag_mode ? ALLOC_WMARK_HIGH : ALLOC_WMARK_MIN,
};

8.5 kswapd 与 kcompactd 的协作

内存分配失败(高阶)
    |
    +---> 直接规整失败
    |
    +---> 唤醒 kswapd(回收页面,增加空闲页总量)
    |         |
    |         +---> 回收成功 -> 唤醒 kcompactd 整理新增空闲页
    |
    +---> 唤醒 kcompactd(直接针对特定 order 规整)

关键约束:should_proactive_compact_node() 检查 kswapd 是否在运行:
    if (!sysctl_compaction_proactiveness || kswapd_is_running(pgdat))
        return false;
    // kswapd 运行时不进行主动规整,避免竞争

9. 主动规整(Proactive Compaction)

主动规整(Proactive Compaction)是 Linux 5.9 引入的特性,让 kcompactd 在后台持续整理内存碎片,而不是等待分配失败后再被动响应。

9.1 碎片化评分

评分系统的核心是 fragmentation_score_zone()mm/compaction.c:2167):

// mm/compaction.c:2167
static unsigned int fragmentation_score_zone(struct zone *zone)
{
    // 以 COMPACTION_HPAGE_ORDER(通常 = HPAGE_PMD_ORDER = 9)为基准
    return extfrag_for_order(zone, COMPACTION_HPAGE_ORDER);
}

节点级评分是各 zone 评分的加权平均(mm/compaction.c:2197):

// mm/compaction.c:2197
static unsigned int fragmentation_score_node(pg_data_t *pgdat)
{
    unsigned int score = 0;
    int zoneid;

    for (zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
        zone = &pgdat->node_zones[zoneid];
        if (!populated_zone(zone)) continue;
        score += fragmentation_score_zone_weighted(zone);  // 按 zone 大小加权
    }
    return score;  // [0, 100]
}

权重计算(mm/compaction.c:2182):

// mm/compaction.c:2182
static unsigned int fragmentation_score_zone_weighted(struct zone *zone)
{
    unsigned long score;
    score = zone->present_pages * fragmentation_score_zone(zone);
    return div64_ul(score, zone->zone_pgdat->node_present_pages + 1);
    // 大 zone 权重高,避免小 zone(如 ZONE_DMA32)影响评分
}

9.2 水位机制

通过 sysctl_compaction_proactiveness(默认值 20,范围 [0, 100])控制阈值:

// mm/compaction.c:2214
static unsigned int fragmentation_score_wmark(bool low)
{
    unsigned int wmark_low, leeway;

    wmark_low = 100U - sysctl_compaction_proactiveness;  // 默认:100-20=80
    leeway = min(10U, wmark_low / 2);                    // 默认:min(10, 40)=10
    return low ? wmark_low : min(wmark_low + leeway, 100U);
    // low水位:80,high水位:min(90, 100)=90
}
proactiveness = 20(默认):
    high watermark = 90  (评分 > 90 时启动主动规整)
    low  watermark = 80  (评分 < 80 时停止主动规整)

proactiveness = 50(积极):
    high watermark = 60  (评分 > 60 时启动)
    low  watermark = 50  (评分 < 50 时停止)

proactiveness = 0:
    禁用主动规整(sysctl_compaction_proactiveness = 0)

9.3 主动规整的触发判断

// mm/compaction.c:2223
static bool should_proactive_compact_node(pg_data_t *pgdat)
{
    int wmark_high;

    // kswapd 运行时不主动规整
    if (!sysctl_compaction_proactiveness || kswapd_is_running(pgdat))
        return false;

    wmark_high = fragmentation_score_wmark(false);  // 高水位
    return fragmentation_score_node(pgdat) > wmark_high;
}

9.4 手动触发主动规整

通过写入 /proc/sys/vm/compact_memory 或节点级接口:

// mm/compaction.c:47
/*
 * order == -1 is expected when compacting proactively via
 * 1. /proc/sys/vm/compact_memory
 * 2. /sys/devices/system/node/nodex/compact
 * 3. /proc/sys/vm/compaction_proactiveness
 */
static inline bool is_via_compact_memory(int order)
{
    return order == -1;
}

10. 直接规整(Direct Compaction)

10.1 触发路径

直接规整在高阶分配失败时触发,调用栈为:

__alloc_pages()                          [mm/page_alloc.c]
  -> __alloc_pages_slowpath()
       -> __alloc_pages_direct_compact()  [mm/page_alloc.c:4164]
            -> try_to_compact_pages()     [mm/compaction.c:2814]
                 -> compact_zone_order()  [mm/compaction.c:2749]
                      -> compact_zone()   [mm/compaction.c:2511]

10.2 try_to_compact_pages()

// mm/compaction.c:2814
enum compact_result try_to_compact_pages(gfp_t gfp_mask, unsigned int order,
        unsigned int alloc_flags, const struct alloc_context *ac,
        enum compact_priority prio, struct page **capture)
{
    // 对 zonelist 中的每个 zone 依次尝试规整
    for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, ...) {
        // 若该 zone 的规整已被 defer,跳过
        if (prio > MIN_COMPACT_PRIORITY && compaction_deferred(zone, order)) {
            rc = max_t(enum compact_result, COMPACT_DEFERRED, rc);
            continue;
        }

        status = compact_zone_order(zone, order, gfp_mask, prio, ...);
        rc = max(status, rc);

        if (status == COMPACT_SUCCESS) {
            compaction_defer_reset(zone, order, false);
            break;
        }
        // 完全扫描但失败 -> 延迟该 zone 的规整
        if (prio != COMPACT_PRIO_ASYNC && status == COMPACT_COMPLETE)
            defer_compaction(zone, order);
    }
}

10.3 规整优先级

// include/linux/compaction.h:9
enum compact_priority {
    COMPACT_PRIO_SYNC_FULL,        // 最高优先级(MIN_COMPACT_PRIORITY)
    COMPACT_PRIO_SYNC_LIGHT,       // 轻同步(MIN_COMPACT_COSTLY_PRIORITY / DEF_COMPACT_PRIORITY)
    COMPACT_PRIO_ASYNC,            // 异步(INIT_COMPACT_PRIORITY,首次尝试)
};

直接规整从 COMPACT_PRIO_ASYNC 开始,失败后逐步提升优先级(最多重试 MAX_COMPACT_RETRIES = 16 次,mm/page_alloc.c:4160)。

10.4 compact_zone_order() 参数设置

直接规整在 compact_zone_order() 中构造 compact_controlmm/compaction.c:2749):

// mm/compaction.c:2754
struct compact_control cc = {
    .order = order,
    .search_order = order,
    .gfp_mask = gfp_mask,
    .zone = zone,
    .mode = (prio == COMPACT_PRIO_ASYNC) ?
                MIGRATE_ASYNC : MIGRATE_SYNC_LIGHT,
    .alloc_flags = alloc_flags,
    .highest_zoneidx = highest_zoneidx,
    .direct_compaction = true,
    .whole_zone = (prio == MIN_COMPACT_PRIORITY),
    .ignore_skip_hint = (prio == MIN_COMPACT_PRIORITY),
    .ignore_block_suitable = (prio == MIN_COMPACT_PRIORITY)
};

注意:当优先级为 MIN_COMPACT_PRIORITY(即 COMPACT_PRIO_SYNC_FULL)时,会同时设置 whole_zoneignore_skip_hintignore_block_suitable 三个标志,确保最全面的扫描,这是"最后一搏"模式。


11. THP 与规整的协作

11.1 THP 分配触发规整

透明大页(THP)的分配阶数为 HPAGE_PMD_ORDER(通常为 9,即 512 页 = 2MB)。当 khugepaged 或缺页异常处理尝试分配 THP 时,若失败会触发规整:

// mm/compaction.c:75
#if defined CONFIG_TRANSPARENT_HUGEPAGE
#define COMPACTION_HPAGE_ORDER  HPAGE_PMD_ORDER

这个宏同时被用作主动规整的碎片评分基准阶数,意味着主动规整的目标就是维持 2MB 大页的可分配性

11.2 THP 分配流程

缺页异常 -> do_fault() -> do_anonymous_page()
    -> alloc_hugepage_direct_gfpmask()
        -> __alloc_pages(order=9)
            -> 如果失败:
                -> __alloc_pages_direct_compact()
                    -> 规整尝试
                -> 失败后降级为 order-0 分配

11.3 THP 与规整的 skip 机制交互

compact_zone()check_drain 阶段(mm/compaction.c:2686)有专门的 THP 处理:

// mm/compaction.c:2686
if (cc->order == COMPACTION_HPAGE_ORDER)
    last_migrated_pfn = 0;
// 当目标为 THP 阶数且迁移失败时,不记录上次迁移位置
// 因为即使 drain PCP 列表,若有页面迁移失败,pageblock 也不会变空

12. CMA:连续内存分配器

12.1 设计目标

CMA(Contiguous Memory Allocator)的核心思路是在系统启动时预留一块连续物理内存区域,平时让内核将其当作普通可迁移内存使用,当设备驱动需要时,将这块区域内的页面迁移出去,腾出连续空间

12.2 pageblock 迁移类型

CMA 区域的 pageblock 被标记为 MIGRATE_CMA

// include/linux/mmzone.h:70
#ifdef CONFIG_CMA
    MIGRATE_CMA,  // 只允许可迁移页分配,分配器不会自动改变其 migratetype

is_migrate_movable() 的定义确保 CMA 区域被视为可迁移的(include/linux/mmzone.h:110):

static inline bool is_migrate_movable(int mt)
{
    return is_migrate_cma(mt) || mt == MIGRATE_MOVABLE;
}

12.3 CMA 初始化

CMA 区域在启动时通过 memblock 预留,激活过程在 cma_activate_area()mm/cma.c:140):

// mm/cma.c:140
static void __init cma_activate_area(struct cma *cma)
{
    // 为每个 memrange 分配 bitmap(追踪哪些页已被 CMA 分配出去)
    for (allocrange = 0; allocrange < cma->nranges; allocrange++) {
        cmr = &cma->ranges[allocrange];
        cmr->bitmap = bitmap_zalloc(cma_bitmap_maxno(cma, cmr), GFP_KERNEL);
    }

    // 验证 CMA 区域不跨越 zone 边界
    if (!cma_validate_zones(cma)) goto cleanup;

    // 将 CMA 页面释放给 buddy system,并标记为 MIGRATE_CMA
    for (pfn = early_pfn[r]; pfn < cmr->base_pfn + cmr->count;
         pfn += pageblock_nr_pages)
        init_cma_reserved_pageblock(pfn_to_page(pfn));
    // ...
}

12.4 cma_alloc() 分配流程

// mm/cma.c:943
struct page *cma_alloc(struct cma *cma, unsigned long count,
                       unsigned int align, bool no_warn)
{
    struct page *page;
    page = cma_alloc_frozen(cma, count, align, no_warn);
    if (page)
        set_pages_refcounted(page, count);
    return page;
}

内部 cma_alloc_frozen() 的实现逻辑:

  1. 在 bitmap 中搜索连续的空闲位
  2. 调用 alloc_contig_range() 尝试隔离并迁移该区域内的页面
  3. alloc_contig_range() 内部调用规整相关的 isolate_freepages_range()isolate_migratepages_range()
cma_alloc()
  -> cma_alloc_frozen()
       -> __cma_alloc_frozen()
            -> alloc_contig_range()        [mm/page_alloc.c:7148]
                 -> start_isolate_page_range()  -- 将目标区域标记为 MIGRATE_ISOLATE
                 -> __alloc_contig_migrate_range() -- 将区域内的页面迁移出去
                 -> test_pages_isolated()        -- 验证隔离成功

12.5 CMA 数据结构

// mm/cma.c:37
struct cma cma_areas[MAX_CMA_AREAS];  // 全局 CMA 区域数组
unsigned int cma_area_count;          // 当前注册的 CMA 区域数量

每个 struct cma 包含:

  • ranges[]:物理内存范围(base_pfn + count)
  • bitmap:分配状态位图(1位 = order_per_bit 个页)
  • available_count:可用页数
  • lock:保护 bitmap 的自旋锁
  • alloc_mutex:序列化并发分配请求

13. 规整延迟机制

为了避免反复进行无效的规整操作,内核实现了延迟(defer)机制。

13.1 延迟计数器

zone 中维护三个计数器(include/linux/mmzone.h:1044):

unsigned int compact_considered;   // 已考虑规整的次数
unsigned int compact_defer_shift;  // 延迟级别(0..COMPACT_MAX_DEFER_SHIFT=6)
int compact_order_failed;          // 最小失败阶数

13.2 延迟策略

// mm/compaction.c:126
static void defer_compaction(struct zone *zone, int order)
{
    zone->compact_considered = 0;
    zone->compact_defer_shift++;  // 延迟级别++

    if (order < zone->compact_order_failed)
        zone->compact_order_failed = order;

    // 最大延迟 COMPACT_MAX_DEFER_SHIFT = 6,即最多跳过 64 次
    if (zone->compact_defer_shift > COMPACT_MAX_DEFER_SHIFT)
        zone->compact_defer_shift = COMPACT_MAX_DEFER_SHIFT;
}

// mm/compaction.c:141
static bool compaction_deferred(struct zone *zone, int order)
{
    unsigned long defer_limit = 1UL << zone->compact_defer_shift;

    if (order < zone->compact_order_failed)
        return false;  // 小阶数不延迟

    if (++zone->compact_considered >= defer_limit) {
        zone->compact_considered = defer_limit;
        return false;  // 延迟次数已满,允许重试
    }

    return true;  // 仍在延迟期,跳过本次规整
}

延迟级别与跳过次数的对应关系:

compact_defer_shift = 0  ->  跳过 0 次(立即重试)
compact_defer_shift = 1  ->  跳过 1 次
compact_defer_shift = 2  ->  跳过 3 次
compact_defer_shift = 3  ->  跳过 7 次
compact_defer_shift = 4  ->  跳过 15 次
compact_defer_shift = 5  ->  跳过 31 次
compact_defer_shift = 6  ->  跳过 63 次(最大延迟)

13.3 延迟重置

// mm/compaction.c:164
void compaction_defer_reset(struct zone *zone, int order, bool alloc_success)
{
    if (alloc_success) {
        zone->compact_considered = 0;
        zone->compact_defer_shift = 0;  // 分配成功:完全重置
    }
    if (order >= zone->compact_order_failed)
        zone->compact_order_failed = order + 1;
}

13.4 compaction_restarting()

当延迟到达最大级别后,允许强制重试,并清除所有 skip 标志:

// mm/compaction.c:178
static bool compaction_restarting(struct zone *zone, int order)
{
    if (order < zone->compact_order_failed)
        return false;

    return zone->compact_defer_shift == COMPACT_MAX_DEFER_SHIFT &&
        zone->compact_considered >= 1UL << zone->compact_defer_shift;
}

如果 compaction_restarting() 返回 true,compact_zone() 会在开始时调用 __reset_isolation_suitable(zone) 清除所有 pageblock 的 PB_compact_skip 标志,从头开始全量扫描(mm/compaction.c:2550):

// mm/compaction.c:2550
if (compaction_restarting(cc->zone, cc->order))
    __reset_isolation_suitable(cc->zone);

14. /proc 接口解读

14.1 /proc/buddyinfo

$ cat /proc/buddyinfo
Node 0, zone      DMA      1      0      0      0      0      0      0      0      0      1      3
Node 0, zone    DMA32   2837   1044    364     44     14      3      2      1      1      1     35
Node 0, zone   Normal  55441  12303   6312   3174    884    218     86     34     19      5     36

格式:Node <N>, zone <zone> <order-0> <order-1> ... <order-10>

每列数字代表该阶数的空闲块数量(注意不是页面数量)。

  • order-0 的值 55441 表示 Normal zone 有 55441 个 4KB 空闲页
  • order-9 的值 5 表示有 5 个 2MB 连续空闲区域(可分配 THP)
  • order-10 的值 36 表示有 36 个 4MB 连续空闲区域

碎片化判断:若高阶(order >= 9)的数量远小于低阶之和,说明碎片严重。

14.2 /proc/pagetypeinfo

$ cat /proc/pagetypeinfo
Page block order: 9
Pages per block:  512

Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10
Node    0, zone      DMA, type    Unmovable      1      0      0      0      0      0      0      0      0      0      0
Node    0, zone      DMA, type      Movable      0      0      0      0      0      0      0      0      0      1      3
Node    0, zone      DMA, type  Reclaimable      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone      DMA, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone      DMA, type          CMA      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone      DMA, type      Isolate      0      0      0      0      0      0      0      0      0      0      0
...

/proc/pagetypeinfobuddyinfo 基础上增加了迁移类型维度,可以判断碎片化的根本原因:

  • Unmovable 占据了本应是 Movable 的大阶页块 -> 说明发生了回退分配(fallback),内存已被"污染"
  • Isolate 类型有非零值 -> 说明有规整或 CMA 分配正在进行,部分内存被隔离

14.3 /proc/vmstat 中的规整统计

$ grep compact /proc/vmstat
compact_migrate_scanned  1234567   # 迁移扫描器扫描的总页数
compact_free_scanned     2345678   # 空闲扫描器扫描的总页数
compact_isolated         345678    # 总隔离页数
compact_stall            123       # 直接规整导致进程停顿的次数
compact_fail             45        # 规整完成但分配仍然失败的次数
compact_success          78        # 规整后分配成功的次数
compact_daemon_wake      890       # kcompactd 被唤醒的次数
compact_daemon_migrate_scanned 111111  # kcompactd 迁移扫描页数
compact_daemon_free_scanned    222222  # kcompactd 空闲扫描页数

15. 性能调优与 sysctl 参数

15.1 主要可调参数

参数 默认值 说明
/proc/sys/vm/compact_memory - 写入任意值触发全量规整(order=-1)
/proc/sys/vm/compaction_proactiveness 20 主动规整积极程度 [0, 100],0=禁用
/proc/sys/vm/extfrag_threshold 500 碎片化指数阈值,高于此值才触发规整(mm/compaction.c:1891)
/proc/sys/vm/compact_unevictable_allowed 1 是否允许规整不可回收页(unevictable)
/sys/devices/system/node/nodeN/compact - 对特定 NUMA 节点触发规整

15.2 compact_gap 与水位

规整需要一定量的空闲页才能进行(作为迁移目标的"缓冲")。compact_gap() 计算最小需求量(include/linux/compaction.h:65):

static inline unsigned long compact_gap(unsigned int order)
{
    return 2UL << order;  // 2 倍于目标阶数的页面数
}

compaction_suitable() 检查水位时会加上这个额外需求:

// mm/compaction.c:2377
watermark += compact_gap(order);
if (order > PAGE_ALLOC_COSTLY_ORDER)
    watermark += low_wmark_pages(zone) - min_wmark_pages(zone);

15.3 调优建议

高 THP 需求场景(数据库、大内存计算):

# 提高主动规整积极性
echo 50 > /proc/sys/vm/compaction_proactiveness
# 降低碎片阈值(更容易触发规整)
echo 200 > /proc/sys/vm/extfrag_threshold

低延迟场景(实时系统):

# 禁用主动规整(避免后台开销)
echo 0 > /proc/sys/vm/compaction_proactiveness
# 禁用 THP 避免触发直接规整
echo never > /sys/kernel/mm/transparent_hugepage/enabled

监控规整效果

# 实时监控规整成功率
watch -n 1 'grep compact /proc/vmstat | grep -E "stall|success|fail"'
# 查看各 zone 碎片化情况
cat /sys/kernel/debug/extfrag/extfrag_index

16. 整体架构图

                     内存分配请求(高阶)
                           |
                    __alloc_pages()
                           |
                    水位检查失败?
                     /         \
                   是             否
                   |              |
         __alloc_pages_slowpath   分配成功
                   |
        +----------+-----------+
        |          |           |
    直接回收    直接规整     wake kswapd
   (reclaim)  (compact)        |
        |          |           v
        |          |       kswapd 运行
        |          |       内存回收完成
        |          |           |
        |          |      wakeup_kcompactd
        |          |           |
        |          v           v
        |    try_to_compact_pages()
        |          |
        |    compact_zone_order()
        |          |
        |    compact_zone()
        |    +----- 主循环 -----+
        |    |                 |
        |    | isolate_        | isolate_
        |    | migratepages()  | freepages_block()
        |    | (低->高)        | (高->低)
        |    |                 |
        |    | migrate_pages() | (move pages)
        |    |                 |
        |    +--- 两扫描器相遇? ---+
        |          |
        |    COMPACT_SUCCESS?
        |      /       \
        |    是          否
        |    |           |
        | 分配成功    defer_compaction()
        |                |
        |           kcompactd 定期重试
        |
        v
      OOM killer
      (最后手段)


kcompactd 主动规整循环(每 500ms):

kcompactd
    |
    +--- wait_event_freezable_timeout(kcompactd_wait, ..., 500ms)
    |           |
    |     被唤醒(响应式)    超时(主动式)
    |           |                  |
    |   kcompactd_do_work()  should_proactive_compact_node()?
    |   (MIGRATE_SYNC_LIGHT)        |
    |                         fragmentation_score_node()
    |                         > fragmentation_score_wmark(high)?
    |                               |
    |                         compact_node(proactive=true)
    |                               |
    |                         score 改善? -> timeout = 500ms
    |                         否         -> timeout = 500ms * 64(退避)
    |
    +--- loop


CMA 分配流程:

cma_alloc()
    |
    bitmap 搜索连续空位
    |
    alloc_contig_range()
    |
    +-- start_isolate_page_range()  -- 标记为 MIGRATE_ISOLATE
    |
    +-- __alloc_contig_migrate_range()
    |       |
    |       isolate_migratepages_range() -- 迁移扫描器(严格模式)
    |       migrate_pages()              -- 将页面迁移出 CMA 区域
    |
    +-- test_pages_isolated()  -- 验证区域已清空
    |
    分配成功,返回连续物理页面

17. 碎片化量化:fragmentation_index 深度分析

17.1 contig_page_info 数据结构

碎片化计算的基础数据结构定义在 mm/vmstat.c:1049

// mm/vmstat.c:1049
struct contig_page_info {
    unsigned long free_pages;           // 总空闲页面数(所有阶数)
    unsigned long free_blocks_total;    // 总空闲块数量(所有阶数的 nr_free 之和)
    unsigned long free_blocks_suitable; // 满足目标阶数的空闲块数量
};

fill_contig_page_info() 函数(mm/vmstat.c:1063)遍历伙伴系统的每个阶数,统计这三个字段:

// mm/vmstat.c:1063
static void fill_contig_page_info(struct zone *zone,
                unsigned int suitable_order,
                struct contig_page_info *info)
{
    unsigned int order;

    info->free_pages = 0;
    info->free_blocks_total = 0;
    info->free_blocks_suitable = 0;

    for (order = 0; order < NR_PAGE_ORDERS; order++) {
        unsigned long blocks;

        // 无锁读取 nr_free(仅用于诊断,允许数据竞争)
        blocks = data_race(zone->free_area[order].nr_free);
        info->free_blocks_total += blocks;

        // 统计空闲页面总数(不是块数)
        info->free_pages += blocks << order;

        // 统计满足目标阶数的块数(>=suitable_order 的块,换算成目标阶数单位)
        if (order >= suitable_order)
            info->free_blocks_suitable += blocks <<
                                (order - suitable_order);
    }
}

17.2 fragmentation_index 的数学含义

__fragmentation_index() 函数(mm/vmstat.c:1102)计算结果在 [-1000, 1000] 范围内:

// mm/vmstat.c:1102
static int __fragmentation_index(unsigned int order, struct contig_page_info *info)
{
    unsigned long requested = 1UL << order;

    if (!info->free_blocks_total)
        return 0;

    // 如果有满足条件的空闲块,返回 -1000(表示分配应该成功)
    if (info->free_blocks_suitable)
        return -1000;

    /*
     * 公式推导:
     * 理想情况(无碎片)下,free_blocks_total == free_pages / requested
     * 实际情况下,free_blocks_total > free_pages / requested(因为有小块碎片)
     *
     * index = 1 - (free_pages / requested) / free_blocks_total
     *       = 1000 - (free_pages * 1000 / requested) / free_blocks_total
     *
     * index 趋近于 0:说明块的平均大小接近 requested,失败是因为内存不足
     * index 趋近于 1000:说明块平均太小,有很多碎片但总内存充足
     */
    return 1000 - div_u64((1000 + (div_u64(info->free_pages * 1000ULL,
                         requested))), info->free_blocks_total);
}

17.3 fragmentation_index 与 extfrag_threshold 的决策

compaction_suitable()mm/compaction.c:2387 中使用 fragmentation_index 来决定是否值得规整:

// mm/compaction.c:2411
if (suitable) {
    compact_result = COMPACT_CONTINUE;
    if (order > PAGE_ALLOC_COSTLY_ORDER) {
        int fragindex = fragmentation_index(zone, order);

        if (fragindex >= 0 &&
            fragindex <= sysctl_extfrag_threshold) {
            // fragindex 低 -> 失败原因是内存不足,规整无效
            suitable = false;
            compact_result = COMPACT_NOT_SUITABLE_ZONE;
        }
    }
}

决策逻辑如下:

fragindex = -1000  ->  有足够的大块,分配应该成功,无需规整
fragindex = 0      ->  失败是因为内存真的不足,规整也无法解决
fragindex = 500    ->  混合情况(extfrag_threshold 默认值)
fragindex = 1000   ->  纯碎片导致,规整效果最佳

当 fragindex <= extfrag_threshold(默认 500)时:
  -> 认为主要是内存不足,跳过规整(COMPACT_NOT_SUITABLE_ZONE)
  -> 建议进行内存回收(reclaim)

当 fragindex > extfrag_threshold 时:
  -> 认为主要是碎片化导致,触发规整

17.4 debugfs 中查看 fragmentation_index

$ cat /sys/kernel/debug/extfrag/extfrag_index
Node 0, zone      DMA  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000 -1.000 -1.000
Node 0, zone    DMA32  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.820  0.912
Node 0, zone   Normal  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.783  0.912  0.945

列格式:order-0 order-1 ... order-10,值越接近 1 说明该 order 的碎片越严重。


18. 可移动页面判定机制

18.1 页面可移动性的三个维度

内核在规整过程中通过多个条件判断一个页面是否可迁移:

                    页面可移动性判断树
                           |
          +----------------+----------------+
          |                                 |
     PageBuddy?                        已分配页面
     (空闲页,跳过)                          |
                              +-------------+-------------+
                              |             |             |
                         PageHuge?    PageCompound?   普通页面
                         (大页处理)   (复合页处理)         |
                                                   +------+------+
                                                   |             |
                                               PageLRU?  page_has_movable_ops?
                                               (LRU 迁移)  (驱动迁移)

18.2 PageLRU 页面的隔离

LRU 页面通过 folio_isolate_lru() 从 LRU 链表中隔离(mm/compaction.c:1074):

// mm/compaction.c:1074
folio = folio_get_nontail_page(page);
if (unlikely(!folio))
    goto isolate_fail;

// 匿名页:检查引用计数,跳过被 pin 住的页面
mapping = folio_mapping(folio);
if (!mapping && (folio_ref_count(folio) - 1) > folio_mapcount(folio))
    goto isolate_fail_put;

实际隔离操作在持有 lruvec 锁的情况下进行(mm/compaction.c:1086):

// mm/compaction.c:1086(简化)
if (!folio_test_lru(folio))
    goto isolate_fail_put;

lruvec = folio_lruvec(folio);
if (lruvec != locked) {
    if (locked) unlock_page_lruvec_irqrestore(locked, flags);
    locked = lock_page_lruvec_irqsave(lruvec, &flags);
}

// 检查页面状态
is_unevictable = folio_test_unevictable(folio);
if (!sysctl_compact_unevictable_allowed && is_unevictable)
    goto isolate_fail_put;

// 从 LRU 链表取出
if (!folio_isolate_lru(folio))
    goto isolate_fail_put;

18.3 page_has_movable_ops 页面

page_has_movable_ops() 返回 true 的页面类型(include/linux/page-flags.h:1147):

// include/linux/page-flags.h:1147
static inline bool page_has_movable_ops(const struct page *page)
{
    return PageMovableOps(page) &&
           (PageOffline(page) || PageZsmalloc(page));
}

当前内核支持两类 movable_ops 页面(mm/migrate.c:54):

  • PageOffline:balloon driver 使用,代表从 guest OS 中"膨胀"出来的页面
  • PageZsmalloc:zsmalloc(用于 zram 等)分配的页面

这些页面通过 struct movable_operations 接口迁移(include/linux/migrate.h:44):

// include/linux/migrate.h:44
struct movable_operations {
    // 准备隔离,返回 true 表示可迁移
    bool (*isolate_page)(struct page *, isolate_mode_t);
    // 将内容从 src 拷贝到 dst
    int  (*migrate_page)(struct page *dst, struct page *src,
                         enum migrate_mode);
    // 如果迁移失败,将页面放回原处
    void (*putback_page)(struct page *);
};

18.4 复合页(Compound Page)的特殊处理

对于 THP 等复合页,规整有专门的跳过逻辑(mm/compaction.c:1033):

// mm/compaction.c:1033
if (PageCompound(page) && !cc->alloc_contig) {
    const unsigned int order = compound_order(page);

    // Skip based on page order and compaction target order
    if (skip_isolation_on_order(order, cc->order)) {
        if (order <= MAX_PAGE_ORDER) {
            low_pfn += (1UL << order) - 1;
            nr_scanned += (1UL << order) - 1;
        }
        goto isolate_fail;
    }
}

skip_isolation_on_order() 的逻辑:若复合页的阶数大于等于目标分配阶数,则跳过(因为迁移它比目标分配还要大,没有必要)。


19. isolate_migratepages_block 逐页隔离详解

19.1 函数签名与初始化

// mm/compaction.c:836
static int
isolate_migratepages_block(struct compact_control *cc, unsigned long low_pfn,
                           unsigned long end_pfn, isolate_mode_t mode)
{
    pg_data_t *pgdat = cc->zone->zone_pgdat;
    unsigned long nr_scanned = 0, nr_isolated = 0;
    struct lruvec *lruvec;
    unsigned long flags = 0;
    struct lruvec *locked = NULL;
    struct folio *folio = NULL;
    struct page *page = NULL, *valid_page = NULL;
    bool skip_on_failure = false;
    unsigned long next_skip_pfn = 0;

19.2 too_many_isolated 检查

在开始隔离前,函数会检查系统是否已经有太多被隔离的页面(mm/compaction.c:861):

// mm/compaction.c:861
while (unlikely(too_many_isolated(cc))) {
    /* stop isolation if there are still pages not migrated */
    if (cc->nr_migratepages)
        return -EAGAIN;

    /* async migration should just abort */
    if (cc->mode == MIGRATE_ASYNC)
        return -EAGAIN;

    reclaim_throttle(pgdat, VMSCAN_THROTTLE_ISOLATED);

    if (fatal_signal_pending(current))
        return -EINTR;
}

too_many_isolated() 通过比较 NR_ISOLATED_ANON + NR_ISOLATED_FILE 与可用页面数量来判断。这防止了规整占用过多内存导致系统陷入死锁。

19.3 skip_on_failure 优化

异步直接规整模式下,存在 skip_on_failure 机制(mm/compaction.c:878):

// mm/compaction.c:878
if (cc->direct_compaction && (cc->mode == MIGRATE_ASYNC)) {
    skip_on_failure = true;
    next_skip_pfn = block_end_pfn(low_pfn, cc->order);
}

这意味着:在异步模式下,如果在某个 order 对齐的子块内没有成功隔离任何页面,就直接跳到下一个子块边界。这避免了在显然无法规整的区域浪费时间。

19.4 逐页扫描主循环关键决策点

for (; low_pfn < end_pfn; low_pfn++) {
    |
    +-- 每 COMPACT_CLUSTER_MAX(32) 页:释放 lruvec 锁,检查致命信号
    |
    +-- 检查 PB_compact_skip(仅在页块首页)
    |       置位 -> 跳过整个页块
    |
    +-- PageHuge?
    |       非 alloc_contig 模式 -> 跳过(+= 1<<order - 1)
    |       alloc_contig 模式 -> isolate_or_dissolve_huge_folio()
    |
    +-- PageBuddy?(空闲页)
    |       跳过,low_pfn += 1<<buddy_order - 1
    |
    +-- PageCompound?(复合页,非 alloc_contig)
    |       skip_isolation_on_order() -> 跳过
    |
    +-- !PageLRU && !page_has_movable_ops?
    |       -> isolate_fail(非 LRU 且非 movable_ops)
    |
    +-- PageLRU?
    |       1. folio_get_nontail_page()(增加引用)
    |       2. 检查匿名页是否被 pin(ref > mapcount + 1)
    |       3. 持 lruvec 锁
    |       4. 检查 unevictable(sysctl_compact_unevictable_allowed)
    |       5. folio_isolate_lru() -> 成功则加入 cc->migratepages
    |
    +-- page_has_movable_ops?
    |       isolate_movable_ops_page() -> 成功则加入 cc->migratepages
    |
    +-- nr_isolated >= COMPACT_CLUSTER_MAX -> break(本批已满)
}

19.5 隔离完成后的 skip 标志处理

如果扫描完整个页块但未隔离到任何页面(nr_isolated == 0),则设置 PB_compact_skipmm/compaction.c:1200):

// mm/compaction.c 相关逻辑(简化)
if (nr_isolated == 0 && valid_page && !skip_updated) {
    if (!cc->no_set_skip_hint)
        set_pageblock_skip(valid_page);
}

反之,如果成功隔离了页面,则清除 skip 标志,确保下次规整还会扫描该页块。


20. migrate_pages() 迁移引擎实现细节

20.1 函数签名与批处理

migrate_pages() 是页面迁移的统一入口(mm/migrate.c:2072):

// mm/migrate.c:2072
int migrate_pages(struct list_head *from, new_folio_t get_new_folio,
        free_folio_t put_new_folio, unsigned long private,
        enum migrate_mode mode, int reason, unsigned int *ret_succeeded)

关键常量(mm/migrate.c:1583):

// mm/migrate.c:1583
#define NR_MAX_BATCHED_MIGRATION    HPAGE_PMD_NR    // 批处理最大页数(= 512)
#define NR_MAX_MIGRATE_PAGES_RETRY  10              // 最大重试次数
#define NR_MAX_MIGRATE_ASYNC_RETRY  3               // 异步最大重试
#define NR_MAX_MIGRATE_SYNC_RETRY   \
    (NR_MAX_MIGRATE_PAGES_RETRY - NR_MAX_MIGRATE_ASYNC_RETRY) // 同步最大重试 = 7

20.2 分步迁移流程

migrate_pages() 将大列表分批(每批最多 NR_MAX_BATCHED_MIGRATION = 512 页)处理(mm/migrate.c:2093):

// mm/migrate.c:2093
again:
    nr_pages = 0;
    list_for_each_entry_safe(folio, folio2, from, lru) {
        if (folio_test_hugetlb(folio)) {
            list_move_tail(&folio->lru, &ret_folios);
            continue;
        }
        nr_pages += folio_nr_pages(folio);
        if (nr_pages >= NR_MAX_BATCHED_MIGRATION)
            break;
    }
    if (nr_pages >= NR_MAX_BATCHED_MIGRATION)
        list_cut_before(&folios, from, &folio2->lru);
    else
        list_splice_init(from, &folios);

    if (mode == MIGRATE_ASYNC)
        rc = migrate_pages_batch(&folios, ...);
    else
        rc = migrate_pages_sync(&folios, ...);

20.3 HugeTLB 页面的特殊处理

大页(HugeTLB)在 migrate_hugetlbs() 中单独处理(mm/migrate.c:1610),在批处理 folios 之前先过滤出 hugetlb 页面,原因是大页的迁移逻辑与普通页完全不同:

// mm/migrate.c:2088
rc_gather = migrate_hugetlbs(from, get_new_folio, put_new_folio, private,
                             mode, reason, &stats, &ret_folios);

HugeTLB 迁移的重试策略:最多重试 NR_MAX_MIGRATE_PAGES_RETRY = 10 次(mm/migrate.c:1623),每次失败后通过 pass > 2 的条件决定是否强制迁移(pass 超过 2 次后会更积极地迁移)。

20.4 migrate_folio_unmap() 的两阶段迁移

普通 folio 的迁移分为两个阶段(mm/migrate.c:1203):

阶段 1:unmap(解除映射)

// mm/migrate.c:1203
static int migrate_folio_unmap(new_folio_t get_new_folio,
        free_folio_t put_new_folio, unsigned long private,
        struct folio *src, struct folio **dstp, enum migrate_mode mode,
        struct list_head *ret)
{
    dst = get_new_folio(src, private);  // 分配目标页(compaction_alloc)

    if (!folio_trylock(src)) {
        // MIGRATE_ASYNC: 跳过(trylock 失败)
        // MIGRATE_SYNC_LIGHT: 只等待 uptodate 的页面
        // MIGRATE_SYNC: 无条件等待锁
    }

    // 检查 writeback 状态
    if (folio_test_writeback(src)) {
        // MIGRATE_SYNC: 等待回写完成(folio_wait_writeback)
        // 其他模式: -EBUSY
    }

    // 对匿名页获取 anon_vma
    if (folio_test_anon(src) && !folio_test_ksm(src))
        anon_vma = folio_get_anon_vma(src);

    // try_to_unmap() 解除所有进程的页表映射,安装迁移 PTE
    try_to_unmap(src, TTU_MIGRATION | TTU_IGNORE_MLOCK);
}

阶段 2:move(物理拷贝 + 元数据更新)

// mm/migrate.c:855
static int __migrate_folio(struct address_space *mapping, struct folio *dst,
                           struct folio *src, void *src_private,
                           enum migrate_mode mode)
{
    rc = folio_mc_copy(dst, src);     // 使用 MOVNTQ 等高速内存拷贝指令
    rc = __folio_migrate_mapping(mapping, dst, src, expected_count);
    folio_migrate_flags(dst, src);    // 拷贝 LRU/dirty/writeback 等标志
}

阶段 3:更新页表(remove_migration_pte)

迁移完成后,通过 remove_migration_pte() 遍历所有进程的页表,将迁移 PTE 替换为指向新物理页的正常 PTE(mm/migrate.c:346):

// mm/migrate.c:346
static bool remove_migration_pte(struct folio *folio,
        struct vm_area_struct *vma, unsigned long addr, void *arg)
{
    // 找到迁移 PTE
    DEFINE_FOLIO_VMA_WALK(pvmw, rmap_walk_arg->folio, vma, addr,
                          PVMW_SYNC | PVMW_MIGRATION);

    while (page_vma_mapped_walk(&pvmw)) {
        // 从旧 PTE 中提取访问位、脏位等属性
        // 构建指向 dst 物理页的新 PTE
        pte = mk_pte(new, READ_ONCE(vma->vm_page_prot));
        // 恢复 young/dirty/write 等标志
        set_pte_at(vma->vm_mm, pvmw.address, pvmw.pte, pte);
    }
}

20.5 migrate_pages_stats 统计结构

// mm/migrate.c:1592
struct migrate_pages_stats {
    int nr_succeeded;       // 成功迁移的基础页数
    int nr_failed_pages;    // 失败的基础页数
    int nr_thp_succeeded;   // 成功迁移的 THP 数
    int nr_thp_failed;      // 失败的 THP 数
    int nr_thp_split;       // 迁移前被拆分的 THP 数
    int nr_split;           // 迁移前被拆分的大 folio 数
};

这些统计最终通过 count_vm_events() 更新到 /proc/vmstat 中的 PGMIGRATE_SUCCESSPGMIGRATE_FAILTHP_MIGRATION_* 等计数器。


21. fast_find_migrateblock 快速搜索优化

21.1 设计动机

在 order > PAGE_ALLOC_COSTLY_ORDER(即 order > 3)的高阶分配场景下,逐 pageblock 线性扫描效率太低。fast_find_migrateblock() 通过直接查看 buddy freelist 来找到一个"有空洞"的 MOVABLE 页块,作为迁移扫描器的起始位置(mm/compaction.c:1923):

// mm/compaction.c:1923
static unsigned long fast_find_migrateblock(struct compact_control *cc)
{
    unsigned int limit = freelist_scan_limit(cc);  // 最多扫描 limit 个 freelist 条目
    unsigned long distance;
    unsigned long pfn = cc->migrate_pfn;
    unsigned long high_pfn;
    int order;
    bool found_block = false;

21.2 适用条件过滤

以下情况直接返回线性扫描位置,不做快速搜索(mm/compaction.c:1934):

// ignore_skip_hint 时不使用(fast search 依赖 skip hint 避免重复)
if (cc->ignore_skip_hint)
    return pfn;

// 正在完成当前页块时不切换
if (cc->finish_pageblock)
    return pfn;

// pfn 不在页块开始处时,说明是上次扫描的延续
if (pfn != cc->zone->zone_start_pfn && pfn != pageblock_start_pfn(pfn))
    return pfn;

// order <= PAGE_ALLOC_COSTLY_ORDER(即 <= 3)时不做快速搜索
if (cc->order <= PAGE_ALLOC_COSTLY_ORDER)
    return pfn;

// 直接规整且目标不是 MOVABLE 时不做快速搜索
if (cc->direct_compaction && cc->migratetype != MIGRATE_MOVABLE)
    return pfn;

21.3 搜索范围计算

搜索范围被限定在前半部分(mm/compaction.c:1975):

// mm/compaction.c:1975
distance = (cc->free_pfn - cc->migrate_pfn) >> 1;
if (cc->migrate_pfn != cc->zone->zone_start_pfn)
    distance >>= 2;  // 非首次:只搜索前 1/8
high_pfn = pageblock_start_pfn(cc->migrate_pfn + distance);

这个设计的原因:如果迁移扫描器已经前进了一段距离,说明低地址部分的碎片比较严重,快速搜索应该缩小范围,避免在已知碎片严重区域浪费时间。

21.4 搜索算法

从高阶到低阶遍历 MOVABLE freelist(mm/compaction.c:1980):

// mm/compaction.c:1980
for (order = cc->order - 1;
     order >= PAGE_ALLOC_COSTLY_ORDER && !found_block && nr_scanned < limit;
     order--) {
    struct free_area *area = &cc->zone->free_area[order];
    freelist = &area->free_list[MIGRATE_MOVABLE];

    list_for_each_entry(freepage, freelist, buddy_list) {
        if (nr_scanned++ >= limit) {
            move_freelist_tail(freelist, freepage);  // 下次从这里继续
            break;
        }

        free_pfn = page_to_pfn(freepage);
        if (free_pfn < high_pfn) {
            if (get_pageblock_skip(freepage))
                continue;  // 跳过最近扫描过的页块

            move_freelist_tail(freelist, freepage);  // 将已检查条目移到尾部
            pfn = pageblock_start_pfn(free_pfn);
            cc->fast_search_fail = 0;
            found_block = true;
            break;
        }
    }
}

21.5 freelist_scan_limit 的自适应控制

freelist_scan_limit() 根据 fast_search_fail 动态调整每次扫描的限制(mm/compaction.c:1406):

// mm/compaction.c:1406
static inline unsigned int
freelist_scan_limit(struct compact_control *cc)
{
    unsigned short shift = BITS_PER_LONG - 1;
    return (COMPACT_CLUSTER_MAX >> min(shift, cc->fast_search_fail)) + 1;
}

fast_search_fail 越大,limit 越小(右移操作),搜索越快但越粗糙。这是一种自适应退避机制:如果快速搜索频繁失败,就减少搜索开销,更快地退回线性扫描。


22. capture_control:零拷贝页面捕获机制

22.1 设计动机

在直接规整过程中,当规整线程释放一个迁移完成的源页面时,这个页面可能正好满足正在等待的高阶分配请求。传统做法是将页面放回 freelist,再由分配路径取出。capture_control 机制允许在页面释放时直接"捕获"它,完全跳过 freelist,减少锁竞争和延迟。

22.2 数据结构

// mm/internal.h(相关结构)
struct capture_control {
    struct compact_control *cc;  // 关联的规整控制结构
    struct page *page;           // 捕获到的页面指针(初始为 NULL)
};

每个进程的 task_struct 中有一个 capture_control 指针:

// include/linux/sched.h(相关字段)
struct task_struct {
    // ...
    struct capture_control __rcu *capture_control;
};

22.3 注册与注销

compact_zone_order() 中注册(mm/compaction.c:2769):

// mm/compaction.c:2769
struct capture_control capc = {
    .cc = &cc,
    .page = NULL,
};

barrier();  // 确保结构完全初始化后再暴露
WRITE_ONCE(current->capture_control, &capc);

ret = compact_zone(&cc, &capc);

WRITE_ONCE(current->capture_control, NULL);
*capture = READ_ONCE(capc.page);
if (*capture)
    ret = COMPACT_SUCCESS;

22.4 捕获时机

free_pages_prepare() 释放页面时,如果发现当前进程有活跃的 capture_control,且页面满足分配条件,则直接将页面存入 capc->page,而不是放回 buddy 系统。

这样直接规整结束后,通过检查 capc->page 是否非空来确认是否成功捕获到目标页面,即使 compact_zone() 返回的不是 COMPACT_SUCCESS,只要捕获到页面就认为成功。

22.5 在 compact_zone 中检查捕获状态

在每次迁移循环后(mm/compaction.c:2693):

// mm/compaction.c:2693
/* Stop if a page has been captured */
if (capc && capc->page) {
    ret = COMPACT_SUCCESS;
    break;
}

这确保了一旦捕获到页面就立即退出规整循环,不浪费更多资源。


23. compaction_suitable 与水位检查

23.1 双层检查

compaction_suitable() 包含两层检查(mm/compaction.c:2387):

第一层:水位检查(__compaction_suitable)

// mm/compaction.c:2357
static bool __compaction_suitable(struct zone *zone, int order,
                                  unsigned long watermark, int highest_zoneidx,
                                  unsigned long free_pages)
{
    watermark += compact_gap(order);  // 需要额外的空闲页作为缓冲
    if (order > PAGE_ALLOC_COSTLY_ORDER)
        watermark += low_wmark_pages(zone) - min_wmark_pages(zone);
    return __zone_watermark_ok(zone, 0, watermark, highest_zoneidx,
                               ALLOC_CMA, free_pages);
}

水位要求:当前可用空闲页 >= watermark + compact_gap(order)

对于高代价阶数(order > 3),额外增加 low - min 水位差,提高门槛。

第二层:碎片指数检查

只有当目标 order > PAGE_ALLOC_COSTLY_ORDER 时才进行碎片指数检查:

if (order > PAGE_ALLOC_COSTLY_ORDER) {
    int fragindex = fragmentation_index(zone, order);
    if (fragindex >= 0 && fragindex <= sysctl_extfrag_threshold) {
        suitable = false;  // 碎片指数低,不值得规整
    }
}

23.2 compaction_zonelist_suitable

为了避免在所有 zone 都不适合规整时反复触发规整尝试,compaction_zonelist_suitable() 会检查整个 zonelist(mm/compaction.c:2432):

// mm/compaction.c:2432
bool compaction_zonelist_suitable(struct alloc_context *ac, int order,
        int alloc_flags)
{
    for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, ...) {
        // 考虑可回收页面(除以 order 是粗略估算)
        available = zone_reclaimable_pages(zone) / order;
        available += zone_page_state_snapshot(zone, NR_FREE_PAGES);
        if (__compaction_suitable(zone, order, min_wmark_pages(zone),
                                  ac->highest_zoneidx, available))
            return true;
    }
    return false;
}

这在 __alloc_pages_slowpath() 中被调用,用于决定是否继续 reclaim+compact 循环。


24. skip 机制的完整生命周期

24.1 skip 标志的设置时机

PB_compact_skip 会在以下情况被设置:

  1. 迁移扫描器扫描页块无收获时mm/compaction.c):扫描完整个页块,nr_isolated == 0,且 !cc->no_set_skip_hint
  2. 页块迁移类型不合适时suitable_migration_source 返回 false):update_cached_migrate() 内部会设置 skip
  3. 空闲扫描器跳过无空闲页的页块时:扫描完整个页块,total_isolated == 0

24.2 skip 标志的清除时机

  1. 成功隔离页面时clear_pageblock_skip()isolate_migratepages_block() 中成功隔离页面后调用
  2. __reset_isolation_suitable() 时:规整重启时(compaction_restarting() 返回 true),重新扫描所有页块,对有 PageLRU 或 PageBuddy 的页块清除 skip 标志
  3. 页面被释放归还 buddy 时free_one_page() 会清除 pageblock 的 skip 标志,因为有新的空闲页产生

24.3 __reset_isolation_suitable 的扫描逻辑

__reset_isolation_suitable() 采用双指针从两端向中间扫描(mm/compaction.c:349):

// mm/compaction.c:349
static void __reset_isolation_suitable(struct zone *zone)
{
    unsigned long migrate_pfn = zone->zone_start_pfn;
    unsigned long free_pfn = zone_end_pfn(zone) - 1;

    for (; migrate_pfn < free_pfn; migrate_pfn += pageblock_nr_pages,
                                    free_pfn -= pageblock_nr_pages) {
        cond_resched();

        // 从低端找第一个有 PageLRU 的页块,更新迁移扫描器缓存位置
        if (__reset_isolation_pfn(zone, migrate_pfn, true, source_set) &&
            migrate_pfn < reset_migrate) {
            source_set = true;
            reset_migrate = migrate_pfn;
            zone->compact_init_migrate_pfn = reset_migrate;
            zone->compact_cached_migrate_pfn[0] = reset_migrate;
            zone->compact_cached_migrate_pfn[1] = reset_migrate;
        }

        // 从高端找第一个有 PageBuddy 的页块,更新空闲扫描器缓存位置
        if (__reset_isolation_pfn(zone, free_pfn, free_set, true) &&
            free_pfn > reset_free) {
            free_set = true;
            reset_free = free_pfn;
            zone->compact_init_free_pfn = reset_free;
            zone->compact_cached_free_pfn = reset_free;
        }
    }
}

这个函数同时更新了两个扫描器的初始位置缓存,让下次规整从最有希望的位置开始,而不是从 zone 的边界重新开始。


25. Huge Page 的 compaction 支持

25.1 HugeTLB 页面在规整中的处理

普通规整(非 CMA/alloc_contig 模式)下,isolate_migratepages_block() 会跳过 HugeTLB 页面(mm/compaction.c:950):

// mm/compaction.c:950
if (PageHuge(page)) {
    const unsigned int order = compound_order(page);
    if (!cc->alloc_contig) {
        // 跳过,但要正确前进 low_pfn
        if (order <= MAX_PAGE_ORDER) {
            low_pfn += (1UL << order) - 1;
            nr_scanned += (1UL << order) - 1;
        }
        goto isolate_fail;
    }
    // alloc_contig 模式下,尝试溶解大页
    folio = page_folio(page);
    ret = isolate_or_dissolve_huge_folio(folio, &cc->migratepages);

25.2 isolate_or_dissolve_huge_folio

isolate_or_dissolve_huge_folio() 有两种处理路径:

  • 隔离路径:如果大页处于正常使用状态,直接隔离(类似 LRU 隔离)
  • 溶解路径:如果大页可以被溶解(dissolved),则调用 dissolve_free_huge_page() 将其分解为普通 buddy 页面

溶解后的页面会以 PageBuddy 状态落入 cc->migratepages,后续会被检测到并当作空闲页处理,不再作为迁移源,而是作为迁移目标。

25.3 alloc_contig_range 与大页

CMA 分配经常需要处理已分配的 HugeTLB 页面。alloc_contig_range() 通过以下流程处理(mm/page_alloc.c:7148):

alloc_contig_range(start, end, migratetype)
    |
    +-- start_isolate_page_range()
    |       将 [start, end) 内所有 pageblock 标记为 MIGRATE_ISOLATE
    |       等待所有 per-cpu 页面被归还
    |
    +-- __alloc_contig_migrate_range()
    |       逐页扫描,对 HugeTLB 页面调用 isolate_or_dissolve_huge_folio()
    |       对普通页调用 isolate_migratepages_range()
    |       最后 migrate_pages() 执行迁移
    |
    +-- test_pages_isolated()
    |       验证区域内所有页面都已隔离(PageBuddy + MIGRATE_ISOLATE)
    |
    +-- undo_isolate_page_range() / 成功返回

25.4 COMPACTION_HPAGE_ORDER 的核心作用

// mm/compaction.c:75
#if defined CONFIG_TRANSPARENT_HUGEPAGE
#define COMPACTION_HPAGE_ORDER  HPAGE_PMD_ORDER
#elif defined CONFIG_HUGETLBFS
#define COMPACTION_HPAGE_ORDER  HUGETLB_PAGE_ORDER
#else
#define COMPACTION_HPAGE_ORDER  (PMD_SHIFT - PAGE_SHIFT)
#endif

该宏在三个关键场景中被使用:

  1. 主动规整触发阈值fragmentation_score_zone() 用它作为 extfrag_for_order() 的参数
  2. skip 机制特殊处理cc->order == COMPACTION_HPAGE_ORDER 时不记录 last_migrated_pfn
  3. 内存分配快速路径:在 __alloc_pages_slowpath() 中作为触发规整的阶数基准

26. NUMA 感知的 compaction

26.1 每个 NUMA 节点独立的 kcompactd

每个 NUMA 节点(pg_data_t)有自己的 kcompactd 线程(mm/compaction.c:3237),保存在 pgdat->kcompactd 中:

系统 NUMA 拓扑示例(双 socket):

Node 0 (socket 0)         Node 1 (socket 1)
+--------------------+    +--------------------+
| CPU 0-7            |    | CPU 8-15           |
| Local Memory       |    | Local Memory       |
| kcompactd0         |    | kcompactd1         |
+--------------------+    +--------------------+
         |                         |
    compact 本节点内各 zone     compact 本节点内各 zone
    DMA32, Normal               Normal

26.2 compact_node 的节点级规整

compact_node() 遍历指定节点的所有 zone 进行规整(mm/compaction.c:2893):

// mm/compaction.c:2893
static int compact_node(pg_data_t *pgdat, bool proactive)
{
    int zoneid;
    struct zone *zone;
    struct compact_control cc = {
        .order = -1,
        .mode = proactive ? MIGRATE_SYNC_LIGHT : MIGRATE_SYNC,
        .ignore_skip_hint = true,
        .whole_zone = true,
        .gfp_mask = GFP_KERNEL,
        .proactive_compaction = proactive,
    };

    for (zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
        zone = &pgdat->node_zones[zoneid];
        if (!populated_zone(zone))
            continue;

        if (fatal_signal_pending(current))
            return -EINTR;

        cc.zone = zone;
        compact_zone(&cc, NULL);
        // ...
    }
    return 0;
}

注意:compact_node() 用于手动触发(/proc/sys/vm/compact_memory/sys/devices/system/node/nodeN/compact)和主动规整,使用 order = -1(全量规整)和 whole_zone = true(全 zone 扫描)。

26.3 /sys/devices/system/node/nodeN/compact 接口

通过 sysfs 接口对单个 NUMA 节点触发规整(mm/compaction.c:2994):

// mm/compaction.c:2994
#if defined(CONFIG_SYSFS) && defined(CONFIG_NUMA)
static ssize_t compact_store(struct device *dev,
        struct device_attribute *attr, const char *buf, size_t count)
{
    int nid = dev->id;

    if (nid >= 0 && nid < nr_node_ids && node_online(nid)) {
        // 刷新 LRU 缓存
        lru_add_drain_all();
        compact_node(NODE_DATA(nid), false);
    }
    return count;
}

26.4 NUMA 拓扑感知的迁移目标选择

在 NUMA 系统中,规整优先在本地节点内进行。compaction_alloc() 通过 compaction_alloc_noprof()cc->freepages 链表中分配目标页,而这些空闲页都来自同一个 zone(mm/compaction.c:1797):

// mm/compaction.c:1853(注释原文)
/*
 * This is a migrate-callback that "frees" freepages back to the isolated
 * freelist.  All pages on the freelist are from the same zone, so there is no
 * special handling needed for NUMA.
 */

因为空闲扫描器只在 cc->zone 内工作,所有迁移目标都在同一个 zone 内,自然也在同一个 NUMA 节点内,不存在跨节点迁移的问题。

26.5 kcompactd 的 NUMA 水位检查

kcompactd_node_suitable() 检查节点内是否有 zone 需要规整(mm/compaction.c):

// mm/compaction.c(相关逻辑)
static bool kcompactd_node_suitable(pg_data_t *pgdat)
{
    int zoneid;
    struct zone *zone;
    enum zone_type highest_zoneidx = pgdat->kcompactd_highest_zoneidx;

    for (zoneid = 0; zoneid <= highest_zoneidx; zoneid++) {
        zone = &pgdat->node_zones[zoneid];

        if (!populated_zone(zone))
            continue;

        // 检查是否满足规整前置条件
        if (compaction_suitable(zone, pgdat->kcompactd_max_order,
                                min_wmark_pages(zone), highest_zoneidx))
            return true;
    }
    return false;
}

26.6 NUMA 感知的主动规整

should_proactive_compact_node() 计算的是节点级碎片评分,自然是 NUMA 感知的:

// mm/compaction.c:2197
static unsigned int fragmentation_score_node(pg_data_t *pgdat)
{
    unsigned int score = 0;
    int zoneid;

    for (zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
        zone = &pgdat->node_zones[zoneid];
        if (!populated_zone(zone)) continue;
        score += fragmentation_score_zone_weighted(zone);
    }
    return score;
}

权重由 zone->present_pages / pgdat->node_present_pages 决定,大内存的 zone 对节点评分贡献更大。


27. 调试接口深度解析

27.1 /sys/kernel/debug/extfrag/

内核在启动时通过 extfrag_debug_init() 注册两个 debugfs 文件(mm/vmstat.c:2415):

// mm/vmstat.c:2415
static int __init extfrag_debug_init(void)
{
    struct dentry *extfrag_debug_root;
    extfrag_debug_root = debugfs_create_dir("extfrag", NULL);
    debugfs_create_file("unusable_index", 0444, extfrag_debug_root, NULL,
                        &unusable_fops);
    debugfs_create_file("extfrag_index", 0444, extfrag_debug_root, NULL,
                        &extfrag_fops);
    return 0;
}
module_init(extfrag_debug_init);

unusable_index/sys/kernel/debug/extfrag/unusable_index):

显示每个 order 的"不可用分数",即该 order 的分配请求失败的概率:

$ cat /sys/kernel/debug/extfrag/unusable_index
Node 0, zone      DMA 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
Node 0, zone    DMA32 0.000 0.000 0.001 0.003 0.012 0.023 0.047 0.064 0.088 0.501 0.432
Node 0, zone   Normal 0.000 0.000 0.000 0.001 0.003 0.008 0.024 0.056 0.201 0.621 0.543

值接近 1.0 表示几乎必然失败,0.0 表示总是成功。

extfrag_index/sys/kernel/debug/extfrag/extfrag_index):

$ cat /sys/kernel/debug/extfrag/extfrag_index
Node 0, zone      DMA  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000 -1.000 -1.000
Node 0, zone    DMA32  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.820  0.912
Node 0, zone   Normal  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.783  0.912  0.945

值 -1.000 表示分配必然成功,0 表示失败是因为内存不足,1.0 表示失败是因为纯碎片。

相关代码(mm/vmstat.c:2373):

// mm/vmstat.c:2373
static void extfrag_show_print(struct seq_file *m,
                               pg_data_t *pgdat, struct zone *zone)
{
    unsigned int order;
    int index;
    struct contig_page_info info;

    seq_printf(m, "Node %d, zone %8s ", pgdat->node_id, zone->name);
    for (order = 0; order < NR_PAGE_ORDERS; ++order) {
        fill_contig_page_info(zone, order, &info);
        index = __fragmentation_index(order, &info);
        // 格式化为 XX.XXX(整数和小数部分分开)
        seq_printf(m, "%2d.%03d ", index / 1000, index % 1000);
    }
    seq_putc(m, '\n');
}

27.2 /proc/sys/vm/ 参数说明

sysctl 参数注册在 vm_compaction 数组中(mm/compaction.c:3286):

// mm/compaction.c:3286
static const struct ctl_table vm_compaction[] = {
    {
        .procname   = "compact_memory",
        .data       = &sysctl_compact_memory,
        .maxlen     = sizeof(int),
        .mode       = 0200,  // 只写
        .proc_handler = sysctl_compaction_handler,
    },
    {
        .procname   = "compaction_proactiveness",
        .data       = &sysctl_compaction_proactiveness,
        .maxlen     = sizeof(sysctl_compaction_proactiveness),
        .mode       = 0644,
        .proc_handler = compaction_proactiveness_sysctl_handler,
        .extra1     = SYSCTL_ZERO,
        .extra2     = SYSCTL_ONE_HUNDRED,  // 范围 [0, 100]
    },
    {
        .procname   = "extfrag_threshold",
        .data       = &sysctl_extfrag_threshold,
        .maxlen     = sizeof(int),
        .mode       = 0644,
        .proc_handler = proc_dointvec_minmax,
        .extra1     = SYSCTL_ZERO,
        .extra2     = SYSCTL_ONE_THOUSAND,  // 范围 [0, 1000]
    },
    {
        .procname   = "compact_unevictable_allowed",
        .data       = &sysctl_compact_unevictable_allowed,
        .maxlen     = sizeof(int),
        .mode       = 0644,
        .proc_handler = proc_dointvec_minmax_warn_RT_change,
        .extra1     = SYSCTL_ZERO,
        .extra2     = SYSCTL_ONE,  // 0 或 1
    },
};

compact_memory 的处理函数(mm/compaction.c:2976):

// mm/compaction.c:2976
static int sysctl_compaction_handler(const struct ctl_table *table, int write,
                                      void *buffer, size_t *length, loff_t *ppos)
{
    int ret = proc_dointvec(table, write, buffer, length, ppos);
    if (ret)
        return ret;
    if (sysctl_compact_memory != 1)
        return -EINVAL;
    if (write)
        return compact_nodes();
    return 0;
}

compaction_proactiveness 的处理函数(mm/compaction.c:2946):

// mm/compaction.c:2946
static int compaction_proactiveness_sysctl_handler(const struct ctl_table *table,
        int write, void *buffer, size_t *length, loff_t *ppos)
{
    int rc, nid;
    rc = proc_dointvec_minmax(table, write, buffer, length, ppos);
    if (rc) return rc;

    if (write && sysctl_compaction_proactiveness) {
        // 写入后立即触发各节点的 kcompactd 检查
        for_each_online_node(nid) {
            pg_data_t *pgdat = NODE_DATA(nid);
            if (pgdat->proactive_compact_trigger) continue;
            pgdat->proactive_compact_trigger = true;
            wake_up_interruptible(&pgdat->kcompactd_wait);
        }
    }
    return 0;
}

27.3 tracepoint 追踪

内核在规整关键路径上设置了 tracepoint,位于 include/trace/events/compaction.h

TRACE_EVENT(mm_compaction_isolate_migratepages, ...)
TRACE_EVENT(mm_compaction_isolate_freepages, ...)
TRACE_EVENT(mm_compaction_migratepages, ...)
TRACE_EVENT(mm_compaction_begin, ...)
TRACE_EVENT(mm_compaction_end, ...)
TRACE_EVENT(mm_compaction_try_to_compact_pages, ...)
TRACE_EVENT(mm_compaction_suitable, ...)
TRACE_EVENT(mm_compaction_deferred, ...)
TRACE_EVENT(mm_compaction_defer_compaction, ...)
TRACE_EVENT(mm_compaction_defer_reset, ...)
TRACE_EVENT(mm_compaction_wakeup_kcompactd, ...)
TRACE_EVENT(mm_compaction_kcompactd_sleep, ...)

使用方法:

# 启用规整 tracepoint
echo 1 > /sys/kernel/debug/tracing/events/compaction/enable

# 查看规整事件
cat /sys/kernel/debug/tracing/trace

# 只追踪规整尝试事件
echo 1 > /sys/kernel/debug/tracing/events/compaction/mm_compaction_try_to_compact_pages/enable

27.4 /proc/zoneinfo 中的规整信息

$ grep -A 30 "Node 0, zone   Normal" /proc/zoneinfo
Node 0, zone   Normal
  pages free     66542
  ...
  compact_threshold        1  <- 最小规整失败阶数(compact_order_failed)
  compact_defer_shift      0  <- 当前延迟级别
  compact_considered       0  <- 已考虑规整次数
  compact_migrate_pfn 0x100200 <- 迁移扫描器缓存 PFN(异步)
  compact_free_pfn    0x3fffe00 <- 空闲扫描器缓存 PFN

28. compaction_free 与 compaction_alloc 的对称设计

28.1 compaction_alloc_noprof

compaction_alloc_noprof() 是规整的内部页面分配器,从空闲扫描器收集的 cc->freepages 链表中取出页面(mm/compaction.c:1797):

// mm/compaction.c:1797
static struct folio *compaction_alloc_noprof(struct folio *src, unsigned long data)
{
    struct compact_control *cc = (struct compact_control *)data;
    int order = folio_order(src);  // 获取源页面的阶数
    int start_order;
    struct page *freepage;
    unsigned long size;

again:
    // 从 src 的 order 开始,向上查找可用的空闲页
    for (start_order = order; start_order < NR_PAGE_ORDERS; start_order++)
        if (!list_empty(&cc->freepages[start_order]))
            break;

    // 如果 freepages 链表为空,触发一次空闲页隔离
    if (start_order == NR_PAGE_ORDERS) {
        if (has_isolated_pages) return NULL;
        isolate_freepages(cc);
        has_isolated_pages = true;
        goto again;
    }

    freepage = list_first_entry(&cc->freepages[start_order], struct page, lru);
    size = 1 << start_order;
    list_del(&freepage->lru);

    // 将大块分割为目标阶数:多余的部分放回对应阶数的 freepages 链表
    while (start_order > order) {
        start_order--;
        size >>= 1;
        list_add(&freepage[size].lru, &cc->freepages[start_order]);
        set_page_private(&freepage[size], start_order);
    }

    // 准备页面(设置引用计数、初始化)
    post_alloc_hook(&dst->page, order, __GFP_MOVABLE);
    set_page_refcounted(&dst->page);
    if (order)
        prep_compound_page(&dst->page, order);

    cc->nr_freepages -= 1 << order;
    cc->nr_migratepages -= 1 << order;
    return page_rmappable_folio(&dst->page);
}

这里有一个重要的优化:如果 cc->freepages 中没有恰好满足 src 阶数的空闲页,会取更大的块并拆分,多余部分放回 freepages 对应阶数的链表中,下次可以直接使用。

28.2 compaction_free

compaction_free() 是与 compaction_alloc() 配套的释放函数,当迁移失败时将目标页面"归还"(mm/compaction.c:1855):

// mm/compaction.c:1855
static void compaction_free(struct folio *dst, unsigned long data)
{
    struct compact_control *cc = (struct compact_control *)data;
    int order = folio_order(dst);
    struct page *page = &dst->page;

    if (folio_put_testzero(dst)) {
        // 引用计数降为 0,可以安全回收
        free_pages_prepare(page, order);
        list_add(&dst->lru, &cc->freepages[order]);
        cc->nr_freepages += 1 << order;
    }
    // 无论是否回收,都更新 nr_migratepages(迁移失败,该页面不再需要目标)
    cc->nr_migratepages += 1 << order;
}

注意:folio_put_testzero() 会原子地减少引用计数并检查是否为零。如果目标页面已经被其他人获取了引用(极少见的竞争情况),则不放回 freepages 链表,由该引用持有者负责释放。

28.3 release_free_list:规整结束后的清理

compact_zone() 结束时(无论成功还是失败),release_free_list() 会将剩余的隔离空闲页还给 buddy 系统(mm/compaction.c:91):

// mm/compaction.c:91
static unsigned long release_free_list(struct list_head *freepages)
{
    int order;
    unsigned long high_pfn = 0;

    for (order = 0; order < NR_PAGE_ORDERS; order++) {
        struct page *page, *next;
        list_for_each_entry_safe(page, next, &freepages[order], lru) {
            unsigned long pfn = page_to_pfn(page);
            list_del(&page->lru);
            // 将页面标记为已分配状态,然后通过 __free_pages 释放
            mark_allocated(page, order, __GFP_MOVABLE);
            __free_pages(page, order);
            if (pfn > high_pfn)
                high_pfn = pfn;
        }
    }
    return high_pfn;  // 返回最高的释放 PFN,用于更新 compact_cached_free_pfn
}

返回的 high_pfn 被用来更新 compact_cached_free_pfn:确保空闲扫描器缓存不比实际归还的最高位置还要高,避免下次扫描从已经没有空闲页的区域开始。


29. 内存热插拔与 compaction 的交互

29.1 内存离线需要规整

内存热拔出(memory hotremove)是 compaction 的一个重要使用场景。要将一段物理内存从系统中移除,必须先将该区域内的所有页面迁移出去,这本质上就是强制规整。

内存离线流程(memory_hotplug.c):
    offline_pages()
        |
        +-- start_isolate_page_range()  // 标记为 MIGRATE_ISOLATE
        |
        +-- do_migrate_range()          // 调用 migrate_pages()
        |
        +-- test_pages_isolated()       // 验证清空
        |
        +-- remove_pfn_range_from_zone() // 从 zone 中移除

29.2 skip_offline_sections

在 SPARSEMEM 配置下,compaction 需要跳过 offline 的内存段(mm/compaction.c:211):

// mm/compaction.c:211
#ifdef CONFIG_SPARSEMEM
static unsigned long skip_offline_sections(unsigned long start_pfn)
{
    unsigned long start_nr = pfn_to_section_nr(start_pfn);
    if (online_section_nr(start_nr))
        return 0;
    while (++start_nr <= __highest_present_section_nr) {
        if (online_section_nr(start_nr))
            return section_nr_to_pfn(start_nr);
    }
    return 0;
}

迁移扫描器和空闲扫描器都会调用这个函数,遇到 offline 段时直接跳到下一个 online 段,避免访问无效内存。

29.3 内存热插入后的规整

新内存加入系统时,相关 pageblock 初始化为 MIGRATE_MOVABLE,并通过 kcompactd_run() 确保对应 NUMA 节点有 kcompactd 守护线程(mm/compaction.c:3237):

// mm/compaction.c(内存热插逻辑)
void __meminit kcompactd_run(int nid)
{
    pg_data_t *pgdat = NODE_DATA(nid);
    if (pgdat->kcompactd)
        return;
    // 创建并启动 kcompactd 线程
    pgdat->kcompactd = kthread_create_on_node(kcompactd, pgdat, nid,
                                               "kcompactd%d", nid);
    // ...
}

29.4 pageblock_skip_persistent

大的 compound 页(阶数 >= pageblock_order)在迁移完成前应该永久跳过,这通过 pageblock_skip_persistent() 函数实现(mm/compaction.c:262):

// mm/compaction.c:262
static bool pageblock_skip_persistent(struct page *page)
{
    if (!PageCompound(page))
        return false;

    page = compound_head(page);

    if (compound_order(page) >= pageblock_order)
        return true;

    return false;
}

调用者包括 suitable_migration_source()mm/compaction.c:1359)和 __reset_isolation_pfn()mm/compaction.c:276),确保这类大型复合页既不被选为迁移源,也不会因 reset_isolation 而被错误清除 skip 标志。


30. 性能分析与常见问题诊断

30.1 compact_stall 过高

现象/proc/vmstatcompact_stall 持续增加,同时 compact_fail 也在增加。

含义:直接规整发生(进程被阻塞),但规整后分配仍然失败。

诊断步骤

# 1. 检查碎片化程度
cat /sys/kernel/debug/extfrag/extfrag_index
# 关注 order-9(THP order)列的值是否接近 1

# 2. 检查内存总量是否不足
free -h
cat /proc/vmstat | grep -E "pgsteal|pgscan|pgrefill"

# 3. 检查 extfrag_threshold 是否过高
cat /proc/sys/vm/extfrag_threshold
# 如果 fragindex 远高于 threshold,说明规整在启动(好)
# 如果 fragindex 低于 threshold,规整不会启动,但分配还在失败 -> 内存真的不足

# 4. 查看 kcompactd 活动
cat /proc/vmstat | grep -E "compact_daemon_wake|compact_daemon_migrate"

常见原因与解决

原因 1:UNMOVABLE 页面污染了 MOVABLE 页块
  -> 查看 /proc/pagetypeinfo 中 Unmovable 占比
  -> 考虑通过 hugetlbfs 预分配大页,减少 THP 分配压力

原因 2:内存真的不足(fragindex 低)
  -> 增加系统内存或减少内存使用量
  -> echo 0 > /proc/sys/vm/extfrag_threshold 强制规整(治标不治本)

原因 3:规整后页面被立即重新分配为 UNMOVABLE
  -> 考虑使用 cpuset 或 NUMA 绑定隔离关键工作负载

30.2 THP 分配失败率高

诊断

# 查看 THP 分配统计
cat /proc/vmstat | grep -E "thp_fault|thp_collapse|thp_split"
# thp_fault_fallback 高 -> THP 分配经常回退到普通页
# thp_collapse_alloc_failed 高 -> khugepaged 合并失败

# 查看规整是否有效
cat /proc/vmstat | grep -E "compact_success|compact_fail|compact_stall"

优化配置

# 提高主动规整积极性,保持 THP 可分配状态
echo 60 > /proc/sys/vm/compaction_proactiveness

# 开启 THP 的延迟分配(madvise 模式性能更好)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

# 为 THP 分配配置规整
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag

30.3 规整效率量化

通过计算规整效率来判断规整是否有效:

# 读取初始值
before_migrate=$(grep compact_migrate_scanned /proc/vmstat | awk '{print $2}')
before_free=$(grep compact_free_scanned /proc/vmstat | awk '{print $2}')
before_success=$(grep compact_success /proc/vmstat | awk '{print $2}')

# 等待一段时间...
sleep 60

# 读取结束值
after_migrate=$(grep compact_migrate_scanned /proc/vmstat | awk '{print $2}')
after_free=$(grep compact_free_scanned /proc/vmstat | awk '{print $2}')
after_success=$(grep compact_success /proc/vmstat | awk '{print $2}')

echo "迁移扫描: $((after_migrate - before_migrate))"
echo "空闲扫描: $((after_free - before_free))"
echo "成功次数: $((after_success - before_success))"

规整效率参考值:

  • 迁移扫描 / 空闲扫描 比值约 0.3-0.7 为正常(过高说明空闲页太少,过低说明可迁移页太少)
  • 如果 compact_success / (compact_success + compact_fail) < 50%,说明规整效率很差

30.4 内存压力与规整的关系状态图

                          系统内存状态

   充足                   适中                   紧张                  极度紧张
+----------+          +----------+          +----------+          +----------+
| 无规整   |          | proactive|          | 直接规整 |          | OOM      |
| 活动     |          | compaction          | 频繁发生 |          | killer   |
|          | -------> | 每500ms  | -------> | compact  | -------> | 激活     |
| frag     |          | 检查frag |          | _stall++ |          |          |
| score    |          | score    |          |          |          |          |
| < high   |          | > high   |          | defer    |          |          |
| wmark    |          | wmark    |          | shift++  |          |          |
+----------+          +----------+          +----------+          +----------+
                           |                    |
                      fragmentation         fragmentation
                      score < low          + reclaim
                      wmark: 停止         无法解决:
                                          -> kswapd 协作

30.5 与 PSI(Pressure Stall Information)的集成

kcompactd_do_work() 会设置 PSI 内存停顿标志(mm/compaction.c:3097):

// mm/compaction.c:3097
psi_memstall_enter(&pflags);
kcompactd_do_work(pgdat);
psi_memstall_leave(&pflags);

这意味着响应式的 kcompactd 规整会被 PSI 系统统计为内存停顿时间(some 级别),可以通过 /proc/pressure/memory 观察:

$ cat /proc/pressure/memory
some avg10=0.00 avg60=0.12 avg300=0.05 total=12345678
full avg10=0.00 avg60=0.03 avg300=0.01 total=3456789

some 值升高但 full 值低:说明部分进程受内存压力影响(包括 kcompactd 规整) full 值升高:说明所有可运行进程都被内存停顿阻塞,通常意味着严重内存压力


附录:关键函数索引

函数 文件:行号 说明
compact_zone() mm/compaction.c:2511 规整主控函数
try_to_compact_pages() mm/compaction.c:2814 直接规整入口
isolate_migratepages() mm/compaction.c:2045 迁移扫描器主循环
isolate_migratepages_block() mm/compaction.c:836 单个页块的页面隔离
isolate_freepages_block() mm/compaction.c:556 空闲页扫描隔离
compact_finished() mm/compaction.c:2345 规整终止判断
compaction_suitable() mm/compaction.c:2387 规整前置条件检查
__compaction_suitable() mm/compaction.c:2357 水位检查核心
compaction_zonelist_suitable() mm/compaction.c:2432 整个 zonelist 的适合性
kcompactd() mm/compaction.c:3165 kcompactd 线程主函数
kcompactd_do_work() mm/compaction.c:3055 响应式规整
wakeup_kcompactd() mm/compaction.c:3135 唤醒 kcompactd
compact_node() mm/compaction.c:2893 节点级规整
compact_nodes() mm/compaction.c:2930 全系统规整
fragmentation_score_node() mm/compaction.c:2197 节点碎片评分
fragmentation_score_zone() mm/compaction.c:2167 zone 碎片评分
fragmentation_score_wmark() mm/compaction.c:2214 碎片评分水位
should_proactive_compact_node() mm/compaction.c:2223 主动规整判断
defer_compaction() mm/compaction.c:126 规整延迟
compaction_deferred() mm/compaction.c:141 检查是否延迟
compaction_restarting() mm/compaction.c:178 检查是否重启规整
fast_find_migrateblock() mm/compaction.c:1923 快速搜索可迁移页块
suitable_migration_source() mm/compaction.c:1359 页块迁移源适合性
suitable_migration_target() mm/compaction.c:1379 页块迁移目标适合性
compact_scanners_met() mm/compaction.c:1418 两扫描器相遇判断
compaction_alloc_noprof() mm/compaction.c:1797 规整内部页面分配器
compaction_free() mm/compaction.c:1855 规整内部页面释放器
release_free_list() mm/compaction.c:91 释放隔离的空闲页
__reset_isolation_suitable() mm/compaction.c:349 重置 skip 标志
pageblock_skip_persistent() mm/compaction.c:262 持久跳过判断
extfrag_for_order() mm/vmstat.c:1130 碎片化程度计算
fragmentation_index() mm/vmstat.c:1144 碎片化指数
__fragmentation_index() mm/vmstat.c:1102 碎片化指数核心计算
fill_contig_page_info() mm/vmstat.c:1063 填充连续页面信息
cma_alloc() mm/cma.c:943 CMA 分配接口
cma_activate_area() mm/cma.c:140 CMA 区域初始化
migrate_pages() mm/migrate.c:2072 页面迁移引擎
migrate_folio_unmap() mm/migrate.c:1203 folio 解映射阶段
__migrate_folio() mm/migrate.c:855 folio 迁移核心
migrate_folio() mm/migrate.c:892 通用 LRU folio 迁移
migrate_hugetlbs() mm/migrate.c:1610 大页迁移
migrate_pages_batch() mm/migrate.c:1783 批量页面迁移
putback_movable_pages() mm/migrate.c:257 将隔离页放回
isolate_movable_ops_page() mm/migrate.c:113 隔离 movable_ops 页面
remove_migration_pte() mm/migrate.c:346 更新页表(移除迁移 PTE)
__alloc_pages_direct_compact() mm/page_alloc.c:4164 分配路径中的规整调用
kcompactd_run() mm/compaction.c:3237 启动 kcompactd 线程
kcompactd_stop() mm/compaction.c:3257 停止 kcompactd 线程

由 Claude Code 分析生成