服务反复使用的 100 MiB 热数据,被一次 2 GiB 文件扫描挤走;回收统计很好看,业务却不断重读。回收的目标不仅是释放页面,还要考虑内容怎样恢复、谁承担停顿、释放后是否很快又需要它。
先按内容后备解释候选,再推演工作集、换出与并发换入,最后把压缩、整理和观测接到同一条线上。
| 要解释的现象 | 阅读位置 |
| 什么页可以丢弃,什么内容必须保存? | 第 1 节:后备与引用 |
| 回收成功很多,业务为何更慢? | 第 2~3 节:refault 与停顿承担者 |
| 换出后,多个线程怎样找到同一份内容? | 第 4 节:swap 身份与并发恢复 |
| 压缩、整理、增加容量分别解决什么? | 第 5~7 节:资源账与诊断 |
1. 先问内容从哪里恢复,再问它有多冷
对一个未修改的普通文件页,如果后端能重新提供内容,内存副本在解除相关引用后可以丢弃。若页里含尚未写回的修改,直接丢弃会破坏文件语义,需要按写回协议推进。匿名内存则通常没有一个原始文件可重读;其中需保留的内容必须有 swap 等后备,或者已经有仍有效的后备副本。
“匿名页必须每次写磁盘才能释放”也过于绝对。已有有效 swap 副本、压缩缓存承接、应用明确放弃内容以及整个映射生命周期结束,处理路径都不同。反过来,tmpfs 虽然以文件接口呈现,内容也不能凭普通磁盘文件那样随意丢弃,它的 shmem 后备关系更接近可交换内存。
冷热、内容后备与引用,是三个独立维度:
| 候选页 | 看起来有利的条件 | 仍要解决的问题 |
| 很冷,但被设备长期 pin | 近期不常访问 | 引用可能阻止回收 |
| 很热的干净文件页 | 内容可以重读 | 丢弃后很快重读,代价可能很高 |
| 含唯一有效内容的匿名页 | 可能已经很冷 | 必须保留可恢复的后备 |
因此候选选择不能永远固定为“干净文件、脏文件、匿名”;应同时考虑释放条件和未来恢复代价。
下面先不讨论淘汰策略,只检查“这份内容能否安全离开 RAM”。尝试保存、修改、回收与再次访问,观察新修改怎样让原来的后备副本失效。
正在呈现知识画面
回收之前,这份内容还能从哪里找回?内容可恢复与没有阻止回收的引用,是释放驻留页的必要条件。修改会使旧后备过期;成功回收后马上恢复,也可能付出重复工作的代价。
{"id":"reclaim-backing-and-refault","title":"回收之前,这份内容还能从哪里找回?","summary":"内容可恢复与没有阻止回收的引用,是释放驻留页的必要条件。修改会使旧后备过期;成功回收后马上恢复,也可能付出重复工作的代价。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"page-choices\"><button data-page=\"0\"><strong>A</strong><span data-i18n=\"fileClean\"></span></button><button data-page=\"1\"><strong>B</strong><span data-i18n=\"fileDirty\"></span></button><button data-page=\"2\"><strong>C</strong><span data-i18n=\"anon\"></span></button><button data-page=\"3\"><strong>D</strong><span data-i18n=\"pinned\"></span></button></div><div class=\"two\"><section class=\"card\"><header data-i18n=\"resident\"></header><div class=\"content-value\" id=\"resident\"></div><label class=\"check\"><input id=\"pin\" type=\"checkbox\"><span data-i18n=\"pin\"></span></label></section><section class=\"card\"><header data-i18n=\"backing\"></header><div class=\"content-value\" id=\"backing\"></div><small id=\"backing-state\"></small></section></div><div class=\"actions\"><button id=\"access\" data-i18n=\"access\"></button><button id=\"write\" data-i18n=\"write\"></button></div><div class=\"actions\"><button id=\"preserve\" data-i18n=\"preserve\"></button><button id=\"reclaim\" data-i18n=\"reclaim\"></button></div><div class=\"metrics\"><div><span data-i18n=\"reclaimed\"></span><strong id=\"reclaimed\"></strong></div><div><span data-i18n=\"refaults\"></span><strong id=\"refaults\"></strong></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"blockedPin\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"blockedBacking\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"saved\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"modified\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"freed\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"restored\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"hit\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"notResident\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.page-choices{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:16px 0}.page-choices button{display:flex;align-items:center;gap:10px;text-align:left;justify-content:flex-start;min-width:0;font-size:12px;min-height:5.5em}.page-choices strong{font-size:18px}.page-choices .chosen{background:var(--accent-soft);border-color:var(--accent)}.content-value{padding:18px 5px;background:var(--surface);border:1px dashed var(--rule);border-radius:7px;text-align:center;font:500 26px var(--viz-font-ui);min-height:3.2em}.content-value.valid{background:var(--accent-soft);border-color:var(--accent)}#backing-state{min-height:4.8em;margin-top:12px}.card header{min-height:3.2em}.card .check{align-items:flex-start;font-size:12px;min-height:4.8em}.card .check span{min-width:0}.card .check input{margin-top:2px}","js":"const $=s=>document.querySelector(s);const initial=()=>[{value:1,backing:1,resident:true,pin:false},{value:2,backing:1,resident:true,pin:false},{value:3,backing:null,resident:true,pin:false},{value:4,backing:4,resident:true,pin:true}];let pages=initial(),chosen=2,reclaims=0,refaults=0,message='initial';function draw(){const p=pages[chosen],valid=p.backing===p.value;document.querySelectorAll('[data-page]').forEach(e=>{e.classList.toggle('chosen',+e.dataset.page===chosen);e.setAttribute('aria-current',String(+e.dataset.page===chosen));});$('#resident').textContent=p.resident?p.value:'—';$('#resident').classList.toggle('valid',p.resident);$('#backing').textContent=p.backing??'—';$('#backing').classList.toggle('valid',valid);$('#backing-state').textContent=viz.t(valid?'valid':'stale');$('#pin').checked=p.pin;$('#pin').disabled=!p.resident;$('#reclaimed').textContent=reclaims;$('#refaults').textContent=refaults;$('#explanation').textContent=viz.t(message);}document.querySelectorAll('[data-page]').forEach(e=>e.addEventListener('click',()=>{chosen=+e.dataset.page;message='initial';draw();}));$('#access').addEventListener('click',()=>{const p=pages[chosen];if(!p.resident){p.resident=true;refaults++;message='restored';}else message='hit';draw();});$('#write').addEventListener('click',()=>{const p=pages[chosen];if(!p.resident)message='notResident';else{p.value++;message='modified';}draw();});$('#preserve').addEventListener('click',()=>{const p=pages[chosen];if(!p.resident)message='notResident';else{p.backing=p.value;message='saved';}draw();});$('#reclaim').addEventListener('click',()=>{const p=pages[chosen];if(!p.resident)message='notResident';else if(p.pin)message='blockedPin';else if(p.backing!==p.value)message='blockedBacking';else{p.resident=false;reclaims++;message='freed';}draw();});$('#pin').addEventListener('change',()=>{pages[chosen].pin=$('#pin').checked;message='initial';draw();});$('#reset').addEventListener('click',()=>{pages=initial();reclaims=refaults=0;message='initial';draw();});draw();","audio":false,"strings":{"access":"访问这一页","anon":"匿名页","backing":"可恢复后备","blockedBacking":"当前驻留页含需要保留的唯一最新内容。直接丢弃会丢数据,必须先建立有效后备。","blockedPin":"pin 仍然阻止回收。后备是否有效与引用是否允许释放,是两项独立条件。","fileClean":"干净文件页","fileDirty":"已修改文件页","freed":"驻留副本已释放,但后备内容与身份还在。以后访问仍能恢复,所以并没有把需要的数据抹掉。","heading":"回收之前,这份内容还能从哪里找回?","hit":"内容仍驻留,当前访问无需从后备恢复。","initial":"先选中匿名页尝试回收,再保存后备。也可尝试固定页:内容可恢复,仍不代表引用允许释放。","intro":"选择一页,尝试修改、保存后备或回收。回收后再次访问,观察内容如何回来,以及 refault 计数怎样变化。","modified":"驻留内容已改变,旧后备不再是最新副本。干净/可恢复状态不能仅凭曾经保存过就一直沿用。","notResident":"本页已不驻留,无需再次回收。先访问恢复后,才有新的驻留副本可处理。","page":"选择页面","pin":"阻止回收的 pin","pinned":"被固定的文件页","preserve":"完成后备保存","reclaim":"尝试回收","reclaimed":"成功回收次数","refaults":"回收后重新访问","reset":"重置","resident":"驻留内容","restored":"访问重新取得后备内容。若很快又回收这页,可能不断重复恢复;回收成功不等于业务代价更低。","saved":"后备现在与驻留内容一致,可以恢复。后续再次修改,会让这个副本重新过期。","scope":"四页的教学状态,后备已成功保存后才允许丢弃驻留内容;省略真实锁、写回、rmap、TLB 和并发协议。file 后备指文件内容,anon 后备指交换身份关联的有效内容,不承诺恢复必定读盘。只演示可释放条件,不模拟 LRU/MGLRU 的具体选择策略或真实 refault 统计实现。","scopeLabel":"模型范围与约定","stale":"无有效副本","togglePin":"切换 pin 引用","valid":"有效","write":"修改内容"}}
2. 用一个工作集例子理解冷热判断
假设服务每轮反复访问 100 MiB 热数据,另有一次性扫描不断读入 2 GiB 文件。如果仅因为扫描刚发生就把所有新页都当成值得长期保留的热点,热工作集可能被挤走;下一轮业务又把它重新读回来。腾出的每一页看起来都成功了,但系统一直在做丢弃与重读的重复工作。
传统 active/inactive 机制用访问线索和队列位置近似区分使用情况,分开管理 anon/file 等类型;它不是记录每次访问的精确全局 LRU,也不是只有 5.x 才有的历史路径。MGLRU 在启用时以多代组织年龄信息,aging 通过页表访问位等线索发现近期使用,eviction 从较老候选推进,并结合 refault 等反馈调整选择。
一代不是每个页上的精确时间戳,也不等于所有页按固定秒数集体过期。扫描页表和反向映射仍有成本;MGLRU 改进了信息收集和组织方式,不是完全不扫描就知道所有页有多热。通过文件描述符发生的访问与通过页表发生的访问还会提供不同线索。见 MGLRU 设计。
Refault 为什么比一次成功回收更能说明问题
比较两个教学窗口,便能看到单看回收率会漏掉什么:
| 窗口 | 扫描 | 回收 | 随后迅速恢复的页 | 需要追问 |
| A | 10000 页 | 8000 页 | 7000 页 | 是否误伤了正在反复使用的内容? |
| B | 同样假设 10000 页 | 3000 页 | 很少 | 虽然回收率低,业务是否更稳定? |
A 的 scan/steal 很好看,却可能支付大量重复恢复成本;B 的较低回收量并不直接代表更差。
这不是把每次 refault 都判成错误:工作集本来就大于可用内存时,总有一些内容必须反复进出。反馈的价值在于判断候选组、类型和保护策略是否仍合适。仅把 pgsteal/pgscan 当作分配器评分,会漏掉恢复成本、页大小差异及应用延迟。
3. kswapd 与 direct reclaim:由谁支付回收时间
后台先做:为后续申请保留余量
后台 kswapd 试图在资源紧张前后恢复节点/zone 的可用余量,让未来申请更容易从快路径完成。常见简化模型是在低水位条件下唤醒,向高水位平衡,但真实目标受 order、zone 和其他策略影响。它不等于“free 一低于 high 就固定开始一轮”。
申请者自己做:回收进入本次延迟
若分配者不能从允许资源中取得页,而且 GFP 允许直接回收,它可以自己执行回收。这会把扫描、等待和其他工作算进本次申请的延迟;因此没有明显磁盘吞吐峰值,也可能出现明显内存停顿。direct reclaim 并不保证一直等待到成功,后续仍可能回退、失败或进入其他处理。
先确定资源域:全局还是 cgroup
回收范围同样重要。某个 memory cgroup 达到约束时,可以在其内部回收,即使全机还有很多空闲页。memory.high 相关节流、memory.max 的硬限制、NUMA 允许节点和普通全局内存压力不是同一问题。要先确认是哪一种资源域缺额度,再决定是否需要分析全局 LRU。
slab 对象不都位于普通页 LRU 中。内核通过 shrinker 请求具有回收能力的缓存子系统释放对象,例如不再需要的缓存项;子系统负责判断引用、锁和业务状态。若仍有活跃使用者,不能为了满足页分配就强制销毁任意对象。
4. 走完一次匿名页换出:需要同时保存内容和名字
设进程 P 的地址 A 指向物理页 F,其中有应用必须保留的数据。若只把 F 写到 swap,却让 PTE 继续允许写,应用可能在写出之后改变 F,使后备副本过期;若只清 PTE,却不记录内容在哪,之后缺页又无从恢复。
换出需要同时维护三类事实:
| 事实 | 对应的工作 |
| 内容在哪里恢复 | 建立 swap 身份并管理后备 |
| 哪些映射还能访问或修改它 | 通过反向映射协调 PTE、写保护与 TLB |
| 物理页何时能复用 | 完成必要写出,确认状态和引用允许释放 |
这三类工作靠并发锁和重试协议衔接。先写盘再任意修改 PTE,无法保证期间没有写入,也无法保证页已经无人使用。
反向映射回答“这份内容可能被哪些虚拟映射使用”,无需盲扫所有进程页表。文件映射可从 address_space 的映射索引找候选 VMA,匿名映射可借助 anon_vma 等关系;还需核对具体页表,候选 VMA 不等于已存在有效 PTE。fork、COW 和大 folio 又会改变共享与映射统计,引用计数不能代替精确枚举用户映射。
两个线程同时换入,为何不应各自得到互不协调的副本
当 PTE 带着 swap 身份 S 触发缺页,内核先需要判断相关内容是否已驻留、是否有人正在恢复。若另一个线程已把 S 的内容读到内存,后来者可以等待或复用适当结果,而不是无条件再次读存储。这样既减少重复 I/O,也保持共享内容、状态与引用的一致性。
swap cache 参与把 swap 身份与驻留 folio 联系起来。它是概念职责,不应把旧版 address_space/XArray 结构固定成所有新内核的实现。找到驻留对象也不等于可立即使用:I/O 是否完成、引用是否稳定、页是否仍对应那个身份都要确认。安装最终映射后,swap 槽何时释放还需考虑其他引用,不能按“一次换入就立刻删除后备”推理。
这解释了为什么 swap-in fault 不一定产生磁盘 I/O:内容可能仍在内存或压缩后备中。大页换出也不必永远先拆成 4 KiB,支持路径与连续后备条件随内核演进,应以当前实现为准。
5. zswap、zram 和 swappiness 各自控制什么
zswap 是 swap 路径上的压缩内存缓存,通常与已配置的 swap 后备协作。压缩成功可以用较少 RAM 保留较多内容,缓存压力下可向后备写出;不可压缩数据和达到上限时又会出现不同处理。它不是无需 swap 身份的通用文件缓存。见 zswap。
zram 暴露压缩内存块设备,可以被配置为 swap,也可用于其他块设备用途。它的逻辑 disksize 不等于已经预留了同样多 RAM;压缩数据和元数据仍占实际内存,部分配置还支持写回后备。不能把“8 GiB zram”直接加到物理内存上当作确定的额外容量。见 zram。
空间账:100 MiB 压成 40 MiB,净收益是多少
忽略元数据,净腾出的是 60 MiB。若只能压成 95 MiB,收益就只剩 5 MiB,却仍支付压缩/解压 CPU 成本。工作集反复访问时,这些恢复工作也可能进入请求长尾。
swappiness:比较成本,不设置使用率阈值
swappiness 表达 swap 与文件页回收成本的相对偏好,当前接口范围为 0 到 200,不能当作“内存用到百分之多少才开始 swap”。在压缩内存后备等相对较便宜的环境中,超过 100 可以有其成本依据;0 也不是关闭 swap 的开关。调节时要看真实重读、换入与 CPU 代价,不能把固定 60 或 0 当作所有工作负载的答案。见 VM sysctl。
6. 回收与内存整理解决两个不同约束
| 手段 | 改变什么 | 没有自动解决什么 |
| 页面回收 | 让内容不再占据当前物理页,增加可用数量 | 不保证形成所需连续块 |
| compaction 整理 | 移动保留内容,聚合分散空闲页 | 通常不增加空闲总量 |
| zswap/zram 内容压缩 | 用较少 RAM 表示内容 | 不等于重新排列物理页 |
接着仍用 16 页模型,看数量够、形状不够时会发生什么。
假设 16 页中有 8 页空闲,但空闲与占用交错。order 0 申请有很多候选,order 3 却没有现成的 8 页连续块。若把可移动的占用页迁到另一侧,可能形成一个合适块,空闲总量仍是 8 页。迁移需要保持虚拟地址语义、处理页表和相关引用;遇到 pin 或不可迁移对象时,这个理想布局未必能达到。见 页面迁移。
kcompactd 与分配者的 direct compaction 分担整理工作,类似后台/同步两种成本承担方式。高阶申请失败、compaction 活跃,不必意味着机器完全没有可用内存;而大量匿名工作集缺容量,也不能仅靠把页排列得更整齐解决。
OOM:失败路径还受申请上下文约束
当允许范围内的回收无法取得足够进展时,部分申请可能进入 OOM 处理,但并不是每次高阶或 GFP_ATOMIC 失败都触发 OOM。OOM 杀死一个任务后,页也要随着引用释放才能逐步变得可用;被外部固定的页或其他共享引用仍可能延后资源回收。判断应结合 OOM 报告中的约束域、申请上下文和受害者信息。
7. 观察一个时间窗口,而不是比较开机累计大数
下面只读取统计,间隔 0.2 秒,不主动申请大量内存、回收缓存或配置 swap。它把扫描与回收的同类字段成对展示,并读取 MGLRU 的实际启用位。
from pathlib import Path
import time
def vmstat():
return {key: int(value) for key, value in
(line.split() for line in Path("/proc/vmstat").read_text().splitlines())}
before = vmstat()
start = time.monotonic()
time.sleep(0.2)
after = vmstat()
elapsed = time.monotonic() - start
for suffix in ("kswapd", "direct"):
scan_key, steal_key = "pgscan_" + suffix, "pgsteal_" + suffix
if scan_key in before and steal_key in before:
scan = after[scan_key] - before[scan_key]
steal = after[steal_key] - before[steal_key]
print(suffix, "scan=", scan, "steal=", steal,
"ratio=", steal / scan if scan > 0 else "无扫描")
for key in ("pswpin", "pswpout", "compact_stall", "compact_success", "compact_fail"):
if key in before and key in after:
print(key, after[key] - before[key])
print("window_seconds", elapsed)
for name in ("/sys/kernel/mm/lru_gen/enabled", "/proc/pressure/memory"):
try:
print(name, Path(name).read_text().strip())
except OSError as error:
print(name, type(error).__name__)
计数可能因内核配置而缺失;无活动窗口的零值只说明此窗口未观测到对应事件。MGLRU 的 enabled 是位掩码,应按接口文档解释各位;目录存在仅说明接口暴露,不代表主功能已启用。见 MGLRU 管理接口。
PSI memory 的 some 描述至少有任务因该资源停顿的时间,full 描述相关范围内所有非空闲任务都受阻的时间;它测量停顿,不是 RAM 使用率。累计 total 的单位为微秒,avg10/60/300 是相应窗口平均;全局与 cgroup 范围不能混算。见 PSI。
读完计数后,按因果顺序继续定位:
- 确认约束域。 压力属于全局、cgroup,还是特定允许节点?
- 确认停顿位置。 direct reclaim 是否把工作压到了申请者路径?
- 比较同一窗口。 联看扫描、回收、refault、swap、compaction 与业务延迟。
anon/file 与 kswapd/direct 是不同分组维度,不能把这些计数不加区分地全部相加。只有约束、恢复代价与业务停顿对应起来,才能判断该改善局部性、扩大容量,还是减少不必要的连续分配要求。