mm/page_alloc: only update lowmem_reserve_ratio on sysctl write
💡 一句话总结
在管理员读取 /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 |
📊 速览卡片
🎯 解决什么问题
lowmem_reserve_ratio(低内存保留比例),用于控制内存分配时各区域保留多少空间以防内存耗尽。它通过 sysctl(/proc/sys/vm/ 下的虚拟文件)供管理员读写。问题所在:这个参数的处理器
lowmem_reserve_ratio_sysctl_handler() 无论读还是写,都调用 setup_per_zone_lowmem_reserve() 去重算各区域的保留值。但**读操作只是查看当前值,值根本没变**——重算完全是白做。这与其他 sysctl(如 min_free_kbytes)的处理方式不一致,后者只在写入时重算。作者动机:作者发现这个不一致,顺带发现两个相关缺陷:读操作也不检查
proc_dointvec_minmax() 的返回值(非法输入被静默接受);写入多个值时若中间某个非法,前面的合法值已被写入导致数据不一致。补丁一并修复。
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 数量多,重算成本被放大。补丁前读操作虽不改变结果,但占用了本可避免的计算和写锁时间。
🧩 核心机制
改造 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 重算 + 写共享变量"降为"直接返回",高频读取场景消除了每次读的无效计算。
来源:基于 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 各版本真实信息):
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。
Andrew Morton 的关键质疑:错误传播后,
proc_dointvec_minmax() 边解析边写真实数组,若写入的多个值中第 N 个非法,前 N-1 个已被修改——数据不一致。他建议"用临时 buffer 全部合法再拷贝"。v4 正是这么做。Johannes Weiner 的 Ack:v2 代码已认可(Acked-by),但要求 changelog 明确区分两个独立效果,避免误导。
💬 讨论焦点
- v3 之前,处理器忽略
proc_dointvec_minmax()的返回值——非法输入被静默接受(这是个 bug) - v3 加了错误传播后,暴露了
proc_dointvec_minmax()自身的缺陷:它边解析边写入真实数组sysctl_lowmem_reserve_ratio[] - 若用户一次写入多个值、其中第 N 个非法,前 N-1 个合法值已被写入,返回错误但数据已部分改变——不一致
- Andrew 建议:"pass proc_dointvec_minmax() a temporary then copy into sysctl_lowmem_reserve_ratio if all OK"
.extra1=SYSCTL_ZERO 的清理("That's a nice cleanup"),并对 changelog 提出建议(区分"报错"与"避免无谓重算"两个独立效果),最终 Acked-by: Johannes Weiner。
⚠️ 风险与局限
lowmem_reserve[] 和 totalreserve_pages。极少数用户可能依赖"读操作顺便刷新这些派生值"这一**未文档化**的副作用——补丁后这些值只在写入时更新。影响面:受影响的是依赖"读刷新"的异常用户;正常读者(只看值)不受影响。这些派生值在下次写入或内存页数变化(
adjust_managed_page_count())时仍会刷新。严重度:MINOR(仅影响依赖未文档化行为的边缘场景,且这些值有替代刷新路径)。
🔗 交叉引用
.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 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。