内存管理-66-默认关闭内核配置汇总

一、CONFIG_SPECULATIVE_PAGE_FAULT

1. 简介

性能考量。

CONFIG_SPECULATIVE_PAGE_FAULT 的核心作用是:在低竞争条件下,尝试无锁处理页面错误,避免长时间持有 mmap_lock,以降低足迹锁竞争和上下文切换开销。

1. 它解决的问题

传统页面错误处理必须获取 mmap_lock 写锁; 在高并发多线程/多进程场景下,mmap_lock 竞争激烈,导致大量线程阻塞; 锁竞争造成缓存行失效、上下文切换、调度延迟等开销。

2. 工作原理

(1) 快速路径:当条件允许时(数据不在内存中导致的简单缺页),先尝试无锁处理。
(2) 序列号验证:使用轻量级序列号(seqcount)代替重锁,记录 VMA 树的版本。
(3) 推测执行:在不持锁的状态下,推测性地查找 VMA、分配页面、建立映射。
(4) 提交验证:处理完成后,重新获取锁并验证期间是否有 VMA 改变。
(5) 回滚或提交:若验证通过,提交结果,释放锁,页面错误处理完成(避免了长时间持锁); 若验证失败,回滚,重新用传统方式处理。

3. 适用场景

(1) 简单页面错误:缺页只是因为数据不在 RAM(不是权限问题或其他复杂情况), 对应的文件系统操作简单快速.
(2) 无 VMA 结构变化:处理过程中没有其他线程做 mmap/munmap/mprotect 等操作; 序列号验证通过,说明 VMA 树状态未变。

4. 性能收益

降低锁竞争:减少 mmap_lock 被持有的时间; 减少上下文切换:线程不会因为争锁而被阻塞调出; 提高缓存局部性:减少因锁竞争导致的跨核缓存同步; 整体吞吐量提升:特别是在多核、高并发工作负载上有显著收益(可能 10-30% 甚至更多).

5. 限制与开销

不适用场景:复杂页面错误(如 COW、特殊 VMA 标记、KSM 等); VMA 频繁变化时,推测失败率高,反而浪费 CPU.
代码复杂度:引入额外的序列号检查逻辑,维护成本提升.
验证开销:推测失败时需要重做,可能反复处理同一错误; 在单线程或低竞争场景下收益不大(可能甚至因验证开销略差).

6. 内核版本演进

提议阶段:由 Laurent Dufour 等人在 2017-2018 年左右提出; 实验性支持:部分主线版本曾经历包含/移除的反复; 当前状态(截至 6.x 内核):在某些发行版(如 RHEL、CentOS Stream)中被启用, 在标准主线中仍为可选项,需手动配置 CONFIG_SPECULATIVE_PAGE_FAULT=y; 稳定性和覆盖面在持续改进.

7. 与其他优化的关系

vs Lock-Free VMA Lookup:后者是更激进的无锁方案,不再需要序列号验证;
vs RCU-based mmap_lock:另一种降低竞争的思路,通过 RCU 机制替代重锁;
vs numa_balancing 相关优化:都在努力减少页面错误处理的竞争;

8. 实践建议

(1) 何时启用:高并发多线程应用(如 Java 应用服务器、Web 框架); 频繁进行内存访问但 VMA 结构稳定的场景.
(2) 何时禁用:实时系统(不确定的延迟敏感); VMA 频繁变化的应用; 旧内核或稳定性优先考虑.
(3) 观测指标:perf stat -e page-faults 看缺页率; perf lock 看 mmap_lock 竞争情况; cat /proc/vmstat | grep speculative 查看推测成功/失败统计(如果有这类计数器)

9. 总结

CONFIG_SPECULATIVE_PAGE_FAULT 是一个面向高并发多线程场景的性能优化,通过推测性无锁决策 + 事后验证的套路,规避了传统页面错误处理中 mmap_lock 的长期持有问题。它不是通用加速器,而是针对特定工作负载(高并发缺页冲突多)的"双刃剑"优化。


二、CONFIG_ARCH_USES_PG_UNCACHED

1. 简介

作用:让 page flags 里出现 PG_uncached 位。用来标记“这个页被按 uncached 属性映射过”。

在 arm64 5.4 的实际情况:arm64 并没有默认 select 这个能力,所以通常这个选项对应逻辑是关闭的。结果是 PG_uncached 相关接口会被编译成空实现。arm64 一般通过页表属性(PTE/MAIR)处理缓存属性,不依赖这个 page flag。


三、CONFIG_MEMORY_FAILURE

1. 简介

作用:开启硬件内存错误恢复机制(ECC/RAS 场景)。出现坏页时,可把页打上 HWPoison(PG_hwpoison),隔离并避免再次分配使用。尽量“局部失效”而不是整机崩溃,避免给相关进程发 SIGBUS,或迁移/回收受影响页。

总结来说就是:把硬件上报的坏内存页转换成内核可管理的 HWPoison 状态,利用反向映射和页隔离机制把影响限定在最小范围,并通过 SIGBUS 把故障精确传播给受影响进程,从而实现“坏页可控、系统尽量不停机”。

在 arm64 5.4 的实际情况:arm64 在 Kconfig 里支持 ARCH_SUPPORTS_MEMORY_FAILURE,因此可以启用。启用后会连带 memory isolation、RAS 路径一起生效,适合服务器/高可靠场景。服务器场景常启用(可靠性)。

代价:增加少量代码和运行时处理复杂度;只有出现硬件故障时才明显走相关慢路径。

1. 坏页检测与隔离原理:

CONFIG_MEMORY_FAILURE 接收硬件/固件上报的内存错误,把对应 PFN 标记为 poisoned(坏页),尽量隔离并恢复系统运行(而不是直接 panic)。

发现坏页通常有3种路径:CPU/内存控制器 ECC 检测到不可纠错错误(UE); 平台固件通过 APEI/GHES(尤其 arm64 服务器常见)上报物理地址错误; 架构相关异常路径(如 x86 MCE,arm64 SError/SEA 配合 RAS)把 PFN 交给内核。这些路径最终会调用内核 memory-failure 处理入口(核心函数族在 mm/memory-failure.c####)。

内核把故障粗分为两类:可恢复路径(recoverable);致命路径(unrecoverable)。还会区分页当前状态(空闲buddy页、page cache页、匿名页、swap cache页、huge/THP页、内核关键页(页表、内核文本等)),不同状态动作不同。

核心处理流程:根据物理地址定位 PFN -> struct page; 尝试把该页标记为 PG_hwpoison(硬件坏页); 阻止该页再次被分配(隔离/从 buddy 摘除); 如果页被进程映射,执行“反向映射 + 解除映射 + 通知进程”; 必要时向相关进程发送 SIGBUS(BUS_MCEERR_*); 对无法安全恢复的场景,可能升级为 panic 或更强动作(依系统策略)。一句话:先“封存坏页”,再“清理映射影响”,最后“通知受害者”。

坏页检测依赖两个重要机制:
(1) HWPoison 标记机制: PG_hwpoison 是核心状态位。标记成功后:分配器不会再把该页发出去, 热插拔/回收路径把它当特殊页处理, 即使页经历 alloc/free 周期,这个位也会被保留(防止重用).
(2) 反向映射(rmap)清理机制: 如果坏页已映射到用户空间,内核会:找到映射它的 VMA/PTE, 尝试拆映射、回收或迁移(取决于页类型和状态), 对受影响任务发信号,让用户态感知并处理(崩溃、重试、降级).这一步是“系统不立即死机”的关键。

不同页类型行为差异:
(1) 空闲页坏掉: 最容易处理,直接从 buddy 隔离并打 poison.
(2) 文件页(page cache): 可丢弃后从磁盘重读的,恢复机会较大.
(3) 匿名页: 没有文件后备,通常需要杀死持有映射的任务或做更激进处理.
(4) swap cache 页: 结合 swap 条目处理,避免后续读回坏数据.
(5) THP/huge 页: 可能需要 split 或按 huge 粒度处理,路径更复杂.
(6) 内核关键页(页表等): 通常风险高,可能不可恢复.

arm64 场景下的现实路径: 硬件 RAS 发现错误, 固件通过 GHES/APEI 报告物理地址, 内核解析后走 memory_failure 路径, 对应 PFN 被 hwpoison,相关用户进程收到 SIGBUS,系统继续运行。所以你常看到“单进程挂了,但系统没挂”的现象。

配置项关系:
开启 CONFIG_MEMORY_FAILURE 通常还会联动:ARCH_SUPPORTS_MEMORY_FAILURE(架构能力), CONFIG_RAS(错误上报框架), MEMORY_ISOLATION(隔离辅助), 如果这些链路不完整,CONFIG_MEMORY_FAILURE 的效果会打折。


四、CONFIG_IDLE_PAGE_TRACKING

1. 简介

作用:提供“页是否在一段时间内未被访问”的跟踪能力。典型接口是 page idle bitmap(用于容量评估、内存冷热分析、云平台调度优化)。

在 arm64 5.4 的实际情况:该选项依赖 MMU + SYSFS;arm64 满足。且 arm64 是 64 位,因此会额外使用 PG_young/PG_idle 这两个 page flag 位来跟踪状态。

用途:估算真实热内存工作集(WSS)。为内存 cgroup 配额、虚机迁移、节点放置提供依据。

代价:需要扫描/采样,会有一定观测开销,不建议默认高频使用在所有业务上。

2. 补充

CONFIG_IDLE_PAGE_TRACKING 的作用是:让内核支持“页空闲访问跟踪(idle page tracking)”,用于统计一段时间内哪些用户态页面“没被访问过”。像一个内核提供的“冷热点观测仪表”。

1. 它解决什么问题
帮你区分“占着内存但实际上很久没碰”的页。这对容量评估、内存回收策略验证、memcg 配额调优、作业放置(尤其大数据/集群)很有价值。

2. 工作方式
内核给页提供一个“idle”标记能力。用户空间可以先把一批页标成 idle。过一段时间再查询:
a. 仍是 idle:这段时间基本没访问。
b. idle 被清掉:期间发生过访问。

3. 依赖与开销
依赖 SYSFS && MMU。功能本身主要用于观测,不直接改变 LRU 回收算法。会有一定元数据和扫描开销,但通常比盲目全量采样更可控。

4. 一句话总结:CONFIG_IDLE_PAGE_TRACKING 是一个内存冷热观测能力开关,重点是“测量和分析”,不是“直接回收策略”。

 

五、CONFIG_SPECULATIVE_PAGE_FAULT

1. 简介

CONFIG_SPECULATIVE_PAGE_FAULT 的核心作用是:让缺页异常处理在“可行时”走一条更轻量的快速路径,尽量不阻塞在传统的 mmap 锁竞争上。

在 Linux 5.4 语境里可以这样理解:

传统缺页路径下,正常 page fault 往往要拿 mm 的读锁(常说 mmap_sem/mmap_lock 读锁)去查 VMA、建页表。多线程高并发缺页时,这把锁会成为热点。

开启 CONFIG_SPECULATIVE_PAGE_FAULT 后, 内核会尝试“推测式”处理:先做无锁/弱锁校验式访问(配合序列号一致性检查)。如果检查期间发现 VMA 或映射关系被并发修改,就立刻放弃这次推测并回退到传统加锁路径重试。####

主要收益是在 mmap/mprotect/munmap 变更不频繁、但 page fault 很频繁的场景下,通常能降低锁竞争、提升吞吐和尾延迟。

代价与边界
并发映射变更频繁时,推测失败重试会增多,收益下降,甚至可能不如传统路径;同时内核实现复杂度更高。

关闭后功能正确性不受影响,只是回到完全传统的加锁缺页处理,通常更保守、更稳定,但高并发下可扩展性可能差一些。

补充一句:这个选项在不同 5.4 分支(主线/Android/common/厂商树)状态可能不同,有的分支没有或默认关闭。

 

六、CONFIG_DEBUG_VM_RB

1. 简介

CONFIG_DEBUG_VM_RB 在 Linux 5.4 里的作用是:打开 VMA 红黑树增强字段的一致性自检,用来尽早发现内存管理子系统里的结构损坏或更新顺序错误。

主要做的事:

(1) 校验 VMA 红黑树基本有序性,检查按地址遍历时是否单调、是否有重叠、前后遍历节点数是否一致。

(2) 校验增强字段 rb_subtree_gap 是否正确, 每个节点的 rb_subtree_gap 都会和“重新计算值”比对,确保 gap 传播逻辑没出错(这对 unmapped area 搜索很关键)。

(3) 在关键路径做 validate_mm/validate_mm_rb 断言, 像 vma 插入、删除、调整后会触发更多一致性检查,帮助定位 __vma_adjust、vma_gap_update、rb erase/insert 等路径中的隐蔽 bug。

(4) 失败时快速暴露问题, 通常通过 VM_BUG_ON/WARN/pr_emerg 等方式直接报错甚至触发崩溃,便于开发阶段定位。

代价与使用建议:

有明显性能开销: 遍历和重算会增加 mmap/munmap/mprotect 等路径成本。

仅建议用于调试内核: 生产环境一般关闭;排查 MM/VMA 红黑树相关问题时再开启最合适。

 

七、CONFIG_STACK_GROWSUP

1. 简介

CONFIG_STACK_GROWSUP 的作用是决定用户栈采用“向高地址增长”模型,而不是常见的“向低地址增长”。

在 Linux 5.4 里它主要影响这几块:

(1) 栈扩展方向: 开启:栈向上增长(VM_GROWSUP 语义), 关闭:走默认向下增长(VM_GROWSDOWN).

(2) 缺页触发时的扩栈逻辑: 开启时走 expand_upwards() 路径, 关闭时通常走 expand_downwards() 路径, 对应 find_extend_vma() 里也会按配置选择不同分支。

(3) VMA 边界更新方式: 向上增长主要改 vma->vm_end, 向下增长主要改 vma->vm_start,并处理 vm_pgoff 等联动, 这会连带影响 gap 维护、统计计数、anon_vma 区间更新等 MM 元数据流程。

适用架构:该选项主要给少数架构/场景用(例如历史上 PA-RISC、IA64 某些栈模型);大多数常见平台(x86/arm64)通常不是 grows-up 栈。

关闭后使用的是默认的向下增长模型;这也是你在主流 Linux 上最常见的行为。

 

八、CONFIG_TRANSPARENT_HUGEPAGE

1. 简介

CONFIG_TRANSPARENT_HUGEPAGE 它的作用是启用透明大页 THP。内核在合适条件下,尽量把普通匿名页按更大的页粒度来映射,典型是把一组 4KB 页合并成 2MB 页,而应用本身不需要显式用 hugetlbfs 或专门 API。

主要效果:

(1) 减少页表项数量, 同样大小的内存区域,用大页映射后页表更小,TLB 压力更低。

(2) 降低 TLB miss, CPU 地址翻译缓存能覆盖更大虚拟地址范围,对大内存工作集常有帮助。

主要面向匿名内存,Linux 5.4 里最典型的是匿名映射、进程堆、栈附近的大块匿名内存。khugepaged 还会后台尝试把满足条件的小页折叠成大页。

不是“强制大页”,之所以叫 transparent,就是应用通常无感知。内核会根据 VMA 属性、对齐情况、碎片情况、是否允许 hugepage 等条件决定是否真的用 THP。

关闭后, 系统仍然完全正常,只是匿名内存通常按普通 4KB 页工作,可能少一些性能收益,但行为更保守、碎片和回收处理也更简单。

相关的VMA标志:VM_HUGEPAGE:倾向启用 THP, VM_NOHUGEPAGE:禁止该 VMA 使用 THP。

 

九、CONFIG_MEM_SOFT_DIRTY

1. 简介

CONFIG_MEM_SOFT_DIRTY 它的作用是启用 soft-dirty 机制,用来跟踪“某个页或某段 VMA 自某个时刻以来是否被用户态写过”。这个机制主要给检查点/恢复、增量内存同步、脏页追踪类功能使用,比如 CRIU。

主要效果:给页提供“软脏”标记能力,用户态可以先清一次 soft-dirty 状态,然后跑一段时间,再查询哪些页被写过。

配合 /proc/PID/clear_refs 和 /proc/PID/pagemap 使用,常见流程是:先清 soft-dirty,进程继续运行,再从 pagemap 或相关接口读取哪些页变脏, VMA 合并时要特殊处理。

源码里有一句话:VM_SOFTDIRTY should not prevent from VMA merging,意思是 soft-dirty 本身不应成为阻止 VMA merge 的条件,否则会平白制造很多碎片化 VMA。

新映射通常会带上 VM_SOFTDIRTY, 这样用户态能区分“旧区域继续存在”与“原区域被拆掉后重新映射了一块新区域”。

关闭后,系统基本功能不受影响,只是 soft-dirty 跟踪能力不存在,CRIU/增量脏页跟踪这类能力会受限或不可用。


十、CONFIG_ZONE_DEVICE

CONFIG_ZONE_DEVICE 的核心作用是:

把“设备发现的物理内存区域”纳入内核内存模型,建立 struct page,并作为 ZONE_DEVICE 统一管理,这样这些地址就能参与很多基于 page 的通用路径(而不是只当裸物理地址)。

可以从这几个层面理解:

1. 能力开关
配置入口在 Kconfig:694, 帮助文本写得很直接:支持 pmem、HMM 等设备内存热插拔进 memmap,支持 pfn_to_page 查找
依赖包括 MEMORY_HOTPLUG、MEMORY_HOTREMOVE、SPARSEMEM_VMEMMAP、ARCH_HAS_PTE_DEVMAP

2. 打开后会启用的关键机制
编译并启用 memremap_pages/devm_memremap_pages 这套 dev_pagemap 路径,见 Makefile:111 和 memremap.h;
引入 ZONE_DEVICE 页判断与初始化接口,见 mm.h:1081;
支持多种设备内存类型(例如 PRIVATE、FS_DAX、DEVDAX、PCI_P2PDMA),见 memremap.h:55;

3. 对 mm/swap.c 这类路径的实际影响
release_pages()释放页(这个不是释放进伙伴系统)时会先分流 device page:is_zone_device_page 和 put_devmap_managed_page 走设备页处理,避免误按普通 RAM 释放,见 swap.c:879;
也就是:ZONE_DEVICE 页“看起来像页”,但生命周期管理可能交给设备驱动####,而不是直接回 buddy;

4. 关闭 CONFIG_ZONE_DEVICE 时会怎样
devm_memremap_pages 会直接返回失败(ENXIO 路径),调用方必须回退到普通映射方案,见 memremap.h:135;
is_zone_device_page 恒为 false,相关分支等同不存在,见 mm.h:1089;

5. 一句话总结:
CONFIG_ZONE_DEVICE 让"设备内存"进入 Linux 的页管理体系(有 struct page、有类型、有专门释放语义),从而支持 DAX/HMM/设备私有内存等高级场景,同时又不把它们错误地当成普通系统内存处理。


十一、CONFIG_HIGHMEM

CONFIG_HIGHMEM 的核心作用:让 32 位内核能管理“超出内核永久线性映射范围”的物理内存。

(1) 一句话版本:

没开 CONFIG_HIGHMEM:内核只能直接、永久映射一部分低端物理内存(lowmem)。开了 CONFIG_HIGHMEM:剩余高地址物理内存(highmem)也能被管理,但访问时要临时映射。

(2) 为什么会有它:

在很多 32 位架构里,内核虚拟地址空间很小(常见 1GB 左右)。这 1GB 里还要放内核代码、vmalloc、ioremap、fixmap 等,不可能把所有物理内存都永久映射。因此超过可永久映射部分的物理页,就叫 HighMem。####

(3) 它解决了什么问题:

让系统在 32 位平台上可使用比 lowmem 更大的 RAM。页缓存、匿名页等可以放在 highmem 里,提升可用内存总量。用户态内存压力场景下帮助明显,否则很多内存会“物理上有、内核却不好直接用”。

(4) 代价是什么:

highmem 页不能像 lowmem 那样直接通过 page_address() 长期访问。需要 kmap_local_page() / kmap_atomic()(旧)这类临时映射。访问路径更复杂,TLB/映射切换有额外开销。某些必须常驻内核地址空间的数据结构/缓冲只能放 lowmem,低端内存仍可能先紧张。

(5) 典型影响到的内核代码习惯:

判断页是否高端:PageHighMem(page)。复制/清零高端页常用 kmap_local_page() 映射后操作。不能假设任意 struct page 都有稳定内核虚拟地址。驱动里做 DMA 时还要结合设备寻址能力(和 HIGHMEM 不是一回事,但经常一起踩坑)。

(6) 和 64 位关系:

绝大多数 64 位平台通常不需要 HIGHMEM 机制。所以在很多 arm64 配置里你会看到 HighMem/MovableOnly 统计为 0, 和日志里的 "0 pages HighMem/MovableOnly" 对应。CONFIG_HIGHMEM 主要是 32 位时代的“空间不够用”解决方案。

这通常意味着当前平台/配置下没有 classic highmem 区(或该统计项里 highmem 部分为 0),也侧面说明你当前问题重点不在 HIGHMEM 本身,而更可能在 CMA/zone/migratetype 可用性上。

(7) 实战判断建议:

如果你是 64 位 Android/arm64:CONFIG_HIGHMEM 一般不是主矛盾。如果你是 32 位平台且内存较大:HIGHMEM 很重要,但要同时关注 lowmem 是否先耗尽。分配失败分析时,别把“总 free 很多”直接等价为“内核可直接访问内存很多”,lowmem/highmem 可达性是两回事。


十二、CONFIG_HUGETLB_PAGE

CONFIG_HUGETLB_PAGE 启用的是 Linux 的显式 HugeTLB 大页机制,也就是“预留大页池 + 应用显式申请使用”的那套能力。它不是 THP 的自动合页,而是管理员和应用都要主动参与的机制。

定义在 fs/Kconfig 中。内核相关文档:

kernel-6.1/Documentation$ find ./ -name "*tlb*"
./translations/zh_CN/core-api/cachetlb.rst
./translations/zh_CN/arm64/hugetlbpage.rst
./translations/zh_CN/mm/hugetlbfs_reserv.rst
./admin-guide/cgroup-v1/hugetlb.rst

1. 这个配置具体打开了什么功能

启用 HugeTLB 页管理子系统(核心实现见 hugetlb.c)。

启用 hugetlbfs 相关内核能力后,系统可维护一个或多个大页池(不同大页尺寸可并存)。支持应用通过显式接口申请大页映射,而不是等内核自动合并。

典型用户态接口会变成可用:
(1) mmap 的 MAP_HUGETLB 路径(定义见 mman.h:21)。
(2) SysV SHM 的 SHM_HUGETLB(见 shm.h:52)。
(3) memfd_create 的 MFD_HUGETLB(见 memfd.h:10)。

hugetlbfs 文件系统挂载与文件映射路径(实现入口在 inode.c)。

2. 开启后系统行为上的关键变化

会出现/生效大页池统计与控制接口,例如 meminfo 里的 HugePages_Total、HugePages_Free、Hugetlb 等(文档说明见 hugetlbpage.rst:25)。

可通过启动参数和 sysctl 预留/调整大页池大小,如 hugepages、hugepagesz、nr_hugepages。HugeTLB 页通常是预留资源,不能像普通匿名页那样被 swap 出去####,分配失败与池容量、物理连续性更相关。

3. 它和 THP 的本质区别

HugeTLB:显式、预留池、强可预测性,适合数据库、低抖动场景。

THP:自动、按策略尝试合页、透明给应用,易用但可预测性弱于 HugeTLB。

两者可以同时存在,但统计口径和分配路径不同。你在配置菜单里看到的关系:

HUGETLB_PAGE 在你这份源码里是 def_bool HUGETLBFS,也就是启用 HUGETLBFS 后自动选中 HUGETLB_PAGE(见 Kconfig:247 到 Kconfig:253)。

可选的 HUGETLB_PAGE_OPTIMIZE_VMEMMAP 是 HugeTLB 的内存元数据优化子特性,不是基础功能本身(见 Kconfig:257)。


十三、CONFIG_HAVE_MEMORYLESS_NODES

可以概括为一句话:

让“CPU 所在节点”和“该 CPU 做内核内存分配时使用的本地内存节点”解耦,专门处理“有 CPU 但无本地内存”的 NUMA 节点。

核心背景: 在一些 NUMA 拓扑里,某个 node 可能只有 CPU、没有可管理内存(memoryless node)。这时如果还把 cpu_to_node 直接当作“本地内存节点”,很多分配路径会拿到一个无内存节点,局部性和回退行为都不理想。

NUMA相关的,嵌入式不用。

 

十四、CONFIG_FAIL_PAGE_ALLOC

1. 作用概述

是内核故障注入(fault injection)框架的一个子功能,用于人为让页分配失败,以测试内核/驱动在分配失败时的错误处理路径是否正确。

2. 数据结构

// page_alloc.c ~5860
static struct {
    struct fault_attr attr;   // 故障注入框架通用属性(概率、间隔等)
    bool ignore_gfp_highmem;  // 是否跳过 __GFP_HIGHMEM 请求
    bool ignore_gfp_reclaim;  // 是否跳过 __GFP_DIRECT_RECLAIM 请求
    u32  min_order;           // 最小触发 order(小于此阶不注入失败)
} fail_page_alloc = {
    .attr                = FAULT_ATTR_INITIALIZER,
    .ignore_gfp_reclaim  = true,   // 默认跳过直接回收路径
    .ignore_gfp_highmem  = true,   // 默认跳过 highmem 请求
    .min_order           = 1,      // 默认跳过 order-0(单页)分配
};

3. 判断逻辑

static bool __should_fail_alloc_page(gfp_t gfp_mask, unsigned int order)
{
    if (order < fail_page_alloc.min_order)          // 1. 阶数过小,跳过
        return false;
    if (gfp_mask & __GFP_NOFAIL)                    // 2. 调用方不允许失败,跳过
        return false;
    if (fail_page_alloc.ignore_gfp_highmem && (gfp_mask & __GFP_HIGHMEM)) // 3. highmem 请求,跳过
        return false;
    if (fail_page_alloc.ignore_gfp_reclaim && (gfp_mask & __GFP_DIRECT_RECLAIM)) // 4. 可直接回收请求,跳过
        return false;

    return should_fail_ex(&fail_page_alloc.attr, 1 << order, flags); // 5. 由通用框架决定是否触发
}

此函数在 prepare_alloc_pages() 中被调用——位于 fast/slow path 进入真实取页逻辑之前,命中时直接返回 NULL,模拟分配失败。

4. debugfs 接口

启用 CONFIG_FAULT_INJECTION_DEBUG_FS 后,在 /sys/kernel/debug/fail_page_alloc/ 下暴露以下文件:

probability: 触发概率(0~100,通用 fault_attr)
interval: 每隔多少次触发一次
times: 最多触发多少次(-1 为无限)
space: 每次触发后的"冷却"计数
verbose: 触发时是否打印调用栈
ignore-gfp-wait: 对应 ignore_gfp_reclaim
ignore-gfp-highmem: 对应 ignore_gfp_highmem
min-order: 对应 min_order

也可通过内核启动参数 fail_page_alloc = 配置 fault_attr

5. 使用场景

# 以 10% 概率让 order>=1 的分配失败(不限次数)
echo 10 > /sys/kernel/debug/fail_page_alloc/probability
echo -1 > /sys/kernel/debug/fail_page_alloc/times
echo 1  > /sys/kernel/debug/fail_page_alloc/verbose

# 配合 /proc/<pid>/fail-nth 对特定进程注入(需 CONFIG_FAULT_INJECTION_STACKTRACE_FILTER)

6. 与周边机制的关系

__alloc_pages()
  └─ prepare_alloc_pages()
       └─ should_fail_alloc_page()   //CONFIG_FAIL_PAGE_ALLOC 在此拦截
            └─ __should_fail_alloc_page()
  └─ get_page_from_freelist()        //正常未拦截时进入此处

与 CONFIG_FAIL_SLAB(slab 层失败注入)、CONFIG_FAIL_IO_TIMEOUT(IO 超时注入)是同一故障注入框架下的不同分支,共同服务于内核鲁棒性测试。


十五、CONFIG_DEFERRED_STRUCT_PAGE_INIT

1. 此配置的作用是:延迟初始化页表元数据(struct page),将启动期间最耗时的初始化工作推迟到后期,加速内核启动过程。主要面向大内存系统(如数百 GB 物理内存的服务器)

2. 问题背景
启动性能瓶颈。在没有此配置时,内核启动流程:

内核加载
    初始化所有 struct page(!!非常耗时)
        对于每个页面(可能数亿个),需要:
            - 初始化 ref count、flags、lru list 等
            - 设置 zone/nid 信息
            - 处理 KASAN/page coloring 等
    初始化其他子系统(文件系统、网络驱动等)
    启动应用

对性能影响是,大内存系统启动缓慢:512GB 内存系统可能需要 10-30 秒才能完成 struct page 初始化。系统在此期间无法响应应用程序。实际上,启动时不是所有页都需要立即初始化,大部分页在启动后期才被使用。


3. 核心实现机制

(1) 延迟初始化的策略

启动流程(启用 CONFIG_DEFERRED_STRUCT_PAGE_INIT):

1. 早期启动(early boot)
   ├─ 初始化前 PAGES_PER_SECTION 个页(约 2MB)
   ├─ 标记剩余页的起点:first_deferred_pfn = X
   └─ 立即启动系统(不再等待)

2. 并行线程初始化(page_alloc_init_late)
   ├─ 为每个 NUMA node 创建一个 deferred_init_memmap 线程
   ├─ 各线程独立初始化所属 node 的页
   └─ 初始化通过后关闭快速路径

3. 运行时快速路径
   ├─ 若需要分配未初始化页面
   ├─ _deferred_grow_zone() 按需初始化一个 SECTION
   └─ 从 buddy 系统取页


4. 关键数据结构

(1) 控制变量

// include/linux/mm.h (pgdat)
struct pglist_data {
    unsigned long first_deferred_pfn;  // 延迟初始化的起点 PFN, ULONG_MAX 表示已完成初始化
    ...
};

// mm/page_alloc.c
static DEFINE_STATIC_KEY_TRUE(deferred_pages);  // 快速路径开关

// 表示:
// - 启用状态:deferred_pages_enabled() = true
// - 禁用状态(初始化完成后):static_key 被关闭,开销为 0

(2) 判断函数

// 检查某个 PFN 是否需要延迟初始化
static inline bool early_page_uninitialised(unsigned long pfn)
{
    int nid = early_pfn_to_nid(pfn);
    
    // 若该 PFN 在 first_deferred_pfn 之后,说明还未初始化
    if (node_online(nid) && pfn >= NODE_DATA(nid)->first_deferred_pfn)
        return true;
    return false;
}

// 决定是否延迟初始化(启动时调用)
static bool defer_init(int nid, unsigned long pfn, unsigned long end_pfn)
{
    static unsigned long nr_initialised;
    
    // 策略:只初始化前 PAGES_PER_SECTION (~128K) 个页,之后的全部延迟
    if ((nr_initialised > PAGES_PER_SECTION) && (pfn & (PAGES_PER_SECTION - 1)) == 0) {
        NODE_DATA(nid)->first_deferred_pfn = pfn;
        return true;  // 从这里开始延迟
    }
    return false;
}


5. 三个关键阶段

阶段 1:启动早期(Early Boot)

时间点:kernel_init() 前
工作内容:
├─ 初始化 LOWMEM zones(DMA、NORMAL 的前 128MB)
├─ 初始化约 PAGES_PER_SECTION 个页(~2MB)
├─ 设置 first_deferred_pfn = PAGES_PER_SECTION(所有后续页标记为待初始化)
├─ 记录到 pgdat->first_deferred_pfn
└─ 立即启动系统(不阻塞)

特点:
- 只初始化足够启动的页面
- 快速完成,避免启动延迟
- 通过 defer_init() 决策


阶段 2:启动完成时的并行初始化(page_alloc_init_late)

时间点:内核启动完成(late initcall),应用启动前工作内容:
│
├─ 创建并行线程(通常为 CPU 核心数)
│  ├─ 每个 NUMA node 一个线程:kthread_run(deferred_init_memmap, ...)
│  └─ 线程 ID:pgdatinit0, pgdatinit1, ...
│
├─ 各线程独立工作
│  ├─ 扫描 [first_deferred_pfn, zone_end_pfn)
│  ├─ 初始化 struct page(调用 __init_single_page)
│  ├─ 将页加入 buddy 系统(deferred_free_range)
│  └─ 定期 touch_nmi_watchdog() 防止软狗超时
│
└─ 等待所有线程完成
   ├─ atomic_set(&pgdat_init_n_undone, num_nodes)
   ├─ 各线程完成后调用 pgdat_init_report_one_done()
   ├─ 最后一个线程调用 complete()
   └─ 关闭 deferred_pages 快速路径

特点:
- 并行处理,充分利用多核
- 各 node 独立初始化,无跨 node 锁竞争
- 完成后关闭快速路径,后续无额外开销


阶段 3:运行时按需初始化(On-Demand Growth)

时间点:系统运行中, 触发条件:
├─ Fast path(get_page_from_freelist)watermark 失败
├─ 且 zone 仍有未初始化页面
└─ 调用 _deferred_grow_zone()

处理流程:
│
├─ if (CONFIG_DEFERRED_STRUCT_PAGE_INIT && static_branch_unlikely(&deferred_pages))
│  ├─ _deferred_grow_zone(zone, order)
│  │  ├─ 计算所需页数:ALIGN(1<<order, PAGES_PER_SECTION)
│  │  ├─ 初始化 [first_deferred_pfn, first_deferred_pfn + needed)
│  │  ├─ 调用 deferred_init_maxorder() 快速初始化
│  │  ├─ 将初始化的页加入 buddy
│  │  └─ 返回 true(表示可重试)
│  └─ goto try_this_zone  // 重新尝试 rmqueue
│
├─ 若 static_key 已关闭(初始化完成)
│  └─ 开销 = 0(JMP 被 NOP 替换)
│
└─ 若无未初始化页
   └─ 返回 false(真正的 OOM,需进入 slow path)

特点:
- 不阻塞启动
- 极少数情况触发(内存使用达到 upper zone 时)
- 成本可控(只初始化一个 SECTION)


6. 代码流程总结

启动期间的流程

// 1. 早期启动检查(zone_init_now)
for (pfn = start; pfn < end; pfn++) {
    if (should_defer_init(zone, pfn, end_pfn)) {
        first_deferred_pfn = pfn;
        break;  // 延迟剩余部分
    }
    __init_single_page(pfn_to_page(pfn), pfn, ...);
}

// 2. 晚期启动并行初始化(page_alloc_init_late)
for_each_node_state(nid, N_MEMORY) {
    kthread_run(deferred_init_memmap, NODE_DATA(nid), ...);
}
wait_for_completion(&pgdat_init_all_done_comp);

// 3. 关闭快速路径
static_key_slow_inc(&deferred_pages);  // 从 true -> false

// 4. 运行时快速路径(已关闭,无开销)
if (static_branch_unlikely(&deferred_pages))  // 编译期 NOP
    _deferred_grow_zone(zone, order);


7. 对分配器的影响

(1) get_page_from_freelist 中的集成

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
// Fast path watermark 检查失败时
if (static_branch_unlikely(&deferred_pages)) {
    if (_deferred_grow_zone(zone, order))
        goto try_this_zone;  // 重新尝试 rmqueue
}

#endif

try_this_zone:
    page = rmqueue(...);
    if (page)
        return page;


(2) 何时初始化保留页(init_reserved_page)

// Bootloader/固件预留的内存(DMA 缓冲等)
void reserve_bootmem_region(phys_addr_t start, phys_addr_t end)
{
    for (start_pfn < end_pfn) {
        init_reserved_page(start_pfn);  // 若需延迟,初始化该页
        __SetPageReserved(page);
        start_pfn++;
    }
}


8. 性能收益

从系统规模, 启动时间改善, 描述:
4GB 内存: ~5%,收益不明显
64GB 内存:~20-30%,显著改善
512GB+ 内存: ~40-60%, 极具价值,可能从 30s 降至 10s


9. 关键优化点

分段初始化:PAGES_PER_SECTION (~128K) 为单位,减少初始 lookup;
并行化:每个 node 一个线程,无全局锁竞争;
按需增长:运行时极少触发,成本可控;
静态 key 关闭:初始化完成后,快速路径开销 = 0(编译期 NOP);
缓存友好:初始化时连续访问内存,充分利用 CPU 缓存;


十六、CONFIG_DEBUG_PAGEALLOC

CONFIG_DEBUG_PAGEALLOC 是 Linux 内核中用于检测页面分配和使用错误的专项调试工具。它通过引入 "Guard Pages"(守卫页)机制 来检测内存越界、use-after-free、double-free 等问题。

它是内存错误检测的强大工具,能够及时捕获页面相关的严重 bug(如越界访问、use-after-free),对内核和驱动开发至关重要。虽然有性能开销,但在调试和测试阶段的回报远大于成本。


十七、CONFIG_DEBUG_VM

CONFIG_DEBUG_VM 是 Linux 内核中一个重要的内存管理调试开关,用于启用各种虚拟内存系统的运行时检查和验证。比如:

(1) 页面范围验证
检查:页面的物理地址 (PFN) 是否在 zone 范围内;页面结构中的 zone 指针是否正确;防止内存管理器处理越界或错误的页面;

(2) Compound Page 验证

检查:高阶页(hugepage、THP 等)的头尾页是否正确链接; 尾页的标志位是否正确设置; 防止高阶页的损坏释放导致的内存泄漏.

(3) 页面状态验证

检查:页面的映射计数 (mapcount); 页面的引用计数 (refcount); Cgroup 内存标记是否清零; 页面标志位是否合法.

(4) VM_BUG_ON 宏

CONFIG_DEBUG_VM 影响众多 VM_BUG_ON 宏的行为, 比如 VM_BUG_ON、VM_BUG_ON_PAGE,检查 Zone 是否初始化,迁移类型是否合法,页面状态是否符合预期,各种不变量是否被违反。

(5) PCP 快速路径检查

 

posted on 2026-04-22 11:06  Hello-World3  阅读(34)  评论(0)    收藏  举报

导航