mm/page_alloc: only update lowmem_reserve_ratio on sysctl write

内存管理 · page_alloc · sysctl 处理路径

💡 一句话总结

在管理员读取 /proc/sys/vm/lowmem_reserve_ratio 这个内存分配参数时,内核原本会无谓地重算各内存区域的低内存保留值(lowmem reserve)——本补丁让"读操作不再触发重算",只在实际写入参数时才重算,并顺带修复了两个问题:非法输入现在会返回错误(而不是静默接受)、写入多个值时不会因中间某个值非法导致数据被部分更新。这是纯逻辑修正,作者未提供 benchmark 数据。

📋 补丁基本信息

项目内容
补丁类型优化(消除读操作的无谓重算)+ 修复(错误传播、部分写入)
状态In Review(v4,已有维护者 Ack)
当前版本v4 · lore 链接
版本演进v1(07-28)→ v2(07-31)→ v3(08-01)→ v4(08-02)
作者机构Jianlin Shi(shijianlin11@foxmail.com)
提交日期2026-08-02(v4)
改动范围mm/page_alloc.c,+16/-8 行,1 文件
核心函数lowmem_reserve_ratio_sysctl_handler() / proc_dointvec_minmax() / setup_per_zone_lowmem_reserve()
原始链接lore Message-ID

📊 速览卡片

核心机制
读不触发重算
优化目标
消除无效计算
适用场景
sysctl 读操作
关键修复
错误传播

🎯 解决什么问题

背景 / 原始动机:sysctl 读操作做了写操作才需要的计算
先讲问题:内核暴露了一个可调参数 lowmem_reserve_ratio(低内存保留比例),用于控制内存分配时各区域保留多少空间以防内存耗尽。它通过 sysctl(/proc/sys/vm/ 下的虚拟文件)供管理员读写。

问题所在:这个参数的处理器 lowmem_reserve_ratio_sysctl_handler() 无论读还是写,都调用 setup_per_zone_lowmem_reserve() 去重算各区域的保留值。但**读操作只是查看当前值,值根本没变**——重算完全是白做。这与其他 sysctl(如 min_free_kbytes)的处理方式不一致,后者只在写入时重算。

作者动机:作者发现这个不一致,顺带发现两个相关缺陷:读操作也不检查 proc_dointvec_minmax() 的返回值(非法输入被静默接受);写入多个值时若中间某个非法,前面的合法值已被写入导致数据不一致。补丁一并修复。
系统层面:读操作触发无谓的 per-zone 重算
机制背景:内存管理里,每个内存区域(zone)都有一个 lowmem_reserve[] 数组,表示该区域为防内存耗尽保留的内存量;还有一个全局的 totalreserve_pages 统计总保留页。这些值由 setup_per_zone_lowmem_reserve() 计算,并在内存分配失败回退(fallback)和脏页限制(dirty-limit)计算中被使用。

问题链条:原处理器在**每次读操作**时也重算这些值——遍历所有 zone、重写 lowmem_reserve[]、重算 totalreserve_pages。但读操作值没变,这些计算的结果与上次完全相同,纯属浪费。
场景层面:谁会被影响
直接场景:管理员或监控脚本**频繁读取** /proc/sys/vm/lowmem_reserve_ratio(如巡检、监控、容器编排读取系统内存参数)。每次读取都触发一次全 zone 重算。

为什么此场景受影响:读操作本身高频(监控可能每秒读),而重算涉及遍历所有 zone 并写共享的 lowmem_reserve[] / totalreserve_pages——在 NUMA 多节点系统上 zone 数量多,重算成本被放大。补丁前读操作虽不改变结果,但占用了本可避免的计算和写锁时间。
受影响负载:高频读取 sysctl 参数的监控/巡检场景 · 为什么此特性解决此场景:读操作直接返回,不再触发重算

🧩 核心机制

改造 lowmem_reserve_ratio_sysctl_handler(),让"读"与"写"分道:读直接返回,写才重算;写入用临时数组先解析再提交。

从系统层面看
模块:mm/page_alloc.c 的 lowmem_reserve_ratio_sysctl_handler()(sysctl 参数处理器)。

改了啥(三处):
① **读/写分离**:if (!write) return proc_dointvec_minmax(...)——读操作直接返回,不重算;
② **错误传播**:把 proc_dointvec_minmax() 的返回值存 rc,非 0 立即返回(原来忽略返回值,非法输入被静默接受);
③ **临时数组防部分写入**(v4 新增):写入时先 memcpy 到临时数组 ratio,用 tmp.data = ratio 让 proc_dointvec_minmax(&tmp,...) 解析到临时区,全部合法后才 memcpy 回 sysctl_lowmem_reserve_ratio——避免中间值非法时前面的合法值已被写入(数据不一致)。

为什么这么改 / 解决什么技术问题:核心是消除"读操作的无谓计算"——读不改值,重算结果必然相同,纯属浪费。错误传播解决"非法输入被静默接受"(如写非数字文本,原代码返回成功但值没变)。临时数组解决"部分写入不一致"——proc_dointvec_minmax() 边解析边写入,若第 3 个值非法,前 2 个已被改。

为什么支撑场景提升:读操作从"遍历所有 zone 重算 + 写共享变量"降为"直接返回",高频读取场景消除了每次读的无效计算。
lowmem_reserve_ratio sysctl 处理对比
图 1:补丁前读操作也触发 per-zone 重算;补丁后读直接返回、写用临时数组先解析
来源:基于 lore 真实补丁 diff(v4)绘制
步骤操作目的
读操作if (!write) return proc_dointvec_minmax(...)读直接返回,不重算
错误传播rc = proc_dointvec_minmax(...); if (rc) return rc;非法输入返回 -EINVAL
临时解析tmp.data = ratio; proc_dointvec_minmax(&tmp,...)先解析到临时区,防部分写入
提交memcpy(sysctl_lowmem_reserve_ratio, ratio, sizeof)全部合法才更新真实值 + 重算

🔬 关键代码

+	if (!write)
+		return proc_dointvec_minmax(table, write, buffer, length, ppos);
 
-	proc_dointvec_minmax(table, write, buffer, length, ppos);
+	memcpy(ratio, sysctl_lowmem_reserve_ratio, sizeof(ratio));
+	tmp.data = ratio;
 
-	for (i = 0; i < MAX_NR_ZONES; i++) {
-		if (sysctl_lowmem_reserve_ratio[i] < 1)
-			sysctl_lowmem_reserve_ratio[i] = 0;
-	}
+	rc = proc_dointvec_minmax(&tmp, write, buffer, length, ppos);
+	if (rc)
+		return rc;
 
+	memcpy(sysctl_lowmem_reserve_ratio, ratio, sizeof(ratio));
 	setup_per_zone_lowmem_reserve();
 	return 0;

▲ 核心三处:① 读操作直接返回不重算;② 传播解析错误;③ 用临时数组 ratio 先解析全部合法后再 memcpy 回真实值(防部分写入不一致)。这是 Andrew Morton review 后 v4 的关键改进。

📈 性能影响

数据出处:补丁作者未提供 benchmark 数据。收益为逻辑层面的消除无效计算,量化需要针对"高频读取 sysctl"场景自行测量。

场景/用例运行环境改进前改进后
读取 /proc/sys/vm/lowmem_reserve_ratio(监控高频场景)NUMA 多节点(zone 数量多)每次读触发全 zone 重算 + 写共享变量读直接返回,零计算

说明:补丁未提供基准数字,此项为逻辑分析(解读(AI 分析)):读操作从 O(N_zone) 计算 + 写共享变量降为 O(1) 直接返回。错误传播和防部分写入是正确性修复,非性能优化。

on-CPU vs off-CPU 视角(解读(AI 分析)):本补丁消除的是 **on-CPU 无效计算**——读 sysctl 时不再遍历 zone 重算。它不涉及锁等待/阻塞(off-CPU),是纯 CPU 指令节省。

🔄 方案演进

本补丁从最初提出到 v4 的演进过程(基于 lore 各版本真实信息):

v1 → v2 → v3 → v4 演进脉络
v1(07-28):读操作不重算 + 传播 proc_dointvec_minmax() 错误。作者用 if (write) 包裹 sanitization 和重算。
v2(07-31):采纳 Vlastimil Babka 建议——去掉手动 sanitization 循环(<1 → 0),改在 ctl_table 条目加 .extra1 = SYSCTL_ZERO,让 proc_dointvec_minmax() 强制最小值。
v3(08-01):Johannes Weiner 建议修正 changelog(分清"报错"和"避免无谓重算"两个独立效果),并 Acked-by。
v4(08-02):Andrew Morton 指出错误传播引入"部分写入不一致"新问题,建议用临时 buffer——v4 采纳,用临时数组 ratio 先解析再 memcpy。
设计权衡 / 讨论推进
Vlastimil Babka 的贡献:把"手动 sanitization"改为"依赖 sysctl 框架的 minmax 校验",更简洁且复用内核机制。
Andrew Morton 的关键质疑:错误传播后,proc_dointvec_minmax() 边解析边写真实数组,若写入的多个值中第 N 个非法,前 N-1 个已被修改——数据不一致。他建议"用临时 buffer 全部合法再拷贝"。v4 正是这么做。
Johannes Weiner 的 Ack:v2 代码已认可(Acked-by),但要求 changelog 明确区分两个独立效果,避免误导。

💬 讨论焦点

质疑:错误传播后可能引入"部分写入不一致"(Andrew Morton)
Andrew Morton(内存子系统维护者)在 v3 review 中指出一个**新引入的缺陷**:
  1. v3 之前,处理器忽略 proc_dointvec_minmax() 的返回值——非法输入被静默接受(这是个 bug)
  2. v3 加了错误传播后,暴露了 proc_dointvec_minmax() 自身的缺陷:它边解析边写入真实数组 sysctl_lowmem_reserve_ratio[]
  3. 若用户一次写入多个值、其中第 N 个非法,前 N-1 个合法值已被写入,返回错误但数据已部分改变——不一致
  4. Andrew 建议:"pass proc_dointvec_minmax() a temporary then copy into sysctl_lowmem_reserve_ratio if all OK"
来源:Andrew Morton review(08-01) · v4 已按此建议修复(临时数组)
维护者认可:Johannes Weiner 的 review 与 Ack
Johannes Weiner 在 v2 review 中评价了补丁的主要效果("This appears to be the primary user-visible effect"——写非数字文本现在返回错误而非静默成功),认可了 .extra1=SYSCTL_ZERO 的清理("That's a nice cleanup"),并对 changelog 提出建议(区分"报错"与"避免无谓重算"两个独立效果),最终 Acked-by: Johannes Weiner。

⚠️ 风险与局限

潜在回归 / 边界
兼容性边界(作者在 changelog 中说明):补丁前,读操作也会重算 lowmem_reserve[] 和 totalreserve_pages。极少数用户可能依赖"读操作顺便刷新这些派生值"这一**未文档化**的副作用——补丁后这些值只在写入时更新。

影响面:受影响的是依赖"读刷新"的异常用户;正常读者(只看值)不受影响。这些派生值在下次写入或内存页数变化(adjust_managed_page_count())时仍会刷新。

严重度:MINOR(仅影响依赖未文档化行为的边缘场景,且这些值有替代刷新路径)。

🔗 交叉引用

📌 关联工作
Vlastimil Babka review(v2) — 提出用 .extra1=SYSCTL_ZERO 替代手动 sanitization 循环
Johannes Weiner review(v2) — Acked-by,建议修正 changelog
Andrew Morton review(v3) — 指出"部分写入不一致",推动 v4 用临时数组
Baoquan He(2020)— only tune sysctl_lowmem_reserve_ratio once — 历史关联:2020 年曾把"<1 → 0"的 sanitization 从 setup_per_zone_lowmem_reserve() 移进 sysctl 处理器;本补丁 v2 又将其移除、改由框架 .extra1=SYSCTL_ZERO 强制——完成一次"手动 sanitization 迁回框架"的闭环

✅ 关键洞察

  • 发现:读 sysctl 参数不该触发 per-zone reserve 重算——值没变,重算必然相同,纯属浪费(on-CPU 无效计算)
  • 证据:补丁未提供基准数据;收益为逻辑层面(读从 O(N_zone) 计算降为 O(1) 直接返回)
  • 边界:依赖"读操作顺便刷新派生值"的未文档化行为会受影响(MINOR,有替代刷新路径)
  • 风险 / 建议:这是 v4 待维护者最终合入的 review 中补丁;Andrew 的"临时 buffer"建议已修复部分写入问题,但 proc_dointvec_minmax() 自身的"边解析边写"缺陷仍未根治(Andrew 称之为该函数的固有 flaw)
⚠️ 免责声明

本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。