一、直接回收是什么
直接回收是分配器慢路径中的同步回收机制。当 kswapd 后台回收来不及跟上分配需求、且快路径分配失败时,分配线程自己动手回收页面,然后重试分配。
核心定位: 直接回收是内存回收的"第二道防线",在 kswapd 之后、OOM 之前工作。与 kswapd 的异步回收不同,直接回收是同步阻塞的——分配线程会暂停,直到回收到足够页面或确认无法回收。
二、整体架构
__alloc_pages_slowpath() 慢路径
│
├─ 阶段1: wake_all_kswapds() + get_page_from_freelist()
│ └─ 成功?→ 返回页面
│
├─ 阶段2: __alloc_pages_direct_compact()
│ └─ 成功?→ 返回页面
│
├─ 阶段3: 使用预留内存 + get_page_from_freelist()
│ └─ 成功?→ 返回页面
│
├─ 阶段4: __alloc_pages_direct_reclaim() ★直接回收★
│ │
│ ├─ __perform_reclaim()
│ │ ├─ cpuset_memory_pressure_bump() // 通知 cpuset 内存压力
│ │ ├─ psi_memstall_enter() // PSI 内存停顿追踪
│ │ ├─ memalloc_noreclaim_save() // 允许使用预留内存
│ │ └─ try_to_free_pages() // ★核心回收函数★
│ │ │
│ │ ├─ throttle_direct_reclaim() // 节流保护
│ │ │ └─ 等待 kswapd 有进展
│ │ │
│ │ └─ do_try_to_free_pages()
│ │ └─ priority 循环 (12→0)
│ │ └─ shrink_zones()
│ │ └─ shrink_node() // 节点级回收(下一篇详析)
│ │
│ ├─ did_some_progress > 0?
│ │ └─ 否 → return NULL
│ │
│ └─ get_page_from_freelist() // 回收后重试分配
│ └─ 失败?
│ ├─ unreserve_highatomic_pageblock()
│ ├─ drain_all_pages()
│ └─ 再次 get_page_from_freelist()
│
├─ 阶段5: __alloc_pages_direct_compact()(回收后压缩)
│
├─ 阶段6: 重试决策 (should_reclaim_retry / should_compact_retry)
│
├─ 阶段7: __alloc_pages_may_oom() // OOM Killer
│
└─ 阶段8: __GFP_NOFAIL 死循环

三、入口函数:__alloc_pages_direct_reclaim
文件: mm/page_alloc.c:4722-4750
static inline struct page *
__alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
unsigned int alloc_flags, const struct alloc_context *ac,
unsigned long *did_some_progress)
{
struct page *page = NULL;
bool drained = false;
// ① 执行回收
*did_some_progress = __perform_reclaim(gfp_mask, order, ac);
if (unlikely(!(*did_some_progress)))
return NULL; // 回收失败,放弃
// ② 回收后重试分配
retry:
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
// ③ 分配失败?排空 PCP 缓存 + 释放 HIGHATOMIC 预留后重试
if (!page && !drained) {
unreserve_highatomic_pageblock(ac, false);
drain_all_pages(NULL);
drained = true;
goto retry;
}
return page;
}
设计要点:
先回收再分配:
__perform_reclaim回收完成后,用get_page_from_freelist重新分配drain_all_pages 补救: PCP 缓存中可能有碎片化的 order-0 页面,排空后配合压缩使用
unreserve_highatomic 释放: 高阶原子分配预留块在紧急时可以释放
只重试一次:
drained标志确保最多做一次 drain + retry,避免死循环
四、回收包装:__perform_reclaim
文件: mm/page_alloc.c:4694-4719
static unsigned long
__perform_reclaim(gfp_t gfp_mask, unsigned int order,
const struct alloc_context *ac)
{
unsigned int noreclaim_flag;
unsigned long pflags, progress;
cond_resched(); // ① 让出 CPU,给其他进程机会
cpuset_memory_pressure_bump(); // ② 通知 cpuset 内存压力
psi_memstall_enter(&pflags); // ③ PSI 内存停顿追踪
fs_reclaim_acquire(gfp_mask); // ④ lockdep 检查
noreclaim_flag = memalloc_noreclaim_save(); // ⑤ 允许使用预留内存
// ⑥ 调用核心回收函数
progress = try_to_free_pages(ac->zonelist, order, gfp_mask, ac->nodemask);
memalloc_noreclaim_restore(noreclaim_flag);
fs_reclaim_release(gfp_mask);
psi_memstall_leave(&pflags);
cond_resched(); // ⑦ 回收后再次让出 CPU
return progress;
}
各步骤作用:
五、核心回收函数:try_to_free_pages
文件: mm/vmscan.c:3761-3802
unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
gfp_t gfp_mask, nodemask_t *nodemask)
{
unsigned long nr_reclaimed;
struct scan_control sc = {
.nr_to_reclaim = SWAP_CLUSTER_MAX, // 32 页
.gfp_mask = current_gfp_context(gfp_mask),
.reclaim_idx = gfp_zone(gfp_mask),
.order = order,
.nodemask = nodemask,
.priority = DEF_PRIORITY, // 12
.may_writepage = !laptop_mode, // laptop_mode 下不写回
.may_unmap = 1, // 允许 unmap
.may_swap = 1, // 允许 swap
};
// ① 节流检查:等待 kswapd 有进展
if (throttle_direct_reclaim(sc.gfp_mask, zonelist, nodemask))
return 1; // 返回 1 避免触发 OOM
// ② 设置回收状态
set_task_reclaim_state(current, &sc.reclaim_state);
trace_mm_vmscan_direct_reclaim_begin(order, sc.gfp_mask);
// ③ 执行回收
nr_reclaimed = do_try_to_free_pages(zonelist, &sc);
trace_mm_vmscan_direct_reclaim_end(nr_reclaimed);
set_task_reclaim_state(current, NULL);
return nr_reclaimed;
}
关键参数:
与 kswapd 的区别:
六、节流机制:throttle_direct_reclaim
文件: mm/vmscan.c:3681-3759
这是直接回收最重要的保护机制之一。
6.1 为什么需要节流
当系统内存极度紧张时,大量分配线程同时进入直接回收会:
竞争回收资源: 多个线程扫描同一个 LRU 链表,浪费 CPU
阻塞关键线程: 如果文件系统线程(如 kjournald)被阻塞,可能导致回收本身无法进展
OOM 误杀: 在 kswapd 正在回收的过程中,直接回收线程可能误判为 OOM
6.2 节流流程
throttle_direct_reclaim(gfp_mask, zonelist, nodemask)
│
├─ ① 跳过条件
│ ├─ 内核线程 (PF_KTHREAD) → 不节流(可能是回收的关键参与者)
│ └─ 有致命信号 (fatal_signal_pending) → 不节流(让进程快速退出释放内存)
│
├─ ② 检查 pfmemalloc 储备
│ └─ 遍历 zonelist,找到第一个 ZONE_NORMAL 或更低的 zone
│ └─ allow_direct_reclaim(pgdat) 检查
│ ├─ free_pages > pfmemalloc_reserve / 2 → 不节流
│ └─ 否则 → 需要节流
│
├─ ③ 节流方式
│ ├─ 有 __GFP_FS?→ wait_event_killable (无限等待 kswapd 唤醒)
│ └─ 无 __GFP_FS?→ wait_event_interruptible_timeout (最多等 1 秒)
│ └─ 无 __GFP_FS 的原因:可能持有 FS 锁,无限等待会死锁
│
└─ ④ 返回
├─ fatal_signal_pending → return true(分配器不触发 OOM)
└─ 正常唤醒 → return false(继续直接回收)
6.3 allow_direct_reclaim 判断
static bool allow_direct_reclaim(pg_data_t *pgdat)
{
// 绝望节点:kswapd 失败 16 次,不再节流
if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES)
return true;
// 计算 pfmemalloc 储备和空闲页
for (i = 0; i <= ZONE_NORMAL; i++) {
pfmemalloc_reserve += min_wmark_pages(zone);
free_pages += zone_page_state(zone, NR_FREE_PAGES);
}
wmark_ok = free_pages > pfmemalloc_reserve / 2;
// 水位不足?唤醒 kswapd
if (!wmark_ok && waitqueue_active(&pgdat->kswapd_wait)) {
WRITE_ONCE(pgdat->kswapd_highest_zoneidx, ZONE_NORMAL);
wake_up_interruptible(&pgdat->kswapd_wait);
}
return wmark_ok;
}
关键设计:
储备阈值:
pfmemalloc_reserve / 2,即 MIN 水位的一半自动唤醒 kswapd: 节流时顺便唤醒 kswapd,让后台线程先干活
绝望不节流: kswapd 连续失败 16 次后,不再节流,让直接回收放手干
七、回收主循环:do_try_to_free_pages
文件: mm/vmscan.c:3544-3630
do_try_to_free_pages(zonelist, sc)
│
├─ 统计 ALLOCSTALL 事件
│
├─ do { // priority 从 12 递减到 0
│ │
│ ├─ vmpressure_prio() // 内存压力通知
│ │
│ ├─ shrink_zones(zonelist, sc) // ★回收所有 zone★
│ │ └─ for_each_zone(zone, zonelist)
│ │ ├─ cpuset_zone_allowed() 检查
│ │ ├─ compaction_ready()?→ 跳过,留给压缩
│ │ ├─ mem_cgroup_soft_limit_reclaim()
│ │ └─ shrink_node(pgdat, sc) // ★节点级回收★
│ │
│ ├─ nr_reclaimed >= nr_to_reclaim?→ break(够了)
│ │
│ ├─ compaction_ready?→ break(压缩可用,停止回收)
│ │
│ ├─ priority < 10?→ may_writepage = 1(困难时允许写回)
│ │
│ └─ priority--
│
│ } while (priority >= 0)
│
├─ snapshot_refaults() // 记录 refault 统计
│
├─ 回收成功?→ return nr_reclaimed
│
├─ compaction_ready?→ return 1(避免 OOM)
│
├─ skipped_deactivate?→ 强制 deactivate,retry
│
├─ memcg_low_skipped?→ 强制回收 memcg low 保护内存,retry
│
└─ return 0(回收失败)
7.1 priority 递减 vs kswapd
7.2 nr_to_reclaim 的差异
kswapd 的 nr_to_reclaim 是各 zone HIGH 水位之和(可能几百页),目标是全面恢复水位。
直接回收的 nr_to_reclaim 是 SWAP_CLUSTER_MAX = 32 页,目标是最小化阻塞时间——只要能分配成功就行。
7.3 两个重试条件
skipped_deactivate 重试:
当
reclaim_idx限制或memory.lowcgroup 设置导致大量内存不可回收LRU 扫描时跳过了活跃页降级(deactivate)
解决方案:强制
force_deactivate = 1,强制降级所有活跃页
memcg_low_skipped 重试:
cgroup 的
memory.low保护了一些内存,回收时跳过了解决方案:设置
memcg_low_reclaim = 1,强制回收被保护的内存这是最后手段,避免 OOM
八、shrink_zones — zone 级回收调度
文件: mm/vmscan.c:3431-3514
static void shrink_zones(struct zonelist *zonelist, struct scan_control *sc)
{
// buffer_heads 过多?扩展到 highmem
if (buffer_heads_over_limit) {
sc->gfp_mask |= __GFP_HIGHMEM;
sc->reclaim_idx = gfp_zone(sc->gfp_mask);
}
for_each_zone_zonelist_nodemask(zone, z, zonelist, sc->reclaim_idx, ...) {
// cpuset 检查
if (!cpuset_zone_allowed(zone, ...))
continue;
// 高阶分配 + 压缩已就绪 → 跳过回收
if (sc->order > PAGE_ALLOC_COSTLY_ORDER && compaction_ready(zone, sc)) {
sc->compaction_ready = true;
continue;
}
// 同一节点只回收一次
if (zone->zone_pgdat == last_pgdat)
continue;
last_pgdat = zone->zone_pgdat;
// cgroup 软限制回收
mem_cgroup_soft_limit_reclaim(zone->zone_pgdat, ...);
// ★调用 shrink_node★
shrink_node(zone->zone_pgdat, sc);
}
}
设计要点:
每个节点只回收一次: 通过
last_pgdat去重压缩优先: 高阶分配场景下,如果压缩已就绪,跳过回收
buffer_heads 特殊处理: 32 位系统上 highmem 页可能 pin 住 lowmem
九、直接回收 vs kswapd 对比
十、直接回收的时序
时间线 ──────────────────────────────────────────────────────►
分配线程 kswapd
│ │
│ __alloc_pages_slowpath() │
│ │
│ wake_all_kswapds() ──────────►│ (异步唤醒)
│ │
│ get_page_from_freelist() │
│ └─ 失败 │
│ │
│ __alloc_pages_direct_compact() │
│ └─ 失败 │
│ │
│ __alloc_pages_direct_reclaim() │
│ │ │
│ ├─ __perform_reclaim() │
│ │ ├─ cond_resched() │
│ │ ├─ throttle_direct_reclaim │
│ │ │ └─ 检查水位 │
│ │ │ ├─ 水位 OK → 继续 │
│ │ │ └─ 水位不足 │
│ │ │ ├─ 唤醒 kswapd─┤
│ │ │ └─ 等待... │ ◄── kswapd 回收中...
│ │ │ │ │
│ │ │ kswapd 唤醒 ──►│
│ │ │ │ │
│ │ ├─ try_to_free_pages() │
│ │ │ └─ do_try_to_free_pages│
│ │ │ └─ shrink_zones() │
│ │ │ └─ shrink_node()│
│ │ │ │
│ │ └─ 回收完成 │
│ │ │
│ ├─ get_page_from_freelist() │
│ │ └─ 成功 ✓ │
│ │ │
│ └─ 返回页面 │
十一、关键设计思想
11.1 最小化阻塞
直接回收只回收 32 页(SWAP_CLUSTER_MAX),而不是像 kswapd 那样恢复整个水位。这是最小化阻塞时间的设计——分配线程只需要"够用"就行,剩下的交给 kswapd 慢慢恢复。
11.2 节流保护
throttle_direct_reclaim 是防止"回收风暴"的关键:
内存紧张时,大量线程同时进入直接回收会加剧竞争
节流让它们等待 kswapd 有进展后再继续
避免了"越回收越紧张"的恶性循环
11.3 渐进式加力
priority 从 12 到 0,扫描量从 1/4096 到 100%。大多数情况下低优先级就够了,只有极端情况才全量扫描。
11.4 两层重试保护
skipped_deactivate重试: 强制降级活跃页memcg_low_skipped重试: 强制回收 cgroup 保护内存
这两层重试确保在最终放弃(进入 OOM)之前,已经尝试了所有可能的回收手段。
11.5 与 kswapd 的协作
直接回收不是独立工作的:
节流时唤醒 kswapd: 让后台线程先干活
kswapd 唤醒被 throttle 的进程: 回收有进展后通知等待的分配线程
compaction_ready 跳过: 如果压缩已就绪,直接回收可以停止,交给压缩
十二、与慢路径的衔接
回到 __alloc_pages_slowpath 的阶段 4:
// 阶段4: 直接回收
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
&did_some_progress);
if (page)
goto got_pg;
did_some_progress 的作用:
> 0: 回收了一些页面,后续的 OOM Killer 判断会考虑这一点0: 回收失败,分配器可能进入 OOM 阶段
在
should_reclaim_retry中:如果did_some_progress且理论上回收够,值得重试
十三、关键数据结构
13.1 scan_control 在直接回收中的初始化
struct scan_control sc = {
.nr_to_reclaim = SWAP_CLUSTER_MAX, // 32(vs kswapd 的 Σ high_wmark)
.gfp_mask = current_gfp_context(gfp_mask),
.reclaim_idx = gfp_zone(gfp_mask), // 由 GFP 标志决定
.order = order,
.nodemask = nodemask,
.priority = DEF_PRIORITY, // 12
.may_writepage = !laptop_mode, // 默认允许
.may_unmap = 1,
.may_swap = 1,
};
13.2 throttle_direct_reclaim 中的关键判断
// 内核线程不节流
if (current->flags & PF_KTHREAD) goto out;
// 致命信号不节流
if (fatal_signal_pending(current)) goto out;
// 水位判断:free_pages > MIN 水位 / 2
wmark_ok = free_pages > pfmemalloc_reserve / 2;
// 无 __GFP_FS → 最多等 1 秒(避免死锁)
// 有 __GFP_FS → 无限等待 kswapd 唤醒
十四、总结
十五、待深入的下一步
直接回收和 kswapd 两条路最终汇聚到同一个函数:
kswapd: balance_pgdat() → kswapd_shrink_node() → shrink_node()
direct: try_to_free_pages() → do_try_to_free_pages() → shrink_zones() → shrink_node()
下一篇: shrink_node() — 节点级回收核心,kswapd 和 direct reclaim 的共同下游。