diff options
Diffstat (limited to 'docs/specs/pmm.md')
| -rw-r--r-- | docs/specs/pmm.md | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/docs/specs/pmm.md b/docs/specs/pmm.md new file mode 100644 index 0000000..5703e7a --- /dev/null +++ b/docs/specs/pmm.md @@ -0,0 +1,209 @@ +# 物理内存管理器 (Physical Memory Manager) + +## 1. 目标 (Objective) + +为 LumeOS 提供对物理内存页帧(Frame)的分配与回收服务。本模块基于伙伴系统(Buddy System)算法管理所有可用物理内存,并引入 Per-CPU 缓存(PCP)机制以降低多核场景下的锁竞争。PMM 是 Slab 分配器和 VMM 的底层基石。 + +## 2. 前置依赖 (Dependencies) + +- **全局初始化契约 (`global_init.md`):** `pmm_init()` 在 `fdt_init()` 之后、`slab_init()` 之前被 BSP 调用。 +- **设备树契约 (`fdt.md`):** 依赖 `fdt_early_get_mem_info()` 获取物理内存的基址和大小。 +- **并发同步契约 (`spinlock.md`):** 依赖 `Spinlock` 实现对全局伙伴系统的互斥访问。 +- **架构抽象契约 (`arch_abstraction.md`):** 依赖 `arch::cpu::id()` 获取当前 CPU 编号以访问 Per-CPU 缓存;依赖 `arch::cpu::intr_off()/intr_on()` 保护 PCP 操作免受中断重入。 +- **项目约定 (`00_conventions.md`):** 遵循所有命名、错误处理和 API 设计规范。 + +## 3. 核心数据结构 (Data Structures) + +### 3.1 物理页帧描述符 (`struct Frame`) + +为了极致地压缩元数据内存占用,`struct Frame` 采用 `union` 设计,并严格保证 `refcount` 独立于 `union` 之外,以支持 COW。 + +```cpp +// 定义于 include/lume/frame.h +#include <lume/types.h> +#include <lume/list.h> + +class KmemCache; // 前向声明 + +enum class FrameState : uint8 { + Free, // 空闲,位于 Buddy 或 PCP 链表中 + Allocated, // 已分配给上层(Slab, VMM) + Slab, // 作为 Slab 页使用 +}; + +struct Frame { + uint32 refcount; // 原子引用计数(独立于 union) + FrameState state; // 页帧当前状态 + uint8 order; // 伙伴系统中的阶数 (0-10 for 4KB-4MB) + + union { + // 当 state == FrameState::Free 时 + struct { + list_node free_link; // 用于挂载到 Buddy 或 PCP 的空闲链表 + } free; + + // 当 state == FrameState::Slab 时 + struct { + KmemCache* cache; // 指向所属的 Slab 缓存 + uint32 obj_count; // 已分配对象数量 + } slab; + }; +}; +``` + +### 3.2 Per-CPU 单页缓存 (`struct PerCpuCache`) + +每个 CPU 私有的单页帧空闲链表。**访问 PCP 时必须关闭中断**,防止定时器中断处理函数在同一 CPU 上重入 PCP 操作导致链表损坏。 + +```cpp +// 定义于 mm/pmm.cc (模块私有) +inline constexpr int kPcpHighWatermark = 64; // PCP 缓存上限 +inline constexpr int kPcpBatchSize = 32; // 每次 refill/drain 的批量大小 + +struct PerCpuCache { + list_node free_list; // 单页帧空闲链表头 + uint32 count; // 当前缓存的页帧数量 +}; +``` + +### 3.3 伙伴系统常量 + +```cpp +// 定义于 include/lume/pmm.h +inline constexpr int kMaxOrder = 11; // 阶数 0..10 → 4KB..4MB +``` + +## 4. 公共 API (Public APIs) + +所有分配函数在失败时返回 `nullptr`。 + +```cpp +// === 初始化 === +void pmm_init(); // BSP 调用,从 FDT 发现内存并初始化伙伴系统 + +// === 单页帧操作 (Fast Path via PCP) === +Frame* pmm_alloc_frame(); // 分配一个 4KB 页帧 +void pmm_free_frame(Frame* f); // 释放一个 4KB 页帧 + +// === 多页帧操作 (Slow Path, 直接伙伴系统) === +Frame* pmm_alloc_frames(uint8 order); // 分配 2^order 个连续页帧 +void pmm_free_frames(Frame* f, uint8 order); // 释放 2^order 个连续页帧 + +// === 引用计数操作 (原子, 无锁) === +void frame_incref(Frame* f); // 原子递增引用计数 +void frame_decref(Frame* f); // 原子递减,降至 0 时自动释放 + +// === 地址转换 === +uint64 frame_to_pa(const Frame* f); // Frame* → 物理地址 +Frame* pa_to_frame(uint64 pa); // 物理地址 → Frame* +uint64 frame_to_va(const Frame* f); // Frame* → 内核虚拟地址 +``` + +## 5. 并发与锁契约 (Concurrency & Locks) + +- **持有锁:** 模块内部持有一把 `Spinlock` (`g_pmm_lock`)。 + +- **锁的作用域:** `g_pmm_lock` **仅保护**全局伙伴系统 (`g_free_areas[]`)。 + +- **PCP 保护机制:** PCP 操作**必须在中断关闭状态下**执行。这不是"无锁"——而是通过关中断防止同一 CPU 上的重入(定时器中断 → IRQ handler → pmm_alloc_frame → 损坏 PCP 链表)。关中断由 `pmm_alloc_frame()`/`pmm_free_frame()` 内部自动管理。 + +- **加锁慢速路径:** + - 当 PCP 为空时,`refill_pcp()` 获取 `g_pmm_lock` 从伙伴系统批量取页。 + - 当 PCP 达到 `kPcpHighWatermark` 时,`drain_pcp()` 获取 `g_pmm_lock` 归还半数页帧。 + - `pmm_alloc_frames()` 和 `pmm_free_frames()` 因直接操作伙伴系统,**始终需要**获取 `g_pmm_lock`。 + +- **原子操作:** `frame_incref()` / `frame_decref()` 使用 `__atomic_fetch_add/sub` + `__ATOMIC_ACQ_REL`。 + +## 6. 错误路径 (Error Paths) + +- **初始化失败:** `pmm_init()` 若无法从 FDT 找到可用内存,或无法为 `frame_map` 分配空间,则 `kernel_panic()`。 + +- **分配失败:** `pmm_alloc_*()` 在物理内存耗尽时返回 `nullptr`。上层负责处理。 + +- **释放错误:** `pmm_free_*()` 若接收到 `nullptr`,立即 `kernel_panic()`。 + +## 7. 状态机 / 时序图 (State Machine / Sequence) + +### 7.1 单页分配时序 (Fast & Slow Path) + +``` +[pmm_alloc_frame() on CPU X] + │ + ├─> 1. 关闭中断 (防止同 CPU 重入) + │ + ├─> 2. 获取 pcp = &g_pcp[cpu_id] + │ + ├─> 3. if (pcp->count > 0) ? + │ ├── [是 (Fast Path)] + │ │ ├─ 3.1 从 pcp->free_list 弹出一个 Frame + │ │ ├─ 3.2 设置 state=Allocated, refcount=1 + │ │ └─ 3.3 恢复中断, return Frame + │ │ + │ └── [否 (Slow Path)] + │ ├─ 4.1 LockGuard guard(g_pmm_lock); + │ ├─ 4.2 refill_pcp(pcp); // 批量从 Buddy 取 kPcpBatchSize 页 + │ ├─ 4.3 释放锁 + │ └─ 4.4 回到步骤 3 重试 + │ + ▼ +[恢复中断, 返回 Frame* 或 nullptr] +``` + +### 7.2 初始化时序 + +``` +[pmm_init() — BSP 单核期] + │ + ├─> 1. fdt_early_get_mem_info(&base, &size) + │ └─ 获取物理内存基址和大小 + │ + ├─> 2. 计算 frame_map 位置 (紧跟 _kernel_end 之后, 页对齐) + │ └─ frame_map 大小 = num_frames * sizeof(Frame) + │ + ├─> 3. 清零 frame_map + │ + ├─> 4. 初始化 Spinlock 和 Buddy 空闲链表头 + │ + ├─> 5. 初始化所有 PCP 链表头 + │ + └─> 6. 将所有空闲页帧 (frame_map 之后) 按最大对齐阶数插入 Buddy +``` + +### 7.3 写时复制 (COW) 引用计数流程 + +``` +[父进程 fork() 创建子进程] + │ + ├─> 1. 遍历父进程所有可写 VMA + │ + ├─> 2. for each VMA: + │ ├─ 2.1 复制父进程的 PTE 到子进程页表 + │ ├─ 2.2 清除父、子 PTE 的 'Write' 权限位 + │ ├─ 2.3 设置父、子 PTE 的 'COW' 软件标志位 + │ └─ 2.4 frame_incref(pa_to_frame(PTE->paddr)); + │ + ▼ +[子进程尝试写入共享页面] + │ + ├─> 1. 触发 Store Page Fault + │ + ├─> 2. VMM 陷阱处理: + │ ├─ 2.1 检查 PTE,确认是合法的 COW Fault + │ ├─ 2.2 old_frame = pa_to_frame(PTE->paddr) + │ ├─ 2.3 if (old_frame->refcount == 1) ? + │ │ ├── [是] 直接恢复 Write 权限,无需拷贝 + │ │ └── [否] 分配新页, 拷贝, 修改 PTE, frame_decref(old) + │ + ▼ +[返回用户态,写操作成功] +``` + +## 8. 已知缺陷与未来挑战 (Known Weaknesses & Future Challenges) + +### 8.1 内存回收 + +当前 API 在物理内存耗尽时仅返回 `nullptr`,缺乏主动回收机制。未来需引入内存回收器。 + +### 8.2 frame_map 存储 + +当前设计假设单一连续 `frame_map` 数组,适用于 QEMU 线性内存。对于多内存区域的真实硬件,需演进为稀疏内存模型。 |
