当内存紧张时出现mm_vmscan_direct_reclaim 直接回收,需要对其分析
1. 核心概念说明
mm_vmscan_direct_reclaim
是 Linux 内核内存管理的关键路径,负责同步直接页面回收:
-
触发条件:当进程申请内存时,若空闲页低于
low watermark
且快速分配失败,会触发直接回收(而非唤醒 kswapd)。 -
关键动作:
-
调用
__perform_reclaim
→try_to_free_pages()
同步释放页面。 -
阻塞申请进程直至回收完成,可能导致性能抖动。
-
2. 性能影响与监控指标
问题现象
-
系统响应延迟或卡顿(尤其内存密集型任务)。
-
/proc/vmstat
中以下指标显著上升:
pgscan_direct # 直接回收扫描的页数
pgsteal_direct # 直接回收实际释放的页数
allocstall # 内存分配停滞次数
监控命令
watch -n 1 "grep -e pgscan_direct -e pgsteal_direct -e allocstall /proc/vmstat"
3. 调优建议
调整水位线参数
修改 /proc/sys/vm/lowmem_reserve_ratio
可优化各内存域的保留页比例,减轻直接回收压力
# 示例:降低 Normal 区域的保留比例
echo 256 > /proc/sys/vm/lowmem_reserve_ratio
try_to_free_pages 函数功能
unsigned long try_to_free_pages(struct zonelist *zonelist, int order, 7217 gfp_t gfp_mask, nodemask_t *nodemask) 7218 { 7219 unsigned long nr_reclaimed; 7220 struct scan_control sc = { 7221 .nr_to_reclaim = SWAP_CLUSTER_MAX, 7222 .gfp_mask = current_gfp_context(gfp_mask), 7223 .reclaim_idx = gfp_order_zone(gfp_mask, order), 7224 .order = order, 7225 .nodemask = nodemask, 7226 .priority = DEF_PRIORITY, 7227 .may_writepage = !laptop_mode, 7228 .may_unmap = 1, 7229 .may_swap = 1, 7230 }; 7231 bool skip_swap = false; 7232 7233 /* 7234 * scan_control uses s8 fields for order, priority, and reclaim_idx. 7235 * Confirm they are large enough for max values. 7236 */ 7237 BUILD_BUG_ON(MAX_ORDER >= S8_MAX); 7238 BUILD_BUG_ON(DEF_PRIORITY > S8_MAX); 7239 BUILD_BUG_ON(MAX_NR_ZONES > S8_MAX); 7240 7241 /* 7242 * Do not enter reclaim if fatal signal was delivered while throttled. 7243 * 1 is returned so that the page allocator does not OOM kill at this 7244 * point. 7245 */ 7246 if (throttle_direct_reclaim(sc.gfp_mask, zonelist, nodemask)) 7247 return 1; 7248 7249 trace_android_vh_tune_scan_control(&skip_swap); 7250 if (skip_swap) 7251 sc.may_swap = 0; 7252 set_task_reclaim_state(current, &sc.reclaim_state); 7253 trace_mm_vmscan_direct_reclaim_begin(order, sc.gfp_mask); 7254 7255 nr_reclaimed = do_try_to_free_pages(zonelist, &sc); 7256 7257 trace_mm_vmscan_direct_reclaim_end(nr_reclaimed); 7258 set_task_reclaim_state(current, NULL); 7259 7260 return nr_reclaimed; 7261 } 7262
这是一个非常经典的内存直接回收函数。当进程申请内存时,如果系统无法快速分配(快速路径失败),且空闲内存低于最低水位线,就会调用这个函数同步地尝试回收内存。下面我们来详细解析它的逻辑。
🔍 函数作用与核心流程
try_to_free_pages
是Linux内核直接内存回收的核心入口。它的目标是立即释放一些内存页,以满足当前进程的分配请求。这个过程是同步的,意味着它会阻塞调用它的进程,直到回收完成或失败。
其最简化的核心流程如下:
- 初始化控制参数(第7220-7230行)。
- 检查是否应该限制本次回收(第7246行)。
- 执行真正的回收操作(第7255行)。
- 清理并返回回收的页数(第7257-7260行)。
⚙️ 关键结构体:scan_control
函数的核心是初始化一个 struct scan_control sc
。这个结构体就像是给回收器的一份“任务说明书”,决定了回收行为的方方面面:
字段 | 作用解析 | 代码中的值 |
---|---|---|
.nr_to_reclaim | 本次回收期望释放的页数。 | SWAP_CLUSTER_MAX (通常为32页) |
.gfp_mask | 本次内存分配的标志,回收器会根据此标志决定哪些操作是允许的(如是否可进行I/O操作)。 | current_gfp_context(gfp_mask) |
.order | 原始内存申请的阶数,表示需要 个连续页框。回收器会考虑这个值。 | order (参数传入) |
.priority | 回收的优先级,数值越小优先级越高,回收越激进。初始值设为默认优先级。 | DEF_PRIORITY (通常为12) |
.may_writepage | 是否允许回写脏页到磁盘。回写脏页会引发I/O,可能阻塞回收过程。 | !laptop_mode (笔记本模式下可能禁止) |
.may_unmap | 是否允许取消页表映射。这是回收页面的前提步骤之一。 | 1 (允许) |
.may_swap | 是否允许交换匿名页到Swap分区。Swap操作也会产生I/O。 | 1 (允许,除非第7250行被覆盖) |
⚠️ 关键检查:throttle_direct_reclaim
有一个非常重要的检查:throttle_direct_reclaim
。这个函数的作用是判断当前进程是否应该被“节流”(throttle),即暂停直接回收,并加入一个等待队列。
- 为什么需要节流? 当系统中多个进程同时触发直接回收时,会造成大量的I/O竞争和锁竞争,导致每个回收效率都极低,系统长时间卡顿。这时,内核会选择让部分进程稍等片刻,由后台回收线程
kswapd
先工作,释放一些内存后再唤醒它们。 - 如何判断? 它会检查内存节点(node)的平衡状态。如果节点内存严重不足,当前进程就会被加入
pgdat->pfmemalloc_wait
等待队列,函数返回1
,从而让try_to_free_pages
也直接返回1
。这相当于告诉内存分配器:“先别急,等一会儿再试”。 - 如果被节流,函数返回
1
,这通常会让内存分配器稍后重试分配。
🚀 执行回收与返回结果
如果通过了节流检查,函数就进入核心步骤:
-
调用
do_try_to_free_pages
:这个函数是回收逻辑的真正执行者。它会根据sc
中的参数,按照优先级从低到高(priority
从DEF_PRIORITY
开始递减)多次尝试扫描和回收内存。其内部会调用shrink_zones
,shrink_node
等函数,遍历LRU链表,尝试回收文件页和匿名页。 -
返回回收页数:
nr_reclaimed
记录了本次实际回收的页面数量。这个返回值至关重要:- 如果
nr_reclaimed > 0
,说明回收取得了一定成效,内存分配器可能会用这些新释放的内存再次尝试分配。 - 如果
nr_reclaimed == 0
,说明直接回收失败了。这通常是内存极度紧张的标志,可能会触发更激进的机制,例如OOM Killer,开始选择并杀死进程来释放内存。
- 如果
💎 总结与问题定位
简单来说,try_to_free_pages
是系统在内存分配路径上设置的一道“紧急关卡”。当它被频繁调用时,说明系统内存压力已经很大。
如何判断回收是否成功? 主要就是看它的返回值 nr_reclaimed
。如果返回值经常为0,就需要警惕OOM的风险。
如何观察这个函数的行为? 你可以通过内核跟踪点(tracepoint)来监控,例如 trace_mm_vmscan_direct_reclaim_begin
和 trace_mm_vmscan_direct_reclaim_end
(,它们会记录每次直接回收的开始、结束以及回收的页数,是分析内存压力的利器。
三.allow_direct_reclaim 函数
static bool allow_direct_reclaim(pg_data_t *pgdat) 7088 { 7089 struct zone *zone; 7090 unsigned long pfmemalloc_reserve = 0; 7091 unsigned long free_pages = 0; 7092 int i; 7093 bool wmark_ok; 7094 7095 if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES) 7096 return true; 7097 7098 for (i = 0; i <= ZONE_NORMAL; i++) { 7099 zone = &pgdat->node_zones[i]; 7100 if (!managed_zone(zone)) 7101 continue; 7102 7103 if (!zone_reclaimable_pages(zone)) 7104 continue; 7105 7106 pfmemalloc_reserve += min_wmark_pages(zone); 7107 free_pages += zone_page_state_snapshot(zone, NR_FREE_PAGES); 7108 } 7109 7110 /* If there are no reserves (unexpected config) then do not throttle */ 7111 if (!pfmemalloc_reserve) 7112 return true; 7113 7114 wmark_ok = free_pages > pfmemalloc_reserve / 2; 7115 7116 /* kswapd must be awake if processes are being throttled */ 7117 if (!wmark_ok && waitqueue_active(&pgdat->kswapd_wait)) { 7118 if (READ_ONCE(pgdat->kswapd_highest_zoneidx) > ZONE_NORMAL) 7119 WRITE_ONCE(pgdat->kswapd_highest_zoneidx, ZONE_NORMAL); 7120 7121 wake_up_interruptible(&pgdat->kswapd_wait); 7122 } 7123 7124 return wmark_ok; 7125 }
allow_direct_reclaim
这个函数是 Linux 内核内存管理中的一个关键决策点,它的核心作用是判断当前内存节点的状况是否允许进程直接进行内存回收,还是应该将回收工作交给专门的kswapd
内核线程来处理,以避免多个进程同时回收导致系统性能恶化。
这个函数通过几个关键检查来决定是否“放行”直接回收。
⚙️ 关键判断逻辑详解
-
检查 kswapd 健康状况
。如果失败次数过多,意味着后台异步回收机制可能已经失效,系统内存压力巨大。此时函数会返回if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES)
:这行代码首先检查kswapd
线程是否已经多次尝试回收内存但均告失败true
,作为一种“兜底”策略,允许进程自己动手进行直接回收,奋力一搏。 -
计算关键内存指标 接着,函数遍历该内存节点(
。它计算两个核心数值:pg_data_t
)中ZONE_NORMAL
及更低区域(通常是ZONE_DMA
和ZONE_DMA32
)的内存区域(zone
)pfmemalloc_reserve
:这是为应对紧急内存分配(如网络处理)而预留的页面总数,简单加总了各个区域的最低水位线(min_wmark_pages
)。free_pages
:这是这些区域中当前空闲页面的总数。
-
评估内存水位线状态
。wmark_ok = free_pages > pfmemalloc_reserve / 2
:这是最关键的判断。它检查当前的空闲内存是否大于预留内存的一半。如果为true
,说明内存状况尚可,允许直接回收;如果为false
,则意味着为系统关键操作预留的内存可能不足,情况比较危险
⚠️ 水位线不足时的应对措施
当 wmark_ok
为 false
时,函数并不会简单地返回结果,它还会采取一个重要行动:
- 唤醒 kswapd:函数会检查
kswapd
的等待队列是否活跃,如果是,则确保kswapd
的工作范围集中在ZONE_NORMAL
及以下的关键区域,并立即唤醒它。这样做的目的是让专业的内核回收线程来统一、高效地处理内存危机,避免多个进程各自为战进行直接回收,从而减少 I/O 竞争和系统锁争用,防止系统性能雪崩。
💎 核心作用总结
简单来说,allow_direct_reclaim
就像是内存回收的“交通警察”。它的决策逻辑是:
- 返回
true
(允许直接回收):要么是kswapd
已经无能为力,需要进程自救;要么是当前内存水位线还算安全,进程可以自行回收。 - 返回
false
(应节流直接回收):当系统关键内存储备不足时,它会拦下想要自己回收内存的进程,同时唤醒kswapd
这个“专业救援队” 来统一处理问题,以此保障系统的整体稳定性。
这个机制是 Linux 内核在面对内存压力时,在“让进程自己快速解决问题”和“保持系统整体流畅稳定”之间做出的重要权衡。