前言
紧接上文,我们从整体脉络上把握了内存回收机制的几种情况,但回收的"内脏"——实际扫描和释放页面的机制——还未深入!本文将从 kswapd 开始详细拆解内部细节:
kswapd 是什么?
kswapd 是 Linux 内核为每个 NUMA 节点创建的后台内存回收线程。它的职责是在内存水位下降到 LOW 水位时被唤醒,异步回收页面,将水位提升到 HIGH 水位后重新睡眠。
核心定位: kswapd 是内存回收的"第一道防线",在分配器慢路径的直接回收(direct reclaim)之前工作,避免分配线程被阻塞在同步回收中。
整体架构

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ 分配路径 │
│ │
│ __alloc_pages_slowpath() │
│ ├─ wake_all_kswapds() ─────────────────────────────────────┐ │
│ ├─ __alloc_pages_direct_compact() │ │
│ ├─ __alloc_pages_direct_reclaim() │ │
│ └─ __alloc_pages_may_oom() │ │
└─────────────────────────────────────────────────────────────────────────┼──────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ kswapd 后台回收 │
│ │
│ wakeup_kswapd() │
│ │ 传递 order / highest_zoneidx │
│ │ 唤醒 pgdat->kswapd_wait │
│ ▼ │
│ kswapd() 主循环 │
│ ├─ kswapd_try_to_sleep() // 两阶段睡眠 │
│ │ ├─ 短睡 100ms(试探) │
│ │ └─ 长睡眠(schedule) │
│ └─ balance_pgdat() // 回收主函数 │
│ ├─ priority 递减循环 (12→1) │
│ ├─ pgdat_balanced() // 平衡检查 │
│ ├─ age_active_anon() // 匿名页老化 │
│ ├─ mem_cgroup_soft_limit_reclaim() │
│ └─ kswapd_shrink_node() │
│ └─ shrink_node() // ★节点级回收核心★ │
│ │
│ 回收完成后: │
│ ├─ 唤醒被 throttle 的 direct reclaim 进程 │
│ └─ 唤醒 kcompactd 做碎片整理 │
└─────────────────────────────────────────────────────────────────────────────────────────────┘唤醒链路详解
wake_all_kswapds
// mm/page_alloc.c:4752-4766
static void wake_all_kswapds(unsigned int order, gfp_t gfp_mask,
const struct alloc_context *ac)
{
struct zoneref *z;
struct zone *zone;
pg_data_t *last_pgdat = NULL;
enum zone_type highest_zoneidx = ac->highest_zoneidx;
for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, highest_zoneidx,
ac->nodemask) {
if (last_pgdat != zone->zone_pgdat)
wakeup_kswapd(zone, gfp_mask, order, highest_zoneidx);
last_pgdat = zone->zone_pgdat;
}
}
设计要点:
遍历 zonelist 中所有 zone,每个 NUMA 节点(pgdat)只唤醒一次
通过
last_pgdat指针去重,避免同一节点多个 zone 重复唤醒
wakeup_kswapd
void wakeup_kswapd(struct zone *zone, gfp_t gfp_flags, int order,
enum zone_type highest_zoneidx)
{
pg_data_t *pgdat;
enum zone_type curr_idx;
// 1. 前置检查
if (!managed_zone(zone)) // zone 无管理页面
return;
if (!cpuset_zone_allowed(zone, gfp_flags)) // cpuset 限制
return;
// 2. 传递参数给 kswapd(只增不减)
pgdat = zone->zone_pgdat;
curr_idx = READ_ONCE(pgdat->kswapd_highest_zoneidx);
if (curr_idx == MAX_NR_ZONES || curr_idx < highest_zoneidx)
WRITE_ONCE(pgdat->kswapd_highest_zoneidx, highest_zoneidx);
if (READ_ONCE(pgdat->kswapd_order) < order)
WRITE_ONCE(pgdat->kswapd_order, order);
// 3. kswapd 没在等待?不唤醒
if (!waitqueue_active(&pgdat->kswapd_wait))
return;
// 4. 绝望节点保护
if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES ||
(pgdat_balanced(pgdat, order, highest_zoneidx) &&
!pgdat_watermark_boosted(pgdat, highest_zoneidx))) {
// 碎片化场景,唤醒 kcompactd 代替
if (!(gfp_flags & __GFP_DIRECT_RECLAIM))
wakeup_kcompactd(pgdat, order, highest_zoneidx);
return;
}
// 5. 唤醒 kswapd
wake_up_interruptible(&pgdat->kswapd_wait);
}
关键设计:
kswapd 主循环
文件: mm/vmscan.c:4463-4549
线程初始化
static int kswapd(void *p)
{
pg_data_t *pgdat = (pg_data_t *)p;
struct task_struct *tsk = current;
// 绑定到本节点 CPU
set_cpus_allowed_ptr(tsk, cpumask_of_node(pgdat->node_id));
// 设置关键标志
tsk->flags |= PF_MEMALLOC | PF_SWAPWRITE | PF_KSWAPD;
set_freezable();
}
主循环流程
for(;;) {
// 1. 读取唤醒参数
alloc_order = pgdat->kswapd_order;
highest_zoneidx = kswapd_highest_zoneidx(pgdat, highest_zoneidx);
// 2. 重置参数(已被取走)
pgdat->kswapd_order = 0;
pgdat->kswapd_highest_zoneidx = MAX_NR_ZONES;
// 3. 尝试睡眠
kswapd_try_sleep:
kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order, highest_zoneidx);
// 4. 再次读取参数(睡眠期间可能被更新)
alloc_order = pgdat->kswapd_order;
highest_zoneidx = kswapd_highest_zoneidx(pgdat, highest_zoneidx);
pgdat->kswapd_order = 0;
pgdat->kswapd_highest_zoneidx = MAX_NR_ZONES;
// 5. 冻结检查(休眠/挂起场景)
if (try_to_freeze()) continue;
if (kthread_should_stop()) break;
// 6. 执行回收
reclaim_order = balance_pgdat(pgdat, alloc_order, highest_zoneidx);
// 7. 回收不足?继续睡眠等待下次唤醒
if (reclaim_order < alloc_order)
goto kswapd_try_sleep;
}
两阶段睡眠机制
文件: mm/vmscan.c:4360-4439
这是 kswapd 最精妙的设计之一。
流程
kswapd_try_to_sleep()
│
├─ prepare_to_wait(TASK_INTERRUPTIBLE)
│
├─ prepare_kswapd_sleep() 检查是否可以睡
│ ├─ 唤醒 pfmemalloc_wait 上的等待者
│ ├─ kswapd_failures >= 16 → return true(绝望,可以睡)
│ └─ pgdat_balanced() → return true(已平衡,可以睡)
│
├─ 可以睡 →
│ ├─ reset_isolation_suitable() // 重置压缩跳过缓存
│ ├─ wakeup_kcompactd() // 唤醒 kcompactd
│ └─ schedule_timeout(HZ/10) // ★短睡 100ms★
│ └─ remaining != 0 → 被提前唤醒,更新参数
│
└─ 短睡完仍可睡?→
├─ 降低 vmstat 阈值
├─ schedule() // ★长睡眠★
└─ 恢复 vmstat 阈值
为什么是两阶段
对比传统设计: 如果只有一阶段长睡眠,kswapd 被唤醒后回收完成就长睡,下一次分配压力来临时才被唤醒。中间如果有持续的小压力,kswapd 会反复"醒来→回收→长睡→被唤醒",每次都有长睡眠的延迟。两阶段设计让 kswapd 在压力持续时保持"浅睡眠",响应更快。
vmstat 阈值调整
// 睡前:降低阈值,减少 per-cpu 计数偏差
set_pgdat_percpu_threshold(pgdat, calculate_normal_threshold);
schedule(); // 长睡眠
// 醒后:恢复正常阈值,减少 CPU 开销
set_pgdat_percpu_threshold(pgdat, calculate_pressure_threshold);
原因: vmstat 计数器(如 NR_FREE_PAGES)有 per-cpu 偏差,偏差量 ≈ nr_online_cpus × threshold。kswapd 醒着时降低阈值,确保水位检查更准确;睡眠时恢复,减少刷新频率。
balance_pgdat 回收主函数
文件: mm/vmscan.c:4120-4343
初始化
struct scan_control sc = {
.gfp_mask = GFP_KERNEL,
.order = order,
.may_unmap = 1,
};
boost 统计
nr_boost_reclaim = 0;
for (i = 0; i <= highest_zoneidx; i++) {
zone = pgdat->node_zones + i;
if (!managed_zone(zone)) continue;
nr_boost_reclaim += zone->watermark_boost;
zone_boosts[i] = zone->watermark_boost;
}
boosted = nr_boost_reclaim;
watermark_boost 是碎片化触发的水位提升量。统计它是为了在回收完成后恢复。
核心循环
restart:
set_reclaim_active(pgdat, highest_zoneidx);
sc.priority = DEF_PRIORITY; // 12
do {
// 1. buffer_heads 过多?扩展回收范围
if (buffer_heads_over_limit)
sc.reclaim_idx = 最高 zone;
// 2. 平衡检查
balanced = pgdat_balanced(pgdat, sc.order, highest_zoneidx);
if (!balanced && nr_boost_reclaim) {
nr_boost_reclaim = 0; // 放弃 boost,全力回收
goto restart;
}
if (!nr_boost_reclaim && balanced)
goto out; // 已平衡,退出
// 3. 设置回收策略
sc.may_writepage = !laptop_mode && !nr_boost_reclaim;
sc.may_swap = !nr_boost_reclaim;
// boost 模式:不写回、不 swap,只释放干净页
// 4. 老化匿名页活跃链表
age_active_anon(pgdat, &sc);
// 5. cgroup 软限制回收
mem_cgroup_soft_limit_reclaim(pgdat, sc.order, ...);
// 6. ★真正回收★
if (kswapd_shrink_node(pgdat, &sc))
raise_priority = false;
// 7. 唤醒被 throttle 的 direct reclaim 进程
if (allow_direct_reclaim(pgdat))
wake_up_all(&pgdat->pfmemalloc_wait);
// 8. 优先级调整
if (raise_priority || !nr_reclaimed)
sc.priority--;
// 9. boost 保护:无进展则退出
if (nr_boost_reclaim && !nr_reclaimed)
break;
} while (sc.priority >= 1);
priority 递减机制
设计思想: 从最轻量开始,大多数情况下低优先级就能回收够。只有在极端压力下才逐步加大力度,避免全量扫描的 CPU 和 IO 开销。
boost 模式详解
当 watermark_boost > 0(碎片化触发)时:
目的: 碎片化场景的"快速止血"——只做最轻量的回收,释放页面后交给 kcompactd 做碎片整理。避免在碎片化场景下触发昂贵的 IO(写脏页/swap)。
kswapd_shrink_node
文件: mm/vmscan.c:4030-4065
static bool kswapd_shrink_node(pg_data_t *pgdat, struct scan_control *sc)
{
// 计算目标回收量
sc->nr_to_reclaim = 0;
for (z = 0; z <= sc->reclaim_idx; z++) {
zone = pgdat->node_zones + z;
if (!managed_zone(zone)) continue;
sc->nr_to_reclaim += max(high_wmark_pages(zone), SWAP_CLUSTER_MAX);
}
// ★调用 shrink_node★
shrink_node(pgdat, sc);
// 高阶回收成功,降为 order-0 继续
if (sc->order && sc->nr_reclaimed >= compact_gap(sc->order))
sc->order = 0;
return sc->nr_scanned >= sc->nr_to_reclaim;
}
关键点:
nr_to_reclaim= 各 zone 的 HIGH 水位之和(至少SWAP_CLUSTER_MAX= 32)shrink_node()是真正的回收执行者,下一层分析的重点高阶回收成功后降为 order-0,避免过度回收
prepare_kswapd_sleep 与 pgdat_balanced
prepare_kswapd_sleep
文件: mm/vmscan.c:3991-4020
static bool prepare_kswapd_sleep(pg_data_t *pgdat, int order, int highest_zoneidx)
{
// 唤醒被 throttle 的 direct reclaim 进程
if (waitqueue_active(&pgdat->pfmemalloc_wait))
wake_up_all(&pgdat->pfmemalloc_wait);
// 绝望节点
if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES)
return true;
// 已平衡
if (pgdat_balanced(pgdat, order, highest_zoneidx)) {
clear_pgdat_congested(pgdat);
return true;
}
return false;
}
pgdat_balanced
文件: mm/vmscan.c:3940-3973
static bool pgdat_balanced(pg_data_t *pgdat, int order, int highest_zoneidx)
{
// 从低 zone 到高 zone 逐个检查
for (i = 0; i <= highest_zoneidx; i++) {
zone = pgdat->node_zones + i;
if (!managed_zone(zone)) continue;
mark = high_wmark_pages(zone);
// 检查:空闲页 > HIGH 水位 + 能分配 2^order 连续页
if (zone_watermark_ok_safe(zone, order, mark, highest_zoneidx))
return true; // 有一个 zone 达标即可
}
if (mark == -1) return true; // 无 populated zone
return false;
}
关键: 只要有一个 zone 达到 HIGH 水位,kswapd 就认为节点已平衡。这是"最低标准"策略——不需要所有 zone 都完美,只要有一个够用就行。
kswapd 与其他组件的协作
与 kcompactd
kswapd 回收完成
├─ kswapd_try_to_sleep() → wakeup_kcompactd()
└─ balance_pgdat() out → wakeup_kcompactd()
kswapd 负责释放页面,kcompactd 负责整理碎片。两者形成流水线:
kswapd 释放页面后,空闲页增多,kcompactd 有更多空间做迁移
kcompactd 整理碎片后,高阶分配成功率提升
与 direct reclaim
balance_pgdat()
└─ if (allow_direct_reclaim(pgdat))
wake_up_all(&pgdat->pfmemalloc_wait)
当 kswapd 回收使水位恢复到 LOW 以上时,唤醒被 throttle 的 direct reclaim 进程。这些进程在 throttle_direct_reclaim() 中等待 kswapd 有了进展再继续。
与 OOM
kswapd_failures >= MAX_RECLAIM_RETRIES (16)
→ kswapd 放弃该节点
→ 交给 direct reclaim + OOM Killer
kswapd 不会触发 OOM。它只是回收失败后"投降",让分配路径的 direct reclaim 和 OOM Killer 接管。
scan_control 结构体速查
文件: mm/vmscan.c:65-175
struct scan_control {
unsigned long nr_to_reclaim; // 目标回收量
unsigned long nr_reclaimed; // 已回收量
unsigned long nr_scanned; // 已扫描量
s8 order; // 分配阶数
s8 priority; // 扫描优先级 (12→1)
s8 reclaim_idx; // 最高回收 zone
gfp_t gfp_mask; // 分配掩码
unsigned int may_writepage:1; // 允许写回脏页
unsigned int may_unmap:1; // 允许 unmap 映射页
unsigned int may_swap:1; // 允许 swap
unsigned int may_deactivate:2; // 允许降级活跃页
unsigned int cache_trim_mode:1;// 文件缓存过多模式
unsigned int file_is_tiny:1; // 文件页过少保护
struct mem_cgroup *target_mem_cgroup; // 目标 cgroup
};
关键数据结构关系
pg_data_t (per NUMA node)
├─ kswapd_wait // 等待队列,wakeup_kswapd 唤醒
├─ kswapd_order // 请求的分配阶数
├─ kswapd_highest_zoneidx // 请求的最高 zone
├─ kswapd_failures // 连续失败次数
├─ pfmemalloc_wait // direct reclaim 等待队列
├─ node_zones[] // zone 数组
│ ├─ _watermark[3] // MIN/LOW/HIGH 水位
│ ├─ watermark_boost // 碎片化 boost 量
│ └─ flags // ZONE_RECLAIM_ACTIVE 等
└─ kswapd // kswapd task_struct 指针
完整时序图
设计思想总结
下一篇预告
kswapd 分析完成后,两条路汇聚到同一个函数:
kswapd 路径: balance_pgdat() → kswapd_shrink_node() → shrink_node()
direct reclaim 路径: try_to_free_pages() → shrink_zones() → shrink_node()
所以我们下一篇就分析 direct reclaim 的路径,然后再下一批深入 shrink_node ,节点级回收调度,它是 kswapd 和 direct reclaim 的共同下游。