内存管理-78-PCP-Per-CPU页缓存-6.1

基于 Linux-6.1.115

PCP列表的完整生命周期全景图:

┌──────────────────────────────────────────────────────────────────┐
│                    PCP 列表生命周期                                │
├───────────┬──────────────────────────────────────────────────────┤
│  创建      │  zone_pcp_init --> setup_zone_pageset                │
├───────────┼──────────────────────────────────────────────────────┤
│  进货(补货)│  rmqueue_bulk (从 buddy 批量取页面填入 PCP)             │
├───────────┼──────────────────────────────────────────────────────┤
│  零售(分配)│  __rmqueue_pcplist (从 PCP 取页面给调用者)              │
├───────────┼──────────────────────────────────────────────────────┤
│  退件(释放)│  free_unref_page_commit (释放的页面放入 PCP)            │
├───────────┼──────────────────────────────────────────────────────┤
│  退货      │  free_pcppages_bulk (PCP 超水位时批量归还 buddy)       │
├───────────┼──────────────────────────────────────────────────────┤
│  主动清空  │  drain_pages_zone / drain_local_pages / drain_all    │
├───────────┼──────────────────────────────────────────────────────┤
│  禁用      │  zone_pcp_disable (水位设0 + drain_all)              │
├───────────┼──────────────────────────────────────────────────────┤
│  恢复      │  zone_pcp_enable (恢复水位)                           │
├───────────┼──────────────────────────────────────────────────────┤
│  销毁      │  zone_pcp_reset (free_percpu)                        │
└───────────┴──────────────────────────────────────────────────────┘


完整行为汇总表:

-------------------------------------------------------------------------------------------------------
行为                函数                        触发场景                    操作 zone->lock
-------------------------------------------------------------------------------------------------------
创建(早期)          zone_pcp_init                内核启动                    否
创建(正式)          setup_zone_pageset           per-cpu 就绪后              否
进货                rmqueue_bulk                PCP list 为空时分配           ✅ 持锁批量取
零售                __rmqueue_pcplist           alloc_pages order≤3         否(PCP 有自己的锁)
退件                free_unref_page_commit      __free_pages order≤3        否
退货                free_pcppages_bulk          PCP count ≥ high            ✅ 持锁批量还
部分清空             drain_zone_pages            kswapd 平衡                 ✅
全量清空(本CPU)      drain_local_pages           direct reclaim              ✅
全量清空(全CPU)      drain_all_pages             CMA/hotplug/drop_caches     ✅
禁用                zone_pcp_disable            CMA 分配/热拔除前             ✅(drain all)
恢复                zone_pcp_enable             CMA 完成/热拔除完成            否
动态调参             zone_pcp_update             hotplug/CPU 变化             否
销毁                zone_pcp_reset              zone 下线                    否
-------------------------------------------------------------------------------------------------------


一、简介

PCP是每CPU的页缓存,用于降低伙伴系统 zone->lock 锁竞争。


二、数据结构

1. struct per_cpu_pages

struct per_cpu_pages {
    spinlock_t lock;
    int count;
    int high;
    int batch;
    short free_factor;
    struct list_head lists[NR_PCP_LISTS];
} ____cacheline_aligned_in_smp;

使用此结构描述 Per-CPU 的 PCP 页,内嵌到 zone->per_cpu_pageset 中,每个 zone 在每个 CPU 上都有一个此结构。

1.1 lock

在操作 PCP 相关成员之前,会先关迁移,然后 try lock 持此锁(不关中断),若持成功,则从 PCP 上分配或释放回 PCP 链表,持失败则直接从 Buddy 分配或释放回 Buddy 链表。其次 pcp->lock 和 zone->lock 的关系是 pcp->lock 固定是外层锁,zone->lock 固定是内层锁,详见 alloc_pages 和 free_pages

1.2 count

PCP 缓存中的总页数(按高阶等价计).

1.3 high

触发释放的高水位,一般是一个固定值,除非 Mem/CPU online 或 offline 了,或用户手动设置了 percpu_pagelist_high_fraction sysctl 接口。

1.4 batch

补货/释放的批量单位。固定值,一般是 63,若zone管理的内存很小,也可能小于 63。

1.5 free_factor

free_factor 是"连续释放加速因子" —— 当 PCP 连续释放页面而没有分配时,每次归还 buddy 的批量数呈指数增长(每次归还 Buddy batch << free_factor 个页面)。它的作用是:如果系统一直在释放而不分配,说明有大量内存正在回流,应该加速将 PCP 中的页面归还 Buddy,避免 PCP 积压过多页面。

正常稳态下:分配和释放交替进行,free_factor 很小或为 0,每次 PCP 超水位时归还 batch 个页面。
非常规状态(比如内存回收过程):连续大量释放,无分配,free_factor 逐渐增大(每次+1), 每次归还 batch << free_factor 个页面(63-->126-->...-->2016),PCP 被快速清空,内存快速回到 Buddy.

(1) free_factor 增加位置

/* 计算本次应该从 PCP 归还多少页给 buddy, 释放页进 PCP 路径时调用 */
static int nr_pcp_free(struct per_cpu_pages *pcp, int high, int batch, bool free_high)
{
    batch <<= pcp->free_factor;
    /* 如果还没到上限,下次再加速 */
    if (batch < max_nr_free && pcp->free_factor < CONFIG_PCP_BATCH_SCALE_MAX) //5
        pcp->free_factor++;
    return batch;
}

(2) free_factor 减少位置

rmqueue_pcplist
    pcp->free_factor >>= 1;

free_factor 在释放路径中只是加加,分配路径却是成指数减少,下降速度更多,大多时候应该是 0.

(3) 状态转换:

              释放页面(连续)                           分配页面
                    │                                    │
                    ▼                                    ▼
   free_factor:  0 --> 1 --> 2 --> 3 --> 4 --> 5    5 --> 2 --> 1 --> 0
                    (每次释放超水位时 +1)           (每次分配时 >>= 1)

1.6 lists[]

(1) Linux-6.1 内核中的 PCP 空闲链表共 NR_PCP_LISTS(17) 条

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
#define NR_PCP_THP 1
#else
#define NR_PCP_THP 0
#endif
/* pcp list 上除了 2^0 还有 2^1, 2^2, 2^3 大小的页块 */
#define NR_LOWORDER_PCP_LISTS (MIGRATE_PCPTYPES * (PAGE_ALLOC_COSTLY_ORDER + 1)) //4 * (3+1) = 16
#define NR_PCP_LISTS (NR_LOWORDER_PCP_LISTS + NR_PCP_THP) //16 + 1 = 17

PCP 中空闲链表维护的 order 有 2^0--2^3 + 2^10(THP), 详见 pcp_allowed_order(), 每个 order 又有 4 种迁移类型,一共 17 条。

(2) lists 数组按 migratetype 和 order 的组织方式如下,详见 order_to_pindex():

lists[0]    <-- order-0, MIGRATE_UNMOVABLE
lists[1]    <-- order-0, MIGRATE_MOVABLE
lists[2]    <-- order-0, MIGRATE_RECLAIMABLE
lists[3]    <-- order-0, MIGRATE_CMA
...
lists[12]   <-- order-3, MIGRATE_UNMOVABLE
lists[13]   <-- order-3, MIGRATE_MOVABLE
lists[14]   <-- order-3, MIGRATE_RECLAIMABLE
lists[15]   <-- order-3, MIGRATE_CMA
...
lists[16]  <-- THP 专用(如果启用 CONFIG_TRANSPARENT_HUGEPAGE)

相关接口:

/* 根据迁移类型和 order 定位在 lists[] 数组中的下标 */
unsigned int order_to_pindex(int migratetype, int order)
/* 提取下标 pindex 对应的 order 值  */
int pindex_to_order(unsigned int pindex)
/* 判断是不是PCP链表维护的order,即0-3或10(THP) */
bool pcp_allowed_order(unsigned int order)

注: Linux-5.4 内核的 PCP 的空闲链表中还只有 order-0 的页面,即 MIGRATE_PCPTYPES 条空闲链表。

PCP 中只维护了 UNMOVABLE、MOVABLE、RECLAIMABLE、CMA 这 4 种迁移类型的空闲链表,其它迁移类型并没有维护,维护的迁移类型少于 Buddy


2. struct zone

struct zone {
    ...
    struct per_cpu_pages __percpu *per_cpu_pageset;
    int pageset_high;
    int pageset_batch;
    ...
    spinlock_t    lock;
}

相关成员介绍:

2.1 per_cpu_pageset

这就是每CPU的 PCP 页缓存结构。

2.2 pageset_high

缓存 pcp->high 的值。

2.3 pageset_batch

缓存 pcp->batch 的值。

2.4 lock

相对于 pcp->lock 来说,zone->lock 是内锁,注意持锁次序。


三、实现逻辑

1. 创建与初始化

1.1 启动早期——boot pageset
/* zone_pcp_init() — 内核启动早期,per-cpu 子系统还没准备好 */
static __meminit void zone_pcp_init(struct zone *zone)
{
    zone->per_cpu_pageset = &boot_pageset;    //使用静态的 boot pageset
    zone->pageset_high = BOOT_PAGESET_HIGH;   //0 high = 0(不缓存)
    zone->pageset_batch = BOOT_PAGESET_BATCH; //1 batch = 1
}

boot pageset 是一个极简的 PCP,基本不做缓存(high=0),所有分配直接走 buddy。

1.2 启动后期——真正的 per-cpu pageset
/* setup_per_cpu_pageset() --> setup_zone_pageset() — per-cpu 子系统就绪后 */
void __init setup_zone_pageset(struct zone *zone)
{
    zone->per_cpu_pageset = alloc_percpu(struct per_cpu_pages);  //为每个 CPU 分配
    for_each_possible_cpu(cpu) {
        pcp = per_cpu_ptr(zone->per_cpu_pageset, cpu);
        per_cpu_pages_init(pcp, pzstats);  //初始化链表、high/batch/free_factor
    }
    zone_set_pageset_high_and_batch(zone, 0);  //根据 zone 大小计算 high 和 batch
}

/* setup_zone_pageset: (zone, 0) */
static void zone_set_pageset_high_and_batch(struct zone *zone, int cpu_online)
{
    int new_high, new_batch;

    new_batch = max(1, zone_batchsize(zone)); //返回 zone->batch 值
    new_high = zone_highsize(zone, new_batch, cpu_online); //返回 zone->high 值

    if (zone->pageset_high == new_high && zone->pageset_batch == new_batch)
        return;

    /* high 和 batch 在 zone 结构上保存一份 */
    zone->pageset_high = new_high;
    zone->pageset_batch = new_batch;

    /* 将 batch 和 high 写到此 zone 的每 CPU 的 pcp->batch 和 pcp->high 中 */
    __zone_set_pageset_high_and_batch(zone, new_high, new_batch);
}

/* 返回 zone->batch 的值 */
static int zone_batchsize(struct zone *zone) //page_alloc.c
{
    int batch;

    /* min(zone->managed_pages >> 10, 256) 一般就是 256 了。下面还会除以4 ==> 设置这里可调整 batch 的大小 */
    batch = min(zone_managed_pages(zone) >> 10, SZ_1M / PAGE_SIZE);
    /* 此时 batch = 64 */
    batch /= 4;        /* We effectively *= 4 below */
    if (batch < 1)
        batch = 1;

    /* 64 - 1 = 63 和 cat /proc/zone 看到的 batch 值一致 */
    batch = rounddown_pow_of_two(batch + batch/2) - 1;

    return batch;
}

/* setup_zone_pageset: (zone, 63, 0) */
static int zone_highsize(struct zone *zone, int batch, int cpu_online)
{
    int high;
    int nr_split_cpus;
    unsigned long total_pages;

    /* cat /proc/sys/vm/percpu_pagelist_high_fraction 是 0 */
    if (!percpu_pagelist_high_fraction) {
        /* 默认情况下,pcp 的 high 值基于 zone 的 low 水线,因此如果它们已满,则不会过早启动后台回收。*/
        total_pages = low_wmark_pages(zone);
    } else {
        /* 如果配置了 percpu_pagelist_high_fraction,则 high 基于区域中托管页面的百分比。改这个变量(非0)也会影响 high 值 */
        total_pages = zone_managed_pages(zone) / percpu_pagelist_high_fraction;
    }

    nr_split_cpus = cpumask_weight(cpumask_of_node(zone_to_nid(zone))) + cpu_online;
    if (!nr_split_cpus)
        nr_split_cpus = num_online_cpus();
    /* total_pages 是所有CPU的PCP页面数之和,再除以 8 得到每CPU的 pcp->high */
    high = total_pages / nr_split_cpus;

    /* 确保 high 至少是 batch 的 4 倍 */
    high = max(high, batch << 2);

    /*
     * echo m > /proc/sysrq-trigger 得到 low:80228kB, 计算 high = 80228kB/4kB/8=2507 和 cat /proc/zoneinfo 看到的 high 值一致
     * 改 low 会水线会影响到 pcp->high 值
     */
    return high;
}

(1) 小结:

pcp->batch: 一般取值是 63, SZ_1M / PAGE_SIZE / 4 - 1 = 63。

pcp->high: 取值有两种途径:
a. 默认方法是 <min 水线> / <num online cpu>, 实测 offline cpu 会导致 pcp->high 变大。
b. 通过 percpu_pagelist_high_fraction sysctl 接口设置允许存在于 PCP 中的最大比例, echo 8 即最大允许 zone 1/8 的页面存在于 PCP 缓存中,假设有 8 个 online CPU,则每个 pcp->high 占 1/64 的 zone 页面。

(2) percpu_pagelist_high_fraction

执行下面命令,看 high 值的却会变:

echo 100 > /proc/sys/vm/percpu_pagelist_high_fraction;
cat /proc/zoneinfo

vm.rst 中对 percpu_pagelist_high_fraction 作用的说明:

这是每个 zone 中可以存储到 PCP 中的页面比例。这是一个上限值,会根据 online 的 CPU 的数量再次进行划分。此值的最小值为 8 (echo 8 等效于 pcp->high = zone->managed_pages / 64),这意味着我们不允许每个 zone 中超过 1/8 的页面存储在 PCP 列表中。此条目仅更改热 PCP 的值。用户可以指定一个数字,例如 100,以将每个区域的 1/100 分配给每个 CPU 页列表。

每个 PCP 列表的 batch 值与 high 比例值无关,因此分配延迟不受影响。

初始值为零。内核使用此值根据 zone 的 low 水线和本地 online CPU 的数量来设置高 pcp->high。如果用户向此 sysctl 写入 0,则会恢复到此默认行为。

1.3 总体调用路径
    start_kernel //init/main.c
        setup_arch //arm64/kernel/setup.c
            bootmem_init //mm/init.c
                zone_sizes_init //mm/init.c
                    free_area_init //page_alloc.c
                        free_area_init_node //page_alloc.c
                            free_area_init_core //page_alloc.c
                                zone_init_internals //page_alloc.c
                                    zone_pcp_init(zone); //【】PCP第一阶段初始化在这
        ...
        build_all_zonelists(NULL);
        page_alloc_init();
        mm_init();
        kmem_cache_init_late();
        ...
        setup_per_cpu_pageset //page_alloc.c
            setup_zone_pageset(zone); //【】PCP第二阶段初始化在这

第一阶段时内存还没初始化,所以线用一个全局变量做PCP。


2. 进货(补货)—— 从 Buddy 批量填充 PCP

/*
 * 触发条件:从 PCP 分配时发现对应迁移类型的 PCP list 为空
 * 路径:rmqueue_pcplist --> __rmqueue_pcplist --> get_populated_pcp_list
 */
static struct list_head *get_populated_pcp_list(struct zone *zone,
        unsigned int order, struct per_cpu_pages *pcp, int migratetype, unsigned int alloc_flags)
{
    struct list_head *list = &pcp->lists[order_to_pindex(migratetype, order)];

    if (list_empty(list)) {
        int batch = READ_ONCE(pcp->batch);
        /* 从 buddy 批量取 batch 个页面放入 PCP list 的尾部(一般PCP list为空后才进货,这时放头部还是尾部没区别了) */
        alloced = rmqueue_bulk(zone, order, batch, list, migratetype, alloc_flags);
        pcp->count += alloced << order;
    }
    return list;
}

rmqueue_bulk 内部持有 zone->lock,从 buddy 的 free_area 中取 batch 个页面(通过 __rmqueue 或 __rmqueue_cma),逐个挂到 PCP list 上。


3. 零售(分配) —— 从 PCP 取页面给调用者

/* 路径:rmqueue --> rmqueue_pcplist --> __rmqueue_pcplist */
static inline struct page *__rmqueue_pcplist(...)
{
    /* CMA 优先 */
    if (migratetype == MIGRATE_MOVABLE && alloc_flags & ALLOC_CMA)
        list = get_populated_pcp_list(zone, order, pcp, CMA_type, ...);
    /* PCP上是递进关系 */
    if (list == NULL)
        list = get_populated_pcp_list(zone, order, pcp, migratetype, ...);

    /*list 头部取一个(LIFO,热页优先) */
    page = list_first_entry(list, struct page, pcp_list);
    list_del(&page->pcp_list);
    pcp->count -= 1 << order;

    return page;
}

同时分配时会衰减 free_factor:

//rmqueue_pcplist 中:
pcp->free_factor >>= 1;    //有分配说明不再是纯释放,加速因子衰减


4. 退件(释放) —— 释放的页面放入 PCP

/* 路径:__free_pages --> free_the_page --> free_unref_page --> free_unref_page_commit */
static void free_unref_page_commit(struct zone *zone, struct per_cpu_pages *pcp, struct page *page, int migratetype, unsigned int order)
{
    int pindex = order_to_pindex(migratetype, order);

    /* 放入 PCP list 头部(LIFO,最近释放的页面下次优先被分配) */
    list_add(&page->pcp_list, &pcp->lists[pindex]);
    pcp->count += 1 << order;

    /* 检查是否超水位,超了就触发溢出回流,进行退货 */
    high = nr_pcp_high(pcp, zone, free_high);
    if (pcp->count >= high) {
        free_pcppages_bulk(zone, nr_pcp_free(...), pcp, pindex);
    }
}


5. 退货 —— PCP 超水位时批量归还 Buddy

/*
 * 触发条件:pcp->count >= high
 * 路径:free_unref_page_commit --> free_pcppages_bulk
 */
static void free_pcppages_bulk(struct zone *zone, int count, struct per_cpu_pages *pcp, int pindex)
{
    spin_lock_irqsave(&zone->lock, flags);

    while (count > 0) {
        /* round-robin 从各个 PCP 子列表尾部取(FIFO,冷页优先归还) */
        page = list_last_entry(list, ...);
        list_del(&page->pcp_list);
        pcp->count -= nr_pages;

        /* 归还给 buddy(调用 __free_one_page 做合并) */
        __free_one_page(page, pfn, zone, order, mt, FPI_NONE);
    }

    spin_unlock_irqrestore(&zone->lock, flags);
}

归还数量由 nr_pcp_free() 决定:

(1) 正常情况:归还 batch 个;
(2) 连续释放(free_factor > 0):归还 batch << free_factor 个(指数加速);
(3) free_high = true:归还全部(PCP 中高阶页面全清空);


6. 主动清空(Drain)—— 内核主动将 PCP 归还 Buddy

6.1 清空当前 CPU 的 PCP
/* drain_local_pages() — 清空本 CPU 的 PCP */
void drain_local_pages(struct zone *zone)
{
    int cpu = smp_processor_id();
    if (zone)
        drain_pages_zone(cpu, zone);   //清指定 zone
    else
        drain_pages(cpu);              //清所有 zone
}

触发场景:
(1) __alloc_pages_slowpath 中 direct reclaim 前;
(2) 内存低压时想让 PCP 中的页面回到 buddy 供其他 CPU 使用;


6.2 清空指定 CPU 的指定 zone
static void drain_pages_zone(unsigned int cpu, struct zone *zone)
{
    struct per_cpu_pages *pcp = per_cpu_ptr(zone->per_cpu_pageset, cpu);

    do {
        spin_lock(&pcp->lock);
        count = pcp->count;
        if (count) {
            to_drain = min(count, pcp->batch << CONFIG_PCP_BATCH_SCALE_MAX);
            free_pcppages_bulk(zone, to_drain, pcp, 0);
            count -= to_drain;
        }
        spin_unlock(&pcp->lock);
    } while (count);   //循环直到全部清空
}

6.3 清空所有 CPU 的 PCP
/* drain_all_pages() / __drain_all_pages() — 清空所有 CPU 的 PCP */
static void __drain_all_pages(struct zone *zone, bool force_all_cpus)
{
    /* 只对有非空 PCP 的 CPU 执行 drain(优化) */
    for_each_online_cpu(cpu) {
        pcp = per_cpu_ptr(zone->per_cpu_pageset, cpu);
        if (pcp->count)
            cpumask_set_cpu(cpu, &cpus_with_pcps);
    }

    /* 在目标 CPU 上执行 drain_pages_zone */
    on_each_cpu_mask(&cpus_with_pcps, drain_local_pages_wq, zone, 1);
}

触发场景:
(1) alloc_contig_range(CMA 分配前需要确保 PCP 中没有缓存目标区域的页面);
(2) memory hotplug offline(下线内存前清空 PCP);
(3) /proc/sys/vm/drop_caches 写入时;


6.4 指定 zone 的部分清空
/* drain_zone_pages() — 只清 batch 个,不全清(kswapd 路径用) */
void drain_zone_pages(struct zone *zone, struct per_cpu_pages *pcp)
{
    to_drain = min(pcp->count, batch);
    free_pcppages_bulk(zone, to_drain, pcp, 0);
}

触发场景: kswapd 做内存平衡时,轻量级地从各 zone PCP 中释放一些页面。


7. 禁用 PCP

/* zone_pcp_disable() — CMA/hotplug 场景临时禁用 PCP */
void zone_pcp_disable(struct zone *zone)
{
    mutex_lock(&pcp_batch_high_lock);
    __zone_set_pageset_high_and_batch(zone, 0, 1);   //high=0 --> 任何页面都"超水位"立即归还
    __drain_all_pages(zone, true);                   //清空所有 CPU 的 PCP
}

效果: high=0 意味着后续释放页面到 PCP 时立刻触发归还(等于绕过 PCP)。分配时 PCP 永远为空,也直接走 buddy。

触发场景:
(1) alloc_contig_range(CMA 分配)前必须禁用 PCP;
(2) memory hotplug offline 前;


8. 恢复 PCP

/* zone_pcp_enable() — 与 zone_pcp_disable 配对 */
void zone_pcp_enable(struct zone *zone)
{
    __zone_set_pageset_high_and_batch(zone, zone->pageset_high, zone->pageset_batch);
    mutex_unlock(&pcp_batch_high_lock);
}

恢复正常的 high/batch 值,PCP 重新开始缓存页面。


9. 销毁

/* zone_pcp_reset() — 内存热拔除后销毁 PCP */
void zone_pcp_reset(struct zone *zone)
{
    if (zone->per_cpu_pageset != &boot_pageset) {
        for_each_online_cpu(cpu) {
            pzstats = per_cpu_ptr(zone->per_cpu_zonestats, cpu);
            drain_zonestat(zone, pzstats); //刷新统计
        }
        free_percpu(zone->per_cpu_pageset);    //释放 per-cpu 内存
        zone->per_cpu_pageset = &boot_pageset; //退回 boot pageset
    }
}


10. 动态调整水位

/* zone_pcp_update() — zone 管理页面数变化时重算 high/batch */
static void zone_pcp_update(struct zone *zone, int cpu_online)
{
    zone_set_pageset_high_and_batch(zone, cpu_online);
}

触发场景:
(1) 内存热插拔;
(2) CPU 上下线(多一个 CPU 意味着 PCP 总量需要调整);


四、相关调试

1. debug文件

1.1 /proc/zoneinfo
  pagesets
    cpu: X
              count: 1418  //pcp->count
              high:  2866  //pcp->high
              batch: 63    //pcp->batch
  vm stats threshold: 54

会打印每个 zone 的在每个 CPU 上的 PCP 信息。


1.2 /proc/sys/vm/percpu_pagelist_high_fraction

用于控制 pcp->high 值,最小是8(等效于PCP中最多只能有 zone->managed_pages / 8 / <nr_cpus> 个页面), 详见上面对 percpu_pagelist_high_fraction 的说明。


2. ftrace

(1) trace_mm_page_pcpu_drain

PCP 页批量释放给伙伴系统时打印,路径:

free_pcppages_bulk //释放页面到PCP, 然后PCP超水位后释放回Buddy
    trace_mm_page_pcpu_drain(page, order, mt)

打印内容:

# P=/sys/kernel/tracing; echo mm_page_pcpu_drain > $P/set_event; > $P/trace; echo 1 > $P/tracing_on; cat $P/trace_pipe
  HwBinder:950_8-4671    [000] d..3.  3584.923905: mm_page_pcpu_drain: page=00000000ea00eb24 pfn=0x11577b order=0 migratetype=3
  HwBinder:950_8-4671    [000] d..3.  3584.923906: mm_page_pcpu_drain: page=00000000519587a1 pfn=0x25cef6 order=1 migratetype=0
  HwBinder:950_8-4671    [000] d..3.  3584.924054: mm_page_pcpu_drain: page=00000000b30f5486 pfn=0x375494 order=2 migratetype=2
  HwBinder:950_8-4671    [000] d..3.  3584.924056: mm_page_pcpu_drain: page=00000000c315d62a pfn=0x40a320 order=3 migratetype=0


五、总结

1. 内核热路径核心子系统优化常用套路: 每CPU缓存 + trylock.

2. 将 pcp->high 设置为 0 等效于关闭 pcp.

3. 用户从 pcp->list[X] 分配和释放页面操作的都是头部,热页优先,PCP 向 Buddy 进货和退货都是放在 PCP->list[X] 的尾部,冷页优先。


六、补充

1. PCP中过多的页会影响 min/low/high 水线的判断吗

会。

水线判断用的是 zone_page_state(zone, NR_FREE_PAGES),这个值统计的只是 Buddy 上的,不包含 PCP 中缓存的页面。真实空闲 = buddy free_area 上的页面 + PCP 中缓存的页面。

所以当大量页面在 PCP 中时:NR_FREE_PAGES(buddy 上的)看起来偏低, 可能提前触发 kswapd 或 direct reclaim,误判为"内存紧张"。分配物理页时的水线检查,是在尝试从 PCP/Buddy 中分配物理页之前进行的,若水线不满足,提前就拦截住了。

内核为了降低这个影响,默认限制 PCP 中最大缓存的页面数不得超过 low 水线,见 zone_highsize(), 但是你也可以配置 percpu_pagelist_high_fraction 不遵守这个限制 。

 

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

导航