一、前言
承接上一篇,上一章我们介绍了:Memory Reclaim 整体架构
但是:有一个问题没有回答。
Memory Reclaim 到底是谁触发的?
这一篇则回答:
Linux内核在何时决定回收?
graph TB
subgraph "用户空间 / 系统调用层"
APP[应用程序<br/>malloc/mmap/etc]
end
subgraph "页面分配入口 (mm/page_alloc.c)"
alloc_pages["alloc_pages()"]
__alloc_pages["__alloc_pages()<br/>gfp.h:529"]
get_page["get_page_from_freelist()<br/>快速路径 - 从 freelist 获取页面"]
end
subgraph "慢速路径 (mm/page_alloc.c)"
slowpath["__alloc_pages_slowpath()<br/>page_alloc.c:4988"]
wake_all["wake_all_kswapds()<br/>page_alloc.c:4751"]
direct_reclaim["__alloc_pages_direct_reclaim()<br/>page_alloc.c:4721"]
perform_reclaim["__perform_reclaim()<br/>page_alloc.c:4693"]
retry["should_reclaim_retry()<br/>page_alloc.c:4861"]
end
subgraph "Direct Reclaim 直接回收 (mm/vmscan.c)"
try_free["try_to_free_pages()<br/>vmscan.c:3760"]
do_try_free["do_try_to_free_pages()<br/>vmscan.c:3543"]
throttle["throttle_direct_reclaim()<br/>节流 - 等待 kswapd"]
shrink_zones_dr["shrink_zones()<br/>vmscan.c:3430"]
end
subgraph "kswapd 后台回收 (mm/vmscan.c)"
kswapd["kswapd()<br/>vmscan.c:4462<br/>内核线程 - 每个 NUMA node"]
balance_pgdat["balance_pgdat()<br/>vmscan.c:4119"]
kswapd_shrink["kswapd_shrink_node()<br/>vmscan.c:4029"]
prepare_sleep["prepare_kswapd_sleep()<br/>vmscan.c:3990"]
allow_dr["allow_direct_reclaim()<br/>vmscan.c:3631"]
end
subgraph "Node/Zone 回收层 (mm/vmscan.c)"
shrink_node["shrink_node()<br/>vmscan.c:3174"]
shrink_node_memcgs["shrink_node_memcgs()<br/>vmscan.c:3112"]
end
subgraph "LRU 列表操作 (mm/vmscan.c)"
shrink_lruvec["shrink_lruvec()<br/>vmscan.c:2914"]
shrink_list["shrink_list()<br/>vmscan.c:2618"]
shrink_active["shrink_active_list()<br/>vmscan.c:2445"]
shrink_inactive["shrink_inactive_list()<br/>vmscan.c:2320"]
end
subgraph "Slab 回收 (mm/vmscan.c)"
shrink_slab["shrink_slab()<br/>vmscan.c:908"]
do_shrink_slab["do_shrink_slab()"]
shrink_slab_memcg["shrink_slab_memcg()"]
end
subgraph "页面回收细节"
isolate["isolate_lru_pages()<br/>从 LRU 隔离页面"]
page_ref["page_referenced()<br/>检查页面引用 (rmap.c)"]
add_swap["add_to_swap()<br/>swap_state.c:184"]
try_unmap["try_to_unmap()<br/>取消页面映射 (rmap.c)"]
try_release["try_to_release_page()<br/>释放 pagecache (filemap.c)"]
free_pages["free_unref_page_list()<br/>释放页面"]
end
subgraph "Memcg 控制 (mm/memcontrol.c)"
memcg_iter["mem_cgroup_iter()"]
memcg_calc["mem_cgroup_calculate_protection()"]
memcg_soft["mem_cgroup_soft_limit_reclaim()"]
reclaim_high["reclaim_high()"]
handle_over["mem_cgroup_handle_over_high()"]
end
subgraph "其他辅助"
compaction["compaction 内存压缩<br/>mm/compaction.c"]
vmpressure["vmpressure 虚拟内存压力<br/>mm/vmpressure.c"]
psi["psi_memstall_enter/leave<br/>PSI 压力监控"]
end
%% 主要调用流
APP --> alloc_pages
alloc_pages --> __alloc_pages
__alloc_pages --> get_page
get_page -->|成功| APP
get_page -->|失败 - 内存不足| slowpath
slowpath --> wake_all
slowpath --> direct_reclaim
slowpath --> retry
wake_all --> kswapd
direct_reclaim --> perform_reclaim
perform_reclaim --> try_free
perform_reclaim --> psi
try_free --> throttle
try_free --> do_try_free
do_try_free --> shrink_zones_dr
kswapd --> balance_pgdat
balance_pgdat --> kswapd_shrink
balance_pgdat --> prepare_sleep
balance_pgdat --> allow_dr
balance_pgdat --> compaction
kswapd_shrink --> shrink_node
shrink_zones_dr --> shrink_node
shrink_node --> shrink_node_memcgs
shrink_node_memcgs --> memcg_iter
shrink_node_memcgs --> memcg_calc
shrink_node_memcgs --> shrink_lruvec
shrink_node_memcgs --> shrink_slab
shrink_lruvec --> shrink_list
shrink_list --> shrink_active
shrink_list --> shrink_inactive
shrink_active --> isolate
shrink_active --> page_ref
shrink_active --> free_pages
shrink_inactive --> isolate
shrink_inactive --> add_swap
shrink_inactive --> try_unmap
shrink_inactive --> try_release
shrink_inactive --> free_pages
shrink_slab --> do_shrink_slab
shrink_slab --> shrink_slab_memcg
%% Memcg 回收路径
handle_over --> reclaim_high
reclaim_high --> shrink_zones_dr
memcg_soft --> do_try_free
retry -->|重试| get_page
retry -->|失败| slowpath
retry -->|OOM| APP
%% 样式
classDef entry fill:#e1f5fe
classDef slowpath fill:#fff3e0
classDef direct fill:#fce4ec
classDef kswapd fill:#e8f5e9
classDef node fill:#f3e5f5
classDef lru fill:#fff8e1
classDef slab fill:#e0f2f1
classDef memcg fill:#f1f8e9
class alloc_pages,__alloc_pages,get_page entry
class slowpath,wake_all,direct_reclaim,perform_reclaim,retry slowpath
class try_free,do_try_free,throttle,shrink_zones_dr direct
class kswapd,balance_pgdat,kswapd_shrink,prepare_sleep,allow_dr kswapd
class shrink_node,shrink_node_memcgs node
class shrink_lruvec,shrink_list,shrink_active,shrink_inactive lru
class shrink_slab,do_shrink_slab,shrink_slab_memcg slab
class memcg_iter,memcg_calc,memcg_soft,reclaim_high,handle_over memcg
__alloc_pages 正是分析Linux内核内存回收流程最核心、最合适的入口函数。
它是内核内存分配请求的“总枢纽”,几乎所有物理页面的分配请求最终都会调用它。从它开始分析,我们可以清晰地看到从“尝试快速分配”到“触发慢速路径回收”的完整决策链条。
__alloc_pages()
│
├─ get_page_from_freelist() ← 快路径,本函数
│ ├─ 水位检查通过 → 直接分配(无需回收)
│ ├─ 水位不足 → node_reclaim() 同步回收(小规模)
│ └─ 全部失败 → return NULL
│
└─ __alloc_pages_slowpath() ← 慢路径(返回 NULL 后进入)
├─ direct reclaim(直接回收)
├─ direct compaction(直接压缩)
├─ OOM killer
从内存回收(reclaim)的角度来看,get_page_from_freelist 是分配路径上的第一道回收触发点。它在"快速分配"和"慢速回收"之间起到了关键的桥梁作用。
从内存回收角度看快速分配( get_page_from_freelist )
三级水位体系
// include/linux/mmzone.h
#define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost)
#define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost)
#define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost)
#define wmark_pages(z, i) (z->_watermark[i] + z->watermark_boost)
每个 zone 维护三个水位线:
get_page_from_freelist 使用的水位由 alloc_flags & ALLOC_WMARK_MASK 决定:
// page_alloc.c:4209
mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
///返回true,表示满足最低水位,且满足分配要求
///条件分支内容,为处理分配失败
if (!zone_watermark_fast(zone, order, mark,
ac->highest_zoneidx, alloc_flags,
gfp_mask)) {
//...
普通分配 → 使用 WMARK_LOW(kswapd 被唤醒后,分配者等待 kswapd 回收到低水位以上)
紧急分配(ALLOC_HARDER)→ 使用 WMARK_MIN 甚至更低
/*
* 检测判断zone是否大于最低水位,且根据order判断是否有足够大空闲内存
* 水位值在初始化时,根据空闲页面设定,也可以在节点/proc/sys/vm/mim_free_kbytes设置,kswapd内核线程也会用到
*
* z:检测是否满足请求的zone
* order:分配2^order个物理页面
* mark:要测试的水位标准;
* highest_zoneidx:首选zone的编号
* alloc_flags:属性标志位
* fgp_mask:属性屏蔽位
* */
static inline bool zone_watermark_fast(struct zone *z, unsigned int order,
unsigned long mark, int highest_zoneidx,
unsigned int alloc_flags, gfp_t gfp_mask)
{
long free_pages;
///获取zone空闲页面数量
free_pages = zone_page_state(z, NR_FREE_PAGES);
/*
* Fast check for order-0 only. If this fails then the reserves
* need to be calculated.
*/
///针对申请单个page,做快速优化
//lowmem_reserve是每个zone预留的内存,为防止高端zone在内存不足时,过度使用低端zone内存资源
if (!order) {
long fast_free;
fast_free = free_pages;
fast_free -= __zone_watermark_unusable_free(z, 0, alloc_flags);
if (fast_free > mark + z->lowmem_reserve[highest_zoneidx])
return true;
}
///进一步检查
if (__zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags,
free_pages))
return true;
/*
* Ignore watermark boosting for GFP_ATOMIC order-0 allocations
* when checking the min watermark. The min watermark is the
* point where boosting is ignored so that kswapd is woken up
* when below the low watermark.
*/
///针对申请单个页面,进一步检查, 忽略临时水位
if (unlikely(!order && (gfp_mask & __GFP_ATOMIC) && z->watermark_boost
&& ((alloc_flags & ALLOC_WMARK_MASK) == WMARK_MIN))) {
mark = z->_watermark[WMARK_MIN];
return __zone_watermark_ok(z, order, mark, highest_zoneidx,
alloc_flags, free_pages);
}
return false;
}
水位检查的降级策略(__zone_watermark_ok)
// page_alloc.c:3918
bool __zone_watermark_ok(struct zone *z, unsigned int order,
unsigned long mark, ...)
{
long min = mark;
// ① ALLOC_HIGH(GFP_HIGHUSER 等)→ 水位降低 50%
if (alloc_flags & ALLOC_HIGH)
min -= min / 2;
// ② ALLOC_HARDER(紧急分配)→ 水位再降低 25%
if (unlikely(alloc_harder)) {
if (alloc_flags & ALLOC_OOM) // OOM 受害者
min -= min / 2; // 总共降低 75%
else
min -= min / 4; // 总共降低 62.5%
}
// ③ 基本检查:空闲页面 > min + lowmem_reserve
if (free_pages <= min + z->lowmem_reserve[highest_zoneidx])
return false;
// ④ 高阶分配:遍历 free_area 检查是否有足够大的连续块
for (o = order; o < MAX_ORDER; o++) {
// 检查 MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RECLAIMABLE
// 以及 CMA, HIGHATOMIC 区域
}
}
设计思想:越是紧急的分配,水位要求越低——宁可让系统内存更紧张,也要保证关键分配成功。
get_page_from_freelist 中的三条回收路径
路径1: zone 快速水位检查失败 → 跳过该 zone
// page_alloc.c:4213
if (!zone_watermark_fast(zone, order, mark, ...)) {
// 水位不满足,进入降级逻辑
}
zone_watermark_fast 是一个快速预检:
order-0:直接比较 free_pages 和 mark + lowmem_reserve,避免进入完整的 __zone_watermark_ok
高阶:委托给 __zone_watermark_ok 做完整检查
如果快速检查失败,有以下几种处理:
路径 2:ALLOC_NO_WATERMARKS —— 跳过水位检查
// page_alloc.c:4230
if (alloc_flags & ALLOC_NO_WATERMARKS)
goto try_this_zone; // 强制分配,不检查水位
用于 __GFP_MEMALLOC 等紧急场景(如 swap IO、网络收包),从系统预留内存中分配。
路径 3:node_reclaim() —— 同步节点回收(核心回收路径)
// page_alloc.c:4237-4257
if (!node_reclaim_enabled() ||
!zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
continue; // 不允许回收,跳到下一个 zone
ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
switch (ret) {
case NODE_RECLAIM_NOSCAN: // 不能扫描(如不允许阻塞)
continue;
case NODE_RECLAIM_FULL: // 扫描了但没有可回收的
continue;
default:
// 回收后再次检查水位
if (zone_watermark_ok(zone, order, mark, ...))
goto try_this_zone; // 水位满足,尝试分配
continue;
}
触发条件:
zone_reclaim_mode 启用(/proc/sys/vm/zone_reclaim_mode,默认关闭)
当前 zone 允许回收(zone_allows_reclaim)
分配可以阻塞(gfpflags_allow_blocking)
node_reclaim 的内部逻辑
// vmscan.c:4817
int node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask, unsigned int order)
{
// ① 快速判断:没有可回收的页面就直接返回
if (node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages &&
node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <= pgdat->min_slab_pages)
return NODE_RECLAIM_FULL; // 没有可回收的
// ② 不允许阻塞的分配不能回收
if (!gfpflags_allow_blocking(gfp_mask) || (current->flags & PF_MEMALLOC))
return NODE_RECLAIM_NOSCAN;
// ③ 只在本地节点或无 CPU 的节点上回收
if (node_state(pgdat->node_id, N_CPU) && pgdat->node_id != numa_node_id())
return NODE_RECLAIM_NOSCAN;
// ④ 加锁防止并发回收
if (test_and_set_bit(PGDAT_RECLAIM_LOCKED, &pgdat->flags))
return NODE_RECLAIM_NOSCAN;
// ⑤ 实际回收
ret = __node_reclaim(pgdat, gfp_mask, order);
...
}
__node_reclaim 的回收策略:
// vmscan.c:4763
static int __node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask, unsigned int order)
{
struct scan_control sc = {
.nr_to_reclaim = max(nr_pages, SWAP_CLUSTER_MAX), // 至少回收这么多页
.priority = NODE_RECLAIM_PRIORITY, // 初始扫描优先级(较低,扫描量少)
.may_writepage = !!(node_reclaim_mode & RECLAIM_WRITE), // 是否写脏页
.may_unmap = !!(node_reclaim_mode & RECLAIM_UNMAP), // 是否 unmap
.may_swap = 1, // 允许 swap
};
// 从优先级 12 开始递减扫描,直到回收足够页面
do {
shrink_node(pgdat, &sc); // 核心:扫描节点的 LRU 链表
} while (sc.nr_reclaimed < nr_pages && --sc.priority >= 0);
return sc.nr_reclaimed >= nr_pages;
}
回收目标:unmapped file-backed pages(无映射的文件页)和 slab 可回收页面。这些页面回收代价低(不需要写回磁盘或
unmap)。
分配成功后的回收触发:rmqueue 中的 kswapd 唤醒
即使分配成功,rmqueue 中也有回收相关的逻辑
// page_alloc.c:3803 (rmqueue 函数末尾)
if (test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags)) {
clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags);
wakeup_kswapd(zone, 0, 0, zone_idx(zone));
}
ZONE_BOOSTED_WATERMARK 标志:当伙伴系统从备份链表(fallback migratetype)借用页面时设置,表明发生了外碎片化。此时唤醒 kswapd,让它提前回收内存,为后续的大阶分配做准备。
这是一种预防性回收——分配成功了,但系统已经开始借用其他迁移类型的页面,说明内存紧张的苗头已经出现。
快速路径下的回收决策流程图
get_page_from_freelist()
│
├─ 遍历 zonelist 中的每个 zone
│ │
│ ├─ 水位检查 (zone_watermark_fast)
│ │ │
│ │ ├─ ✅ 通过 → try_this_zone → rmqueue() 分配
│ │ │ └─ 分配成功后检查 ZONE_BOOSTED_WATERMARK
│ │ │ └─ 有标志 → wakeup_kswapd() 预防性回收
│ │ │
│ │ └─ ❌ 失败
│ │ │
│ │ ├─ ALLOC_NO_WATERMARKS?→ 强制 try_this_zone
│ │ │
│ │ ├─ deferred_pages?→ 扩展 zone 容量后 try_this_zone
│ │ │
│ │ └─ zone_reclaim_mode 启用?
│ │ │
│ │ ├─ 不启用 → continue(跳到下一个 zone)
│ │ │
│ │ └─ 启用 → node_reclaim()
│ │ │
│ │ ├─ 无可回收 → NODE_RECLAIM_FULL → continue
│ │ ├─ 不能扫描 → NODE_RECLAIM_NOSCAN → continue
│ │ └─ 回收成功 → zone_watermark_ok 再检查
│ │ ├─ ✅ → try_this_zone
│ │ └─ ❌ → continue
│ │
│ └─ rmqueue() 失败 → 继续遍历下一个 zone
│
├─ 全部 zone 遍历完,且有 ALLOC_NOFRAGMENT
│ └─ 关闭碎片化避免,retry(最后尝试)
│
└─ return NULL → 进入 __alloc_pages_slowpath()
├─ direct_reclaim(直接回收,更激进)
├─ direct_compaction(直接压缩)
├─ __alloc_pages_may_oom(OOM killer)
└─ ...
-get_page_from_freelist 内存回收策略.drawio.png
关键设计总结
从回收视角看,get_page_from_freelist 的核心哲学是:能不回收就不回收(水位够就不触发),能轻量回收就不重量回收(node_reclaim 而非 direct reclaim),能本地回收就不跨节点(zone_reclaim_mode)。
从内存回收角度看慢速分配 ( __alloc_pages_slowpath )
当 get_page_from_freelist(快路径)返回 NULL 时进入。从内存回收视角看,这里集中了内核所有重量级回收手段。
慢路径下的回收策略
路径1: 调整分配标志 + 唤醒 kswapd
// ① 降低水位要求:从 WMARK_LOW → WMARK_MIN
alloc_flags = gfp_to_alloc_flags(gfp_mask);
// ② 重新选择首选 zone
ac->preferred_zoneref = first_zones_zonelist(...);
// ③ 唤醒所有相关节点的 kswapd 线程
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
// ④ 用新的低水位再次尝试分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
设计思想:快路径用 WMARK_LOW,失败后慢路径降级到 WMARK_MIN——降低 50% 的水位要求,同时唤醒 kswapd 后台回收。这是异步回收 + 放宽条件的组合策略。
阶段 2:直接压缩(Direct Compaction)—— 高阶分配优先
if (can_direct_reclaim &&
(costly_order || (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
&& !gfp_pfmemalloc_allowed(gfp_mask)) {
page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
INIT_COMPACT_PRIORITY, &compact_result);
if (page)
goto got_pg;
}
// page_alloc.c:4993
bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
关键标志:
GFP_DIRECT_RECLAIM:允许直接回收(GFP_KERNEL 有,GFP_ATOMIC 没有)
GFP_NORETRY:分配失败不重试
GFP_NOFAIL:必须分配成功,永不放弃
GFP_RETRY_MAYFAIL:可以重试,但最终允许失败
触发条件:
允许直接回收
高阶分配(order > PAGE_ALLOC_COSTLY_ORDER)或不可移动的高阶分配
不是紧急分配(不能访问系统预留内存)
为什么先压缩后回收? 对于高阶分配,失败的原因往往不是内存总量不足,而是外碎片化——内存碎片导致没有连续的大块。压缩(compaction)通过迁移页面来合并碎片,比重回收费用更低。
__alloc_pages_direct_compact 内部流程:
// page_alloc.c:4457
static struct page *__alloc_pages_direct_compact(...)
{
// ① 只处理高阶分配
if (!order) return NULL;
// ② 执行压缩:try_to_compact_pages()
*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac, prio, &page);
// ③ 压缩过程中可能直接捕获到页面
if (page) {
prep_new_page(page, order, gfp_mask, alloc_flags);
return page;
}
// ④ 压缩完成后再次从 freelist 分配
if (!page)
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
return page;
}
内存规整本章节不过多叙述,待后面内存规整章节详细描述。
阶段 3:使用预留内存 + 再次尝试分配
// 检查是否可以使用系统预留内存
reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
if (reserve_flags)
alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, reserve_flags);
// 如果有预留内存权限,忽略 cpuset 限制
if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
ac->nodemask = NULL;
ac->preferred_zoneref = first_zones_zonelist(...);
}
// 再次尝试分配(可能已经使用预留内存)
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
__gfp_pfmemalloc_flags 返回值:
阶段 4:直接回收(Direct Reclaim)—— 核心回收路径
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
&did_some_progress);
这是最重量级的同步回收。调用链:
__alloc_pages_direct_reclaim()
│
├─ __perform_reclaim()
│ ├─ cpuset_memory_pressure_bump() // 记录内存压力
│ ├─ psi_memstall_enter() // PSI 压力统计
│ ├─ fs_reclaim_acquire() // 锁依赖检查
│ ├─ memalloc_noreclaim_save() // 设置 NORECLAIM 防止递归
│ │
│ ├─ try_to_free_pages() // ★ 核心回收函数
│ │ ├─ shrink_zones()
│ │ │ └─ shrink_node()
│ │ │ ├─ shrink_lruvec() // 扫描 LRU 链表
│ │ │ │ ├─ shrink_active_list() // 活性链表 → 不活性链表
│ │ │ │ └─ shrink_inactive_list() // 不活性链表 → 回收
│ │ │ │ ├─ shrink_page_list()
│ │ │ │ │ ├─ pageout() // 脏页写回
│ │ │ │ │ └─ free_page() // 回收干净页
│ │ │ │ └─ ...
│ │ │ └─ shrink_slab() // 回收 slab 缓存
│ │ └─ ...
│ │
│ ├─ memalloc_noreclaim_restore()
│ └─ psi_memstall_leave()
│
├─ 回收成功?→ get_page_from_freelist() 再次分配
│
└─ 分配仍失败?
├─ unreserve_highatomic_pageblock() // 释放 HIGHATOMIC 预留
├─ drain_all_pages() // 排空所有 PCP 缓存
└─ get_page_from_freelist() 再次尝试
关键设计:
// page_alloc.c:4722
static inline struct page *
__alloc_pages_direct_reclaim(...)
{
// ① 执行回收
*did_some_progress = __perform_reclaim(gfp_mask, order, ac);
if (!(*did_some_progress))
return NULL; // 没有回收到任何页面,直接失败
retry:
// ② 回收后再次分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
// ③ 分配失败的补救措施
if (!page && !drained) {
unreserve_highatomic_pageblock(ac, false); // 释放紧急原子预留
drain_all_pages(NULL); // 排空 PCP 缓存,释放碎片化的页面
drained = true;
goto retry;
}
return page;
}
drain_all_pages 的作用:每个 CPU 都有 per-cpu 页面缓存(per_cpu_pages),这些缓存中的页面不会出现在 zone 的空闲链表中。回收后可能有页面积压在 PCP 缓存中,排空它们可以释放出碎片化的页面,配合压缩使用。
PS: 不过多赘述 __perform_reclaim 的内部流程,之后的文章会叙述。
阶段 5:再次直接压缩
page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
compact_priority, &compact_result);
直接回收释放了页面后,再次尝试压缩——回收提供了更多空闲页面,压缩成功率更高。
重试决策
这是慢路径中最复杂的决策逻辑
// 1. __GFP_NORETRY → 不重试
if (gfp_mask & __GFP_NORETRY)
goto nopage;
// 2. 高阶分配 + 非 RETRY_MAYFAIL → 不重试
if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
goto nopage;
// 3. 是否值得重试回收?
if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
did_some_progress > 0, &no_progress_loops))
goto retry;
// 4. 是否值得重试压缩?
if (did_some_progress > 0 &&
should_compact_retry(ac, order, alloc_flags,
compact_result, &compact_priority,
&compaction_retries))
goto retry;
// 5. cpuset 变化检查
if (check_retry_cpuset(cpuset_mems_cookie, ac))
goto retry_cpuset;
should_reclaim_retry 的核心逻辑(4862-4951):
static inline bool should_reclaim_retry(...)
{
// 1 计数器管理
if (did_some_progress && order <= PAGE_ALLOC_COSTLY_ORDER)
*no_progress_loops = 0; // 有进展,重置计数
else
(*no_progress_loops)++; // 无进展,计数增加
// 2 超过最大重试次数(16次)→ 放弃
if (*no_progress_loops > MAX_RECLAIM_RETRIES) {
return unreserve_highatomic_pageblock(ac, true); // 最后尝试释放预留
}
// 3 关键判断:如果回收所有可回收页面,水位能否满足?
for_each_zone_zonelist_nodemask(zone, z, ...) {
available = reclaimable = zone_reclaimable_pages(zone);
available += zone_page_state_snapshot(zone, NR_FREE_PAGES);
// 假设回收了所有可回收页面,水位是否满足?
wmark = __zone_watermark_ok(zone, order, min_wmark,
ac->highest_zoneidx, alloc_flags, available);
if (wmark) {
// 水位能满足 → 值得继续回收
// 但如果没进展且有大量脏页在写回,等待 IO
if (!did_some_progress) {
write_pending = zone_page_state_snapshot(zone, NR_ZONE_WRITE_PENDING);
if (2 * write_pending > reclaimable) {
congestion_wait(BLK_RW_ASYNC, HZ/10); // 等待 IO 完成
return true;
}
}
ret = true;
goto out;
}
}
// 所有 zone 的可回收页面加起来都不够 → 不值得重试
return ret;
}
核心判断:不是看"回收了多少",而是看"如果把所有可回收页面都回收掉,能否满足水位"。如果理论上都不够,就不再浪费时间重试。
阶段 7:OOM Killer —— 最后的手段
// 所有回收手段都失败,启动 OOM Killer
page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
__alloc_pages_may_oom 的保护逻辑:
static inline struct page *
__alloc_pages_may_oom(gfp_t gfp_mask, unsigned int order,
const struct alloc_context *ac, unsigned long *did_some_progress)
{
*did_some_progress = 0;
// ① 获取 oom_lock,防止并发 OOM
if (!mutex_trylock(&oom_lock)) {
*did_some_progress = 1; // 别人在处理 OOM,视为有进展
return NULL;
}
// ② 再做一次高水位分配(可能有并发 OOM 释放了内存)
page = get_page_from_freelist((gfp_mask | __GFP_HARDWALL) & ~__GFP_DIRECT_RECLAIM,
order, ALLOC_WMARK_HIGH|ALLOC_CPUSET, ac);
if (page) goto out;
// ③ 跳过 OOM 的条件(保护机制)
if (current->flags & PF_DUMPCORE) goto out; // coredump 不杀进程
if (order > PAGE_ALLOC_COSTLY_ORDER) goto out; // 高阶分配不杀进程
if (gfp_mask & (__GFP_RETRY_MAYFAIL | __GFP_THISNODE)) goto out; // 可失败的分配
if (ac->highest_zoneidx < ZONE_NORMAL) goto out; // lowmem 区域
if (pm_suspended_storage()) goto out; // 存储挂起
// ④ 执行 OOM Killer
if (out_of_memory(&oc) || WARN_ON_ONCE(gfp_mask & __GFP_NOFAIL)) {
*did_some_progress = 1;
// __GFP_NOFAIL 的分配,使用预留内存
if (gfp_mask & __GFP_NOFAIL)
page = __alloc_pages_cpuset_fallback(gfp_mask, order,
ALLOC_NO_WATERMARKS, ac);
}
}
OOM 后的重试:
// OOM killer 成功杀死了进程 → 重试
if (did_some_progress) {
no_progress_loops = 0;
goto retry;
}
阶段 8:__GFP_NOFAIL 的死循环保底
if (gfp_mask & __GFP_NOFAIL) {
// 降低水位,忽略 cpuset,使用预留内存
page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
if (page) goto got_pg;
cond_resched();
goto retry; // 永不放弃,循环重试
}
慢路径下的流程图
__alloc_pages_slowpath()
│
├─ 阶段1: 降低水位(WMARK_MIN) + 唤醒kswapd + 分配尝试
│ └─ 成功?→ 返回
│
├─ 阶段2: 直接压缩(高阶分配优先,异步优先级)
│ └─ 成功?→ 返回
│
├─ 阶段3: 使用预留内存(ALLOC_NO_WATERMARKS/ALLOC_OOM) + 分配尝试
│ └─ 成功?→ 返回
│
├─ 阶段4: 直接回收(Direct Reclaim) ★核心★
│ ├─ try_to_free_pages() → 扫描 LRU + 回收页面
│ ├─ 分配尝试
│ ├─ 失败 → drain_all_pages() + unreserve_highatomic + 再分配
│ └─ 成功?→ 返回
│
├─ 阶段5: 再次直接压缩(回收后有更多空闲页)
│ └─ 成功?→ 返回
│
├─ 阶段6: 重试决策
│ ├─ should_reclaim_retry() → 值得回收?→ goto retry
│ ├─ should_compact_retry() → 值得压缩?→ goto retry
│ └─ cpuset 变化?→ goto retry_cpuset
│
├─ 阶段7: OOM Killer
│ ├─ 条件检查(保护机制)
│ ├─ out_of_memory() → 杀死进程释放内存
│ └─ 成功?→ goto retry
│
├─ 阶段8: __GFP_NOFAIL 死循环
│ └─ cond_resched() + retry(永不放弃)
│
└─ nopage: 返回 NULL
__alloc_pages_slowpath 内存回收策略流程图.png
慢路径下回收策略的设计哲学
渐进式降级:水位从 LOW→MIN→NO_WATERMARKS,回收从轻量→重量→OOM,层层递进
压缩优先于回收(高阶分配):碎片化比内存不足更可能是高阶分配失败的原因
回收后必排空:drain_all_pages 释放 PCP 缓存中积压的碎片化页面
理论上限判断:should_reclaim_retry 不是看"回收了多少",而是看"全部回收够不够"
IO 感知:脏页写回中时 congestion_wait,避免盲目重试
永不放弃的底线:__GFP_NOFAIL 确保关键路径(如文件系统元数据)不会因内存分配失败而崩溃
快速路径和慢速路径下的回收策略对比
总结
这次我们从页面分配器出发,沿着两条路深入分析了内存回收的触发机制:
快路径:get_page_from_freelist
定位:分配的第一道尝试,能不回收就不回收。
核心逻辑:遍历 zone → 过滤检查 → 水位判断 → 分配或轻量回收
设计哲学:快速、轻量、尽量不阻塞。水位是核心判据,node_reclaim 是唯一的同步回收手段。
慢路径:__alloc_pages_slowpath
定位:快路径失败后的全面回收,层层降级直到成功或放弃。
核心逻辑:降低水位 → 压缩 → 预留内存 → 直接回收 → 再压缩 → 重试决策 → OOM
设计哲学:不惜代价保证分配成功。先做性价比高的事(压缩),再做昂贵的事(直接回收),最后动用终极手段(OOM)
全景对比
┌─────────────────────────────────────────────┐
│ __alloc_pages_nodemask() │
│ 分配入口 │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ get_page_from_freelist (快路径) │
│ │
│ 水位检查 ──→ node_reclaim (轻量同步回收) │
│ 分配成功 ──→ wakeup_kswapd (预防性回收) │
│ │
│ 返回 page?──→ 是 ──→ 分配完成 │
│ │ │
│ 否 │
└──────┼─────────────────────────────────────┘
│
┌──────▼─────────────────────────────────────┐
│ __alloc_pages_slowpath (慢路径) │
│ │
│ ① 降低水位 + kswapd │
│ ② 直接压缩 (compaction) │
│ ③ 预留内存 │
│ ④ 直接回收 (direct reclaim) ★核心★ │
│ ⑤ 再次压缩 │
│ ⑥ 重试决策 │
│ ⑦ OOM Killer │
│ ⑧ NOFAIL 死循环 │
│ │
│ 返回 page?──→ 是 ──→ 分配完成 │
│ │ │
│ 否 ──→ return NULL (分配彻底失败) │
└────────────────────────────────────────────┘
核心回收手段总结
内存回收的后续章节
本篇分析覆盖了回收的触发点和调度逻辑,但回收的"内脏"——实际扫描和释放页面的机制——还未深入。后续可以从以下方向继续: