内存管理-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) 收藏 举报
浙公网安备 33010602011771号