diff options
| author | Richard Qin <richard.qin.x@gmail.com> | 2026-04-17 00:54:20 +0800 |
|---|---|---|
| committer | Richard Qin <richard.qin.x@gmail.com> | 2026-04-17 00:54:20 +0800 |
| commit | 81563cb415d8214f1d1e34f73d49a404424f8f2c (patch) | |
| tree | ce4926b598f8eef00fe2fb2eff62766688c7bad9 /docs | |
| parent | ab83dfb9a607807a6373f9bd47190848b822cf6e (diff) | |
| download | lume-81563cb415d8214f1d1e34f73d49a404424f8f2c.tar.gz lume-81563cb415d8214f1d1e34f73d49a404424f8f2c.tar.xz | |
docs: add global initialization, linker and entry specifications, and spinlock documentation
- Introduced global initialization sequence documentation outlining the objectives, dependencies, core data structures, public APIs, concurrency contracts, error paths, and state machine for LumeOS.
- Created linker and entry documentation defining early memory layout and assembly startup sequence compatible with multiple bootloaders, including API specifications and error handling.
- Added spinlock documentation detailing the implementation of spinlocks, concurrency synchronization, memory ordering guarantees, and error handling mechanisms.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/specs/arch_abstraction.md | 223 | ||||
| -rw-r--r-- | docs/specs/global_init.md | 208 | ||||
| -rw-r--r-- | docs/specs/linker_and_entry.md | 208 | ||||
| -rw-r--r-- | docs/specs/spinlock.md | 195 |
4 files changed, 834 insertions, 0 deletions
diff --git a/docs/specs/arch_abstraction.md b/docs/specs/arch_abstraction.md new file mode 100644 index 0000000..a90137c --- /dev/null +++ b/docs/specs/arch_abstraction.md @@ -0,0 +1,223 @@ +# 架构抽象层 (Architecture Abstraction) + +## 1. 目标 (Objective) + +为 LumeOS 的平台无关层(MI)提供统一的底层硬件原语接口。本模块负责将 RISC-V 架构特有的控制状态寄存器 (CSR) 操作、汇编级上下文切换 (Context Switch)、陷阱帧 (TrapFrame) 布局、以及 TLB 刷新机制进行封装。**本项目所有内联汇编 (Inline Assembly) 严格限定于本模块内部,上层模块禁止直接操作硬件寄存器。** + +## 2. 前置依赖 (Dependencies) + +- **全局类型契约:** 依赖 `include/lume/types.h` 提供的 `uint64` 等标准类型。 + +- **编译器支持:** 依赖 GCC/Clang 的内联汇编能力 (`__asm__ volatile`),必须确保不被过度优化(使用 `memory` clobber 建立编译器屏障)。 + +- **启动序列契约:** `arch::cpu::id()` 依赖 `tp` 寄存器在 `entry.S` 中被正确初始化为当前 CPU 编号(参见 `linker_and_entry.md`)。在 `tp` 初始化之前,禁止调用本模块任何依赖 CPU ID 的函数。 + + +## 3. 核心数据结构 (Data Structures) + +### 3.1 上下文切换结构 (`struct Context`) + +用于调度器在不同内核流(如不同 Task 或 KThread)之间进行主动切换(Switch)。它只保存 Callee-Saved(被调用者保存)寄存器。 + +```cpp +// 定义于 arch/riscv/include/arch/context.h +namespace arch { + struct Context { + uint64 ra; // Return Address (返回地址) + uint64 sp; // Stack Pointer (栈指针) + uint64 s[12]; // s0 ~ s11 (Callee-saved 寄存器) + }; +} +``` + +### 3.2 陷阱帧结构 (`struct TrapFrame`) + +用于在发生中断、异常或系统调用时,由硬件与汇编入口 `trap_vector` 协同保存当前 CPU 的执行快照。此结构同时跨越用户态和内核态,必须精确布局。 + +```cpp +// 定义于 arch/riscv/include/arch/trapframe.h +// 对齐由分配方保证(页对齐或栈对齐),struct 本身不施加 alignas +namespace arch { + struct TrapFrame { + /* 内核元数据区 (偏移 0 - 39) */ + uint64 kernel_satp; // 0: 内核页表地址 (挂载至 satp) + uint64 kernel_sp; // 8: 顶层内核栈地址 + uint64 kernel_trap; // 16: 内核 C++ trap_handler 的绝对地址 + uint64 kernel_cpu_id; // 24: 当前 CPU 编号 (trap 入口加载至 tp) + uint64 epc; // 32: 中断/异常时的指令地址 (sepc) + + /* CSR 备份区 (偏移 40 - 47) */ + uint64 sstatus; // 40: 中断前的特权状态 (含 SPP, SPIE 位) + + /* 通用寄存器备份区 (偏移 48 - 295) */ + uint64 ra; // 48 + uint64 sp; // 56 (用户态的栈指针) + uint64 gp; // 64 + uint64 tp; // 72 + uint64 t[7]; // 80: t0-t6 (Caller-saved 临时寄存器) + uint64 s[12]; // 136: s0-s11 (Callee-saved 寄存器) + uint64 a[8]; // 232: a0-a7 (函数参数/系统调用参数与返回值) + }; + static_assert(sizeof(TrapFrame) == 296); +} +``` + +**相比原版的关键修正:** +- **新增 `sstatus` 字段:** 这是陷阱帧中最容易遗漏但最致命的字段。`sstatus` 包含 `SPP`(之前的特权级)和 `SPIE`(之前的中断使能状态)。若不保存/恢复 `sstatus`,`sret` 返回用户态时将无法正确恢复特权级和中断状态。 +- **将 `kernel_cpu_id` 移入元数据区:** 它不是通用寄存器的备份,而是内核注入的元数据(trap 入口用来设置 `tp`),不应放在"通用寄存器备份区"中。 +- **简化寄存器数组:** 将 `t0_t2[3]` + `t3_t6[4]` 合并为 `t[7]`,将 `s0_s1[2]` + `s2_s11[10]` 合并为 `s[12]`。编译器按偏移访问不受影响,且减少了心智负担。 + + +## 4. 公共 API (Public APIs) + +所有接口统一在 `arch` 及其子命名空间下,通过静态内联函数或汇编实现,实现**零运行时代价**。这些操作不会引发常规意义的"错误"(如不会返回 `-ENOMEM`),因此不采用错误码范式。 + +### 4.1 CPU 控制与状态感知 (`namespace arch::cpu`) + +```cpp +namespace arch::cpu { + // 获取当前 CPU 编号 (从 tp 寄存器中读取) + // 前置条件:tp 必须已在 entry.S 中初始化 + uint64 id(); + + // 开启中断 (设置 sstatus.SIE = 1) + void intr_on(); + + // 关闭中断 (设置 sstatus.SIE = 0) + void intr_off(); + + // 获取当前中断状态 (返回 true 表示中断已开启) + bool intr_enabled(); + + // 使 CPU 进入低功耗睡眠,直到下一个中断到来 (wfi 指令) + void halt_until_interrupt(); + + // 读取硬件单调时钟计数 (rdtime 指令) + // 返回自系统启动以来的 tick 数(频率由 FDT timebase-frequency 决定) + uint64 read_time(); +} +``` + +### 4.2 内存管理硬件原语 (`namespace arch::mmu`) + +```cpp +namespace arch::mmu { + // 读取缺页异常发生时的虚拟地址 (从 stval 寄存器读取) + uint64 get_fault_address(); + + // 切换顶级页表 + // 参数:physical_pgdir_addr 为页表的物理地址(非 PPN), + // 函数内部负责右移 12 位并组装 SV39 模式字段写入 satp + void set_page_table(uint64 physical_pgdir_addr); + + // 读取当前页表物理地址 (从 satp 提取 PPN 并左移还原为物理地址) + uint64 get_page_table(); + + // 刷新整个 TLB (sfence.vma zero, zero) + void flush_tlb_all(); + + // 刷新特定虚拟地址的 TLB 条目 (sfence.vma vaddr, zero) + void flush_tlb_page(uint64 vaddr); +} +``` + +### 4.3 陷阱向量控制 (`namespace arch::trap`) + +```cpp +namespace arch::trap { + // 设置 S-mode 陷阱向量基址 (写入 stvec 寄存器) + // 参数:handler_addr 为汇编 trap_vector 入口的虚拟地址 + // 模式:Direct(所有异常/中断统一入口) + void set_vector(uint64 handler_addr); + + // 读取当前陷阱原因 (scause 寄存器) + uint64 get_cause(); + + // 读取陷阱附加信息 (stval 寄存器) + uint64 get_value(); +} +``` + +### 4.4 上下文切换原语 + +```cpp +namespace arch { + // 汇编实现的上下文切换函数。定义于 arch/riscv/swtch.S + // 语义:将当前 CPU 寄存器保存到 old_ctx,并将 next_ctx 的内容恢复到 CPU + // 本函数由 C++ 调用,汇编实现,绝对不会失败。 + extern "C" void context_switch(Context* old_ctx, Context* next_ctx); +} +``` + +## 5. 并发与锁契约 (Concurrency & Locks) + +本模块属于内核的绝对最底层,位于 `Spinlock` 模块的下方,因此**不感知、不持有、也不获取任何高级锁。** + +- **调用方契约(中断开关):** 上层模块(如 `Spinlock`)需直接依赖 `arch::cpu::intr_off()` 与 `intr_on()` 来防止单核死锁。 + +- **内存屏障契约(Memory Barriers):** `flush_tlb_all` 与 `flush_tlb_page` 的底层 `sfence.vma` 指令本身即为硬件级内存屏障。内联汇编实现中仍须添加编译器屏障(`"memory"` clobber),防止编译器跨越 TLB 刷新点重排内存访问。 + +- **跨核(SMP)同步警告:** 当前 `flush_tlb_page` 仅执行**本地** `sfence.vma`。在实现基于 IPI (核间中断) 的 TLB Shootdown 前,VMM 须确保同一时间只有一个 CPU 在修改共享的页表映射。未来将在本命名空间中增加 `flush_tlb_page_remote()` 接口。 + + +## 6. 错误路径 (Error Paths) + +底层指令级操作无传统错误返回。 + +- `flush_tlb_page` 对任何地址执行 `sfence.vma` 都不会触发异常(仅使对应 TLB 表项失效)。 + +- `context_switch` 若传入无效指针(如 nullptr),加载/存储操作将触发 Store/Load Page Fault,由 Phase 2 的 `trap.md` 规定的异常分发层兜底并触发 `Panic`。 + +- `set_page_table` 传入非法物理地址将导致后续的所有内存访问引发 Page Fault。此类错误属于编程逻辑错误,无法在本层恢复。 + + +## 7. 状态机 / 时序图 (State Machine / Sequence) + +### 7.1 上下文切换时序 (`context_switch`) + +调度器在选择出新的 `Task` 后,调用此接口的时序如下: + +``` +[当前 Task A] (执行 sched() 决定放弃 CPU) + │ + ▼ +[调用 arch::context_switch(&TaskA->context, &TaskB->context)] + │ + ├─> 1. 保存 ra, sp, s0-s11 到 TaskA->context 内存区块中 + ├─> 2. 从 TaskB->context 内存区块中加载 ra, sp, s0-s11 到物理寄存器 + └─> 3. ret (返回到被加载的 ra 地址) + │ + ▼ +[恢复至 Task B] (PC 从 Task B 上次挂起的 sched() 处继续执行) +``` + +### 7.2 陷阱帧保存/恢复时序 (trap_vector) + +``` +[用户态正在运行] + │ + ├─ 中断/异常/ecall 触发 + ▼ +[trap_vector (汇编入口, arch/riscv/trap/trap_vector.S)] + │ + ├─> 1. 交换 sscratch <-> tp (sscratch 预存 TrapFrame 地址) + ├─> 2. 将 31 个通用寄存器写入 TrapFrame + ├─> 3. csrr sstatus → TrapFrame.sstatus (保存特权状态) + ├─> 4. csrr sepc → TrapFrame.epc (保存中断 PC) + ├─> 5. 加载 kernel_sp, kernel_satp, kernel_cpu_id 到 sp, satp, tp + ├─> 6. sfence.vma (切换到内核页表后刷新 TLB) + └─> 7. jalr TrapFrame.kernel_trap (跳转至 C++ trap_handler) + │ + ▼ +[MI 层 trap_handler()] → 分发至 syscall / page fault / IRQ 处理 + │ + ▼ +[usertrapret: 恢复用户态] + │ + ├─> 1. 写回 TrapFrame.epc → sepc + ├─> 2. 写回 TrapFrame.sstatus → sstatus + ├─> 3. 切换回用户页表 (satp) + ├─> 4. 从 TrapFrame 恢复 31 个通用寄存器 + ├─> 5. 交换 sscratch <-> tp (恢复 TrapFrame 地址到 sscratch) + └─> 6. sret (返回用户态) +```
\ No newline at end of file diff --git a/docs/specs/global_init.md b/docs/specs/global_init.md new file mode 100644 index 0000000..cd14dae --- /dev/null +++ b/docs/specs/global_init.md @@ -0,0 +1,208 @@ +# 全局初始化序列 (Global Initialization) + +## 1. 目标 (Objective) + +统筹 LumeOS 从汇编跃迁至 C++ 后的全局初始化流程。定义子系统间的严格启动顺序、全局对象的生命周期管理范式、以及 BSP/AP 的两阶段同步协议。 + +## 2. 前置依赖 (Dependencies) + +- **汇编环境契约:** 依赖 `linker_and_entry.md` 保证的执行环境:BSS 段已被清零、高半区虚拟内存(`early_pgdir`)已映射、`tp` 寄存器已保存当前 CPU ID、`gp` 寄存器已就绪。注意:`early_cpp_init()` 已在 `entry.S` 中由 BSP 调用完毕(在 `kernel_main` 之前),不在本模块的管辖范围内。 + +- **全局基础契约:** 依赖 `00_conventions.md` 中禁用了 C++ 异常与 RTTI 的环境。 + +- **并发同步契约:** 依赖 `spinlock.md` 提供的 `Spinlock`(但在 BSP 独占期不使用)。 + + +## 3. 核心数据结构与生命周期范式 (Data Structures & Lifecycle) + +在内核早期,物理内存管理 (PMM) 和字节级分配器 (Slab) 尚未就绪,传统的 `new` 和隐式的全局构造函数极易引发缺页崩溃或顺序未定义行为。 + +### 3.1 显式生命周期模型 (Placement New 范式) + +对于所有复杂的内核全局单例(如 `PhysicalMemoryManager`, `Scheduler`),**严禁**直接定义为全局变量依赖 `.init_array` 隐式构造。必须采用以下静态缓冲区分配 + 显式 Placement New 的范式: + +```cpp +// 示例:kernel/mm/pmm_global.cc + +// Placement new 声明(freestanding 环境无 <new> 头文件,自行提供) +// 定义于 include/lume/new.h +inline void* operator new(size_t, void* ptr) noexcept { return ptr; } +inline void operator delete(void*, void*) noexcept {} + +// 1. 在 .bss 段静态分配对齐的内存区块,不触发任何构造函数 +alignas(PhysicalMemoryManager) uint8 g_pmm_buf[sizeof(PhysicalMemoryManager)]; + +// 2. 暴露全局指针供外部访问 +PhysicalMemoryManager* g_pmm = nullptr; + +// 3. 在 kernel_main 的严格时序中被显式调用 +void pmm_init(uint64 fdt_paddr) { + // 使用 Placement New 在预分配的内存上执行构造函数 + g_pmm = new (g_pmm_buf) PhysicalMemoryManager(fdt_paddr); +} +``` + +**关于 `<new>` 头文件:** 在 `-ffreestanding -nostdlib` 编译环境下,`<new>` 可能不可用或行为不一致。LumeOS 在 `include/lume/new.h` 中自行提供 placement new 声明,不依赖任何标准库头文件。 + +### 3.2 简单数据结构的按需初始化 + +对于没有复杂依赖的 Plain Old Data (POD) 或简单锁对象: + +- `Spinlock` 等对象直接作为 BSS 段全局变量,由 `entry.S` 清零。随后在所属子系统的 `init()` 方法中调用 `lock.init("name")` 赋予身份。 + +### 3.3 `.init_array` 的使用边界 + +`early_cpp_init()` 在 `entry.S` 中遍历 `.init_array`。只有满足以下**全部**条件的全局构造函数才允许出现在此表中: + +- 构造函数不依赖任何子系统(PMM、Slab、VMM、Scheduler 均未初始化)。 +- 构造函数不分配动态内存。 +- 构造函数不获取任何锁。 + +实际上,应该尽量让 `.init_array` 为空。绝大多数全局对象应走 §3.1 的 Placement New 路径。 + + +## 4. 公共 API (Public APIs) + +### 4.1 内核 C++ 唯一入口 + +```cpp +// 定义于 kernel/main.cc +// 被 entry.S 中的 real_start 在开启 MMU、调用 early_cpp_init 后跳转 +// 所有 CPU 从此入口进入 C++ 世界 +// a0 = cpu_id, a1 = fdt_paddr +extern "C" void kernel_main(uint64 cpu_id, uint64 fdt_paddr) __attribute__((noreturn)); +``` + +### 4.2 子系统初始化签名规范 + +所有子系统的初始化入口必须遵循统一签名,并在 `kernel/main.cc` 内部按图谱依次调用。每个 `xxx_init()` 函数成功时无返回值,失败时直接 `kernel_panic()`: + +```cpp +void console_init(); // 1. UART 就绪,此后 kprintf/kernel_panic 可输出 +void fdt_init(uint64 fdt_paddr); // 2. 解析 FDT,构建设备节点表 +void pmm_init(); // 3. 从 FDT 获取内存布局,建立 Buddy 分配器 +void slab_init(); // 4. 建立 kmem_cache,此后 operator new 可用 +void vmm_init(); // 5. 建立精细化内核页表,废弃 early_pgdir +void trap_init(); // 6. 设置 stvec,注册 IRQ 分发器 +void plic_init(); // 7. 配置 PLIC 中断控制器(阈值、优先级、使能位) +void timer_init(); // 8. 配置定时器中断(调度器时钟源) +void sched_init(); // 9. 构造 BSP 的 Idle Task 和初始就绪队列 +``` + +### 4.3 AP 初始化签名 + +AP 在 BSP 完成全局初始化后执行,仅限 Per-CPU 私有数据: + +```cpp +void vmm_init_ap(); // 切换 satp 至 BSP 建好的内核页表 +void trap_init_ap(); // 设置本 CPU 的 stvec +void plic_init_ap(); // 配置本 CPU 在 PLIC 上的 Context 使能 +void timer_init_ap(); // 启动本 CPU 的定时器中断 +void sched_init_ap(); // 初始化本 CPU 的 O(1) 就绪队列、创建 Idle Task +``` + +## 5. 并发与锁契约 (Concurrency & Locks) + +这是整个内核生命周期中**唯一一段不受自旋锁约束的特殊时期**,必须严格遵循以下阶段隔离: + +### 5.1 两阶段同步协议 + +LumeOS 启动过程中存在**两个独立的同步屏障**,不要混淆: + +| 同步点 | 位置 | 变量 | 含义 | +|--------|------|------|------| +| 第一道 | `entry.S` (汇编层) | `boot_sync_flag` | AP 等待 BSP 建立 `early_pgdir`,完成后所有 CPU 齐步进入虚拟地址空间 | +| 第二道 | `kernel_main` (C++ 层) | `ap_boot_sync` | AP 等待 BSP 完成全部子系统初始化,完成后 AP 开始 Per-CPU 初始化 | + +### 5.2 绝对单核期 (BSP 独占) + +- 在 `kernel_main` 启动初期,AP 卡在第二道屏障 `ap_boot_sync` 处自旋。 + +- BSP 拥有对全局数据结构的**绝对独占权**。在此期间调用 `pmm_init`、`vmm_init` 等函数时,**无需获取该子系统的 `Spinlock`**(也不应获取,因为锁可能尚未初始化)。 + +### 5.3 多核唤醒与并发期 (AP 启动) + +- BSP 完成所有全局初始化后,通过 `__atomic_store_n(&ap_boot_sync, 1, __ATOMIC_RELEASE)` 唤醒 AP。`RELEASE` 语义保证 AP 读到 `ap_boot_sync == 1` 时,所有全局单例对象必然已构造完毕。 + +- AP 进入后,**只能执行 Per-CPU 的局部初始化**(如设置自身的 `stvec`、初始化 PLIC Context、创建 Per-CPU Idle Task)。 + +- 一旦 AP 的 Per-CPU 初始化完成并开启中断,所有子系统对全局数据的访问必须严格遵守 `spinlock.md` 契约。 + + +## 6. 错误路径 (Error Paths) + +初始化阶段的错误处理极度苛刻: + +- **console_init 之前的 Panic:** 此时 UART 尚未就绪,`kernel_panic()` 无法输出任何信息。唯一行为是关中断 + `wfi` 死循环(静默死亡)。可通过 JTAG/GDB 附加调试。 + +- **console_init 之后的 Panic:** 可输出 Panic 消息和调用栈信息后死循环。 + +- 任何子系统的 `init` 方法如果检测到硬件缺失(如 FDT 解析不到内存节点)或关键资源耗尽(Slab 无法建立早期缓存),必须**立即调用 `kernel_panic()`**。 + +- 此阶段系统尚未具备任务调度与资源回收能力,无后路可退。 + + +## 7. 状态机 / 时序图 (State Machine / Sequence) + +### 7.1 全局初始化依赖 DAG 图 + +`kernel_main` 必须严格按照自顶向下的单向依赖顺序执行,任何逆序调用将导致 `kernel_panic` 或缺页崩溃。 + +``` + entry.S (汇编世界) + │ + early_cpp_init() ← 遍历 .init_array (应尽量为空) + │ + ════════════════ + kernel_main() + ════════════════ + │ + ┌─────────────┴──────────────┐ + │ │ + [BSP (cpu_id == 0)] [AP (cpu_id != 0)] + │ │ + 1. console_init() 自旋等待 + └─ UART 就绪 ap_boot_sync == 1 + └─ 此后 Panic 可输出 │ + │ │ + 2. fdt_init(fdt_paddr) │ + └─ 解析 FDT 设备树 │ + │ │ + 3. pmm_init() │ + └─ Buddy 分配器就绪 │ + │ │ + 4. slab_init() │ + └─ operator new 可用 │ + │ │ + 5. vmm_init() │ + └─ 完整内核页表就绪 │ + └─ 废弃 early_pgdir 恒等映射 │ + │ │ + 6. trap_init() │ + └─ stvec 挂载 trap_vector │ + │ │ + 7. plic_init() │ + └─ PLIC 中断控制器就绪 │ + │ │ + 8. timer_init() │ + └─ 定时器中断就绪 │ + │ │ + 9. sched_init() │ + └─ Idle Task (Task 0) 创建 │ + └─ Per-CPU 就绪队列初始化 │ + │ │ + ──[RELEASE 屏障]──────────────────> 读到 1 + ap_boot_sync = 1 │ + │ 1. vmm_init_ap() + │ 2. trap_init_ap() + │ 3. plic_init_ap() + │ 4. timer_init_ap() + │ 5. sched_init_ap() + │ │ + ▼ ▼ + arch::cpu::intr_on() arch::cpu::intr_on() + BSP 变为 Idle Task AP 变为 Idle Task + 进入调度循环 进入调度循环 +``` + +**BSP 进入调度的方式:** BSP 在 `sched_init()` 中将自身的执行上下文注册为 CPU 0 的 Idle Task (Task 0)。开启中断后,第一个定时器中断触发调度器接管执行流。不是通过 `sched_yield()` 主动让出,而是被定时器中断被动抢占进入调度循环。
\ No newline at end of file diff --git a/docs/specs/linker_and_entry.md b/docs/specs/linker_and_entry.md new file mode 100644 index 0000000..a59d28d --- /dev/null +++ b/docs/specs/linker_and_entry.md @@ -0,0 +1,208 @@ +# 链接器与引导入口 (Linker & Entry) + +## 1. 目标 (Objective) + +定义 LumeOS 在 RISC-V 64 架构上的早期内存布局与汇编启动序列。负责兼容多种 Bootloader(OpenSBI、GRUB、U-Boot),建立高半区内核(Higher-Half Kernel)映射,初始化早期 C++ 运行环境(BSS、GP 寄存器、全局构造函数),并安全地将各个 CPU 同步导入虚拟地址空间的 `kernel_main`。 + +## 2. 前置依赖 (Dependencies) + +- **引导协议契约 (Boot Protocol):** 为了同时支持 OpenSBI(直接跳转偏移 0)和 GRUB/U-Boot(解析元数据后引导),LumeOS 采用 **RISC-V Linux Boot Protocol**。内核文件起始处必须包含标准的 64 字节 Image Header。 + +- **寄存器传参契约:** 无论哪种引导器,在跳转至内核执行的第一条指令时,必须保证 `a0` = 当前 CPU ID (Hart ID),`a1` = FDT (Device Tree) 的物理基址。 + +- **编译模型契约:** 内核必须使用 `-mcmodel=medany` 编译,以支持基于 PC 的相对寻址,确保在 MMU 开启前后代码均能正确执行。 + +- **硬件 MMU 契约:** 目标硬件必须支持 RISC-V SV39 分页模式。 + + +## 3. 核心数据结构与内存布局 (Data Structures & Memory Map) + +### 3.1 虚拟与物理地址基址 + +- **物理加载基址 (Physical Base):** `0x80200000` (跳过低地址的 OpenSBI 固件区,对齐 2MB 以支持大页映射)。 + +- **内核虚拟基址 (Virtual Base):** `0xFFFFFFC080200000` (SV39 地址空间顶部,硬编码偏移量 `PAGE_OFFSET = 0xFFFFFFC000000000`)。 + +- **虚拟-物理偏移常量:** `KERNEL_VA_OFFSET = 0xFFFFFFC000000000`,用于虚拟/物理地址互转:`VA = PA + KERNEL_VA_OFFSET`,`PA = VA - KERNEL_VA_OFFSET`。 + + +### 3.2 链接脚本段分布 (`linker.ld`) + +必须在所有代码之前放置 `.head.text` 段,以确保 Image Header 绝对位于二进制文件的第 0 字节处。所有段强制 4KB 对齐: + +``` ++-----------------------+ 0xFFFFFFC080200000 (Virtual) / 0x80200000 (Physical) +| .head.text (镜像头) | -> 严格位于内核入口 0 字节处,包含 Linux Boot Header ++-----------------------+ +| .text (执行代码) | -> ALIGN(4096), 权限: RX ++-----------------------+ +| .rodata (只读数据) | -> ALIGN(4096), 权限: R- +| (含 __init_array) | -> 全局构造函数指针表 ++-----------------------+ +| .data (已初始化数据) | -> ALIGN(4096), 权限: RW +| (含 __global_pointer$)| -> 位于 .sdata 附近,用于优化全局变量访问 +| (含 early_pgdir) | -> 4KB 静态早期页表 ++-----------------------+ +| .bss (未初始化数据) | -> ALIGN(4096), 权限: RW ++-----------------------+ +| boot_stack (早期栈) | -> ALIGN(4096), 为每个 CPU 预留 8KB 栈空间 ++-----------------------+ +``` + +**链接脚本必须导出的符号:** + +| 符号名 | 含义 | +|--------|------| +| `_start` | 内核入口点(Image Header 首字节) | +| `_stext` / `_etext` | .text 段边界 | +| `_srodata` / `_erodata` | .rodata 段边界 | +| `_sdata` / `_edata` | .data 段边界 | +| `_sbss` / `_ebss` | .bss 段边界(用于启动清零) | +| `__global_pointer$` | GP 寄存器锚点 | +| `__init_array_start` / `__init_array_end` | C++ 全局构造函数表边界 | +| `_kernel_end` | 内核镜像结束地址(PMM 初始化起点) | + +### 3.3 早期页表 (Early Page Table) + +- **`early_pgdir`:** 一块静态分配的 4KB `.data` 内存,用作初始顶级页表 (Root Page Table)。 + +- 内含两个 1GB 的超级大页(Giga Page)映射: + + 1. **恒等映射 (Identity Map):** `0x80000000 -> 0x80000000`,保证开启 MMU 瞬间下一条指令能正确取指。 + + 2. **高半区映射 (Higher-Half Map):** `0xFFFFFFC080000000 -> 0x80000000`,供后续 C++ 代码正常运行。 + +- **生命周期:** 此恒等映射为临时性过渡,在 VMM 建立完整页表后必须被撤销。高半区映射将被 VMM 接管并细化为 4KB 粒度的精确权限映射。 + + +## 4. 公共 API 与核心符号 (Public APIs & Symbols) + +### 4.1 RISC-V OS Image Header 规范 + +在 `entry.S` 的最开始(`.head.text` 段),必须按照以下结构手写 64 字节的元数据。 +定义参照 Linux `arch/riscv/include/asm/image.h` 中的 `struct riscv_image_header`: + +```asm + .section .head.text, "ax" + .globl _start +_start: + /* code0 (u32, offset 0): 可执行跳转指令,OpenSBI 裸启入口 */ + j real_start + /* code1 (u32, offset 4): 保留 */ + .word 0 + /* text_offset (u64, offset 8): Image 加载偏移 */ + .dword 0x200000 + /* image_size (u64, offset 16): 镜像大小,链接后由脚本填充,初期可写 0 */ + .dword 0 + /* flags (u64, offset 24): 内核标志位 (bit 0: 0=LE, 1=BE) */ + .dword 0 + /* version (u32, offset 32): Header 版本 (major << 16 | minor),当前 0.2 */ + .word 0x0002 + /* res1 (u32, offset 36): 保留 */ + .word 0 + /* res2 (u64, offset 40): 保留 */ + .dword 0 + /* magic (u64, offset 48): RISCV_IMAGE_MAGIC = "RISCV\0\0\0" (LE) */ + .dword 0x5643534952 + /* magic2 (u32, offset 56): RISCV_IMAGE_MAGIC2 = "RSC\x05" (LE) */ + .word 0x05435352 + /* res3 (u32, offset 60): 预留给 PE/COFF 偏移 (UEFI),目前填 0 */ + .word 0 +``` + +### 4.2 汇编函数签名 + +```cpp +// 定义于 arch/riscv/boot/entry.S +// _start 中的 `j real_start` 跳转目标,执行早期硬件初始化。 +void real_start() __attribute__((noreturn)); // 汇编实现,无 C++ 调用约定 + +// 定义于 arch/riscv/boot/entry.S 或 kernel/main.cc +// 仅由 CPU 0 调用,遍历 .init_array 执行全局构造函数。 +void early_cpp_init(); + +// 定义于 kernel/main.cc +// C++ 内核总入口。各 CPU 在开启 MMU 后汇聚于此。 +// 参数:cpu_id = 当前 CPU 编号,fdt_paddr = FDT 物理基址。 +void kernel_main(uint64 cpu_id, uint64 fdt_paddr) __attribute__((noreturn)); +``` + +## 5. 并发与锁契约 (Concurrency & Locks) + +此时 `Spinlock` 尚未初始化,必须使用汇编级的极简同步机制来避免多核访问冲突。 + +- **本模块持有的锁:** 无高级锁。 + +- **本模块可能获取的外部锁:** 无。 + +- **原子同步契约 (SMP Sync):** + + 1. 在 `.data` 段定义一个全局标志 `uint32 boot_sync_flag = 0;`。 + + 2. 只有 **CPU 0** (或 `a0` 读取到的第一个 CPU) 作为主核心 (BSP),获准执行 BSS 清零、早期页表填充等破坏性操作。 + + 3. **副核心 (AP, Application Processors)** 必须使用原子加载指令在汇编层盲等 `boot_sync_flag` 变为 1。具体实现为 `lw` + 循环比较(不需要 `amoswap`,因为只有 BSP 写入,AP 只读)。 + + 4. BSP 完成 `early_pgdir` 写入并唤醒 AP 前,必须执行 `fence rw, rw` 以保证所有先前的内存写入对其他 CPU 可见,随后使用 `fence.i` 刷新指令缓存(如有需要),最后将 `boot_sync_flag` 设为 1。 + + +## 6. 错误路径 (Error Paths) + +在早期的裸机启动阶段,内核没有任何错误恢复能力。 + +- **检测点:** 所有 CPU 在 `real_start` 入口处可选地检查 `misa` 或 `marchid` 判断基本硬件兼容性(如 64 位检查)。 + +- **失败行为:** 立即关闭本 CPU 的中断使能(`csrc sstatus, SSTATUS_SIE`),进入 `wfi` 死循环。无串口输出(此时 UART 尚未初始化)。 + +- **无回滚:** 早期启动无任何资源可回收,失败即死。 + + +## 7. 状态机 / 时序图 (State Machine / Sequence) + +以下描述了从底层固件直到进入 `kernel_main` 的全流程: + +``` +[引导分流] + ├──> OpenSBI 裸启: 直接跳转到内存基址 (0x80200000) + │ └─ 命中 _start 处的第一条指令 `j real_start` + │ + └──> GRUB / U-Boot 引导: 解析位于 0x80200000 处的 64 字节 Header + ├─ 验证 magic ("RISCV\0\0\0") 与 magic2 ("RSC\x05") + ├─ 验证 text_offset + └─ 跳转到 0x80200000 (此时 a0=cpu_id, a1=fdt_paddr) + └─ 同样命中 _start 处的第一条指令 `j real_start` + + │ (汇聚点) + ▼ +[real_start] + │ + ├─> 1. 设置 gp 寄存器 (la gp, __global_pointer$) + │ // 极其关键:medany 模型下访问全局变量依赖此寄存器 + │ + ├─> 2. 设置早期内核栈 (la sp, boot_stack + (cpu_id + 1) * 8192) + │ + ├─> 3. 判断是否为主核心 (CPU 0)? + │ ├── [是 (BSP)] + │ │ ├─ 3.1 循环清理 BSS 段 (_sbss 至 _ebss 填 0) + │ │ ├─ 3.2 填充 early_pgdir (建立恒等映射与高半区映射) + │ │ ├─ 3.3 fence rw, rw (确保内存修改对其他 CPU 可见) + │ │ └─ 3.4 释放副核心 (boot_sync_flag = 1) + │ │ + │ └── [否 (AP)] + │ └─ 3.1 汇编级自旋等待,直到 boot_sync_flag == 1 + │ + ├─> 4. 高半区蹦床 (Higher-Half Trampoline - 所有 CPU 执行) + │ ├─ 4.1 将 early_pgdir 物理地址写入 satp 寄存器 (MODE=SV39) + │ ├─ 4.2 执行 sfence.vma 刷新 TLB + │ ├─ 4.3 计算 kernel_main 的高半区虚拟地址 + │ └─ 4.4 jr 绝对跳转至虚拟地址,彻底抛弃低位物理 PC + │ + ▼ +[进入虚拟世界: call early_cpp_init] (仅 BSP 执行) + │ + └─> 遍历 __init_array_start 到 __init_array_end + 依次调用全局构造函数,完成 C++ early runtime setup + │ + ▼ +[跳转至 C++ 逻辑: call kernel_main(a0, a1)] (所有 CPU 汇合) +```
\ No newline at end of file diff --git a/docs/specs/spinlock.md b/docs/specs/spinlock.md new file mode 100644 index 0000000..586ee43 --- /dev/null +++ b/docs/specs/spinlock.md @@ -0,0 +1,195 @@ +# 自旋锁与并发同步 (Spinlock & Synchronization) + +## 1. 目标 (Objective) + +为 LumeOS 提供多核环境下的底层互斥机制。本模块实现基于原子操作的自旋锁(Spinlock)和基于 RAII 范式的局部锁守卫(LockGuard)。同时,严格定义锁操作前后的硬件中断屏蔽策略(避免单核中断死锁)以及底层内存序保证(Memory Ordering)。 + +## 2. 前置依赖 (Dependencies) + +- **全局类型契约:** `include/lume/types.h`。 + +- **架构原语契约:** 依赖 `arch_abstraction.md` 中的 `arch::cpu::id()`, `arch::cpu::intr_on()`, `arch::cpu::intr_off()`, `arch::cpu::intr_enabled()`。 + +- **Panic 机制:** 依赖底层的内核崩溃函数 `kernel_panic(const char* msg)`。 + +- **全局初始化契约:** `g_cpu_sync_states[]` 为 BSS 段全局数组,由 `entry.S` 的 BSS 清零保证初始值为零。`Spinlock` 作为全局对象时,必须通过 `init()` 方法显式初始化(参见 `global_init.md`),不得依赖构造函数。 + + +## 3. 核心数据结构 (Data Structures) + +### 3.1 CPU 中断嵌套跟踪器 + +为了支持锁的嵌套(如持有 `A` 锁时去获取 `B` 锁),必须在每 CPU 维度跟踪锁的深度,确保只有释放**最后一把锁**时,才恢复真实的中断状态。 + +```cpp +// 定义于 kernel/sync/cpu_state.h +namespace sync { + struct CpuSyncState { + int lock_depth; // 当前 CPU 持有的自旋锁数量 (嵌套深度) + bool intr_was_on; // 在获取第一把锁之前,中断是否处于开启状态 + }; + // BSS 段全局数组,由 entry.S 清零初始化 + inline constexpr uint64 kInvalidCpuId = ~0ULL; + extern CpuSyncState g_cpu_sync_states[kMaxCPUs]; +} +``` + +### 3.2 自旋锁类 (`Spinlock`) + +```cpp +// 定义于 kernel/sync/spinlock.h +class Spinlock { +public: + // 默认构造:locked_ = 0, name_ = "uninit", cpu_id_ = kInvalidCpuId + // 全局 Spinlock 在 BSS 清零后通过 init() 显式初始化 + Spinlock() = default; + + // 显式初始化(用于全局对象或 Placement New 场景) + void init(const char* name); + + // 禁用拷贝与移动 (锁对象具有固定的内存地址身份) + Spinlock(const Spinlock&) = delete; + Spinlock& operator=(const Spinlock&) = delete; + + void acquire(); + void release(); + bool is_held_by_current_cpu() const; + +private: + uint32 locked_ = 0; // 锁状态:0=空闲, 1=占用 + const char* name_ = "uninit"; // 锁名称 (用于 Panic 输出) + uint64 cpu_id_ = sync::kInvalidCpuId; // 持有者 CPU 编号 +}; +``` + +### 3.3 RAII 锁守卫 (`LockGuard`) + +```cpp +// 定义于 kernel/sync/spinlock.h +class LockGuard { +public: + explicit LockGuard(Spinlock& lock) : lock_(lock) { + lock_.acquire(); + } + + ~LockGuard() { + lock_.release(); + } + + // 禁用拷贝与移动 + LockGuard(const LockGuard&) = delete; + LockGuard& operator=(const LockGuard&) = delete; + +private: + Spinlock& lock_; +}; +``` + +## 4. 公共 API 与中断管理协议 (Public APIs) + +### 4.1 中断压栈/出栈协议 (Internal API) + +必须在 `Spinlock::acquire()` 和 `release()` 内部隐式调用,严禁外部业务层手动调用。 + +```cpp +namespace sync { + // 关闭中断,并增加当前 CPU 的 lock_depth。 + // 如果是第一把锁 (depth 从 0 变 1),记录当前 sstatus.SIE 状态到 intr_was_on。 + void push_intr(); + + // 减少当前 CPU 的 lock_depth。 + // 如果 depth 降为 0 且入栈前中断是开启的 (intr_was_on == true),则恢复中断。 + void pop_intr(); +} +``` + +### 4.2 锁语义约定 + +本模块属于内核生命线,发生任何违反并发契约的行为(如重复加锁、越权解锁)均属于致命代码逻辑错误。 **此类函数无返回值,一旦检测到契约被破坏,直接触发 `kernel_panic()` 宕机。** + +## 5. 并发与锁契约 (Concurrency & Locks) + +### 5.1 内存序保证 (Memory Order Guarantee) + +为了确保临界区内的读写操作不会被编译器或 CPU 的乱序执行(Out-of-Order Execution)重排到临界区外,必须使用带有内存屏障语义的原子操作。 + +- **Acquire 语义:** `Spinlock::acquire()` 内部的原子交换必须使用 `__ATOMIC_ACQUIRE`。在 RISC-V 层面,这将生成带有 `.aq` (Acquire) 后缀的原子指令(如 `amoswap.w.aq`),确保锁获取之后的任何内存读写,都不会被重排到拿锁之前。 + +- **Release 语义:** `Spinlock::release()` 内部的原子写零必须使用 `__ATOMIC_RELEASE`。使用 `__atomic_store_n(&locked_, 0, __ATOMIC_RELEASE)`,在 RISC-V 层面生成 `fence rw, w` + `sw`。注意:release 不需要原子 RMW 指令(`amoswap`),普通的 release-store 即可——因为此时本 CPU 已经是唯一的锁持有者,不存在竞争写。 + +### 5.2 自旋优化 (TTAS: Test-and-Test-and-Set) + +朴素的 `while(__atomic_exchange_n(...))` 会在每次迭代都发出总线级原子操作,导致缓存一致性协议风暴。必须使用 TTAS 模式: + +```cpp +void Spinlock::acquire() { + sync::push_intr(); + // 外层:非原子读,利用本地缓存副本空转,不产生总线流量 + while (true) { + while (__atomic_load_n(&locked_, __ATOMIC_RELAXED)) + ; // 在 L1 cache 上旋转,等待释放信号via cache invalidation + // 内层:看到可能空闲,才发起真正的原子交换 + if (__atomic_exchange_n(&locked_, 1, __ATOMIC_ACQUIRE) == 0) + break; // 成功获取 + } + cpu_id_ = arch::cpu::id(); +} +``` + +### 5.3 业务层使用契约 (Caller Contract) + +1. **禁止睡眠:** 在持有 `Spinlock`(或处于 `LockGuard` 作用域内)的任何期间,**绝对禁止**调用可能导致当前 Task 休眠或主动放弃 CPU 的函数(如 `sleep()`, `yield()`, 或由于分配内存触发了等待)。 + +2. **优先 RAII:** 常规业务逻辑中应通过 `LockGuard` 对象管理锁的生命周期,利用 C++ 析构确保锁在异常或提前 return 时必定释放。`acquire()/release()` 公开暴露仅供**调度器内部**等必须跨函数边界持有锁的特殊场景使用(如 `sleep()` 中释放锁 + 上下文切换 + 醒来后重新获取)。 + +3. **锁序遵守:** 嵌套获取多把 Spinlock 时,必须严格遵循全局锁序 DAG(将在 Phase 2 完成后汇总为 `lock_ordering.md`)。任何违反锁序的代码在 Code Review 中必须被驳回。 + + +## 6. 错误路径与 Panic 触发点 (Error Paths) + +在以下情况下,自旋锁必须立即执行 `kernel_panic()` 阻断系统: + +1. **死锁防御 (Double Lock):** 在 `acquire()` 中,调用 `push_intr()` 关中断后、原子操作前,检查 `is_held_by_current_cpu()`。若为 `true`,说明当前 CPU 试图获取自己持有的锁,必然死锁,立即 Panic。打印锁名称与 CPU ID。 + +2. **越权释放 (Invalid Release):** 在 `release()` 中,若 `is_held_by_current_cpu()` 为 `false`,说明代码逻辑严重错乱(锁未被持有、或被其他 CPU 持有),立即 Panic。 + +3. **中断状态校验:** 在 `acquire()` 入口(`push_intr()` 之前),若中断已被开启但 `lock_depth > 0`,说明存在中断状态泄漏(某处代码在持有锁的同时非法开启了中断),可选 Panic。此检查在 `#ifdef DEBUG` 下启用以避免正式构建的性能损失。 + + +## 7. 状态机 / 时序图 (State Machine / Sequence) + +### 7.1 Spinlock Acquire / Release 硬件级时序 + +``` +[当前 CPU (CPU X) 请求 LockGuard] + │ + ├─> 1. push_intr() + │ ├─ 读取当前中断状态: was_on = arch::cpu::intr_enabled() + │ ├─ 关中断: arch::cpu::intr_off() + │ └─ if (lock_depth == 0) intr_was_on = was_on; lock_depth++ + │ + ├─> 2. 断言: is_held_by_current_cpu() == false (否则 Panic: double lock) + │ + ├─> 3. TTAS 自旋获取 + │ ├─ 外层 while: __atomic_load_n(&locked_, RELAXED) 非原子读空转 + │ └─ 内层 CAS: __atomic_exchange_n(&locked_, 1, ACQUIRE) == 0 → 成功 + │ + ├─> 4. 记录属主: cpu_id_ = arch::cpu::id() + │ + ▼ +[执行临界区 C++ 业务逻辑 (关中断、无休眠)] + │ + ▼ +[LockGuard 析构,触发 release()] + │ + ├─> 1. 断言: is_held_by_current_cpu() == true (否则 Panic: invalid release) + │ + ├─> 2. 擦除属主: cpu_id_ = kInvalidCpuId + │ + ├─> 3. __atomic_store_n(&locked_, 0, __ATOMIC_RELEASE) + │ └─ RISC-V: fence rw, w + sw zero → 临界区写入全局可见后才释放锁 + │ + └─> 4. pop_intr() + ├─ lock_depth-- + └─ if (depth == 0 && intr_was_on) arch::cpu::intr_on() +```
\ No newline at end of file |
