diff options
| author | Richard Qin <richard.qin.x@gmail.com> | 2026-04-21 23:07:23 +0800 |
|---|---|---|
| committer | Richard Qin <richard.qin.x@gmail.com> | 2026-04-21 23:07:23 +0800 |
| commit | e9a3dac62746f175f92ace4cc461de2924ea92f8 (patch) | |
| tree | 9348a2fbbe6d61e7373f74c927f1b55faed3c2c9 /docs | |
| parent | b0ccdda4bc3af04e54677cd910ed04c5ba1b1a63 (diff) | |
| download | lume-e9a3dac62746f175f92ace4cc461de2924ea92f8.tar.gz lume-e9a3dac62746f175f92ace4cc461de2924ea92f8.tar.xz | |
docs: add specifications for C++ Kernel Runtime, Flattened Device Tree, Physical Memory Manager, SLUB Allocator, and Virtual Memory Manager
- Introduced `cpp_runtime.md` detailing the C++ ABI stubs required for LumeOS kernel, including panic mechanisms and error paths.
- Added `fdt.md` to outline the device tree management for hardware discovery and configuration, including data structures and public APIs.
- Created `pmm.md` to specify the physical memory management system based on the buddy system, including frame allocation and concurrency handling.
- Developed `slab.md` to describe the SLUB allocator for efficient memory allocation, detailing its structure and public APIs.
- Established `vmm.md` to define the virtual memory manager's objectives, dependencies, and core data structures for managing virtual address space.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/specs/cpp_runtime.md | 55 | ||||
| -rw-r--r-- | docs/specs/fdt.md | 191 | ||||
| -rw-r--r-- | docs/specs/pmm.md | 209 | ||||
| -rw-r--r-- | docs/specs/slab.md | 233 | ||||
| -rw-r--r-- | docs/specs/vmm.md | 158 |
5 files changed, 846 insertions, 0 deletions
diff --git a/docs/specs/cpp_runtime.md b/docs/specs/cpp_runtime.md new file mode 100644 index 0000000..e3ebd90 --- /dev/null +++ b/docs/specs/cpp_runtime.md @@ -0,0 +1,55 @@ +# C++ 内核运行时 (C++ Kernel Runtime) + +## 1. 目标 (Objective) + +为 LumeOS 内核提供最小化的 C++ ABI 存根函数。在 `-fno-exceptions -fno-rtti -nostdlib` 环境下,编译器和链接器仍会引用若干 C++ 运行时符号(如纯虚函数陷阱、全局析构注册、DSO handle)。本模块提供这些符号的内核实现,防止链接失败。 + +**职责边界:** +- `operator new/delete` 由 Slab 分配器模块 (`mm/slab.cc`) 提供,**不在本模块**。 +- Placement new 由 `include/lume/new.h` 提供,**不在本模块**。 +- 本模块仅提供 ABI 存根和编译器辅助符号。 + +## 2. 前置依赖 (Dependencies) + +- **Panic 机制 (`lib/panic.cc`):** 依赖 `kernel_panic()` 在遇到不可恢复错误时终止系统。 +- **项目约定 (`00_conventions.md`):** 遵循 `-fno-exceptions -fno-rtti` 编译约束。 + +## 3. 核心数据结构 (Data Structures) + +无。本模块仅实现全局函数和符号。 + +## 4. 公共 API (Public APIs) + +这些符号由编译器/链接器隐式引用,内核代码不应直接调用。 + +```cpp +// 定义于 lib/cxx_abi.cc + +// 当代码通过基类指针调用未实现的纯虚函数时, +// 编译器生成的 vtable 条目会引用此符号。 +// 这是不可恢复的编程错误。 +extern "C" void __cxa_pure_virtual(); + +// 全局对象析构注册。内核不支持全局对象析构(全局对象 +// 通过 Placement New 手动管理生命周期)。若编译器生成 +// 对此符号的调用,说明代码中存在违规的全局对象。 +extern "C" int __cxa_atexit(void (*func)(void*), void* arg, void* dso_handle); + +// DSO handle — 链接器在某些情况下引用此符号 +// (与 __cxa_atexit 配合标识动态共享对象)。 +// 内核为静态链接,此符号仅需存在即可。 +extern "C" void* __dso_handle; +``` + +## 5. 并发与锁契约 (Concurrency & Locks) + +无。所有函数要么立即 panic,要么是静态符号。 + +## 6. 错误路径 (Error Paths) + +- `__cxa_pure_virtual()` → 立即 `kernel_panic("pure virtual function call")`。 +- `__cxa_atexit()` → 立即 `kernel_panic("__cxa_atexit called in kernel")`。 + +## 7. 状态机 / 时序图 (State Machine / Sequence) + +无复杂时序。所有路径均为:检测到违规 → panic。 diff --git a/docs/specs/fdt.md b/docs/specs/fdt.md new file mode 100644 index 0000000..2a1d542 --- /dev/null +++ b/docs/specs/fdt.md @@ -0,0 +1,191 @@ +# 设备树 (Flattened Device Tree) + +## 1. 目标 (Objective) + +为 LumeOS 提供一套标准的硬件发现与配置机制,彻底摆脱硬编码的物理地址。本模块负责解析由 Bootloader 传入的 FDT 二进制数据,并分两个阶段将其转化为内核可用的数据结构: + +1. **早期扫描 (Early Scan):** 在内存管理(PMM)初始化之前,以只读、无分配的方式扫描 FDT,提取启动所必需的核心信息(如物理内存布局)。 +2. **对象化展开 (Unflattening):** 在内存管理(PMM/Slab)就绪后,将完整的 FDT 解析为一个常驻内存的 C++ `DeviceNode` 对象树,为后续的驱动探测(Driver Probing)提供面向对象的接口。 + +## 2. 前置依赖 (Dependencies) + +- **引导协议契约 (`linker_and_entry.md`):** 依赖 Bootloader 在跳转至内核时,将 FDT 的物理基址置于 `a1` 寄存器。 +- **全局初始化契约 (`global_init.md`):** `fdt_init()` 在 `console_init()` 之后、`pmm_init()` 之前被 BSP 调用。`pmm_init()` 自身会回调 FDT 的早期扫描接口。 +- **内存管理契约 (`pmm.md`, `slab.md`):** 对象化展开阶段依赖 Slab 分配器(`operator new`)来动态创建 `DeviceNode` 对象。 +- **项目约定 (`00_conventions.md`):** 遵循所有命名、错误处理和 API 设计规范。 + +## 3. 核心数据结构 (Data Structures) + +### 3.1 FDT 属性 (`struct FdtProperty`) + +表示设备节点中的一个属性(如 `reg`, `compatible`)。 + +```cpp +// 定义于 include/lume/fdt_prop.h +struct FdtProperty { + const char* name; // 属性名 + const void* value; // 指向 FDT blob 中原始数据的指针 + int len; // 数据长度(字节) + list_node link; // 链接到下一个属性 +}; +``` + +### 3.2 设备节点 (`class DeviceNode`) + +FDT 对象树的核心,代表一个硬件设备或总线。 + +```cpp +// 定义于 include/lume/device_node.h +class DeviceNode { +public: + // 构造/析构 + DeviceNode(DeviceNode* parent, const char* name); + ~DeviceNode(); + + // 属性访问 + const FdtProperty* get_prop(const char* name) const; + uint32_t get_prop_u32(const char* name, uint32_t default_val = 0) const; + uint64 get_prop_u64(const char* name, uint64 default_val = 0) const; + const char* get_prop_string(const char* name) const; + + // 子节点遍历 + DeviceNode* find_child(const char* name); + void for_each_child(void (*callback)(DeviceNode*)); + + // 内部接口,供 FdtManager 构建树时使用 + void add_child(DeviceNode* child); + void add_prop(FdtProperty* prop); + +private: + const char* name_; + DeviceNode* parent_; + list_node children_list_; // list_head of children + list_node props_list_; // list_head of properties + list_node sibling_link_; // link in parent's children_list_ +}; +``` + +### 3.3 FDT 管理器 (`class FdtManager`) + +全局单例,负责 FDT 的解析与查询。 + +```cpp +// 定义于 include/lume/fdt.h +class FdtManager { +public: + // 由 fdt_init() 通过 Placement New 调用 + FdtManager(uint64 fdt_paddr); + + // 阶段二:对象化展开 + void unflatten(); + + // 驱动探测接口 + DeviceNode* find_compatible(const char* compat_string); + DeviceNode* get_node_by_path(const char* path); + + // 阶段一:早期扫描接口 (静态,无状态) + static void early_scan_mem(uint64 fdt_paddr, uint64* base, uint64* size); + static void early_scan_uart(uint64 fdt_paddr, uint64* uart_addr); + +private: + uint64 fdt_paddr_; + DeviceNode* root_ = nullptr; + bool is_unflattened_ = false; +}; + +// 全局 FDT 管理器实例指针 +extern FdtManager* g_fdt; +``` + +## 4. 公共 API (Public APIs) + +```cpp +// === 初始化 API (由 kernel_main 调用) === + +// 阶段一和阶段二的入口。仅保存 FDT 地址,真正的解析被推迟。 +void fdt_init(uint64 fdt_paddr); + +// 在 Slab 就绪后调用,执行对象化展开。 +void fdt_unflatten(); + + +// === 驱动探测 API (供各驱动 init 函数调用) === + +// 根据 "compatible" 字符串查找第一个匹配的设备节点。 +// 返回值:成功则为 DeviceNode 指针,失败为 nullptr。 +DeviceNode* fdt_find_compatible(const char* compat_string); + +// 根据完整路径查找设备节点。 +DeviceNode* fdt_get_node_by_path(const char* path); + + +// === 早期扫描 API (供 PMM 和早期控制台调用) === + +// 扫描 FDT blob 获取 /memory 节点的 base 和 size。 +// 失败则 panic。 +void fdt_early_get_mem_info(uint64* base, uint64* size); + +// 扫描 FDT blob 获取 /chosen/stdout-path 对应的 UART 物理地址。 +void fdt_early_get_uart_info(uint64* uart_addr); +``` + +## 5. 并发与锁契约 (Concurrency & Locks) + +- **无锁设计:** FDT 的所有初始化和解析操作(`fdt_init`, `fdt_unflatten`)均由 BSP 在多核启动前的**绝对单核期**完成。 + +- **只读安全:** 一旦 `fdt_unflatten()` 完成,`DeviceNode` 对象树即被视为**全局只读**数据。任何 CPU 都可以随时无锁访问它进行驱动探测和属性查询。 + +- **禁止修改:** 任何模块在初始化之后都**不得**修改 `DeviceNode` 树的内容。 + +## 6. 错误路径 (Error Paths) + +- **FDT 损坏:** 在 `fdt_init()` 或 `fdt_unflatten()` 中,如果检测到 FDT 的 magic number 无效或结构损坏,必须立即调用 `kernel_panic()`。 + +- **关键节点缺失:** `fdt_early_get_mem_info()` 如果找不到 `/memory` 节点,说明无法确定物理内存大小,属于致命错误,必须 `kernel_panic()`。 + +- **内存分配失败:** `fdt_unflatten()` 在使用 `new` 创建 `DeviceNode` 时如果 Slab 返回 `nullptr`,说明内存耗尽,必须 `kernel_panic()`。 + +- **查询失败:** `fdt_find_compatible()` 或 `fdt_get_node_by_path()` 在未找到匹配节点时,安全地返回 `nullptr`。调用方(驱动)负责处理此情况(通常意味着该硬件不存在,驱动不进行初始化)。 + +## 7. 状态机 / 时序图 (State Machine / Sequence) + +### FDT 二阶段解析时序 + +``` +[entry.S] -> a1 = fdt_paddr + │ + ▼ +[kernel_main on BSP] + │ + ├─> 1. fdt_init(fdt_paddr) + │ └─ g_fdt = new FdtManager(fdt_paddr); // Placement New + │ (仅保存地址,不进行任何解析) + │ + ├─> 2. pmm_init() + │ └─ fdt_early_get_mem_info(&base, &size); + │ └─ FdtManager::early_scan_mem(fdt_paddr, ...); + │ ├─ 遍历原始 FDT blob (无内存分配) + │ ├─ 找到 /memory 节点 + │ └─ 返回 reg 属性值 + │ + ├─> 3. slab_init() + │ └─ operator new 可用 + │ + ├─> 4. (稍后) fdt_unflatten() + │ └─ g_fdt->unflatten(); + │ ├─ 再次遍历原始 FDT blob + │ ├─ for each node: new DeviceNode(...) + │ ├─ for each prop: new FdtProperty(...) + │ └─ 构建完整的 C++ 对象树 + │ + ▼ +[驱动初始化阶段, e.g., plic_init()] + │ + ├─> node = fdt_find_compatible("riscv,plic0"); + │ └─ g_fdt->find_compatible(...); + │ └─ 遍历已构建的 DeviceNode C++ 对象树 (只读, 无锁) + │ + ├─> reg_prop = node->get_prop("reg"); + │ + └─> (使用 reg_prop->value 初始化 PLIC 驱动) +```
\ No newline at end of file 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 线性内存。对于多内存区域的真实硬件,需演进为稀疏内存模型。 diff --git a/docs/specs/slab.md b/docs/specs/slab.md new file mode 100644 index 0000000..dc6953a --- /dev/null +++ b/docs/specs/slab.md @@ -0,0 +1,233 @@ +# SLUB 分配器 (SLUB Allocator) + +## 1. 目标 (Objective) + +在物理内存管理器 (PMM) 之上,为内核提供高效的字节级内存分配器。采用 **SLUB** 设计(Simple List of Unfull Blocks),通过复用 PMM 的 `Frame` 描述符存储 per-page 元数据、将空闲链表嵌入对象体内、以及 Per-CPU 活跃页机制,实现低碎片、低锁竞争的小对象分配。本模块是内核 `operator new` 的直接后端。 + +**为何选择 SLUB 而非经典 Slab:** +- `Frame` 结构已包含 `cache` 指针和 `obj_count` 字段,无需额外的 per-page `struct Slab` 头部。 +- 省去 full/partial/empty 三链表管理,仅需一个 partial 链表。 +- Per-CPU 缓存仅需一个"当前活跃页"指针,无需动态分配对象数组(避免 slab_init 的鸡生蛋问题)。 +- Linux 自 2.6.23 起默认 SLUB,6.8 已完全移除经典 Slab。 + +## 2. 前置依赖 (Dependencies) + +- **全局初始化契约 (`global_init.md`):** `slab_init()` 在 `pmm_init()` 之后、`vmm_init()` 之前被 BSP 调用。 +- **物理内存契约 (`pmm.md`):** 依赖 `pmm_alloc_frame()` / `pmm_free_frame()` 以及 `Frame` 描述符中的 `slab.cache` 和 `slab.obj_count` 字段。还依赖 `pa_to_frame()` / `frame_to_pa()` 进行地址转换。 +- **并发同步契约 (`spinlock.md`):** 依赖 `Spinlock` 保护全局 partial 链表。 +- **架构抽象契约 (`arch_abstraction.md`):** 依赖 `arch::cpu::id()` 访问 Per-CPU 缓存;依赖 `arch::cpu::intr_off()/intr_on()` 保护 Per-CPU 操作免受中断重入。 +- **项目约定 (`00_conventions.md`):** 遵循所有命名和类型规范。 + +## 3. 核心数据结构 (Data Structures) + +### 3.1 空闲对象链表(嵌入式) + +空闲对象的前 `sizeof(void*)` 字节存储下一个空闲对象的指针。对象被分配后,该区域归用户使用。因此**最小对象尺寸 = `sizeof(void*)` = 8 字节**。 + +``` +[ Free Object ] [ Free Object ] [ Free Object ] ++-------------+ +-------------+ +-------------+ +| next ---------.------->| next ---------.------->| next = NULL | +| (unused) | | (unused) | | (unused) | ++-------------+ +-------------+ +-------------+ +``` + +### 3.2 Frame 描述符中的 Slab 元数据 + +复用 `include/lume/frame.h` 中已有的字段(无需新增结构体): + +```cpp +// 当 Frame.state == FrameState::Slab 时有效 +struct { + KmemCache* cache; // 此页属于哪个 KmemCache + uint32 obj_count; // 此页上已分配的对象数量 +} slab; +``` + +- 此外,需要在 `Frame` 的 `free` union 成员中增加一个 `void* freelist` 字段,或者直接将 freelist 头指针嵌入 union。为保持 Frame 结构简洁,**freelist 头指针存储在页面固定偏移处(页起始的前 8 字节)**,不侵入 Frame 结构。 + +**修正方案:** 在 `Frame` 的 `slab` union 成员中增加 `void* freelist` 字段: + +```cpp +struct { + KmemCache* cache; // 所属缓存 + uint32 obj_count; // 已分配对象数 + void* freelist; // 页内空闲对象链表头 +} slab; +``` + +### 3.3 Per-CPU 活跃页 + +每个 CPU 对每个 KmemCache 维护一个"当前活跃页"(cpu_slab)。分配时优先从此页的 freelist 弹出对象,无需获取全局锁。 + +```cpp +// 定义于 mm/slab.h (模块私有) +struct PerCpuSlab { + Frame* active; // 当前 CPU 正在使用的 slab 页(可能为 nullptr) +}; +``` + +### 3.4 内核内存缓存 (`class KmemCache`) + +```cpp +// 定义于 mm/slab.h +class KmemCache { +public: + KmemCache() = default; + void init(const char* name, uint32 obj_size, uint32 obj_align); + + void* alloc(); + void free(void* obj); + +private: + Frame* get_partial(); // 从 partial 链表取一个页 + void put_partial(Frame* f); // 将页放入 partial 链表 + Frame* new_slab(); // 从 PMM 申请新页并格式化 + + Spinlock lock_; // 保护 partial 链表 + list_node partial_; // 部分填充的 slab 页链表 + + PerCpuSlab cpu_slab_[kMaxCPUs]; // Per-CPU 活跃页 + + const char* name_ = "uninit"; + uint32 obj_size_ = 0; // 对齐后的对象尺寸 + uint32 obj_align_ = 0; // 对齐要求 + uint32 objs_per_slab_ = 0; // 每页可容纳的对象数 +}; +``` + +### 3.5 通用尺寸缓存 + +```cpp +// 预创建的 2^N 尺寸缓存: 8, 16, 32, 64, 128, 256, 512, 1024, 2048 +inline constexpr int kNumSizeClasses = 9; +inline constexpr uint32 kSizeClasses[kNumSizeClasses] = { + 8, 16, 32, 64, 128, 256, 512, 1024, 2048 +}; + +extern KmemCache g_size_caches[kNumSizeClasses]; +``` + +## 4. 公共 API (Public APIs) + +```cpp +// === 初始化 === +void slab_init(); // 初始化所有通用尺寸缓存 + +// === 对象缓存 API === +void kmem_cache_init(KmemCache* cache, const char* name, + uint32 size, uint32 align); +void* kmem_cache_alloc(KmemCache* cache); +void kmem_cache_free(KmemCache* cache, void* obj); + +// === 通用分配 API === +void* kmalloc(uint32 size); // 自动选择尺寸缓存 +void kfree(void* ptr); // 通过 Frame 反查 cache,自动归还 + +// === C++ Runtime === +void* operator new(size_t size); +void* operator new[](size_t size); +void operator delete(void* ptr) noexcept; +void operator delete[](void* ptr) noexcept; +void operator delete(void* ptr, size_t) noexcept; +void operator delete[](void* ptr, size_t) noexcept; +``` + +## 5. 并发与锁契约 (Concurrency & Locks) + +- **持有锁:** 每个 `KmemCache` 持有一把 `Spinlock lock_`。 + +- **锁的作用域:** `lock_` **仅保护** `partial_` 链表。 + +- **Per-CPU 保护:** 访问 `cpu_slab_[cpu_id]` 时必须关闭中断(同 PMM 的 PCP 策略),防止中断重入。 + +- **快速路径(关中断,无锁):** 从当前 CPU 的 `active` 页的 `freelist` 弹出/压入对象。 + +- **慢速路径(需要锁):** + - alloc:当 `active` 页无空闲对象时,获取 `lock_` 从 `partial_` 取页(或调用 PMM 新建)。 + - free:当归还对象使一个页变为完全空闲时,获取 `lock_` 归还 PMM。 + +- **与 PMM 交互:** `new_slab()` 调用 `pmm_alloc_frame()` 时**不持有** `lock_`(先释放锁,申请页,再获取锁挂入链表),避免 Slab lock → PMM lock 嵌套。 + +## 6. 错误路径 (Error Paths) + +- **初始化失败:** `slab_init()` 若 PMM 无法提供页面,`kernel_panic()`。 + +- **分配失败:** `kmalloc()` / `kmem_cache_alloc()` 在 OOM 时返回 `nullptr`。 + +- **释放错误:** `kfree(nullptr)` 为 no-op。传入非法指针(不属于任何 slab 页)时 `kernel_panic()`。 + +## 7. 状态机 / 时序图 (State Machine / Sequence) + +### 7.1 `kmem_cache_alloc` 时序 + +``` +[kmem_cache_alloc(cache) on CPU X] + │ + ├─> 1. 关中断 + ├─> 2. active = cache->cpu_slab_[X].active + │ + ├─> 3. if (active && active->slab.freelist != NULL) ? + │ ├── [是 (Fast Path)] + │ │ ├─ obj = active->slab.freelist + │ │ ├─ active->slab.freelist = *(void**)obj + │ │ ├─ active->slab.obj_count++ + │ │ ├─ 恢复中断 + │ │ └─ return obj + │ │ + │ └── [否 (Slow Path)] + │ ├─ 4.1 将 active (若满) 从 cpu_slab 移出 + │ ├─ (满页不需要跟踪,但其 Frame 中 cache 指针仍有效) + │ ├─ 4.2 恢复中断 + │ ├─ 4.3 LockGuard guard(cache->lock_) + │ ├─ 4.4 new_active = get_partial() // 从 partial 链表取 + │ │ └─ 若无,释放锁 → new_slab() → 重新获取锁 + │ ├─ 4.5 释放锁 + │ ├─ 4.6 关中断 + │ ├─ 4.7 cache->cpu_slab_[X].active = new_active + │ ├─ 4.8 从 new_active->slab.freelist 弹出 obj + │ ├─ 4.9 恢复中断 + │ └─ return obj + │ + ▼ +[返回 obj* 或 nullptr] +``` + +### 7.2 `kfree` 时序 + +``` +[kfree(ptr)] + │ + ├─> 1. pa = ptr 的物理地址 (对于 identity/higher-half,减去偏移) + ├─> 2. frame = pa_to_frame(pa & ~(kPageSize-1)) + ├─> 3. cache = frame->slab.cache + │ + ├─> 4. 关中断 + ├─> 5. 将 obj 压入 frame->slab.freelist + ├─> 6. frame->slab.obj_count-- + │ + ├─> 7. if (obj_count == 0) ? + │ ├── [是:页完全空闲] + │ │ ├─ 若此页是当前 CPU 的 active,清除 active + │ │ ├─ 恢复中断 + │ │ ├─ LockGuard: 从 partial_ 链表移除 (如果在链表中) + │ │ └─ pmm_free_frame(frame) // 归还 PMM + │ │ + │ └── [否] + │ ├─ 若此页不是任何 CPU 的 active 且不在 partial, + │ │ 则需获取锁放入 partial (满→部分满的过渡) + │ └─ 恢复中断 + │ + ▼ +[完成] +``` + +## 8. 已知缺陷与未来挑战 + +### 8.1 缓存收缩 + +当前不主动收缩 partial 链表。未来可增加 `kmem_cache_shrink()` 供内存回收线程调用。 + +### 8.2 大对象分配 + +大于 2048 字节的分配直接退化为 `pmm_alloc_frames()`。当前由 `kmalloc` 透明处理。 diff --git a/docs/specs/vmm.md b/docs/specs/vmm.md new file mode 100644 index 0000000..7492a02 --- /dev/null +++ b/docs/specs/vmm.md @@ -0,0 +1,158 @@ +# 虚拟内存管理器 (Virtual Memory Manager) + +## 1. 目标 (Objective) + +为 LumeOS 管理虚拟地址空间。本模块分两个阶段实现: + +- **Phase 1(本文档重点):** + 1. 在 `vmm_init()` 中建立精细化的内核页表(4KB 粒度、精确权限)。 + 2. 实现基于 Rbtree 和 Slab 的 VMA (Virtual Memory Area) 区间树以管理用户态内存。 + 3. 实现架构无关的按需分页 (Demand Paging) 模拟接口。 +- **Phase 2(未来):** 将 VMM 的按需分页逻辑绑定到真实的 Trap 异常中断系统,并引入进程 (Process/Task) 关联和基于硬件触发的缺页异常处理。 + +采用 pmap(Physical Map)分层模型:MI 层管理地址空间布局逻辑,MD 层(`arch/riscv/mm/pmap.cc`)封装 SV39 页表硬件操作。 + +## 2. 前置依赖 (Dependencies) + +- **全局初始化契约 (`global_init.md`):** `vmm_init()` 在 `slab_init()` 和 `fdt_unflatten()` 之后、`trap_init()` 之前被 BSP 调用。 +- **物理内存契约 (`pmm.md`):** 依赖 `pmm_alloc_frame()` 分配页表页。 +- **Slab 分配器契约 (`slab.md`):** Phase 2 依赖 `kmalloc` 创建 VMA 对象。 +- **架构抽象契约 (`arch_abstraction.md`):** 依赖 `arch::mmu::set_page_table()`, `arch::mmu::flush_tlb_all()` 切换和刷新页表。 +- **链接脚本契约 (`linker_and_entry.md`):** 依赖 `_stext/_etext`, `_srodata/_erodata`, `_sdata/_edata`, `_sbss/_ebss`, `_kernel_end` 确定各段边界。 + +## 3. 核心数据结构 (Data Structures) + +### 3.1 SV39 页表项 (PTE) 格式 + +``` +63 54 53 28 27 19 18 10 9 8 7 6 5 4 3 2 1 0 ++---------+----------+----------+----------+----+-+-+-+-+-+-+-+-+ +| Reserved| PPN[2] | PPN[1] | PPN[0] |RSW |D|A|G|U|X|W|R|V| +| (10) | (26) | (9) | (9) |(2) |1|1|1|1|1|1|1|1| ++---------+----------+----------+----------+----+-+-+-+-+-+-+-+-+ +``` + +权限位常量: + +```cpp +namespace pmap { + inline constexpr uint64 PTE_V = 1 << 0; // Valid + inline constexpr uint64 PTE_R = 1 << 1; // Read + inline constexpr uint64 PTE_W = 1 << 2; // Write + inline constexpr uint64 PTE_X = 1 << 3; // Execute + inline constexpr uint64 PTE_U = 1 << 4; // User-accessible + inline constexpr uint64 PTE_G = 1 << 5; // Global + inline constexpr uint64 PTE_A = 1 << 6; // Accessed + inline constexpr uint64 PTE_D = 1 << 7; // Dirty +} +``` + +### 3.2 pmap 接口 (MD 层, namespace 函数) + +遵循 `00_conventions.md` 的多态策略——pmap 为极热路径,使用 namespace 函数 + 编译期链接。 + +```cpp +// 定义于 arch/riscv/mm/pmap.h (arch 私有头) 和 mm/pmap.h (MI 接口) +namespace pmap { + // 分配并初始化一个新的根页表,返回其物理地址。 + // 失败返回 0。 + uint64 create(); + + // 销毁一个页表及其所有子页表页。 + void destroy(uint64 root_pa); + + // 建立映射:va -> pa,权限为 perm。 + // 自动分配中间页表页。 + // 返回 0 成功,负错误码失败。 + int map(uint64 root_pa, uint64 va, uint64 pa, uint64 perm); + + // 解除 va 处的映射。 + void unmap(uint64 root_pa, uint64 va); + + // 查询 va 对应的 pa。成功返回 true 并写入 *pa_out。 + bool lookup(uint64 root_pa, uint64 va, uint64* pa_out); + + // 将 root_pa 写入 satp 并刷新 TLB。 + void activate(uint64 root_pa); +} +``` + +### 3.3 内核地址空间布局 + +`vmm_init()` 建立的精细内核映射: + +``` +Virtual Address Physical Address Perm Description +───────────────────────────────────────────────────────────────────── +arch::kVAOffset + 0x80200000 0x80200000 R-X .text (代码段) +arch::kVAOffset + _srodata _srodata_pa R-- .rodata (只读数据) +arch::kVAOffset + _sdata _sdata_pa RW- .data + .bss +arch::kVAOffset + frame_map_pa frame_map_pa RW- PMM frame_map 数组 +arch::kVAOffset + 0x10000000 0x10000000 RW- UART MMIO +arch::kVAOffset + 0x0C000000 0x0C000000 RW- PLIC MMIO +arch::kVAOffset + mem_end ... RW- 剩余物理内存 (到 mem_end) +``` + +## 4. 公共 API (Public APIs) + +### 4.1 Phase 1 API + +```cpp +// 初始化内核页表,替代 early_pgdir。 +// 1. 分配新根页表 +// 2. 按段权限映射内核各段 +// 3. 映射 MMIO 区域 +// 4. 激活新页表 +// 5. 旧的 early_pgdir 恒等映射自动失效 +void vmm_init(); + +// AP 切换到 BSP 建好的内核页表 +void vmm_init_ap(); +``` + +### 4.2 Phase 2 API (未来) + +```cpp +// 暂为 stub,Phase 2 实现 +int vmm_map_user(uint64 root_pa, uint64 va, uint64 len, uint64 perm); +int vmm_unmap_user(uint64 root_pa, uint64 va, uint64 len); +int vmm_handle_page_fault(uint64 root_pa, uint64 fault_addr, uint64 cause); +uint64 vmm_clone(uint64 parent_root_pa); +``` + +## 5. 并发与锁契约 (Concurrency & Locks) + +- **Phase 1:** `vmm_init()` 在 BSP 单核期执行,无需锁。 +- **pmap 锁:** 每个根页表关联一把 Spinlock(Phase 2 在 AddressSpace 结构中)。多核操作同一页表必须持有此锁。 +- **TLB Shootdown:** Phase 1 仅有内核页表且在单核期建立,无需 shootdown。Phase 2 需实现。 + +## 6. 错误路径 (Error Paths) + +- `pmap::create()` 或 `pmap::map()` 中 `pmm_alloc_frame()` 失败 → `kernel_panic()`(内核页表映射失败不可恢复)。 +- `vmm_init()` 中任何映射步骤失败 → `kernel_panic()`。 + +## 7. 状态机 / 时序图 (State Machine / Sequence) + +### 7.1 vmm_init 时序 + +``` +[kernel_main on BSP, after slab_init + fdt_unflatten] + │ + ├─> 1. root_pa = pmap::create() // 分配新根页表 + │ + ├─> 2. 映射内核 .text 段 (RX) + │ for (pa = _stext_pa; pa < _etext_pa; pa += PAGE_SIZE) + │ pmap::map(root_pa, pa + arch::kVAOffset, pa, PTE_R|PTE_X|PTE_G) + │ + ├─> 3. 映射 .rodata 段 (R-) + ├─> 4. 映射 .data + .bss 段 (RW) + ├─> 5. 映射 frame_map 区域 (RW) + ├─> 6. 映射剩余物理内存 (RW) + ├─> 7. 映射 MMIO: UART 0x10000000, PLIC 0x0C000000 (RW) + │ + ├─> 8. pmap::activate(root_pa) // 写 satp, sfence.vma + │ └─ 此刻 early_pgdir 的恒等映射失效 + │ └─ 代码继续执行(因为高半区映射已建立) + │ + └─> 9. 保存 root_pa 到全局变量 g_kernel_pgtbl +``` |
