跳转至

第一篇:Buddy 页分配器 — 伙伴系统、水位线与迁移类型

源码:mm/page_alloc.c, mm/internal.h | 头文件:include/linux/mmzone.h, include/linux/gfp.h

系列目录:内存管理 内核源码深度解析


1. 概述

Buddy 页分配器是 Linux 内核物理页管理的基石。它以 MAX_PAGE_ORDER 阶(order)组织空闲页、按迁移类型隔离页面、通过水位线控制内存回收节奏,最终为 SLUB/slab 分配器、缺页异常处理、文件缓存等上层子系统提供物理页。

核心文件: - mm/page_alloc.c — 分配器主体逻辑(~7800 行) - include/linux/mmzone.h — zone、free_area、migratetype、watermark 数据结构 - mm/internal.h — ALLOC_WMARK_* 等内部分配标志 - include/linux/gfp.h — 用户可见的 GFP 标志与 alloc_pages 接口

架构关系

用户调用             内核路径
───────────────────────────────────────────────
kmalloc()      ──►  SLUB/slab allocator
                       │ 调用 alloc_pages()
alloc_pages()  ──►  伙伴系统 (Buddy Allocator)
                       │ 从 free_area[order] 取页
                       │ 不足时分裂高阶块
物理页          ──►  struct page / struct folio


2. 核心数据结构

2.1 MAX_PAGE_ORDER — 最大分配阶

include/linux/mmzone.h:31

#ifndef CONFIG_ARCH_FORCE_MAX_ORDER
#define MAX_PAGE_ORDER 10
#else
#define MAX_PAGE_ORDER CONFIG_ARCH_FORCE_MAX_ORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << MAX_PAGE_ORDER)  // 1024 pages = 4MB (4K base)
#define NR_PAGE_ORDERS (MAX_PAGE_ORDER + 1)       // order 0..10, 共11个阶

x86_64 默认配置下: - MAX_PAGE_ORDER = 10,单次最大分配 1024 个连续物理页 = 4MB(4K 页) - 若启用大页(HugeTLB),base page 为 2MB 时最大分配可达 2GB - order=0 分配 1 页,order=3 分配 8 页,依此类推

2.2 struct free_area — 空闲区域

include/linux/mmzone.h:192-195

struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};

每个 zone 包含 NR_PAGE_ORDERS(11)个 free_area,每个 free_area: - free_list[MIGRATE_TYPES]:按迁移类型分组的空闲页链表数组 - nr_free:该阶所有迁移类型的空闲页总数

zone → free_area 层次关系

pglist_data (NUMA node)
├── node_zones[]
│   ├── ZONE_DMA32
│   │   └── free_area[MAX_PAGE_ORDER + 1]  (order 0..10)
│   │       └── free_area[order]
│   │           ├── free_list[MIGRATE_UNMOVABLE]
│   │           ├── free_list[MIGRATE_MOVABLE]
│   │           ├── free_list[MIGRATE_RECLAIMABLE]
│   │           ├── free_list[MIGRATE_CMA]
│   │           └── nr_free
│   ├── ZONE_NORMAL
│   │   └── free_area[...]
│   └── ZONE_MOVABLE
│       └── free_area[...]
└── ...

2.3 migratetype — 迁移类型(反碎片)

include/linux/mmzone.h:118-144

enum migratetype {
    MIGRATE_UNMOVABLE,      // 不可移动页:内核数据结构
    MIGRATE_MOVABLE,        // 可移动页:用户进程页
    MIGRATE_RECLAIMABLE,    // 可回收页:文件缓存
    MIGRATE_PCPTYPES,       // PCP list 类型边界
    MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,  // 高优先级原子分配
#ifdef CONFIG_CMA
    MIGRATE_CMA,            // CMA 连续内存区
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,        // 内存隔离(热插拔/离线)
#endif
    MIGRATE_TYPES
};

设计目标:同类页面聚集在同一 pageblock(通常 2^(MAX_PAGE_ORDER-1) 页),便于连续内存分配和外碎片预防: - UNMOVABLE:内核代码、页表、slab 对象等——不可迁移 - MOVABLE:匿名页、用户态 malloc——可迁移,通过 move_pages() 或 compaction - RECLAIMABLE:page cache、tmpfs——可回收(直接丢弃或写回) - CMA:为 DMA 预留的连续内存区,只用 MOVABLE 页面填充,需要时可迁移 - ISOLATE:标记为离线的 pageblock,禁止分配


3. 水位线 (Watermarks)

3.1 水位线枚举

include/linux/mmzone.h:796-802

enum zone_watermarks {
    WMARK_MIN,      // 最低警戒:仅允许紧急分配(PF_MEMALLOC、OOM)
    WMARK_LOW,      // 低水位:唤醒 kswapd 后台回收
    WMARK_HIGH,     // 高水位:kswapd 可休眠
    WMARK_PROMO,    // 晋升水位(内存分层/PROMOTION)
    NR_WMARK
};

每个 zone 在初始化时计算三条水位线:

WMARK_MIN  = 根据 zone 大小按比例计算的最小保留页
WMARK_LOW  = WMARK_MIN + (WMARK_MIN / 4)
WMARK_HIGH = WMARK_MIN + (WMARK_MIN / 2)

┌───────────────────────────────────┐
│          free pages               │
│                                   │  above HIGH  → kswapd 休眠
│  ─ ─ HIGH watermark ─ ─ ─ ─ ─ ─ │
│                                   │  HIGH ~ LOW  → 正常分配,kswapd 不启动
│  ─ ─ LOW  watermark ─ ─ ─ ─ ─ ─ │
│                                   │  LOW ~ MIN   → 申请分配 → wake kswapd
│  ─ ─ MIN  watermark ─ ─ ─ ─ ─ ─ │
│  xxxxx 保留给紧急分配 xxxxxx      │  below MIN   → 直接回收 / OOM
└───────────────────────────────────┘

3.2 分配标志 — ALLOC_WMARK_*

mm/internal.h:1445-1480

#define ALLOC_WMARK_MIN      WMARK_MIN       // 使用 MIN 水位做检查
#define ALLOC_WMARK_LOW      WMARK_LOW       // 使用 LOW 水位做检查
#define ALLOC_WMARK_HIGH     WMARK_HIGH      // 使用 HIGH 水位做检查
#define ALLOC_NO_WATERMARKS  0x04            // 不检查水位(紧急分配)
#define ALLOC_OOM            0x08            // OOM 场景,允许突破 MIN
#define ALLOC_NOFRAGMENT     0x100           // 禁止混合 pageblock 类型
#define ALLOC_KSWAPD         0x800           // 允许唤醒 kswapd

内核分配路径中的水位线升级策略:

初始尝试    ALLOC_WMARK_LOW  —— 正常分配
  ↓ 失败
慢速路径    ALLOC_WMARK_MIN  —— 降低水位要求
  ↓ 仍失败
            ALLOC_OOM        —— OOM 受害者可以突破 MIN
  ↓ 所有都失败
OOM killer  ALLOC_NO_WATERMARKS —— 最后手段


4. fallback 数组 — 迁移类型降级

mm/page_alloc.c:1915-1919

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

当首选迁移类型的空闲链表为空时,按 fallback 顺序从其他类型"借用"页:

UNMOVABLE 请求:
  ① MIGRATE_UNMOVABLE 链表
  ② MIGRATE_RECLAIMABLE 链表 (fallback)
  ③ MIGRATE_MOVABLE 链表 (fallback)

MOVABLE 请求:
  ① MIGRATE_MOVABLE 链表
  ② MIGRATE_RECLAIMABLE 链表 (fallback)
  ③ MIGRATE_UNMOVABLE 链表 (fallback)

注意 MIGRATE_MOVABLE 的 fallback 不会首先借用 UNMOVABLE——保持 MOVABLE 区域尽量纯净,有利于后续 compaction。


5. 快速路径:单页/低阶分配

5.1 alloc_pages 入口

include/linux/gfp.h:322

struct page *alloc_pages_noprof(gfp_t gfp, unsigned int order);

用户态最常用的接口(GFP_KERNEL、GFP_ATOMIC 等):

// 分配 order-0 页
struct page *page = alloc_page(GFP_KERNEL);

// 分配 order-3 页(8 pages)
struct page *pages = alloc_pages(GFP_KERNEL, 3);

5.2 __alloc_frozen_pages_noprof — 分配器心脏

mm/page_alloc.c:5185-5247

这是整个 buddy 分配器的核心函数:

struct page *__alloc_frozen_pages_noprof(gfp_t gfp, unsigned int order,
        int preferred_nid, nodemask_t *nodemask)
{
    struct page *page;
    unsigned int alloc_flags = ALLOC_WMARK_LOW;    // 初始使用 LOW 水位
    struct alloc_context ac = { };

    // 1. 参数校验:order 必须 <= MAX_PAGE_ORDER
    if (WARN_ON_ONCE_GFP(order > MAX_PAGE_ORDER, gfp))
        ...

    // 2. GFP 上下文修正(NOFS/NOIO 继承等)
    gfp = current_gfp_context(gfp);

    // 3. 准备分配上下文(zonelist、migratetype 确定)
    prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac, ...);

    // 4. 禁止碎片化降级标志
    alloc_flags |= alloc_flags_nofragment(...);

    // 5. 第一次尝试:快速路径
    page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
    if (likely(page))
        goto out;

    // 6. 快速路径失败 → 进入慢速路径
    page = __alloc_pages_slowpath(alloc_gfp, order, &ac);

out:
    // 7. memcg kmem 记账
    if (memcg_kmem_online() && (gfp & __GFP_ACCOUNT) && page)
        __memcg_kmem_charge_page(page, gfp, order);

    return page;
}

5.3 get_page_from_freelist — 遍历 zonelist

mm/page_alloc.c:3787-3820

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
    z = ac->preferred_zoneref;
    for_next_zone_zonelist_nodemask(zone, z, ...) {
        // 水位检查:free pages 是否足够
        if (!zone_watermark_fast(zone, order,
                watermark (from alloc_flags), ...))
            continue;  // 水位不足,跳过该 zone

        // 尝试从该 zone 分配
        page = rmqueue(ac->preferred_zoneref->zone, zone, order,
                       gfp_mask, alloc_flags, ac->migratetype);
        if (page) {
            prep_new_page(page, order, gfp_mask, alloc_flags);
            return page;
        }
    }
    return NULL;
}

5.4 rmqueue — 单 zone 分配入口

mm/page_alloc.c:3389-3410

static inline
struct page *rmqueue(struct zone *preferred_zone,
                     struct zone *zone, unsigned int order,
                     gfp_t gfp_flags, unsigned int alloc_flags,
                     int migratetype)
{
    struct page *page;

    // order ≤ pcp 允许的最大阶数 → 从 per-CPU 页缓存分配(快速)
    if (likely(pcp_allowed_order(order))) {
        page = rmqueue_pcplist(preferred_zone, zone, order,
                               migratetype, alloc_flags);
        if (likely(page))
            goto out;
    }

    // PCP 缓存没有 → 从 buddy free_area 直接分配
    page = rmqueue_buddy(zone, order, migratetype, alloc_flags);
    ...
}

5.5 Per-CPU Pages (PCP) — order-0 的极致快速路径

每个 CPU 维护一个 per_cpu_pages 结构,缓存当前 CPU 最近的 order-0 页:

CPU0  per_cpu_pages            CPU1  per_cpu_pages
┌──────────────────┐          ┌──────────────────┐
│ pcp->lists[]     │          │ pcp->lists[]     │
│  [MIGRATE_       │          │  [MIGRATE_       │
│   UNMOVABLE]     │          │   UNMOVABLE]     │
│  [MIGRATE_       │          │  [MIGRATE_       │
│   MOVABLE]       │          │   MOVABLE]       │
│  [MIGRATE_       │          │  [MIGRATE_       │
│   RECLAIMABLE]   │          │   RECLAIMABLE]   │
│                  │          │                  │
│ count: 剩余页数   │          │ count: 剩余页数   │
│ high:  缓存上限   │          │ high:  缓存上限   │
│ batch: 批量数量   │          │ batch: 批量数量   │
└──────────────────┘          └──────────────────┘
         │                            │
         │ 不足时批量补充              │
         ▼                            ▼
┌──────────────────────────────────────────┐
│         zone->free_area[]                │
│         (buddy allocator)                │
└──────────────────────────────────────────┘

mm/page_alloc.c:3302-3343__rmqueue_pcplist

static inline
struct page *__rmqueue_pcplist(struct zone *zone, unsigned int order,
        int migratetype, unsigned int alloc_flags,
        struct per_cpu_pages *pcp, struct list_head *list)
{
    struct page *page;

    do {
        // PCP 链表为空 → 从 buddy bulk 补充
        if (list_empty(list)) {
            int batch = nr_pcp_alloc(pcp, zone, order);
            int alloced;

            alloced = rmqueue_bulk(zone, order, batch, list,
                                   migratetype, alloc_flags);
            pcp->count += alloced << order;
            if (unlikely(list_empty(list)))
                return NULL;
        }

        // 从 PCP 链表取第一页
        page = list_first_entry(list, struct page, pcp_list);
        list_del(&page->pcp_list);
        pcp->count -= 1 << order;  // order-0 减1,order-N 减 2^N
    } while (check_new_pages(page, order));

    return page;
}

PCP 缓存热/冷:PCP 页被释放时置于链表头部(热端),下次分配直接取头部——L1/L2 cache 仍然命中。

5.6 __rmqueue_smallest — 伙伴分裂

mm/page_alloc.c:1883-1906

当 PCP 缓存为空,或者请求 order > 0 时,直接从 buddy free_area 分配:

static __always_inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                                int migratetype)
{
    unsigned int current_order;
    struct free_area *area;
    struct page *page;

    // 从请求的 order 开始向上搜索更大的空闲块
    for (current_order = order; current_order < NR_PAGE_ORDERS; ++current_order) {
        area = &(zone->free_area[current_order]);
        page = get_page_from_free_area(area, migratetype);
        if (!page)
            continue;  // 当前阶没空闲页,尝试更大的阶

        // 找到后执行 expand:将高阶块一半一半地分裂
        page_del_and_expand(zone, page, order, current_order, migratetype);
        return page;
    }

    return NULL;  // 所有阶都为空
}

伙伴分裂示意图(请求 order=1,当前 order=3 有块):

order=3: ┌──────────────────────────────────────────┐  (8 pages)
         │  page0  page1  page2  page3  page4...    │
         └────────┬─────────────────────────────────┘
                  │ expand (分裂)
order=2: ┌──────────────────┐ ┌──────────────────┐
         │   新伙伴A (4页)    │ │   新伙伴B (4页)   │  ← B 归还 free_area[2]
         └────────┬─────────┘ └──────────────────┘
                  │ expand (继续分裂)
order=1: ┌───────────┐ ┌───────────┐
         │  返回调用者  │ │ 归还 free_area[1] │
         └───────────┘ └───────────┘

合并场景(释放 order=1 页):

释放前:
order=1: [页A] (空闲)  +  [页B] (已分配)

释放 order=1 页B:
order=1: [页A] [页B]  → 二者的 PFN 是伙伴关系 →  合并
order=2: [页A+B]                                  → 继续检查上级伙伴
order=3: [页A+B+伙伴]                             → 最终合并到最高可能阶

伙伴地址计算公式:

// 给定 page 的 PFN,找到其伙伴的 PFN
buddy_pfn = page_pfn ^ (1 << order);
// 例如:order=2,PFN=8 → buddy_pfn = 8 ^ 4 = 12


6. 慢速路径:__alloc_pages_slowpath

mm/page_alloc.c:4682-4820

__alloc_pages_slowpath
├─ 1. 唤醒 kswapd(后台回收线程)
│      wake_all_kswapds(order, gfp_mask, ac)
├─ 2. 降低水位再次尝试
│      alloc_flags = gfp_to_alloc_flags() → ALLOC_WMARK_MIN
│      page = get_page_from_freelist()
├─ 3. 尝试直接 compaction
│      (costly order 或 non-MOVABLE 高阶分配)
│      page = __alloc_pages_direct_compact()
├─ 4. 尝试直接回收 (direct reclaim)
│      page = __alloc_pages_direct_reclaim()
│      → 回收干净页 / 写回脏页 / swap out 匿名页
├─ 5. 再次尝试分配
│      page = get_page_from_freelist()
│      → 成功则返回
├─ 6. should_compact_retry / should_reclaim_retry
│      → 决定是否再次尝试 compaction/reclaim
│      → 最多循环 16 次 (MAX_RECLAIM_RETRIES)
└─ 7. 所有手段耗尽
       → __GFP_NOFAIL ? goto retry : goto nopage

6.1 __alloc_pages_direct_reclaim

mm/page_alloc.c:4409-4430

直接回收:当 kswapd 无法及时补充空闲页时,分配者自己进行回收:

static inline struct page *
__alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
        unsigned int alloc_flags, const struct alloc_context *ac,
        unsigned long *did_some_progress)
{
    struct page *page = NULL;

    // 进入 direct reclaim
    // → __perform_reclaim() → try_to_free_pages() → shrink_node()
    *did_some_progress = __perform_reclaim(gfp_mask, order, ac);

    if (unlikely(!(*did_some_progress)))
        return NULL;

    // 回收了一些页 → 重试分配
retry:
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

    if (!page && !(gfp_mask & __GFP_NORETRY))
        goto retry;  // 允许重试,继续循环

    return page;
}

6.2 循环重试策略

循环计数器: no_progress_loops
compaction_retries: 最多 COMPACT_MAX_DEFERRED (6次)
MAX_RECLAIM_RETRIES: 16

每次循环:
  ① 尝试分配                      ── 成功 → 返回
  ② 尝试 compaction               ── 成功 → 返回
  ③ 尝试 direct reclaim           ── 未进展 → no_progress_loops++
  ④ should_compact_retry()         ── 是 → 继续
  ⑤ should_reclaim_retry()         ── 是 → 继续
  ⑥ 否 → goto nopage (返回 NULL)

7. 触发 OOM 的条件

当慢速路径所有尝试失败后:

nopage:
    // __alloc_pages_may_oom()
    //   → out_of_memory() → oom_kill_process()

触发 OOM 的前置条件(在慢速路径中逐步升级):

ALLOC_WMARK_LOW  失败
  → ALLOC_WMARK_MIN  失败
  → direct_compact   失败
  → direct_reclaim   失败 (no progress)
  → should_compact_retry() = false
  → should_reclaim_retry() = false
  → __alloc_pages_may_oom()  → 如果没有 OOM 受害者正在退出
  → out_of_memory() → 选择并杀死进程


8. 完整分配路径总结

alloc_pages(gfp, order)
__alloc_frozen_pages_noprof()
  ├─[Fast Path]─────────────────────────────────────────
  │  alloc_flags = ALLOC_WMARK_LOW
  │  │
  │  ▼
  │  get_page_from_freelist()
  │    │ 遍历 zonelist 中的每个 zone
  │    │ 检查 zone_watermark_fast()
  │    │
  │    ▼
  │    rmqueue()
  │      │  order 小且 pcp_allowed → rmqueue_pcplist()
  │      │    └─ __rmqueue_pcplist() 从 CPU 本地缓存取
  │      │
  │      └─ order 大或 PCP 空 → rmqueue_buddy()
  │           └─ __rmqueue_smallest()
  │                从 free_area 找合适阶,expand 分裂
  └─[Slow Path]─────────────────────────────────────────
     __alloc_pages_slowpath()
       ├─ wake kswapd
       ├─ 降低水位 ALLOC_WMARK_MIN → 再试分配
       ├─ compaction
       ├─ direct reclaim
       ├─ 循环重试 (最多16次)
       └─ OOM killer

9. 伙伴分配器中的伙伴关系验证

内核在释放页时会验证 PFN 伙伴关系,核心逻辑:

// 检查两个 page 是否为伙伴关系
static inline bool page_is_buddy(struct page *page, struct page *buddy,
                                  unsigned int order)
{
    // 1. buddy 必须是空闲页 (PageBuddy)
    // 2. buddy 必须属于同一 order
    // 3. buddy 必须在同一 zone
    // 4. buddy 的 PFN = page_pfn ^ (1 << order)
}

伙伴分裂与合并保证了分配后的内部碎片最小化,同时释放时尽可能将小块合并为大块。


10. 关键配置与调试

/proc 接口

# 查看伙伴分配器状态
cat /proc/buddyinfo
Node 0, zone   Normal   1203   876   432   201    98    42    18     5    1

# 解读:每列是一个 order 的空闲块数量
# order 0: 1203 个单页
# order 1: 876  个连续2页块
# order 2: 432  个连续4页块
# ...
# order 9: 1    个连续512页块(2MB)
# 查看水位线
cat /proc/zoneinfo | grep -A5 "pages free"

常见 GFP 标志组合

GFP 标志 含义 能否睡眠
GFP_KERNEL 内核常规分配 可以
GFP_ATOMIC 中断/原子上下文 不可以
GFP_NOWAIT 不等待 不可以
GFP_NOFS 禁止文件系统递归 可以
GFP_NOIO 禁止 I/O 递归 可以
__GFP_DIRECT_RECLAIM 允许直接回收 可以
__GFP_NOFAIL 不允许失败(危险) 可以(无限重试)

下一篇文章

第二篇:SLUB 分配器 — sheaf 缓存、slab freelist 与 kmalloc


💬 评论