行业资讯
Workingset 检测机制与 Refault 分析
Workingset 检测机制与 Refault 分析一、双 CLOCK 链表每个 NUMA 节点维护 inactive 和 active 两个链表fault ────────────────────── | -------------- | ------------- reclaim - | inactive | --- 降级 | active | -- -------------- ------------- | | | -------------- 提升 -----------------------新 fault 页面从 inactive 头部插入回收从 inactive 尾部扫描在 inactive 上被再次访问 → 提升到 activeactive 太大 → 降级回 inactiveshrink_active_list二、Refault 距离核心思想精确追踪每个页面的访问频率代价太高但可以通过 refault 距离做合理近似。nonresident_age 计数器每个 lruvec 维护一个单调递增计数器lruvec-nonresident_age。以下事件会推进这个计数器页面从 inactive list 被驱逐时nr_pages在workingset_eviction里页面从 inactive list 被激活时nr_pages在workingset_age_nonresident里本质上这个计数器记录的是有多少个 inactive 页面被访问过无论是驱逐还是激活。E 和 R 的具体含义EEviction snapshot页面被驱逐那一刻的nonresident_age值。// mm/workingset.c:411 (workingset_eviction)evictionatomic_long_read(lruvec-nonresident_age);// ← 这就是 Eworkingset_age_nonresident(lruvec,folio_nr_pages(folio));returnpack_shadow(...,eviction,...);// E 编码进 shadow entryRRefault snapshot页面 refault 回来那一刻的nonresident_age值。// mm/workingset.c:493 (workingset_test_recent)refaultatomic_long_read(eviction_lruvec-nonresident_age);// ← 这就是 R距离计算// mm/workingset.c:511refault_distance(refault-eviction)EVICTION_MASK;// R - ER - E的物理含义从页面被驱逐到页面 refault 回来这段时间内有多少个 inactive 页面被访问过驱逐 激活的总和。这个数字近似于如果 inactive list 足够长需要多长才能把这个页面留住。判定逻辑// mm/workingset.c:521-536workingset_sizeNR_ACTIVE_FILE;if(!file)workingset_sizeNR_INACTIVE_FILE;if(has_swap)workingset_sizeNR_ACTIVE_ANON(file?NR_INACTIVE_ANON:0);returnrefault_distanceworkingset_size;// (R-E) ≤ 可用内存距离 ≤ workingset_size含义如果 inactive list 能再大 (R-E) 个槽位这个页面就不会被驱逐而系统中确实有这么多页面active 页面占着的空间可以腾出来结论值得激活与 active 页面竞争空间操作直接放 active list距离 workingset_size含义就算把全部可用内存都当 inactive list也留不住这个页面结论访问间隔太长激活没意义操作当普通 fault 处理放 inactive list 头部举例内存: inactive 4 页, active 4 页, 共 8 页 nonresident_age 当前值 200 时刻 T1: 页面 P 被驱逐 E atomic_long_read(lruvec-nonresident_age) 200 shadow entry 里存 E200 ...期间其他 5 个 inactive 页面被驱逐或激活... nonresident_age 变成 205 时刻 T2: 页面 P refault 回来 R atomic_long_read(lruvec-nonresident_age) 205 refault_distance R - E 205 - 200 5 workingset_size NR_ACTIVE_FILE 4假设无 swap 5 ≤ 4? → 否 → 不激活P 放 inactive ──────────────── 另一个页面 Q: E 200, R 203 refault_distance 3 3 ≤ 4? → 是 → 激活Q 放 active三、Workingset 标记降级时打标记shrink_active_list将页面从 active 降级到 inactive 时folio_clear_active(folio);// 清掉 active 标志folio_set_workingset(folio);// 打标记曾在 active list 上list_add(folio-lru,l_inactive);驱逐时编码页面被驱逐时workingset 标记连同 eviction count 一起编码进 shadow entry。Refault 时区分路径refault 时从 shadow entry 解码出 workingset 标记用于区分两种 refault 类型。完整代码流程shrink_active_list() [mm/vmscan.c:2068] │ ├─ isolate_lru_folios() 从 active LRU 取出一批页面到 l_hold │ ├─ 遍历 l_hold 中每个 folio: │ │ │ ├─ folio_referenced() 检查是否被访问过 │ │ │ │ │ ├─ 有引用 VM_EXEC file页 → 保留 activerotate │ │ │ list_add(folio-lru, l_active) │ │ │ │ │ └─ 其他情况 → 降级到 inactive │ │ folio_clear_active(folio) ← 清 PG_active │ │ folio_set_workingset(folio) ← 设 PG_workingset │ │ list_add(folio-lru, l_inactive) │ │ │ └─ !folio_evictable → folio_putback_lru (unevictable) │ ├─ move_folios_to_lru(l_active) → 放回 active LRUPG_active1 ├─ move_folios_to_lru(l_inactive) → 放回 inactive LRUPG_active0 │ │ move_folios_to_lru 内部: │ lruvec_add_folio(lruvec, folio) │ → folio_lru_list(folio) 根据 PG_active/PG_swapbacked 选 LRU │ → list_add 到对应 lruvec-lists[lru] │ └─ 统计 PGDEACTIVATE, 更新 NR_ISOLATED驱逐与 Shadow Entry 编码流程shrink_inactive_list() [mm/vmscan.c:1986] │ ├─ isolate_lru_folios() 从 inactive LRU 取出页面 │ └─ shrink_folio_list() [mm/vmscan.c:1209] │ └─ 对每个页面尝试回收 │ ├─ 文件页且 clean → __remove_mapping() │ │ │ └─ workingset_eviction(folio, target_memcg) [mm/workingset.c] │ │ │ ├─ 读取 folio_test_workingset(folio) → 得到 workingset 标记 │ ├─ 读取 nonresident_age 计数器作为 eviction count │ ├─ 将 (workingset | node | memcg_id | eviction) 编码 │ └─ 写入 page cache 的 shadow entry (xarray slot) │ └─ 匿名页 → swap out → shadow entry 存入 swap cacheRefault 解码与激活流程filemap_fault() / do_swap_page() │ └─ 发现 shadow entry曾被驱逐的页面 │ └─ workingset_refault(folio, shadow) [mm/workingset.c:555] │ ├─ 解码 shadow entry → 得到 eviction count workingset 标记 │ ├─ WORKINGSET_REFAULT [所有 refault 都计] │ ├─ workingset_test_recent(shadow, ...) │ │ │ ├─ 计算 refault 距离 (R - E) │ ├─ 比较距离 vs 可用内存 (NR_inactive NR_active) │ │ │ ├─ 距离 可用内存 → return false → goto out不激活 │ └─ 距离 ≤ 可用内存 → return true 传出 workingset 值 │ ├─ folio_set_active(folio) [放入 active list] ├─ WORKINGSET_ACTIVATE │ └─ if (workingset true): [曾在 active 上] ├─ folio_set_workingset(folio) [保留标记供下次使用] ├─ lru_note_cost_refault(folio) [增加回收代价权重] └─ WORKINGSET_RESTORE四、Shadow Entry是什么Shadow entry 是页面被驱逐后留在原来位置上的墓碑标记。文件页在 page cache 里的存储结构是 xarraymapping-i_pages索引是文件偏移量。正常 slot 存 folio 指针驱逐后改存一个特殊的非指针值mapping-i_pages (xarray): offset 0: [folio *] ← 还在内存中 offset 1: [shadow entry] ← 页面被驱逐了留个墓碑 offset 2: [folio *] offset 3: [shadow entry]编码格式通过pack_shadow()把信息压缩进一个unsigned long┌──────────────────────────────────────────────────────────────┐ │ eviction_count │ memcg_id │ node_id │ workingset(1bit) │ tag │ └──────────────────────────────────────────────────────────────┘ 一个 unsigned long (64位)// mm/workingset.c:202staticvoid*pack_shadow(intmemcgid,pg_data_t*pgdat,unsignedlongeviction,bool workingset,bool file){evictionfile?EVICTION_MASK:EVICTION_MASK_ANON;eviction(evictionMEM_CGROUP_ID_SHIFT)|memcgid;eviction(evictionNODES_SHIFT)|pgdat-node_id;eviction(evictionWORKINGSET_SHIFT)|workingset;returnxa_mk_value(eviction);}各字段含义eviction_count驱逐时 nonresident_age 的快照用于计算 refault 距离memcg_id属于哪个 memory cgroupnode_id在哪个 NUMA 节点被驱逐workingset驱逐前是否在 active list 上待过1 bittagxarray 用最低位标识这是 value 不是指针xa_mk_value什么时候写入shrink_folio_list() → __remove_mapping() → workingset_eviction(folio) → pack_shadow(...) → 存入 xarray slot什么时候读取下次访问同一文件偏移时filemap_fault在 xarray 里找到 shadow entry// mm/filemap.centry__filemap_get_folio(mapping,index,...);if(xa_is_value(entry)){// 这是 shadow entry不是真实页面// 分配新页面读入数据// 调 workingset_refault(folio, shadow) 判断是否激活}为什么不直接清空 slot如果驱逐时直接清空refault 时就不知道这个页面曾经来过——无法计算 refault 距离无法做 workingset 判定。Shadow entry 就是为了在页面离开内存后保留这一点历史信息。生命周期页面 fault 进入 → xarray slot 存 folio 指针 页面被驱逐 → slot 改存 shadow entry墓碑 页面 refault 回来 → 读 shadow entry 做判定然后替换为新 folio 指针 shadow entry 过多时 → shadow entry shrinker 清理过期条目避免内存浪费五、PG_workingset 与 PG_active 标志位流程图标志位含义PG_active页面当前在 active list 上受保护不会被直接回收PG_workingset页面曾经在 active list 上待过历史标记用于 refault 判定完整生命周期流转含 referenced 判定细节首次 fault │ ▼ ┌───────────────────────┐ ┌──▶│ Inactive List │ │ │ PG_active0 │ │ │ PG_workingset0 │ │ │ PG_referenced0 │ │ └───────────┬────────────┘ │ │ │ ▼ │ shrink_folio_list() 扫描 inactive 尾部 │ │ │ ▼ │ folio_check_references(folio, sc) [vmscan.c:860] │ │ │ ├─ folio_referenced(folio, 1, memcg, vm_flags) │ │ 扫描所有映射该页的 PTE 的 accessed bit │ │ 返回 referenced_ptes 数量收集 vm_flags │ │ │ ├─ VM_LOCKED → FOLIOREF_ACTIVATE移到 unevictable │ │ │ ├─ referenced_ptes 0: │ │ │ │ │ ├─ folio_set_referenced(folio) ← 设 PG_referenced │ │ │ │ │ ├─ PG_referenced 之前已设 OR pte引用1: │ │ │ → FOLIOREF_ACTIVATE两次访问确认 → 提升 │ │ │ │ │ ├─ VM_EXEC file 页: │ │ │ → FOLIOREF_ACTIVATE可执行代码优先提升 │ │ │ │ │ └─ 其他: → FOLIOREF_KEEP再给一轮机会 │ │ │ └─ referenced_ptes 0: │ ├─ PG_referenced file页 → FOLIOREF_RECLAIM_CLEAN │ └─ 其他 → FOLIOREF_RECLAIM可以回收 │ │ 根据返回值: │ │ │ ├─ FOLIOREF_ACTIVATE → 提升到 Active List │ │ folio_set_active(folio) PG_active1 │ │ ┌─────────────────────┐ │ │ │ Active List │ │ │ │ PG_active1 │ │ │ └──────────┬────────────┘ │ │ │ (见下方 shrink_active_list) │ │ ▼ │ │ │ ├─ FOLIOREF_KEEP → 留在 inactiverotate 到头部 │ │ stat-nr_ref_keep │ │ (下轮再扫时如果还有引用 → 双重确认 → 提升) │ │ │ ├─ FOLIOREF_RECLAIM → 回收 │ │ │ │ │ ▼ │ │ __remove_mapping() → workingset_eviction() │ │ pack_shadow(workingsetPG_workingset的值, Enonresident_age) │ │ 存入 xarray → [Shadow Entry] │ │ │ └─ FOLIOREF_RECLAIM_CLEAN → 仅回收 clean 页 │ │ │ ════════════════════════════════════════════ │ shrink_active_list() 降级流程 [vmscan.c:2068] │ ════════════════════════════════════════════ │ │ 从 Active List 取出一批页面逐个判断: │ │ │ ▼ │ folio_referenced(folio, 0, memcg, vm_flags) │ (注意第2个参数0不清除 PTE accessed bit) │ │ │ ├─ 返回 ! 0有引用: │ │ │ │ │ ├─ VM_EXEC file 页: │ │ │ → rotate 回 Active可执行代码额外保护 │ │ │ list_add(folio-lru, l_active) │ │ │ PG_active 保持1 │ │ │ │ │ └─ 其他有引用: │ │ → 依然降级active list 只给 exec 特殊待遇 │ │ ↓ 落入下方降级路径 │ │ │ └─ 返回 0无引用或非 exec 有引用: │ → 降级到 Inactive │ folio_clear_active(folio) PG_active: 1→0 │ folio_set_workingset(folio) PG_workingset: 0→1 │ list_add(folio-lru, l_inactive) │ │ │ ▼ │ ┌───────────────────────┐ │ │ Inactive List │ │ │ PG_active0 │ │ │ PG_workingset1 ← 新! │ │ └───────────┬────────────┘ │ │ │ ▼ │ (再次被 shrink_folio_list 扫描流程同上) │ 如果被回收: │ workingset_eviction() │ pack_shadow(workingset1, Enonresident_age) │ │ │ ▼ │ [Shadow Entry] │ workingset1 │ │ │ ▼ refault │ workingset_refault() │ │ │ 距离 ≤ 可用内存? │ ├─ 否 → 放 inactivePG_active0, PG_ws0 │ └─ 是: │ folio_set_active PG_active1 │ WORKINGSET_ACTIVATE │ workingset1? │ ├─ 否 → 仅 ACTIVATE │ └─ 是: │ folio_set_workingset PG_ws1 │ lru_note_cost_refault │ WORKINGSET_RESTORE │ → 放 Active List └────────────────────────── (循环)referenced 判定的两次确认机制在 inactive list 上的页面不会因为一次访问就提升到 active。需要两次确认才能提升防止 streaming IO 的一次性页面污染 active list第一次扫描: folio_referenced() 发现 PTE accessed1 → folio_set_referenced(folio) 设 PG_referenced 标记 → 返回 FOLIOREF_KEEP再给一次机会留在 inactive 第二次扫描: folio_referenced() 又发现 PTE accessed1 → referenced_ptes 0 且 referenced_folio1PG_referenced 已设 → 双重确认真的被反复访问 → 返回 FOLIOREF_ACTIVATE → 提升到 Active例外VM_EXEC的文件页代码段一次访问就提升代码执行比普通文件更值得保护多个 PTE 同时引用referenced_ptes 1一次扫描就提升共享页面更重要folio_mark_accessed不经过页表的访问标记除了folio_referenced()扫描 PTE accessed bit内核还有另一条路径标记页面访问folio_mark_accessed()。这是软件层面主动调用的不依赖硬件页表 accessed bit。调用场景文件读取路径: read() → filemap_read() → folio_mark_accessed() 文件 mmap fault: filemap_fault() → folio_mark_accessed() shmem 访问: shmem_get_folio() → folio_mark_accessed() GUP (get_user_pages): follow_page → folio_mark_accessed() splice / sendfile: splice_read → folio_mark_accessed()状态转换逻辑// mm/swap.c:496voidfolio_mark_accessed(structfolio*folio)folio_mark_accessed() 的状态机 ┌─────────────────────────┬──────────────────────────────────────┐ │ 当前状态 │ 动作 │ ├─────────────────────────┼──────────────────────────────────────┤ │ inactive, PG_ref0 │ 设 PG_referenced │ │ (第一次访问) │ → inactive, PG_ref1 │ ├─────────────────────────┼──────────────────────────────────────┤ │ inactive, PG_ref1 │ 立即激活! folio_activate() │ │ (第二次访问) │ 清 PG_referenced │ │ │ 调 workingset_activation() │ │ │ → active, PG_ref0 │ ├─────────────────────────┼──────────────────────────────────────┤ │ active, PG_ref0 │ 设 PG_referenced │ │ │ → active, PG_ref1 │ ├─────────────────────────┼──────────────────────────────────────┤ │ active, PG_ref1 │ 无变化已经在 active 且有引用 │ └─────────────────────────┴──────────────────────────────────────┘与 folio_referenced 的区别folio_referenced()被动检测回收时调用: - 扫描所有映射该页的 PTE 的 hardware accessed bit - 清除 accessed bit为下一轮检测做准备 - 由 shrink_folio_list / shrink_active_list 在回收扫描时调用 - 适用于有页表映射的页面mmap 的文件页、匿名页 folio_mark_accessed()主动标记访问时调用: - 软件层面直接设置 PG_referenced 或激活页面 - 不涉及硬件页表 - 由内核代码在访问页面时主动调用 - 适用于通过 read/write 系统调用访问的文件页不经过 mmap无 PTE关键点两条路径共用同一个两次确认机制无论是folio_mark_accessed还是folio_referenced都操作PG_referenced标志场景 1: read() 访问文件页无 mmap 第一次 read() → folio_mark_accessed() → 设 PG_referenced 第二次 read() → folio_mark_accessed() → PG_ref 已设 → 立即激活 场景 2: mmap 访问文件页 用户访问 → 硬件设 PTE accessed bit 回收扫描 → folio_referenced() 发现 accessed → 设 PG_referencedKEEP 再次访问 → 硬件再设 accessed bit 再次扫描 → folio_referenced() PG_referenced 已设 → ACTIVATE 场景 3: 混合路径 read() → folio_mark_accessed() → 设 PG_referenced 之后回收扫描 → folio_referenced() 发现 PTE accessed PG_ref 已设 → referenced_folio1 referenced_ptes0 → ACTIVATE标志位状态转换表事件 PG_active PG_workingset 所在位置 ───────────────────────────────────────────────────────────────────── 首次 fault 0 0 Inactive 在 inactive 被再次访问(提升) 1 0 Active shrink_active_list 降级 0 1 Inactive 在 inactive 被再次访问(提升) 1 1 Active 再次被降级 0 1 Inactive 被驱逐 - - Shadow Entry (ws1) refault 距离够短 1 1 Active (RESTORE) refault 距离太远 0 0 Inactive (重置)关键区分Shadow Entry workingset0 refault 且距离够短: → ACTIVATE工作集可能在扩张/迁移 → 给一次机会进 active 竞争 Shadow Entry workingset1 refault 且距离够短: → ACTIVATE RESTORE工作集在 thrashing → 进 active 额外增加回收代价lru_note_cost_refault → 告诉系统不是新页面在竞争是老页面被反复踢出来四、两种 Refault 路径路径 A从未上过 activeInactive Refaultfault → inactive → 驱逐 → refault shadow entry: workingset0含义冷页面再次被需要工作集可能在切换/扩张。路径 B曾在 active 上Active Refaultfault → inactive → active → 降级回 inactive → 驱逐 → refault shadow entry: workingset1含义热页面被挤出去了内存不够撑住当前工作集现有工作集在 thrashing。五、三个计数器代码流程// mm/workingset.c: workingset_refault()mod_lruvec_state(lruvec,WORKINGSET_REFAULT_BASEfile,nr);// 所有 refaultif(!workingset_test_recent(shadow,file,workingset,true))gotoout;// 距离太远不激活folio_set_active(folio);mod_lruvec_state(lruvec,WORKINGSET_ACTIVATE_BASEfile,nr);// 距离够短激活if(workingset){// 曾在 active 上folio_set_workingset(folio);lru_note_cost_refault(folio);mod_lruvec_state(lruvec,WORKINGSET_RESTORE_BASEfile,nr);// 热页面恢复}层级关系所有 refault (有 shadow entry 的缺页) │ ├── 距离太远不激活放 inactive → 仅 REFAULT │ └── 距离够短激活放 active → REFAULT ACTIVATE │ ├── workingset0路径 A 的页面 → 仅 ACTIVATE │ 含义新页面 refault工作集在迁移 │ └── workingset1路径 B 的页面 → ACTIVATE RESTORE 含义热页面被恢复工作集在 thrashing严格子集关系REFAULT ⊇ ACTIVATE ⊇ RESTORE六、实际数据分析数据workingset_refault_anon 33 workingset_refault_file 685014 workingset_activate_anon 32 workingset_activate_file 685008 workingset_restore_anon 0 workingset_restore_file 634385解读计算值含义activate/refault685008/685014 99.99%几乎所有 refault 距离都够短restore/activate634385/685008 92.6%绝大部分是曾经的热页面纯 activate路径 A685008 - 634385 50623新页面 refault 被激活未激活的 refault685014 - 685008 6极少有距离太远的诊断结论典型的内存不足 工作集 thrashing同一批热页面反复被降级 → 驱逐 → refault → 激活 → 降级…inactive list 周转极快页面刚进去就被推到尾部赶出去文件缓存严重不足活跃的文件工作集塞不进内存七、三种典型模式对比Thrashing内存不足健康工作集切换activate/refault~100%~5%~90%restore/activate~90%~20%~10%特征同一批热页面反复颠簸偶尔冷页面 refault大部分驱逐是合理的新页面大量涌入旧工作集退出诊断内存不够撑住工作集正常运行负载变化短暂过渡健康系统示例workingset_refault_file 100000 workingset_activate_file 5000 (5%) workingset_restore_file 1000 (1%)95% 的 refault 距离超过可用内存 —— 说明这些页面两次访问间隔很久驱逐它们是正确的决策。工作集切换示例workingset_refault_file 200000 workingset_activate_file 180000 (90%) workingset_restore_file 20000 (10%)activate 高但 restore 低 —— 大量 refault 被激活但没上过 active list路径 A说明一批新页面在密集涌入不是同一批页面在颠簸。
郑州网站建设
网页设计
企业官网