「Linux sparsemem/mem_section 深度笔记:从地址网格到 struct page 的完整心智模型」
以下是基于你提到的这些点,整理的一份Linux sparsemem/mem_section 笔记,并在需要的地方做了实现层面的纠正和补充。
1. sparsemem 与 mem_section 的角色
- sparsemem 的目的:应对物理地址空间碎片化(大洞、多节点、热插拔等),不再假设物理内存连续。
- mem_section 的定义:
- 是固定大小的物理地址区间管理单元(例如 128MB、256MB、1GB,取决于架构和配置)。
- 内核启动时按整个物理地址空间划分一个个 section:section0 管理
[0, SECTION_SIZE), section1 管理[SECTION_SIZE, 2*SECTION_SIZE),以此类推。 - 关键点:mem_section 管的是“覆盖的地址范围”,不保证这段范围内全部都是真实内存。
2. mem_section 与物理内存的关系
2.1 mem_section 不等于“整块内存”
- 一个 mem_section 不要求被真实物理内存“填满”。
- 在一个 mem_section 覆盖的地址范围内,可能出现:
- 完全是可用 RAM;
- RAM + 内存空洞(设备 MMIO / 保留区域)混合;
- 完全没有 RAM,仅仅是“洞”(此时整个 section 甚至可以不被标记为 present)。
结论:
mem_section 是“网格”;真实内存是“散落在网格里的土地”。网格可满可空可半满。
2.2 起始位置不必对齐到 section 开头
- 一块真正插在主板上的内存块(比如一个 DIMM 条),在物理地址上的起始地址和长度完全由硬件+固件决定,和 mem_section 的边界无关。
- 因此:
- 一个 section 覆盖
[A, A+SECTION_SIZE); - 可用 RAM 可能只在其中的一段,例如
[A+64MB, A+128MB); - section 前半段可能是 MMIO/保留区域(内存空洞)。
- 一个 section 覆盖
结论:
“一块内存如果不足以填满一个 section,也不一定从这个 section 的第一个物理页开始”,它可以从 section 中间任意一个 pfn 开始,只要固件这么报告即可。
3. 内存热插拔与非 SECTION_SIZE 倍数
- 固件(通常 ACPI)在热插拔内存时,报告的是一个任意起始地址 + 任意大小的内存区域,例如:
- 起始地址:
start - 大小:
size
- 起始地址:
- 内核处理流程(逻辑粒度):
- 根据
[start, start + size)计算涉及到的 mem_section 下标集合。 - 为这些 section 分配/初始化
struct mem_section和section_mem_map(如有必要)。 - 在这个地址区间内,按 页 (page) 粒度遍历:
- 对每一个可用的 page(4KB 等),初始化对应的
struct page。 - 把这些 page 加入伙伴系统。
- 对每一个可用的 page(4KB 等),初始化对应的
- 根据
重要区分:
- 管理粒度:mem_section(例如 128MB/256MB/1GB),便于批量标记 online/offline。
- 操作粒度:page(例如 4KB),真正初始化和参与分配的是 page。
结论:
热插拔内存绝不要求是 SECTION_SIZE 的整数倍,只要是页粒度对齐即可,内核可以精确到每一页管理。
4. struct mem_section 里有没有 start_pfn?
4.1 概念层面 vs 实现层面
-
概念层面,描述流程时常会说:
“为 section 设置 start_pfn”
实际含义是:“把这个 section 与它负责的起始物理页号关联起来”。 -
实现层面:
-
在
struct mem_section中并没有start_pfn这个字段。 -
起始页帧号是通过 下标计算 获得的:
-
设
PAGES_PER_SECTION = SECTION_SIZE / PAGE_SIZE -
第
i个 section 的起始 pfn: -
给定一个 pfn,要找到它属于哪个 section:
-
-
由于这种关系是固定可计算的,所以没必要在结构体里再存一个
start_pfn字段。
-
总结:
“设置 start_pfn”是逻辑动作;实际代码是“通过 section 下标 + 常量计算 start_pfn”,不需要成员变量。
5. mem_section 的 present / section_mem_map
5.1 mem_sections 全局数组
- 内核维护一个全局的 section 数组(逻辑上):
struct mem_section *mem_sections[NR_MEM_SECTIONS]; - 每个下标
i对应一个覆盖固定地址范围的 section。
5.2 present 的“隐式”表示
- 概念上,会说
mem_section[i].present = 1表示这个 section 里有内存。 - 实现上,通常通过:
mem_sections[i]是否为NULL;- 或者在
section_mem_map指针的低位上编码一些 bit; 来表示这个 section 是否 present / online。
结论:
“present 字段”一般也是概念描述;实际使用的是指针是否为空、指针上的标志位等手段。
5.3 section_mem_map:指向 struct page 数组
struct mem_section的核心字段是section_mem_map:- 指向一个
struct page数组。 - 数组长度覆盖整个 section 范围的所有页数:
PAGES_PER_SECTION。
- 指向一个
- 后续所有关于该 section 内 page 的信息(是否空洞、是否可用、属于哪个 zone/node 等)都通过这些
struct page的字段来表达。
6. 内存空洞与 struct page 的分配
6.1 “空洞也会分配 page 吗?”
更精确的说法:
- 对于已经标记为 present 的 section:
- 内核会为该 section 覆盖的整个地址范围分配一整块
struct page数组。 - 其中:
- 位于真实 RAM 范围的那些页,对应的
struct page会被初始化为 可用内存页; - 位于内存空洞(MMIO/保留区域)范围的那些页,对应的
struct page会被标记为PG_reserved,不会加入伙伴系统。
- 位于真实 RAM 范围的那些页,对应的
- 内核会为该 section 覆盖的整个地址范围分配一整块
- 对于完全没有 RAM 的 section(整个 section 是洞):
- 该 section 通常不会被标记为 present,也不会分配
struct mem_section和section_mem_map,自然也就没有struct page。
- 该 section 通常不会被标记为 present,也不会分配
所以:
- “空洞也会分配相应的 page” 这句话在上下文上,应理解为:
“在一个已存在(present)的 section 内部,即便某些子范围是内存空洞,也会有对应的struct page,但这些 page 会标记为PG_reserved,不参与分配。”
6.2 这样做的核心理由:pfn_to_page / page_to_pfn 的极致效率
-
核心需求:
pfn_to_page(pfn)和page_to_pfn(page)必须非常快。 -
在 sparsemem 下,
pfn_to_page()的典型计算逻辑:- 计算 section 下标:
- 计算在 section 内的页偏移:
- 找到该 section 的
section_mem_map:mem_map = __nr_to_section(section)->section_mem_map; - 返回对应页:
page = &mem_map[offset];
- 计算 section 下标:
-
之所以能做到纯算术 + 数组下标访问,前提就是:
- 对于一个 present 的 section,整个 PRFN 范围都有一个连续的
struct page数组与其一一对应,无论是否洞。
- 对于一个 present 的 section,整个 PRFN 范围都有一个连续的
-
对于洞中的页,
struct page仍然存在,但通过flags(如PG_reserved)表示“不可用”。
这是一种典型的“空间换时间”设计:
- 空间成本:
每个struct page占几十字节,一个大 section 会用掉几百 KB 或几 MB 的内核内存。 - 时间收益:
换来 pfn/page 双向转换在内核核心路径上的极高性能,避免复杂查表和搜索逻辑。
7. SPARSEMEM_VMEMMAP vs 传统 SPARSEMEM(简要)
- SPARSEMEM_VMEMMAP:
- 将所有
struct page自身放进一个专门的虚拟地址空间(vmemmap)。 - 对于“可能存在的所有 PFN”都有一条
struct page,洞则用PG_reserved等标识。 - pfn 与
struct page*之间几乎是一一映射(加偏移即可),更统一。
- 将所有
- 传统 SPARSEMEM(非 vmemmap):
- 每个 present 的 section 单独分配一块
struct page数组。 - Section 内部的洞也有对应
struct page,用于保持索引连续,但完全空的 section 不分配。
- 每个 present 的 section 单独分配一块
共同点:
只要 section 被认为是 present,其覆盖范围内的 PFN 一般都会有对应的 struct page 对象;“是否真实内存”则通过 struct page 的标志位区分。
8. 总结思维模型
可以用以下模型在脑子里“画图”:
-
第一层:mem_section 网格
- 整个物理地址空间被均匀划分为一个个网格(mem_section)。
- 每个网格编号为
i,负责[i * SECTION_SIZE, (i+1) * SECTION_SIZE)。
-
第二层:section_mem_map 账本
- 每个**存在内存的网格(present section)**拥有一本“账本”(
section_mem_map),账本的每一页是一个struct page,对应网格里的一个 PFN。 - 账本是连续页码 0 ~
PAGES_PER_SECTION-1,可以通过简单算术找到某一页。
- 每个**存在内存的网格(present section)**拥有一本“账本”(
-
第三层:page 标志位
- 真实内存页:账本该页记录为“可用”,会被加入伙伴系统。
- 内存空洞页:账本该页记录为“保留/无效”(例如
PG_reserved),不会参与分配。
-
逻辑上的 start_pfn
- 概念上:“为 section 设定 start_pfn”。
- 实现上:通过 section 下标 * PAGES_PER_SECTION 计算,不存成字段。
这样,你之前那套叙述:
- “mem_section 不需要是完整的 256M 内存”
- “热插拔不要求是 SECTION_SIZE 的倍数”
- “内存不一定从 section 起始 PFN 开始”
- “空洞也会有对应 page(在 present section 内部)”
- “start_pfn 是通过位置计算出来的,并不真有字段”
在逻辑层面都是自洽且与实现吻合的。只要加上一个小修正:
“完全空的 section 一般连 mem_section/section_mem_map 都不会分配,自然也没有 struct page”,空洞有 page 这件事是“存在于已经 present 的 section 内部”的语境下成立的。