内存管理-79-free_pages-6.1

调用路径:

free_pages(addr, order)
    __free_pages(page, order)
        free_the_page(page, order)
            if (pcp_allowed_order(order)) {  //计划还给PCP
                free_unref_page(page, order)
                    free_unref_page_prepare(page, pfn, order)
                        free_pcp_prepare
                            free_pages_prepare
                                trace_mm_page_free(page, order)
                    if (is_migrate_isolate(migratetype))  //ISOLATE 类型页面直接归还 buddy,不走 PCP,HIGHATOMIC 当作 MOVABLE 管理
                        free_one_page
                    pcp = pcp_spin_trylock(zone->per_cpu_pageset) //先关迁移,然后再try lock 持 per-cpu 的 zone->per_cpu_pageset->lock
                    if (pcp) {
                        free_unref_page_commit(zone, pcp, page, pcpmigratetype, order);
                            order_to_pindex(migratetype, order)
                            list_add(&page->pcp_list, &pcp->lists[pindex]); //还给PCP
                            if (pcp->count >= high) //若PCP上页面比较多,为了避免碎片化,批量转移到伙伴系统
                                free_pcppages_bulk
                                    spin_lock_irqsave(&zone->lock, flags); //关中断后持 zone->lock 锁
                                    page = list_last_entry(list, struct page, pcp_list); //从PCP链表末尾取页释放进伙伴系统
                                    list_del(&page->pcp_list);
                                    if (bulkfree_pcp_prepare(page)) //这里也有丢页行为,但出现了会有报错打印
                                        continue;
                                    __free_one_page(page, pfn, zone, order, mt, FPI_NONE);
                                    trace_mm_page_pcpu_drain(page, order, mt)
                                    spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 锁后开中断
                        pcp_spin_unlock(pcp); //先释放 per-cpu 的 zone->per_cpu_pageset->lock 锁,然后开迁移
                    } else {
                        free_one_page(zone, page, pfn, order, migratetype, FPI_NONE); //尝试持PCP锁失败,释放回 buddy.
                            spin_lock_irqsave(&zone->lock, flags); //先关中断再持 zone->lock
                            __free_one_page(page, pfn, zone, order, migratetype, fpi_flags); //见下面实现
                            spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 后开中断
                    }
                    pcp_spin_unlock(pcp);
            } else {
                __free_pages_ok(page, order, FPI_NONE) //不满足 PCP 的 order 要求,还给 Buddy
                    free_pages_prepare(page, order, true, fpi_flags) //释放页面之前的准备工作
                        trace_mm_page_free(page, order);
                    spin_lock_irqsave(&zone->lock, flags); //关中断后持 zone->lock
                    __free_one_page(page, pfn, zone, order, migratetype, fpi_flags);
                        while (order < MAX_ORDER - 1) { //核心合并循环
                            compaction_capture(capc, page, order, migratetype)
                            find_buddy_page_pfn(page, pfn, order, &buddy_pfn);
                            del_page_from_free_list(buddy, zone, order);
                        }
                        set_buddy_order(page, order);
                        add_to_free_list/add_to_free_list_tail //合并后的插入空闲链表头或链表尾
                    spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 后开中断
            }

无论释放到 PCP 还是释放到 Buddy, 都有执行 trace_mm_page_free。

释放路径锁的包含关系也是 zone->per_cpu_pageset->lock 在外,zone->lock 锁在内。


一、简介


二、相关结构


三、全局变量


四、接口函数

/* 通过虚拟地址释放页 */
void free_pages(unsigned long addr, unsigned int order)
/* 通过page指针释放页 */
void __free_pages(struct page *page, unsigned int order)


五、逻辑实现

1. free_pages()

void free_pages(unsigned long addr, unsigned int order) //page_alloc.c
{
    /* 若是 free 一个 0 地址,直接什么都不做 */
    if (addr != 0) {
        /* 断言 addr 必须得是线性映射的地址,且有对应物理内存的,且物理没存没有被标记 MEMBLOCK_NOMAP */
        VM_BUG_ON(!virt_addr_valid((void *)addr));
        /* 将地址转换为 page 指针 */
        __free_pages(virt_to_page((void *)addr), order);
    }
}


1.1 virt_addr_valid()

//arch/arm64/include/asm/memory.h
#define virt_addr_valid(addr)    ({                    \
    __typeof__(addr) __addr = __tag_reset(addr);            \
    __is_lm_address(__addr) && pfn_is_map_memory(virt_to_pfn(__addr));    \
})

__tag_reset: 清除 MTE/指针 tag. ARM64 支持 MTE(Memory Tagging Extension),指针的最高位字节(最高8bit)可能被用作 tag.

作用: 清除 addr 最高字节的 tag 标签(替换为 ff),然后要求 addr 必须是线性映射的地址,且 addr 所在的物理页必须属于"被内核线性映射的正常系统 RAM",排除掉 MMIO 设备地址(不是系统 RAM), 和虽然是系统 RAM 但被标记为 MEMBLOCK_NOMAP(如 TEE 保留区、firmware 保留区)的保留区域。


1.2 virt_to_page()

#define virt_to_page(x)    ({                        \
    u64 __idx = (__tag_reset((u64)x) - PAGE_OFFSET) / PAGE_SIZE;    \
    u64 __addr = VMEMMAP_START + (__idx * sizeof(struct page));    \
    (struct page *)__addr;                        \
})

作用: 给定一个内核虚拟地址,返回该地址对应物理页面的 struct page 指针。

实现逻辑: 一个线性映射的地址 x 减去线性映射的起始地址,除以 PAGE_SIZE 后得到 page 页序号 idx, 然后 page 映射的首地址 VMEMMAP_START 加上这个偏移,得到 page 地址。

 

2. __free_pages()

(1) 此函数前的英文注释

这是内核中最常用的页面释放接口。它先减少页面引用计数,只有当引用计数降为 0 时才真正释放

此函数可以释放非复合页的多页分配。它不会检查传入的 @order 是否与分配 order 匹配,因此很容易造成内存泄漏。释放的内存超过分配的内存量可能会发出警告。

如果对该页面的最后一个引用是推测性的,则会由 put_page() 释放,但 put_page() 只会释放非复合页分配的第一个页面####。为了防止剩余页面泄漏,我们在此处释放后续页面。如果您希望使用页面的引用计数来决定何时释放分配,则应分配复合页,并使用 put_page() 而不是 __free_pages()。

调用上下文:可以在中断上下文中或持有普通 spinlock 时调用,但不能在 NMI 上下文中或持有 raw spinlock(非RT内核可以) 时调用。


(2) spinlock 和 raw spinlock 的区别

在标准的不使能 PREEMPT_RT 的内核中,spinlock_t 和 raw_spinlock_t 行为一样,都是自旋锁,即持 raw spinlock 后调用也是安全的。区别体现在 PREEMPT_RT 带实时补丁的内核中,两者区别:

--------------------------------------------------------------------------------------
类型                  非 RT 内核               PREEMPT_RT 内核
--------------------------------------------------------------------------------------
spinlock_t            真正的自旋(关抢占)        被转换为 rt_mutex(可睡眠、可抢占)
raw_spinlock_t        真正的自旋(关抢占)        仍然是真正的自旋(关抢占、可能关中断)
--------------------------------------------------------------------------------------

raw_spinlock_t 的含义是:无论什么内核配置,我都必须真正自旋,不能被转换为睡眠锁。使用场景:调度器、中断控制器、时钟子系统等绝对不能睡眠的底层关键路径。


(3) 为什么持有 raw spinlock 时不能调用

这只是针对使能了 PREEMPT_RT 的内核来说的。在 __free_pages() 执行流程中会去持 pcp_spin_trylock(zone->per_cpu_pageset) 和 zone->lock 锁,这两个都是通过 spin_lock() 去持有的(而不是 raw_spin_lock()), 会休眠。原子上下文不能休眠,所以不能调用。


(4) 为什么 NMI 上下文中不能调用

NMI(Non-Maskable Interrupt)的特性:

--------------------------------------------------------------------------------------
特性                        普通中断(IRQ)                 NMI
--------------------------------------------------------------------------------------
能否被屏蔽                   能(local_irq_disable)        不能
能否被其他中断打断            不能(同级)                    能打断任何东西,包括关中断区间
能否安全获取 spinlock        能(如果用 _irqsave 变体)       不能
--------------------------------------------------------------------------------------

NMI 可以在任何时刻打断 CPU,包括 打断正在持有 spin_lock_irqsave(&zone->lock, flags) 的代码、打断正在持有 PCP 锁的代码、打断正在关中断的代码。

若 NMI 中断 handler 中调用 __free_pages(), NMI 中断打断 zone->lock 持锁临时取,而函数内部会再次持有 zone->lock,会造成 AA 死锁。


(5) 函数实现

void __free_pages(struct page *page, unsigned int order)
{
    /*
     * 在减少引用计数之前,先记录这是否是 compound page 的 head page。因为 put_page_testzero 之后 page 可能已经
     * 被其他 CPU 重新分配,此时再读 PageHead 就不安全了。
     * 检查 page->flags 中的 PG_head 标志
     */
    int head = PageHead(page);

    /* 原子地执行 page->_refcount--, 若结果变为 0 了, 则为真,才执行释放页面的操作 */
    if (put_page_testzero(page))
        /* 正常路径:引用计数归零,释放整个 2^order 块 */
        free_the_page(page, order);
    /*
     * 若是引用计数未归 0 的复合页的 head 页此函数什么都不做,直接退出。
     * 特殊情况:page 引用计数未归零,且不是 compound page 的 head 页。
     */
    else if (!head)
        /*
         * 这发生在高 order 分配被拆分使用的场景(比如分配16页但是只需要9页,如 page fragment):
         * 调用者分配了 order=N 的页,但只用了前半部分,前半部分的 head page 还有其他引用(_refcount > 0),后半部
         * 分的页面没人用了,需要逐级释放回 buddy
         *
         * 具体做法:从高 order 到低 order,依次释放后半部分。例如 order=3(8页),如果 page[0] 还有引用:
         *   释放 page[4..7] 作为 order=2
         *   释放 page[2..3] 作为 order=1
         *   释放 page[1]    作为 order=0
         *   page[0] 保留(仍有引用,只保留 page[0]页)
         */
        while (order-- > 0)
            free_the_page(page + (1 << order), order);
}

内核中几乎没有 "else if" 这种使用case, 可理解为是一个防御性保障,这个分支确保"我释放不了 head page,但至少把不需要的尾部页面还给系统"。思想是非 compound 高阶分配可以部分释放。


2.1 put_page_testzero()

static inline int put_page_testzero(struct page *page)
{
    int ret;

    /* 释放前,要求 page->_refcount 不能等于0(重复释放) */
    VM_BUG_ON_PAGE(page_ref_count(page) == 0, page);
    /* 原子执行 page->_refcount--, 若结果 page->_refcount=0 了则返回 true, 否则返回 false */
    ret = page_ref_dec_and_test(page);
    page_pinner_put_page(page);

    return ret;
}


2.2 free_the_page()

static inline void free_the_page(struct page *page, unsigned int order)
{
    /* order=[0,3] 或 10(THP), 归还到 PCP, 相当于归还时的快速路径 */
    if (pcp_allowed_order(order))
        free_unref_page(page, order);
    else
        /* 归还给 buddy,相当于归还时的慢速路径 */
        __free_pages_ok(page, order, FPI_NONE);
}


2.2.1 free_unref_page()

/**
 * free_unref_page - 将页面释放到当前 CPU 的 PCP 列表。
 * PCP(Per-CPU Pages)是每个 CPU 维护的页面缓存列表,分配/释放时不需要获取 zone->lock,大幅减少锁竞争。
 */
void free_unref_page(struct page *page, unsigned int order)
{
    unsigned long __maybe_unused UP_flags;
    struct per_cpu_pages *pcp;
    struct zone *zone;
    unsigned long pfn = page_to_pfn(page);
    int migratetype, pcpmigratetype;
    bool skip_free_unref_page = false;

    /* 做释放前的准备工作,包括一些校验、清理、涂毒等 */
    if (!free_unref_page_prepare(page, pfn, order))
        return;

    /* 获取页面的迁移类型,函数内直接 return page->index, 上面函数刚将 page->index = migratetype */
    migratetype = get_pcppage_migratetype(page);
    /* Android vendor hook: 允许厂商拦截释放操作 */
    trace_android_vh_free_unref_page_bypass(page, order, migratetype, &skip_free_unref_page);
    if (skip_free_unref_page)
        return;

    /*
     * PCP 列表只维护 UNMOVABLE, RECLAIMABLE, MOVABLE, CMA 这几种 migratetype:
     *
     * 特殊处理:
     * - ISOLATE 类型:不能放入 PCP,直接走 buddy 释放。
     * - HIGHATOMIC 类型:当作 MOVABLE 放入 PCP,因为 PCP 中没有高阶原子类型的空闲链表。
     */
    migratetype = pcpmigratetype = get_pcppage_migratetype(page); //返回的是 page->index;
    if (unlikely(migratetype > MIGRATE_RECLAIMABLE)) {
         /* ISOLATE 页面直接归还 buddy,不走 PCP */
        if (unlikely(is_migrate_isolate(migratetype))) {
            free_one_page(page_zone(page), page, pfn, order, migratetype, FPI_NONE);
            return;
        }
        /* 因为 PCP 不维护高阶原子类型, 将其当作 MOVABLE 类型放入PCP */
        if (pcpmigratetype == MIGRATE_HIGHATOMIC)
            pcpmigratetype = MIGRATE_MOVABLE;
    }

    /* page->flags 中有记录它属于哪个 node 和哪个 zone */
    zone = page_zone(page);
    /* 默认不使能 CONFIG_PREEMPT_RT, 是空实现 */
    pcp_trylock_prepare(UP_flags);

    /*
     * 尝试获取 PCP 的锁(trylock,非阻塞, 任何上下文都安全)。
     * 如果成功,释放到 PCP 列表; 如果失败,释放到 buddy 中,避免自旋等待.
     *
     * 获取PCP锁的方式: 先关迁移,然后再尝试持 per-cpu 的 zone->per_cpu_pageset->lock 锁
     */
    pcp = pcp_spin_trylock(zone->per_cpu_pageset);
    if (pcp) {
        /* 将页面实际放入 PCP 列表的函数 */
        free_unref_page_commit(zone, pcp, page, pcpmigratetype, order);
        /* 先释放 per-cpu 的 zone->per_cpu_pageset->lock 锁,然后开迁移 */
        pcp_spin_unlock(pcp);
    } else {
        /* PCP 锁获取失败,回退到 buddy 路径 */
        free_one_page(zone, page, pfn, order, migratetype, FPI_NONE);
    }

    /* 默认不使能 CONFIG_PREEMPT_RT, 是空实现 */
    pcp_trylock_finish(UP_flags);
}


2.2.1.1 free_unref_page_prepare()

static bool free_unref_page_prepare(struct page *page, unsigned long pfn,    unsigned int order)
{
    int migratetype;

    if (!free_pcp_prepare(page, order))
        return false;

    /* 获取 page 所在的 pageblock 的迁移类型 */
    migratetype = get_pfnblock_migratetype(page, pfn);
    /* 只是执行 page->index = migratetype, 将迁移类型保存到 page->index 上 */
    set_pcppage_migratetype(page, migratetype);
    return true;
}


2.2.1.1.1 free_pcp_prepare()

static bool free_pcp_prepare(struct page *page, unsigned int order)
{
    if (debug_pagealloc_enabled_static())
        return free_pages_prepare(page, order, true, FPI_NONE);
    else
        /* 走这里 */
        return free_pages_prepare(page, order, false, FPI_NONE);
}


2.2.1.1.1.1 free_pages_prepare()

/*
 * 返回值:true = 页面合法可释放;false = 页面有问题,不要释放(如硬件中毒页)
 *
 * 这个函数做三件大事:
 * (1) 合法性检查(检测 double-free、corruption 等错误)
 * (2) 状态清理(清 flags、解除 memcg 计费、重置 owner)
 * (3) 内存投毒(KASAN poison、debug_pagealloc unmap,帮助检测 use-after-free)
 /

此函数中有个trace打印,格式为:

   kworker/u16:1-22406   [003] ..... 32708.519937: mm_page_free: page=00000000fe6f1acd pfn=0x5890e4 order=2
 WatchdogPerfSvc-420     [004] ..s.. 32708.519956: mm_page_free: page=00000000ea25779c pfn=0x5932e8 order=3
 SpeechStateMana-8819    [007] ...2. 32708.520236: mm_page_free: page=00000000c6bd1051 pfn=0x439a26 order=0


2.2.1.2 free_one_page()

static void free_one_page(struct zone *zone,  struct page *page, unsigned long pfn,
                unsigned int order, int migratetype, fpi_t fpi_flags) //传参 fpi_flags=FPI_NONE
{
    unsigned long flags;

    spin_lock_irqsave(&zone->lock, flags); //先关中断再持 zone->lock
    /*
     * 持锁后对特定类型再次获取一次迁移类型,确保迁移类型是准确的
     *
     * 等效于 if(zone->nr_isolate_pageblock || migratetype == MIGRATE_ISOLATE)
     */
    if (unlikely(has_isolate_pageblock(zone) || is_migrate_isolate(migratetype))) {
        migratetype = get_pfnblock_migratetype(page, pfn);
    }
    /* 伙伴系统的核心释放+合并函数 */
    __free_one_page(page, pfn, zone, order, migratetype, fpi_flags);
    spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 后开中断
}

__free_one_page 放在后续向伙伴系统释放时详解。


2.2.1.3 free_unref_page_commit()

/*
 * 此函数将页面实际放入 PCP 列表。
 * PCP 有多个子列表,按 (migratetype, order) 二维索引。当 PCP 中缓存的页面数超过 high watermark 时,
 * 批量将多余的页面归还给伙伴系统。
 */
static void free_unref_page_commit(struct zone *zone, struct per_cpu_pages *pcp, struct page *page, int migratetype, unsigned int order)
{
    int high;
    int pindex;
    bool free_high;

    /* 统计 PGFREE 事件,是释放进PCP链表中的页数. cat /proc/vmstat | grep -i PGFREE */
    __count_vm_events(PGFREE, 1 << order);
    /* 根据 migratetype 和 order 计算 PCP 列表索引, 找到要释放回的PCP中具体的空闲链表 */
    pindex = order_to_pindex(migratetype, order);
    /* 将页面加入对应 PCP 列表的头部(LIFO,热页优先复用) */
    list_add(&page->pcp_list, &pcp->lists[pindex]);
    pcp->count += 1 << order;

    /*
     * 由于存储在 PCP 上的除 THP 之外的高阶页面也会导致碎片,因此在 PCP 大量释放内存且未分配内存时,
     * 应限制存储的高阶页面数量。批量释放停止后,剩余的页面将从 vmstat 刷新上下文中移除。
     *
     * 如果最近持续在释放高 order 页面(free_factor > 0),且 order 在 1~3 之间,标记需要更积极地回收 PCP。
     * 避免 PCP 中积累过多高 order 页导致碎片化*/
    free_high = (pcp->free_factor && order && order <= PAGE_ALLOC_COSTLY_ORDER); //3

    /* 计算当前 PCP 的 high watermark,实际使用中的 high 值不是固定不变的,是变化的,pcp->high 只是最大值限制 */
    high = nr_pcp_high(pcp, zone, free_high);
    /*
     * 如果 PCP 中的页面数超过 high watermark,批量释放一部分页面回伙伴分配器。
     * 这是"懒释放"策略:不是每次都归还 buddy,而是攒到一定量再批量处理。
     */
    if (pcp->count >= high) {
        int batch = READ_ONCE(pcp->batch);
        /* 释放回伙伴系统 */
        free_pcppages_bulk(zone, nr_pcp_free(pcp, high, batch, free_high), pcp, pindex);
    }
}


2.2.1.3.1 nr_pcp_high()

/**
 * nr_pcp_high - 计算当前 PCP 最多允许能缓存多少页(high watermark)
 * @pcp:       当前 CPU 的 per_cpu_pages
 * @zone:      所属的 zone
 * @free_high: 是否因为"连续释放高阶页面"而需要积极清空
 *
 * 返回值含义:
 *   > 0: PCP 当前最多允许缓存这么多页,超过就触发归还;
 *   = 0: PCP 当前不允许缓存任何页面,任何释放都立刻归还 buddy(相当于屏蔽了 PCP), 因为 free_unref_page_commit() 中判断 pcp->batch >= 这里返回的 high 就会往伙伴系统释放 。
 *
 *  此函数在 free_unref_page_commit() 中唯一调用。传参: free_high = (pcp->free_factor && order && order <= PAGE_ALLOC_COSTLY_ORDER);
 */
static int nr_pcp_high(struct per_cpu_pages *pcp, struct zone *zone, bool free_high)
{
    int high = READ_ONCE(pcp->high);

    /*
     * 情况1: PCP 已被禁用 或在连续释放高阶页(1-3)
     * (1) high == 0 表示 PCP 已被禁用(zone_pcp_disable 设了 pcp->high=0)
     * (2) free_high == true 表示正在连续释放高阶页面,不应在 PCP 中积压.
     * 返回 0 表示 PCP 不允许缓存任何页面,任何释放都立刻归还 buddy, PCP 等于"透明"即不缓存。
     */
    if (unlikely(!high || free_high))
        return 0;

    /*
     * 情况2: zone 没有在做异步内存回收,返回正常的 high 值.
     * 这是常规路径。high 通常是几百到几千(根据 zone 大小计算)。PCP 可以自由缓存到 high 个页面才触发归还。
     */
    if (!test_bit(ZONE_RECLAIM_ACTIVE, &zone->flags))
        return high;

    /*
     * 情况3:zone 的 kswapd 正在做异步内存回收
     *
     * 回收活跃时,限制 PCP 缓存页面数量为 batch * 4(应该还可以再小点),以便让更多页面回到 buddy 的 free_area 中,
     * 这样 reclaim 释放的页面能被其他 CPU 看到和使用,而不是积压在某个 CPU 的 PCP 里"私藏"。
     *
     * 如果在回收流程中,限制可存储在 PCP 列表中的页面数量。
     */
    return min(READ_ONCE(pcp->batch) << 2, high);
}

zone->flags 中的 ZONE_RECLAIM_ACTIVE 的位置和取消位置:

kswapd
    balance_pgdat
        restart:
            set_reclaim_active
                update_reclaim_active(pgdat, highest_zoneidx, true)
                    set_bit(ZONE_RECLAIM_ACTIVE, &zone->flags);
        out:
            clear_reclaim_active
                update_reclaim_active(pgdat, highest_zoneidx, false)
                    clear_bit(ZONE_RECLAIM_ACTIVE, &zone->flags);

即 zone->flags 中的 ZONE_RECLAIM_ACTIVE 表示当前此 zone 中正在进行 kswapd 异步内存回收。


2.2.1.3.2 nr_pcp_free()

/**
 * nr_pcp_free - 计算本次应该从 PCP 归还多少页给 buddy
 * @pcp:       当前 CPU 的 per_cpu_pages
 * @high:      当前的 PCP 水位线(来自 nr_pcp_high 的返回值)
 * @batch:     pcp->batch 的值,基础归还批量,默认固定为63。
 * @free_high: 是否需要激进清空,连续释放高阶页面时会传true.
 *
 * 返回值:本次应归还的页面数
 *
 * 设计思想:
 * - 正常情况下归还 batch 个页面,PCP 中保留 batch 个供下次分配
 * - 连续释放时逐次翻倍(加速清空),避免 PCP 成为瓶颈
 * - 高阶页面激进清空时全部归还。
 *
 * 唯一调用位置 free_unref_page_commit:
 * 传参 free_high = (pcp->free_factor && order && order <= PAGE_ALLOC_COSTLY_ORDER); //3
 */
static int nr_pcp_free(struct per_cpu_pages *pcp, int high, int batch, bool free_high)
{
    int min_nr_free, max_nr_free;

    /*
     * 情况1:归还 PCP 中的全部页面
     *
     * free_high = true 表示正在连续释放 order 1~3 的页面。
     * 高阶页面占空间大,积压会加剧碎片化。全部归还 buddy,让 buddy 有机会将它们合并成更大的块。
     */
    if (unlikely(free_high))
        return pcp->count;

    /*
     * 情况2:PCP 已禁用或 boot pageset(high < batch)
     *
     * high=0 且 batch=1 的情况(zone_pcp_disable 后)。只归还 1 个,因为 PCP 此时每次只有 1 页进来就触发归还。
     */
    if (unlikely(high < batch))
        return 1;

    /*
     * 正常路径:计算归还数量
     *
     * 原则:PCP 中至少保留 batch 个页面(供下次分配用,避免频繁从 buddy 进货), 最多归还 high - batch 个.
     */
    /* Leave at least pcp->batch pages on the list */
    min_nr_free = batch;
    max_nr_free = high - batch; //可能 max_nr_free 比 min_nr_free 还小,但是不会造成逻辑异常。

    /*
     * 核心:指数加速机制
     *
     * batch 左移 free_factor 位 = batch * 2^free_factor
     *
     * 如果连续释放无分配插入:
     *     第1次超水位:归还 batch * 1  = 31    (free_factor=0)
     *     第2次超水位:归还 batch * 2  = 62    (free_factor=1)
     *     第3次超水位:归还 batch * 4  = 124 (free_factor=2)
     *     第4次超水位:归还 batch * 8  = 248 (free_factor=3)
     *     第5次超水位:归还 batch * 16 = 496 (free_factor=4)
     *     第6次超水位:归还 batch * 32 = 992 (free_factor=5,上限)
     */
    batch <<= pcp->free_factor;

    /*
     * 如果加速后的 batch 还没到最大值且 free_factor 没到上限:则增加 free_factor,下次归还更多。
     * CONFIG_PCP_BATCH_SCALE_MAX = 5,所以 free_factor 最大为 5(即翻倍 32 次)
     */
    if (batch < max_nr_free && pcp->free_factor < CONFIG_PCP_BATCH_SCALE_MAX) //5
        pcp->free_factor++;

    /*
     * 将返回值限制在 min 和 max 之间
     * 若出现 max_nr_free 比 min_nr_free,还小,clamp 实现是 min(max(val, lo), hi),即先被 lo 抬高,再被 hi 压低,
     * 当 hi < lo 时,最终结果等于 hi,也是安全的
     */
    batch = clamp(batch, min_nr_free, max_nr_free);

    return batch;
}


2.2.1.3.3 free_pcppages_bulk()

/**
 * free_pcppages_bulk - 批量将 PCP 中的页面归还给 buddy allocator
 * @count:    要归还的总页面数(以 page 为单位,不是条目数)
 * @pindex: 触发归还的那个 PCP 子列表索引(优先从它开始释放)
 *
 * 此函数持有 zone->lock 操作 buddy 的 free_area。采用 round-robin 方式从各个 PCP 子列表轮流取页面归还,
 * 避免某一种 migratetype/order 的缓存被全部清空而其他的不动。
 * 此函数是在持 zone->per_cpu_pageset->lock 锁的条件下调用的,目标是从 pcp->list[pindex] 上释放 count 个页, 
 * 若此 list 不够释放,再释放别的 list 上的页
 */
static void free_pcppages_bulk(struct zone *zone, int count, struct per_cpu_pages *pcp,    int pindex)
{
    unsigned long flags;
    /* round-robin 的下界 */
    int min_pindex = 0;
    /* round-robin 的上界 */
    int max_pindex = NR_PCP_LISTS - 1; //17-1
    unsigned int order;
    bool isolated_pageblocks;
    struct page *page;

    /*
     * 防御性检查:如果请求归还的 count 比 PCP 中实际拥有的还多,
     * 以实际拥有的为准,避免后面循环时 PCP 已空但 count 仍 > 0 导致死循环。
     */
    count = min(pcp->count, count);

    /*
     * 让 round-robin 从 pindex-1 开始。因为循环开头会先 ++pindex,这样第一次 ++pindex 后恰好等于传入的 pindex,
     * 确保触发归还的那个列表**优先**被释放。
     *
     * 为什么优先释放触发者?
     * 因为触发归还的原因是"这个 pindex 对应的列表刚刚又收到了页面导致超水位",优先释放它能最快降低 PCP 的 count。
     */
    pindex = pindex - 1;

    /* 要操作 buddy 的 free_area 链表,因此要持锁 */
    spin_lock_irqsave(&zone->lock, flags); //关中断后持 zone->lock 锁

    /*
     * 检查 zone 中是否存在 ISOLATE 类型的 pageblock。如果有,后面每个页面释放时需要重新确认其 pageblock 的
     * migratetype,因为 pageblock 类型可能在我们缓存 migratetype 之后被并发修改为 ISOLATE*/
    isolated_pageblocks = has_isolate_pageblock(zone); //zone->nr_isolate_pageblock;

    /* 主循环:归还 count 个页面给 buddy */
    while (count > 0) {
        struct list_head *list;
        int nr_pages;

        /* Round-robin 选择下一个非空的 PCP 子列表。这样各个 order 和各个 migration 的链表能均匀减少 */
        do {
            /* 让 pindex 循环 */
            if (++pindex > max_pindex)
                pindex = min_pindex;
            list = &pcp->lists[pindex];
            /* 选择一个非空的 list */
            if (!list_empty(list))
                break;

            /*
             * 当前 pindex 对应的列表为空。收缩搜索范围——如果边界处的列表持续为空,就把搜索范围缩小,
             * 避免反复扫描已知为空的列表。
             * 这是一个优化:当大部分列表都空了时,快速缩小 [min_pindex, max_pindex] 范围,减少无效遍历。
             */
            if (pindex == max_pindex)
                max_pindex--;
            if (pindex == min_pindex)
                min_pindex++;
        } while (1);

        /* 此 pindex 对应的 order 和每个页面块的页数 */
        order = pindex_to_order(pindex);
        nr_pages = 1 << order;

        /*
         * 内循环:从当前列表中连续取页面归还 buddy,直到 count 耗尽或这个列表被取空。
         * 从列表尾部取:释放到 PCP 时放头部(热页优先分配),归还 buddy 时从尾部取(冷页优先归还)
         */
        do {
            int mt;
            /* 从列表尾部取页面(FIFO,冷页优先释放) */
            page = list_last_entry(list, struct page, pcp_list);
            /* 获取这个页面缓存时记录的 migratetype */
            mt = get_pcppage_migratetype(page);

            /* must delete to avoid corrupting pcp list */
            /* 从 PCP 链表上摘下来,在持 zone->per_cpu_pageset->lock 锁的条件下走到这里,是安全的 */
            list_del(&page->pcp_list);
            count -= nr_pages;
            pcp->count -= nr_pages;

            /*
             * 重新检查页面合法性。返回 true 表示页面有问题(如 double-free),跳过不归还。若有丢弃会有带限速
             * 的报错打印的。
             */
            if (bulkfree_pcp_prepare(page))
                continue;

            /* 合法性断言:ISOLATE 类型的页面不应该出现在 PCP 中 */
            VM_BUG_ON_PAGE(is_migrate_isolate(mt), page);

            /*
              * 如果 zone 中存在 ISOLATE pageblock:
              * 页面进入 PCP 时记录的 migratetype 可能已过期——在 PCP 缓存期间,该页面所在的 pageblock 可能被
              * 并发修改为 ISOLATE(如 memory hotplug 正在隔离)。需要重新读取当前的 pageblock migratetype。
              */
            if (unlikely(isolated_pageblocks))
                mt = get_pageblock_migratetype(page);

            /*
             * 核心:将页面归还给 buddy allocator。此函数会尝试与伙伴合并,放入对应 order 的 free_list。
             * 同时 NR_FREE_PAGES 会增加。
             */
            __free_one_page(page, page_to_pfn(page), zone, order, mt, FPI_NONE);

            /* ftrace 事件:PCP drain 一个页面块 */
            trace_mm_page_pcpu_drain(page, order, mt);
            /*
             * 内循环退出条件:(1) count <= 0:已归还够了; (2) list 为空:这个子列表清空了,需要回到外层 round-robin 选下一个
             * 也即使命的操作单个列表,直到清空或还够。
             * 这会不会乒乓呢?好像可优化,比如每个列表可先释放一半,还不够每个列表再释放一半。或 根据 PCP 中每个 list 的 order
             * 决定释放比例。
             */
        } while (count > 0 && !list_empty(list));
    }

    spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 锁后开中断
}

trace打印如下,每行表示从 PCP 的一个 list 上释放回伙伴系统一个页面块。

khugepaged-69       [004] d..2. 17900.614102: mm_page_pcpu_drain: page=000000008f06b236 pfn=0x1156d2 order=0 migratetype=3
khugepaged-69       [004] d..2. 17900.614110: mm_page_pcpu_drain: page=000000002bb7288c pfn=0x16b178 order=1 migratetype=0
khugepaged-69       [004] d..2. 17900.614111: mm_page_pcpu_drain: page=00000000bc93fb5c pfn=0x147214 order=2 migratetype=0


2.2.1.3.3.1 bulkfree_pcp_prepare

这是从 PCP 释放回伙伴系统前的检测。详见后续 页面校验 相关BK。


2.2.1.3.3.2 __free_one_page

见下面伙伴系统释放小节详解。


2.2.2 __free_pages_ok()

/**
 * __free_pages_ok - 直接将高 order 页面直接归还 buddy allocator
 * @page:      要释放的页面
 * @order:     页面的 order
 * @fpi_flags: 释放标志(如 FPI_TO_TAIL 等)
 * 不经过 PCP,直接在 buddy 的 free_area 中操作。需要持有 zone->lock。
 *
 * free_the_page: (page, order, FPI_NONE) 
 */
static void __free_pages_ok(struct page *page, unsigned int order, fpi_t fpi_flags)
{
    unsigned long flags;
    int migratetype;
    unsigned long pfn = page_to_pfn(page);
    struct zone *zone = page_zone(page);
    bool skip_free_unref_page = false;

    /* 释放前的通用准备(与 free_unref_page_prepare 类似) */
    if (!free_pages_prepare(page, order, true, fpi_flags))
        return;

    /* 根据页面 PFN 对应的 pageblock 获取 migratetype */
    migratetype = get_pfnblock_migratetype(page, pfn);
    /* Android vendor hook */
    trace_android_vh_free_unref_page_bypass(page, order, migratetype, &skip_free_unref_page);
    if (skip_free_unref_page)
        return;

    spin_lock_irqsave(&zone->lock, flags); //关中断后持 zone->lock
    /*
     * 如果 zone 中存在 ISOLATE 的 pageblock,需要重新读取 migratetype,因为在获取锁之前 pageblock 
     * 的类型可能已经被并发修改。
     */
    if (unlikely(has_isolate_pageblock(zone) || is_migrate_isolate(migratetype))) {
        migratetype = get_pfnblock_migratetype(page, pfn);
    }
    /* 核心:将页面归还到 buddy free_area,并尝试合并伙伴 */
    __free_one_page(page, pfn, zone, order, migratetype, fpi_flags);
    spin_unlock_irqrestore(&zone->lock, flags); //释放 zone->lock 后开中断

    /* 无论释放进伙伴系统还是释放进PCP, 都会有这个计数 */
    __count_vm_events(PGFREE, 1 << order);
}


2.2.2.1 free_pages_prepare()

在之后的 内存准备与校验 中介绍。


2.2.2.2 __free_one_page

/**
 * __free_one_page - buddy allocator 的核心释放+合并函数
 * @page:        要释放的页面
 * @pfn:         页面的物理页帧号
 * @zone:        所属的 zone
 * @order:        当前 order
 * @migratetype: 迁移类型
 * @fpi_flags:    释放标志
 *
 * 这是 buddy system 的精髓:释放一个 2^order 的页面块时,检查它的"伙伴"(buddy)是否也空闲,
 * 如果是则合并成 2^(order+1) 的块,然后继续检查更高 order 的伙伴,直到无法再合并为止。
 *
 * Buddy 的定义:对于 PFN 为 pfn、大小为 2^order 的块,它的 buddy 的 PFN = pfn XOR (1 << order)
 * 两者合并后的 PFN = pfn AND buddy_pfn(取较小值),见下面六章节说明。 
 * 即 pfn 的 bit[order] 区反找伙伴,bit[order] 位为0的是合并后的起始 pfn.
 */
static inline void __free_one_page(struct page *page,    unsigned long pfn,
        struct zone *zone, unsigned int order, int migratetype, fpi_t fpi_flags)
{
    struct capture_control *capc = task_capc(zone);
    unsigned long buddy_pfn = 0;
    unsigned long combined_pfn;
    struct page *buddy;
    bool to_tail;
    bool bypass = false;

    /* Android vendor hook: 允许厂商完全接管释放逻辑 */
    trace_android_vh_free_one_page_bypass(page, zone, order, migratetype, (int)fpi_flags, &bypass);
    if (bypass)
        return;

    VM_BUG_ON(!zone_is_initialized(zone)); //return zone->initialized;
    /* 释放前不应残留分配时才有的标志 */
    VM_BUG_ON_PAGE(page->flags & PAGE_FLAGS_CHECK_AT_PREP, page);
    /* migratetype 不能为非法值 */
    VM_BUG_ON(migratetype == -1);

    /*
     * 更新 NR_FREE_PAGES(NR_FREE_CMA_PAGES)  统计。页面回到 buddy, 让 zone 的空闲页数增加 2^order。
     * ISOLATE 类型不计入空闲统计,因为它们被隔离了,不参与正常分配。
     */
    if (likely(!is_migrate_isolate(migratetype)))
        /* 在 NR_FREE_PAGES(NR_FREE_CMA_PAGES) 中记录这个页面块 */
        __mod_zone_freepage_state(zone, 1 << order, migratetype);

    /* 合法性检查:PFN 必须是 2^order 对齐的 */
    VM_BUG_ON_PAGE(pfn & ((1 << order) - 1), page);
    /* 页面必须在 zone 的合法范围内,默认不使能 CONFIG_DEBUG_VM, bad_range()恒返回 false,不执行 */
    VM_BUG_ON_PAGE(bad_range(zone, page), page);

    /* 核心合并循环: 从当前 order 开始,逐级向上尝试合并伙伴,直到 MAX_ORDER-1 或被 compaction 捕获。*/
    while (order < MAX_ORDER - 1) {
        /*
         * Compaction 捕获检查。如果当前任务在做 direct compaction,且页面刚好合并到了目标 order,"截胡"这
         * 个页面 —— 不放入 free_list,直接交给 compaction 分配者。
         */
        if (compaction_capture(capc, page, order, migratetype)) {
            /* 捕获成功!扣除 free 计数(因为页面不放入 free_list),再从 NR_FREE_PAGES(NR_FREE_CMA_PAGES) 中移除这个页面块 */
            __mod_zone_freepage_state(zone, -(1 << order), migratetype);
            /* 直接返回,不继续合并,不放入 free_list */
            return;
        }

        /*
         * 寻找伙伴: buddy_pfn = pfn ^ (1 << order), 即 bit[order] 位取反得到伙伴的 pfn.
         * 然后验证 buddy 是否:
         * 在同一个 zone 内、确实空闲(PageBuddy 标志被设置)、order 匹配(buddy_order == order). 任一条件不满足返回 NULL --> 停止合并。
         */
        buddy = find_buddy_page_pfn(page, pfn, order, &buddy_pfn);
        if (!buddy)
            goto done_merging;

        /*
         * 跨 pageblock 合并限制: 当 order >= pageblock_order 时,合并将跨越 pageblock 边界。
         *
         * 不同 migratetype 的 pageblock 之间合并有限制:ISOLATE 和 HIGHATOMIC 属于"不可合并"类型。如果两个 pageblock
         * 一个是 ISOLATE/HIGHATOMIC 另一个不是, 则放弃合并,原因是合并后统计会出错(一个块跨两种计费方式),且 ISOLATE
         * pageblock 被隔离是有意为之的。
         *
         * MOVABLE/UNMOVABLE/RECLAIMABLE 三者之间是允许跨 pageblock 合并的。
         *
         * 默认配置下这个恒不成立。因为上面有判断 order < 10 才会进来,这里不可能大于或等于10。这是采用小 pageblock
         * 的条件下才成立。
         */
        if (unlikely(order >= pageblock_order)) {
            /*
             * We want to prevent merge between freepages on pageblock
             * without fallbacks and normal pageblock. Without this,
             * pageblock isolation could cause incorrect freepage or CMA
             * accounting or HIGHATOMIC accounting.
             */
            /* 获取伙伴所在pageblock 的迁移类型 */
            int buddy_mt = get_pageblock_migratetype(buddy);

            if (migratetype != buddy_mt
                    && (!migratetype_is_mergeable(migratetype) || !migratetype_is_mergeable(buddy_mt))) //return mt <= MIGRATE_RECLAIMABLE;
                goto done_merging;
        }

        /*
         * 合并操作:将 buddy 从当前 order 的 free_list 中删除, 计算合并后的 PFN(两者中较小的那个), order + 1,
         * 继续尝试更高级别的合并。
         * debug 模式下可能将某些页面标记为 guard(不可访问哨兵页)。合并时需要先清除 guard 状态。
         * 默认不使能 CONFIG_DEBUG_PAGEALLOC,  page_is_guard 恒返回 false, 执行 else 分支。
         */
        if (page_is_guard(buddy))
            clear_page_guard(zone, buddy, order, migratetype); //TODO: 为啥是 guard 就不需要从buddy上摘下来了呢?
        else
            del_page_from_free_list(buddy, zone, order);
        /* 合并后的起始 pfn */
        combined_pfn = buddy_pfn & pfn;
        /* page 指向合并后块的首页, delta 小于 0 或等于 0, page 往前指 */
        page = page + (combined_pfn - pfn);
        /* 以页面块起始为 pfn, 大小为 order+1 继续遍历 */
        pfn = combined_pfn;
        order++;
    }

done_merging:
    /* 合并循环逻辑执行完毕,准备放回伙伴系统。设置最终合并后的 order 到 page->private 并对 page->flags 设置 PG_buddy 标志 */
    set_buddy_order(page, order);

    /*
     * 决定将合并后的块添加到 free_list 的头部还是尾部:
     * - FPI_TO_TAIL: 强制放尾部(如启动时初始化内存)。尾部,页面停留更久,有更多时间等伙伴空闲后继续合并。头部,页面会被更快分
     *                配出去(LIFO 热页优先)
     * - shuffle: 随机化(CONFIG_SHUFFLE_PAGE_ALLOCATOR)
     * - buddy_merge_likely: 如果预测未来还能继续合并,放尾部(给其他页面先被分配出去的机会,让本块有时间等到伙伴空闲后继续合并)
     *
     * 看起来空闲链表上的页面快并不是按 pfn 顺序排列的。
     */
    if (fpi_flags & FPI_TO_TAIL)
        /* FPI_TO_TAIL:强制放尾部。场景:内存上线、隔离放回 —— 这些是"冷"页面。启动时初始化内存也是 */
        to_tail = true;
    else if (is_shuffle_order(order)) //return order >= 10
        /* CONFIG_SHUFFLE_PAGE_ALLOCATOR:对高阶页面(order >= 10)做随机化,随机放链表头或链表尾。安全加固:防止攻击者预测页面分配顺序 */
        to_tail = shuffle_pick_tail();
    else
        /*
         * 默认启发式:
         * (1) 对于 9 10 两个 order,恒返回false, 表示放在空闲链表头。
         * (2) 其它order 检查"如果此块的下一级伙伴也空闲了,能否继续合并?" 即:查看合并后块的 order+1 伙伴是否已经在 free_list 上。
         *   如果 likely --> 放尾部(给它时间等待进一步合并,减少碎片);
         *   如果 unlikely --> 放头部(不太可能再合并了,让它快速被使用);
         */
        to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order);

    /* 将页面块加入 zone->free_area[order] 的对应 migratetype 链表 */
    if (to_tail)
        add_to_free_list_tail(page, zone, order, migratetype);
    else
        add_to_free_list(page, zone, order, migratetype);

    /* 通知 Page Reporting 子系统. 虚拟化场景下告知 Host 有新的空闲高阶块。FPI_SKIP_REPORT_NOTIFY 跳过通知(PCP 归还、隔离放回等不算"新释放") */
    if (!(fpi_flags & FPI_SKIP_REPORT_NOTIFY))
        page_reporting_notify_free(order);
}


2.2.2.2.1 task_capc()

/**
 * task_capc - 获取当前任务的 capture_control(如果正在做 compaction)
 * @zone:  当前正在这个 zone 上释放页面。
 *
 * 返回值:
 *   非 NULL: 当前任务正在对 @zone 做 compaction,且还没捕获到页面
 *   NULL: 不需要捕获(不在 compaction 中、或已经捕获到了、或 zone 不匹配)
 */
static inline struct capture_control *task_capc(struct zone *zone)
{
    struct capture_control *capc = current->capture_control;

    return unlikely(capc) &&              /* 1. 当前任务在做 compaction? */
        !(current->flags & PF_KTHREAD) && /* 2. 不是内核线程(kcompactd 后台回收不捕获页面) */
        !capc->page &&                    /* 3. 还没有捕获到页面?(只捕获一个) */
        capc->cc->zone == zone ? capc : NULL; /* 4. 释放的页面 zone 就是目标 zone ? */
}


2.2.2.2.2 compaction_capture()

/**
 * compaction_capture - 在 buddy 合并过程中,尝试捕获合适大小的页面块
 * @capc:       当前任务的捕获控制(可能为 NULL)
 * @page:       当前正在合并过程中的页面(已达到某个 order)
 * @order:      该页面块当前的 order
 * @migratetype: 该页面块的迁移类型
 *
 * 返回值:
 *   true:  捕获成功!页面不放入 free_list,直接交给 compaction 调用者
 *   false: 不捕获,继续正常的 buddy 合并流程
 *
 * 调用位置:__free_one_page 的合并循环中,每次合并后都检查一次。
 */
static inline bool
compaction_capture(struct capture_control *capc, struct page *page, int order, int migratetype)
{
    /*
     * 条件 1:capc 为空(当前任务没在做 compaction) 或 order 不匹配(还没合并到目标大小)
     *
     * compaction 的目标 order 存在 capc->cc->order 中。只有页面合并到恰好等于目标 order 时才捕获。
     * 如果比目标小 --> 继续合并;如果比目标大 --> 不会发生(合并到目标就捕获了)
     */
    if (!capc || order != capc->cc->order)
        return false;

    /*
     * 条件 2:不捕获 CMA 或 ISOLATE 类型的页面
     *
     * CMA 页面属于设备预留区域,不应被普通分配"截胡"。ISOLATE 页面正在被隔离(可能是 hotplug),不应被捕获。
     */
    /* Do not accidentally pollute CMA or isolated regions*/
    if (is_migrate_cma(migratetype) || is_migrate_isolate(migratetype))
        return false;

    /*
     * 条件 3:小于 pageblock_order 的 MOVABLE 页面不捕获
     *
     * 原因是防止"污染":一个 MOVABLE 类型的 pageblock 中释放出来的页面块,如果被一个 UNMOVABLE 的分配请求捕获走
     * 就会导致 MOVABLE pageblock 中出现不可迁移的页面,影响后续 compaction 的迁移能力。
     *
     * 但如果 order >= pageblock_order(整个 pageblock),那捕获者会拥有整个 pageblock 的控制权,migratetype 可以
     * 改变,所以允许捕获。
     */
    if (order < pageblock_order && migratetype == MIGRATE_MOVABLE)
        return false;

    /* 所有条件满足 --> 捕获!将页面指针存入 capc->page。*/
    capc->page = page;

    return true;
}

看起来能被 compaction 捕获的概率也不高,因为系统最多的是 MOVABLE 类型的页面。


2.2.2.2.3 find_buddy_page_pfn()

/*
 * find_buddy_page_pfn: 找到当前块的伙伴 buddy PFN = pfn ^ (1 << order), 即 pfn 的 bit[order] 位取反
 *
 * 检查伙伴是否:
 * (1) 在同一个 zone 内
 * (2) 确实是空闲的(PageBuddy 标志被设置)
 * (3) order 匹配
 * 如果任一条件不满足,返回 NULL,停止合并。
 */
static inline struct page *find_buddy_page_pfn(struct page *page, unsigned long pfn, unsigned int order, unsigned long *buddy_pfn)
{
    unsigned long __buddy_pfn = __find_buddy_pfn(pfn, order); //return page_pfn ^ (1 << order);
    struct page *buddy;

    /* 指向找到的 buddy page */
    buddy = page + (__buddy_pfn - pfn);
    /* 通过参数返回 buddy pfn */
    if (buddy_pfn)
        *buddy_pfn = __buddy_pfn;

    if (page_is_buddy(page, buddy, order))
        return buddy;

    return NULL;
}


2.2.2.2.3.1 page_is_buddy()

/*
 * 此函数检查页面是否空闲,以及是否是伙伴页面。如果满足以下条件,我们可以合并页面及其伙伴页面:
 * (a) 伙伴页面不在空洞中(调用前请检查!)&&
 * (b) 伙伴页面在伙伴系统中(是空闲页面) &&
 * (c) 页面及其伙伴页面的 order 相同 &&
 * (d) 页面及其伙伴页面位于同一 zone。
 *
 * 为了记录页面是否在伙伴系统中,我们设置了 PageBuddy。PageBuddy 的设置、清除和测试由 zone->lock 序列化。
 *
 * 为了记录页面的 order,我们使用 page_private(page)。
 */
static inline bool page_is_buddy(struct page *page, struct page *buddy, unsigned int order)
{
    /* 默认不使能 CONFIG_DEBUG_PAGEALLOC page_is_guard 恒返回false. 要求伙伴页必须是 buddy 系统中的空闲页 */
    if (!page_is_guard(buddy) && !PageBuddy(buddy))
        return false;

    /* 两者的 order 相同 */
    if (buddy_order(buddy) != order) //page->private
        return false;

    /* page->flags 有存储 zone 信息,两者的 zone 必须得是同一个 */
    if (page_zone_id(page) != page_zone_id(buddy))
        return false;

    /* 伙伴的 page->_refcount 必须得等于 0, 若是空闲在伙伴系统中就是等于 0 的 */
    VM_BUG_ON_PAGE(page_count(buddy) != 0, buddy);

    return true;
}


2.2.2.2.4 buddy_merge_likely()

/* 检查下一级是否有Buddy可以继续合并 */
static inline bool
buddy_merge_likely(unsigned long pfn, unsigned long buddy_pfn, struct page *page, unsigned int order)
{
    unsigned long higher_page_pfn;
    struct page *higher_page;

    /* 对合并后的 9 10 两个 order,恒返回false, 表示放在空闲链表头 */
    if (order >= MAX_ORDER - 2) //9
        return false;

    /* 若继续向上层合并,合并后在更大页块的起始 pfn */
    higher_page_pfn = buddy_pfn & pfn;
    /* 若继续向上层合并,合并后在更大页块的起始 page 指针 */
    higher_page = page + (higher_page_pfn - pfn);

    /* 计算更上以及当前在伙伴系统中能否合并 */
    return find_buddy_page_pfn(higher_page, higher_page_pfn, order + 1, NULL) != NULL;
}


六、伙伴确认原理

Buddy在合并前需要确认伙伴,这里介绍其定位与合并的 XOR/AND 原理。

1. 核心规则

buddy_pfn   = pfn XOR (1 << order) //找伙伴
combined_pfn = pfn AND buddy_pfn   //合并后的起始 PFN


2. 直观理解:对齐与配对

Buddy system 的本质是 按对齐地址配对。

对于 order=N 的块:

- 块大小 = 2^N 页
- 块的起始 PFN 必须是 2^N 对齐的(即 PFN 的低 N 位为 0)
- 合并后的 order=N+1 块,起始 PFN 必须是 2^(N+1) 对齐的

两个相邻的 order=N 块,如果它们合并后能形成一个 2^(N+1) 对齐的块,它们就是彼此的 buddy。

3. 举例

(1) order = 0 (单页,最简单情况)

order = 0, 块大小 = 1 页, 1 << order = 1

假设 pfn = 6 (二进制: 110), 其 buddy_pfn = 6 XOR 1 = 7 (二进制: 111)

内存布局:

PFN:  0  1  2  3  4  5 [6] [7]  8  9 ...
                       ^^^  ^^^
                      块A  buddy

合并后: combined_pfn = 6 AND 7 = 6 (二进制: 110 & 111 = 110) --> 合并成 PFN=6 开始的 order=1 块(2 页)

假设 pfn = 7: buddy_pfn = 7 XOR 1 = 6 --> 反过来也能找到对方!
combined_pfn = 7 AND 6 = 6 --> 合并结果相同:PFN=6 开始


(2) order = 2 (4 页块)

order = 2, 块大小 = 4 页, 1 << order = 4 (二进制: 100)

假设 pfn = 8 (二进制: 1000), buddy_pfn = 8 XOR 4 = 12 (二进制: 1100)

内存布局:

PFN:  0  1  2  3  4  5  6  7 [ 8  9 10 11] [12 13 14 15] 16 ...
                             ^^^^^^^^^^^^  ^^^^^^^^^^^^
                                块A          buddy B

合并后: combined_pfn = 8 AND 12 = 8 (二进制: 1000 & 1100 = 1000) --> 合并成 PFN=8 开始的 order=3 块 (8 页)

反过来验证:

假设 pfn = 12 (二进制: 1100), buddy_pfn = 12 XOR 4 = 8 (二进制: 1000)
combined_pfn = 12 AND 8 = 8 --> 同样的结果


(3) 连续合并过程(order 0 --> 1 --> 2 --> 3)

释放 PFN=5 这个页面 (order=0)

第 1 轮: order=0, pfn=5 (101), buddy_pfn = 5 XOR 1 = 4 (100), 检查 PFN=4 是否空闲 --> 假设空闲  ==> combined_pfn = 5 AND 4 = 4 (101 & 100 = 100), 合并成 [4,5], order=1, pfn=4

第 2 轮: order=1, pfn=4 (100), buddy_pfn = 4 XOR 2 = 6 (110), 检查 PFN=6~7 是否空闲 --> 假设空闲 ==> combined_pfn = 4 AND 6 = 4 (100 & 110 = 100), 合并成 [4,5,6,7], order=2, pfn=4

第 3 轮: order=2, pfn=4 (100), buddy_pfn = 4 XOR 4 = 0 (000), 检查 PFN=0~3 是否空闲 --> 假设空闲 ==> combined_pfn = 4 AND 0 = 0 (100 & 000 = 000), 合并成 [0,1,2,3,4,5,6,7], order=3, pfn=0

第 4 轮: order=3, pfn=0 (0000), buddy_pfn = 0 XOR 8 = 8 (1000), 检查 PFN=8~15 是否空闲 --> 假设不空闲 ==> 停止合并,将 [0..7] 作为 order=3 块放入 free_area[3]


4. 什么 XOR 能找到 buddy

关键在于 对齐约束。看 order=2 的情况:

order=2 的所有合法块(必须 4 对齐):

[0,1,2,3]  [4,5,6,7]  [8,9,10,11]  [12,13,14,15]  [16...]
 PFN=0      PFN=4      PFN=8        PFN=12         PFN=16

order=3 的所有合法块(必须 8 对齐):

[0..7]  [8..15]  [16..23] ...
 PFN=0   PFN=8    PFN=16

要合并成 order=3,两个 order=2 块必须是同一个 order=3 块的"上半/下半"。

观察 PFN 的二进制表示(以 order=2 为例):

PFN     二进制    bit[2]  -->  buddy pair
 0       000       0     ─┐  合并成 order=3 块 (PFN=0)
 4       100       1     ─┘
 8      1000       0     ─┐  合并成 order=3 块 (PFN=8)
12      1100       1     ─┘

bit[order] 这一位决定了这个块是 buddy pair 中的"前半"还是"后半"。XOR (1<<order) 翻转这一位,就得到了配对的另一半


5. 为什么 AND 能得到合并后的起始

pfn       = 12 = 1100
buddy_pfn =  8 = 1000

pfn AND buddy_pfn = 1000 = 8 <-- 就是两者中较小的那个, 因为 buddy pair 中一定有一个 bit[order]=0,另一个 bit[order]=1:

AND 操作会让 bit[order] = 0, 更高位两者相同(同属一个更大的块),AND 后不变, 低位都是 0(对齐约束),AND 后仍为 0, 所以 AND
结果就是 bit[order]=0 的那个,即两者中较小的 PFN,也就是合并后 2^(order+1) 块的起始地址。


6. 用二进制一图总结

order=2, 1<<order = 100(二进制)

    pfn     =  0100  (PFN=4)
    buddy   =  0000  (PFN=0)   <-- XOR 100 翻转 bit[2]
    combined=  0000  (PFN=0)   <-- AND 取合并起始

    pfn     =  1100  (PFN=12)
    buddy   =  1000  (PFN=8)   <-- XOR 100 翻转 bit[2]
    combined=  1000  (PFN=8)   <-- AND 取合并起始

规律:
  XOR (1<<order) → 翻转第 order 位 --> 找到另一半
  AND            → 清零第 order 位 --> 得到对齐的起始

这就是 buddy system 优雅的地方——用两条位运算指令就完成了伙伴定位和合并地址计算,不需要任何查表或遍历。


六、相关调试


七、相关实验


八、总结

1. 高频访问的核心子系统的常规优化策略: 每CPU的小缓存, 实现为 Per-CPU锁的 try lock。每次分配一批而不是一个,每次释放一批而不是一个。

 

posted on 2026-08-22 22:19  Hello-World3  阅读(3)  评论(0)    收藏  举报

导航