内存管理-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) 收藏 举报
浙公网安备 33010602011771号