diff --git a/document/README.md b/document/README.md index 55ffa4c5..c995ed33 100644 --- a/document/README.md +++ b/document/README.md @@ -1,46 +1,58 @@ -# 文档总览(document/) - -本目录存放 comix 项目的设计文档、模块说明与写作指南。此 README 面向“文档作者与维护者”,用于解释组织结构、写作约定、如何预览/发布。 - -## 快速浏览与预览 - -- 直接阅读:按下述“目录导航”中的链接浏览各子模块文档。 -- 使用 mdBook 预览(若需书籍化浏览): - 1. 安装 mdBook(一次性) - - 使用 Rust 工具链:`cargo install mdbook` - - 或从发行包获取,参考 mdBook 官方说明 - 2. 在仓库根目录执行(假定 book.toml 位于 document/ 或仓库根): - - 预览:`mdbook serve document -n 0.0.0.0 -p 4000` - - 构建:`mdbook build document` - 3. 浏览器打开预览 - - 终端:`$BROWSER http://127.0.0.1:4000/` -- 无 book.toml 时:你仍可直接阅读 Markdown 文件;如需 mdBook 视图,请新增 book.toml 并确保 SUMMARY.md 正确列出条目。 - -## 写作与维护约定 - -- 文件组织 - - 每个子系统一个子目录;跨子系统主题放在更高层次目录(如 kernel 与 mm 的交叉主题)。 - - 子目录首选提供一个该子系统的 README.md 做概览与导航。 -- 链接与路径 - - 文档内引用源码时,尽量使用以仓库根为基准的绝对路径提示(便于读者搜索源文件),例如:`os/src/ipc/pipe.rs` - - 面向 mdBook 的导航请在 SUMMARY.md 中登记;面向贡献者的说明放在各自 README.md。 -- 风格与结构 - - 先给结论与关键 API,再给背景与细节;长文档建议提供“导航/目录”与“总结/要点”。 - - 代码片段应最小可读,可附运行/调用路径。 -- 校验与工具 - - 风格与链接检查可参考:`document/scripts/style-check.md`、`document/scripts/rewrite_links.md` - - 提交前自查:新增文档是否需要出现在 SUMMARY.md;是否在对应子目录 README 中被导航到。 - -## 新增或更新文档的建议流程 - -1. 在对应子目录内新增 Markdown 文件,或同步更新该子目录 README 的导航。 -2. 若需要在 mdBook 中展示,更新 `document/SUMMARY.md`,保持目录结构清晰。 -3. 本地预览(如使用 mdBook):`mdbook serve document -n 0.0.0.0 -p 4000`,浏览器打开:`$BROWSER http://127.0.0.1:4000/` -4. 在 PR 描述中简述新增内容与对应源码位置,便于评审。 +# 文档总览 + +`document/` 是 Comix 的正式设计文档入口,面向内核贡献者和评审者。这里解释系统为什么这样分层、模块如何协作、哪些约束不能破坏;具体函数签名、字段语义和局部实现细节应优先维护在 rustdoc、模块注释和源码注释中。 + +## 文档分层 + +- `document/`:设计、边界、关键流程、不变量、已知限制和维护约束。 +- `os/src/**` rustdoc:公共类型、函数、错误条件、调用约束和小型示例。 +- 源码行内注释:unsafe 依据、架构细节、锁顺序、性能取舍和非显而易见的局部逻辑。 +- `local_doc/`:本地草稿和阶段性研究资料,不作为正式文档同步目标。 + +## 写作契约 + +- 每个子系统首选一个 `README.md` 做概览,子页面只覆盖清晰的设计主题。 +- 一篇设计文档应说明当前状态、目标/非目标、模块边界、关键流程、并发或生命周期约束、已知限制和源码索引。 +- 源码路径使用仓库根相对路径,例如 `os/src/ipc/pipe.rs`。 +- 避免维护函数清单、字段大全、完整错误分支和长代码示例;这些内容更适合 rustdoc。 +- 文档只描述当前代码已经提供的能力。未实现能力必须明确标为限制或后续方向。 +- 新增正式页面后必须更新 `document/SUMMARY.md`,并在对应子系统 README 中加入导航。 + +## 导航策略 + +Comix 采用"细项主导航 + 简洁页面"的混合风格。主导航保留当前实现相关的细分设计页,方便读者直接跳到具体主题;页面内容则保持设计优先,避免膨胀成 API 手册。 + +- `document/SUMMARY.md` 可以列出有维护价值的细项,例如 MM 的地址/页表/地址空间、VFS 的 File/FDTable/路径挂载、IPC 的 pipe/shared memory/signal。 +- 历史状态、未实现方案或重复 rustdoc 的页面不进入主导航,只从子系统 README 的历史或延伸阅读区域链接。 +- 子系统 README 必须解释这些细项的阅读顺序和用途,避免主导航只是文件列表。 +- 参考 SanktaOS 的短页面写法,但不照搬它的极简主导航。 + +## 预览与构建 + +在仓库根目录执行: + +```bash +mdbook serve document -n 0.0.0.0 -p 4000 +mdbook build +``` + +API 细节通过 rustdoc 生成: + +```bash +cd os +cargo doc --no-deps --target riscv64gc-unknown-none-elf +``` + +## 维护流程 + +1. 先读最新源码入口和现有文档,确认文档要表达的是设计而不是代码搬运。 +2. 在对应子系统目录更新 Markdown;跨子系统主题放到更高层级。 +3. 更新 `document/SUMMARY.md` 和子系统 README 导航。 +4. 运行 `mdbook build`,必要时运行 `python3 scripts/rewrite_links.py document/` 后再次构建。 +5. 若改动涉及公共 API 文档,运行 `cargo doc --no-deps --target riscv64gc-unknown-none-elf`。 ## 常见问题 -- 为什么我新增的文档在左侧目录看不到? - - 需要把文档添加到 `document/SUMMARY.md` 中。README 只提供作者指引,不参与 mdBook 的目录生成。 -- 链接在 mdBook 中 404? - - 检查相对路径是否以 `document/` 为根进行组织;必要时使用脚本 `document/scripts/rewrite_links.md` 的建议重写规则。 \ No newline at end of file +- 新页面没有出现在左侧目录:需要加入 `document/SUMMARY.md`。 +- 文档和 rustdoc 内容重复:正式文档保留设计说明,把 API 细节移回 rustdoc。 +- 文档引用了不存在的源码路径:以 `os/src` 当前目录结构为准修正,不保留历史路径作为当前实现。 diff --git a/document/SUMMARY.md b/document/SUMMARY.md index 8d8daf12..93c295ba 100644 --- a/document/SUMMARY.md +++ b/document/SUMMARY.md @@ -19,8 +19,8 @@ - [架构设计](log/architecture.md) - [日志级别](log/level.md) - [缓冲区和条目](log/buffer_and_entry.md) - - [使用方法](log/usage.md) - - [API 参考](log/api_reference.md) + - [使用边界](log/usage.md) + - [API 文档入口](log/api_reference.md) # 网络 @@ -31,16 +31,16 @@ - [Socket 与 syscall](net/socket_syscall.md) - [Loopback 与 poll](net/loopback_poll.md) - [测试与排查](net/testing.md) - - [网络实现指南](net/network_implementation_guide.md) + - [网络维护边界](net/network_implementation_guide.md) - [netperf / netserver 测试说明](net/netperf.md) # 同步原语 - [同步机制概述](sync/README.md) + - [RawSpinLock](sync/raw_spin_lock.md) - [自旋锁](sync/spin_lock.md) + - [互斥锁](sync/mutex.md) - [读写锁](sync/rwlock.md) - - [票号锁](sync/ticket_lock.md) - - [睡眠锁](sync/sleep_lock.md) - [中断保护](sync/intr_guard.md) - [Per-CPU 变量](sync/per_cpu.md) - [抢占控制](sync/preempt.md) @@ -49,6 +49,8 @@ # 内核子系统 +- [启动流程](kernel/boot.md) + ## 任务管理 - [任务管理概述](kernel/task/README.md) @@ -73,7 +75,7 @@ - [路径解析与挂载](vfs/path_and_mount.md) - [FileSystem 与错误处理](vfs/filesystem_and_errors.md) - [文件锁与设备管理](vfs/filelock_and_devices.md) - - [使用指南](vfs/usage.md) + - [维护边界](vfs/usage.md) # 文件系统实现 @@ -82,6 +84,7 @@ - [ProcFS - 进程信息](fs/procfs.md) - [SysFS - 系统设备](fs/sysfs.md) - [Ext4 - Linux文件系统](fs/ext4.md) + - [VFAT/FAT - mount 兼容路径](fs/vfat.md) - [SimpleFS - 测试文件系统](fs/simple_fs.md) # 设备与驱动 @@ -102,6 +105,8 @@ # 架构相关 +- [架构抽象概览](arch/README.md) + ## RISC-V - [RISC-V寄存器](arch/riscv/riscv_register.md) diff --git a/document/api.md b/document/api.md index e54c63bf..128c3b80 100644 --- a/document/api.md +++ b/document/api.md @@ -1,4 +1,16 @@ # API 文档 -以下是本项目的API文档链接: -- [RISC-V64](https://comix-kernel.github.io/comix/api/os/) -- [LoongArch64(TODO)]() \ No newline at end of file + +正式设计文档只保留模块边界和关键流程,公共 API 细节以 rustdoc 为准。 + +## 在线入口 + +- [RISC-V64 rustdoc](https://comix-kernel.github.io/comix/api/os/) + +## 本地生成 + +```bash +cd os +cargo doc --no-deps --target riscv64gc-unknown-none-elf +``` + +LoongArch64 代码仍在演进中。面向架构差异的设计说明见 `document/arch/`,具体 API 以当前目标架构的 rustdoc 和源码为准。 diff --git a/document/arch/README.md b/document/arch/README.md new file mode 100644 index 00000000..edfe1cb2 --- /dev/null +++ b/document/arch/README.md @@ -0,0 +1,90 @@ +# 架构层设计 + +本文记录 `arch/` 的当前设计边界.具体寄存器编码,页表位和汇编保存槽位以源码与 rustdoc 为准. + +## 当前状态 + +Comix 目前支持 RISC-V64,LoongArch64 和宿主测试用 mock 架构.`os/src/arch/mod.rs` 是架构层唯一公开门面: 目标架构由条件编译选择, 内核其余模块通过 `crate::arch::*`,`ArchImpl`,`PlatformImpl` 和统一的 `TrapFrame` 类型访问架构能力. + +RISC-V64 的多核启动,软件中断 IPI,定时器中断和外部中断路径已经接入通用调度.LoongArch64 当前以单核 bringup 和用户态运行为主, 提供同名接口保持通用内核代码可复用, 但 IPI 和外部中断处理仍是后续工作. + +## 目标和非目标 + +目标: + +- 把条件编译和寄存器操作限制在 `arch/` 内部. +- 为内核启动,任务切换,trap 返回,地址转换,时钟和 IPI 提供稳定的跨架构接口. +- 允许新架构只实现最小 hook, 复用 `kernel::boot`,调度器和任务模型. +- 保持宿主测试可以通过 mock 架构构建核心模块. + +非目标: + +- 不在正式文档中维护寄存器字段大全或页表位清单. +- 不把架构私有 crate 或 CSR 操作暴露给 `arch/` 外部模块. +- 不在架构层实现调度策略,任务生命周期或文件系统启动策略. + +## 模块边界 + +- `arch/mod.rs`: 目标架构选择,统一类型别名和便捷包装函数. +- `arch/arch.rs`,`arch/cpu_ops.rs`,`arch/plat.rs`: 定义跨架构 trait 边界. +- `arch/{riscv,loongarch}/boot`: 只负责进入公共启动流前后的架构 hook. +- `arch/{riscv,loongarch}/trap`: 汇编入口,TrapFrame 布局,异常/中断分派和 trap 返回. +- `arch/{riscv,loongarch}/kernel`: 任务上下文和切换汇编. +- `arch/{riscv,loongarch}/mm`: 页表,地址转换和 TLB 相关实现. + +通用内核模块不应直接读取 RISC-V CSR 或 LoongArch CSR.确实需要架构行为时, 优先增加 trait 方法或 `arch/mod.rs` 包装. + +## 关键流程 + +### 启动交接 + +```text +arch entry.S + -> arch::boot::main + -> kernel::boot::run_primary_boot + -> mm/trap/platform/time/timer + -> idle task + -> rest_init + -> /sbin/init +``` + +架构入口只决定早期 CPU 指针,FPU/CSR 等本架构必须先完成的状态.公共启动顺序由 `run_primary_boot` 维护, 避免两个架构各自复制 init/idle/rootfs 逻辑. + +### trap 和任务切换 + +trap 入口保存完整 `TrapFrame`, Rust handler 分派 syscall,timer,IPI 或设备中断.调度器若在 handler 中切换任务, trap 返回必须恢复"当前任务"的 `TrapFrame`, 而不是入口时传入的旧指针. + +普通任务切换使用 `Context`, 只保存调用约定要求的最小寄存器集合.`TrapFrame` 面向异常边界, `Context` 面向调度边界, 二者不能互相替代. + +## 并发和生命周期约束 + +- 访问 `current_cpu()` 前必须处在不可迁移的临界区, 现有代码通常使用 `PreemptGuard`. +- 任务迁移或切换后必须同步 `TrapFrame.cpu_ptr`, 否则 trap entry 恢复内核 `tp` 时可能指向旧 CPU. +- trap handler 运行在硬中断上下文, 不应执行可能阻塞或长期持锁的工作. +- RISC-V IPI 使用 per-CPU 原子 pending 标志, 发送端设置标志后再触发 SBI 软件中断. + +## 已知限制 + +- LoongArch IPI 当前是单核 no-op 接口, 尚未接入多核硬件中断. +- LoongArch trap 当前主要处理 syscall 和 timer; 用户态未知异常仍走 panic/诊断路径, 外部设备中断路径还未与 RISC-V 对齐. +- RISC-V TLB shootdown IPI 已有发送和处理接口, 但更完整的同步等待策略需要由内存管理侧继续收敛. + +## 文档导航 + +- [RISC-V 寄存器速查](riscv/riscv_register.md): RISC-V 常用寄存器和特权态入口背景. +- [RISC-V 用户栈布局](riscv/stack_layout.md): `execve` 后用户栈参数, 环境变量和辅助向量布局. +- [RISC-V 多核启动](riscv/smp_boot.md): 主核/从核启动交接和在线 CPU 管理. +- [RISC-V IPI](riscv/ipi.md): reschedule 和 TLB flush IPI 的当前协议. +- [LoongArch64 状态](loongarch/README.md): LoongArch 当前支持边界. +- [LoongArch bringup 记录](loongarch/bringup_userland.md): LoongArch 用户态启动修复和剩余限制. + +## 源码索引 + +- `os/src/arch/mod.rs`: 架构门面,统一类型和跨架构包装. +- `os/src/arch/arch.rs`: `Arch` 和 `HwTrapFrame` trait 边界. +- `os/src/arch/riscv/boot/mod.rs`: RISC-V 主核 hook,从核启动和在线 CPU 掩码. +- `os/src/arch/loongarch/boot/mod.rs`: LoongArch 主核 hook 和早期 FPU 使能. +- `os/src/arch/riscv/trap/*`: RISC-V trap 入口,分派和恢复. +- `os/src/arch/loongarch/trap/*`: LoongArch trap 入口,分派,TLB refill 入口安装和恢复. +- `os/src/arch/riscv/ipi.rs`: RISC-V IPI pending 标志和 SBI 发送. +- `os/src/arch/loongarch/ipi.rs`: LoongArch 单核 IPI 兼容接口. diff --git a/document/arch/loongarch/README.md b/document/arch/loongarch/README.md index cb9ca0cc..193d82ae 100644 --- a/document/arch/loongarch/README.md +++ b/document/arch/loongarch/README.md @@ -1,4 +1,54 @@ -# LoongArch64 +# LoongArch64 架构设计 -- [启动与用户态运行修复总结(comix-1 当前分支)](bringup_userland.md) +LoongArch64 当前文档聚焦启动,trap 和用户态运行的设计边界.历史 bringup 复盘见 `bringup_userland.md`, 当前架构总览见 `../README.md`. +## 当前状态 + +LoongArch64 已接入公共启动流,用户态 exec 栈构造,trap 保存恢复,timer 中断和 TLB refill 入口安装.启动阶段只通过 `PrimaryBootOps` 挂接基础 FPU 使能, 其余 init/idle/rootfs 逻辑复用 `kernel::boot`. + +当前仍是单核架构路径.IPI 接口保留为 no-op 兼容层, 让通用调度代码不需要为 LoongArch 特判. + +## 目标和非目标 + +目标: + +- 与 RISC-V 共享公共启动,任务和调度模型. +- 在 `arch/loongarch` 内封装 CSR,DMW,TLB refill 和 trap entry 差异. +- 保持用户态 busybox/init 类程序可通过 Linux ABI 子集运行. + +非目标: + +- 不承诺 LoongArch 与 RISC-V 当前具备相同 SMP 能力. +- 不在文档中维护 CSR 编号和 TrapFrame 槽位清单. +- 不把 bringup 调试日志视为长期接口. + +## 关键流程 + +启动: + +```text +entry.S -> loongarch::boot::main -> enable_base_fp -> kernel::boot::run_primary_boot +``` + +trap: + +```text +trap_entry -> trap_handler -> syscall/timer/exception -> restore current TrapFrame +``` + +TLB refill 入口由 trap 初始化阶段写入 CSR, 使用独立汇编路径完成软件页表遍历和 `tlbfill`. + +## 已知限制 + +- IPI 和 SMP 启动尚未实现. +- 外部中断分派还未与 RISC-V 对齐. +- 用户态未知异常当前仍是诊断/panic 路径, 后续应收敛为任务终止或信号. + +## 源码索引 + +- `os/src/arch/loongarch/boot/mod.rs`: 主核入口和基础 FPU 使能. +- `os/src/arch/loongarch/trap/mod.rs`: trap 门面. +- `os/src/arch/loongarch/trap/trap_handler.rs`: trap 分派和 TLB refill 入口安装. +- `os/src/arch/loongarch/trap/trap_entry.S`: 保存恢复和 TLB refill 汇编. +- `os/src/arch/loongarch/mm/mod.rs`: 直接映射窗口和内核根页表记录. +- `os/src/arch/loongarch/ipi.rs`: 单核 IPI 兼容接口. diff --git a/document/arch/loongarch/bringup_userland.md b/document/arch/loongarch/bringup_userland.md index 00dfe20e..01c63973 100644 --- a/document/arch/loongarch/bringup_userland.md +++ b/document/arch/loongarch/bringup_userland.md @@ -1,202 +1,60 @@ -# LoongArch64 启动与用户态运行修复总结(comix-1 当前分支) +# LoongArch64 用户态 bringup 设计记录 -本文档总结当前分支中,为了让 comix 在 LoongArch64 架构下能够**稳定完成启动、进入并持续运行用户态程序(busybox init / shell)**而做的一组修复与对齐工作。重点目标是:让 LoongArch 的整体启动语义尽可能与 RISC-V 路径一致(尤其是 `/dev` 等挂载点由 rcS 负责挂载),并且把“卡在 trap / 无法进入用户态 / 用户态一运行就异常”的问题收敛到可解释、可复现、可继续演进的状态。 +本文保留 LoongArch64 能进入用户态所依赖的设计点, 不再作为逐项修复流水账维护. ---- +## 当前状态 -## 1. 背景与目标 +LoongArch64 已能通过公共启动流创建 PID 1, 执行 `/sbin/init`, 并依赖用户态 rcS 完成 `/dev`,`/proc`,`/sys`,`/tmp` 等挂载.内核侧在 mount 特殊路径中提供必要兜底, 让 rootfs 缺少挂载点目录时仍能继续启动. -### 1.1 现象概述(修复前) +用户态运行依赖四个架构点: -LoongArch64 上曾出现以下典型现象(可单独出现或互相叠加): +- trap entry 和 `TrapFrame` 布局必须严格一致. +- TLB refill 入口不能破坏 trap entry 依赖的 scratch/CSR 状态. +- exec 栈布局必须满足 LoongArch Linux ABI 对 argv/envp/auxv/TLS 的基本要求. +- 启动早期必须使能基础浮点指令, 避免用户程序早期 FP 指令异常. -- `kernel_execve("/sbin/init")` 之后无法稳定进入用户态,串口输出停止或在 GDB 中反复停在 `trap_entry`。 -- 即使偶尔能进入用户态执行少量 syscall,也很快出现用户态异常: - - `estat` 指示地址相关异常(如地址错、权限错等), - - `badv` 出现 `0x9000...` 这一类**内核高地址**,明显是用户态不应访问的区域。 -- init 进程行为异常(例如 busybox 提示必须是 PID 1,或内核侧找不到 init 进程)。 -- `/dev/ttyS0` 等设备节点缺失,导致 rcS 无法起交互 shell。 +## 目标和非目标 -### 1.2 修复目标 +目标: -- **可靠进入用户态并持续运行**:用户态能完成 `init -> rcS -> spawn shell`,至少出现 `/ #` 提示符。 -- **启动流程对齐**:LoongArch 与 RISC-V 的职责边界一致: - - 内核负责挂载根文件系统(Ext4 root)。 - - `/dev /proc /sys /tmp` 等由用户态 rcS 执行 `mount` 完成。 - - 内核在 `mount("/dev")` 时做必要的内核侧初始化(创建设备节点)。 -- **可调试性**:提供足够的日志/断点点位,能明确区分: - - trap 入口保存/恢复错误 - - TLB refill 错误 - - 用户栈/TLS 约定问题 - - rootfs/挂载点目录缺失等“系统集成”问题 +- 让 LoongArch 与 RISC-V 在通用启动语义上保持一致. +- 把用户态 ABI 差异限制在 `arch/loongarch` 的 trap,task 和 mm 代码中. +- 保留定位用户态早期异常所需的诊断线索. ---- +非目标: -## 2. 关键问题定位方法(建议保留) +- 不长期保留每个历史 bug 的详细分支和补丁说明. +- 不在架构文档中描述 VFS mount 的完整实现. +- 不把当前调试日志数量视为稳定行为. -### 2.1 典型 GDB/寄存器观察点 +## 关键设计点 -当卡在 `trap_entry` 或用户态异常时,优先观察: +### TrapFrame 一致性 -- `ESTAT/ERA/BADV/BADI`: - - `ESTAT` 的 `ecode/esubcode` 用来区分 syscall / page fault / 地址错 / 指令错。 - - `ERA` 是异常发生时的 PC(用户态异常时通常在 `0x12...` 这类用户映射区)。 - - `BADV` 是访问地址(若出现在 `0x9000...`,高度怀疑寄存器现场被内核保存/恢复逻辑污染)。 - - `BADI` 是故障指令(可用来对照用户 ELF 的指令流)。 +LoongArch 用户态寄存器污染通常会表现为 `BADV` 指向内核高地址.此类问题优先检查 `trap_entry.S` 保存/恢复顺序与 `trap_frame.rs` 布局是否一致, 尤其是 callee-saved 寄存器和 `$tp/$sp/$ra`. -### 2.2 “用户态 BADV 指向 0x9000...” 的含义 +### TLB refill -这是本次最有信息量的信号之一:用户程序在执行正常指令时,某个寄存器被污染成内核高地址(或某个指针被写错),从而产生“用户态非法访问内核区”。这种现象在 LoongArch 上最常见的根因是: +TLB refill 使用独立入口, 通过软件页表遍历填充 TLB.该路径必须避免覆盖 trap entry 用来定位保存区的 scratch 状态, 否则会出现 trap storm 或恢复到错误现场. -- trap 入口保存现场写错槽位(TrapFrame 布局与汇编保存顺序不一致),或 -- trap 返回恢复现场读错槽位,导致用户寄存器恢复后被污染。 +### 用户栈和 TLS ---- +exec 路径构造 argv/envp/auxv, 并为 LoongArch 设置 TLS/thread pointer.用户程序进入 libc 早期初始化前, `$sp`,`$tp` 和参数寄存器必须同时满足 ABI 预期. -## 3. 修复项总览(按子系统) +### 启动职责 -### 3.1 启动入口与 DTB(设备树)指针探测 +内核负责发现并挂载根文件系统, `/dev` 等运行期伪文件系统交给 rcS.内核保留挂载点兜底创建, 这是为了兼容不同架构 rootfs 镜像内容差异. -**问题**:QEMU/固件传入 DTB 指针的寄存器约定不稳定;直接使用错误指针会导致 FDT 解析失败,影响设备初始化。 +## 已知限制 -**修复**:在汇编入口 `_start` 中对候选 DTB 地址做 magic 检查,选择有效指针后写入全局 `DTP`(保存物理地址,由 Rust 侧再转换直映虚拟地址)。 +- 用户态异常处理仍偏 bringup 诊断, 还没有完整映射到 POSIX 信号. +- 外部中断和多核 IPI 未完成. +- rootfs 目录结构最好继续向 RISC-V 镜像对齐, 减少内核兜底逻辑的必要性. -- 相关文件: - - `os/src/arch/loongarch/boot/entry.S` - -### 3.2 TLB Refill:可用的 refill 入口与“不要踩 CSR” - -**问题**:用户态运行时大量缺页/首次访问需要 TLB refill;若 refill 入口不正确或破坏了 trap 入口依赖的 CSR/寄存器,会表现为随机 trap storm 或直接卡死。 - -**修复**: - -- 使用 `lddir/ldpte/tlbfill` 的硬件辅助页表遍历路径实现 `tlb_refill_entry`。 -- refill 入口只使用 `CSR 0x8b (TLBRSAVE)` 保存/恢复 `$t0`,避免覆写 `KSAVE/KSCRATCH` 一类 CSR,从而不干扰 `trap_entry` 的 TrapFrame 指针机制。 - -- 相关文件: - - `os/src/arch/loongarch/trap/trap_entry.S` - -### 3.3 trap_entry / __restore:寄存器现场保存/恢复一致性(核心) - -**问题**:TrapFrame 与汇编保存/恢复顺序不一致会直接导致: - -- 用户态 syscall 返回后寄存器被污染(例如 s0..s8 偏移错一位), -- 进而出现用户态 `BADV=0x9000...` 的非法访存异常。 - -**修复**: - -- trap 入口先把原始 `$a0/$t1` 写入 scratch CSR,再读取 `KScratch0` 中的 TrapFrame 指针; -- 保存/恢复时严格按 LoongArch ABI 寄存器编号对应 TrapFrame 槽位; -- 修正了 callee-saved 区(`$s0..$s8`)的槽位映射,避免“漏存 $s0 导致整段错位”。 - -- 相关文件: - - `os/src/arch/loongarch/trap/trap_entry.S` - - `os/src/arch/loongarch/trap/trap_frame.rs`(TrapFrame 结构与 exec 初始化) - - `os/src/arch/loongarch/trap/trap_handler.rs`(用户态异常打印、syscall 分发与 restore) - -### 3.4 用户态 TLS/TP 与用户栈布局(musl/busybox 依赖) - -**问题**:许多 LoongArch Linux-ABI 用户程序依赖 `$tp` 作为 TLS 指针;如果 execve 后 `$tp` 没有按约定设置,早期 libc 初始化可能会崩。 - -**修复**: - -- 在用户栈顶预留一页作为 TLS/TCB 区域; -- 将 `$tp` 设置为该页内一个稳定的对齐地址,并写入最小 self-pointer(满足常见 libc 期望); -- 在用户栈中构造 `argc/argv/envp/auxv`(包含 `AT_PHDR/AT_ENTRY/AT_PLATFORM/AT_RANDOM/AT_EXECFN` 等); -- `set_exec_trap_frame()` 将 `tp/sp/a0/a1/a2` 等寄存器按 LoongArch ABI 写入 TrapFrame。 - -- 相关文件: - - `os/src/arch/loongarch/kernel/task.rs`(`setup_stack_layout()`) - - `os/src/kernel/task/task_struct.rs`(execve 路径调用与写回) - - `os/src/arch/loongarch/trap/trap_frame.rs` - -### 3.5 FPU 使能(EUEN.FPE) - -**问题**:部分用户程序会在非常早的阶段执行 FP 指令;若未启用基础 FPU,会出现异常或非预期行为。 - -**修复**:在 LoongArch 启动早期设置 `EUEN.FPE = 1`。 - -- 相关文件: - - `os/src/arch/loongarch/boot/mod.rs` - -### 3.6 启动流程与任务模型对齐(init PID=1、idle_task) - -**问题**: - -- busybox init 要求自己是 PID 1;如果内核创建的 init task 不是 TID/PID 1,会直接报错。 -- 调度器在 runqueue 为空时会切换到每 CPU 的 idle_task;若未设置,会触发 panic。 - -**修复**: - -- LoongArch `rest_init()` 固定创建 init 任务为 `tid=pid=1`(与 RISC-V 一致)。 -- 在 CPU0 预先创建并登记 idle 任务(不加入 runqueue,仅作为兜底)。 - -- 相关文件: - - `os/src/arch/loongarch/boot/mod.rs` - -### 3.7 `/dev` 挂载与设备节点:让 LoongArch 与 RISC-V 行为一致 - -**问题根因**(为什么 RISC-V 没问题而 LoongArch 出问题): - -- 运行时镜像由 `os/build.rs` 从 `data/_musl` 目录生成: - - `fs-riscv.img` 根目录自带 `/dev /proc /sys` 等目录; - - `fs-loongarch.img` 根目录不包含 `/dev`(也可能不包含 `/proc /sys`)。 -- RISC-V 路径把 `/dev` 挂载留给 rcS,并依赖内核对 `mount("/dev")` 的特殊处理自动 `init_dev()`;因为 `/dev` 目录存在,所以工作正常。 -- LoongArch 若沿用“rcS 挂载 /dev”,但镜像里没有 `/dev` 目录,则挂载点不存在,后续 `init_dev()` 无法创建 `/dev/ttyS0`,最终出现 `can't open /dev/ttyS0`。 - -**修复策略**: - -1) **启动流程对齐**:LoongArch 与 RISC-V 一样,把 `/dev(/proc,/sys,/tmp)` 的挂载交给用户态 rcS 完成。 - -- `os/src/arch/loongarch/boot/mod.rs` -- `data/loongarch_musl/etc/init.d/rcS` - -2) **内核兜底**:在 `SYS_MOUNT` 的特殊分支中,为 `/dev /proc /sys /tmp` 增加“挂载点目录不存在则先创建”的逻辑,然后再执行挂载动作;`/dev` 分支继续在挂载 tmpfs 后调用 `init_dev()` 创建设备节点。 - -- `os/src/kernel/syscall/fs.rs` - -> 这使得:即使某个架构的 rootfs 镜像缺少挂载点目录,rcS 的挂载也不会因为“目录不存在”而失败,从而让启动语义更稳健、更一致。 - ---- - -## 4. 启动流程对齐总结(RISC-V vs LoongArch) - -对齐后的关键点: - -- 两个架构都在内核 init task 中挂载/初始化 Ext4 root(作为根文件系统)。 -- `/dev /proc /sys /tmp` 均由用户态 rcS 执行 `mount` 完成。 -- 内核在 `SYS_MOUNT` 中对这些 target 做特殊处理: - - `/dev`:挂载 tmpfs 后自动 `init_dev()` 创建设备节点; - - `/proc`:`init_procfs()`; - - `/sys`:`init_sysfs()`; - - `/tmp`:`mount_tmpfs()`; - - 并在进入这些特殊处理前确保挂载点目录存在。 - ---- - -## 5. 验证结果(当前分支) - -### 5.1 运行验证 - -使用: - -- `make run ARCH=loongarch` - -预期能观察到: - -- `/sbin/init` 启动并执行 rcS, -- 后续出现 shell 提示符(例如 `/ #`),说明用户态已经稳定运行并能进行基本交互。 - -### 5.2 验证点解释 - -- 能连续看到大量 syscall(包含 `mount`、`openat`、`read/write`、`clone` 等)且不再出现用户态 `BADV=0x9000...` 类型的地址异常,说明 trap 保存/恢复与 TLB refill 已经基本稳定。 -- shell 能起则说明 `/dev/ttyS0` 已存在且可打开(这依赖 rcS mount("/dev") + 内核 init_dev())。 - ---- - -## 6. 后续建议(非阻塞) - -1) **rootfs 内容对齐**:建议让 `data/loongarch_musl` 也包含 `/dev /proc /sys` 等空目录,使镜像结构更接近 `data/risc-v_musl`,减少对内核“兜底创建目录”的依赖。 -2) **减少调试噪声**:目前 LoongArch trap/syscall 输出较多(用于 bringup);在稳定后可逐步降级为 `pr_debug` 或加开关。 -3) **设备/网络完善**:virtio-net 可能仍会报 `DeviceNotReady`,这属于设备初始化/驱动完善方向,与“能进用户态”已解耦。 +## 源码索引 +- `os/src/arch/loongarch/trap/trap_entry.S`: trap 保存恢复和 TLB refill. +- `os/src/arch/loongarch/trap/trap_frame.rs`: LoongArch `TrapFrame` 和 exec 返回现场. +- `os/src/arch/loongarch/trap/trap_handler.rs`: syscall/timer/异常分派和入口安装. +- `os/src/arch/loongarch/kernel/task.rs`: 用户栈和 TLS 布局. +- `os/src/arch/loongarch/boot/mod.rs`: 基础 FPU 使能和公共启动入口. diff --git a/document/arch/riscv/ipi.md b/document/arch/riscv/ipi.md index 49df8ed3..5cfcb48e 100644 --- a/document/arch/riscv/ipi.md +++ b/document/arch/riscv/ipi.md @@ -1,552 +1,59 @@ -# RISC-V 核间中断 (IPI) +# RISC-V IPI 设计 -本文档详细描述 Comix 内核在 RISC-V 架构上的核间中断(Inter-Processor Interrupt, IPI)实现,包括设计原理、API 接口、使用场景和性能考虑。 +## 当前状态 -## 1. 概述 +RISC-V IPI 用于跨 CPU 通知.当前实现基于 SBI 发送软件中断, 并用 per-CPU 原子 pending 标志记录待处理动作.已接入的动作包括 reschedule,TLB flush 和 stop. -### 1.1 什么是 IPI +调度器唤醒任务时, 如果目标 CPU 不是当前 CPU, 会发送 reschedule IPI.接收 CPU 在软件中断 trap 中处理 pending 标志, 然后按 run queue 状态决定是否调度. -IPI(Inter-Processor Interrupt,核间中断)是多核系统中 CPU 之间进行通信的基本机制。通过 IPI,一个 CPU 可以向其他 CPU 发送中断信号,通知它们执行特定操作。 +## 目标和非目标 -### 1.2 主要用途 +目标: -Comix 内核的 IPI 实现支持以下功能: +- 在硬中断上下文中以无分配方式处理跨核通知. +- 支持多个 IPI 类型合并到同一次 pending 标志. +- 支持批量发送, 减少 SBI 调用. +- 为调度唤醒和 TLB shootdown 提供统一机制. -- **任务调度唤醒**:通知目标 CPU 有新任务需要调度 -- **TLB 刷新同步**:页表更新后通知其他 CPU 刷新 TLB(TLB Shootdown) -- **系统停机协调**:关机时停止所有 CPU +非目标: -### 1.3 当前状态 +- 不在 IPI 层等待远端 CPU 完成某项工作. +- 不在 IPI handler 中执行可阻塞操作. +- 不由 IPI 层决定调度策略. -- **实现方式**:基于 SBI(Supervisor Binary Interface)的软件中断 -- **支持的 IPI 类型**:Reschedule、TlbFlush、Stop -- **性能优化**:支持批量发送,减少 SBI 调用次数 -- **内存安全**:使用原子操作,中断上下文不进行内存分配 +## 关键流程 -## 2. 架构设计 +```text +sending CPU + -> set IPI_PENDING[target] with Release + -> SBI send_ipi -### 2.1 IPI 类型 - -IPI 类型使用位标志表示,支持组合多种类型: - -```rust -#[repr(u32)] -#[derive(Debug, Clone, Copy)] -pub enum IpiType { - /// 重新调度(通知目标 CPU 有新任务) - Reschedule = 1 << 0, // 0b001 - /// TLB 刷新(页表更新后同步) - TlbFlush = 1 << 1, // 0b010 - /// 停止 CPU(系统关机) - Stop = 1 << 2, // 0b100 -} -``` - -使用位标志的好处: -- 可以组合多种 IPI 类型(例如:`Reschedule | TlbFlush`) -- 使用原子操作的 `fetch_or` 可以高效地设置多个标志 -- 接收端可以一次处理多种 IPI 类型 - -### 2.2 Per-CPU 待处理标志 - -每个 CPU 都有一个独立的原子变量,存储待处理的 IPI 类型: - -```rust -/// Per-CPU 待处理 IPI 标志 -static IPI_PENDING: [AtomicU32; MAX_CPU_COUNT] = [ - AtomicU32::new(0), - AtomicU32::new(0), - // ... 最多 8 个 CPU -]; -``` - -**设计要点**: -- 使用 `AtomicU32` 保证多核环境下的线程安全 -- 每个 CPU 独立的变量,避免缓存行竞争(False Sharing) -- 使用 `Release`/`AcqRel` 内存序保证可见性 - -### 2.3 工作流程 - -IPI 的完整工作流程如下: - -``` -发送端 CPU: -1. IPI_PENDING[target].fetch_or(ipi_type) // 设置待处理标志 -2. sbi::send_ipi(hart_mask) // 通过 SBI 触发软件中断 - ↓ - ↓ (SBI 固件处理) - ↓ -接收端 CPU: -1. 触发 Trap::Interrupt(1) 软件中断 -2. trap_handler 调用 handle_ipi() -3. 读取并清除 IPI_PENDING[cpu] -4. 根据标志位执行相应操作: - - Reschedule: 标记需要重新调度 - - TlbFlush: 执行 sfence.vma 刷新 TLB - - Stop: 进入 WFI 循环停机 -``` - -## 3. 实现原理 - -### 3.1 基于 SBI 的 IPI 发送 - -RISC-V 平台通过 SBI(Supervisor Binary Interface)提供 IPI 支持。Comix 使用两种 SBI 接口: - -1. **SBI IPI 扩展**(优先使用): - ```rust - const EID_IPI: usize = 0x735049; - const FID_SEND_IPI: usize = 0; - sbi_call(EID_IPI, FID_SEND_IPI, hart_mask, 0, 0); - ``` - -2. **Legacy SBI**(回退方案): - ```rust - const LEGACY_SEND_IPI: usize = 4; - sbi_call(LEGACY_SEND_IPI, 0, &hart_mask as *const _ as usize, 0, 0); - ``` - -实现会先尝试 SBI IPI 扩展,如果失败则回退到 Legacy SBI,确保兼容性。 - -### 3.2 软件中断处理 - -IPI 通过 RISC-V 的软件中断(Supervisor Software Interrupt)实现: - -1. **中断号**:`Trap::Interrupt(1)`(SUPERVISOR_SOFTWARE) -2. **中断使能**:在 trap 初始化时通过 `sie::set_ssoft()` 使能 -3. **处理入口**:在 `trap_handler.rs` 的 `user_trap` 和 `kernel_trap` 中处理 - -```rust -// 在 trap_handler.rs 中 -Trap::Interrupt(1) => { - // 软件中断(IPI) - crate::arch::ipi::handle_ipi(); -} -``` - -### 3.3 原子操作和内存序 - -IPI 实现使用严格的内存序保证正确性: - -- **发送端**:使用 `Ordering::Release` 确保标志位设置对接收端可见 - ```rust - IPI_PENDING[target_cpu].fetch_or(ipi_type as u32, Ordering::Release); - ``` - -- **接收端**:使用 `Ordering::AcqRel` 确保读取到最新值并清除 - ```rust - let pending = IPI_PENDING[cpu].swap(0, Ordering::AcqRel); - ``` - -这保证了即使在弱内存序的架构上,IPI 也能正确工作。 - -## 4. API 接口 - -### 4.1 send_ipi - 发送 IPI 到单个 CPU - -```rust -pub fn send_ipi(target_cpu: usize, ipi_type: IpiType) -``` - -**功能**:向指定 CPU 发送 IPI。 - -**参数**: -- `target_cpu`: 目标 CPU ID(0 到 NUM_CPU-1) -- `ipi_type`: IPI 类型(Reschedule、TlbFlush 或 Stop) - -**使用示例**: -```rust -use crate::arch::ipi::{send_ipi, IpiType}; - -// 通知 CPU 1 有新任务需要调度 -send_ipi(1, IpiType::Reschedule); - -// 通知 CPU 2 刷新 TLB -send_ipi(2, IpiType::TlbFlush); -``` - -**注意事项**: -- 如果 `target_cpu >= NUM_CPU`,会触发 panic -- 可以向当前 CPU 发送 IPI(会立即触发软件中断) - -### 4.2 send_ipi_many - 批量发送 IPI - -```rust -pub fn send_ipi_many(hart_mask: usize, ipi_type: IpiType) -``` - -**功能**:向多个 CPU 批量发送 IPI,只需一次 SBI 调用。 - -**参数**: -- `hart_mask`: hart 位掩码,第 i 位为 1 表示向 CPU i 发送 -- `ipi_type`: IPI 类型 - -**使用示例**: -```rust -// 向 CPU 1, 2, 3 发送调度 IPI -let mask = (1 << 1) | (1 << 2) | (1 << 3); // 0b1110 -send_ipi_many(mask, IpiType::Reschedule); - -// 向所有 CPU 发送 TLB 刷新 IPI -let all_mask = (1 << NUM_CPU) - 1; -send_ipi_many(all_mask, IpiType::TlbFlush); -``` - -**性能优势**: -- 批量发送只需一次 SBI 调用,减少系统调用开销 -- 适合需要通知多个 CPU 的场景(如 TLB Shootdown) - -### 4.3 send_reschedule_ipi - 发送调度 IPI - -```rust -pub fn send_reschedule_ipi(cpu: usize) -``` - -**功能**:通知目标 CPU 有新任务需要调度(`send_ipi` 的便捷封装)。 - -**参数**: -- `cpu`: 目标 CPU ID - -**使用示例**: -```rust -// 唤醒 CPU 2 进行任务调度 -send_reschedule_ipi(2); -``` - -**典型场景**: -- 任务迁移:将任务从一个 CPU 迁移到另一个 CPU -- 负载均衡:唤醒空闲 CPU 处理新任务 -- 优先级抢占:高优先级任务到达时唤醒目标 CPU - -### 4.4 send_tlb_flush_ipi_all - 广播 TLB 刷新 - -```rust -pub fn send_tlb_flush_ipi_all() -``` - -**功能**:向所有其他 CPU 广播 TLB 刷新 IPI(不包括当前 CPU)。 - -**使用示例**: -```rust -// 修改页表后,通知所有其他 CPU 刷新 TLB -unsafe { - // 修改页表映射 - page_table.map(vaddr, paddr, flags); - - // 刷新当前 CPU 的 TLB - core::arch::asm!("sfence.vma"); - - // 通知其他 CPU 刷新 TLB - send_tlb_flush_ipi_all(); -} -``` - -**注意事项**: -- 当前 CPU 不会收到 IPI,需要自己刷新 TLB -- 这是一个同步点:调用后应等待其他 CPU 完成刷新(当前实现是异步的) - -### 4.5 handle_ipi - 处理 IPI - -```rust -pub fn handle_ipi() -``` - -**功能**:处理当前 CPU 收到的 IPI(在软件中断处理程序中调用)。 - -**处理流程**: -1. 读取并清除 `IPI_PENDING[cpu]` -2. 根据标志位执行相应操作: - - `Reschedule`: 标记需要重新调度(实际调度在中断返回时进行) - - `TlbFlush`: 执行 `sfence.vma` 刷新 TLB - - `Stop`: 进入 WFI 循环停机 - -**注意事项**: -- 此函数在中断上下文中调用,不能进行内存分配或持有睡眠锁 -- 调度 IPI 不会立即切换任务,而是在中断返回时由调度器处理 - -## 5. 使用场景 - -### 5.1 跨核任务唤醒 - -当一个 CPU 创建新任务或任务变为就绪状态时,可以通过 IPI 唤醒目标 CPU: - -```rust -// 在 CPU 0 上创建任务,分配给 CPU 1 -let task = Task::new(entry, args); -scheduler.add_task(task, target_cpu = 1); - -// 唤醒 CPU 1 进行调度 -send_reschedule_ipi(1); -``` - -### 5.2 TLB Shootdown - -在多核系统中,修改页表后需要通知所有 CPU 刷新 TLB。Comix 提供了两种方式: - -#### 自动 TLB Shootdown(推荐) - -**从 SMP 分支开始,页表操作会自动处理 TLB shootdown**,无需手动调用: - -```rust -// 页表操作会自动刷新所有 CPU 的 TLB -page_table.map(vpn, ppn, PageSize::Size4K, UniversalPTEFlag::user_rw())?; -// ✓ 自动刷新当前 CPU 的 TLB -// ✓ 自动通过 IPI 通知其他 CPU 刷新 TLB - -page_table.unmap(vpn)?; -// ✓ 自动处理 TLB shootdown - -page_table.update_flags(vpn, UniversalPTEFlag::kernel_rw())?; -// ✓ 自动处理 TLB shootdown -``` - -详见 [5.2.1 页表自动 TLB Shootdown](#521-页表自动-tlb-shootdown)。 - -#### 手动 TLB Shootdown(特殊场景) - -在某些特殊场景下(如直接操作页表项、批量修改等),可能需要手动触发 TLB shootdown: - -```rust -// 手动修改页表项后 -unsafe { - // 刷新当前 CPU 的 TLB - core::arch::asm!("sfence.vma"); - - // 通知所有其他 CPU 刷新 TLB - send_tlb_flush_ipi_all(); -} -``` - -**注意事项**: -- 大多数情况下应使用页表的标准 API(`map`/`unmap`/`update_flags`),它们会自动处理 TLB shootdown -- 只有在绕过页表 API 直接操作硬件时才需要手动调用 -- 手动调用时必须先刷新当前 CPU 的 TLB,再发送 IPI - -#### 5.2.1 页表自动 TLB Shootdown - -Comix 的页表实现在 `PageTableInner` 中集成了自动 TLB shootdown 机制,确保多核环境下的内存一致性。 - -**实现原理**: - -页表操作(`map`、`unmap`、`update_flags`)内部调用 `tlb_flush_all_cpus()` 方法: - -```rust -impl PageTableInner { - fn tlb_flush_all_cpus(vpn: Vpn) { - // 1. 刷新当前 CPU 的 TLB - >::tlb_flush(vpn); - - // 2. 通知所有其他 CPU 刷新 TLB - let num_cpu = unsafe { crate::kernel::NUM_CPU }; - if num_cpu > 1 { - send_tlb_flush_ipi_all(); - } - } -} -``` - -**行为特性**: - -| 环境 | 行为 | 性能开销 | -|------|------|----------| -| 单核(NUM_CPU = 1) | 只刷新本地 TLB | 最小(~10 周期) | -| 多核(NUM_CPU > 1) | 刷新本地 TLB + 发送 IPI | 中等(~500 周期) | -| 测试模式 | 自动检测环境 | 根据环境决定 | - -**使用示例**: - -```rust -// 示例 1:映射用户页面 -let vpn = Vpn::from_usize(0x10000); -let ppn = alloc_frame().unwrap().ppn(); -page_table.map(vpn, ppn, PageSize::Size4K, UniversalPTEFlag::user_rw())?; -// ✓ 所有 CPU 的 TLB 已自动刷新 - -// 示例 2:批量映射 -for i in 0..100 { - let vpn = Vpn::from_usize(0x10000 + i * 0x1000); - let ppn = alloc_frame().unwrap().ppn(); - page_table.map(vpn, ppn, PageSize::Size4K, UniversalPTEFlag::user_rw())?; - // 每次映射都会触发 TLB shootdown -} -// 注意:批量操作可能产生较多 IPI,未来可优化为批量刷新 - -// 示例 3:修改权限 -page_table.update_flags(vpn, UniversalPTEFlag::kernel_r())?; -// ✓ 权限更新后,所有 CPU 的 TLB 已自动刷新 -``` - -**性能考虑**: - -- **单核优化**:单核环境下无 IPI 开销,性能与传统实现相同 -- **多核开销**:每次页表操作约增加 0.5 微秒(4 核系统) -- **批量操作**:频繁的页表修改会产生大量 IPI,建议: - - 使用更大的页面(2MB/1GB)减少映射次数 - - 预分配页表,减少运行时修改 - - 未来可实现延迟批量刷新机制 - -**相关实现**: - -- 源码位置:`os/src/arch/riscv/mm/page_table.rs` -- IPI 发送:`os/src/arch/riscv/ipi.rs` 中的 `send_tlb_flush_ipi_all()` -- 详细说明:参见 [页表文档](../../mm/page_table.md#多核-tlb-shootdown) - -### 5.3 系统关机 - -关机时需要停止所有 CPU: - -```rust -// 向所有其他 CPU 发送停止 IPI -let current = cpu_id(); -let num_cpu = unsafe { crate::kernel::NUM_CPU }; - -for cpu in 0..num_cpu { - if cpu != current { - send_ipi(cpu, IpiType::Stop); - } -} - -// 等待所有 CPU 停止 -// ... - -// 当前 CPU 最后关机 -sbi::shutdown(false); -``` - -## 6. 性能考虑 - -### 6.1 IPI 延迟 - -IPI 的延迟主要来自: - -1. **SBI 调用开销**:从 S 模式陷入 M 模式(约 100-200 周期) -2. **中断传播延迟**:硬件传播中断信号(约 10-50 周期) -3. **中断处理开销**:保存上下文、调用处理函数(约 50-100 周期) - -总延迟约为 **200-400 个时钟周期**(在 1GHz CPU 上约 0.2-0.4 微秒)。 - -### 6.2 优化策略 - -#### 6.2.1 批量发送 - -使用 `send_ipi_many` 代替多次 `send_ipi`: - -```rust -// 不推荐:多次 SBI 调用 -for cpu in 1..num_cpu { - send_ipi(cpu, IpiType::TlbFlush); -} - -// 推荐:一次 SBI 调用 -let mask = ((1 << num_cpu) - 1) & !1; // 除了 CPU 0 -send_ipi_many(mask, IpiType::TlbFlush); -``` - -#### 6.2.2 合并 IPI 类型 - -利用位标志合并多种 IPI 类型: - -```rust -// 同时发送调度和 TLB 刷新 IPI -let combined = (IpiType::Reschedule as u32) | (IpiType::TlbFlush as u32); -IPI_PENDING[target].fetch_or(combined, Ordering::Release); -sbi::send_ipi(1 << target); -``` - -#### 6.2.3 避免不必要的 IPI - -- **本地操作**:如果目标是当前 CPU,直接执行操作而不发送 IPI -- **延迟批处理**:收集多个 IPI 请求,批量发送 -- **TLB 优化**:使用 ASID(地址空间标识符)减少 TLB 刷新需求 - -### 6.3 性能开销 - -在典型的多核系统中,IPI 的性能开销: - -- **单次 IPI**:约 0.5 微秒(包括发送和处理) -- **TLB Shootdown**:约 1-2 微秒(4 核系统) -- **任务唤醒**:约 0.3 微秒(仅发送 IPI) - -对于大多数应用,IPI 开销可以忽略不计。但在高频场景(如频繁的页表修改)中,应考虑优化。 - -## 7. 调试和验证 - -### 7.1 检查中断使能 - -确保软件中断已使能: - -```rust -// 在 trap::init() 中 -unsafe { - crate::arch::intr::enable_software_interrupt(); -} - -// 检查 sie 寄存器 -let sie = riscv::register::sie::read(); -assert!(sie.ssoft(), "Software interrupt not enabled"); -``` - -### 7.2 测试 IPI 发送和接收 - -```rust -// 测试 IPI 类型标志 -test_case!(test_ipi_type_flags, { - kassert!(IpiType::Reschedule as u32 == 1); - kassert!(IpiType::TlbFlush as u32 == 2); - kassert!(IpiType::Stop as u32 == 4); -}); - -// 测试 IPI 类型组合 -test_case!(test_ipi_type_combination, { - let combined = (IpiType::Reschedule as u32) | (IpiType::TlbFlush as u32); - kassert!(combined == 3); -}); -``` - -### 7.3 常见问题排查 - -| 问题 | 可能原因 | 解决方法 | -|------|----------|----------| -| IPI 未触发 | 软件中断未使能 | 检查 `sie.ssoft` 位 | -| IPI 丢失 | 标志位被覆盖 | 使用 `fetch_or` 而非直接赋值 | -| 死锁 | 在 IPI 处理中持有锁 | 避免在 `handle_ipi` 中持有锁 | -| 性能下降 | 过多的 IPI | 使用批量发送,减少 IPI 频率 | - -### 7.4 调试日志 - -启用 IPI 调试日志: - -```rust -// 在 handle_ipi() 中 -crate::pr_debug!("[IPI] CPU {} handling IPI: {:#x}", cpu, pending); -``` - -查看日志输出: -``` -[IPI] CPU 1 handling IPI: 0x1 // Reschedule -[IPI] CPU 2 handling IPI: 0x2 // TlbFlush -[IPI] CPU 3 handling IPI: 0x3 // Reschedule | TlbFlush +target CPU + -> software interrupt trap + -> clear SSIP + -> swap pending with AcqRel + -> handle reschedule/TLB flush/stop + -> trap handler may schedule ``` -## 8. 相关文件和参考资料 +reschedule IPI 只负责打断目标 CPU 并暴露"有调度事件"这一事实.是否真正切换任务, 由 trap handler 查看当前 CPU run queue 后决定. -### 8.1 源码位置 +## 并发约束 -| 文件路径 | 功能描述 | -|----------|----------| -| [`os/src/arch/riscv/ipi.rs`](/os/src/arch/riscv/ipi.rs) | IPI 核心实现 | -| [`os/src/arch/riscv/lib/sbi.rs`](/os/src/arch/riscv/lib/sbi.rs) | SBI IPI 接口封装 | -| [`os/src/arch/riscv/trap/trap_handler.rs`](/os/src/arch/riscv/trap/trap_handler.rs) | 软件中断处理入口 | -| [`os/src/arch/riscv/intr/mod.rs`](/os/src/arch/riscv/intr/mod.rs) | 软件中断使能/禁用 | -| [`os/src/arch/riscv/constant.rs`](/os/src/arch/riscv/constant.rs) | SUPERVISOR_SOFTWARE 常量 | +- 发送端先写 pending, 再触发 SBI IPI. +- 接收端用 `swap(0)` 一次性取走并清空 pending, 合并处理多个标志. +- IPI handler 不能分配内存, 不能持有会阻塞的锁. +- TLB flush IPI 当前只执行本地 `sfence.vma`; 更强完成确认应由调用方设计同步协议. -### 8.2 相关文档 +## 已知限制 -- [多核启动](./smp_boot.md) - SMP 系统的启动流程和 Per-CPU 数据结构 -- [SMP 与中断](../../sync/smp_interrupts.md) - SMP 系统中的中断和并发问题 -- [Per-CPU 变量](../../sync/per_cpu.md) - Per-CPU 数据结构的详细说明 +- RISC-V hart id 当前按 CPU id 位图发送, 依赖平台启动时的 hart/CPU 映射保持一致. +- TLB shootdown 缺少远端完成 ack. +- stop IPI 进入等待中断循环, 没有完整关机协调协议. -### 8.3 外部参考资料 +## 源码索引 -- [RISC-V SBI Specification](https://github.com/riscv-non-isa/riscv-sbi-doc) - SBI IPI 扩展规范 -- [RISC-V Privileged Specification](https://riscv.org/technical/specifications/) - 软件中断机制 -- [Linux Kernel IPI Implementation](https://www.kernel.org/doc/html/latest/core-api/irq/irq-domain.html) - Linux 的 IPI 实现参考 +- `os/src/arch/riscv/ipi.rs`: IPI 类型,pending 标志,发送和处理. +- `os/src/arch/riscv/trap/trap_handler.rs`: 软件中断分派和调度触发. +- `os/src/kernel/scheduler/mod.rs`: 跨 CPU wakeup 发送 reschedule IPI. +- `os/src/arch/riscv/lib.rs`: SBI 调用封装. diff --git a/document/arch/riscv/smp_boot.md b/document/arch/riscv/smp_boot.md index 64a0d97a..1aa94c69 100644 --- a/document/arch/riscv/smp_boot.md +++ b/document/arch/riscv/smp_boot.md @@ -1,509 +1,77 @@ -# RISC-V 多核启动 (SMP Boot) +# RISC-V SMP 启动设计 -本文档详细描述 Comix 内核在 RISC-V 架构上的多核启动实现,包括启动流程、Per-CPU 数据结构、tp 寄存器处理以及当前的限制。 +## 当前状态 -## 1. 概述 +RISC-V 使用 SBI HSM 启动从核.主核完成公共内核初始化后, 在 time 初始化之后启动从核; 从核建立自己的 per-CPU 状态,idle task,trap 和 timer, 然后进入 idle loop 等待调度. -### 1.1 目标 +当前调度器是 per-CPU 运行队列.任务唤醒时可以按 affinity 选择目标 CPU, 跨核唤醒通过 reschedule IPI 通知目标 CPU. -Comix 内核实现了基础的对称多处理(SMP)启动支持,能够在 QEMU RISC-V virt 平台上启动多个 CPU 核心。当前实现的主要目标是: +## 目标和非目标 -- 使用 SBI HSM (Hart State Management) 接口启动从核心 -- 建立 Per-CPU 数据结构,为每个核心提供独立的运行环境 -- 实现 tp 寄存器的保存与恢复(为内核 Per-CPU 访问和未来的用户态 TLS 做准备) -- 为未来的多核调度器奠定基础 +目标: -### 1.2 当前状态 +- 让所有在线 hart 都拥有独立 `Cpu` 状态和 idle task. +- 使用统一内核页表作为从核进入完整内核后的地址空间. +- 用在线位图向主核确认从核上线结果. +- 为 per-CPU 调度器和 IPI 唤醒提供启动基础. -- **支持核心数**:最多 8 个核心(由 `config.rs` 中的 `MAX_CPU_COUNT` 定义) -- **主核心**:hartid 0 负责所有初始化工作和任务调度 -- **从核心**:hartid 1-7 启动后进入 WFI 空挂状态,等待未来的多核调度器实现 -- **调度状态**:当前仅主核心运行任务,从核心暂不参与调度 +非目标: -## 2. 启动流程 +- 不在启动阶段做复杂负载均衡. +- 不假设所有请求启动的 hart 都一定成功上线. +- 不在 RISC-V 文档中描述 LoongArch SMP 行为. -### 2.1 主核心启动流程 +## 主核流程 -主核心(hartid 0)的启动流程如下: - -``` -entry.S (_start) - ↓ -清空 BSS 段 - ↓ -boot/mod.rs (main) - ↓ -初始化内存管理 (mm_init) - ↓ -初始化 CPUS 数组,设置 tp 指向 CPU 0 - ↓ -激活内核地址空间 (activate_kernel_space) - ↓ -初始化 trap、平台、时间 - ↓ -启动从核心 (boot_secondary_cpus) - ↓ -初始化定时器 - ↓ -启动第一个任务 (rest_init) -``` - -关键代码位于 [`os/src/arch/riscv/boot/mod.rs`](/os/src/arch/riscv/boot/mod.rs) 的 `main()` 函数(第 211-258 行): - -```rust -pub fn main(hartid: usize) { - // 1. 清空 BSS - clear_bss(); - - // 2. 初始化内存管理 - let kernel_space = mm::init(); - - // 3. 初始化 CPUS 并设置 tp 指向 CPU 0 - { - use crate::kernel::CPUS; - let cpu_ptr = &*CPUS.get_of(0) as *const _ as usize; - unsafe { - core::arch::asm!("mv tp, {}", in(reg) cpu_ptr); - } - } - - // 4. 激活内核地址空间并设置 current_memory_space - { - let _guard = crate::sync::PreemptGuard::new(); - current_cpu().switch_space(kernel_space); - } - - // 5. 初始化 trap、平台、时间 - trap::init_boot_trap(); - platform::init(); - time::init(); - - // 6. 启动从核心(在启用定时器中断之前) - let num_cpus = unsafe { NUM_CPU }; - if num_cpus > 1 { - boot_secondary_cpus(num_cpus); - } - - // 7. 初始化定时器(在从核心启动后) - timer::init(); - - // 8. 启动第一个任务 - rest_init(); -} -``` - -### 2.2 从核心启动流程 - -从核心的启动由主核心通过 SBI HSM 接口触发: - -``` -boot_secondary_cpus() [主核心] - ↓ -对每个从核心调用 sbi_hart_start() - ↓ -entry.S (secondary_sbi_entry) [从核心] - ↓ -设置页表、栈 - ↓ -secondary_start() [从核心] - ↓ -初始化 trap 处理 - ↓ -设置 tp 指向当前 CPU - ↓ -标记 CPU 在线 - ↓ -进入 WFI 循环 -``` - -#### 2.2.1 主核心侧:启动从核心 - -`boot_secondary_cpus()` 函数(第 388-439 行)负责启动所有从核心: - -```rust -pub fn boot_secondary_cpus(num_cpus: usize) { - if num_cpus <= 1 { - pr_info!("[SMP] Single CPU mode, skipping secondary boot"); - return; - } - - pr_info!("[SMP] Booting {} secondary CPUs...", num_cpus - 1); - - // 主核标记上线 - CPU_ONLINE_MASK.fetch_or(1, Ordering::Release); - - // 使用 SBI HSM 调用启动每个从核 - for hartid in 1..num_cpus { - let start_vaddr = secondary_sbi_entry as usize; - let start_paddr = unsafe { crate::arch::mm::vaddr_to_paddr(start_vaddr) }; - - let ret = crate::arch::lib::sbi::hart_start(hartid, start_paddr, hartid); - if ret.error != 0 { - pr_err!("[SMP] Failed to start hart {}: SBI error {}", hartid, ret.error); - } - } - - // 等待所有核心上线(带超时) - let expected_mask = (1 << num_cpus) - 1; - let mut timeout = 10_000_000; - - while CPU_ONLINE_MASK.load(Ordering::Acquire) != expected_mask { - if timeout == 0 { - let current_mask = CPU_ONLINE_MASK.load(Ordering::Acquire); - panic!( - "[SMP] Timeout waiting for secondary CPUs! Expected: {:#b}, got: {:#b}", - expected_mask, current_mask - ); - } - timeout -= 1; - core::hint::spin_loop(); - } - - pr_info!("[SMP] All {} CPUs are online!", num_cpus); -} -``` - -#### 2.2.2 从核心侧:初始化和空挂 - -从核心从 `entry.S` 的 `secondary_sbi_entry` 开始执行,经过页表和栈设置后,调用 `secondary_start()` 函数(第 333-374 行): - -```rust -pub extern "C" fn secondary_start(hartid: usize) -> ! { - // 1. 初始化 boot trap 处理 - trap::init_boot_trap(); - - // 2. 设置 tp 指向当前 CPU 的 Cpu 结构 - { - use crate::kernel::CPUS; - let cpu_ptr = &*CPUS.get_of(hartid) as *const _ as usize; - unsafe { - core::arch::asm!("mv tp, {}", in(reg) cpu_ptr); - } - } - - // 3. 标记 CPU 在线 - CPU_ONLINE_MASK.fetch_or(1 << hartid, Ordering::Release); - - pr_info!("[SMP] CPU {} is online", hartid); - - // 4. 禁用中断 - unsafe { - crate::arch::intr::disable_interrupts(); - } - - pr_info!("[SMP] CPU {} entering WFI loop with interrupts disabled", hartid); - - // 5. 进入 WFI 循环 - loop { - unsafe { - core::arch::asm!("wfi"); - } - } -} -``` - -### 2.3 启动同步机制 - -主核心和从核心之间通过原子变量 `CPU_ONLINE_MASK` 进行同步: - -```rust -/// CPU 在线掩码,第 i 位为 1 表示 CPU i 已上线 -static CPU_ONLINE_MASK: AtomicUsize = AtomicUsize::new(0); -``` - -- **从核心**:启动完成后,通过 `fetch_or` 设置自己的位 -- **主核心**:轮询 `CPU_ONLINE_MASK`,等待所有核心的位都被设置(带超时) - -## 3. Per-CPU 数据结构 - -### 3.1 Cpu 结构体 - -每个 CPU 核心都有一个独立的 `Cpu` 结构体,定义在 [`os/src/kernel/cpu.rs`](/os/src/kernel/cpu.rs): - -```rust -#[repr(C)] -pub struct Cpu { - pub cpu_id: usize, // 必须是第一个字段,用于快速访问 - pub current_task: Option, - pub current_memory_space: Option>>, -} +```text +riscv entry.S + -> riscv::boot::main + -> setup temporary tp + -> kernel::boot::run_primary_boot + -> mm::init + -> setup CPU0 tp + -> time::init + -> boot_secondaries + -> timer/trap/rest_init ``` -**设计要点**: -- `cpu_id` 必须是第一个字段,这样可以通过 `ld {}, 0(tp)` 快速读取 CPU ID -- 每个核心维护自己的当前任务和内存空间 - -### 3.2 CPUS 全局数组 +`tp` 在 MM 初始化前先指向一个临时值, MM 初始化后再指向 `CPUS[0]`.这保证公共代码通过 `current_cpu` 访问 per-CPU 数据时有稳定基础. -所有 CPU 的 `Cpu` 结构体存储在全局数组中: +## 从核流程 -```rust -static CPUS: PerCpu = PerCpu::new(); +```text +SBI HSM secondary_sbi_entry + -> secondary_start + -> init boot trap + -> set tp to CPUS[hartid] + -> mark online + -> create idle task + -> switch to global kernel space + -> trap::init + -> timer::init + -> enable interrupts + -> idle_loop ``` -`PerCpu` 是一个特殊的容器(定义在 [`os/src/sync/per_cpu.rs`](/os/src/sync/per_cpu.rs)),它: -- 为每个 CPU 分配独立的数据副本 -- 使用缓存行对齐(64 字节),避免伪共享(False Sharing) -- 提供安全的访问接口 - -### 3.3 tp 寄存器的使用 - -RISC-V 的 tp (Thread Pointer) 寄存器在 Comix 内核中有双重用途: - -| 模式 | tp 指向的内容 | 用途 | -|------------|------------------------------|-------------------------------------------| -| 内核模式 | 当前 CPU 的 `Cpu` 结构体指针 | 快速访问 Per-CPU 数据 | -| 用户模式 | 用户线程的 TLS 指针 | 预留给未来的用户态线程局部存储(当前仅保存/恢复) | - -**注意**:用户模式下的 TLS 功能当前仅在寄存器层面进行保存和恢复,完整的 TLS 支持尚未实现。详见第 4 节。 - -#### 3.3.1 快速访问 CPU ID - -由于 `cpu_id` 是 `Cpu` 结构体的第一个字段(偏移 0),可以通过一条指令读取: - -```rust -pub fn cpu_id() -> usize { - let id: usize; - unsafe { - core::arch::asm!("ld {}, 0(tp)", out(reg) id); - } - id -} -``` - -#### 3.3.2 访问当前 CPU - -```rust -pub fn current_cpu() -> &'static Cpu { - let ptr: usize; - unsafe { - core::arch::asm!("mv {}, tp", out(reg) ptr); - &*(ptr as *const Cpu) - } -} -``` - -## 4. tp 寄存器的保存与恢复 - -### 4.1 问题背景 - -在多核系统中,tp 寄存器需要同时满足两个需求: -1. **内核需求**:快速访问当前 CPU 的 Per-CPU 数据 -2. **用户需求**:为未来的用户态线程局部存储(TLS)预留支持 - -**重要说明**:当前实现**仅在寄存器层面**保存和恢复了 tp 的值,**并未实现完整的 TLS 功能**。完整的 TLS 实现还需要: -- 加载和解析 ELF 文件中的 TLS 段(`.tdata`, `.tbss`) -- 为每个线程分配 TLS 内存区域 -- 初始化 TLS 变量 -- 在线程创建时设置正确的 tp 值 - -当前的工作只是在 trap 入口和出口时切换 tp 的值,为未来的 TLS 支持奠定了寄存器层面的基础。 - -### 4.2 TrapFrame 中的 tp 字段 - -`TrapFrame` 结构体(定义在 [`os/src/arch/riscv/trap/trap_frame.rs`](/os/src/arch/riscv/trap/trap_frame.rs))包含两个与 tp 相关的字段: - -```rust -#[repr(C)] -pub struct TrapFrame { - // ... 其他寄存器 ... - pub x4_tp: usize, // 用户态 tp(偏移 32) - // ... 其他字段 ... - pub cpu_ptr: usize, // 内核态 tp,指向 Cpu 结构(偏移 272) -} -``` - -### 4.3 trap_entry.S 中的 tp 切换 - -在 trap 入口([`os/src/arch/riscv/trap/trap_entry.S`](/os/src/arch/riscv/trap/trap_entry.S),第 15-19 行),保存用户 tp 并加载内核 tp: - -```asm -trap_entry: - # a0 已经指向 TrapFrame(由 sscratch 提供) - - # 保存用户 tp 到 TrapFrame.x4_tp(偏移 32) - sd tp, 32(a0) - - # 加载内核 tp 从 TrapFrame.cpu_ptr(偏移 272) - ld tp, 272(a0) - - # ... 保存其他寄存器 ... -``` - -在 trap 返回时(`trap_return`),恢复用户 tp: - -```asm -trap_return: - # ... 恢复其他寄存器 ... - - # 恢复用户 tp 从 TrapFrame.x4_tp - ld tp, 32(a0) - - # 返回用户态 - sret -``` - -### 4.4 TrapFrame 初始化 - -为了确保 `cpu_ptr` 字段正确初始化,`TrapFrame::zero_init()` 会自动设置它: - -```rust -impl TrapFrame { - pub fn zero_init() -> Self { - let cpu_ptr = { - let _guard = PreemptGuard::new(); - crate::kernel::current_cpu() as *const _ as usize - }; - - TrapFrame { - // ... 所有字段初始化为 0 ... - cpu_ptr, // 设置为当前 CPU - } - } -} -``` - -对于内核线程,`set_kernel_trap_frame()` 还会将 `x4_tp` 设置为内核 tp: - -```rust -pub fn set_kernel_trap_frame(&mut self, entry: usize, arg: usize, kstack_base: usize) { - // ... 其他设置 ... - - self.x4_tp = { - let _guard = PreemptGuard::new(); - crate::kernel::current_cpu() as *const _ as usize - }; -} -``` - -## 5. 从核心空挂 - -### 5.1 为什么空挂 - -当前从核心启动后立即进入 WFI(Wait For Interrupt)循环,原因是: - -1. **调度器未就绪**:当前的调度器是单核设计,不支持多核任务分配 -2. **避免竞争**:如果从核心尝试运行任务,会与主核心产生数据竞争 -3. **节能**:WFI 指令让 CPU 进入低功耗状态,直到中断到来 - -### 5.2 为什么禁用中断 - -从核心在 WFI 循环前禁用中断(`disable_interrupts()`),原因是: - -1. **避免意外唤醒**:禁用中断后,WFI 不会被时钟中断等唤醒 -2. **简化状态**:从核心处于完全静止状态,不会执行任何代码 -3. **等待显式唤醒**:未来的多核调度器可以通过 IPI(处理器间中断)显式唤醒从核心 - -### 5.3 未来的多核调度器 - -要让从核心参与任务调度,需要实现: - -1. **Per-CPU 运行队列**:每个核心维护自己的就绪任务队列 -2. **负载均衡**:在核心之间分配任务,避免某些核心空闲 -3. **IPI 支持**:通过 IPI 唤醒空闲核心或请求任务迁移 -4. **TLB Shootdown**:修改页表后,通过 IPI 通知其他核心刷新 TLB -5. **锁优化**:减少锁竞争,使用 Per-CPU 数据结构 - -## 6. 关键代码路径 - -### 6.1 文件列表 - -| 文件路径 | 功能描述 | -|----------|----------| -| [`os/src/arch/riscv/boot/mod.rs`](/os/src/arch/riscv/boot/mod.rs) | 主核心和从核心的启动逻辑 | -| [`os/src/arch/riscv/boot/entry.S`](/os/src/arch/riscv/boot/entry.S) | 汇编入口点(`_start`, `secondary_sbi_entry`) | -| [`os/src/kernel/cpu.rs`](/os/src/kernel/cpu.rs) | `Cpu` 结构体和访问函数 | -| [`os/src/sync/per_cpu.rs`](/os/src/sync/per_cpu.rs) | `PerCpu` 容器实现 | -| [`os/src/arch/riscv/trap/trap_frame.rs`](/os/src/arch/riscv/trap/trap_frame.rs) | `TrapFrame` 结构体 | -| [`os/src/arch/riscv/trap/trap_entry.S`](/os/src/arch/riscv/trap/trap_entry.S) | trap 入口汇编(tp 切换) | -| [`os/src/arch/riscv/sbi.rs`](/os/src/arch/riscv/sbi.rs) | SBI HSM 接口封装 | -| [`os/src/config.rs`](/os/src/config.rs) | `MAX_CPU_COUNT` 配置 | - -### 6.2 关键函数 - -| 函数名 | 位置 | 功能 | -|--------|------|------| -| `main()` | `boot/mod.rs:223` | 主核心启动入口 | -| `boot_secondary_cpus()` | `boot/mod.rs:397` | 启动所有从核心 | -| `secondary_start()` | `boot/mod.rs:350` | 从核心启动入口 | -| `init_cpus()` | `kernel/cpu.rs` | 初始化 CPUS 数组 | -| `cpu_id()` | `kernel/cpu.rs` | 获取当前 CPU ID | -| `current_cpu()` | `kernel/cpu.rs` | 获取当前 CPU 结构 | -| `TrapFrame::zero_init()` | `trap/trap_frame.rs` | 初始化 TrapFrame | - -### 6.3 代码示例:启动 4 核系统 - -假设在 QEMU 中启动 4 核系统(`-smp 4`),启动过程如下: - -``` -时刻 T0: CPU 0 从 _start 开始执行 -时刻 T1: CPU 0 初始化内存、CPUS、页表 -时刻 T2: CPU 0 调用 boot_secondary_cpus() -时刻 T3: CPU 0 通过 SBI 启动 CPU 1, 2, 3 -时刻 T4: CPU 1 从 secondary_sbi_entry 开始执行 -时刻 T5: CPU 2 从 secondary_sbi_entry 开始执行 -时刻 T6: CPU 3 从 secondary_sbi_entry 开始执行 -时刻 T7: CPU 1 设置 tp,标记在线,进入 WFI -时刻 T8: CPU 2 设置 tp,标记在线,进入 WFI -时刻 T9: CPU 3 设置 tp,标记在线,进入 WFI -时刻 T10: CPU 0 检测到 CPU_ONLINE_MASK == 0b1111,继续初始化 -时刻 T11: CPU 0 启动第一个任务,开始调度 -``` - -## 7. 测试 - -多核启动的测试位于: -- [`os/src/arch/riscv/boot/mod.rs`](/os/src/arch/riscv/boot/mod.rs)(第 296-331 行) -- [`os/src/kernel/cpu.rs`](/os/src/kernel/cpu.rs)(第 108-196 行) - -主要测试项: -- `test_num_cpu`:验证 `NUM_CPU` 正确设置 -- `test_cpu_online_mask`:验证所有 CPU 都上线 -- `test_cpus_initialization`:验证 CPUS 数组初始化 -- `test_cpu_id`:测试 CPU ID 读取 -- `test_per_cpu_independence`:验证 Per-CPU 数据独立性 - -运行测试: -```bash -cd os && make test -``` - -## 8. 限制和未来工作 - -### 8.1 当前限制 - -- 最多支持 8 个核心 -- 从核心不参与任务调度 -- 没有负载均衡 -- 没有 IPI 支持 -- 没有 TLB Shootdown - -### 8.2 未来工作 +主核为每个目标 hart 调用 SBI HSM, 然后等待 `CPU_ONLINE_MASK` 达到预期值.超时不会阻塞系统启动, 内核会按实际上线 CPU 数继续运行. -1. **多核调度器** - - Per-CPU 运行队列 - - 任务迁移和负载均衡 - - 优先级调度 +## 并发和生命周期约束 -2. **IPI 支持** - - 唤醒空闲核心 - - TLB Shootdown - - 系统停机协调 +- 从核上线后必须先设置 `tp`, 再访问 `current_cpu`. +- 从核的第一个任务必须是本 CPU idle task. +- 从核切到全局内核页表后才能进入完整 trap/timer 运行期. +- 在线 CPU 数由实际上线位图决定, 不是单纯由配置上限决定. -3. **性能优化** - - 减少锁竞争 - - Per-CPU 缓存 - - NUMA 感知(如果硬件支持) +## 已知限制 -4. **调试支持** - - Per-CPU 日志缓冲区 - - 多核死锁检测 - - 性能计数器 +- 启动等待使用固定超时, 没有更复杂的错误恢复. +- 从核上线后先进入 idle, 任务迁移依赖后续 wakeup 和 IPI. +- CPU hotplug 不在当前设计范围内. -## 9. 参考资料 +## 源码索引 -- [SMP 与中断](../sync/smp_interrupts.md) - SMP 系统中的中断和并发问题 -- [Per-CPU 变量](../sync/per_cpu.md) - Per-CPU 数据结构的详细说明 -- [抢占控制](../sync/preempt.md) - 访问 Per-CPU 数据时的抢占保护 -- [RISC-V 寄存器](./riscv_register.md) - tp 寄存器的详细说明 -- [RISC-V SBI Specification](https://github.com/riscv-non-isa/riscv-sbi-doc) - SBI HSM 接口规范 +- `os/src/arch/riscv/boot/mod.rs`: 主核 hook,SBI HSM 启动,`secondary_start` 和在线位图. +- `os/src/kernel/boot.rs`: 公共启动,idle task 创建. +- `os/src/kernel/cpu.rs`: `CPUS` 和 per-CPU 状态. +- `os/src/kernel/scheduler/mod.rs`: per-CPU 调度器和 CPU 选择. +- `os/src/arch/riscv/ipi.rs`: reschedule IPI. diff --git a/document/arch/riscv/stack_layout.md b/document/arch/riscv/stack_layout.md index e5df167c..d1d3de9b 100644 --- a/document/arch/riscv/stack_layout.md +++ b/document/arch/riscv/stack_layout.md @@ -1,106 +1,66 @@ -# 用户程序栈布局(Stack Layout) - -本文档说明内核在为用户程序构造初始用户栈(execve / spawn)时的内存布局、不变量和实现注意点。将原来散落在代码中的长注释集中到文档,便于维护与校验。 - -## 概览(栈增长方向) -- RISC‑V / 本项目栈向下增长:栈顶为低地址,栈底为高地址。 -- 内核在构造用户栈时从高地址向低地址压入字符串、指针数组和 argc,然后把最终对齐的栈指针写入 TrapFrame.x2_sp(用户 sp)。 -- main 的调用约定(由内核在 TrapFrame 中设置): - - a0 = argc - - a1 = argv(指向指针数组的首地址) - - a2 = envp(指向指针数组的首地址) - -## 典型布局(高地址 → 低地址) -(注:示例中 argv.len() == 4, envp.len() == 3) - -(高地址 — 栈底) -+-----------------------+ -| ... | -+-----------------------+ -| "USER=john" | <-- envp[2] 指向这里 -+-----------------------+ -| "HOME=/home/john" | <-- envp[1] 指向这里 -+-----------------------+ -| "SHELL=/bin/bash" | <-- envp[0] 指向这里 -+-----------------------+ -| "hello world" | <-- argv[3] 指向这里 -+-----------------------+ -| "arg2" | <-- argv[2] 指向这里 -+-----------------------+ -| "arg1" | <-- argv[1] 指向这里 -+-----------------------+ -| "./prog" | <-- argv[0] 指向这里 -+-----------------------+ <-- 字符串存储区域开始 -| ... | -+-----------------------+ <-- 进入 main 时的栈指针 (sp) 附近 -| char* envp[3] (NULL) | -+-----------------------+ -| char* envp[2] | --> 指向上面的 "USER=john" -| char* envp[1] | --> 指向上面的 "HOME=/home/john" -| char* envp[0] | --> 指向上面的 "SHELL=/bin/bash" -+-----------------------+ -| char* argv[4] (NULL) | -+-----------------------+ -| char* argv[3] | --> 指向上面的 "hello world" -| char* argv[2] | --> 指向上面的 "arg2" -| char* argv[1] | --> 指向上面的 "arg1" -| char* argv[0] | --> 指向上面的 "./prog" -+-----------------------+ -| int argc | // 在本内核实现中通常通过寄存器 a0 传递 -+-----------------------+ -| Return Address | -+-----------------------+ <-- main 的栈帧开始 -(低地址 — 栈顶) - -## 要求与不变量 -- 指针与整数按机器字(usize)对齐;最终用户 sp 必须满足 ABI 要求(本项目要求 16 字节对齐)。 -- argv 与 envp 指针数组必须以 NULL 结尾:`argv[argc] == NULL`,`envp[n] == NULL`。 -- 所有字符串必须以 NUL (0) 结尾。 -- 内核写入用户栈时须确保用户地址可写: - - 要么在写之前已激活用户页表并临时允许 SUM(sstatus.SUM = 1), - - 要么通过封装的 copy_to_user 接口(推荐),将页面错误映射为 -EFAULT。 -- 在写字符串或指针前,必须保证目标页已被映射(MemorySpace::from_elf 应已完成映射),否则会触发页故障(Load/Store Page Fault)。 - -## 实现顺序建议(从高地址向低地址) -1. 将 current_sp 设为用户栈的“高地址”(stack_top)。 -2. 先按 reverse order 将 env 字符串写入(每个字符串后写 NUL),记录字符串地址到 env_ptrs。 -3. 再按 reverse order 将 argv 字符串写入,记录地址到 arg_ptrs。 -4. 按机器字对 current_sp 做对齐(word 对齐)。 -5. 写入 envp 的 NULL 终止器(写入 0)。 -6. 逆序写入 env_ptrs,使 envp[0] 在最低地址;记录 envp_vec_ptr。 -7. 写入 argv 的 NULL 终止器(写入 0)。 -8. 逆序写入 arg_ptrs,使 argv[0] 在最低地址;记录 argv_vec_ptr。 -9. 写入 argc(如果非寄存器传递)。 -10. 对最终 current_sp 做 ABI 对齐(16 字节),并将其作为用户 sp 写入 TrapFrame.x2_sp。 -11. 在 TrapFrame 中设置 sepc(入口 PC)、sstatus(SPP=U, SPIE=1)、kernel_sp、a0/a1/a2 等寄存器,并清零 ra(避免从用户态返回到内核)。 - -## 常见陷阱 -- 未对齐 pointer 数组或最终 sp:会导致 libc / 程序行为异常或非法指令错误。 -- 在尚未切换到用户页表或没有开启 SUM 的情况下直接向用户地址写入,会在 trap_entry 或 execve 过程中触发页面错误(Store/Load Page Fault)。解决办法:先 activate(new_space.root_ppn()),再 write;或使用 copy_to_user。 -- 将字符串或指针写到错误的地址(off-by-one)会破坏栈布局并难以调试。建议在测试中验证 argv/envp 指针能正确 deref。 -- 在构造堆栈时务必记录并使用写入时的实际虚拟地址(不要使用临时计算出的物理地址)。 - -## 安全建议与封装 -- 不要在多个位置散写 SUM 的设置/清除;把用户内存访问集中到 `user_mem::copy_to_user` / `copy_from_user`: - - 该函数负责开启 SUM、逐页写入并在失败时返回 Err(UserCopyError::Fault)。 -- 在 execve 路径中: - - 先构造 MemorySpace 并完成段映射(包含用户栈页)。 - - activate(new_space.root_ppn()) 切换页表(使内核可以通过 SUM 访问 U 页)。 - - 再执行栈构造与 TrapFrame 写入流程。 - -## 验证与测试 -- 单元测试中可提供 helper:`new_dummy_memory_space_with_stack()` 返回一个可写的 MemorySpace,便于验证栈布局写入后的读取正确性。 -- 在集成/仿真测试中: - - 验证用户入口处的指令字节非零; - - 在用户程序中读取 argv/envp 并打印,确认内核构造无误。 - -## 参考示例(伪代码) -```rust -// 假设 new_space 已激活 -let mut sp = stack_top; -for s in envp.iter().rev() { sp -= s.len()+1; write_user(sp, s); env_ptrs.push(sp); } -for s in argv.iter().rev() { sp -= s.len()+1; write_user(sp, s); arg_ptrs.push(sp); } -sp &= !(usize::BITS as usize/8 - 1); // word-align -// 写 envp NULL 与指针数组... -// 最终 sp 对齐到 16 字节后写入 TrapFrame.x2_sp +# 用户 exec 栈布局 + +本文位于 RISC-V 文档目录是历史原因.当前设计通过 `arch::task::ExecStackLayout` 抽象为跨架构接口, RISC-V 和 LoongArch 分别实现自己的用户栈和 TLS 细节. + +## 当前状态 + +`Task::execve` 调用架构 `setup_exec_stack_layout`, 得到初始用户栈指针,`argc`,`argv`,`envp` 和可选 TLS/thread pointer.随后通过 `HwTrapFrame::set_exec_trap_frame_from_layout` 写入架构 `TrapFrame`. + +共同语义: + +- 用户栈向低地址增长. +- `argv` 和 `envp` 字符串以 NUL 结尾. +- 指针数组以 NULL 结尾. +- 最终栈指针满足 ABI 对齐要求. +- auxv 至少提供动态链接器和 libc 启动所需的基本条目. + +RISC-V 当前不设置 TLS 值, LoongArch 会在用户栈顶部预留 TLS/TCB 区域并设置 `$tp`. + +## 目标和非目标 + +目标: + +- 为 `/sbin/init`,busybox 和动态链接器提供足够的 Linux ABI 启动栈. +- 把 RISC-V 和 LoongArch 的寄存器差异限制在各自 `kernel/task.rs` 和 `trap_frame.rs` 中. +- 让通用 exec 路径只处理 `ExecStackLayout`, 不解释架构寄存器. + +非目标: + +- 不在文档中维护 auxv 条目大全. +- 不为每种 libc 变体记录特例. +- 不在正式文档中保留长伪代码. + +## 关键流程 + +```text +exec loader builds MemorySpace + -> activate or otherwise make user stack writable + -> arch setup_exec_stack_layout + -> Task::execve stores new MemorySpace + -> HwTrapFrame::set_exec_trap_frame_from_layout + -> forkret_restore returns to user entry ``` + +栈内容从高地址向低地址写入.实现通常先写字符串和少量平台数据, 再写 auxv,envp,argv 和 argc 区域.通用代码只依赖返回的 `ExecStackLayout`, 不依赖实际排布顺序. + +## 并发和生命周期约束 + +- 写用户栈前必须确保目标用户页已映射且内核可以访问. +- RISC-V 直接写用户栈时需要临时开启 SUM; LoongArch 通过地址空间翻译后写入. +- `argv`,`envp`,auxv 指针必须全部指向新地址空间内的用户地址. +- `TrapFrame` 写入必须在任务私有保存区内完成, 不能复用旧用户现场. + +## 已知限制 + +- auxv 内容是 Linux ABI 子集, 不是完整内核实现. +- RISC-V TLS 仍为空值, 后续线程 TLS 支持需要补齐. +- LoongArch TLS 布局是满足当前 libc 启动的最小实现, 后续可与更完整 ABI 文档对齐. + +## 源码索引 + +- `os/src/arch/task.rs`: `ExecStackLayout` 跨架构返回结构. +- `os/src/kernel/task/task_struct.rs`: `Task::execve` 调用栈布局并写入 `TrapFrame`. +- `os/src/arch/riscv/kernel/task.rs`: RISC-V 用户栈布局. +- `os/src/arch/loongarch/kernel/task.rs`: LoongArch 用户栈和 TLS 布局. +- `os/src/arch/riscv/trap/trap_frame.rs`: RISC-V exec `TrapFrame` 写入. +- `os/src/arch/loongarch/trap/trap_frame.rs`: LoongArch exec `TrapFrame` 写入. diff --git a/document/book.toml b/document/book.toml new file mode 100644 index 00000000..718bf927 --- /dev/null +++ b/document/book.toml @@ -0,0 +1,8 @@ +[book] +title = "Comix Documentation" +language = "zh-CN" +src = "." + +[build] +build-dir = "../target/mdbook-document" +create-missing = false diff --git a/document/devices/README.md b/document/devices/README.md index 0e1de5cf..e02f9569 100644 --- a/document/devices/README.md +++ b/document/devices/README.md @@ -1,71 +1,107 @@ -# 设备与驱动概览 +# 设备与存储路径 -面向内核贡献者的设备子系统说明,覆盖驱动模型、设备树探测、VirtIO 适配、块/网/控制台/RTC 等核心组件。架构与代码入口位于 os/src/device/ 下。 +设备层为 Comix 提供驱动注册, 设备树探测, VirtIO 传输, 块设备, 网络, 控制台, RTC 等基础能力. 对文档同步最关键的是存储路径: 块设备和分区被枚举为 `vda`, `vda1`, `vda2` 等名字, 上层 FS 再从这些候选中探测 rootfs 或挂载 VFAT 测试分区. -## 驱动模型与注册表 +## 当前状态 -- 抽象:所有驱动实现 `Driver`,统一提供 `try_handle_interrupt`、`device_type`、`get_id`,并通过可选的 `as_block/as_net/as_rtc/as_serial` 返回具体接口。 -- 注册:全局表 `DRIVERS/BLK_DRIVERS/RTC_DRIVERS/SERIAL_DRIVERS`(见 os/src/device/mod.rs)存放已初始化驱动;`register_driver()` 用于统一登记并在中断路径可遍历。 -- 中断派发:`IRQ_MANAGER`(根级)基于中断号或全局列表调用驱动的 `try_handle_interrupt`(见 os/src/device/irq/mod.rs)。 +- 所有驱动实现 `Driver`, 按类型可向下暴露 `BlockDriver`, `NetDevice`, `RtcDriver`, `SerialDriver`. +- 全局注册表包括 `DRIVERS`, `BLK_DRIVERS`, `RTC_DRIVERS`, `SERIAL_DRIVERS`. +- 设备树初始化先处理中断控制器, 再处理普通设备. +- VirtIO MMIO 是主要设备传输路径, PCI 也有部分驱动入口. +- 块设备支持整盘和 MBR/GPT 分区包装. +- sysfs 通过设备注册表构建 `/sys/class/*`, FS 初始化通过同一设备列表创建 `/dev` 节点. -## 设备树探测流程 +## 目标 -- 引导期将 DTP 指针指向内核可见的 FDT,`device_tree::init()` 解析 CPU/时钟/内存信息并读取 `bootargs`(见 os/src/device/device_tree.rs)。 -- `DEVICE_TREE_REGISTRY` 按 `compatible` 注册探测函数;初始化时分两轮遍历:先初始化中断控制器,再初始化其他设备。 -- `DEVICE_TREE_INTC` 保存 phandle→中断控制器驱动映射,供设备解析其 `interrupts` 属性时使用。 +- 为 FS 层提供稳定的块设备抽象. +- 为 VFS 设备文件提供字符/块设备驱动入口. +- 让 rootfs 探测不依赖固定 virtio 设备枚举顺序. +- 让 VFAT/FAT 测试分区能作为普通分区块设备被挂载和卸载. -## 中断控制器:PLIC +## 非目标 -- 驱动位于 os/src/device/irq/plic.rs,使用 MMIO 寄存器完成 claim/complete。 -- 初始化:在 device tree 中匹配 `riscv,plic0`,映射 MMIO,注册到根 `IRQ_MANAGER` 的 `SUPERVISOR_EXTERNAL` 路径。 -- 提供 `IntcDriver::register_local_irq` 辅助驱动将中断号与处理器上下文绑定。 +- 不在设备文档中描述具体文件系统格式. +- 不承诺完整热插拔设备管理. +- 不复制每个驱动寄存器和队列实现. -## 总线与 VirtIO 传输 +## 模块边界 -- bus/virtio_mmio.rs、bus/pcie.rs 提供传输层占位/适配(目前主要使用 VirtIO MMIO)。 -- VirtIO 设备驱动共享 `VirtIOHal`(os/src/device/virtio_hal.rs)作为 DMA/内存屏障实现。 +- `device/mod.rs`: 驱动 trait 和全局注册表. +- `device_tree.rs`: FDT 解析, bootargs, compatible 到 probe 的分发. +- `bus/virtio_mmio.rs`: VirtIO MMIO transport 探测和设备类型分发. +- `virtio_hal.rs`: virtio-drivers 使用的 DMA/MMIO HAL. +- `block/mod.rs`: `BlockDriver` 接口. +- `block/virtio_blk.rs`: virtio block 整盘设备. +- `block/partition.rs`: MBR/GPT 分区发现和 `PartitionBlockDevice`. +- `console/`, `serial/`, `rtc/`, `net/`: 字符, 时间和网络设备来源. +- `fs/sysfs/device_registry.rs`: 设备列表投影到 sysfs 和 FS 初始化. -## 块设备 +## 存储关键流程 -- 接口:`BlockDriver` trait(os/src/device/block/mod.rs)定义读写/flush/块大小/容量。 -- RAMDisk:`ram_disk.rs`,纯内存实现,用于测试或引导阶段;无中断,支持读写与容量查询。 -- VirtIO-Block:`virtio_blk.rs`,基于 virtio-drivers 的 `VirtIOBlk`;初始化后注册到 `DRIVERS`、`BLK_DRIVERS`、`IRQ_MANAGER`。块大小 512 字节,容量由设备报告。 -- 文件系统集成:VFS/ext4 通过 `BlockDriver` 适配层访问块设备,首次构建会生成 ext4 镜像 `fs.img` 并通过 virtio-blk 挂载。 +### 设备发现 -## 网络设备 +```text +device_tree init + -> virtio mmio probe + -> virtio block init + -> BLK_DRIVERS push whole disk + -> sysfs device_registry list_block_devices + -> discover partitions + -> vda, vda1, vda2 ... +``` -- 接口:`NetDevice` trait(os/src/device/net/net_device.rs)提供 send/receive/MTU/MAC 信息。 -- VirtIO-Net:`virtio_net.rs` 使用 `VirtioNetDevice` 包装 virtio-drivers 的实现,默认 MTU 1500。`init()` 创建设备后同时: - - 加入 `NETWORK_DEVICES` 列表; - - 创建 `NetworkInterface`(os/src/net/interface.rs)并注册到接口管理器; - - 通过 `register_driver` 让 IRQ 路径可见。 +`BLK_DRIVERS` 只保存整盘驱动. 分区设备是在 `list_block_devices` 时根据分区表动态包装出来的逻辑块设备. -## 控制台与串口 +### rootfs selection -- 接口:`Console` trait(os/src/device/console/mod.rs)提供读写/flush;`CONSOLES` 与 `MAIN_CONSOLE` 管理活动控制台。 -- 实现:`uart_console.rs`、`frame_console.rs`(后者可用于图形帧缓冲输出)。串口驱动还可通过 `SerialDriver`(os/src/device/serial/mod.rs)统一暴露给 VFS/日志。 +```text +list block devices + -> prefer partition names + -> FS tries ext4 + -> accept if /bin/sh or /bin/ash exists +``` -## RTC 与时间 +默认分区盘设计是 ext4 rootfs 和 VFAT 测试分区共存. 一般情况下 ext4 rootfs 在 `vda1`, VFAT/FAT 测试分区在 `vda2`, 但代码以内容探测为准, 不写死设备名. -- RTC 驱动接口在 os/src/device/rtc/mod.rs,当前实现 `rtc_goldfish.rs` 对接 virtio/goldfish RTC(用于墙钟时间/定时)。 +### VFAT test partition -## 其他占位 +VFAT/FAT 分区通过同一 `BlockDriver` 路径进入 `os/src/fs/vfat/`. 它用于 mount/umount, statfs, flush 和跨文件系统路径解析测试, 不参与默认 rootfs 选择. -- GPU:`gpu/virtio_gpu.rs` 占位实现,提供未来图形输出路径。 -- 输入:`input/virtio_input.rs` 占位,为键鼠/触摸设备预留。 -- IRQ:`irq/mod.rs` 定义通用中断管理逻辑,除 PLIC 外可扩展本地或板级控制器。 +### `/dev` and `/sys` -## 初始化顺序(概览) +```text +list_block_devices + -> /sys/class/block entries + -> /dev block nodes +``` -1. device_tree::init() 解析 FDT,注册 compatible→init 钩子。 -2. 先初始化中断控制器(如 PLIC),完成根 IRQ 管理器设置。 -3. 逐个设备匹配 compatible:VirtIO-MMIO → virtio-blk / virtio-net / virtio-gpu / virtio-input / virtio-console 等。 -4. 驱动完成自注册:加入 DRIVERS/子列表,必要时在 IRQ_MANAGER 中登记。 -5. 上层子系统使用对应 trait(BlockDriver/NetDevice/Console/SerialDriver/RTC)完成挂载与服务暴露。 +这保证用户态能通过 `/sys/class/block/vda1` 观察分区, 也能通过 `/dev/vda1` 作为 mount source 使用. -## 调试提示 +## 并发和生命周期约束 -- 查看已注册驱动:在调试或日志中读取 DRIVERS/BLK_DRIVERS/NETWORK_DEVICES 等全局表。 -- 中断无法响应:确认 PLIC `register_local_irq` 是否被调用、IRQ 号与设备树一致、`IRQ_MANAGER.try_handle_interrupt` 返回路径。 -- 块设备异常:检查 `fs.img` 是否生成、块大小与 `config::VIRTIO_BLK_SECTOR_SIZE` 保持一致。 -- 网络收发异常:确认 virtio-net 设备已添加到接口管理器、MTU 未超限,并检查队列是否因 `QueueFull/QueueEmpty` 返回错误。 +- 驱动注册表初始化后主要读多写少. +- `PartitionBlockDevice` 持有底层整盘 `Arc`, 不复制数据. +- 分区读写会检查逻辑块范围, 再偏移到底层整盘块号. +- VirtIO 设备通过内部锁串行化驱动对象访问, IRQ 路径通过 `IRQ_MANAGER` 分发. +- DMA allocation 由 `VirtIOHal` 记录物理帧范围, 释放时注意锁顺序. + +## 已知限制 + +- 设备热插拔后 `/dev` 和 `/sys` 不是自动增量更新模型. +- 分区解析支持 primary MBR 和基础 GPT 条目, 不覆盖所有复杂分区格式. +- 分区发现假设 512 字节扇区. +- 多块设备命名使用 `vda`, `vdb` 顺序, 仍应避免在 rootfs 策略中硬编码. + +## 源码索引 + +- `os/src/device/mod.rs`: 驱动模型和注册表. +- `os/src/device/device_tree.rs`: FDT 初始化和 probe 分发. +- `os/src/device/bus/virtio_mmio.rs`: VirtIO MMIO 设备识别. +- `os/src/device/virtio_hal.rs`: DMA/MMIO HAL. +- `os/src/device/block/mod.rs`: `BlockDriver`. +- `os/src/device/block/virtio_blk.rs`: virtio block 驱动. +- `os/src/device/block/partition.rs`: MBR/GPT 分区和分区块设备. +- `os/src/fs/sysfs/device_registry.rs`: 块设备和分区枚举. +- `os/src/fs/mod.rs`: rootfs 探测和 `/dev` 节点创建. +- `os/src/fs/vfat/`: VFAT/FAT 分区挂载路径. +- `os/src/fs/ext4/`: ext4 rootfs 挂载路径. diff --git a/document/fs/README.md b/document/fs/README.md index f5fb8d40..4eadcd85 100644 --- a/document/fs/README.md +++ b/document/fs/README.md @@ -1,536 +1,97 @@ -# 文件系统模块 (FS) +# FS 文件系统实现层 -## 概述 +FS 层提供 Comix 当前可挂载的具体文件系统. VFS 负责路径, fd 和挂载表; FS 负责实现 `FileSystem` 和 `Inode`, 并把内存结构, 块设备或动态内核状态映射成文件树. -FS 模块是 comix 内核的文件系统实现层,提供了多种具体的文件系统类型。这些文件系统通过实现 VFS 的 `FileSystem` 和 `Inode` trait,与虚拟文件系统层无缝集成。 +本文档只同步设计和边界. 具体 API, 字段和错误分支以 `os/src/fs/` 源码和 rustdoc 为准. -## 支持的文件系统 +## 当前状态 -comix 内核目前支持以下文件系统类型: +| 文件系统 | 源码路径 | 主要用途 | 存储来源 | +| --- | --- | --- | --- | +| ext4 | `os/src/fs/ext4/` | 默认 rootfs 候选, 持久化读写 | 块设备或分区 | +| VFAT/FAT | `os/src/fs/vfat/` | FAT/VFAT mount 兼容路径, mount/umount 测试分区 | 块设备或分区 | +| tmpfs | `os/src/fs/tmpfs/` | `/tmp` 等临时目录 | 内存页 | +| procfs | `os/src/fs/proc/` | `/proc` 进程和系统快照 | 动态生成 | +| sysfs | `os/src/fs/sysfs/` | `/sys` 设备和内核属性 | 设备注册表 | +| simple_fs | `os/src/fs/simple_fs.rs` | rootfs fallback 和测试镜像 | 编译期嵌入 ramdisk | -| 文件系统 | 类型 | 用途 | 持久化 | 特点 | -|---------|------|------|--------|------| -| [Tmpfs](tmpfs.md) | 内存 | 临时存储 | ❌ | 快速、容量可配置 | -| [ProcFS](procfs.md) | 伪文件系统 | 进程信息 | ❌ | 动态生成、只读 | -| [SysFS](sysfs.md) | 伪文件系统 | 系统设备 | ❌ | 设备树、属性导出 | -| [Ext4](ext4.md) | 磁盘 | 持久化存储 | ✅ | Linux标准、完整读写 | -| [SimpleFS](simple_fs.md) | 测试 | 调试/测试 | ❌ | 预加载镜像 | +注意: 当前 FAT 相关源码目录是 `os/src/fs/vfat/`, 文档和索引不应再使用过期 FAT 目录名. -## 文件系统特性对比 +## 目标 -### 性能特性 +- 为 VFS 提供多个可互换的文件系统实现. +- 让 ext4 rootfs 自动从已发现块设备和分区中选择. +- 让 procfs/sysfs/tmpfs 在 rootfs 之上提供运行时文件树. +- 让 VFAT/FAT 分区用于 mount/umount 和兼容性测试, 不作为默认 rootfs. -```mermaid -graph LR - A[读写性能] --> B[Tmpfs: 最快] - A --> C[ProcFS/SysFS: 动态生成] - A --> D[Ext4: 块设备速度] - A --> E[SimpleFS: 内存访问] - - style B fill:#90EE90 - style C fill:#FFD700 - style D fill:#87CEEB - style E fill:#DDA0DD -``` - -### 使用场景 - -#### Tmpfs - 临时文件存储 -```rust -// 适用场景: -// - /tmp 目录 -// - 进程间共享内存 -// - 构建系统的临时产物 - -mount_tmpfs("/tmp", 64)?; // 挂载64MB tmpfs到/tmp -``` - -**优点**: -- 读写速度快(纯内存操作) -- 容量可配置 -- 支持完整的POSIX语义 - -**限制**: -- 重启后数据丢失 -- 占用内核内存 - -#### ProcFS - 进程信息导出 -```rust -// 适用场景: -// - 进程状态查询 -// - 系统监控工具 -// - 调试和诊断 - -// 读取进程信息 -let stat = read_to_string("/proc/1/stat")?; -let status = read_to_string("/proc/self/status")?; -``` - -**优点**: -- 标准Linux接口 -- 动态生成,无存储开销 -- 易于扩展新条目 - -**限制**: -- 只读文件系统 -- 数据实时性依赖内核状态 - -#### SysFS - 设备信息导出 -```rust -// 适用场景: -// - 设备发现 -// - 驱动参数配置 -// - 设备状态监控 - -// 查询设备信息 -let class = read_to_string("/sys/class/block/vda/dev")?; -``` +## 非目标 -**优点**: -- 统一的设备接口 -- 支持设备热插拔 -- 层次化设备树 +- 不在 FS 文档中维护完整 trait 实现清单. +- 不复述第三方库内部结构. +- 不承诺每个 Linux 文件系统高级特性均实现. -**限制**: -- 只读(当前实现) -- 需要设备驱动支持 - -#### Ext4 - 持久化存储 -```rust -// 适用场景: -// - 根文件系统 -// - 用户数据存储 -// - 配置文件持久化 - -init_ext4_from_block_device()?; // 挂载ext4为根文件系统 -``` - -**优点**: -- 数据持久化 -- Linux标准格式 -- 可与Linux主机交换数据 - -**限制**: -- 需要块设备支持 -- 性能受硬件限制 -- 部分高级特性未支持(mknod等) - -#### SimpleFS - 测试与调试 -```rust -// 适用场景: -// - 单元测试 -// - 快速原型 -// - 预加载测试数据 - -init_simple_fs()?; // 从编译时嵌入的镜像加载 -``` - -**优点**: -- 镜像编译时嵌入 -- 快速启动 -- 测试环境一致 - -**限制**: -- 只读 -- 镜像大小受限 -- 仅用于测试 - -## 架构设计 - -### FS层与VFS层的关系 - -```mermaid -graph TB - subgraph "系统调用层" - A[sys_open/read/write] - end - - subgraph "VFS层" - B[vfs_lookup] - C[File trait] - D[Inode trait] - E[FileSystem trait] - end - - subgraph "FS实现层" - F[TmpFs] - G[ProcFS] - H[SysFS] - I[Ext4] - J[SimpleFS] - end - - subgraph "设备层" - K[BlockDriver] - L[CharDriver] - end - - A --> B - B --> C - C --> D - D --> E - - E --> F - E --> G - E --> H - E --> I - E --> J - - I --> K - F -.内存.-> M[RamDisk] - J -.内存.-> M - - style E fill:#FFD700 - style F fill:#90EE90 - style G fill:#87CEEB - style H fill:#DDA0DD - style I fill:#FFA07A - style J fill:#F0E68C -``` +## 初始化和 rootfs 探测 -### 文件系统初始化流程 +当前默认路径是分区盘探测: -```rust -pub fn init_filesystems() -> Result<(), FsError> { - // 1. 挂载根文件系统 (Ext4 或 SimpleFS) - #[cfg(feature = "ext4")] - init_ext4_from_block_device()?; - - #[cfg(not(feature = "ext4"))] - init_simple_fs()?; - - // 2. 创建必要的目录 - let root = vfs::get_root_dentry()?; - root.inode.mkdir("dev", FileMode::S_IFDIR | FileMode::S_IRWXU)?; - root.inode.mkdir("proc", FileMode::S_IFDIR | FileMode::S_IRWXU)?; - root.inode.mkdir("sys", FileMode::S_IFDIR | FileMode::S_IRWXU)?; - root.inode.mkdir("tmp", FileMode::S_IFDIR | FileMode::S_IRWXU)?; - - // 3. 挂载伪文件系统 - init_procfs()?; - init_sysfs()?; - - // 4. 挂载tmpfs到/tmp - mount_tmpfs("/tmp", 64)?; // 64MB - - // 5. 初始化设备文件 - init_dev()?; - - pr_info!("All filesystems initialized successfully"); - Ok(()) -} +```text +device discovery + -> list block disks and partitions + -> sort partitions before whole disks + -> try ext4 open + -> mount temporarily at / + -> accept only if /bin/sh or /bin/ash exists + -> create common mount dirs + -> mount procfs, sysfs, tmpfs and create /dev nodes ``` -## 快速开始 - -### 挂载文件系统 - -#### 挂载Tmpfs - -```rust -use crate::fs::mount_tmpfs; - -// 挂载64MB的tmpfs到/tmp -mount_tmpfs("/tmp", 64)?; - -// 无限制大小的tmpfs -mount_tmpfs("/run", 0)?; -``` - -#### 挂载ProcFS - -```rust -use crate::fs::init_procfs; - -// 挂载procfs到/proc -init_procfs()?; - -// 读取进程状态 -let stat = vfs_load_file("/proc/1/stat")?; -``` - -#### 挂载SysFS - -```rust -use crate::fs::init_sysfs; - -// 挂载sysfs到/sys -init_sysfs()?; - -// 查询块设备 -let dev = vfs_load_file("/sys/class/block/vda/dev")?; -``` - -### 文件操作示例 - -所有文件系统都通过VFS层统一操作: - -```rust -// 打开文件(无论什么文件系统) -let fd = sys_open("/tmp/test.txt", - OpenFlags::O_WRONLY | OpenFlags::O_CREAT, - FileMode::S_IRUSR | FileMode::S_IWUSR)?; - -// 写入数据 -sys_write(fd, b"Hello, FS!")?; -sys_close(fd)?; - -// 读取数据 -let fd = sys_open("/tmp/test.txt", OpenFlags::O_RDONLY, FileMode::empty())?; -let mut buf = vec![0u8; 128]; -let n = sys_read(fd, &mut buf)?; -sys_close(fd)?; - -println!("Read {} bytes: {}", n, String::from_utf8_lossy(&buf[..n])); -``` - -## 实现新文件系统 - -### 步骤1:实现FileSystem trait - -```rust -use alloc::sync::Arc; -use crate::vfs::{FileSystem, Inode, FsError, StatFs}; - -pub struct MyFS { - root: Arc, -} - -impl FileSystem for MyFS { - fn fs_type(&self) -> &'static str { - "myfs" - } - - fn root_inode(&self) -> Arc { - self.root.clone() - } - - fn sync(&self) -> Result<(), FsError> { - // 同步数据到持久化存储 - Ok(()) - } - - fn statfs(&self) -> Result { - Ok(StatFs { - block_size: 4096, - total_blocks: 1000, - free_blocks: 500, - // ... - }) - } -} -``` - -### 步骤2:实现Inode trait - -```rust -struct MyFsInode { - inode_no: usize, - inode_type: InodeType, - // ... 其他字段 -} - -impl Inode for MyFsInode { - fn metadata(&self) -> Result { - // 返回文件元数据 - } - - fn read_at(&self, offset: usize, buf: &mut [u8]) - -> Result { - // 读取文件数据 - } - - fn write_at(&self, offset: usize, buf: &[u8]) - -> Result { - // 写入文件数据 - } - - fn lookup(&self, name: &str) -> Result, FsError> { - // 查找子文件/目录 - } - - // ... 实现其他必需方法 -} -``` - -### 步骤3:注册和挂载 - -```rust -pub fn init_myfs() -> Result<(), FsError> { - let myfs = Arc::new(MyFS::new()); - - MOUNT_TABLE.mount( - myfs, - "/mnt/myfs", - MountFlags::empty(), - Some(String::from("myfs")), - )?; - - Ok(()) -} -``` - -## 配置选项 - -### 编译时配置 - -在 `Cargo.toml` 中配置特性: - -```toml -[features] -default = ["ext4"] -ext4 = [] -devfs = [] -``` - -### 运行时配置 - -```rust -// config.rs - -/// Tmpfs 默认最大大小 (MB) -pub const TMPFS_DEFAULT_SIZE: usize = 64; - -/// Ext4 块大小 -pub const EXT4_BLOCK_SIZE: usize = 4096; - -/// SimpleFS 镜像路径 -pub const SIMPLE_FS_IMAGE: &str = env!("SIMPLE_FS_IMAGE"); -``` - -## 调试与监控 - -### 查看挂载点 - -```rust -// 列出所有挂载点 -let mounts = MOUNT_TABLE.list_mounts(); -for (path, fstype) in mounts { - pr_info!("Mount point: {}, Type: {}", path, fstype); -} -``` - -### 文件系统统计 - -```rust -// 获取文件系统统计信息 -let root = vfs::get_root_dentry()?; -let statfs = root.inode.fs()?.statfs()?; - -pr_info!("Block size: {}", statfs.block_size); -pr_info!("Total blocks: {}", statfs.total_blocks); -pr_info!("Free blocks: {}", statfs.free_blocks); -``` - -## 性能优化 - -### Tmpfs优化建议 - -1. **合理设置容量限制**: 避免无限制使用导致OOM -2. **及时清理临时文件**: 释放内存 -3. **大文件使用块设备**: Tmpfs适合小文件 - -### Ext4优化建议 - -1. **块大小对齐**: 确保DMA传输对齐 -2. **预读策略**: 顺序读取时启用预读 -3. **缓存管理**: 合理使用页缓存 - -### ProcFS/SysFS优化建议 - -1. **延迟生成**: 只在读取时生成内容 -2. **缓存静态数据**: 不变的数据可以缓存 -3. **批量读取**: 减少系统调用次数 - -## 最佳实践 - -### 1. 选择合适的文件系统 - -- **临时数据** → Tmpfs -- **持久化数据** → Ext4 -- **进程信息** → ProcFS -- **设备信息** → SysFS -- **测试环境** → SimpleFS - -### 2. 错误处理 - -```rust -match init_ext4_from_block_device() { - Ok(_) => pr_info!("Ext4 mounted successfully"), - Err(FsError::NoDevice) => { - pr_warn!("No block device, fallback to SimpleFS"); - init_simple_fs()?; - } - Err(e) => return Err(e), -} -``` - -### 3. 资源管理 - -```rust -// 及时卸载不需要的文件系统 -MOUNT_TABLE.umount("/mnt/temp")?; - -// 同步数据到持久化存储 -vfs::sync_all()?; -``` - -## 故障排查 - -### 常见问题 - -#### 挂载失败 - -**问题**: `init_ext4_from_block_device()` 返回 `NoDevice` - -**原因**: 没有可用的块设备 - -**解决**: 检查块设备驱动是否正确初始化 - -```rust -let drivers = BLK_DRIVERS.read(); -pr_info!("Found {} block devices", drivers.len()); -``` - -#### 文件不存在 - -**问题**: 读取ProcFS文件返回 `NotFound` - -**原因**: 进程不存在或文件未注册 - -**解决**: 检查进程ID,确认Generator已注册 +默认运行镜像预期是分区盘: `vda1` 一般承载 ext4 rootfs, `vda2` 预留给 VFAT/FAT mount/umount 测试. 代码不依赖固定顺序, 而是按内容探测 rootfs. 如果无法找到含 shell 的 ext4 rootfs, 才回退到编译期嵌入的 simple_fs. -#### 权限拒绝 +rootfs 选中后会确保 `/dev`, `/proc`, `/sys`, `/tmp`, `/mnt`, `/tests` 等顶层挂载点存在. `/dev` 节点随后根据设备注册表创建, 包括整盘和分区块设备. -**问题**: 写入文件返回 `PermissionDenied` +## 模块边界 -**原因**: 文件系统只读或权限不足 +- `fs/mod.rs`: 文件系统初始化, rootfs 探测, tmpfs/procfs/sysfs 挂载, `/dev` 节点创建. +- `ext4/`: ext4_rs 适配, root inode 和 ext4 inode 操作. +- `vfat/`: fatfs 适配, VFAT/FAT 文件树接入 VFS. +- `tmpfs/`: 内存页和 inode 统计. +- `proc/`: 动态 generator 和进程路径. +- `sysfs/`: 设备注册表到 `/sys` 的冷插拔树. +- `simple_fs.rs`: 嵌入式只读镜像和 fallback rootfs. +- `smfs.rs`: 简单内存文件系统实验路径. -**解决**: 检查文件系统类型和挂载标志 +## 并发和生命周期约束 -## 相关资源 +- 所有 FS 实例通过 `Arc` 进入 mount table. +- inode 生命周期通常由 dentry 和 open file 间接持有. +- 块设备 FS 必须把底层驱动错误转换成 `FsError`. +- rootfs probe 会临时挂载候选 ext4, 不符合条件时卸载并清空当前任务 root/cwd 与 dentry cache. +- VFAT 当前串行化对底层 `fatfs` 的访问, 以匹配库的打开和 unmount 模型. -### 文档导航 +## 已知限制 -- [VFS 架构](../vfs/architecture.md) -- [Tmpfs 详解](tmpfs.md) -- [ProcFS 详解](procfs.md) -- [SysFS 详解](sysfs.md) -- [Ext4 详解](ext4.md) -- [SimpleFS 详解](simple_fs.md) +- rootfs 只从 ext4 候选中选择, VFAT 分区用于测试和兼容挂载. +- ext4 不覆盖完整 journaling 和全部 Linux 高级特性. +- procfs/sysfs 当前以只读和冷插拔为主. +- simple_fs 是只读 fallback, 不是生产 rootfs 格式. -### 源代码位置 +## 文档导航 -- **FS 模块**: `os/src/fs/` -- **Tmpfs**: `os/src/fs/tmpfs/` -- **ProcFS**: `os/src/fs/proc/` -- **SysFS**: `os/src/fs/sysfs/` -- **Ext4**: `os/src/fs/ext4/` -- **SimpleFS**: `os/src/fs/simple_fs.rs` +- [ext4.md](ext4.md): ext4 rootfs 和块设备适配. +- [vfat.md](vfat.md): VFAT/FAT mount 兼容路径. +- [tmpfs.md](tmpfs.md): 内存临时文件系统. +- [procfs.md](procfs.md): `/proc` 动态文件树. +- [sysfs.md](sysfs.md): `/sys` 设备视图. +- [simple_fs.md](simple_fs.md): 编译期嵌入 fallback 文件系统. -### 参考标准 +## 源码索引 -- [Linux VFS Documentation](https://www.kernel.org/doc/html/latest/filesystems/vfs.html) -- [Ext4 Disk Layout](https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout) -- [Linux /proc Filesystem](https://www.kernel.org/doc/html/latest/filesystems/proc.html) -- [Linux /sys Filesystem](https://www.kernel.org/doc/html/latest/filesystems/sysfs.html) +- `os/src/fs/mod.rs`: FS 初始化和 rootfs 探测入口. +- `os/src/fs/ext4/`: ext4 implementation. +- `os/src/fs/vfat/`: VFAT/FAT implementation. +- `os/src/fs/tmpfs/`: tmpfs implementation. +- `os/src/fs/proc/`: procfs implementation. +- `os/src/fs/sysfs/`: sysfs implementation and device registry. +- `os/src/fs/simple_fs.rs`: simple fs fallback. +- `os/src/device/block/partition.rs`: MBR/GPT 分区块设备. +- `os/src/vfs/mount.rs`: 挂载表消费 FS 实例. diff --git a/document/fs/ext4.md b/document/fs/ext4.md index 547e3b01..0e5e305b 100644 --- a/document/fs/ext4.md +++ b/document/fs/ext4.md @@ -1,136 +1,79 @@ -# Ext4 - Linux Ext4文件系统支持 - -## 概述 - -Ext4 文件系统支持允许 comix 内核访问 Linux Ext4 格式的文件系统,支持完整的读写操作。 - -**主要特点**: -- ✅ 完整读写:支持文件读写、创建、删除、重命名 -- ✅ 目录操作:支持mkdir、rmdir、readdir -- ✅ 链接操作:支持symlink、link、readlink -- ✅ 元数据:支持chmod、chown、set_times -- ✅ 块设备适配:通过BlockDriver接口访问 -- ✅ 标准格式:兼容Linux ext4 -- ⚠️ 部分特性:mknod未实现 - -## 架构设计 - -```mermaid -graph TB - A[Ext4FileSystem] -->|适配| B[BlockDeviceAdapter] - B -->|访问| C[BlockDriver] - C -->|硬件| D[VirtIO Block] - - A -->|实现| E[Inode trait] - A -->|实现| F[FileSystem trait] - - style A fill:#FFA07A - style B fill:#FFD700 - style C fill:#87CEEB -``` - -### BlockDeviceAdapter - -```rust -pub struct BlockDeviceAdapter { - driver: Arc, - block_size: usize, - offset: usize, // 分区偏移(扇区) -} - -impl BlockDeviceAdapter { - /// 读取块(以ext4块为单位,通常4KB) - pub fn read_block(&self, block_id: usize, buf: &mut [u8]) - -> Result<(), FsError> { - // 将ext4块转换为设备扇区 - let sector_size = 512; - let sectors_per_block = self.block_size / sector_size; - let start_sector = block_id * sectors_per_block + self.offset; - - // 读取扇区 - for i in 0..sectors_per_block { - let sector_buf = &mut buf[i * sector_size..(i + 1) * sector_size]; - self.driver.read_block(start_sector + i, sector_buf)?; - } - - Ok(()) - } -} -``` +# Ext4 -## 挂载Ext4 +Ext4 是当前默认 rootfs 的持久化文件系统候选. FS 初始化会在已发现块设备和分区中寻找可打开的 ext4, 并以 `/bin/sh` 或 `/bin/ash` 判断它是否是可启动 rootfs. -### 从块设备挂载 +## 当前状态 -```rust -use crate::fs::init_ext4_from_block_device; - -// 自动检测并挂载第一个块设备上的ext4 -init_ext4_from_block_device()?; -``` +- 源码位于 `os/src/fs/ext4/`. +- 使用 `ext4_rs` 作为底层 ext4 操作库. +- `BlockDeviceAdapter` 把内核 `BlockDriver` 适配成 ext4_rs 可读写的块设备. +- `Ext4FileSystem` 实现挂载级 `FileSystem`. +- `Ext4Inode` 实现 VFS `Inode`, 并通过 weak dentry 反向引用按需取得路径. -### 配置参数 +## 目标 -```rust -// config.rs -pub const EXT4_BLOCK_SIZE: usize = 4096; // 必须与mkfs.ext4 -b 匹配 -pub const FS_IMAGE_SIZE: usize = 128 * 1024 * 1024; // 128MB -``` +- 作为默认 rootfs 的主要格式. +- 支持普通文件, 目录, symlink, hard link, rename 和基础元数据操作. +- 支持分区块设备, 让 rootfs 不依赖整盘设备顺序. +- 为 `/dev` 上的块设备节点和 VFAT 测试分区共存提供基础. -## 使用示例 +## 非目标 -```rust -// 读取ext4文件系统中的文件 -let content = vfs_load_file("/bin/ls")?; +- 不在文档中复述 ext4_rs 内部结构. +- 不承诺完整 journaling 语义. +- 不把旧的 `init_ext4_from_block_device` 描述为默认运行路径; 它只是内部/测试兼容入口. -// 列出目录 -let bin = vfs_lookup("/bin")?; -let entries = bin.inode.readdir()?; -for entry in entries { - pr_info!("File: {}", entry.name); -} -``` +## 模块边界 -## 创建Ext4镜像 +- `mod.rs`: `Ext4FileSystem::open`, superblock 预检, statfs/sync. +- `adpaters.rs`: `BlockDriver` 到 ext4_rs block interface 的适配. +- `inode.rs`: VFS inode 操作到 ext4_rs 的映射和 inode cache. +- `fs/mod.rs`: rootfs 探测和临时挂载策略. -```bash -# 创建128MB镜像 -dd if=/dev/zero of=fs.img bs=1M count=128 +## 关键流程 -# 格式化为ext4(块大小4KB) -mkfs.ext4 -b 4096 fs.img +### rootfs probe -# 挂载并复制文件 -sudo mount -o loop fs.img /mnt -sudo cp -r myfiles/* /mnt/ -sudo umount /mnt +```text +list block devices + -> prefer partition names + -> Ext4FileSystem open + -> mount at / + -> vfs_lookup /bin/sh or /bin/ash + -> accept or rollback ``` -## 限制与注意事项 +这个流程避免把 `vda1` 写死为 rootfs. 如果 QEMU 或设备注册顺序变化, 只要某个分区包含可用 shell, 仍可被选中. -### 当前限制 +### block access -1. **mknod**: 不支持创建设备文件 -2. **块大小**: 必须是4096字节 -3. **崩溃安全**: 非日志模式,系统崩溃可能导致不一致 +```text +Ext4Inode + -> ext4_rs + -> BlockDeviceAdapter + -> BlockDriver + -> virtio block or partition device +``` -### 块大小对齐 +分区设备由 `PartitionBlockDevice` 包装整盘设备, ext4 层看到的是从分区起点开始的逻辑块空间. -```rust -// ⚠️ 重要:确保块大小匹配 -// mkfs.ext4 -b 4096 fs.img -pub const EXT4_BLOCK_SIZE: usize = 4096; // 必须匹配mkfs参数 -``` +## 并发和生命周期约束 + +- ext4_rs 对象由内核锁保护. +- ext4 inode 和 VFS dentry 之间不能形成强引用环. +- `sync` 下推到底层块设备 flush. +- rootfs probe 的失败候选必须卸载并清理 dentry cache, 否则后续候选会看到旧根路径. -## 性能考虑 +## 已知限制 -- **块缓存**: 实现块缓存可显著提升性能 -- **预读**: 顺序读取时启用预读 -- **DMA对齐**: 确保缓冲区对齐以使用DMA +- 高级 ext4 特性和崩溃恢复不是当前文档承诺范围. +- superblock 预检只用于避免明显坏镜像进入 ext4_rs. +- rootfs 判定只检查 `/bin/sh` 或 `/bin/ash`, 不验证完整用户态环境. -## 相关资源 +## 源码索引 -- **源代码**: `os/src/fs/ext4/` -- **适配器**: `os/src/fs/ext4/adapters.rs` -- [Ext4 Disk Layout](https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout) -- [FS模块概览](README.md) +- `os/src/fs/ext4/mod.rs`: 文件系统打开, superblock 预检, statfs/sync. +- `os/src/fs/ext4/adpaters.rs`: 块设备适配层. +- `os/src/fs/ext4/inode.rs`: ext4 inode 到 VFS inode 的映射. +- `os/src/fs/mod.rs`: `init_rootfs_from_discovered_block_devices`. +- `os/src/device/block/partition.rs`: 分区块设备包装. diff --git a/document/fs/procfs.md b/document/fs/procfs.md index 439500af..59ea7fd6 100644 --- a/document/fs/procfs.md +++ b/document/fs/procfs.md @@ -1,156 +1,67 @@ -# ProcFS - 进程信息文件系统 - -## 概述 - -ProcFS 是一个虚拟文件系统,用于导出内核状态和进程信息。所有文件内容都是动态生成的,不占用磁盘空间。 - -**主要特点**: -- 虚拟文件系统:文件内容动态生成 -- 只读:不支持写入操作 -- 标准接口:兼容Linux `/proc` 接口 -- 可扩展:易于添加新的信息导出 - -## 架构设计 - -### 核心组件 - -```mermaid -graph TB - A[ProcFS] -->|root| B[ProcInode] - B -->|静态子节点| C[meminfo/cpuinfo/uptime...] - B -->|动态子节点| D["/proc/self"] - B -->|进程目录| E["/proc/[pid]/"] - - C -->|Generator| F[MeminfoGenerator] - C -->|Generator| G[CpuinfoGenerator] - E -->|Generator| H[StatGenerator] - E -->|Generator| I[StatusGenerator] - - style A fill:#87CEEB - style F fill:#90EE90 - style G fill:#90EE90 - style H fill:#FFD700 - style I fill:#FFD700 -``` - -### Generator机制 - -所有proc文件使用Generator模式动态生成内容: - -```rust -pub trait Generator: Send + Sync { - /// 生成文件内容 - fn generate(&self) -> alloc::vec::Vec; -} - -// 示例:内存信息生成器 -pub struct MeminfoGenerator; - -impl Generator for MeminfoGenerator { - fn generate(&self) -> Vec { - let total = get_total_memory(); - let free = get_free_memory(); - - format!( - "MemTotal: {} kB\nMemFree: {} kB\nMemAvailable: {} kB\n", - total / 1024, - free / 1024, - free / 1024 - ).into_bytes() - } -} -``` +# ProcFS -## 文件列表 +ProcFS 把进程和系统运行时状态暴露为 `/proc` 文件树. 它是动态伪文件系统, 文件内容通常在读取时生成. -### 系统信息文件 +## 当前状态 -| 文件 | 内容 | 示例 | -|------|------|------| -| `/proc/meminfo` | 内存使用信息 | `MemTotal: 2048 MB` | -| `/proc/cpuinfo` | CPU信息 | `processor: 0` | -| `/proc/uptime` | 系统运行时间 | `12345.67 12345.67` | -| `/proc/mounts` | 挂载点列表 | `tmpfs /tmp tmpfs rw 0 0` | +- 源码位于 `os/src/fs/proc/`. +- `ProcFS::init_tree` 创建固定根条目, 如 `meminfo`, `uptime`, `cpuinfo`, `mounts`, `psmem`, `self`. +- 进程相关路径由 proc inode/generator 动态提供. +- 文件内容由 generator 生成, 不落盘. +- 部分动态 inode 使用非缓存策略, 避免进程退出后路径陈旧. -### 进程信息文件 +## 目标 -| 文件 | 内容 | 生成器 | -|------|------|--------| -| `/proc/[pid]/cmdline` | 命令行参数 | CmdlineGenerator | -| `/proc/[pid]/stat` | 进程状态 | StatGenerator | -| `/proc/[pid]/status` | 详细状态 | StatusGenerator | -| `/proc/[pid]/maps` | 内存映射 | MapsGenerator | +- 为用户态工具提供 Linux 风格 `/proc` 入口. +- 暴露进程, 内存, CPU, mount 等调试信息. +- 让动态内容以 VFS inode 方式接入, 不绕过路径和 fd 模型. -### 特殊符号链接 +## 非目标 -| 链接 | 目标 | 说明 | -|------|------|--------| -| `/proc/self` | `/proc/[current_pid]` | 指向当前进程 | +- 不实现完整 Linux procfs. +- 不保证每个字段和 Linux 完全一致. +- 不把 generator 的输出格式细节写入设计文档. -## 使用示例 +## 模块边界 -### 读取系统信息 +- `proc.rs`: 文件系统实例和根树初始化. +- `inode.rs`: proc inode 类型, 动态文件, 动态 symlink, 目录行为. +- `generators/`: 具体内容生成器. +- `generators/process/`: 进程相关文件内容. -```rust -// 读取内存信息 -let meminfo = vfs_load_file("/proc/meminfo")?; -pr_info!("Memory info:\n{}", String::from_utf8_lossy(&meminfo)); +## 关键流程 -// 读取CPU信息 -let cpuinfo = vfs_load_file("/proc/cpuinfo")?; +### read dynamic file -// 读取系统运行时间 -let uptime = vfs_load_file("/proc/uptime")?; +```text +vfs lookup /proc/... + -> ProcInode + -> generator snapshot + -> File read path returns bytes ``` -### 读取进程信息 - -```rust -// 读取当前进程状态 -let stat = vfs_load_file("/proc/self/stat")?; +generator 应尽量生成一个一致的快照, 避免读取过程中依赖长期锁. -// 读取特定进程的状态 -let pid1_status = vfs_load_file("/proc/1/status")?; +### /proc/self -// 读取命令行参数 -let cmdline = vfs_load_file("/proc/self/cmdline")?; -``` - -## 添加新的Proc文件 +`/proc/self` 是动态 symlink, 每次解析时根据当前任务 pid 指向对应进程目录. -### 步骤1:实现Generator +## 并发和生命周期约束 -```rust -pub struct MyInfoGenerator; +- 进程状态会变化, generator 需要容忍目标进程退出. +- 动态进程路径不应无条件缓存 dentry. +- 读取 proc 文件不应长期阻塞全局调度或任务锁. -impl Generator for MyInfoGenerator { - fn generate(&self) -> Vec { - format!("my_value: {}\n", get_my_value()) - .into_bytes() - } -} -``` +## 已知限制 -### 步骤2:注册到ProcFS - -```rust -pub fn init_tree(self: &Arc) -> Result<(), FsError> { - // ... 其他文件 ... - - // 添加新文件 - let myinfo = ProcInode::new_dynamic_file( - "myinfo", - Arc::new(MyInfoGenerator), - FileMode::from_bits_truncate(0o444), - ); - root.add_child("myinfo", myinfo)?; - - Ok(()) -} -``` +- 当前 procfs 以只读信息为主. +- Linux 工具依赖的某些 `/proc` 文件和字段尚未实现. +- `/proc/mounts` 反映当前 VFS mount table 的可见状态, 不是完整 namespace 视图. -## 相关资源 +## 源码索引 -- **源代码**: `os/src/fs/proc/` -- **生成器**: `os/src/fs/proc/generators/` -- [FS模块概览](README.md) +- `os/src/fs/proc/proc.rs`: `ProcFS` 和根树初始化. +- `os/src/fs/proc/inode.rs`: proc inode 类型. +- `os/src/fs/proc/generators/`: 系统级动态文件. +- `os/src/fs/proc/generators/process/`: 进程级动态文件. +- `os/src/fs/tests/proc/`: procfs 测试. diff --git a/document/fs/simple_fs.md b/document/fs/simple_fs.md index 3e7ed505..bd025597 100644 --- a/document/fs/simple_fs.md +++ b/document/fs/simple_fs.md @@ -1,165 +1,59 @@ -# SimpleFS - 简单测试文件系统 +# SimpleFS -## 概述 +SimpleFS 是编译期嵌入的只读测试文件系统. 当前它作为 rootfs 探测失败时的 fallback, 也用于不依赖外部磁盘镜像的测试路径. -SimpleFS 是一个轻量级的只读文件系统,用于测试和调试。文件系统镜像在编译时嵌入到内核中,启动时加载到RamDisk。 +## 当前状态 -**主要特点**: -- 编译时嵌入:镜像作为静态数据包含在内核中 -- 快速启动:无需外部文件系统 -- 测试友好:提供一致的测试环境 -- 只读:不支持修改 +- 源码位于 `os/src/fs/simple_fs.rs`. +- 镜像由构建阶段生成并通过 `include_bytes!` 嵌入内核. +- 启动时镜像被放入 `RamDisk`, 再解析成 `SimpleFsInode` 树. +- 支持多级路径, 普通文件和目录. +- 运行时只读. -## 镜像格式 +## 目标 -### 镜像结构 +- 在没有可用 ext4 rootfs 时保持系统可启动或可测试. +- 提供稳定, 小型, 可嵌入的测试文件树. +- 避免早期启动完全依赖 virtio block 或分区盘. -``` -+------------------+ -| Header (512B) | -| - Magic: RAMDISK | -| - File count | -+------------------+ -| File Entry 1 | -| - Header (32B) | -| - Name (aligned) | -| - Data (aligned) | -+------------------+ -| File Entry 2 | -| ... | -+------------------+ -``` +## 非目标 -### 文件条目格式 - -```rust -struct FileEntry { - magic: u32, // 0x46494C45 ("FILE") - name_len: u32, // 文件名长度 - data_len: u32, // 数据长度 - file_type: u32, // 0=文件, 1=目录 - mode: u32, // 权限位 - // 之后是name(4字节对齐) - // 之后是data(512字节对齐) -} -``` +- 不作为正式持久化 rootfs 格式. +- 不支持运行时写入. +- 不复刻 ext4 或 FAT 的磁盘结构. -## 构建流程 - -### build.rs 脚本 - -```rust -// build.rs -fn build_simple_fs() { - let out_dir = env::var("OUT_DIR").unwrap(); - let image_path = format!("{}/simple_fs.img", out_dir); - - // 创建镜像 - create_ramdisk_image(&image_path, "user")?; - - // 设置环境变量供include_bytes!使用 - println!("cargo:rustc-env=SIMPLE_FS_IMAGE={}", image_path); -} -``` +## 模块边界 -### 嵌入到内核 - -```rust -// fs/mod.rs -static SIMPLE_FS_IMAGE: &[u8] = include_bytes!(env!("SIMPLE_FS_IMAGE")); - -pub fn init_simple_fs() -> Result<(), FsError> { - // 从静态数据创建RamDisk - let ramdisk = RamDisk::from_bytes( - SIMPLE_FS_IMAGE.to_vec(), - 512, // 块大小 - 0 // 偏移 - ); - - // 加载SimpleFS - let simplefs = SimpleFs::from_ramdisk(ramdisk)?; - - // 挂载为根文件系统 - MOUNT_TABLE.mount( - Arc::new(simplefs), - "/", - MountFlags::empty(), - Some(String::from("ramdisk0")), - )?; - - Ok(()) -} -``` +- `simple_fs.rs`: 镜像解析, inode 树, `FileSystem` 实现. +- `fs/mod.rs`: `init_simple_fs` fallback 入口. +- `device/block/ram_disk.rs`: 嵌入镜像的块设备承载. +- build 脚本: 生成 `SIMPLE_FS_IMAGE`. -## 使用场景 +## 关键流程 -### 测试环境 - -```rust -#[test] -fn test_with_simplefs() { - init_simple_fs().unwrap(); - - // 测试文件系统操作 - let content = vfs_load_file("/bin/hello").unwrap(); - assert_eq!(content, b"Hello, World!"); -} +```text +include bytes image + -> RamDisk + -> SimpleFs from_ramdisk + -> mount at / ``` -### 预加载用户程序 - -```bash -# 将用户程序添加到镜像 -cp user/target/riscv64gc-unknown-none-elf/release/init user/ -cp user/target/riscv64gc-unknown-none-elf/release/sh user/bin/ - -# 重新构建内核(会自动重建镜像) -make build -``` - -## 添加文件到镜像 - -### 方式1:修改构建脚本 - -```rust -// build.rs -let files = vec![ - ("bin/init", "user/target/.../init"), - ("bin/sh", "user/target/.../sh"), - ("etc/rc", "scripts/rc"), -]; - -for (dest, src) in files { - add_file_to_image(&mut image, src, dest)?; -} -``` - -### 方式2:使用目录 - -```rust -// build.rs -// 将整个目录添加到镜像 -add_directory_to_image(&mut image, "user", "/")?; -``` +默认优先尝试分区盘 ext4 rootfs. SimpleFS 只在 rootfs 探测失败时作为回退路径. -## 限制 +## 并发和生命周期约束 -1. **只读**: 运行时无法修改 -2. **大小限制**: 镜像过大会增加内核体积 -3. **重启丢失**: 运行时的修改不会保存 +- 文件内容驻留内存, 无同步到磁盘路径. +- 只读语义应在 inode 操作中保持一致. +- 镜像大小和内容由构建产物决定, 不是运行时配置. -## 对比:SimpleFS vs Ext4 +## 已知限制 -| 特性 | SimpleFS | Ext4 | -|------|----------|------| -| 持久化 | ❌ | ✅ | -| 修改支持 | ❌ | ✅(读写) | -| 启动速度 | 快 | 慢(需块设备) | -| 镜像大小 | 小 | 大 | -| 用途 | 测试 | 生产 | +- 只读. +- 文件系统格式只服务内核测试和 fallback. +- 元数据语义较简单. -## 相关资源 +## 源码索引 -- **源代码**: `os/src/fs/simple_fs.rs` -- **构建脚本**: `os/build.rs` -- [FS模块概览](README.md) +- `os/src/fs/simple_fs.rs`: SimpleFS 实现. +- `os/src/fs/mod.rs`: `init_simple_fs`. +- `os/src/device/block/ram_disk.rs`: RamDisk. diff --git a/document/fs/sysfs.md b/document/fs/sysfs.md index dc78276c..132f80a3 100644 --- a/document/fs/sysfs.md +++ b/document/fs/sysfs.md @@ -1,82 +1,68 @@ -# SysFS - 系统设备文件系统 - -## 概述 - -SysFS 是一个虚拟文件系统,用于导出内核中的设备信息和状态。它提供了一个层次化的设备树视图。 - -**主要特点**: -- 设备层次结构:反映设备的物理和逻辑关系 -- 属性导出:每个设备可导出多个属性文件 -- Builder模式:使用Builder构建设备树 -- 只读:当前实现仅支持读取 - -## 架构设计 - -```mermaid -graph TB - A[SysFS] -->|root| B[SysfsInode /sys] - B --> C[/sys/class] - B --> D[/sys/devices] - B --> E[/sys/bus] - - C --> F[/sys/class/block] - F --> G[/sys/class/block/vda] - G --> H[dev属性文件] - - style A fill:#DDA0DD - style B fill:#87CEEB - style H fill:#90EE90 -``` +# SysFS -### 设备注册表 +SysFS 把设备注册表和内核属性暴露为 `/sys`. 它是冷插拔构建的伪文件系统, 当前主要用于设备发现和调试. -```rust -pub struct DeviceRegistry { - /// 块设备列表: 名称 -> (major, minor) - block_devices: BTreeMap, - - /// 字符设备列表: 名称 -> (major, minor) - char_devices: BTreeMap, -} -``` +## 当前状态 -## 目录结构 - -| 路径 | 内容 | 说明 | -|------|------|------| -| `/sys/class` | 设备类别 | 按功能分类的设备 | -| `/sys/class/block` | 块设备 | vda, vdb等 | -| `/sys/class/net` | 网络设备 | eth0, lo等 | -| `/sys/devices` | 设备树 | 物理设备层次 | -| `/sys/bus` | 总线 | pci, usb等 | - -## 使用示例 - -```rust -// 查询块设备号 -let dev_str = vfs_load_file("/sys/class/block/vda/dev")?; -pr_info!("vda device number: {}", String::from_utf8_lossy(&dev_str)); - -// 列出所有块设备 -let block_dir = vfs_lookup("/sys/class/block")?; -let entries = block_dir.inode.readdir()?; -for entry in entries { - pr_info!("Block device: {}", entry.name); -} -``` +- 源码位于 `os/src/fs/sysfs/`. +- `SysFS::init_tree` 创建 `/sys/class`, `/sys/devices`, `/sys/kernel` 等目录. +- `/sys/class/block` 根据块设备和分区列表创建符号链接. +- `/sys/block` 是指向 `class/block` 的兼容 symlink. +- `device_registry.rs` 复用设备层全局注册表, 不创建另一套设备来源. + +## 目标 + +- 为用户态和内核调试提供统一设备视图. +- 让块设备, 网络设备, tty, input, rtc 等类别能被枚举. +- 为 FS rootfs 探测和 `/dev` 创建提供设备列表来源. + +## 非目标 + +- 不实现完整 Linux sysfs 属性写入模型. +- 不承诺设备热插拔后自动增量更新 sysfs 树. +- 不在文档中复制所有 builder 生成的节点. + +## 模块边界 + +- `sysfs.rs`: 文件系统实例, 根目录结构, builder 调度. +- `inode.rs`: 目录, 属性文件和 symlink inode. +- `device_registry.rs`: 从 `DRIVERS`, `BLK_DRIVERS`, `RTC_DRIVERS` 等全局表生成设备信息. +- `builders/`: 各类 `/sys` 子树构建器. -## 添加设备 +## 关键流程 -```rust -// 注册块设备到sysfs -pub fn register_block_device(name: &str, major: u32, minor: u32) { - let registry = DEVICE_REGISTRY.write(); - registry.add_block_device(name, major, minor); -} +### cold build + +```text +SysFS new + -> create base directories + -> build platform devices + -> build class symlinks + -> mount at /sys ``` -## 相关资源 +设备应先在 device 层完成注册, sysfs 初始化再读取注册表构建树. + +### block devices + +`list_block_devices` 会为每个 virtio block 设备生成 `vda`, `vdb` 等整盘名称, 并解析 MBR/GPT 分区生成 `vda1`, `vda2` 等逻辑分区设备. 这些名字会同时影响 `/sys/class/block` 和 `/dev` 节点创建. + +## 并发和生命周期约束 + +- 当前 sysfs 树按初始化时设备注册表冷构建. +- 属性文件读取应从设备或内核状态生成当前值. +- 分区设备是包装整盘块设备的 `Arc`, 生命周期依赖底层驱动全局注册表. + +## 已知限制 + +- 写属性和热插拔更新能力有限. +- sysfs 结构只覆盖当前内核已有设备类别. +- 分区解析依赖块大小和分区表可读性. + +## 源码索引 -- **源代码**: `os/src/fs/sysfs/` -- **Builders**: `os/src/fs/sysfs/builders/` -- [FS模块概览](README.md) +- `os/src/fs/sysfs/sysfs.rs`: `SysFS` 初始化和树构建流程. +- `os/src/fs/sysfs/inode.rs`: sysfs inode 类型. +- `os/src/fs/sysfs/device_registry.rs`: 设备列表和分区枚举. +- `os/src/fs/sysfs/builders/`: class/devices/kernel 子树构建器. +- `os/src/device/block/partition.rs`: MBR/GPT 分区解析. diff --git a/document/fs/tmpfs.md b/document/fs/tmpfs.md index 5cf14f22..1b56ae2c 100644 --- a/document/fs/tmpfs.md +++ b/document/fs/tmpfs.md @@ -1,638 +1,68 @@ -# Tmpfs - 临时文件系统 +# Tmpfs -## 概述 +Tmpfs 是内存文件系统, 当前主要用于 `/tmp` 等临时路径. 它实现 VFS `FileSystem` 和 `Inode`, 不依赖块设备. -Tmpfs (Temporary File System) 是一个完全基于内存的文件系统实现,提供快速的文件读写性能。所有数据均存储在物理内存中,系统重启后数据会丢失。 +## 当前状态 -**主要特点**: -- ✅ 高性能:纯内存操作,无磁盘I/O -- ✅ 动态分配:按需分配物理页 -- ✅ 容量控制:可配置最大内存使用量 -- ✅ 完整语义:支持完整的POSIX文件系统语义 -- ❌ 非持久化:重启后数据丢失 +- 源码位于 `os/src/fs/tmpfs/`. +- `TmpFs` 保存根 inode 和全局统计. +- `TmpfsInode` 表示目录, 文件和 symlink. +- 容量限制以页为单位统计, `max_size_mb = 0` 表示无限制. +- `mount_tmpfs` 是 FS 初始化阶段和其他挂载路径使用的便捷入口. -## 架构设计 +## 目标 -### 核心组件 +- 为临时文件提供快速读写路径. +- 支持目录, 普通文件, symlink 和基础元数据. +- 在 rootfs 之上挂载 `/tmp`, 避免临时文件污染持久化 rootfs. +- 为测试提供不依赖磁盘镜像的可写文件系统. -```mermaid -graph TB - A[TmpFs] -->|持有| B[Root TmpfsInode] - A -->|共享| C[TmpfsStats] - - B -->|children| D[子文件/目录] - B -->|pages| E[物理页列表] - - C -->|追踪| F[已分配页数] - C -->|限制| G[最大页数] - C -->|分配| H[inode编号] - - style A fill:#90EE90 - style B fill:#87CEEB - style C fill:#FFD700 -``` - -### 数据结构 - -#### TmpFs - 文件系统结构 - -```rust -pub struct TmpFs { - /// 根 inode - root: Arc, - - /// 全局统计信息 - stats: Arc>, -} - -pub struct TmpfsStats { - /// 已分配的物理页数 - allocated_pages: usize, - - /// 最大页数限制 (0 = 无限制) - max_pages: usize, - - /// 下一个可用的 inode 编号 - next_inode_no: usize, -} -``` - -#### TmpfsInode - 文件/目录节点 - -```rust -pub struct TmpfsInode { - /// Inode 编号 - inode_no: usize, - - /// 节点类型 (文件/目录/符号链接) - inode_type: InodeType, - - /// 核心元数据 - inner: SpinLock, - - /// 物理页列表 (仅文件类型使用) - pages: Mutex>, - - /// 子节点 (仅目录类型使用) - children: SpinLock>>, - - /// 父目录的弱引用 - parent: Weak, - - /// 全局统计信息 - stats: Arc>, -} - -struct TmpfsInodeInner { - mode: FileMode, - uid: u32, - gid: u32, - size: usize, - atime: TimeSpec, - mtime: TimeSpec, - ctime: TimeSpec, - nlinks: usize, - /// 符号链接目标 (仅 Symlink 类型) - symlink_target: Option, -} -``` - -## 内存管理 - -### 页面分配策略 - -Tmpfs 采用按需分配策略: - -1. **延迟分配**: 创建文件时不分配内存 -2. **写时分配**: 第一次写入时才分配物理页 -3. **页对齐**: 所有数据按页(4KB)对齐存储 -4. **容量检查**: 分配前检查是否超过限制 - -```rust -// 写入数据时的分配流程 -pub fn write_at(&self, offset: usize, buf: &[u8]) -> Result { - let new_size = offset + buf.len(); - let current_size = self.inner.lock().size; - - // 1. 计算需要的页数 - let current_pages = (current_size + PAGE_SIZE - 1) / PAGE_SIZE; - let needed_pages = (new_size + PAGE_SIZE - 1) / PAGE_SIZE; - - // 2. 如果需要更多页,先分配 - if needed_pages > current_pages { - let additional = needed_pages - current_pages; - - // 检查容量限制 - if !self.can_alloc_pages(additional) { - return Err(FsError::NoSpace); - } - - // 分配新页 - for _ in 0..additional { - let page = alloc_kernel_frames(1) - .ok_or(FsError::NoSpace)?; - self.pages.lock().push(page); - } - - self.inc_allocated_pages(additional); - } - - // 3. 写入数据 - // ... -} -``` - -### 容量限制 - -```rust -// 创建时指定最大容量 -let tmpfs = TmpFs::new(64); // 64MB - -// 容量检查 -fn can_alloc_pages(&self, num_pages: usize) -> bool { - let stats = self.stats.lock(); - if stats.max_pages == 0 { - return true; // 无限制 - } - stats.allocated_pages + num_pages <= stats.max_pages -} -``` - -### 内存释放 - -```rust -impl Drop for TmpfsInode { - fn drop(&mut self) { - // 释放所有物理页 - let pages = self.pages.lock(); - let num_pages = pages.len(); - - for page_addr in pages.iter() { - dealloc_kernel_frames(*page_addr, 1); - } - - // 更新统计 - self.dec_allocated_pages(num_pages); - } -} -``` - -## Inode 实现 - -### 文件操作 - -#### 读取文件 - -```rust -fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result { - if self.inode_type == InodeType::Directory { - return Err(FsError::IsDirectory); - } - - let inner = self.inner.lock(); - let size = inner.size; - drop(inner); - - // 超出文件末尾 - if offset >= size { - return Ok(0); - } - - // 计算实际读取长度 - let read_len = core::cmp::min(buf.len(), size - offset); - let pages = self.pages.lock(); - - // 逐页读取 - let mut copied = 0; - while copied < read_len { - let page_idx = (offset + copied) / PAGE_SIZE; - let page_offset = (offset + copied) % PAGE_SIZE; - let copy_len = core::cmp::min( - PAGE_SIZE - page_offset, - read_len - copied - ); - - // 从物理页复制数据 - let page_addr = pages[page_idx]; - let src = unsafe { - core::slice::from_raw_parts( - page_addr.as_ptr(), - PAGE_SIZE - ) - }; - - buf[copied..copied + copy_len] - .copy_from_slice(&src[page_offset..page_offset + copy_len]); - - copied += copy_len; - } - - // 更新访问时间 - self.update_atime(); - Ok(read_len) -} -``` - -#### 截断文件 - -```rust -fn truncate(&self, new_size: usize) -> Result<(), FsError> { - let mut inner = self.inner.lock(); - let old_size = inner.size; - - if new_size == old_size { - return Ok(()); - } - - if new_size < old_size { - // 缩小文件:释放多余的页 - let old_pages = (old_size + PAGE_SIZE - 1) / PAGE_SIZE; - let new_pages = (new_size + PAGE_SIZE - 1) / PAGE_SIZE; - - if new_pages < old_pages { - let mut pages = self.pages.lock(); - let freed = old_pages - new_pages; - - // 释放末尾的页 - for _ in 0..freed { - if let Some(page) = pages.pop() { - dealloc_kernel_frames(page, 1); - } - } - - self.dec_allocated_pages(freed); - } - - // 清零最后一页的尾部 - if new_size % PAGE_SIZE != 0 { - let last_page_idx = new_size / PAGE_SIZE; - let last_page_offset = new_size % PAGE_SIZE; - - let pages = self.pages.lock(); - if let Some(&page_addr) = pages.get(last_page_idx) { - unsafe { - let ptr = page_addr.as_mut_ptr().add(last_page_offset); - core::ptr::write_bytes(ptr, 0, PAGE_SIZE - last_page_offset); - } - } - } - } - - inner.size = new_size; - self.update_mtime(); - Ok(()) -} -``` - -### 目录操作 - -#### 创建文件 +## 非目标 -```rust -fn create(&self, name: &str, mode: FileMode) -> Result, FsError> { - if self.inode_type != InodeType::Directory { - return Err(FsError::NotDirectory); - } - - let mut children = self.children.lock(); - - // 检查是否已存在 - if children.contains_key(name) { - return Err(FsError::AlreadyExists); - } - - // 创建新 inode - let inode_no = self.alloc_inode_no(); - let new_inode = TmpfsInode::new( - inode_no, - InodeType::File, - mode, - Arc::downgrade(&(self.clone() as Arc)), - self.stats.clone(), - ); - - // 添加到子节点 - children.insert(name.to_string(), new_inode.clone()); - - // 更新目录修改时间 - self.update_mtime(); - - Ok(new_inode as Arc) -} -``` +- 不提供 swap 后端. +- 不实现 Linux tmpfs 的所有 mount options. +- 不在文档中列出每个 inode 操作分支. -#### 删除文件 +## 模块边界 -```rust -fn unlink(&self, name: &str) -> Result<(), FsError> { - if self.inode_type != InodeType::Directory { - return Err(FsError::NotDirectory); - } - - let mut children = self.children.lock(); - - // 查找子节点 - let child = children.get(name) - .ok_or(FsError::NotFound)?; - - // 不能删除目录 - if child.inode_type == InodeType::Directory { - return Err(FsError::IsDirectory); - } - - // 删除子节点 - children.remove(name); - - // 更新修改时间 - self.update_mtime(); - - Ok(()) -} -``` +- `tmpfs.rs`: 文件系统实例, 容量统计, statfs. +- `inode.rs`: 内存 inode, 数据页, 目录项和 symlink 内容. +- `fs/mod.rs`: `mount_tmpfs` 初始化入口. -### 符号链接 +## 关键流程 -```rust -fn symlink(&self, name: &str, target: &str) -> Result, FsError> { - if self.inode_type != InodeType::Directory { - return Err(FsError::NotDirectory); - } - - let mut children = self.children.lock(); - - if children.contains_key(name) { - return Err(FsError::AlreadyExists); - } - - // 创建符号链接 inode - let inode_no = self.alloc_inode_no(); - let symlink_inode = TmpfsInode::new( - inode_no, - InodeType::Symlink, - FileMode::S_IFLNK | FileMode::S_IRWXU | FileMode::S_IRWXG | FileMode::S_IRWXO, - Arc::downgrade(&(self.clone() as Arc)), - self.stats.clone(), - ); - - // 设置符号链接目标 - symlink_inode.inner.lock().symlink_target = Some(target.to_string()); - - children.insert(name.to_string(), symlink_inode.clone()); - self.update_mtime(); - - Ok(symlink_inode as Arc) -} +### mount -fn readlink(&self) -> Result { - if self.inode_type != InodeType::Symlink { - return Err(FsError::InvalidArgument); - } - - self.inner.lock() - .symlink_target - .clone() - .ok_or(FsError::InvalidArgument) -} +```text +TmpFs new + -> root TmpfsInode + -> MOUNT_TABLE mount + -> path lookup enters tmpfs ``` -## 使用指南 - -### 挂载Tmpfs - -#### 基本挂载 - -```rust -use crate::fs::mount_tmpfs; - -// 挂载 64MB tmpfs 到 /tmp -mount_tmpfs("/tmp", 64)?; - -// 无限制大小的 tmpfs -mount_tmpfs("/run", 0)?; -``` - -#### 在系统初始化时挂载 - -```rust -pub fn init_filesystems() -> Result<(), FsError> { - // ... 挂载根文件系统 ... - - // 创建 /tmp 目录 - let root = vfs::get_root_dentry()?; - root.inode.mkdir("tmp", FileMode::S_IFDIR | FileMode::S_IRWXU)?; - - // 挂载tmpfs - mount_tmpfs("/tmp", 64)?; - - Ok(()) -} -``` +### write -### 文件操作示例 +普通文件写入会按页扩展内存数据, 并更新统计. 如果设置了容量上限, 分配前需要检查剩余页数. -#### 创建和写入文件 +### umount -```rust -// 创建文件 -let fd = sys_open("/tmp/test.txt", - OpenFlags::O_WRONLY | OpenFlags::O_CREAT | OpenFlags::O_TRUNC, - FileMode::S_IRUSR | FileMode::S_IWUSR)?; +tmpfs 没有持久化同步. 当 mount table 和 open file 都释放对应 `Arc`, 内存由 Rust 生命周期释放. -// 写入数据 -sys_write(fd, b"Hello, Tmpfs!")?; -sys_close(fd)?; -``` - -#### 读取文件 - -```rust -let fd = sys_open("/tmp/test.txt", OpenFlags::O_RDONLY, FileMode::empty())?; -let mut buf = vec![0u8; 128]; -let n = sys_read(fd, &mut buf)?; -sys_close(fd)?; - -pr_info!("Read {} bytes: {}", n, String::from_utf8_lossy(&buf[..n])); -``` - -#### 创建子目录 - -```rust -// 创建多级目录 -sys_mkdir("/tmp/cache", FileMode::S_IRWXU)?; -sys_mkdir("/tmp/cache/data", FileMode::S_IRWXU)?; - -// 创建文件 -let fd = sys_open("/tmp/cache/data/file.dat", - OpenFlags::O_WRONLY | OpenFlags::O_CREAT, - FileMode::S_IRUSR | FileMode::S_IWUSR)?; -sys_close(fd)?; -``` - -### 容量管理 - -#### 查询使用情况 - -```rust -// 获取文件系统统计信息 -let tmpfs_dentry = vfs_lookup("/tmp")?; -let statfs = tmpfs_dentry.inode.fs()?.statfs()?; - -pr_info!("Tmpfs statistics:"); -pr_info!(" Block size: {} bytes", statfs.block_size); -pr_info!(" Total blocks: {}", statfs.total_blocks); -pr_info!(" Free blocks: {}", statfs.free_blocks); -pr_info!(" Used: {} MB", - (statfs.total_blocks - statfs.free_blocks) * statfs.block_size / 1024 / 1024); -``` - -#### 处理空间不足 - -```rust -match sys_write(fd, large_data) { - Err(FsError::NoSpace) => { - pr_warn!("Tmpfs is full, cleaning up old files..."); - // 清理临时文件 - cleanup_old_files("/tmp")?; - // 重试 - sys_write(fd, large_data)?; - } - Err(e) => return Err(e), - Ok(n) => pr_info!("Written {} bytes", n), -} -``` - -## 性能特性 - -### 性能优势 - -1. **零磁盘I/O**: 所有操作在内存中完成 -2. **快速分配**: 物理页分配延迟极低 -3. **无碎片**: 页对齐避免内部碎片 - -### 性能对比 - -| 操作 | Tmpfs | Ext4(SSD) | Ext4(HDD) | -|------|-------|------------|------------| -| 顺序读 | ~10 GB/s | ~500 MB/s | ~150 MB/s | -| 顺序写 | ~8 GB/s | ~450 MB/s | ~120 MB/s | -| 随机读 | ~8 GB/s | ~300 MB/s | ~1 MB/s | -| 创建文件 | ~500k ops/s | ~10k ops/s | ~200 ops/s | - -### 使用场景建议 - -**适合**: -- 构建系统的临时目录 -- 进程间共享数据 -- 缓存数据 -- 临时日志 - -**不适合**: -- 需要持久化的数据 -- 超大文件(占用过多内存) -- 长期存储 - -## 最佳实践 - -### 1. 合理设置容量限制 - -```rust -// 根据系统内存设置 tmpfs 大小 -let total_mem = get_total_memory(); -let tmpfs_size = total_mem / 4; // 使用 1/4 内存 - -mount_tmpfs("/tmp", tmpfs_size / 1024 / 1024)?; -``` - -### 2. 定期清理 - -```rust -// 定期清理过期文件 -pub fn cleanup_tmpfs() -> Result<(), FsError> { - let tmp_dentry = vfs_lookup("/tmp")?; - let entries = tmp_dentry.inode.readdir()?; - - let now = TimeSpec::now(); - - for entry in entries { - let path = format!("/tmp/{}", entry.name); - let dentry = vfs_lookup(&path)?; - let metadata = dentry.inode.metadata()?; - - // 删除超过1小时未访问的文件 - if now.seconds - metadata.atime.seconds > 3600 { - sys_unlink(&path)?; - } - } - - Ok(()) -} -``` - -### 3. 错误处理 - -```rust -// 健壮的文件写入 -pub fn safe_write_tmpfs(path: &str, data: &[u8]) -> Result<(), FsError> { - // 先检查空间 - let tmp = vfs_lookup("/tmp")?; - let statfs = tmp.inode.fs()?.statfs()?; - let free_bytes = statfs.free_blocks * statfs.block_size; - - if data.len() > free_bytes { - return Err(FsError::NoSpace); - } - - // 写入文件 - let fd = sys_open(path, - OpenFlags::O_WRONLY | OpenFlags::O_CREAT | OpenFlags::O_TRUNC, - FileMode::S_IRUSR | FileMode::S_IWUSR)?; - - sys_write(fd, data)?; - sys_close(fd)?; - - Ok(()) -} -``` - -## 限制与注意事项 - -### 当前限制 - -1. **不支持硬链接**: `link()` 返回 `NotSupported` -2. **不支持rename**: 跨父目录的重命名未实现 -3. **不支持权限检查**: 所有操作忽略权限(TODO) -4. **不支持扩展属性**: 无 xattr 支持 - -### 内存占用注意事项 - -```rust -// ❌ 错误:创建大量小文件会浪费内存 -for i in 0..10000 { - sys_open(&format!("/tmp/file{}", i), - OpenFlags::O_CREAT, FileMode::S_IRUSR)?; -} -// 即使文件是空的,每个文件也会占用至少一个inode结构 - -// ✅ 正确:合并小文件 -let mut merged_data = Vec::new(); -for small_file in small_files { - merged_data.extend_from_slice(&small_file); -} -write_file("/tmp/merged", &merged_data)?; -``` +## 并发和生命周期约束 -## 相关资源 +- 文件系统统计和 inode 内容由锁保护. +- open file 的 offset 仍属于 VFS `File`, tmpfs inode 只保存内容和元数据. +- 容量统计必须和 truncate/unlink 等释放路径保持一致. -### 源代码位置 +## 已知限制 -- **Tmpfs 实现**: `os/src/fs/tmpfs/` - - `tmpfs.rs` - TmpFs 文件系统 - - `inode.rs` - TmpfsInode 实现 - - `mod.rs` - 模块导出 +- 无 swap 和回收策略. +- 无限容量配置仍受实际内核内存限制. +- 权限和时间戳语义以当前 VFS 需要为主, 不是完整 Linux tmpfs. -### 参考文档 +## 源码索引 -- [FS 模块概览](README.md) -- [VFS 架构](../vfs/architecture.md) -- [内存管理](../mm/memory.md) -- [Linux tmpfs Documentation](https://www.kernel.org/doc/html/latest/filesystems/tmpfs.html) +- `os/src/fs/tmpfs/tmpfs.rs`: `TmpFs`, 容量和 statfs. +- `os/src/fs/tmpfs/inode.rs`: `TmpfsInode`. +- `os/src/fs/mod.rs`: `mount_tmpfs`. +- `os/src/fs/tests/tmpfs/`: tmpfs 行为测试. diff --git a/document/fs/vfat.md b/document/fs/vfat.md new file mode 100644 index 00000000..bedb92d2 --- /dev/null +++ b/document/fs/vfat.md @@ -0,0 +1,77 @@ +# VFAT/FAT + +VFAT/FAT 支持位于 `os/src/fs/vfat/`. 这是当前 FAT 相关实现的正式入口, 用于 VFAT/FAT mount 兼容路径和分区 mount/umount 测试. + +## 当前状态 + +- `VfatFileSystem` 实现 VFS `FileSystem`. +- `VfatInode` 把 FAT/VFAT 目录和文件操作映射到 VFS `Inode`. +- `FatBlockDevice` 把内核 `BlockDriver` 的整块读写适配为 `fatfs` 需要的字节流读写. +- `fs_type` 返回 `vfat`, 但底层通过 `fatfs` 支持 FAT 家族卷. +- 默认分区盘中 VFAT 分区用于 mount/umount 测试, 不参与 rootfs 自动选择. + +## 目标 + +- 让 FAT/VFAT 分区可以挂载到 VFS 路径树. +- 支持常见文件和目录读写, 便于与主机制作的 FAT 分区交换测试数据. +- 验证块设备分区, mount stack, umount, statfs 和设备 flush 路径. +- 保持和 Linux 用户习惯一致的 `vfat` 类型名. + +## 非目标 + +- 不作为默认 rootfs. +- 不在文档中描述 FAT 表布局和长文件名编码细节. +- 不承诺完整 Linux vfat mount option 集合. + +## 模块边界 + +- `mod.rs`: VFAT 模块入口和公开类型. +- `fs.rs`: 挂载实例, fatfs 打开, statfs, sync, umount. +- `inode.rs`: 文件, 目录, lookup, create, readdir 等 inode 适配. +- `adapter.rs`: 任意字节范围 I/O 到 block I/O 的转换. +- `device/block/partition.rs`: 为 VFAT 分区提供逻辑块设备. + +## 关键流程 + +### mount + +```text +/dev/vda2 or another FAT partition + -> BlockDriver or PartitionBlockDevice + -> FatBlockDevice + -> fatfs FileSystem + -> VfatFileSystem + -> MOUNT_TABLE +``` + +初始化代码默认不会把 VFAT 选为 `/`. 测试或用户路径可以把它挂载到 `/mnt` 等目录, 用于覆盖 mount/umount 和跨文件系统路径解析. + +### unaligned I/O + +`fatfs` 以字节流方式读写, 但内核块设备只接受块 I/O. `FatBlockDevice` 对非对齐写入执行 read-modify-write, 以保留同一块中未覆盖的数据. + +### synchronization + +`VfatFileSystem::sync` 会重新打开 fatfs 视图完成同步检查, 然后 flush 底层设备. `umount` 当前走 sync 路径, 确保 FAT 侧状态和块设备状态尽量落盘. + +## 并发和生命周期约束 + +- `VfatState` 使用锁串行化对 fatfs 的访问. +- 每次操作会打开一个 fatfs 视图, 执行闭包, 再 unmount 该视图. +- 块设备边界由 `FatBlockDevice` 检查, 越界转换为 VFS 错误. +- VFAT inode 不能依赖 Unix inode 号和权限模型的完整语义. + +## 已知限制 + +- Linux vfat 的 mount options, codepage 和大小写策略尚未完整暴露. +- FAT 没有 Unix inode/权限模型, statfs 和 metadata 会有适配值. +- 设计目标是兼容 FAT/VFAT 卷读写和 mount 测试, 不是替代 ext4 rootfs. + +## 源码索引 + +- `os/src/fs/vfat/mod.rs`: 模块入口. +- `os/src/fs/vfat/fs.rs`: `VfatFileSystem` 和 fatfs 生命周期. +- `os/src/fs/vfat/inode.rs`: `VfatInode`. +- `os/src/fs/vfat/adapter.rs`: `FatBlockDevice`. +- `os/src/fs/tests/vfat/mod.rs`: VFAT 适配和 VFS 行为测试. +- `os/src/device/block/partition.rs`: 分区块设备. diff --git a/document/ipc/README.md b/document/ipc/README.md index 261a4ff3..57bdb81a 100644 --- a/document/ipc/README.md +++ b/document/ipc/README.md @@ -1,34 +1,69 @@ # IPC 子系统概述 -## 简介 - -IPC(进程间通信)为 Task 之间传递数据、事件与共享状态提供标准机制。本子系统目前包含四类能力: -- 管道(Pipe):基于内核缓冲区的字节流通信,适合一写一读或少量端点的单机通信。 -- 消息(Message):以离散消息为单位的传递机制,适合结构化、边界明确的通信。 -- 共享内存(Shared Memory):多任务映射同一物理页,实现零拷贝数据共享。 -- 信号(Signal):面向事件/控制流的异步通知与中断唤醒。 - -## 设计目标 -- 统一抽象:各模块在接口与错误语义上尽量对齐,便于组合使用。 -- 高效与可预期:常见路径零拷贝(共享内存)、有界缓冲(管道/消息)、明确的阻塞/非阻塞行为。 -- 与内核其他子系统良好耦合:调度器、等待队列、VFS、内存管理。 - -## 与其他子系统的交互 -- 调度与等待:阻塞型 API 通过 `WaitQueue` 与调度器配合实现睡眠与唤醒。 -- VFS:管道以文件的形式出现在 VFS 中(`vfs/impls/pipe_file.rs`),可被 `fd_table` 引用。 -- 内存管理:共享内存通过 `mm` 为多个 `MemorySpace` 建立映射。 -- 任务/信号:信号可打断可中断的睡眠,并作为错误返回(如 `EINTR`)或触发默认动作。 - -## 源码导览 -- IPC 模块根:`os/src/ipc/mod.rs` -- 管道:`os/src/ipc/pipe.rs`(与 `os/src/vfs/impls/pipe_file.rs` 协作) -- 消息:`os/src/ipc/message.rs` -- 共享内存:`os/src/ipc/shared_memory.rs` -- 信号:`os/src/ipc/signal.rs` +IPC 子系统为任务之间传递字节流, 离散消息, 共享页和异步事件提供内核侧基础设施。当前实现不是单一框架, 而是一组和 VFS, Task, MemorySpace, syscall 层协作的机制。 + +## 当前状态 + +- Pipe: 字节流端点由 VFS `PipeFile` 暴露为 fd, 底层复用 `ipc::Pipe` 和环形缓冲区。 +- Message: 内核内消息队列, 以消息类型和 payload 为边界, 通过等待队列提供阻塞收发。 +- SysV shared memory: 全局 segment registry 管理 `shmid/key`, syscall 层把 segment 映射进当前 `MemorySpace`。 +- Signal: 任务私有 pending 和线程组共享 pending 共同决定返回用户态前的投递行为。 + +## 目标 + +- 让 IPC 对外表现为清晰的内核对象生命周期, 而不是把实现结构泄漏给 syscall 或应用层。 +- 让等待, 唤醒和信号中断语义集中在内核调度边界处理。 +- 让共享内存的数据面通过页映射共享, 控制面通过 registry 和 attachment table 管理。 + +## 非目标 + +- 不在文档中维护完整 API/字段清单, 具体参数和错误分支以 rustdoc 和源码为准。 +- 不承诺完整 Linux System V IPC 全集。当前正式落地的是 SysV shm, 消息队列仍是内核内基础队列。 +- 不把管道, socket, 消息队列抽象成统一传输层。 + +## 模块边界 + +- `os/src/ipc/`: IPC 核心对象和共享状态。 +- `os/src/kernel/syscall/ipc.rs`: pipe/dup 和 SysV shm syscall 的 ABI 边界。 +- `os/src/vfs/impls/pipe_file.rs`: pipe 的 fd/File 语义。 +- `os/src/kernel/task/`: exit, exec, clone 中和 IPC 相关的资源复制或清理。 +- `os/src/kernel/syscall/signal.rs`: 信号 syscall 和用户态信号栈恢复。 + +## 关键流程 + +1. 用户态通过 syscall 进入 ABI 层。 +2. syscall 层完成 fd 查找, 用户指针复制, flag 校验和 errno 映射。 +3. IPC 核心对象只维护内核状态, 如 ring buffer, message queue, shm registry, pending signal。 +4. 阻塞路径交给调度器或等待队列, 可被可投递信号中断。 +5. 进程退出或 exec 时由 task 清理路径关闭 fd, 分离 shm, 释放地址空间。 + +## 并发和生命周期约束 + +- IPC 对象通常由 `Arc` 持有, 生命周期由 fd table, task attachment table 或全局 registry 共同决定。 +- 等待路径必须避免在持有长生命周期锁时睡眠。 +- SysV shm 的 registry 锁, MemorySpace 锁和 task 锁不能长期嵌套持有。 +- Signal 投递只标记 pending, 真正处理发生在安全检查点。 + +## 已知限制 + +- MessageQueue 当前是内核内队列, 没有完整 msgget/msgsnd/msgrcv/msgctl ABI。 +- Pipe 的阻塞和 poll 语义主要在 VFS `PipeFile` 中体现, `ipc::Pipe` 本身保持为底层字节缓冲对象。 +- Signal 尚未完整实现 SA_RESTART 和实时信号队列。 + +## 源码索引 + +- `os/src/ipc/mod.rs`: IPC 模块导出入口。 +- `os/src/ipc/pipe.rs`: pipe 底层字节缓冲对象。 +- `os/src/ipc/message.rs`: 内核消息队列。 +- `os/src/ipc/shared_memory.rs`: SysV shm segment registry。 +- `os/src/ipc/signal.rs`: pending signal, 默认动作和用户态 handler 安装。 +- `os/src/kernel/syscall/ipc.rs`: pipe2, dup, shmget/shmat/shmdt/shmctl。 +- `os/src/kernel/task/mod.rs`: exit/exec 的 shm detach 和进程资源清理。 ## 导航 -- [管道(Pipe)](./pipe.md) -- [消息(Message)](./message.md) -- [共享内存(Shared Memory)](./shared_memory.md) -- [信号(Signal)](./signal.md) -- [信号生命周期](./signal_lifecycle.md) \ No newline at end of file + +- [管道](./pipe.md) +- [消息](./message.md) +- [共享内存](./shared_memory.md) +- [信号](./signal.md) +- [信号生命周期](./signal_lifecycle.md) diff --git a/document/ipc/message.md b/document/ipc/message.md index eb16cb48..e4b20980 100644 --- a/document/ipc/message.md +++ b/document/ipc/message.md @@ -1,34 +1,62 @@ -# 消息(Message) - -消息机制以离散的消息单元进行通信,适合结构化数据和请求-响应模式。 - -- 源码:`os/src/ipc/message.rs` - -## 设计目标 -- 明确边界:每条消息具备独立边界,避免应用层自行包/拆帧。 -- 背压控制:有界队列,超限时发送方阻塞或返回错误。 -- 可阻塞/非阻塞:与调度器/等待队列协作,提供一致的阻塞语义。 - -## 核心抽象 -- Message:描述消息元数据与负载(类型、来源/目标、长度、权限等,具体字段以实现为准)。 -- 信箱/通道:维护一端或双端的消息队列,内部以 `WaitQueue` 管理收发双方的睡眠与唤醒。 -- 容量与策略:固定或可配置队列深度;丢弃策略(拒绝、覆盖最旧)依据实现而定。 - -## 工作流程 -- 发送: - 1. 校验容量与权限。 - 2. 拷贝用户缓冲到内核消息缓冲(或引用计数封装)。 - 3. 入队并唤醒等待接收的任务。 -- 接收: - 1. 队列为空则阻塞(可中断)或立即返回。 - 2. 出队消息,拷贝到用户缓冲并返回消息长度/元数据。 -- 取消与中断:可中断睡眠,收到信号后返回中断错误码。 - -## 与其他子系统的协作 -- 调度/等待队列:空队列等待、满队列背压均通过 `WaitQueue` 实现。 -- 信号:允许 `recv` 被信号打断。 -- VFS(可选):若实现为文件化端点,可复用通用 `read/write/poll` 接口(以实际实现为准)。 - -## 使用建议 -- 小消息高频通信:优先使用消息通道,避免在管道中自定义帧格式。 -- 大块数据:建议配合共享内存传递数据地址或句柄,消息仅携带控制信息。 \ No newline at end of file +# 消息队列 + +MessageQueue 是内核内的离散消息队列。它保留消息边界, 支持按类型接收, 并通过等待队列提供阻塞收发。 + +## 当前状态 + +- `Message` 包含类型, 大小和 payload。 +- 队列以 `VecDeque` 保存消息, 同时记录当前占用字节数。 +- 默认容量是有界字节数, 发送超限时等待空间。 +- 接收可以取队首消息, 也可以按 `mtype` 查找匹配消息。 +- 当前模块没有暴露完整 System V message queue syscall ABI。 + +## 目标 + +- 为内核内部或后续 syscall ABI 提供清晰的消息边界模型。 +- 在队列空和队列满时使用等待队列表达背压。 +- 支持非阻塞 try 路径, 便于上层根据自身语义选择阻塞策略。 + +## 非目标 + +- 不在本文描述尚未落地的 msgget/msgsnd/msgrcv/msgctl 兼容语义。 +- 不承诺复杂权限模型, 持久化队列 namespace 或消息队列 ID registry。 +- 不将大块数据直接塞入消息队列作为零拷贝通道。 + +## 模块边界 + +- `os/src/ipc/message.rs`: 消息结构, 队列状态, 等待队列。 +- 调度器/等待队列: 在满队列和空队列路径挂起与唤醒任务。 +- 若未来接入 syscall, 用户指针复制和权限检查应放在 syscall 层。 + +## 关键流程 + +发送: + +1. 计算消息大小。 +2. 持有队列状态锁检查容量。 +3. 容量足够时入队并唤醒接收者。 +4. 容量不足时释放状态锁, 进入发送等待队列, 被唤醒后重试。 + +接收: + +1. 持有队列状态锁取队首或匹配类型消息。 +2. 成功出队后减少已用字节数, 唤醒发送者。 +3. 没有可接收消息时进入接收等待队列, 被唤醒后重试。 + +## 并发和生命周期约束 + +- 队列数据受 `Mutex` 保护。 +- 发送等待者和接收等待者分别由 `SpinLock` 管理。 +- 等待前必须释放队列状态锁, 否则发送者和接收者会互相阻塞。 +- 当前源码中保留了丢失唤醒风险的 TODO, 修改等待路径时需要优先验证。 + +## 已知限制 + +- 容量策略简单, 没有 per-user/per-namespace 限额。 +- 没有完整 Linux SysV message queue ABI。 +- 等待队列和状态锁之间的竞态仍需后续审计。 + +## 源码索引 + +- `os/src/ipc/message.rs`: `Message`, `MessageQueue`, 阻塞和 try 收发。 +- `os/src/kernel/scheduler/`: 等待和唤醒基础设施。 diff --git a/document/ipc/pipe.md b/document/ipc/pipe.md index a889f8f7..b7a9b606 100644 --- a/document/ipc/pipe.md +++ b/document/ipc/pipe.md @@ -1,33 +1,56 @@ -# 管道(Pipe) - -管道提供基于内核缓冲区的单向字节流通信,典型用于父子任务或线程间的数据传递。 - -- 源码: - - 内核对象与缓冲:`os/src/ipc/pipe.rs` - - VFS 文件封装:`os/src/vfs/impls/pipe_file.rs` - -## 设计与数据路径 -- 内核缓冲:通常使用环形缓冲区(参考 `os/src/tool/ring_buffer.rs`),在写端写入字节,在读端按序读出。 -- 引用计数:读/写端在 `File` 层分别持有到同一管道对象的引用,端点关闭逻辑据此判断 EOF/EPIPE。 -- 同步与阻塞:内部维护两个 `WaitQueue`(读队列/写队列),在缓冲区空/满时进行睡眠与唤醒;配合调度器实现让出 CPU。 - -## 语义要点 -- EOF:写端全部关闭后,读端读到缓冲区耗尽返回 0。 -- EPIPE/SIGPIPE:无读者时写入返回错误,并可触发 `SIGPIPE`(由 `signal` 模块注入)。 -- 阻塞/非阻塞: - - 阻塞读:缓冲为空则睡眠,直到有新数据或写端全部关闭。 - - 阻塞写:缓冲为满则睡眠,直到有空间或读端全部关闭(报错)。 - - 非阻塞模式返回类 `EAGAIN`/`EWOULDBLOCK`(具体常量以实现为准)。 -- 原子性:小于等于实现规定的原子写入大小的写操作在语义上应尽量保持原子(同一写调用中的字节不被其他写穿插)。 - -## 与 VFS 的集成 -- `pipe()` 创建成对的读/写端 `File`,通过 `fd_table` 暴露为整数 fd。 -- 文件操作实现 `read/write/poll/close` 等通用接口,支持被 `dup`/`fork` 共享。 - -## 与调度/等待队列 -- 读空或写满路径进入 `WaitQueue::sleep()`;数据到达或空间释放时 `wake_up_one()`。 -- 可中断睡眠:收到信号时从睡眠返回并携带中断错误码。 - -## 性能与边界 -- 缓冲区大小固定(实现依赖),背压由写阻塞或错误返回体现。 -- 内核态单次拷贝:调用方缓冲区与内核环形缓冲之间的拷贝;跨任务通信无需额外拷贝。 +# 管道 + +Pipe 提供单向字节流 IPC。当前设计分成两层: `ipc::Pipe` 负责共享环形缓冲区, VFS `PipeFile` 负责 fd 语义, 阻塞/非阻塞和 poll 边界。 + +## 当前状态 + +- `make_pipe()` 创建读端和写端, 两端共享同一个 `PipeRingBuffer`。 +- 底层缓冲以字节为单位读写, 不保存消息边界。 +- syscall 层的 `pipe2()` 创建 `PipeFile` 对, 放入当前任务 fd table, 再把 fd 写回用户空间。 +- `PipeRingBuffer` 保存写端弱引用, 用于判断写端是否已经全部释放。 + +## 目标 + +- 把 pipe 作为普通 fd 暴露给 VFS I/O 路径。 +- 让底层 IPC 代码只关心字节流和端点生命周期。 +- 让用户指针复制, fd 分配, close-on-exec 等策略停留在 syscall/VFS 层。 + +## 非目标 + +- `ipc::Pipe` 不直接实现 Linux pipe syscall 的全部错误分支。 +- 不在 pipe 层保存应用协议消息边界。 +- 不在本文维护 ring buffer 容量和每个 errno 的清单。 + +## 模块边界 + +- `os/src/ipc/pipe.rs`: 共享缓冲区和读写端对象。 +- `os/src/vfs/impls/pipe_file.rs`: `File` trait, read/write/poll/close 语义。 +- `os/src/kernel/syscall/ipc.rs`: `pipe2()` 参数校验和 fd table 写入。 +- `os/src/kernel/syscall/io.rs`: 通用 read/write 重试, `WouldBlock`, 信号中断。 + +## 关键流程 + +1. `pipe2()` 校验 flag, 创建读端和写端文件对象。 +2. fd table 分配两个 fd, 失败时回滚已分配 fd。 +3. 读写 syscall 通过 VFS `File` 进入 pipe 文件对象。 +4. pipe 文件对象在需要时访问 `ipc::Pipe` 的 ring buffer。 +5. 写端全部关闭后, 读侧可根据 VFS 层状态形成 EOF。 + +## 并发和生命周期约束 + +- 共享缓冲区由 `Arc>` 保护。 +- `PipeRingBuffer` 使用写端 `Weak` 判断端点释放, 避免写端和缓冲区互相强引用。 +- 用户缓冲区复制发生在 syscall/VFS I/O 边界, IPC 层不保存用户指针。 +- 阻塞等待必须在释放缓冲区锁后进行。 + +## 已知限制 + +- 底层 `ipc::Pipe` 是尽量简单的字节缓冲封装, 复杂语义依赖 VFS pipe 文件实现。 +- 文档不承诺固定的 pipe 原子写大小, 以源码和测试为准。 + +## 源码索引 + +- `os/src/ipc/pipe.rs`: `Pipe`, `PipeRingBuffer`, `make_pipe()`。 +- `os/src/vfs/impls/pipe_file.rs`: pipe 作为 `File` 的行为。 +- `os/src/kernel/syscall/ipc.rs`: `pipe2()`。 +- `os/src/kernel/syscall/io.rs`: 通用 I/O 等待和信号中断。 diff --git a/document/ipc/shared_memory.md b/document/ipc/shared_memory.md new file mode 100644 index 00000000..1f0221b4 --- /dev/null +++ b/document/ipc/shared_memory.md @@ -0,0 +1,121 @@ +# SysV 共享内存 + +SysV shared memory 当前由全局 segment registry 和每个任务的 attachment table 共同管理。registry 管理 `shmid/key` 生命周期, task attachment table 管理某个进程地址空间里的映射关系。 + +## 当前状态 + +- `shmget` 创建或查找 segment。 +- `shmat` 把 segment 的物理页映射进当前 `MemorySpace`。 +- `shmdt` 从当前进程分离指定映射。 +- `shmctl` 当前支持 `IPC_STAT` 和 `IPC_RMID`。 +- exit 和 exec 都会分离当前进程持有的 shm attachments。 +- clone/fork 会根据是否共享 VM 决定 attachment table 是共享还是复制, 非线程 clone 会增加 segment attach 计数。 + +## 目标 + +- 把 SysV shm 的命名, 权限和删除标记放在全局 registry。 +- 把实际页映射放在 `MemorySpace`, 让共享数据面走页表而不是内核拷贝。 +- 让 exit/exec cleanup 能统一回收映射并更新 attach 计数。 + +## 非目标 + +- 当前不支持 huge page shm。 +- 当前不维护完整 Linux shm tunables 和 namespace。 +- 不在本文列出所有 flag 和 errno 分支, 以 `uapi::ipc` 和 syscall 源码为准。 + +## Segment registry + +全局 `SHM_REGISTRY` 保存两张索引: + +- `by_id`: `shmid -> Arc`。 +- `by_key`: `key -> shmid`, `IPC_PRIVATE` 不进入 key 索引。 + +`ShmSegment` 保存 segment 元数据, 物理页 frame 列表和受锁保护的运行时状态: + +- `marked_removed`: 是否已被 `IPC_RMID` 标记删除。 +- `attach_count`: 当前 attach 数。 +- `atime/dtime/ctime/lpid`: SysV stat 所需的时间和最后操作 pid。 + +删除采用延迟语义: `IPC_RMID` 先移除 key 可见性并标记 removed, 如果仍有 attachment, segment 会等最后一次 detach 后再从 registry 移除。 + +## shmget 生命周期 + +`shmget` 的核心语义是"按 key 查找或创建": + +1. 拒绝当前不支持的 huge page flag。 +2. 非 `IPC_PRIVATE` key 先查 `by_key`。 +3. 已存在且未删除时, 校验 `IPC_CREAT|IPC_EXCL`, size 和访问权限。 +4. 不存在且未指定 `IPC_CREAT` 时返回 not found。 +5. 创建新 segment, 分配页帧, 建立 `by_id` 和可选 `by_key` 索引。 + +## shmat 映射关系 + +`shmat` 把 registry 中的 segment 接入当前进程: + +1. 查找 segment 并校验读写权限。 +2. 根据 `shmaddr`, `SHM_RND`, `SHM_REMAP` 和空地址 hint 选择起始地址。 +3. 计算覆盖 segment 页数的 `VpnRange`。 +4. 根据 `SHM_RDONLY` 和 `SHM_EXEC` 构造页表权限。 +5. 调用 `MemorySpace::insert_shared_area()` 把 segment frames 映射进当前地址空间。 +6. 更新 segment attach 计数, 并把 `ShmAttachment` 写入当前 task 的 attachment table。 + +attachment table 以用户虚拟起始地址为 key, 保存地址, 长度和 segment 引用。它是后续 `shmdt`, exit 和 exec cleanup 的依据。 + +## shmdt 分离关系 + +`shmdt` 只接受当前进程已经 attach 的起始地址: + +1. 地址必须页对齐。 +2. 从 attachment table 移除对应 attachment。 +3. 调用 `MemorySpace::munmap()` 解除虚拟地址映射。 +4. 调用 `shm_detach_segment()` 更新 segment detach 时间, last pid 和 attach count。 +5. 如果 segment 已被 `IPC_RMID` 标记且 attach count 归零, registry 移除 segment。 + +若 `munmap` 失败, syscall 会把 attachment 放回 table, 保持 task 元数据和地址空间一致。 + +## shmctl 控制关系 + +- `IPC_STAT`: 校验只读权限, 把 segment stat 写回用户缓冲区。 +- `IPC_RMID`: 校验 owner 或 `IPC_OWNER` capability, 标记删除并从 key 索引摘除。 + +`IPC_RMID` 不强制拆除其他进程已映射的地址, 它只阻止后续按 key 查找并等待最后 detach 完成实际释放。 + +## exit/exec cleanup + +`detach_all_shm()` 是统一清理入口: + +1. 从 task 取出 `memory_space` 和 `shm_attachments`。 +2. 用 `mem::take` 清空 attachment table, 避免长时间持有 task 锁。 +3. 对每个 attachment 执行 `MemorySpace::munmap()`。 +4. 对每个 segment 调用 `shm_detach_segment()` 更新 registry 状态。 + +exit 路径在关闭 fd 后调用它, exec 路径在切换到新地址空间前调用它。这样旧程序的 SysV shm 映射不会泄漏到新程序。 + +## clone/fork 关系 + +- `CLONE_VM` 共享地址空间时, attachment table 也随线程共享。 +- 非线程 clone/fork 会复制 attachment table, 并为复制出的每个 attachment 增加 segment attach count。 +- 新地址空间由 `clone_for_fork()` 复制, shared area 的物理页仍指向相同 segment frames。 + +## 并发和生命周期约束 + +- registry 由 `SpinLock` 保护。 +- 每个 segment 的 attach/removal 状态由 segment 内部锁保护。 +- task attachment table 由 `SpinLock>` 保护。 +- cleanup 先取走 attachment 元数据再 munmap 和更新 registry, 避免 task -> memory_space -> registry 的长锁链。 + +## 已知限制 + +- 不支持 `SHM_HUGETLB`。 +- `shmctl` 当前只覆盖 `IPC_STAT` 和 `IPC_RMID`。 +- 没有完整 shm namespace, limits 和 accounting。 +- 权限模型已有 owner/group/other 和 `IPC_OWNER` 检查, 但不是完整 Linux IPC namespace 语义。 + +## 源码索引 + +- `os/src/ipc/shared_memory.rs`: `ShmSegment`, registry, 权限和删除语义。 +- `os/src/kernel/syscall/ipc.rs`: `shmget`, `shmat`, `shmdt`, `shmctl`。 +- `os/src/kernel/task/mod.rs`: `detach_all_shm()` 和 exit cleanup。 +- `os/src/kernel/syscall/task/exec_ops.rs`: exec 前 detach shm。 +- `os/src/kernel/syscall/task/clone_ops.rs`: clone/fork 的 attachment table 复制和 attach count。 +- `os/src/mm/memory_space/`: shared area 映射和 `munmap()`。 diff --git a/document/ipc/signal.md b/document/ipc/signal.md index a85635a1..4aa80c8a 100644 --- a/document/ipc/signal.md +++ b/document/ipc/signal.md @@ -1,29 +1,60 @@ -# 信号(Signal) +# 信号 -信号为异步事件通知机制,用于控制流管理、唤醒可中断睡眠、传达错误(如 `SIGPIPE`)。 +信号是任务异步事件机制。当前实现把"产生信号"和"处理信号"分离: 发送路径只把信号放入 pending 集合, 返回用户态前的检查点再决定忽略, 默认处理或安装用户态 handler。 -- 源码:`os/src/ipc/signal.rs` -- 相关:调度器/等待队列、任务管理 +## 当前状态 -## 概念与目标 -- 异步:可在目标任务不主动配合的情况下投递“待处理事件”。 -- 可中断:中断阻塞型系统调用/等待,返回中断错误码,交由上层恢复或重试。 -- 默认动作:对部分信号可定义缺省行为(忽略、终止等,具体以实现为准)。 +- 每个任务有私有 pending, 线程组共享 pending 和 blocked mask。 +- 信号动作表保存默认, 忽略或用户 handler 配置。 +- `check_signal()` 在安全点选择第一个未屏蔽 pending 信号处理。 +- 默认动作覆盖终止, core dump stub, stop, continue 和 ignore。 +- 用户 handler 通过构造 `rt_sigframe` 并修改 trap frame 进入用户态。 +- `rt_sigreturn` 从用户栈恢复被信号打断前的上下文。 -## 关键组成 -- 待处理队列/位图:每个任务维护“待处理信号”集合。 -- 屏蔽/处理:任务可设置屏蔽集与(可选的)处理方式;未处理时采用默认动作。 -- 派发点:陷入返回(trap return)或显式检查点派发并执行处理逻辑。 +## 目标 -## 常见语义 -- 投递:根据目标(任务/进程)查找对象,将信号标记为待处理并尝试唤醒。 -- 唤醒:若目标处于“可中断睡眠”,立即从等待队列移出并返回中断错误。 -- SIGPIPE:对无读者管道写入时由管道模块触发。 -- 不可屏蔽/强制:如 `SIGKILL` 之类(若实现)绕过屏蔽直接生效。 +- 让信号成为阻塞 syscall 和任务控制的统一异步事件来源。 +- 把用户态 ABI, 如 sigaction/sigprocmask/sigreturn, 放在 syscall 层。 +- 把具体架构寄存器恢复封装在 trap frame 抽象之后。 -## 与调度/等待队列 -- 可中断睡眠通过 `WaitQueue` 实现,中断路径在唤醒后返回特定错误码,由上层重启或终止操作。 +## 非目标 -## 使用建议 -- 将信号用于控制流与唤醒;不要在信号处理上下文做复杂/阻塞操作。 -- 对可重启的阻塞调用,收到中断错误后按需重试。 +- 当前不实现完整实时信号队列。 +- 当前不完整实现 SA_RESTART, 因此部分阻塞 syscall 会直接向用户态暴露 `EINTR`。 +- 不在文档列出所有信号编号和默认动作分支。 + +## 模块边界 + +- `os/src/ipc/signal.rs`: pending 选择, 默认动作, 用户 handler 栈帧安装。 +- `os/src/kernel/syscall/signal.rs`: sigaction, sigprocmask, sigpending, sigtimedwait, sigsuspend, sigreturn。 +- `os/src/kernel/task/`: 任务状态, 线程组, exit/stop/continue。 +- `os/src/arch/*/trap/`: 返回用户态前的检查点和 trap frame 恢复。 + +## 关键流程 + +1. syscall 或内核事件向目标任务/线程组标记 pending。 +2. 阻塞等待路径用 `signal_interrupts_syscall()` 判断是否应返回 `EINTR`。 +3. 返回用户态前调用 `check_signal()`。 +4. 内核从私有 pending 或共享 pending 中找第一个未屏蔽信号。 +5. 默认/忽略动作在内核完成, 用户 handler 动作通过修改用户 trap frame 完成投递。 +6. 用户 handler 结束后调用 `rt_sigreturn`, 内核恢复保存的上下文和 blocked mask。 + +## 并发和生命周期约束 + +- pending 和动作表受 task 内部锁保护。 +- 选择可投递信号时会同时考虑 private pending, shared pending 和 blocked mask。 +- 安装用户 handler 时必须写用户栈, 失败路径需要谨慎处理, 避免破坏原 trap frame。 +- `SIGKILL` 和 `SIGSTOP` 这类不可屏蔽语义不能被普通 mask 延迟。 + +## 已知限制 + +- `siginfo_t` 字段只填充基础信息。 +- core dump 仍是 stub。 +- `SIGCHLD` 等默认忽略信号在 syscall 中断判断上有兼容性特判, 但不是完整 SA_RESTART。 + +## 源码索引 + +- `os/src/ipc/signal.rs`: signal pending, 投递, 默认动作。 +- `os/src/kernel/syscall/signal.rs`: 信号 syscall。 +- `os/src/uapi/signal.rs`: 用户态 ABI 数据结构和常量。 +- `os/src/arch/*/trap/`: 信号检查点和 trap frame 恢复。 diff --git a/document/ipc/signal_lifecycle.md b/document/ipc/signal_lifecycle.md index 0e84eec5..c4804cd9 100644 --- a/document/ipc/signal_lifecycle.md +++ b/document/ipc/signal_lifecycle.md @@ -1,74 +1,72 @@ # 信号生命周期 -本文档描述了IPC子系统中的信号机制及其工作流程 +本文描述信号从产生到恢复原执行流的设计路径。具体 syscall 参数, 信号编号和错误分支以源码为准。 -## 概述 +## 当前状态 -信号是系统中用于实现**异步事件通知**的 IPC(进程间通信)机制。信号的处理过程被划分为三个核心阶段:**发送、递送**和**处理**,由内核的不同子系统和架构代码负责。 +信号生命周期分为四段: ---- +- 产生: syscall, trap, 内核事件或 IPC 路径请求发送信号。 +- 挂起: 目标任务或线程组的 pending 集合记录信号。 +- 投递: 返回用户态前检查未屏蔽信号。 +- 恢复: 默认动作直接在内核完成, 用户 handler 通过 `rt_sigreturn` 恢复上下文。 -## I. 信号的产生与发送(Sending) +## 目标 -这一阶段的目标是将信号标记给目标进程,使其进入**挂起(Pending)**状态。 +- 保证信号只在安全检查点改变用户态执行流。 +- 让阻塞 syscall 能被真正需要处理的信号打断。 +- 把用户栈帧格式集中在信号投递和 `rt_sigreturn` 两端。 -### 1. 事件触发与系统调用 +## 非目标 -* **负责者:** 用户程序、硬件、C 库 (`glibc`)。 -* **过程:** - * **软件触发:** 用户调用 `kill()`、`raise()` 等函数,这些函数通过 C 库包装,最终执行 `syscall` 指令进入内核态。 - * **硬件触发:** 如除零 (`SIGFPE`) 或非法内存访问 (`SIGSEGV`) 等异常,由 CPU 捕获后交由内核的异常处理程序处理。 - * **内核触发:** 如定时器到期 (`SIGALRM`),由内核的计时器子系统发送。 +- 不描述所有信号默认行为表。 +- 不承诺完整 POSIX/Linux restart semantics。 +- 不把信号处理函数视为内核回调, handler 始终在用户态运行。 -### 2. 内核标记 +## 关键流程 -* **负责者:** 内核的进程管理子系统和信号子系统。 -* **过程:** - * 内核检查发送方权限。 - * 内核在目标进程的 PCB (`task_struct`) 中,修改其 `pending` 信号位图,将信号标记为**已到达**。 - * 如果目标进程正在阻塞等待,内核可能会将其唤醒。 +### 1. 发送 ---- +发送方根据目标 pid/tid/tgid 找到任务, 校验基本参数后把对应 signal bit 放入 pending 集合。进程级信号进入共享 pending, 线程定向信号进入任务私有 pending。 -## II. 信号的检查与递送(Delivering) +### 2. 等待中断 -这一阶段负责在最安全的时机将信号插入进程的执行流。 +阻塞 I/O, poll, socket 等路径在让出 CPU 后会检查 pending 信号。只有未被屏蔽且动作不是默认忽略/显式忽略的信号才应让 syscall 返回 `EINTR`。 -### 3. 检查点(Checkpoint) +### 3. 返回用户态前投递 -* **负责者:** 内核的系统调用和中断返回路径。 -* **时机:** 进程执行完成一个系统调用、中断或异常处理,即将**从内核态返回到用户态**的瞬间。 -* **过程:** 内核检查当前线程是否有待处理的挂起信号。 +`check_signal()` 读取当前任务状态: -### 4. 递送决策 +1. 先检查私有 pending。 +2. 再检查共享 pending。 +3. 过滤 blocked mask。 +4. 取编号最小的可投递信号。 +5. 根据动作表执行默认动作, 忽略或安装用户态 handler。 -* **负责者:** 内核信号子系统。 -* **过程:** - * **屏蔽检查:** 检查信号是否被当前线程的**信号屏蔽集** (`blocked`) 屏蔽。如果被屏蔽,信号保持挂起,不进行处理。 - * **处理方式判断:** 检查进程的**信号处理表** (`sighand`),确定信号处理方式:默认、忽略,或用户自定义函数。 +### 4. 用户 handler ---- +自定义 handler 需要内核在用户栈上写入 `rt_sigframe`, 保存被打断时的 mcontext 和 sigmask。随后内核修改 trap frame, 让用户态从 handler 入口继续执行。 -## III. 信号的处理与返回(Handling) +### 5. sigreturn -最终的处理操作,可能在内核态完成,也可能在用户态完成。 +handler 结束后进入 `rt_sigreturn` syscall。内核从用户栈读取 ucontext, 恢复 blocked mask 和 trap frame, 再返回原执行点。 -### 5. 最终处理操作 +## 并发和生命周期约束 -#### 选项 A:内核处理(默认或忽略) +- pending 信号只表示有事件待处理, 不等于马上改变执行流。 +- 共享 pending 需要在线程组语义下处理, 避免多个线程重复消费同一信号。 +- stop/continue 会修改线程组内多个任务状态, 必须避开 Zombie 任务。 +- 终止信号会走进程级资源清理, 包括 fd 和 SysV shm detach。 -* **负责者:** 内核进程/信号子系统。 -* **过程:** 如果信号是忽略 (`SIG_IGN`),内核直接清除 `pending` 标志并恢复执行。如果信号是默认行为(如 `SIGKILL`),内核在内核态直接终止或修改进程状态。 +## 已知限制 -#### 选项 B:用户处理(捕获) +- 实时信号队列未完整实现。 +- `SA_RESTORER` 支持基础兼容, 默认 restorer 依赖架构 trampoline。 +- `SA_RESTART` 尚未完整驱动 syscall restart。 -* **负责者:** 内核的架构相关代码。 -* **过程:** - * 内核将原有的用户态寄存器上下文和信号信息压入**用户栈**,构建**信号栈帧**。 - * 内核修改进程的**程序计数器(PC/RIP)**,指向用户注册的信号处理函数地址。 - * 进程返回用户态后,立即开始执行 Handler 代码。 +## 源码索引 -### 6. 恢复执行 - -* **负责者:** C 库的 `sigreturn()` 函数和内核。 -* **过程:** 用户 Handler 执行完毕后,调用 `sigreturn` 系统调用,通知内核。内核读取用户栈上的保存的上下文信息,恢复进程被中断前的寄存器状态,进程继续执行原有代码。 \ No newline at end of file +- `os/src/ipc/signal.rs`: `check_signal()`, 默认动作, handler 栈帧安装。 +- `os/src/kernel/syscall/signal.rs`: 信号 ABI 和 `rt_sigreturn()`。 +- `os/src/kernel/task/mod.rs`: 终止路径的资源清理。 +- `os/src/uapi/signal.rs`: `SignalAction`, `RtSigFrame`, `UContextT`。 diff --git a/document/kernel/boot.md b/document/kernel/boot.md new file mode 100644 index 00000000..b2b305e1 --- /dev/null +++ b/document/kernel/boot.md @@ -0,0 +1,84 @@ +# 内核启动设计 + +本文描述架构入口交给通用内核后的启动模型.具体初始化函数参数和错误分支以源码为准. + +## 当前状态 + +启动流程已经从架构目录收敛到 `os/src/kernel/boot.rs`.RISC-V 和 LoongArch 只提供 `PrimaryBootOps` hook, 公共代码负责 BSS 清零,内存管理,trap/platform/time/timer 初始化,idle task 建立,PID 1 创建和进入 `/sbin/init`. + +RISC-V 在公共启动中插入 CPU 指针初始化和从核启动.LoongArch 当前插入基础 FPU 使能, 多核启动尚未接入. + +## 目标和非目标 + +目标: + +- 让两个架构共享同一条内核启动主线. +- 保证每个在线 CPU 都有 `Cpu` 结构,当前任务,当前地址空间和 idle task. +- 在启用中断前完成 trap,timer 和 PID 1 的最小可调度状态. +- 把 rootfs,网络默认接口和用户态 init 放到 PID 1 中完成, 避免早期启动路径继续膨胀. + +非目标: + +- 不在架构启动代码里复制 `rest_init` 或用户态初始化. +- 不在正式文档中列出每个设备驱动的初始化细节. +- 不保证所有架构具有相同 SMP 能力; 能力差异应由架构 hook 和限制说明表达. + +## 模块边界 + +- 架构入口: 设置机器状态, 然后调用 `kernel::boot::run_primary_boot`. +- `PrimaryBootOps`: 暴露少量有序 hook, 用于 BSS 前后,MM 初始化后,time 初始化后. +- `run_primary_boot`: 维护主核公共启动顺序. +- `rest_init`: 创建 PID 1 并放入 CPU0 调度队列. +- `init`: 作为 PID 1 运行, 创建 `kthreadd`, 初始化 rootfs/网络, 最后执行 `/sbin/init`. +- `create_idle_task`: 为指定 CPU 创建不进入普通运行队列的 idle 任务. + +## 主核流程 + +```text +arch::boot::main + -> before_clear_bss + -> clear_bss + -> after_clear_bss + -> early tests and boot log + -> mm::init + -> after_mm_init + -> switch to kernel address space + -> init_boot_trap + -> platform::init + -> time::init + -> after_time_init + -> timer::init + -> create CPU0 idle task + -> trap::init + -> rest_init + -> enable interrupts + -> idle_loop +``` + +`rest_init` 创建的 PID 1 初始仍是内核任务形态, 第一次被调度后进入 `init`, 再通过 `kernel_execve("/sbin/init")` 变成用户态 init.这样可以在完整调度,trap 和文件系统上下文中完成剩余初始化. + +## 从核流程 + +RISC-V 从核通过 SBI HSM 启动到 `secondary_start`.从核建立自己的 `Cpu` 指针,idle task,全局内核地址空间,trap 和 timer, 然后启用中断进入 idle loop.主核用在线位图等待从核上线, 超时后按实际上线数量继续运行. + +LoongArch 当前没有对应的多核 bringup 流程, `num_cpu` 保持单核语义. + +## 并发和生命周期约束 + +- `current_cpu().switch_space` 和 `current_cpu().switch_task` 必须在不可迁移区域内执行. +- idle task 是每 CPU 生命周期资源, 不进入普通 run queue, 只在没有可运行任务时作为兜底上下文. +- PID 1 必须固定为 `tid == pid == 1`, 否则用户态 init 语义会出错. +- 从核上线前只能依赖全局内核页表和 per-CPU idle; 不能假设用户任务已经可迁移到该 CPU. + +## 已知限制 + +- LoongArch 尚无 SMP 启动和 IPI 唤醒. +- `idle_loop` 当前执行一次架构 halt 后依赖中断返回路径继续调度, 不是复杂的电源管理循环. +- `init` 中 rootfs 和网络初始化失败时只记录警告并继续, 便于 bringup, 但不是最终的生产级启动策略. + +## 源码索引 + +- `os/src/kernel/boot.rs`: 公共启动流,PID 1,kthreadd 和 idle task. +- `os/src/arch/riscv/boot/mod.rs`: RISC-V CPU 指针,SBI HSM 从核启动和在线等待. +- `os/src/arch/loongarch/boot/mod.rs`: LoongArch 主核入口和基础 FPU 使能 hook. +- `os/src/kernel/cpu.rs`: per-CPU 状态,当前任务,当前地址空间和 idle task. diff --git a/document/kernel/task/README.md b/document/kernel/task/README.md index 14080286..75b84b66 100644 --- a/document/kernel/task/README.md +++ b/document/kernel/task/README.md @@ -1,138 +1,92 @@ -# 进程模块概述 +# 任务子系统设计 -## 简介 +任务子系统定义内核的运行单元,生命周期和阻塞/唤醒边界.字段细节和具体 syscall 分支请看源码与 rustdoc. -这篇文档简要记录了进程模块的设计思路,具体实现细节见该文件夹下各个文件 +## 当前状态 -### 导航 +Comix 使用统一的 `Task` 表示进程,线程和内核线程.进程是 `pid == tid` 的线程组 leader, 线程与 leader 共享部分资源, 内核线程没有用户地址空间.所有任务通过 `SharedTask = Arc>` 在调度器,任务管理器,wait queue,信号和文件系统之间传递. -- **[任务的结构及生命周期管理](./task.md)** -- **[任务的上下文](./context.md)** -- **[任务的调度](./scheduler.md)** -- **[等待队列与任务阻塞](./wait_queue.md)** -- **[任务的内存空间](./memory_space.md)** +当前任务模型已经支持: -## 设计 -### 概要 +- 每任务独立内核栈,`TrapFrame` 和调度 `Context`. +- 用户任务与内核线程共用第一次调度入口 `forkret`. +- `execve` 替换用户地址空间,重建用户栈和 `TrapFrame`. +- `TASK_MANAGER` 维护全局 tid 映射,父子关系查询和退出状态. +- 调度器负责运行状态转换和 run queue 维护. -本模块将进程称为 **Task**,并不区分线程与进程。我们假定: +## 目标和非目标 -**线程是共享某些资源的 Task**。 +目标: -### 进程的表示 +- 用一个任务模型承载进程,线程和内核线程. +- 将任务身份/资源生命周期与调度队列状态分开维护. +- 让 clone/fork/exec/exit/wait 共享清晰的资源所有权约束. +- 通过 `Arc` 表达线程共享资源, 通过独立内核栈和上下文表达可调度实体. -#### Task 结构 +非目标: -`Task` 结构体表示一个程序及其运行所需的资源和信息。其主要职责是管理与任务相关的资源和调度信息。 +- 不在文档中维护 `Task` 字段大全. +- 不把 Linux 完整调度策略或完整线程组语义放进当前模型. +- 不在任务模块直接实现具体架构的 trap 保存格式. -```rust -pub struct Task { - /// 中断上下文。指向任务内核栈上的 TrapFrame,仅在任务被中断时有效。 - pub trap_frame_ptr: AtomicPtr, - - /// 任务的内存空间。对于内核任务,该字段为 None。 - pub memory_space: Option>, - - // TODO: 存放任务持有的文件句柄 - // ...... -} -``` - -#### 资源管理 - -任务执行需要以下资源: - -1. **CPU**:用于计算,通常包含寄存器、ALU、控制单元等。 -2. **内存**:用于存储数据(包括代码)。内存通过 MMU 被虚拟化成虚拟内存。 -3. **外部设备**:用于与外部世界交互,抽象为文件(通过 VFS 访问)。 - -#### 调度相关信息 - -`Task` 还需要包含调度器所需的关键信息,以便任务切换和调度。关键字段包括: - -* `context`:最小上下文,包含任务切换时需要恢复的寄存器(例如 `sp`、`ra` 等)。 -* `state`:任务当前的状态,可能的值包括: +## 模块边界 - * `Running`:任务正在执行。 - * `Interruptible`:任务可以被中断。 - * `Uninterruptible`:任务无法被中断。 - * `Stopped`:任务已经终止。 -* `priority`:任务的优先级,供调度器参考。 -* `preempt_count`:抢占计数,防止在关键区域内被抢占。 -* `kstack_base`:内核栈的基地址,供内核线程或陷阱处理使用。 -* `trap_frame_ptr`:指向当前任务内核栈上的 TrapFrame,用于处理中断或从陷阱返回。 +- `task_struct.rs`: 任务对象,共享资源引用,创建和 exec 上下文重建. +- `task_manager.rs`: tid 分配,全局任务表,退出状态和任务查询. +- `process.rs`,`ktask.rs`: 用户进程/内核线程创建和进程级操作. +- `scheduler/*`: run queue,状态迁移,CPU 选择和上下文切换计划. +- `arch/*/kernel/task.rs`: 架构 ABI 相关的用户栈和返回准备. -### 进程的生命周期 +任务模块可以持有文件,地址空间,信号和凭证对象的引用, 但这些对象的内部规则由各自子系统负责. -`Task` 的生命周期从创建到销毁,涉及多个状态转换,常见状态包括:创建、运行、就绪、阻塞、退出。 +## 生命周期 -#### 1. 创建(Created) - -* 通过 `ktask_create` 或 `utask_create` 创建。 -* 分配内核栈、TrapFrame,并初始化 `context` 和 `trap_frame`。 -* 初始状态为 **Running**。 - -#### 2. 运行(Running) - -* 当前 CPU 正在执行该任务。 -* 状态保持为 **Running**。 - -#### 3. 就绪(Runnable / Ready) - -* 任务已准备好等待 CPU 调度。 -* 被放入就绪队列,等待调度器选择。 -* 状态为 **Ready**。 +```text +create + -> Running and queued + -> scheduled on CPU + -> running in kernel or user mode + -> sleep on event + -> wake and queued again + -> exit to Zombie + -> parent wait or release removes global reference +``` -#### 4. 睡眠(Blocked) +`Running` 同时表示"正在 CPU 上运行"或"可运行并在队列中等待".当前模型没有单独的 Ready 状态, 是否已经入队由调度器队列和 `on_cpu` 辅助表达. -* 任务因等待某些资源(如 I/O、锁、信号等)被阻塞。 -* 阻塞状态分为: +退出路径分两层: - * **Interruptible**:任务可以在等待期间被中断。 - * **Uninterruptible**:任务无法被中断。 -* 任务将加入 `waitqueue`,在满足条件时被唤醒。 +- 任务管理器写入退出码并维护全局任务表. +- 调度器把任务状态切到 `Zombie` 并从 run queue 移除. -#### 5. 退出(Stopped / Dead) +进程级退出会先切回全局内核页表, 再释放用户地址空间,关闭 fd,分离 SysV shared memory.线程退出会释放自己对共享地址空间的引用, 最后一个引用由 `Arc` 生命周期回收. -* 任务执行完毕,进入退出状态。 -* 设置 `exit_code` 或 `return_value`,并开始资源回收(如内存、文件句柄等)。 -* 状态为 **Stopped** 或 **Dead**。 +## 并发和生命周期约束 -### 典型 API 操作 +- `Task` 内部由 `SpinLock` 保护, 不要长期持有任务锁后再调用可能唤醒或调度的路径. +- `TASK_MANAGER` 负责全局可见性, 但不拥有调度状态转换. +- `trap_frame_ptr` 指向任务私有保存区, 同一任务不能被两个 CPU 同时调度运行. +- 多核唤醒必须幂等: 已经是 `Running`,`Zombie` 或 `Stopped` 的任务不能重复入队. +- `execve` 写用户栈前必须确保新地址空间已可访问, 否则会在内核态触发页错误. -以下是任务管理中的常用 API 操作: +## 已知限制 -* **create** / **spawn**:创建并初始化 Task,分配必要资源。 -* **schedule** / **yield**:让出 CPU,触发调度操作。 -* **sleep** / **wake_up**:使任务进入阻塞状态或从阻塞中唤醒,通常通过 `waitqueue` 和锁模块实现。 -* **exit** / **terminate**:终止任务并回收相关资源。 -* **join** / **waitpid**:等待子任务退出,并获取其 `exit_code` 或 `return_value`。 +- `Task` 仍混合了调度热字段和进程资源字段, 后续可拆分为更清晰的运行态/资源态结构. +- `preempt_count` 和通用优先级字段还没有形成完整内核抢占模型. +- 线程用户栈隔离由调用方和 clone 参数保证, 任务结构本身不自动分配用户线程栈. -### 进程状态转换图 +## 文档导航 -```plaintext - +-------------------+ +-------------------+ - | Created |-----> | Running | - +-------------------+ +-------------------+ - | - v - +-------------------+ - | Ready | - +-------------------+ - | - v - +-------------------+ - | Blocked | - +-------------------+ - | - v - +-------------------+ - | Stopped | - +-------------------+ -``` +- [任务结构](task.md): `Task` 模型, 身份关系和资源引用边界. +- [调度器](scheduler.md): per-CPU RR 调度器, CPU 选择和唤醒幂等性. +- [上下文切换](context.md): `Context` 与架构切换边界. +- [内存空间](memory_space.md): 任务和 `MemorySpace` 的生命周期关系. +- [等待队列](wait_queue.md): sleep/wake 约束和阻塞路径. -### 总结 +## 源码索引 -* `Task` 是程序的基本运行单元,负责管理进程的资源和调度。 -* 任务的生命周期经历创建、运行、就绪、睡眠、退出五个状态。 -* 提供了一些核心的 API 来操作任务,支持调度、阻塞、退出等功能。 +- `os/src/kernel/task/task_struct.rs`: `Task`,`SharedTask`,创建,exec 和资源引用. +- `os/src/kernel/task/task_manager.rs`: `TASK_MANAGER`,tid 分配,退出码和全局查询. +- `os/src/kernel/task/mod.rs`: `forkret`,当前任务访问,进程退出资源清理. +- `os/src/kernel/boot.rs`: PID 1,kthreadd 和 per-CPU idle task 创建. +- `os/src/kernel/syscall/task/*`: fork/clone/exec/exit/wait 等 syscall 入口. diff --git a/document/kernel/task/context.md b/document/kernel/task/context.md index a44a855a..4cb9b321 100644 --- a/document/kernel/task/context.md +++ b/document/kernel/task/context.md @@ -1,55 +1,38 @@ -# 任务的上下文 +# 任务上下文 -本文档解释了 `Task` 结构中的 `context` 字段,以及它在任务调度和上下文切换中的核心作用。 +## 当前状态 -## 1. 什么是任务上下文? +Comix 有两类上下文: -任务上下文(`Context`)是一个数据结构,它保存了任务在被切换出去时,为了能在未来被准确无误地恢复执行所需要保存的最小CPU状态。进一步的描述见[执行上下文](../trap/context.md) +- `Context`: 调度切换用的最小上下文, 保存调用约定要求跨函数调用保持的寄存器. +- `TrapFrame`: trap 边界用的完整上下文, 保存异常/中断/系统调用返回所需的寄存器和特权状态. -在 `comix` 中,`Context` 主要用于 **非中断驱动的上下文切换**,例如任务主动调用 `yield_task()` 或因时间片用完而被调度器切换。 +任务结构同时持有二者.普通 `schedule` 使用 `Context`; syscall,timer,IPI,异常返回使用 `TrapFrame`. -**源码链接**: -- `Context` 结构体: [`os/src/arch/riscv/kernel/context.rs`](/os/src/arch/riscv/kernel/context.rs) -- 切换汇编代码: [`os/src/arch/riscv/kernel/switch.S`](/os/src/arch/riscv/kernel/switch.S) +## 设计边界 -## 2. `Context` 结构的设计 +`Context` 是架构私有布局, 但通过 `arch::kernel::context::Context` 暴露给通用调度器.RISC-V 保存 `ra/sp/s0-s11`; LoongArch 保存 `ra/sp/fp+s0-s8`.调度器只传递旧/新 `Context` 指针, 不解释字段. -```rust -// os/src/arch/riscv/kernel/context.rs -#[derive(Debug, Default, Clone, Copy)] -#[repr(C)] -pub struct Context { - pub ra: usize, - pub sp: usize, - s: [usize; 12], // s0..s11 -} -``` +`TrapFrame` 由 trap 汇编入口和架构 `HwTrapFrame` 实现解释.通用任务代码只通过跨架构方法初始化内核线程,exec 返回和 clone/fork 返回. -`Context` 的设计遵循了 RISC-V 调用约定,只保存 **被调用者保存(callee-saved)** 的寄存器。 +## 第一次运行 -- `ra` (Return Address): 返回地址寄存器。对于 `__switch` 函数来说,它保存了调用 `__switch` 的函数的返回地址。 -- `sp` (Stack Pointer): 栈指针寄存器。指向当前任务的内核栈顶。 -- `s0` - `s11`: Callee-saved 寄存器。调用约定规定,如果一个函数(被调用者)要使用这些寄存器,它必须在返回前将它们恢复到调用前的状态。因此,在任务切换时,我们必须为任务保存这些寄存器的值。 +新任务的 `Context.ra` 被设置为 `forkret`.任务第一次被调度时, 汇编 `context_switch` 通过恢复 `ra/sp` 进入 `forkret`, 再由 `forkret` 根据任务类型恢复对应 `TrapFrame`: -**为什么不保存所有寄存器?** +- 内核线程恢复到内核态入口. +- 用户任务恢复到用户态入口和用户栈. -- 调用者保存(caller-saved)的寄存器(如 `a0-a7`, `t0-t6`)由调用者负责保存。因为 `schedule()` 函数调用了 `__switch`,编译器生成的代码已经确保了在调用 `__switch` 前后,这些寄存器的值对于 `schedule()` 函数来说是正确的。因此,`__switch` 无需为任务保存它们。 -- 这种设计使得 `Context` 结构更小,上下文切换更快。当中断发生时,所有寄存器都会被保存在 `TrapFrame` 中,那是一个更完整的上下文。 +## 并发和生命周期约束 -## 3. 上下文切换流程 (`__switch`) +- `Context` 指针在切换期间必须指向仍然存活的任务对象. +- `TrapFrame` 指针属于任务私有保存区, 任务被迁移到其他 CPU 后需要更新其中的 CPU 指针. +- trap handler 中发生调度后, 返回时必须恢复当前任务的 `TrapFrame`, 不能盲目恢复入口参数. -当 `schedule()` 函数决定进行任务切换时,它会调用汇编函数 `__switch(old_ctx_ptr, new_ctx_ptr)`。 +## 源码索引 -1. **保存旧任务上下文**: - - `__switch` 将 `ra` 和 `sp` 寄存器的当前值保存到 `old_ctx_ptr` 指向的 `Context` 结构中。 - - 接着,它将 `s0` 到 `s11` 这12个 callee-saved 寄存器的值也依次保存到 `Context` 的 `s` 数组中。 - -2. **恢复新任务上下文**: - - `__switch` 从 `new_ctx_ptr` 指向的 `Context` 结构中,将之前为新任务保存的 `ra`, `sp`, `s0-s11` 的值加载回 CPU 的物理寄存器。 - -3. **返回并切换执行流**: - - `__switch` 的最后一条指令是 `ret`。 - - `ret` 指令会将 `ra` 寄存器中的值加载到程序计数器 `pc` 中。由于 `ra` 刚刚从新任务的 `Context` 中恢复,CPU 的执行流便无缝地切换到了新任务上次被切走的地方。 - - **对于一个新任务**,它的 `ra` 在创建时被初始化为 `forkret` 函数的地址。因此,新任务第一次被调度时,会从 `forkret` 开始执行,并最终通过 `sret` 进入任务的真正入口。 - -这个过程精确地完成了 CPU 核心状态的交接,实现了任务的平滑切换。 \ No newline at end of file +- `os/src/arch/riscv/kernel/context.rs`: RISC-V `Context`. +- `os/src/arch/loongarch/kernel/context.rs`: LoongArch `TaskContext`. +- `os/src/arch/riscv/kernel/switch.S`: RISC-V 上下文切换汇编. +- `os/src/arch/loongarch/kernel/switch.S`: LoongArch 上下文切换汇编. +- `os/src/kernel/task/mod.rs`: `forkret`. +- `os/src/kernel/scheduler/rr_scheduler.rs`: 创建 `SwitchPlan`. diff --git a/document/kernel/task/memory_space.md b/document/kernel/task/memory_space.md index ecd0e697..404a0ede 100644 --- a/document/kernel/task/memory_space.md +++ b/document/kernel/task/memory_space.md @@ -1,59 +1,57 @@ -# 内核虚拟内存设计方案 +# 任务地址空间设计 -这篇文档是对三种常见的操作系统内核虚拟内存设计方案的详细对比和评估,以指导内核进程内存空间的设计。 +## 当前状态 -## 方案概述 +任务通过 `memory_space: Option>>` 关联地址空间.用户任务持有 `Some`, 内核线程持有 `None`.CPU 结构保存当前激活地址空间, 任务切换到用户任务时会激活该任务页表; 内核线程沿用当前 CPU 已有的内核地址空间. -| 方案编号 | 方案描述 | SATP 切换频率 | -| :--- | :--- | :--- | -| **方案 1** | **内核独立页表**:整个内核独立使用一张页表(`satp` 指向内核页表),用户空间页表只映射用户态地址。 | **每次陷阱进入/返回都需要切换 `satp`。** | -| **方案 2** | **内核与用户共享页表(相同 VA 映射)**:内核态和用户态用同一张页表,所有内核态物理地址映射到**相同的虚拟地址 (VA)** 上。 | **仅在切换到不同进程时切换 `satp`。** | -| **方案 3** | **内核与用户共享页表(不同 VA 映射)**:内核态和用户态用同一张页表,但是每张页表的内核态映射的**虚拟地址都不同**。 | **仅在切换到不同进程时切换 `satp`。** | +当前采用"用户映射 + 统一内核映射"的共享页表模型.用户进程的地址空间包含自己的用户区域, 同时包含相同的内核高地址映射, 因此 syscall/trap 进入内核时不需要每次切换到独立内核页表.进程退出释放用户地址空间前, 当前 CPU 会先切回全局内核页表. ---- +## 目标和非目标 -## 详细对比与评价 +目标: -### 方案 1:内核独立页表 (Separate Kernel Page Table) +- 让用户任务拥有独立用户地址空间. +- 让内核代码在所有用户页表中使用一致的高地址映射. +- 让 fork/clone/exec/exit 可以通过 `Arc` 生命周期表达共享和释放. +- 在任务切换时由 CPU 层统一激活页表. -| 优点 (Pros) | 缺点 (Cons) | 评价 (Evaluation) | -| :--- | :--- | :--- | -| **安全性高** | **性能开销大:** 每次从用户态进入内核态(中断/系统调用)或返回用户态时,**都必须**写入 `satp` 切换页表。 | **古老且低效:** 类似于早期 x86 系统中的设计。频繁的 `satp` 切换和随之而来的 TLB 刷新会导致**巨大的性能损失**。不推荐用于高性能系统。 | -| **隔离性强** | **TLB 污染:** 每次切换都会导致用户态 TLB 条目失效,增加开销。 | | -| **设计简单** | **内存开销:** 内核页表和用户页表通常都需要映射完整的虚拟地址空间,但只有一半有用。 | | +非目标: -### 方案 2:内核与用户共享页表 (Shared Page Table with Same Kernel VA) +- 不实现 KPTI 或 per-process kernel mapping 随机化. +- 不在任务文档中展开 VMA,mmap,page fault 的完整策略. +- 不让内核线程拥有独立用户页表. -* **设计:** 每一个进程的页表都包含两部分:用户空间映射 + **统一的内核空间映射**。 +## 关键流程 -| 优点 (Pros) | 缺点 (Cons) | 评价 (Evaluation) | -| :--- | :--- | :--- | -| **效率最高 (零陷阱开销):** 陷阱进入/退出内核时,**无需修改 `satp`**,这是性能的关键。 | **安全性低(TLB 侧信道):** 由于所有进程的内核 VA 相同,易受 Meltdown/Spectre 等侧信道攻击。 | **现代主流设计:** 这是目前大多数高性能 OS 的标准做法。**性能最佳,** 适用于对系统调用和中断延迟敏感的系统。 | -| **TLB 缓存友好:** 切换用户进程时,内核的 TLB 缓存可以保留。 | **KPTI 成本:** 为了缓解侧信道攻击,需要引入如 KPTI 等隔离技术,这会部分牺牲性能。 | | -| **实现简单** | **地址空间冲突:** 必须确保用户进程不会触及内核使用的虚拟地址区域。 | | +### 切换任务 -### 方案 3:内核与用户共享页表 (Shared Page Table with Different Kernel VA) +`current_cpu().switch_task` 设置当前任务.若目标任务不是内核线程, 会把 CPU 当前地址空间切到任务的 `memory_space` 并激活根页表.若目标任务是内核线程, 当前地址空间保持不变. -* **设计:** 每个进程的页表都包含两部分:用户空间映射 + **针对该进程定制的内核空间映射**。 +### exec -| 优点 (Pros) | 缺点 (Cons) | 评价 (Evaluation) | -| :--- | :--- | :--- | -| **高安全性** | **复杂性高:** 每次创建进程时,需要动态生成或链接一份定制的内核映射,增加了内核页表管理的复杂性。 | **定制安全场景:** 适用于对内核空间安全有极致要求的系统,或需要实现内核地址空间随机化(KASLR)的系统。 | -| **地址随机化:** 可以实现针对每个进程的内核虚拟地址空间布局随机化。 | **内存开销:** 每个进程都需要存储一份独立的内核映射页表结构,增加了内核页表占用的物理内存。 | | -| **中断低开销:** 陷阱进入/退出内核时,**仍无需修改 `satp`**。 | **调试困难:** 内核代码在不同进程中运行在不同的虚拟地址上,使调试和日志记录更加复杂。 | | +`execve` 创建或接收新的 `MemorySpace`, 替换当前任务地址空间, 关闭 `CLOEXEC` fd, 构造用户栈, 然后重建 `TrapFrame`.用户栈写入要求新地址空间已可访问. ---- +### exit -## 综合评价与推荐 +进程 leader 退出时先切回全局内核页表, 再关闭 fd,分离 shared memory 并释放用户地址空间引用.线程退出只释放自己对共享地址空间的引用. -| 评价维度 | 方案 1 (独立) | 方案 2 (共享/相同 VA) | 方案 3 (共享/不同 VA) | -| :--- | :--- | :--- | :--- | -| **陷阱/系统调用延迟** | **最差** (需切换 `satp`) | **最佳** (无需切换 `satp`) | **好** (无需切换 `satp`) | -| **切换进程开销** | **差** (双重 `satp` 切换) | **最佳** (仅在进程间切换 `satp`) | **好** (仅在进程间切换 `satp`) | -| **安全性/隔离性** | **高** | **低** (易受侧信道攻击) | **最高** | -| **实现复杂性** | **低** | **低** | **高** | -| **性能** | **低** | **高** | **中/高** | +## 并发和生命周期约束 -## 结论 +- 地址空间对象由 `Arc>` 共享, 修改映射需要持有对应锁. +- 释放当前正在使用的用户页表前必须先切换到全局内核页表. +- TLB 刷新和跨核 shootdown 属于内存管理与架构 IPI 协作边界, 任务层只表达地址空间所有权. +- 内核线程 `memory_space == None`, 调用 `current_memory_space()` 前必须确认当前 CPU 已有有效地址空间. -因性能需求,在目前的内核中,我们采用**方案2** \ No newline at end of file +## 已知限制 + +- 当前模型优先性能和实现简单性, 内核映射在用户页表中可见, 没有 KPTI 隔离. +- 内核线程沿用 CPU 当前地址空间, 这要求内核线程不依赖用户映射语义. +- 更完整的多核 TLB shootdown 完成确认仍需继续完善. + +## 源码索引 + +- `os/src/kernel/task/task_struct.rs`: `Task.memory_space`,exec 地址空间替换. +- `os/src/kernel/task/mod.rs`: `current_memory_space` 和进程退出资源清理. +- `os/src/kernel/cpu.rs`: `switch_task`,`switch_space` 和页表激活. +- `os/src/mm/mod.rs`: MM 初始化,全局内核地址空间记录和页表激活入口. +- `os/src/mm/memory_space/space/*`: `MemorySpace` 创建,clone,drop,mmap 和内核映射. diff --git a/document/kernel/task/scheduler.md b/document/kernel/task/scheduler.md index e52571f0..dabf5489 100644 --- a/document/kernel/task/scheduler.md +++ b/document/kernel/task/scheduler.md @@ -1,78 +1,80 @@ -# 任务的调度 +# 调度器设计 -本文档阐述了 `comix` 内核中的任务调度机制,包括调度器设计、调度时机以及上下文切换流程。 +## 当前状态 -## 1. 调度器设计 (`Scheduler` Trait) +调度器采用 per-CPU `RRScheduler`.每个 CPU 有独立 run queue 和 idle task, 新任务或被唤醒任务按 affinity mask 选择目标 CPU.RISC-V 跨核唤醒会发送 reschedule IPI; LoongArch 当前单核 IPI 为 no-op. -为了实现可扩展和可替换的调度策略,我们抽象出了一个 `Scheduler` Trait。任何具体的调度器实现都必须实现这个 Trait 中定义的方法。 +策略上仍是简单 RR, 但 run queue 弹出时会优先选择更高 `sched_priority` 的任务, 同优先级保持队列相对顺序. -**源码链接**: [`os/src/kernel/scheduler/mod.rs`](/os/src/kernel/scheduler/mod.rs) +## 目标和非目标 -`Scheduler` Trait 定义了以下核心接口: +目标: -- `new() -> Self`: 创建一个新的调度器实例。 -- `add_task(&mut self, task: SharedTask)`: 将一个新任务添加到调度器的运行队列中。 -- `next_task(&mut self) -> Option`: 从运行队列中选择下一个要执行的任务。 -- `prepare_switch(&mut self) -> Option`: 准备进行任务切换。这是调度的核心决策逻辑,它会选择下一个任务,并返回一个包含新旧任务上下文指针的 `SwitchPlan`。 -- `sleep_task(&mut self, task: SharedTask, ...)`: 将一个任务置于睡眠状态,并将其从运行队列中移除。 -- `wake_up(&mut self, task: SharedTask)`: 唤醒一个睡眠中的任务,将其重新放回运行队列。 -- `exit_task(&mut self, task: SharedTask, ...)`: 处理一个任务的退出,将其从调度系统中永久移除。 +- 在每个 CPU 上维护独立运行队列, 减少全局调度锁. +- 保证 wakeup 幂等, 避免同一任务同时进入多个 CPU 的 run queue. +- 用统一 `Scheduler` trait 隔离调度策略和外部任务生命周期调用. +- 在没有可运行任务时切到本 CPU idle task. -## 2. 轮转调度器 (`RRScheduler`) +非目标: -当前内核中实现的具体调度策略是简单的 **轮转调度(Round-Robin Scheduler)**。 +- 不实现完整 CFS/RT 调度语义. +- 不实现通用内核抢占模型. +- 不在调度器里负责进程资源释放或 wait 语义. -**源码链接**: [`os/src/kernel/scheduler/rr_scheduler.rs`](/os/src/kernel/scheduler/rr_scheduler.rs) +## 模块边界 -### 实现机制 +- `scheduler/mod.rs`: per-CPU 调度器数组,CPU 选择,公共 sleep/wake/exit/schedule 接口. +- `rr_scheduler.rs`: run queue 选择,时间片,上下文切换计划. +- `task_queue.rs`: 基于 `SharedTask` 身份的队列容器. +- `wait_queue.rs`: 事件等待队列, 调用调度器完成睡眠和唤醒. +- `kernel/cpu.rs`: 当前任务,当前地址空间和 idle task 切换. -- **运行队列**: `RRScheduler` 内部使用一个 `TaskQueue`(基于 `Vec` 的 FIFO 队列)作为运行队列。新加入的任务被放在队尾,调度器总是从队首取出任务执行。 -- **时间片**: 每个任务被分配一个固定的时间片(`DEFAULT_TIME_SLICE`)。当时钟中断发生时,`RRScheduler::update_time_slice` 方法会被调用,减少当前任务的剩余时间片。当时间片耗尽时,就会触发一次抢占式调度。 +## 关键流程 -## 3. 调度时机 +### schedule -调度器在以下几个关键时刻被触发,以决定是否切换任务: +```text +disable interrupts + -> if current task can keep running and run queue empty, return + -> lock current CPU scheduler + -> choose next task or idle + -> current_cpu.switch_task + -> build SwitchPlan + -> unlock scheduler + -> arch context_switch +restore interrupt state +``` -1. **时钟中断(抢占式调度)**: - - `riscv::timer` 模块设置了定时器,在固定间隔后触发时钟中断。 - - 中断处理程序 `trap_handler` 会调用 `schedule()`。 - - `schedule()` 内部会检查当前任务的时间片是否耗尽。如果是,则会执行 `prepare_switch` 来选择下一个任务,实现抢占。 +调度器锁不覆盖汇编切换本身.`next_task` 生成旧/新 `Context` 指针, 真正保存恢复由架构 `context_switch` 完成. -2. **任务主动让出 (`yield`)**: - - 任务可以调用 `yield_task()` 主动放弃 CPU。 - - `yield_task()` 会直接调用 `schedule()`,立即触发一次调度,将当前任务放回运行队列末尾,并切换到下一个任务。 +### sleep -3. **任务阻塞**: - - 当任务因等待资源(如 `SleepLock`)而需要睡眠时,它会调用 `sleep_task()`。 - - `sleep_task()` 将任务从运行队列中移除,并改变其状态为 `Interruptible` 或 `Uninterruptible`。 - - 随后会调用 `schedule()` 来切换到一个新的可运行任务。 +sleep 只改变任务状态并从所属 CPU run queue 移除, 不隐式切换.调用方通常随后调用 `schedule` 或在当前路径返回到可调度点. -## 4. 上下文切换流程 +`sleep_task_prepare` 把条件检查和睡眠状态转换放在调度器锁内, 用于避免 lost wakeup. -上下文切换是调度机制的核心,它实现了 CPU 执行流从一个任务到另一个任务的平滑过渡。 +### wake -**核心函数**: `schedule()` in [`os/src/kernel/scheduler/mod.rs`](/os/src/kernel/scheduler/mod.rs) +唤醒先按任务 affinity 和在线 CPU mask 选择目标 CPU.随后在目标 CPU 调度器锁下持有任务锁, 如果任务已经是 `Running`,`Zombie` 或 `Stopped`, 直接返回; 否则设置 `Running`,更新 `on_cpu` 并入队.目标 CPU 不是当前 CPU 时发送 reschedule IPI. -**流程详解**: +## 并发和生命周期约束 -1. **触发调度**: 当上述任一调度时机发生时,`schedule()` 函数被调用。 +- 调度入口会禁用中断并在返回时恢复原状态. +- `current_cpu().switch_task` 会切换用户地址空间并更新 `TrapFrame.cpu_ptr`. +- wakeup 必须以任务状态为幂等屏障, 防止同一任务被两个 CPU 同时运行. +- idle task 不在普通 run queue 中, 只作为空队列兜底. +- run queue 内的身份判断基于 `Arc::ptr_eq`, 不是 tid 值. -2. **准备切换 (`prepare_switch`)**: - - `schedule()` 函数会调用当前调度器(`RRScheduler`)的 `prepare_switch` 方法。 - - `prepare_switch` 从 CPU 的本地存储中取出当前任务 (`prev_task`),并从运行队列中选出下一个任务 (`next_task`)。 - - 如果没有其他可运行任务,则不进行切换。 - - 如果有,它会获取 `prev_task` 和 `next_task` 的上下文指针 (`Context`)。 - - 如果 `prev_task` 仍然是可运行状态(例如,时间片用完但未阻塞),它会被重新放回运行队列的末尾。 - - 最后,更新 CPU 的当前任务为 `next_task`,并返回包含新旧上下文指针的 `SwitchPlan`。 +## 已知限制 -3. **执行切换 (`__switch`)**: - - `schedule()` 函数拿到 `SwitchPlan` 后,会调用一个底层的汇编函数 `__switch(old_ctx_ptr, new_ctx_ptr)`。 - - **源码链接**: [`os/src/arch/riscv/kernel/switch.S`](/os/src/arch/riscv/kernel/switch.S) - - `__switch` 函数执行以下操作: - a. **保存旧上下文**: 将当前任务(`old_task`)的 callee-saved 寄存器(如 `ra`, `sp`, `s0-s11`)保存到其 `Context` 结构体中(由 `old_ctx_ptr` 指向)。 - b. **恢复新上下文**: 从新任务(`next_task`)的 `Context` 结构体中(由 `new_ctx_ptr` 指向),将之前保存的寄存器值加载回 CPU 的物理寄存器。 - c. **返回**: `__switch` 函数的最后一条指令是 `ret`。它会跳转到新任务 `Context` 中保存的 `ra` (返回地址)。 - - 对于一个从未执行过的新任务,其 `ra` 在创建时被设置为 `forkret`。 - - 对于一个之前被切换出去的任务,其 `ra` 指向它被切换时 `__switch` 调用的下一条指令。 +- 时间片默认很小, 当前用于可用性而非性能调优. +- `sched_policy` 字段存在, 但完整 Linux 调度策略尚未实现. +- LoongArch 暂无跨核唤醒能力, 因此 per-CPU 设计在该架构上仍以单核方式运行. -通过这个流程,CPU 的执行状态被完整地从一个任务切换到另一个任务,实现了多任务的并发执行。 \ No newline at end of file +## 源码索引 + +- `os/src/kernel/scheduler/mod.rs`: per-CPU 调度器,CPU 选择,sleep/wake/schedule. +- `os/src/kernel/scheduler/rr_scheduler.rs`: RR 策略,idle fallback 和 `SwitchPlan` 创建. +- `os/src/kernel/scheduler/task_queue.rs`: run queue 容器. +- `os/src/kernel/scheduler/wait_queue.rs`: wait queue 与调度器交互. +- `os/src/kernel/cpu.rs`: `switch_task`,地址空间切换和 idle task. diff --git a/document/kernel/task/task.md b/document/kernel/task/task.md index 57317d69..531da738 100644 --- a/document/kernel/task/task.md +++ b/document/kernel/task/task.md @@ -1,107 +1,55 @@ -# 任务结构及生命周期管理 +# Task 模型 -本文档详细说明了 `Task` 的核心数据结构、生命周期状态转换以及暴露给其他内核模块的接口。 +## 当前状态 -## 1. 核心数据结构:`Task` +`Task` 是内核唯一的可调度实体.用户进程,用户线程,内核线程和 idle task 都以 `Task` 表示, 差异由地址空间,pid/tid 关系和共享资源引用体现. -操作系统的核心任务表示是 `Task` 结构体,它统一了进程和线程的概念。所有与执行流相关的信息都封装在其中。 +基本规则: -**源码链接**: [`os/src/kernel/task/task_struct.rs`](/os/src/kernel/task/task_struct.rs) +- `pid == tid` 表示线程组 leader, 即进程. +- `pid != tid` 表示同一进程内的线程. +- `memory_space == None` 表示内核线程. +- 每个任务都有自己的内核栈,`TrapFrame` 保存区和 `Context`. +- 文件表,信号表,命名空间,资源限制等对象按 clone/创建语义通过 `Arc` 共享或复制. -`Task` 结构体的主要字段可以分为以下几类: +## 目标和非目标 -### 1.1 身份与亲属关系 +目标: -这些字段用于唯一标识一个任务并建立任务间的层级关系。 +- 让调度器只关心 `SharedTask`,状态,CPU 归属和上下文. +- 让进程资源可以随 clone/exec/exit 明确共享,复制或释放. +- 让 trap 返回和普通任务切换都能从任务对象找到自己的保存区. -- `tid: u32`: **任务ID (Task ID)**。由 `TidAllocator` 分配的全系统唯一标识符。 -- `pid: u32`: **进程ID (Process ID)**。对于线程,它与创建它的主任务 `pid` 相同。对于一个进程的第一个任务,`pid` 等于其 `tid`。 -- `ppid: u32`: **父任务ID (Parent Process ID)**。 +非目标: -### 1.2 调度与执行上下文 +- 不复制 Linux `task_struct` 的完整字段和状态机. +- 不在 `Task` 文档里展开文件系统,信号和内存管理的内部实现. -这些字段由调度器和中断处理机制在任务切换和执行时使用。 +## 关键流程 -- `context: Context`: **任务上下文**。保存了任务切换时需要恢复的最小寄存器集合(主要是 `ra` 和 `sp`),用于非中断驱动的上下文切换。 -- `trap_frame_ptr: AtomicPtr`: **中断帧指针**。当任务从用户态或内核态陷入(trap)时,CPU的完整上下文(所有通用寄存器、`sepc`、`sstatus`等)被保存在其内核栈上,此指针指向该 `TrapFrame` 的位置。当中断返回时,`__restore` 会用它来恢复现场。 -- `state: TaskState`: **任务状态**。定义在 [`os/src/kernel/task/task_state.rs`](/os/src/kernel/task/task_state.rs),是任务生命周期管理的核心。 -- `preempt_count: usize`: **抢占计数器**。当大于0时,禁止内核抢占,用于保护临界区。 +### 创建 -### 1.3 资源管理 +内核线程和用户任务创建时都会分配内核栈和 `TrapFrame` 保存区, 初始化 `Context.ra = forkret`.第一次被调度时, `forkret` 根据任务是否有用户地址空间选择内核线程恢复或用户态恢复. -这些字段管理任务执行所必需的系统资源。 +### exec -- `kstack_base: usize`: **内核栈顶地址**。每个任务都有自己独立的内核栈。 -- `kstack_tracker` & `trap_frame_tracker`: 用于跟踪内核栈和中断帧所占用的物理页帧,以便在任务销毁时正确回收。 -- `memory_space: Option>`: **内存地址空间**。对于用户任务,它包含了页表、内存映射区域等信息。对于内核线程,此字段为 `None`。 +`execve` 替换当前任务的用户地址空间, 处理 `CLOEXEC` fd, 构造 argv/envp/auxv 用户栈, 最后由架构 `HwTrapFrame` 接口重建返回用户态所需的 `TrapFrame`. -### 1.4 生命周期与退出状态 +### exit -这些字段用于处理任务的终止和父任务的等待。 +任务退出先写入退出码和状态, 再从 run queue 移除.进程 leader 退出时释放进程级资源并唤醒父任务 wait 路径; 非 leader 线程退出时释放线程自己的引用. -- `exit_code: Option`: **退出码**。用于进程,在调用 `exit` 系统调用时设置。 -- `return_value: Option`: **返回值**。用于线程,在线程函数返回时设置。 +## 并发和生命周期约束 -## 2. 任务的生命周期与状态转换 +- `Task` 对象被 `Arc` 持有, 从 `TASK_MANAGER` 移除不等于立即析构. +- 当前 CPU 的任务引用必须在不可迁移上下文中读取. +- 地址空间释放前必须确保 CPU 已切到全局内核页表. +- 任务状态的调度可见变化应经过调度器接口, 避免绕过 run queue. -任务的生命周期由 `TaskState` 枚举驱动,并通过一系列接口函数进行管理。 +## 源码索引 -### 2.1 任务的创建 - -#### 内核线程 - -- **接口**: `kthread_spawn(name: &'static str, entry: fn(usize) -> !, arg: usize)` -- **源码**: [`os/src/kernel/task/ktask.rs`](/os/src/kernel/task/ktask.rs) -- **机制**: - 1. 调用 `TASK_MANAGER` 分配 `tid`。 - 2. 分配内核栈和中断帧所需的物理内存。 - 3. 调用 `Task::ktask_create` 创建 `Task` 实例。此函数会: - - 初始化 `Context`,将 `ra` 指向 `forkret`,`sp` 指向内核栈顶。 - - 初始化位于内核栈上的 `TrapFrame`,将 `sepc` 设置为线程入口点 `entry`,`sstatus` 设置为S模式,并设置好内核栈指针 `x2_sp`。 - 4. 将新创建的任务包装在 `Arc>` (即 `SharedTask`) 中,并交给调度器 `SCHEDULER` 的运行队列。 - -#### 用户任务 (待实现) - -- **接口**: (例如 `utask_create` 或 `sys_clone`) -- **机制**: 与内核线程类似,但需要额外创建和关联一个 `MemorySpace`(用户地址空间),并初始化 `TrapFrame` 以便从S模式返回到U模式执行。 - -### 2.2 任务的执行与切换 - -- **`forkret`**: - - **源码**: [`os/src/kernel/task/mod.rs`](/os/src/kernel/task/mod.rs) - - **机制**: 所有新创建的任务在第一次被调度器选中时,都会从 `__switch` 跳转到 `forkret` 函数。`forkret` 的唯一职责是从当前任务的 `trap_frame_ptr` 中加载中断帧地址,并调用 `restore` 汇编例程。`restore` 会将中断帧中的寄存器值恢复到CPU中,最后通过 `sret` 指令跳转到任务的真正入口点(`sepc`),任务从而开始执行。 - -### 2.3 任务的睡眠与唤醒 - -- **接口**: - - `sleep_task(task: SharedTask, receive_signal: bool)` - - `wake_up(task: SharedTask)` -- **源码**: [`os/src/kernel/scheduler/rr_scheduler.rs`](/os/src/kernel/scheduler/rr_scheduler.rs) (作为 `Scheduler` trait 的一部分) -- **机制**: - - **睡眠**: 当任务需要等待资源时(例如等待一个 `SleepLock`),它会调用 `sleep_task`。调度器会将该任务的 `state` 设置为 `Interruptible` 或 `Uninterruptible`,并将其从运行队列中移除。随后调度器会选择下一个任务运行。 - - **唤醒**: 当资源可用时,持有该资源的模块会调用 `wake_up`。调度器会将任务的 `state` 恢复为 `Running`,并将其重新加入运行队列。 - -### 2.4 任务的终止 - -- **接口**: `terminate_task(return_value: usize) -> !` -- **源码**: [`os/src/kernel/task/mod.rs`](/os/src/kernel/task/mod.rs) -- **机制**: - 1. 当一个内核线程的入口函数返回时,`TrapFrame` 中预设的返回地址 `ra` 会指向 `terminate_task`。 - 2. `terminate_task` 获取当前任务,将其 `state` 设置为 `Stopped`,并保存返回值 `return_value`。 - 3. 它主动调用 `schedule()` 让出CPU,由于任务状态已是 `Stopped`,它将不会再被调度器放回运行队列。 - 4. 任务占用的资源(如内核栈)的最终回收依赖于 `Arc` 的引用计数。当所有对该任务的 `SharedTask` 引用都消失后,`Task` 的 `Drop` 实现会被调用,从而释放内存。 - -## 3. 暴露接口 - -- **`os/src/kernel/task/mod.rs`**: - - `SharedTask`: `Arc>` 的类型别名,是任务在内核中传递的标准形式。 - - `into_shared(task: TaskStruct) -> SharedTask`: 将一个 `Task` 结构包装为 `SharedTask`。 -- **`os/src/kernel/task/ktask.rs`**: - - `kthread_spawn(...)`: 创建内核线程的顶层API。 -- **`os/src/kernel/mod.rs` (通过 `scheduler` 模块暴露)**: - - `yield_task()`: 主动让出CPU,触发一次调度。 - - `sleep_task(...)`: 使指定任务进入睡眠状态。 - - `wake_up(...)`: 唤醒指定任务。 - - `exit_task(...)`: 终止指定任务并设置退出码。 - -这些接口共同构成了任务管理的核心功能,为上层模块(如锁、IPC、系统调用)提供了构建并发服务的基础。 \ No newline at end of file +- `os/src/kernel/task/task_struct.rs`: 任务对象和创建/exec 逻辑. +- `os/src/kernel/task/task_manager.rs`: 全局任务生命周期管理. +- `os/src/kernel/task/process.rs`: 进程级创建,退出和 wait 关系. +- `os/src/kernel/task/ktask.rs`: 内核线程创建. +- `os/src/kernel/task/task_state.rs`: 任务状态定义. diff --git a/document/kernel/task/wait_queue.md b/document/kernel/task/wait_queue.md index afabcd77..f2593a77 100644 --- a/document/kernel/task/wait_queue.md +++ b/document/kernel/task/wait_queue.md @@ -1,65 +1,47 @@ -# 等待队列 (`WaitQueue`) +# 等待队列设计 -等待队列是实现任务同步和阻塞的核心机制。当一个任务需要等待某个条件(如锁被释放、I/O完成)才能继续执行时,它就会被放入一个等待队列中并进入睡眠状态。 +## 当前状态 -**源码链接**: [`os/src/kernel/scheduler/wait_queue.rs`](/os/src/kernel/scheduler/wait_queue.rs) +`WaitQueue` 是事件等待队列, 用于把任务从运行队列移出并在事件满足时重新唤醒.它内部用 `TaskQueue` 保存等待者, 用 `RawSpinLock` 保护队列. -## 1. 设计与目的 +等待队列不拥有任务生命周期.它只保存 `SharedTask` 引用, 状态转换仍委托给调度器. -`WaitQueue` 的主要职责是管理一组正在等待同一个事件的睡眠任务。它通常与一个自旋锁 (`RawSpinLock`) 配合使用,以保证在多核环境下的操作原子性。 +## 目标和非目标 -其核心数据结构如下: -```rust -// os/src/kernel/scheduler/wait_queue.rs -pub struct WaitQueue { - tasks: TaskQueue, - lock: RawSpinLock, -} -``` -- `tasks`: 一个 `TaskQueue`,用于存放等待此队列的 `SharedTask` 句柄。 -- `lock`: 一个自旋锁,用于保护 `tasks` 队列在并发访问时的数据一致性。 +目标: -## 2. 核心接口与工作流程 +- 为锁,定时器,wait 子进程等事件等待提供统一阻塞容器. +- 在唤醒时先从等待队列移除, 再调用调度器唤醒. +- 支持一次唤醒一个或全部等待者. -### `sleep(task: SharedTask)` +非目标: -当一个任务需要阻塞等待时,持有资源的模块(如 `SleepLock`)会调用此方法。 +- 不表达等待条件本身; 条件由调用方维护. +- 不负责进程退出码,信号递送或资源回收. +- 不在持有内部队列锁时执行复杂唤醒工作. -1. 获取 `WaitQueue` 的内部锁 `lock`。 -2. 将需要睡眠的 `task` 添加到内部的 `tasks` 队列中。 -3. 释放 `lock` 锁。 -4. 调用调度器提供的 `sleep_task(task, ...)` 函数,将任务状态设置为 `Interruptible` 或 `Uninterruptible`,并将其从调度器的运行队列中移除。 -5. 触发一次调度 (`schedule()`),CPU切换到其他可运行的任务。 +## 关键流程 -**关键点**: 必须在调用 `sleep_task` **之前** 释放 `WaitQueue` 的内部锁,以避免在持有锁的情况下进行任务调度,这可能导致死锁。 +### 睡眠 -### `wake_up_one()` +调用方把当前任务加入等待队列, 然后调用调度器将其置为 `Interruptible` 或 `Uninterruptible` 并从 run queue 移除.该操作本身不必立即切换 CPU, 调用方应在合适位置进入调度. -当等待的条件满足时,持有资源的模块会调用此方法来唤醒一个等待的任务。 +### 唤醒 -1. 获取 `WaitQueue` 的内部锁 `lock`。 -2. 从 `tasks` 队列的队首弹出一个任务(`pop_task`)。 -3. 释放 `lock` 锁。 -4. 如果成功弹出了一个任务,则调用调度器提供的 `wake_up(task)` 函数。 -5. `wake_up` 函数会将任务的状态改回 `Running`,并将其重新加入到调度器的运行队列中,使其有机会在下一次调度时被执行。 +唤醒路径先在等待队列锁内取出任务, 释放锁后调用 `wake_up_task`.这样可以避免 `WaitQueue -> Scheduler -> Task` 的锁链在等待队列内部长期持有. -### `wake_up_all()` +### lost wakeup 防护 -此方法用于唤醒等待队列中的所有任务,流程与 `wake_up_one` 类似,但它会遍历并清空整个 `tasks` 队列,并逐个唤醒所有任务。 +需要"检查条件并睡眠"的场景应使用原子 prepare 风格接口, 在同一临界区内完成条件检查,入队和状态转换, 避免事件在检查后,睡眠前到达. -## 3. 应用示例:`SleepLock` +## 并发和生命周期约束 -`SleepLock` 是 `WaitQueue` 的一个典型应用场景。 +- 等待队列锁只保护等待者列表, 不保护业务条件. +- 唤醒应在释放等待队列锁后进行. +- 重复唤醒由调度器的 `Running` 状态检查兜底, 但调用方仍应尽量维护清晰的事件状态. -**源码链接**: [`os/src/sync/sleep_lock.rs`](/os/src/sync/sleep_lock.rs) +## 源码索引 -- **`lock()`**: - 1. 尝试获取锁。如果锁已被占用,则获取当前任务的句柄。 - 2. 调用 `SleepLock` 内部 `WaitQueue` 的 `sleep()` 方法,将当前任务放入等待队列并使其睡眠。 - 3. 当任务被唤醒后,它会回到 `lock()` 的循环开头,再次尝试获取锁。 - -- **`unlock()`**: - 1. 释放锁。 - 2. 调用 `WaitQueue` 的 `wake_up_one()` 方法,唤醒一个正在等待此锁的任务。 - -通过 `WaitQueue`,`SleepLock` 实现了当锁不可用时,任务会放弃CPU进入睡眠,而不是空耗CPU进行自旋等待,从而大大提高了系统效率。 +- `os/src/kernel/scheduler/wait_queue.rs`: `WaitQueue` 实现. +- `os/src/kernel/scheduler/mod.rs`: `sleep_task`,`wake_up_task`,`sleep_task_prepare`. +- `os/src/kernel/scheduler/task_queue.rs`: 等待队列复用的任务队列容器. diff --git a/document/kernel/trap/context.md b/document/kernel/trap/context.md index 2cf5f8d7..8d0b80d8 100644 --- a/document/kernel/trap/context.md +++ b/document/kernel/trap/context.md @@ -1,55 +1,34 @@ -# 执行上下文 +# TrapFrame 与 Context -本文档定义了程序执行上下文(Execution Context)的构成及其在执行流切换(Context Switch)时的操作流程。该机制广泛应用于多任务操作系统中,用于任务调度和资源管理。 +## 当前状态 -## 1. 执行上下文(Execution Context)的构成 +内核使用 `TrapFrame` 和 `Context` 两种上下文保存格式: -执行上下文是 CPU 恢复一个任务(进程或线程)执行所需的所有状态信息的集合。它主要由以下几个部分组成: +- `TrapFrame`: 完整 trap 现场, 用于 syscall,异常,中断和信号返回. +- `Context`: 调度现场, 用于 `schedule` 选择任务后的普通上下文切换. -| 要素 | 核心内容 | 存储位置/管理机制 | 切换需求 | -|----------------|---------------------------------------------|---------------------------------|----------------| -| **CPU 状态** | 包括通用寄存器、程序计数器、标志寄存器等 | 当前任务的内存区域中 | 每次切换时必需 | -| **地址空间** | 虚拟地址与物理地址的映射关系 | 特定的地址映射表 | 进程切换时必需 | -| **系统资源** | 文件描述符、网络连接等资源的引用 | 任务控制块(task control block) | 自动传递 | +二者服务不同边界.`TrapFrame` 由 trap entry/restore 使用, 需要精确匹配汇编布局; `Context` 由 `context_switch` 使用, 只保存调用约定要求的最小状态. -### 解释 -- **CPU 状态**:包括 CPU 寄存器的内容(如程序计数器、栈指针等),这些内容描述了任务在 CPU 执行期间的当前状态。 -- **地址空间**:指任务的内存布局,包括虚拟地址到物理地址的映射。每个任务的地址空间独立,任务切换时需要切换到新任务的地址空间。 -- **系统资源**:包括任务持有的资源,如文件、网络连接等。通常由任务控制块管理,切换时资源状态会自动继承。 +## 关键区别 -## 2. 核心数据结构 +| 项目 | TrapFrame | Context | +| --- | --- | --- | +| 触发边界 | 中断,异常,系统调用 | 调度器切换任务 | +| 保存内容 | 用户/内核返回所需的完整寄存器和特权状态 | callee-saved 寄存器,sp,ra | +| 所属模块 | `arch/*/trap` | `arch/*/kernel` | +| 通用代码视角 | 通过 `HwTrapFrame` 操作 | 只传递指针给 `context_switch` | -在任务管理过程中,以下数据结构用于存储任务的状态和相关信息: +## 生命周期 -| 结构体/寄存器 | 作用 | 存储内容 | 所属模块 | -|------------------|--------------------------------|----------------------------------|-----------------| -| **任务控制块 (task control block)** | 管理任务状态、资源、调度信息 | 存储任务的所有信息,包括系统资源、CPU 状态等 | 任务管理 | -| **内存栈** | 任务的独立执行栈 | 存储上下文信息、局部变量等 | 内存管理 | -| **当前任务指针** | 标识当前执行的任务 | 指向当前任务的任务控制块 | 调度管理 | -| **地址映射信息** | 管理任务的内存映射信息 | 存储任务地址空间的映射信息 | 内存管理 | +每个任务持有一个 `TrapFrame` 保存区和一个 `Context`.创建任务时先初始化 `Context` 让第一次切换进入 `forkret`; 之后 `forkret` 根据 `TrapFrame` 恢复到内核线程入口或用户态入口. -### 解释 -- **任务控制块 (task control block)**:每个任务都有一个独立的控制块,存储任务的所有状态信息和资源管理信息。 -- **内存栈**:任务的执行栈,保存该任务的局部变量和中间计算结果。 -- **当前任务指针**:全局指针,始终指向当前正在执行的任务,确保操作系统知道当前哪个任务在 CPU 上执行。 +当 timer 或 IPI 在 trap handler 中触发调度时, 当前 CPU 的任务可能已经改变.trap 返回阶段必须重新从当前任务读取 `TrapFrame`, 否则会恢复到旧任务. -## 3. 执行流切换(Context Switch)的关键操作 +## 源码索引 -当操作系统决定切换当前任务(任务 A)到另一个任务(任务 B)时,执行流切换涉及以下几个关键操作: - -| 步骤 | 操作内容 | 触发条件 | 模块职责 | -|------------------|----------------------------------------------------------------------------|--------------------|--------------------| -| **步骤 1**: 保存与恢复状态 | 1. 将任务 A 的 CPU 寄存器和状态保存到任务 A 的内存区域中。
2. 从任务 B 的内存区域恢复 CPU 寄存器和状态。 | 每次任务切换时 | 调度管理/CPU核心 | -| **步骤 2**: 切换地址空间 | 更新任务的内存映射信息,确保任务 B 能访问到其专有的内存空间。 | 进程切换时 | 内存管理 | -| **步骤 3**: 更新当前任务 | 更新当前任务指针,指向任务 B 的控制块,确保调度器知道当前正在执行的是任务 B。 | 每次任务切换时 | 调度管理 | - -### 解释 -- **步骤 1:保存与恢复状态**:保存当前任务的状态(如寄存器的值),然后恢复新任务的状态。这确保了每个任务在执行流切换后能从它离开时的状态继续运行。 -- **步骤 2:切换地址空间**:任务的地址空间必须被切换,以便确保每个任务访问的是它自己的内存区域。通常通过更新地址映射信息来完成这一操作。 -- **步骤 3:更新当前任务**:调度器需要更新当前任务指针,指向新任务的控制块,确保下一次调度时可以恢复正确的任务。 - ---- - -### 总结 - -执行上下文切换是多任务操作系统中的关键机制,它确保操作系统能够在多个任务之间切换,保持各任务的独立性和执行状态。通过管理任务的 CPU 状态、地址空间和系统资源,操作系统能够高效地实现任务调度、资源分配与切换。本流程涉及的关键操作包括保存当前任务的状态、切换内存空间和更新任务指针,确保每个任务的状态能够正确恢复。 +- `os/src/kernel/task/task_struct.rs`: 任务持有 `Context` 和 `trap_frame_ptr`. +- `os/src/kernel/task/mod.rs`: `forkret` 和当前任务访问. +- `os/src/arch/riscv/trap/trap_frame.rs`: RISC-V `TrapFrame`. +- `os/src/arch/loongarch/trap/trap_frame.rs`: LoongArch `TrapFrame`. +- `os/src/arch/riscv/kernel/context.rs`: RISC-V `Context`. +- `os/src/arch/loongarch/kernel/context.rs`: LoongArch `TaskContext`. diff --git a/document/kernel/trap/switch.md b/document/kernel/trap/switch.md index 6e5b775a..f8dc85f5 100644 --- a/document/kernel/trap/switch.md +++ b/document/kernel/trap/switch.md @@ -1,182 +1,53 @@ -# 执行上下文与切换机制 +# 上下文切换流程 -本文档详细阐述了 `comix` 内核中用于实现多任务并发执行的两种核心上下文(`TrapFrame` 和 `Context`)及其切换机制。 +## 当前状态 -## 1. 上下文的两种类型 +上下文切换由调度器和架构汇编共同完成.调度器决定下一个任务并更新 `current_cpu`, 架构 `context_switch` 保存旧 `Context`,恢复新 `Context`.如果切换发生在 trap handler 中, 最终 trap return 会恢复新任务的 `TrapFrame`. -在 `comix` 中,"上下文" 指的是恢复一个任务执行所必需的CPU状态。根据触发场景的不同,我们有两种不同的上下文结构: +## 目标和非目标 -1. **`TrapFrame` (中断上下文)**: 当发生**中断、异常或系统调用**时,由硬件和底层汇编代码 `trap_entry` 保存的 **完整** CPU状态。它是一个任务在被意外打断时的精确快照。 -2. **`Context` (任务上下文)**: 当任务**主动放弃CPU (`yield`) 或时间片用尽**时,由调度器和底层汇编代码 `__switch` 保存的 **最小** CPU状态。它仅包含恢复任务执行流所必需的寄存器。 +目标: -这两种上下文的设计,是为了在不同场景下实现效率和功能的平衡。 +- 让调度策略不依赖具体寄存器布局. +- 在切换任务时同步地址空间和 `TrapFrame.cpu_ptr`. +- 让第一次调度,普通 yield,timer 抢占和 idle fallback 使用同一套切换机制. ---- +非目标: -## 2. `TrapFrame` 与中断上下文切换 +- 不在文档中逐条描述汇编保存指令. +- 不把 `TrapFrame` 当作普通调度上下文使用. +- 不在调度器锁内执行长期工作. -`TrapFrame` 是处理所有硬件中断、异常和系统调用的基础。 +## 普通切换 -**源码链接**: -- `TrapFrame` 结构体: [`os/src/arch/riscv/trap/mod.rs`](/os/src/arch/riscv/trap/mod.rs) -- 汇编入口/出口: [`os/src/arch/riscv/trap/trap_entry.S`](/os/src/arch/riscv/trap/trap_entry.S) +```text +schedule + -> scheduler.next_task + -> current_cpu.switch_task(next) + -> build old/new Context pointers + -> arch context_switch(old, new) + -> ret into next Context.ra +``` -### 2.1. `TrapFrame` 的构成 +`current_cpu.switch_task` 对用户任务会激活其地址空间.对所有任务都会调用架构 hook 更新 trap frame 中保存的 CPU 指针, 这是多核迁移后 trap entry 恢复 per-CPU 指针的关键. -`TrapFrame` 结构体保存了任务被中断时的 **几乎所有CPU状态**。 +## 第一次切换 -| 字段 | 核心内容 | 作用 | -|--------------|--------------------------------------------------------------|--------------------------------------------------------------| -| `x[32]` | 32个通用寄存器 (`x0`-`x31`) | 保存了任务在中断前的所有计算状态和参数。 | -| `sstatus` | `sstatus` 寄存器,包含特权级、中断使能等状态。 | 用于在 `sret` 时恢复正确的特权级和中断状态。 | -| `sepc` | `sepc` 寄存器,保存了被中断指令的地址。 | `sret` 指令会跳转到此地址,从中断处继续执行。 | -| `kernel_sp` | 内核栈指针。 | 在 `trap_entry` 中用于切换到正确的内核栈。 | +新任务的 `Context.ra` 指向 `forkret`.第一次恢复该上下文时, CPU 从 `forkret` 继续执行.`forkret` 不返回普通 Rust 调用栈, 而是调用架构恢复逻辑进入内核线程入口或用户态入口. -### 2.2. `TrapFrame` 的工作原理 +## trap 中切换 -当中断发生时,CPU硬件和软件会协同完成一次精确的上下文保存与恢复流程: +timer 或 IPI 进入 trap handler 后可能调用 `schedule`.此时入口传入的 `TrapFrame` 属于旧任务; 调度后当前任务可能已经变成另一个任务.因此 handler 尾部必须读取 `try_current_task().trap_frame_ptr` 并恢复该指针. -| 步骤 | 操作内容 | 核心代码/指令 | -|----------------------|-------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------| -| **1. 硬件操作** | 1. CPU暂停当前指令,根据 `scause` 记录中断原因。
2. 将当前PC存入 `sepc`。
3. 切换到S模式。
4. 跳转到 `stvec` 寄存器指向的地址。 | (硬件自动完成) | -| **2. 保存上下文** | 1. `trap_entry` 在当前任务的内核栈上分配 `TrapFrame` 空间。
2. 将 **所有** 通用寄存器和 `sstatus`、`sepc` 保存到 `TrapFrame` 中。 | `trap_entry` in `trap_entry.S` | -| **3. 中断处理** | `trap_entry` 调用 `trap_handler` (Rust函数),并传入 `TrapFrame` 的可变引用。`trap_handler` 可以读取和修改 `TrapFrame`。 | `trap_handler` in `trap_handler.rs` | -| **4. 恢复上下文** | 1. `trap_handler` 返回后,`__restore` 从 `TrapFrame` 中将所有寄存器值加载回CPU。
2. `sret` 指令原子地恢复 `pc`、特权级和中断状态。 | `__restore` 和 `sret` in `trap_entry.S` | +## idle fallback -**应用场景**: -- **系统调用**: `trap_handler` 从 `TrapFrame` 的 `a7` 读取系统调用号,从 `a0-a6` 读取参数,并将返回值写入 `a0`。 -- **任务抢占**: 时钟中断处理函数可以通过修改 `sepc` 来强制任务在恢复时跳转到调度器代码,从而实现抢占。 +每个 CPU 在启动时创建 idle task.run queue 为空且当前任务已阻塞或退出时, `RRScheduler` 切换到本 CPU idle task.idle task 不入普通 run queue, 也不参与公平性计算. ---- +## 源码索引 -## 3. `Context` 与任务调度上下文切换 - -`Context` 用于常规的、非中断驱动的任务切换,追求的是极致的效率。 - -**源码链接**: -- `Context` 结构体: [`os/src/arch/riscv/kernel/context.rs`](/os/src/arch/riscv/kernel/context.rs) -- 切换汇编代码: [`os/src/arch/riscv/kernel/switch.S`](/os/src/arch/riscv/kernel/switch.S) - -### 3.1. `Context` 的构成 - -`Context` 仅保存了 RISC-V 调用约定中 **被调用者保存 (callee-saved)** 的寄存器。 - -| 字段 | 核心内容 | 作用 | -|---------|----------------------------------------|----------------------------------------------------------------------| -| `ra` | 返回地址寄存器 (`x1`) | 保存了调用 `__switch` 后的返回点,是恢复执行流的关键。 | -| `sp` | 栈指针寄存器 (`x2`) | 指向任务的内核栈顶。 | -| `s[12]` | `s0`-`s11` ( `x8-x9`, `x18-x27`) | 调用约定要求函数在返回前必须恢复这些寄存器,因此任务切换时必须保存。 | - -**为什么只保存这些?** -因为调用者保存(caller-saved)的寄存器(如 `a0-a7`, `t0-t6`)由调用 `__switch` 的函数(即 `schedule`)负责维护。编译器已经保证了在 `__switch` 调用前后,这些寄存器的值对于 `schedule` 函数是无损的。这使得 `Context` 非常小,切换速度极快。 - -### 3.2. `Context` 的工作原理 - -当 `schedule()` 函数决定切换任务时(例如,当前任务调用 `yield_task()`),会执行以下流程: - -| 步骤 | 操作内容 | 核心代码/指令 | -|------------------|-------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------| -| **1. 准备切换** | `schedule()` 调用调度器的 `prepare_switch()`,获取旧任务和新任务的 `Context` 指针。 | `schedule()` in `scheduler/mod.rs` | -| **2. 保存与恢复**| `schedule()` 调用 `__switch(old_ctx, new_ctx)`。
1. `__switch` 将 `ra`, `sp`, `s0-s11` 保存到 `old_ctx`。
2. `__switch` 从 `new_ctx` 恢复 `ra`, `sp`, `s0-s11`。 | `__switch` in `switch.S` | -| **3. 切换执行流**| `__switch` 的 `ret` 指令会跳转到刚刚从 `new_ctx` 恢复的 `ra` 地址,从而将CPU控制权无缝转移给新任务。 | `ret` in `switch.S` | - -这个流程就像两个函数调用彼此的中间点,实现了两个独立执行流(任务)之间的切换。 - -### 总结 - -`TrapFrame` 和 `Context` 共同构成了 `comix` 内核的上下文切换基石: -- **`TrapFrame`** 是 **重量级** 的全状态快照,用于处理与硬件交互的、不可预期的中断事件。 -- **`Context`** 是 **轻量级** 的执行流锚点,用于处理可预期的、由调度器驱动的任务切换。 - -通过这两种机制的协同工作,内核既能高效地响应硬件事件,又能快速地在任务之间进行调度。 -# 执行上下文与切换机制 - -本文档详细阐述了 `comix` 内核中用于实现多任务并发执行的两种核心上下文(`TrapFrame` 和 `Context`)及其切换机制。 - -## 1. 上下文的两种类型 - -在 `comix` 中,"上下文" 指的是恢复一个任务执行所必需的CPU状态。根据触发场景的不同,我们有两种不同的上下文结构: - -1. **`TrapFrame` (中断上下文)**: 当发生**中断、异常或系统调用**时,由硬件和底层汇编代码 `trap_entry` 保存的 **完整** CPU状态。它是一个任务在被意外打断时的精确快照。 -2. **`Context` (任务上下文)**: 当任务**主动放弃CPU (`yield`) 或时间片用尽**时,由调度器和底层汇编代码 `__switch` 保存的 **最小** CPU状态。它仅包含恢复任务执行流所必需的寄存器。 - -这两种上下文的设计,是为了在不同场景下实现效率和功能的平衡。 - ---- - -## 2. `TrapFrame` 与中断上下文切换 - -`TrapFrame` 是处理所有硬件中断、异常和系统调用的基础。 - -**源码链接**: -- `TrapFrame` 结构体: [`os/src/arch/riscv/trap/mod.rs`](/os/src/arch/riscv/trap/mod.rs) -- 汇编入口/出口: [`os/src/arch/riscv/trap/trap_entry.S`](/os/src/arch/riscv/trap/trap_entry.S) - -### 2.1. `TrapFrame` 的构成 - -`TrapFrame` 结构体保存了任务被中断时的 **几乎所有CPU状态**。 - -| 字段 | 核心内容 | 作用 | -|--------------|--------------------------------------------------------------|--------------------------------------------------------------| -| `x[32]` | 32个通用寄存器 (`x0`-`x31`) | 保存了任务在中断前的所有计算状态和参数。 | -| `sstatus` | `sstatus` 寄存器,包含特权级、中断使能等状态。 | 用于在 `sret` 时恢复正确的特权级和中断状态。 | -| `sepc` | `sepc` 寄存器,保存了被中断指令的地址。 | `sret` 指令会跳转到此地址,从中断处继续执行。 | -| `kernel_sp` | 内核栈指针。 | 在 `trap_entry` 中用于切换到正确的内核栈。 | - -### 2.2. `TrapFrame` 的工作原理 - -当中断发生时,CPU硬件和软件会协同完成一次精确的上下文保存与恢复流程: - -| 步骤 | 操作内容 | 核心代码/指令 | -|----------------------|-------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------| -| **1. 硬件操作** | 1. CPU暂停当前指令,根据 `scause` 记录中断原因。
2. 将当前PC存入 `sepc`。
3. 切换到S模式。
4. 跳转到 `stvec` 寄存器指向的地址。 | (硬件自动完成) | -| **2. 保存上下文** | 1. `trap_entry` 在当前任务的内核栈上分配 `TrapFrame` 空间。
2. 将 **所有** 通用寄存器和 `sstatus`、`sepc` 保存到 `TrapFrame` 中。 | `trap_entry` in `trap_entry.S` | -| **3. 中断处理** | `trap_entry` 调用 `trap_handler` (Rust函数),并传入 `TrapFrame` 的可变引用。`trap_handler` 可以读取和修改 `TrapFrame`。 | `trap_handler` in `trap_handler.rs` | -| **4. 恢复上下文** | 1. `trap_handler` 返回后,`__restore` 从 `TrapFrame` 中将所有寄存器值加载回CPU。
2. `sret` 指令原子地恢复 `pc`、特权级和中断状态。 | `__restore` 和 `sret` in `trap_entry.S` | - -**应用场景**: -- **系统调用**: `trap_handler` 从 `TrapFrame` 的 `a7` 读取系统调用号,从 `a0-a6` 读取参数,并将返回值写入 `a0`。 -- **任务抢占**: 时钟中断处理函数可以通过修改 `sepc` 来强制任务在恢复时跳转到调度器代码,从而实现抢占。 - ---- - -## 3. `Context` 与任务调度上下文切换 - -`Context` 用于常规的、非中断驱动的任务切换,追求的是极致的效率。 - -**源码链接**: -- `Context` 结构体: [`os/src/arch/riscv/kernel/context.rs`](/os/src/arch/riscv/kernel/context.rs) -- 切换汇编代码: [`os/src/arch/riscv/kernel/switch.S`](/os/src/arch/riscv/kernel/switch.S) - -### 3.1. `Context` 的构成 - -`Context` 仅保存了 RISC-V 调用约定中 **被调用者保存 (callee-saved)** 的寄存器。 - -| 字段 | 核心内容 | 作用 | -|---------|----------------------------------------|----------------------------------------------------------------------| -| `ra` | 返回地址寄存器 (`x1`) | 保存了调用 `__switch` 后的返回点,是恢复执行流的关键。 | -| `sp` | 栈指针寄存器 (`x2`) | 指向任务的内核栈顶。 | -| `s[12]` | `s0`-`s11` ( `x8-x9`, `x18-x27`) | 调用约定要求函数在返回前必须恢复这些寄存器,因此任务切换时必须保存。 | - -**为什么只保存这些?** -因为调用者保存(caller-saved)的寄存器(如 `a0-a7`, `t0-t6`)由调用 `__switch` 的函数(即 `schedule`)负责维护。编译器已经保证了在 `__switch` 调用前后,这些寄存器的值对于 `schedule` 函数是无损的。这使得 `Context` 非常小,切换速度极快。 - -### 3.2. `Context` 的工作原理 - -当 `schedule()` 函数决定切换任务时(例如,当前任务调用 `yield_task()`),会执行以下流程: - -| 步骤 | 操作内容 | 核心代码/指令 | -|------------------|-------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------| -| **1. 准备切换** | `schedule()` 调用调度器的 `prepare_switch()`,获取旧任务和新任务的 `Context` 指针。 | `schedule()` in `scheduler/mod.rs` | -| **2. 保存与恢复**| `schedule()` 调用 `__switch(old_ctx, new_ctx)`。
1. `__switch` 将 `ra`, `sp`, `s0-s11` 保存到 `old_ctx`。
2. `__switch` 从 `new_ctx` 恢复 `ra`, `sp`, `s0-s11`。 | `__switch` in `switch.S` | -| **3. 切换执行流**| `__switch` 的 `ret` 指令会跳转到刚刚从 `new_ctx` 恢复的 `ra` 地址,从而将CPU控制权无缝转移给新任务。 | `ret` in `switch.S` | - -这个流程就像两个函数调用彼此的中间点,实现了两个独立执行流(任务)之间的切换。 - -### 总结 - -`TrapFrame` 和 `Context` 共同构成了 `comix` 内核的上下文切换基石: -- **`TrapFrame`** 是 **重量级** 的全状态快照,用于处理与硬件交互的、不可预期的中断事件。 -- **`Context`** 是 **轻量级** 的执行流锚点,用于处理可预期的、由调度器驱动的任务切换。 - -通过这两种机制的协同工作,内核既能高效地响应硬件事件,又能快速地在任务之间进行调度。 \ No newline at end of file +- `os/src/kernel/scheduler/mod.rs`: `schedule` 和公共调度入口. +- `os/src/kernel/scheduler/rr_scheduler.rs`: `next_task`,idle fallback 和 `SwitchPlan`. +- `os/src/kernel/cpu.rs`: `switch_task`,地址空间切换和 `TrapFrame.cpu_ptr` 更新. +- `os/src/kernel/task/mod.rs`: `forkret`. +- `os/src/arch/riscv/kernel/switch.S`: RISC-V 切换汇编. +- `os/src/arch/loongarch/kernel/switch.S`: LoongArch 切换汇编. diff --git a/document/kernel/trap/trap.md b/document/kernel/trap/trap.md index e24c0ee2..f208deeb 100644 --- a/document/kernel/trap/trap.md +++ b/document/kernel/trap/trap.md @@ -1,85 +1,68 @@ -# 中断处理模块概述 +# Trap 处理设计 -本文档描述了 `comix` 内核如何处理来自硬件的异常、中断和系统调用,这一整套机制统称为中断(Trap)。 +本文记录异常,中断和系统调用的跨架构设计.具体异常号,寄存器字段和汇编槽位以源码为准. -## 1. 什么是中断 (Trap)? +## 当前状态 -在 RISC-V 架构中,任何导致正常指令流被意外打断的事件都称为一个 Trap。它主要分为三类: +RISC-V 和 LoongArch 都提供 `arch::trap` 模块, 对通用内核暴露初始化,恢复,信号 trampoline 和 `TrapFrame` 操作.trap 汇编入口保存现场后进入 Rust handler, handler 分派 syscall,timer,IPI 或设备中断, 最终恢复当前任务的 `TrapFrame`. -1. **异常 (Exception)**: 在执行指令时由内部事件引发,例如访问了无效的内存地址(页错误)、执行了非法指令等。这是同步事件。 -2. **中断 (Interrupt)**: 由外部设备异步引发的事件,例如时钟中断、I/O设备中断等。 -3. **系统调用 (System Call)**: 由用户态程序通过 `ecall` 指令主动触发,请求内核服务的事件。 +RISC-V 路径已覆盖 syscall,timer,software interrupt IPI 和 external interrupt.LoongArch 路径已覆盖 syscall,timer,TLB refill 入口安装和基本恢复, 外部中断/IPI 仍未与 RISC-V 对齐. -当一个 Trap 发生时,CPU硬件会自动暂停当前执行流,并将控制权转移给内核预设的中断处理程序。 +## 目标和非目标 -## 2. 初始化 +目标: -为了让内核能够响应中断,必须在启动阶段进行初始化。 +- 在架构层封装 trap entry 和 trap return. +- 让 syscall 分派,计时器队列和调度器复用通用内核逻辑. +- 支持 trap handler 内发生调度后恢复新任务上下文. +- 保持信号返回 trampoline 的架构字节由各架构提供. -**源码链接**: [`os/src/arch/riscv/trap/mod.rs`](/os/src/arch/riscv/trap/mod.rs) +非目标: -初始化函数 `init()` 执行以下关键操作: +- 不在正式文档中维护完整异常码表. +- 不把硬中断处理写成可阻塞路径. +- 不把用户态所有异常都立即实现为完整 POSIX 信号语义. -1. **设置中断向量**: - - 将 `stvec` 寄存器的值设置为汇编函数 `trap_entry` 的地址。`stvec` (Supervisor Trap Vector Base Address Register) 告诉 CPU 在S模式下发生 Trap 时应该跳转到哪里。 -2. **使能中断**: - - 通过 `sie` (Supervisor Interrupt Enable) 寄存器,使能内核需要处理的几类中断,主要是外部中断(`SEIE`)、时钟中断(`STIE`)和软件中断(`SSIE`)。 +## 关键流程 -## 3. 中断处理流程 +```text +hardware trap + -> arch trap_entry + -> save TrapFrame + -> Rust trap_handler + -> syscall/timer/IPI/device/exception dispatch + -> maybe schedule + -> restore current task TrapFrame + -> sret/ertn +``` -一次完整的中断处理和返回流程可以分为三个阶段:进入、处理和返回。 +系统调用会调整返回 PC, 然后把 `TrapFrame` 交给 syscall dispatcher.时钟中断推进全局 ticks,timer queue 和 interval timer, 必要时触发调度.RISC-V 软件中断先处理 IPI pending 标志, 再根据 run queue 判断是否调度. -### 3.1. 进入中断 (`trap_entry`) +## 用户态和内核态异常 -当 Trap 发生时,硬件完成初步状态保存后,会立即跳转到 `stvec` 指向的 `trap_entry` 函数。这是一个汇编实现的底层入口。 +用户态异常不应直接破坏内核.RISC-V 当前会打印诊断信息并终止当前任务, 后续可进一步映射为 SIGILL/SIGSEGV 等信号.LoongArch 当前用户态未知异常仍偏 bringup 诊断, 会 panic, 这是需要继续收敛的限制. -**源码链接**: [`os/src/arch/riscv/trap/trap_entry.S`](/os/src/arch/riscv/trap/trap_entry.S) +内核态异常按致命错误处理, 会打印关键寄存器并 panic. -`trap_entry` 的核心职责是 **保存完整的CPU上下文**: +## 并发和生命周期约束 -1. **准备栈空间**: 它首先在当前任务的内核栈上分配一块空间,用于存放 `TrapFrame`。 -2. **交换 `sscratch`**: 使用 `csrrw` 指令,将 `sscratch` 寄存器(通常预先保存了指向 `TrapFrame` 的指针)与一个通用寄存器(如 `a0`)交换,以便在不破坏任何寄存器的情况下安全地访问 `TrapFrame`。 -3. **保存通用寄存器**: 将全部32个通用寄存器(`x0`-`x31`)的值保存到内核栈上的 `TrapFrame` 结构中。 -4. **保存 CSR**: 将 `sstatus` 和 `sepc` 这两个关键的控制状态寄存器(CSR)的值也保存到 `TrapFrame` 中。 -5. **调用 Rust 处理函数**: 在所有上下文都安全保存后,它会调用 Rust 实现的 `trap_handler` 函数,并将指向 `TrapFrame` 的指针作为参数传递过去。 +- trap handler 入口时中断通常已关闭, 返回必须通过架构 restore 恢复特权状态和中断状态. +- trap handler 中不要执行可能阻塞或长期持锁的工作; 网络轮询等工作应转交 kworker. +- timer 中断唤醒任务时依赖调度器 wakeup 幂等性. +- 如果 trap handler 中发生任务切换, 返回必须读取当前任务的 `trap_frame_ptr`. +- `TrapFrame` 布局必须和汇编保存/恢复严格一致. -### 3.2. 中断分发 (`trap_handler`) +## 已知限制 -`trap_handler` 是用 Rust 实现的高层中断处理函数,负责根据中断原因进行分发。 +- LoongArch 外部中断和 IPI 尚未接入完整分派. +- LoongArch 用户态未知异常还没有按 RISC-V 方式转换为任务终止或信号. +- RISC-V TLB flush IPI 当前处理本地 `sfence.vma`, 更强同步协议需要内存管理侧补足. -**源码链接**: [`os/src/arch/riscv/trap/trap_handler.rs`](/os/src/arch/riscv/trap/trap_handler.rs) +## 源码索引 -其工作流程如下: - -1. **识别中断原因**: 读取 `scause` 寄存器,判断 Trap 的类型(是中断还是异常)和具体原因码。 -2. **分发处理**: - - **系统调用**: 如果 `scause` 表明是来自用户态的 `ecall`,则调用 `syscall()` 函数处理系统调用。`TrapFrame` 中的 `a7` 寄存器存放系统调用号,`a0`-`a6` 存放参数。 - - **时钟中断**: 如果是时钟中断,则调用 `timer_tick()`,这会触发调度器的 `update_time_slice`,可能导致任务抢占。 - - **页错误**: 如果是访存异常(`LoadPageFault`, `StorePageFault`),则调用相应的页错误处理函数。 - - **其他异常/中断**: 根据 `scause` 的值,分发到对应的处理逻辑。如果遇到无法处理的异常,则会触发 `panic`。 -3. **返回**: 处理完成后,`trap_handler` 函数返回,控制权回到 `trap_entry.S`。 - -### 3.3. 返回中断 (`__restore`) - -当 `trap_handler` 返回后,汇编代码会跳转到 `__restore` 标签处,开始执行中断返回流程。 - -**源码链接**: [`os/src/arch/riscv/trap/trap_entry.S`](/os/src/arch/riscv/trap/trap_entry.S) - -`__restore` 的职责与 `trap_entry` 相反,它负责 **恢复完整的CPU上下文**: - -1. **恢复 CSR**: 从 `TrapFrame` 中加载 `sstatus` 和 `sepc` 的值,并写回对应的物理寄存器。 -2. **恢复通用寄存器**: 从 `TrapFrame` 中将 `x0`-`x31` 的值依次加载回 CPU 的通用寄存器。 -3. **执行 `sret`**: 最后,执行 `sret` (Supervisor Return) 指令。这条指令是原子操作,它会: - - 将 `pc` (程序计数器) 的值设置为 `sepc` 寄存器的值。 - - 根据 `sstatus` 中的 `SPP` 位恢复到之前的特权级(S模式或U模式)。 - - 根据 `sstatus` 中的 `SPIE` 位恢复中断使能状态。 - -至此,CPU 的状态完全恢复到 Trap 发生前的样子,任务得以从被中断的地方继续无缝执行。 - -## 4. 关键数据结构:`TrapFrame` - -`TrapFrame` 是整个中断处理机制的基石。它是一个定义在 Rust 中的结构体,其内存布局 **必须** 与 `trap_entry.S` 中保存和恢复寄存器的顺序严格一致。 - -**源码链接**: [`os/src/arch/riscv/trap/mod.rs`](/os/src/arch/riscv/trap/mod.rs) - -它包含了所有通用寄存器以及 `sstatus`、`sepc` 等关键信息,是任务在被中断那一刻的完整快照。通过在 `trap_handler` 中修改 `TrapFrame` 的内容(例如,修改 `sepc` 来改变返回地址,或修改 `a0` 来设置系统调用的返回值),内核可以精确地控制任务恢复执行时的状态。 \ No newline at end of file +- `os/src/arch/riscv/trap/mod.rs`: RISC-V trap 初始化和恢复门面. +- `os/src/arch/riscv/trap/trap_handler.rs`: RISC-V syscall/timer/IPI/device/异常分派. +- `os/src/arch/riscv/trap/trap_entry.S`: RISC-V 保存和恢复汇编. +- `os/src/arch/loongarch/trap/mod.rs`: LoongArch trap 初始化和恢复门面. +- `os/src/arch/loongarch/trap/trap_handler.rs`: LoongArch syscall/timer/TLB refill 入口安装和异常分派. +- `os/src/arch/loongarch/trap/trap_entry.S`: LoongArch 保存,恢复和 TLB refill 汇编. diff --git a/document/log/README.md b/document/log/README.md index ddba4f1c..b9e3b411 100644 --- a/document/log/README.md +++ b/document/log/README.md @@ -1,263 +1,70 @@ -# Log 子系统文档 +# Log 子系统概述 -## 简介 +Log 子系统提供内核级分级日志, 固定大小环形缓冲, 控制台即时输出和 `syslog` 读取接口。当前设计偏向早期可用和低依赖, 具体 API 细节以 rustdoc 和源码注释为准。 -Log 子系统是 Comix 内核的日志记录系统,提供类似 Linux 内核 `printk` 的日志功能。该系统专为裸机环境设计,采用无锁并发架构,支持多核环境下的高效日志记录。 +## 当前状态 -Log 子系统的核心特点是**双路输出策略**:日志既会被缓存到环形缓冲区供后续读取,又可以根据级别立即输出到控制台。这种设计平衡了实时监控和日志持久化的需求,使得开发者既能在运行时观察关键信息,又能在事后分析完整的日志记录。 +- 全局 `LogCore` 使用 `const fn` 静态初始化, 无需运行时 init。 +- `pr_*` 宏按级别过滤后写入日志核心。 +- `print!`/`println!` 保持原始控制台输出, 同时以 Info 级别写入日志缓冲。 +- 日志条目固定大小, 消息超过上限会截断。 +- 环形缓冲使用多生产者单消费者模型, 溢出时覆盖最旧未读日志并累计 dropped count。 +- `syslog` syscall 支持读取, 非破坏性读取, 清空, 控制台级别和大小查询。 +- panic/trap 关键路径可使用 `console::emergency_print()` 绕开常规日志路径。 -作为内核基础设施的一部分,Log 子系统在系统启动早期即可使用,无需复杂的初始化过程。它采用编译期初始化的单例模式,保证零运行时开销,并且完全避免动态内存分配,适合在资源受限的裸机环境中运行。 +## 目标 -### 主要功能 +- 让内核任意阶段都能输出关键诊断信息。 +- 把常规日志保存在环形缓冲中, 供用户态 syslog/dmesg 风格读取。 +- 让控制台噪声可控, 缓冲记录和即时输出使用独立级别阈值。 +- 避免日志路径依赖堆分配或阻塞锁。 -- **分级日志记录**:提供 8 个级别的日志分类(Emergency、Alert、Critical、Error、Warning、Notice、Info、Debug),模仿 Linux 内核的日志级别系统 -- **双路输出策略**:日志同时写入环形缓冲区和控制台,支持异步读取和实时监控 -- **无锁并发设计**:采用 MPSC(多生产者单消费者)模型的环形缓冲区,支持多核并发写入而无需锁 -- **级别过滤机制**:支持全局级别和控制台级别的独立过滤,灵活控制日志的缓存和显示 -- **零动态分配**:所有数据结构在编译期确定大小,无堆内存分配,适合裸机环境 -- **彩色控制台输出**:根据日志级别使用不同的 ANSI 颜色,提高可读性 -- **早期过滤优化**:在宏展开阶段检查级别,避免格式化被禁用的日志,降低性能开销 -- **syslog 系统调用**:提供兼容 Linux 的 `syslog` 系统调用,允许用户空间程序读取和控制内核日志 -- **非破坏性读取**:支持 peek 操作,可以读取日志而不从缓冲区删除它们 -- **精确字节计数**:实时追踪未读日志的格式化字节数,支持缓冲区状态查询 +## 非目标 -## 模块结构 +- 不提供持久化日志文件。 +- 不在文档维护完整 API/宏清单。 +- 不保证多消费者无锁读取。 +- 不让日志系统承担审计, tracing 或结构化事件系统职责。 -``` -os/src/log/ -├── mod.rs # 模块入口,导出公共 API 和全局单例 GLOBAL_LOG -├── macros.rs # 用户宏接口 (pr_info!, pr_err! 等) -├── log_core.rs # 核心日志系统,封装缓冲区和双过滤器 -├── buffer.rs # 无锁环形缓冲区实现 (MPSC 模型) -├── entry.rs # 日志条目结构和序列化逻辑 -├── level.rs # 日志级别枚举定义 -├── context.rs # 上下文信息收集 (CPU ID、时间戳等) -├── config.rs # 配置常量 (缓冲区大小、消息长度限制等) -└── tests/ # 测试模块 - ├── mod.rs # 测试入口和辅助宏 - ├── basic.rs # 基本读写和 FIFO 测试 - ├── filter.rs # 日志级别过滤测试 - ├── overflow.rs # 缓冲区溢出测试 - ├── format.rs # 消息格式化测试 - ├── byte_counting.rs # 字节计数测试 - └── nondestructive_read.rs # 非破坏性读取测试 -``` +## 模块边界 -### 模块职责 +- `os/src/log/mod.rs`: 全局单例, 公共门面。 +- `os/src/log/macros.rs`: `pr_*` 宏和早期级别过滤。 +- `os/src/log/log_core.rs`: 双过滤器, 条目创建, 控制台输出,格式化。 +- `os/src/log/buffer.rs`: MPSC 环形缓冲。 +- `os/src/log/entry.rs`: 固定大小日志条目。 +- `os/src/log/context.rs`: CPU, task, timestamp 收集。 +- `os/src/kernel/syscall/sys.rs`: `syslog` syscall。 +- `os/src/console.rs`: 常规控制台和 emergency 输出。 -- **mod.rs**:模块的统一入口,导出全局单例 `GLOBAL_LOG` 和所有公共 API,对外屏蔽内部实现细节 -- **macros.rs**:提供用户友好的宏接口(`pr_emerg!`、`pr_alert!`、`pr_crit!`、`pr_err!`、`pr_warn!`、`pr_notice!`、`pr_info!`、`pr_debug!`),负责早期级别过滤和格式化参数的传递 -- **log_core.rs**:日志系统的核心逻辑,管理环形缓冲区和双过滤器(全局级别和控制台级别),协调日志的写入和读取 -- **buffer.rs**:实现无锁环形缓冲区,使用原子操作和票号系统保证多核并发安全,处理缓冲区溢出和数据同步 -- **entry.rs**:定义日志条目的内存布局,实现日志序列化和格式化显示,管理固定大小的消息缓冲区 -- **level.rs**:定义 8 级日志分类和颜色映射,提供级别比较和序列化功能 -- **context.rs**:收集日志的上下文信息,包括 CPU ID、任务 ID、时间戳等,依赖架构特定的接口 -- **config.rs**:集中管理配置常量,如环形缓冲区大小、消息最大长度等,便于调整和维护 +## 关键流程 -## 文档导航 - -### 核心概念 - -- **[整体架构](architecture.md)**:Log 子系统的分层架构、模块依赖、双路输出策略、同步机制、设计决策和性能考量 - -### 子模块详解 - -- **[环形缓冲区与日志条目](buffer_and_entry.md)**:无锁 MPSC 环形缓冲区的实现原理、票号系统、溢出处理、日志条目的内存布局和序列化机制 -- **[日志级别与宏接口](level.md)**:8 级日志分类的语义、颜色映射、宏接口说明、双过滤器工作原理 -- **[使用指南](usage.md)**:日志系统的基本使用、配置方法、最佳实践、常见陷阱和调试技巧 - -### API 参考 - -- **[API 索引](api_reference.md)**:所有公共 API 的完整列表、函数签名、使用示例和源代码位置 - -## 设计原则 - -Log 子系统的设计遵循以下核心原则: - -### 1. 无锁并发 - -采用 MPSC(多生产者单消费者)模型的环形缓冲区,通过原子操作和票号系统实现无锁并发写入。多个 CPU 核心可以同时记录日志而无需等待锁,避免了传统锁机制带来的性能瓶颈和优先级反转问题。这种设计特别适合裸机环境,在中断处理程序和关键路径中也能安全使用。 - -### 2. 早期过滤 - -在宏展开阶段检查日志级别,避免格式化被禁用级别的日志。通过 `is_level_enabled()` 函数提前判断,确保被过滤的日志不会产生任何格式化开销。这种优化使得即使代码中存在大量 Debug 级别的日志,在生产环境中禁用 Debug 后也不会影响性能。 - -### 3. 固定大小分配 - -所有数据结构在编译期确定大小,完全避免堆内存分配。日志条目使用固定 256 字节的消息缓冲区,环形缓冲区的容量在配置文件中静态定义。这种设计保证了内存使用的可预测性,避免了动态分配的不确定性和碎片化问题,特别适合资源受限的嵌入式环境。 - -### 4. 双路输出策略 - -日志同时写入环形缓冲区和控制台,通过独立的级别过滤器控制两条路径。环形缓冲区缓存所有达到全局级别的日志供后续分析,控制台立即显示达到控制台级别的日志供实时监控。这种策略既保证了日志的完整性,又提供了灵活的实时反馈,满足不同场景的需求。 - -## 重要约定 - -### MPSC 并发模型 - -环形缓冲区采用**多生产者单消费者**(MPSC)模型: -- **多生产者**:多个 CPU 核心可以并发调用日志宏写入日志,通过原子操作的票号系统协调,无需锁 -- **单消费者**:只能有一个读取者顺序读取日志,通常是用户态工具或内核日志线程 -- **同步保证**:写入使用 Release 语义发布数据,读取使用 Acquire 语义获取数据,保证内存可见性 - -### 消息长度限制 - -每条日志消息最多 **256 字节**(定义在 `config.rs:MAX_MESSAGE_LEN`): -- 超过限制的消息会被自动截断,不会报错或丢失整条日志 -- UTF-8 字符边界会被尊重,避免截断产生无效字符序列 -- 建议在日志中使用简洁的描述,避免冗长的字符串 - -### 缓冲区容量 - -环形缓冲区大小为 **16 KB**(定义在 `config.rs:BUFFER_SIZE`),约可容纳 50-60 条日志: -- 当缓冲区满时,新日志会覆盖最旧的日志(FIFO 策略) -- 系统会记录被丢弃的日志数量,可通过 `log_dropped_count()` 查询 -- 频繁丢弃日志表示消费速度不足,应考虑提高读取频率或增大缓冲区 - -### 默认级别配置 - -- **全局级别**:默认为 `Info`,控制哪些日志被缓存 -- **控制台级别**:默认为 `Warning`,控制哪些日志立即显示 - -这意味着 Info 及以上级别的日志会被缓存,但只有 Warning 及以上级别的日志会立即打印到控制台。开发时可以调低控制台级别以查看更多实时信息。 - -## 快速开始 - -### 基本使用 - -Log 子系统在内核启动时自动初始化,无需显式调用初始化函数。使用日志宏即可记录日志: - -```rust -use log::{pr_info, pr_err, pr_warn}; - -// 记录信息性日志 -pr_info!("Kernel initialized successfully"); - -// 记录错误 -pr_err!("Failed to mount filesystem: {}", error_code); +1. 调用 `pr_info!` 等宏。 +2. 宏先检查 global level, 被过滤的日志不进入格式化。 +3. `LogCore` 收集上下文并构造固定大小 `LogEntry`。 +4. 条目写入环形缓冲。 +5. 若级别达到 console level, 直接格式化输出到控制台。 +6. 用户态通过 `syslog` 读取格式化后的日志文本。 -// 记录警告 -pr_warn!("Memory usage: {} MB", usage); +## 并发和生命周期约束 -// 带变量的格式化输出 -let pid = 42; -let name = "init"; -pr_info!("Starting process {} ({})", pid, name); -``` +- 写入端使用原子序列号分配槽位。 +- 读取端按单消费者模型推进 read sequence。 +- 溢出时写入端推进 read sequence 并增加 dropped count。 +- `context::collect_context()` 使用 `try_lock` 读取当前 task id, 避免在已经持有 task lock 的路径死锁。 +- `direct_print_entry()` 和 `format_log_entry()` 的格式需要和缓冲区字节计数逻辑保持一致。 -### 配置级别过滤器 +## 已知限制 -可以动态调整全局级别和控制台级别: +- `syslog` 权限检查当前是兼容 stub, 预留完整 capability/dmesg_restrict 逻辑。 +- `copy_to_user` 失败在部分 syslog 读取路径中没有细粒度回滚。 +- 缓冲区大小固定为编译期常量, 高日志量场景会覆盖旧日志。 +- ANSI 颜色码会进入 syslog 格式化输出。 -```rust -use log::{set_global_level, set_console_level, LogLevel}; - -// 设置全局级别为 Debug,缓存所有级别的日志 -set_global_level(LogLevel::Debug); - -// 设置控制台级别为 Info,显示 Info 及以上级别的日志 -set_console_level(LogLevel::Info); - -// 生产环境可以提高级别减少日志量 -set_global_level(LogLevel::Warning); -set_console_level(LogLevel::Error); -``` - -### 读取日志 - -从环形缓冲区读取缓存的日志: - -```rust -use log::{read_log, log_len, log_dropped_count, log_unread_bytes}; - -// 检查有多少条日志和未读字节数 -let count = log_len(); -let bytes = log_unread_bytes(); -println!("Buffered logs: {}, unread bytes: {}", count, bytes); - -// 顺序读取所有日志(破坏性读取) -while let Some(entry) = read_log() { - println!("{}", entry); -} - -// 非破坏性读取(不移除日志) -use log::{peek_log, log_reader_index, log_writer_index}; -let start = log_reader_index(); -let end = log_writer_index(); -for index in start..end { - if let Some(entry) = peek_log(index) { - println!("{}", entry); - } -} - -// 检查是否有日志被丢弃 -let dropped = log_dropped_count(); -if dropped > 0 { - println!("Warning: {} logs were dropped due to buffer overflow", dropped); -} -``` - -### syslog 系统调用 - -用户空间程序可以通过 `syslog` 系统调用读取和控制内核日志: - -```c -#include - -// 读取内核日志(破坏性) -char buf[8192]; -int len = syscall(SYS_syslog, SYSLOG_ACTION_READ, buf, sizeof(buf)); - -// 读取所有日志(非破坏性) -len = syscall(SYS_syslog, SYSLOG_ACTION_READ_ALL, buf, sizeof(buf)); - -// 查询未读字节数 -int unread = syscall(SYS_syslog, SYSLOG_ACTION_SIZE_UNREAD, NULL, 0); - -// 查询缓冲区总大小 -int size = syscall(SYS_syslog, SYSLOG_ACTION_SIZE_BUFFER, NULL, 0); - -// 设置控制台日志级别 -int old_level = syscall(SYS_syslog, SYSLOG_ACTION_CONSOLE_LEVEL, NULL, 5); - -// 清空日志缓冲区 -syscall(SYS_syslog, SYSLOG_ACTION_CLEAR, NULL, 0); -``` - -## 相关资源 - -### 源代码位置 - -- **主模块**:`os/src/log/mod.rs` -- **核心实现**:`os/src/log/log_core.rs` -- **环形缓冲区**:`os/src/log/buffer.rs` -- **完整源码**:`os/src/log/` 目录 - -### 配置文件 - -- **配置常量**:`os/src/log/config.rs` - - `BUFFER_SIZE`:环形缓冲区大小(16 KB) - - `MAX_MESSAGE_LEN`:单条日志消息最大长度(256 字节) - - `MAX_ENTRIES`:缓冲区可容纳的最大日志条目数(自动计算) - -### 依赖模块 - -- **arch::timer**:提供时间戳功能(`get_time()`) -- **arch::kernel::cpu**:提供 CPU ID 获取功能(`cpu_id()`) -- **kernel::cpu**:提供当前任务信息(`current_cpu()` → `current_task`) -- **console::Stdout**:控制台输出接口 - -### 测试 - -运行 Log 模块的测试: - -```bash -cd os && make test -``` - -测试覆盖了基本读写、级别过滤、缓冲区溢出、消息格式化等核心功能。 - -### 版本信息 +## 文档导航 -- **Rust 版本**:nightly-2025-01-13 -- **目标架构**:riscv64gc-unknown-none-elf -- **支持架构**:RISC-V (当前),LoongArch (规划中) +- [架构设计](architecture.md) +- [日志级别](level.md) +- [缓冲区和条目](buffer_and_entry.md) +- [使用方法](usage.md) +- [API 边界](api_reference.md) diff --git a/document/log/api_reference.md b/document/log/api_reference.md index 6dfa85f5..403bed14 100644 --- a/document/log/api_reference.md +++ b/document/log/api_reference.md @@ -1,1294 +1,48 @@ -# Log 子系统 API 参考 +# Log API 边界 -## 概述 +本文不是完整 API reference。完整函数签名, 宏定义和字段请看 rustdoc 与源码注释。这里仅说明哪些 API 属于稳定使用边界, 哪些属于内部实现边界。 -本文档提供 Log 子系统所有公共 API 的完整参考,包括宏接口、写入 API、读取 API、配置 API 以及核心类型定义。每个 API 都包含函数签名、功能描述、源代码位置和使用示例。 +## 公共使用边界 -## 目录 +- `pr_emerg!`, `pr_alert!`, `pr_crit!`, `pr_err!`, `pr_warn!`, `pr_notice!`, `pr_info!`, `pr_debug!`: 内核分级日志入口。 +- `print!`, `println!`: 原始控制台输出, 同时写入日志缓冲。 +- `set_global_level`, `get_global_level`: 缓冲过滤阈值。 +- `set_console_level`, `get_console_level`: 控制台输出阈值。 +- `read_log`, `peek_log`, `log_len`, `log_unread_bytes`, `log_dropped_count`: 内核内读取和状态查询。 +- `syslog`: 用户态读取和控制日志缓冲的 syscall。 -- [宏接口](#宏接口) -- [写入 API](#写入-api) -- [读取 API](#读取-api) - - [read_log](#read_log) - 破坏性读取 - - [peek_log](#peek_log) - 非破坏性读取 - - [log_len](#log_len) - 日志条目数量 - - [log_unread_bytes](#log_unread_bytes) - 未读字节数 - - [log_reader_index](#log_reader_index) - 读指针位置 - - [log_writer_index](#log_writer_index) - 写指针位置 - - [log_dropped_count](#log_dropped_count) - 丢弃计数 -- [配置 API](#配置-api) -- [核心类型](#核心类型) -- [系统调用](#系统调用) - - [syslog](#syslog) - 用户空间日志控制 +## 内部边界 ---- +- `log_impl`: 由 `pr_*` 宏调用, 普通代码不应绕过宏直接调用。 +- `print_impl`: 由 `print!`/`println!` 调用。 +- `LogCore`: 日志核心状态对象, 生产路径使用全局实例。 +- `GlobalLogBuffer`: 环形缓冲实现细节。 +- `LogEntry` 内存布局: 不作为用户态 ABI。 -## 宏接口 +## syslog action 分组 -Log 子系统提供 8 个宏,对应 8 个日志级别。这些宏是用户代码记录日志的主要接口。 +- Open/Close: 兼容 NOP。 +- Read/ReadAll/ReadClear: 读取日志文本。 +- Clear: 清空缓冲。 +- ConsoleOff/ConsoleOn/ConsoleLevel: 调整控制台输出级别。 +- SizeUnread/SizeBuffer: 查询大小。 -### pr_emerg! +参数校验和权限检查位于 syscall util 和 `sys.rs`。当前权限检查仍是预留实现, 后续接入 capability 后应保持 action 分组语义不变。 -**级别**:Emergency (0) +## 维护约束 -**位置**:`os/src/log/macros.rs:60-67` +- 修改日志格式时, 同步更新控制台格式, syslog 格式和 formatted length 计算。 +- 新增公共 API 前先确认是否可以由现有读取/级别门面表达。 +- 不要让用户态 ABI 依赖 `LogEntry` 的 Rust 布局。 +- 不要在文档复制完整函数清单, 避免和 rustdoc 分叉。 -**签名**: -```rust -macro_rules! pr_emerg { - ($($arg:tt)*) => { ... } -} -``` +## 源码索引 -**功能**:记录 Emergency 级别的日志,表示系统不可用或即将崩溃。 - -**使用示例**: -```rust -pr_emerg!("Kernel panic: unable to continue"); -pr_emerg!("Critical hardware failure: {}", device_name); -``` - ---- - -### pr_alert! - -**级别**:Alert (1) - -**位置**:`os/src/log/macros.rs:79-86` - -**签名**: -```rust -macro_rules! pr_alert { - ($($arg:tt)*) => { ... } -} -``` - -**功能**:记录 Alert 级别的日志,表示必须立即采取行动的严重情况。 - -**使用示例**: -```rust -pr_alert!("Filesystem corruption detected"); -pr_alert!("Critical device failure: {}", error_code); -``` - ---- - -### pr_crit! - -**级别**:Critical (2) - -**位置**:`os/src/log/macros.rs:98-105` - -**签名**: -```rust -macro_rules! pr_crit { - ($($arg:tt)*) => { ... } -} -``` - -**功能**:记录 Critical 级别的日志,表示临界错误,系统功能受到严重影响。 - -**使用示例**: -```rust -pr_crit!("Failed to initialize memory subsystem"); -pr_crit!("Security violation detected: {}", violation_type); -``` - ---- - -### pr_err! - -**级别**:Error (3) - -**位置**:`os/src/log/macros.rs:118-127` - -**签名**: -```rust -macro_rules! pr_err { - ($($arg:tt)*) => { ... } -} -``` - -**功能**:记录 Error 级别的日志,表示错误条件,某个功能无法正常工作。 - -**使用示例**: -```rust -pr_err!("Failed to open file: {}", filename); -pr_err!("Device driver error: code = {}", error_code); -``` - ---- - -### pr_warn! - -**级别**:Warning (4) - -**位置**:`os/src/log/macros.rs:139-148` - -**签名**: -```rust -macro_rules! pr_warn { - ($($arg:tt)*) => { ... } -} -``` - -**功能**:记录 Warning 级别的日志,表示警告条件,可能导致问题但当前没有错误。 - -**使用示例**: -```rust -pr_warn!("Memory usage high: {}%", usage_percent); -pr_warn!("Deprecated API called: use {} instead", new_api); -``` - ---- - -### pr_notice! - -**级别**:Notice (5) - -**位置**:`os/src/log/macros.rs:158-167` - -**签名**: -```rust -macro_rules! pr_notice { - ($($arg:tt)*) => { ... } -} -``` - -**功能**:记录 Notice 级别的日志,表示正常但重要的信息,值得注意但不是错误。 - -**使用示例**: -```rust -pr_notice!("Network interface {} is up", interface_name); -pr_notice!("User {} logged in", username); -``` - ---- - -### pr_info! - -**级别**:Info (6) - -**位置**:`os/src/log/macros.rs:178-187` - -**签名**: -```rust -macro_rules! pr_info { - ($($arg:tt)*) => { ... } -} -``` - -**功能**:记录 Info 级别的日志,表示信息性消息,记录系统的正常操作。 - -**使用示例**: -```rust -pr_info!("Kernel initialized successfully"); -pr_info!("Loading module: {}", module_name); -``` - ---- - -### pr_debug! - -**级别**:Debug (7) - -**位置**:`os/src/log/macros.rs:199-208` - -**签名**: -```rust -macro_rules! pr_debug { - ($($arg:tt)*) => { ... } -} -``` - -**功能**:记录 Debug 级别的日志,表示调试级别的详细信息,仅供开发和问题诊断使用。 - -**使用示例**: -```rust -pr_debug!("Entering function: allocate_frame()"); -pr_debug!("Page table entry: PTE[{}] = {:#x}", index, value); -``` - ---- - -## 写入 API - -### log_impl - -**位置**:`os/src/log/mod.rs:93-95` - -**签名**: -```rust -pub fn log_impl(level: LogLevel, args: core::fmt::Arguments) -``` - -**功能**:日志写入的核心函数,由宏调用。直接调用此函数会绕过早期过滤,不推荐用户代码直接使用。 - -**参数**: -- `level: LogLevel` - 日志级别 -- `args: core::fmt::Arguments` - 格式化参数(由 `format_args!` 生成) - -**返回值**:无 - -**使用示例**: -```rust -use log::{log_impl, LogLevel}; -use core::format_args; - -// 不推荐直接使用,应使用宏 -log_impl(LogLevel::Info, format_args!("Message: {}", value)); - -// 推荐使用宏,有早期过滤优化 -pr_info!("Message: {}", value); -``` - -**注意事项**: -- 直接调用会绕过早期过滤,即使级别被禁用,格式化参数仍然会被求值 -- 宏接口(`pr_*!`)会自动进行早期过滤,性能更好 - ---- - -### is_level_enabled - -**位置**:`os/src/log/mod.rs:99-101` - -**签名**: -```rust -pub fn is_level_enabled(level: LogLevel) -> bool -``` - -**功能**:检查指定的日志级别是否启用(即是否达到或超过 global_level)。宏展开时使用此函数进行早期过滤。 - -**参数**: -- `level: LogLevel` - 要检查的日志级别 - -**返回值**: -- `bool` - 如果级别启用返回 `true`,否则返回 `false` - -**使用示例**: -```rust -use log::{is_level_enabled, LogLevel, pr_debug}; - -// 检查 Debug 级别是否启用 -if is_level_enabled(LogLevel::Debug) { - // 执行昂贵的计算 - let result = expensive_calculation(); - pr_debug!("Result: {}", result); -} - -// 宏内部使用此函数进行早期过滤 -// pr_info!("message") 展开为: -// if is_level_enabled(LogLevel::Info) { -// log_impl(LogLevel::Info, format_args!("message")); -// } -``` - -**注意事项**: -- 此函数只检查 `global_level`,不检查 `console_level` -- 返回 `true` 表示日志会被缓存,但不一定会显示到控制台 - ---- - -## 读取 API - -### read_log - -**位置**:`os/src/log/mod.rs:104-106` - -**签名**: -```rust -pub fn read_log() -> Option -``` - -**功能**:从环形缓冲区读取一条日志。日志按 FIFO(先进先出)顺序返回。如果缓冲区为空,返回 `None`。 - -**参数**:无 - -**返回值**: -- `Option` - 成功返回 `Some(LogEntry)`,缓冲区为空返回 `None` - -**使用示例**: -```rust -use log::read_log; - -// 读取单条日志 -if let Some(entry) = read_log() { - println!("{}", entry); -} - -// 读取所有日志 -while let Some(entry) = read_log() { - println!("{}", entry); -} - -// 处理日志条目 -if let Some(entry) = read_log() { - println!("Level: {:?}", entry.level()); - println!("CPU: {}", entry.cpu_id()); - println!("Timestamp: {}", entry.timestamp()); - println!("Message: {}", entry.message()); -} -``` - -**注意事项**: -- 每次调用消费一条日志,下次调用返回下一条 -- 只能有一个读取者(MPSC 模型),多个读取者会导致竞争条件 -- 读取是非阻塞的,如果缓冲区为空立即返回 `None` - ---- - -### log_len - -**位置**:`os/src/log/mod.rs:109-111` - -**签名**: -```rust -pub fn log_len() -> usize -``` - -**功能**:返回缓冲区中当前有多少条日志等待读取。 - -**参数**:无 - -**返回值**: -- `usize` - 缓冲区中的日志数量 - -**使用示例**: -```rust -use log::{log_len, read_log}; - -// 检查缓冲区状态 -let count = log_len(); -println!("Buffered logs: {}", count); - -// 批量读取 -if count > 0 { - println!("Reading {} logs:", count); - for i in 0..count { - if let Some(entry) = read_log() { - println!("{}: {}", i, entry); - } - } -} - -// 检查缓冲区是否接近满 -let capacity = 58; // 缓冲区容量约 58 条 -if count > capacity * 80 / 100 { - println!("Warning: log buffer is {}% full", count * 100 / capacity); -} -``` - -**注意事项**: -- 返回值是快照,可能在读取过程中发生变化(其他 CPU 可能并发写入) -- 不保证能读取到返回的数量,因为可能被覆盖 - ---- - -### log_dropped_count - -**位置**:`os/src/log/mod.rs:114-116` - -**签名**: -```rust -pub fn log_dropped_count() -> usize -``` - -**功能**:返回由于缓冲区溢出而被丢弃的日志数量。这是一个累计计数,系统启动后持续增长。 - -**参数**:无 - -**返回值**: -- `usize` - 被丢弃的日志总数 - -**使用示例**: -```rust -use log::log_dropped_count; - -// 检查是否有日志被丢弃 -let dropped = log_dropped_count(); -if dropped > 0 { - pr_warn!("Warning: {} logs were dropped due to buffer overflow", dropped); -} - -// 监控丢弃率 -let mut last_dropped = 0; -loop { - sleep_ms(1000); - - let current_dropped = log_dropped_count(); - let rate = current_dropped - last_dropped; - last_dropped = current_dropped; - - if rate > 0 { - println!("Dropping {} logs per second", rate); - } -} - -// 诊断性能问题 -if log_dropped_count() > 1000 { - pr_err!("Excessive log dropping detected, consider:"); - pr_err!(" 1. Increasing buffer size (BUFFER_SIZE in config.rs)"); - pr_err!(" 2. Reading logs more frequently"); - pr_err!(" 3. Reducing log verbosity (increase global_level)"); -} -``` - -**注意事项**: -- 这是累计计数,不会重置 -- 非零值表示日志读取速度跟不上写入速度 -- 频繁丢弃日志表示系统存在性能问题或配置不当 - ---- - -### peek_log - -**位置**:`os/src/log/mod.rs:103-105` - -**签名**: -```rust -pub fn peek_log(index: usize) -> Option -``` - -**功能**:非破坏性读取:按索引 peek 日志条目,不移动读指针。允许读取缓冲区中的日志而不删除它们,主要用于 `SyslogAction::ReadAll` 操作。 - -**参数**: -- `index: usize` - 全局序列号(从读指针开始计数) - -**返回值**: -- `Option` - 成功返回 `Some(LogEntry)`,索引超出范围或条目已被覆盖返回 `None` - -**使用示例**: -```rust -use log::{peek_log, log_reader_index, log_writer_index}; - -// 读取所有可用日志(不删除) -let start = log_reader_index(); -let end = log_writer_index(); - -for index in start..end { - if let Some(entry) = peek_log(index) { - println!("{}", entry); - // 日志仍保留在缓冲区中 - } -} - -// 可以重复读取 -for index in start..end { - if let Some(entry) = peek_log(index) { - // 再次读取相同的日志 - process_entry(&entry); - } -} -``` - -**注意事项**: -- 不移除日志,可以重复读取 -- 索引必须在 `[log_reader_index(), log_writer_index())` 范围内 -- 如果缓冲区已满并发生覆盖,旧索引可能返回 `None` -- 并发安全:可以与 write 并发调用 - ---- - -### log_reader_index - -**位置**:`os/src/log/mod.rs:108-110` - -**签名**: -```rust -pub fn log_reader_index() -> usize -``` - -**功能**:获取当前可读取的起始索引(读指针位置)。 - -**参数**:无 - -**返回值**: -- `usize` - 当前读指针位置(全局序列号) - -**使用示例**: -```rust -use log::{log_reader_index, log_writer_index, peek_log}; - -// 获取可读范围 -let start = log_reader_index(); -let end = log_writer_index(); -let count = end - start; - -println!("Available logs: {} (from {} to {})", count, start, end); - -// 遍历所有可用日志 -for index in start..end { - if let Some(entry) = peek_log(index) { - println!("Log #{}: {}", index, entry); - } -} -``` - -**注意事项**: -- 返回值是快照,可能在使用过程中发生变化 -- 配合 `log_writer_index()` 使用可以获取可读范围 - ---- - -### log_writer_index - -**位置**:`os/src/log/mod.rs:113-115` - -**签名**: -```rust -pub fn log_writer_index() -> usize -``` - -**功能**:获取当前写入位置(下一个要写入的索引)。 - -**参数**:无 - -**返回值**: -- `usize` - 当前写指针位置(全局序列号) - -**使用示例**: -```rust -use log::{log_reader_index, log_writer_index}; - -// 计算未读日志数量 -let start = log_reader_index(); -let end = log_writer_index(); -let unread_count = end - start; - -println!("Unread logs: {}", unread_count); - -// 检查缓冲区使用率 -let capacity = 58; // 缓冲区容量约 58 条 -let usage_percent = (unread_count * 100) / capacity; -println!("Buffer usage: {}%", usage_percent); -``` - -**注意事项**: -- 返回值是快照,其他 CPU 可能并发写入导致值变化 -- 配合 `log_reader_index()` 使用可以获取可读范围 - ---- - -### log_unread_bytes - -**位置**:`os/src/log/mod.rs:118-120` - -**签名**: -```rust -pub fn log_unread_bytes() -> usize -``` - -**功能**:返回未读日志的总字节数(格式化后)。精确计算所有未读日志格式化为字符串后的总字节数,用于 `SyslogAction::SizeUnread` 系统调用。 - -**参数**:无 - -**返回值**: -- `usize` - 未读日志的总字节数(格式化后) - -**使用示例**: -```rust -use log::{log_len, log_unread_bytes}; - -// 查询缓冲区状态 -let count = log_len(); -let bytes = log_unread_bytes(); - -println!("Buffered logs: {} entries, {} bytes", count, bytes); - -// 分配足够的缓冲区读取所有日志 -let mut buffer = vec![0u8; bytes]; -// ... 使用 syslog 系统调用读取 ... - -// 检查是否需要刷新日志 -if bytes > 4096 { - println!("Log buffer has {} bytes, consider flushing", bytes); -} -``` - -**注意事项**: -- 返回值是精确的字节数,包括 ANSI 颜色代码、时间戳等格式化内容 -- 每次 `read_log()` 会减少相应的字节数 -- 并发安全:使用原子操作维护计数 - ---- - -## 配置 API - -### set_global_level - -**位置**:`os/src/log/mod.rs:119-121` - -**签名**: -```rust -pub fn set_global_level(level: LogLevel) -``` - -**功能**:设置全局日志级别。低于此级别的日志会被完全忽略(宏展开时就跳过),达到或超过此级别的日志会被缓存。 - -**参数**: -- `level: LogLevel` - 新的全局级别 - -**返回值**:无 - -**使用示例**: -```rust -use log::{set_global_level, LogLevel}; - -// 缓存所有日志(包括 Debug) -set_global_level(LogLevel::Debug); - -// 只缓存 Info 及以上级别(默认) -set_global_level(LogLevel::Info); - -// 只缓存警告和错误 -set_global_level(LogLevel::Warning); - -// 只缓存错误 -set_global_level(LogLevel::Error); - -// 临时调整级别 -let old_level = get_global_level(); -set_global_level(LogLevel::Debug); -// ... 执行需要调试的代码 ... -set_global_level(old_level); -``` - -**注意事项**: -- 设置立即生效,影响所有后续的日志调用 -- 应该小于或等于 `console_level`,否则部分缓存的日志无法显示 -- 降低级别(如设置为 Error)可以减少日志开销,提高性能 - ---- - -### get_global_level - -**位置**:`os/src/log/mod.rs:124-126` - -**签名**: -```rust -pub fn get_global_level() -> LogLevel -``` - -**功能**:获取当前的全局日志级别。 - -**参数**:无 - -**返回值**: -- `LogLevel` - 当前的全局级别 - -**使用示例**: -```rust -use log::{get_global_level, set_global_level, LogLevel}; - -// 查询当前级别 -let level = get_global_level(); -println!("Current global level: {:?}", level); - -// 保存和恢复级别 -let saved_level = get_global_level(); -set_global_level(LogLevel::Debug); -// ... 执行需要详细日志的代码 ... -set_global_level(saved_level); - -// 条件设置 -if get_global_level() > LogLevel::Info { - println!("Info logs are disabled, enabling..."); - set_global_level(LogLevel::Info); -} -``` - ---- - -### set_console_level - -**位置**:`os/src/log/mod.rs:129-131` - -**签名**: -```rust -pub fn set_console_level(level: LogLevel) -``` - -**功能**:设置控制台日志级别。低于此级别的日志不会打印到控制台(但仍可能被缓存),达到或超过此级别的日志会立即显示。 - -**参数**: -- `level: LogLevel` - 新的控制台级别 - -**返回值**:无 - -**使用示例**: -```rust -use log::{set_console_level, LogLevel}; - -// 显示所有日志(包括 Debug) -set_console_level(LogLevel::Debug); - -// 显示 Info 及以上级别 -set_console_level(LogLevel::Info); - -// 只显示警告和错误(默认) -set_console_level(LogLevel::Warning); - -// 只显示错误 -set_console_level(LogLevel::Error); - -// 完全禁用控制台输出 -set_console_level(LogLevel::Emergency); // 只有 Emergency 才显示 -// 或者使用一个不存在的高级别(但不推荐,使用最高级别即可) -``` - -**注意事项**: -- 设置立即生效,影响所有后续的日志调用 -- 应该大于或等于 `global_level`,否则被过滤的日志不会被缓存 -- 控制台输出较慢,提高级别可以减少串口通信开销 - ---- - -### get_console_level - -**位置**:`os/src/log/mod.rs:134-136` - -**签名**: -```rust -pub fn get_console_level() -> LogLevel -``` - -**功能**:获取当前的控制台日志级别。 - -**参数**:无 - -**返回值**: -- `LogLevel` - 当前的控制台级别 - -**使用示例**: -```rust -use log::{get_console_level, set_console_level, LogLevel}; - -// 查询当前级别 -let level = get_console_level(); -println!("Current console level: {:?}", level); - -// 保存和恢复级别 -let saved_level = get_console_level(); -set_console_level(LogLevel::Info); -// ... 执行需要详细控制台输出的代码 ... -set_console_level(saved_level); - -// 比较两个级别 -let global = get_global_level(); -let console = get_console_level(); -if console < global { - println!("Warning: console_level < global_level, some logs won't be displayed"); -} -``` - ---- - -## 核心类型 - -### LogLevel - -**位置**:`os/src/log/level.rs:23-36` - -**定义**: -```rust -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum LogLevel { - Emergency = 0, - Alert = 1, - Critical = 2, - Error = 3, - Warning = 4, - Notice = 5, - Info = 6, - Debug = 7, -} -``` - -**功能**:定义 8 个日志级别,数值越小优先级越高。 - -**方法**: - -#### LogLevel::from_u8 - -```rust -pub fn from_u8(value: u8) -> Option -``` - -从 u8 值创建 LogLevel,如果值无效返回 None。 - -**使用示例**: -```rust -use log::LogLevel; - -let level = LogLevel::Info; -println!("Level: {:?}", level); - -// 级别比较 -if level >= LogLevel::Warning { - println!("This is a warning or error"); -} - -// 从整数创建 -if let Some(level) = LogLevel::from_u8(6) { - println!("Level: {:?}", level); // Info -} - -// 使用 match -match level { - LogLevel::Emergency | LogLevel::Alert | LogLevel::Critical => { - println!("Critical situation!"); - } - LogLevel::Error | LogLevel::Warning => { - println!("Problem detected"); - } - _ => { - println!("Normal operation"); - } -} -``` - ---- - -### LogEntry - -**位置**:`os/src/log/entry.rs:20-30` - -**定义**: -```rust -#[repr(C, align(8))] -pub struct LogEntry { - seq: AtomicUsize, - level: LogLevel, - cpu_id: usize, - length: usize, - task_id: u32, - timestamp: usize, - message: [u8; MAX_MESSAGE_LEN], -} -``` - -**功能**:表示单条日志记录,包含所有元数据和消息内容。 - -**方法**: - -#### LogEntry::level - -```rust -pub fn level(&self) -> LogLevel -``` - -返回日志级别。 - -#### LogEntry::cpu_id - -```rust -pub fn cpu_id(&self) -> usize -``` - -返回记录日志的 CPU 核心 ID。 - -#### LogEntry::timestamp - -```rust -pub fn timestamp(&self) -> usize -``` - -返回时间戳(架构相关单位)。 - -#### LogEntry::task_id - -```rust -pub fn task_id(&self) -> u32 -``` - -返回记录日志的任务 ID。 - -#### LogEntry::message - -```rust -pub fn message(&self) -> &str -``` - -返回日志消息字符串。 - -**使用示例**: -```rust -use log::read_log; - -if let Some(entry) = read_log() { - // 使用 Display trait 格式化输出 - println!("{}", entry); - - // 访问各个字段 - println!("Level: {:?}", entry.level()); - println!("CPU: {}", entry.cpu_id()); - println!("Timestamp: {}", entry.timestamp()); - println!("Task: {}", entry.task_id()); - println!("Message: {}", entry.message()); - - // 条件处理 - if entry.level() <= LogLevel::Error { - // 错误日志需要特殊处理 - send_alert(&entry); - } - - // 过滤特定 CPU 的日志 - if entry.cpu_id() == 0 { - println!("Log from CPU 0: {}", entry.message()); - } -} -``` - ---- - -### LogCore - -**位置**:`os/src/log/log_core.rs:15-20` - -**定义**: -```rust -pub struct LogCore { - buffer: GlobalLogBuffer, - global_level: AtomicU8, - console_level: AtomicU8, -} -``` - -**功能**:日志系统的核心结构,管理环形缓冲区和双过滤器。用户代码通常不直接使用此类型,而是通过 `GLOBAL_LOG` 单例和公共 API。 - -**全局单例**: - -```rust -// 定义在 os/src/log/mod.rs:87 -pub static GLOBAL_LOG: LogCore = LogCore::new(); -``` - -**使用示例**: -```rust -// 用户代码不需要直接使用 LogCore -// 所有操作都通过公共 API 进行 - -// 如果需要访问全局单例(不推荐) -use log::GLOBAL_LOG; - -// 但通常应该使用公共 API -use log::{pr_info, read_log, set_global_level}; -pr_info!("Use public APIs instead"); -``` - ---- - -## 完整示例 - -### 示例 1:基本日志记录 - -```rust -use log::*; - -fn main() { - // 配置日志级别 - set_global_level(LogLevel::Info); - set_console_level(LogLevel::Warning); - - // 记录不同级别的日志 - pr_debug!("This won't be logged (below Info)"); - pr_info!("System starting..."); - pr_warn!("Memory usage: 80%"); - pr_err!("Failed to load module"); - - // Info 被缓存但不显示(低于 Warning) - // Warning 和 Error 被缓存并显示 -} -``` - -### 示例 2:读取和处理日志 - -```rust -use log::*; - -fn log_processor() { - loop { - // 检查缓冲区状态 - let count = log_len(); - if count > 0 { - println!("Processing {} logs", count); - - // 读取所有日志 - while let Some(entry) = read_log() { - // 根据级别处理 - match entry.level() { - LogLevel::Emergency | LogLevel::Alert | LogLevel::Critical => { - // 发送紧急通知 - send_emergency_alert(&entry); - } - LogLevel::Error => { - // 记录到错误文件 - write_to_error_log(&entry); - } - _ => { - // 正常处理 - write_to_log_file(&entry); - } - } - } - } - - // 检查溢出 - let dropped = log_dropped_count(); - if dropped > last_dropped { - pr_warn!("Dropped {} logs since last check", dropped - last_dropped); - last_dropped = dropped; - } - - sleep_ms(100); - } -} -``` - -### 示例 3:性能分析 - -```rust -use log::*; -use arch::timer::get_time; - -fn benchmark_logging() { - // 测试缓冲区写入性能(无控制台输出) - set_console_level(LogLevel::Emergency); - - let start = get_time(); - for i in 0..1000 { - pr_info!("Message {}", i); - } - let buffered_time = get_time() - start; - - // 测试控制台输出性能 - set_console_level(LogLevel::Info); - - let start = get_time(); - for i in 0..100 { - pr_info!("Message {}", i); - } - let console_time = get_time() - start; - - pr_info!("Benchmark results:"); - pr_info!(" Buffered: {} cycles for 1000 logs ({} cycles/log)", - buffered_time, buffered_time / 1000); - pr_info!(" Console: {} cycles for 100 logs ({} cycles/log)", - console_time, console_time / 100); -} -``` - ---- - -## 注意事项 - -### 线程安全 - -所有公共 API 都是线程安全的,可以在多核环境下并发调用: - -- 写入 API(`log_impl`、宏):多核并发安全,使用原子操作协调 -- 读取 API(`read_log`):只能有一个读取者(MPSC 模型) -- 配置 API(`set_*_level`、`get_*_level`):多核并发安全,使用原子操作 - -### 中断上下文 - -Log 子系统可以在中断处理程序中安全使用: - -- 无锁设计,不会导致死锁 -- 固定大小分配,不使用堆内存 -- 原子操作由硬件支持,不需要禁用中断 - -但应注意: - -- 中断处理程序应该快速完成,避免大量日志记录 -- 控制台输出较慢,中断中应避免触发控制台输出 - -### 性能考虑 - -- 早期过滤:被禁用级别的日志零开销(宏展开时跳过) -- 缓冲区写入:无锁,非常快(约 100-200 纳秒) -- 控制台输出:较慢,取决于串口速度(约几毫秒) - -建议: - -- 热路径使用 `pr_debug!`,生产环境禁用 Debug 级别 -- 提高 `console_level` 减少控制台输出 -- 定期读取日志避免缓冲区溢出 - ---- - -## 系统调用 - -### syslog - -**位置**:`os/src/kernel/syscall/sys.rs:127-378` - -**签名**: -```rust -pub fn syslog(type_: i32, bufp: *mut u8, len: i32) -> isize -``` - -**功能**:读取和控制内核日志缓冲区。完全兼容 Linux `syslog(2)` 系统调用,允许用户空间程序查询、读取和控制内核日志。 - -**参数**: -- `type_: i32` - 操作类型 (0-10),详见 `SyslogAction` -- `bufp: *mut u8` - 用户空间缓冲区指针(某些操作需要) -- `len: i32` - 缓冲区长度或命令参数(取决于操作类型) - -**返回值**: -* **成功**: - - 类型 2/3/4: 读取的字节数 - - 类型 8: 旧的 console_loglevel (1-8) - - 类型 9: 未读字节数 - - 类型 10: 缓冲区总大小 - - 其他: 0 -* **失败**:负的 errno - - `-EINVAL`: 无效参数 - - `-EPERM`: 权限不足 - - `-EINTR`: 被信号中断 - - `-EFAULT`: 无效的用户空间指针 - -**操作类型** (`SyslogAction`): - -| 值 | 名称 | 描述 | -|----|------|------| -| 0 | CLOSE | 关闭日志(NOP) | -| 1 | OPEN | 打开日志(NOP) | -| 2 | READ | 破坏性读取日志 | -| 3 | READ_ALL | 非破坏性读取所有日志 | -| 4 | READ_CLEAR | 读取并清空日志 | -| 5 | CLEAR | 清空日志缓冲区 | -| 6 | CONSOLE_OFF | 禁用控制台输出 | -| 7 | CONSOLE_ON | 启用控制台输出 | -| 8 | CONSOLE_LEVEL | 设置控制台日志级别 | -| 9 | SIZE_UNREAD | 查询未读字节数 | -| 10 | SIZE_BUFFER | 查询缓冲区总大小 | - -**使用示例**: - -```c -#include -#include -#include - -// 1. 读取内核日志(破坏性) -char buf[8192]; -int len = syscall(SYS_syslog, 2, buf, sizeof(buf)); -if (len > 0) { - write(STDOUT_FILENO, buf, len); -} - -// 2. 读取所有日志(非破坏性) -len = syscall(SYS_syslog, 3, buf, sizeof(buf)); - -// 3. 查询未读字节数 -int unread = syscall(SYS_syslog, 9, NULL, 0); -printf("Unread bytes: %d\n", unread); - -// 4. 查询缓冲区总大小 -int size = syscall(SYS_syslog, 10, NULL, 0); -printf("Buffer size: %d\n", size); - -// 5. 设置控制台日志级别(1-8) -// 返回旧的级别 -int old_level = syscall(SYS_syslog, 8, NULL, 5); // 设置为 5 (Notice) -printf("Old level: %d\n", old_level); - -// 6. 清空日志缓冲区 -syscall(SYS_syslog, 5, NULL, 0); - -// 7. 禁用控制台输出 -syscall(SYS_syslog, 6, NULL, 0); - -// 8. 启用控制台输出 -syscall(SYS_syslog, 7, NULL, 0); -``` - -**日志级别映射**: - -Linux `console_loglevel` 使用 1-8 的值,其中数值越小优先级越高: -- `console_loglevel = N` 表示显示级别 < N 的消息 -- Comix 内部使用 0-7 (LogLevel::Emergency 到 Debug) -- 转换公式:`comix_level = linux_level - 1` - -| Linux Level | Comix Level | 显示级别 | -|-------------|-------------|----------| -| 1 | 0 (Emergency) | 只显示 Emergency | -| 2 | 1 (Alert) | Emergency, Alert | -| 3 | 2 (Critical) | Emergency, Alert, Critical | -| 4 | 3 (Error) | Emergency ~ Error | -| 5 | 4 (Warning) | Emergency ~ Warning | -| 6 | 5 (Notice) | Emergency ~ Notice | -| 7 | 6 (Info) | Emergency ~ Info | -| 8 | 7 (Debug) | 显示所有级别 | - -**权限要求**: - -1. **特殊情况:ReadAll 和 SizeBuffer** - - 如果 `dmesg_restrict == 0`:允许所有用户访问 - - 如果 `dmesg_restrict != 0`:需要特权 -2. **其他操作**:需要以下任一权限: - - `euid == 0` (root 用户) - - `CAP_SYSLOG` (推荐) - - `CAP_SYS_ADMIN` (向后兼容) - -**注意事项**: -- READ (类型 2) 是破坏性的,读取后日志从缓冲区删除 -- READ_ALL (类型 3) 是非破坏性的,可以重复读取 -- CONSOLE_LEVEL 的参数范围是 1-8,超出范围返回 `-EINVAL` -- SIZE_UNREAD 返回的是精确的格式化后字节数,可用于分配缓冲区 -- 当前权限检查未完全实现,等待用户管理系统完善 - -**用户空间工具示例** (`dmesg` 实现): - -```c -// 简化的 dmesg 工具实现 -#include -#include -#include -#include - -#define SYSLOG_ACTION_READ_ALL 3 -#define SYSLOG_ACTION_SIZE_UNREAD 9 - -int main() { - // 查询需要多少空间 - int size = syscall(SYS_syslog, SYSLOG_ACTION_SIZE_UNREAD, NULL, 0); - if (size < 0) { - perror("syslog"); - return 1; - } - - // 分配缓冲区 - char *buf = malloc(size + 1); - if (!buf) { - perror("malloc"); - return 1; - } - - // 读取所有日志(非破坏性) - int len = syscall(SYS_syslog, SYSLOG_ACTION_READ_ALL, buf, size); - if (len < 0) { - perror("syslog"); - free(buf); - return 1; - } - - // 显示日志 - buf[len] = '\0'; - printf("%s", buf); - - free(buf); - return 0; -} -``` - ---- - -## 相关文档 - -- [整体架构](architecture.md) - Log 子系统的设计和架构 -- [使用指南](usage.md) - 详细的使用示例和最佳实践,包括 syslog 使用 -- [日志级别](level.md) - 日志级别的语义和使用建议 -- [缓冲区和条目](buffer_and_entry.md) - 环形缓冲区和日志条目的实现细节 +- `os/src/log/mod.rs`: 公共门面。 +- `os/src/log/macros.rs`: 宏入口。 +- `os/src/log/log_core.rs`: `LogCore`, `format_log_entry()`。 +- `os/src/log/buffer.rs`: 缓冲读取和统计。 +- `os/src/log/entry.rs`: `LogEntry`。 +- `os/src/kernel/syscall/sys.rs`: `syslog()`。 +- `os/src/kernel/syscall/util.rs`: syslog 参数和权限辅助。 +- `os/src/uapi/log.rs`: `SyslogAction`。 diff --git a/document/log/architecture.md b/document/log/architecture.md index 0684affe..aa44d2b7 100644 --- a/document/log/architecture.md +++ b/document/log/architecture.md @@ -1,607 +1,85 @@ -# Log 子系统架构 - -## 概述 - -本文档详细介绍 Log 子系统的整体架构、模块依赖关系、数据流转过程、同步机制、设计决策以及性能和安全性考量。Log 子系统采用分层架构设计,各层职责清晰,通过无锁环形缓冲区和双过滤器实现高效的日志记录。 - -## 分层架构 - -Log 子系统采用四层架构,从上到下依次为用户层、模块入口层、核心系统层和底层组件层: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 用户层 (User Layer) │ -│ │ -│ pr_emerg!() pr_alert!() pr_crit!() pr_err!() │ -│ pr_warn!() pr_notice!() pr_info!() pr_debug!() │ -│ │ -│ 内核代码通过宏接口记录日志,宏负责早期级别过滤 │ -└─────────────────────────────┬───────────────────────────────────┘ - │ - │ 宏展开调用 - │ -┌─────────────────────────────▼───────────────────────────────────┐ -│ 模块入口层 (Module Entry Layer) │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ mod.rs - 全局单例和公共 API │ │ -│ │ │ │ -│ │ · GLOBAL_LOG: LogCore (编译期初始化的全局单例) │ │ -│ │ · log_impl() - 日志写入入口 │ │ -│ │ · is_level_enabled() - 级别检查 │ │ -│ │ · read_log() / log_len() / log_dropped_count() - 读取 │ │ -│ │ · set_global_level() / set_console_level() - 配置 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────┬───────────────────────────────────┘ - │ - │ 委托给核心系统 - │ -┌─────────────────────────────▼───────────────────────────────────┐ -│ 核心系统层 (Core System Layer) │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ log_core.rs - LogCore 结构体 │ │ -│ │ │ │ -│ │ · buffer: GlobalLogBuffer - 环形缓冲区 │ │ -│ │ · global_level: AtomicU8 - 全局级别过滤器 │ │ -│ │ · console_level: AtomicU8 - 控制台级别过滤器 │ │ -│ │ │ │ -│ │ 协调日志写入的两条路径: │ │ -│ │ 1. 缓冲区路径:检查 global_level → 写入 buffer │ │ -│ │ 2. 控制台路径:检查 console_level → 立即打印 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└──────────┬──────────────────────┬───────────────────────────────┘ - │ │ - │ │ - ▼ ▼ -┌──────────────────┐ ┌─────────────────────┐ -│ 缓冲区写入 │ │ 控制台输出 │ -└──────────────────┘ └─────────────────────┘ - │ │ - │ │ -┌──────────▼──────────────────────▼───────────────────────────────┐ -│ 底层组件层 (Component Layer) │ -│ │ -│ ┌───────────────┐ ┌───────────────┐ ┌──────────────────┐ │ -│ │ buffer.rs │ │ entry.rs │ │ context.rs │ │ -│ │ │ │ │ │ │ │ -│ │ 环形缓冲区 │ │ 日志条目 │ │ 上下文收集 │ │ -│ │ MPSC 模型 │ │ 序列化 │ │ CPU/Task/时间 │ │ -│ └───────────────┘ └───────────────┘ └──────────────────┘ │ -│ │ -│ ┌───────────────┐ ┌───────────────┐ │ -│ │ level.rs │ │ config.rs │ │ -│ │ │ │ │ │ -│ │ 日志级别 │ │ 配置常量 │ │ -│ │ 颜色映射 │ │ 缓冲区大小 │ │ -│ └───────────────┘ └───────────────┘ │ -└───────────────────────────────┬───────────────────────────────┘ - │ - │ 依赖外部模块 - │ -┌───────────────────────────────▼───────────────────────────────┐ -│ 外部依赖 (External Dependencies) │ -│ │ -│ · arch::timer::get_time() - 获取时间戳 │ -│ · arch::kernel::cpu::cpu_id() - 获取当前 CPU ID │ -│ · kernel::cpu::current_cpu() - 获取当前任务信息 │ -│ · console::Stdout - 控制台输出接口 │ -│ · core::sync::atomic - 原子操作 │ -└─────────────────────────────────────────────────────────────────┘ +# Log 架构 + +Log 架构围绕一个全局 `LogCore` 展开。它把分级过滤, 条目构造, 环形缓冲和控制台即时输出放在同一核心对象中, 对外暴露轻量门面。 + +## 当前状态 + +```text +pr_* macro / print! + | + v +log::mod public facade + | + v +LogCore + | | + v v +GlobalLogBuffer Console + | + v +syslog syscall ``` -### 各层职责 - -#### 用户层 (User Layer) - -用户层是内核代码与 Log 子系统的接口,通过 8 个宏(`pr_emerg!` 到 `pr_debug!`)提供简洁的日志记录 API。宏在展开时负责早期级别过滤,通过调用 `is_level_enabled()` 判断日志级别是否启用,避免格式化被禁用的日志。宏接口的定义位于 `os/src/log/macros.rs`。 - -#### 模块入口层 (Module Entry Layer) - -模块入口层由 `os/src/log/mod.rs` 实现,负责: -- 定义全局单例 `GLOBAL_LOG`,采用编译期初始化(`const fn`)保证零运行时开销 -- 导出所有公共 API,包括写入 API(`log_impl`、`is_level_enabled`)、读取 API(`read_log`、`log_len`、`log_dropped_count`)和配置 API(`set_global_level`、`set_console_level` 等) -- 对外屏蔽内部实现细节,提供稳定的公共接口 - -#### 核心系统层 (Core System Layer) - -核心系统层由 `os/src/log/log_core.rs` 中的 `LogCore` 结构体实现,是 Log 子系统的核心逻辑: -- 管理环形缓冲区 `GlobalLogBuffer`,处理日志的写入和读取 -- 维护两个独立的级别过滤器:`global_level` 控制哪些日志被缓存,`console_level` 控制哪些日志立即打印 -- 协调双路输出策略,确保日志既被缓存又能实时显示(根据级别配置) -- 收集日志的上下文信息(CPU ID、时间戳等)并创建日志条目 - -#### 底层组件层 (Component Layer) - -底层组件层提供核心系统层所需的基础设施: -- **buffer.rs**:实现无锁 MPSC 环形缓冲区,处理并发写入和溢出 -- **entry.rs**:定义日志条目的内存布局,实现序列化和格式化显示 -- **context.rs**:收集日志的上下文信息(CPU ID、任务 ID、时间戳) -- **level.rs**:定义 8 级日志分类和颜色映射 -- **config.rs**:集中管理配置常量(缓冲区大小、消息长度限制等) - -## 模块依赖关系 - -Log 子系统内部模块之间的依赖关系如下图所示: - -``` - ┌──────────────┐ - │ macros.rs │ 宏接口层 - └──────┬───────┘ - │ 依赖 - ▼ - ┌──────────────┐ - │ mod.rs │ 模块入口 - │ (GLOBAL_LOG) │ - └──────┬───────┘ - │ 依赖 - ▼ - ┌─────────────────┐ - │ log_core.rs │ 核心逻辑 - │ (LogCore) │ - └────┬────────┬───┘ - │ │ - │ └─────────────────┐ - │ │ - ▼ ▼ - ┌──────────────┐ ┌──────────────┐ - │ buffer.rs │ │ level.rs │ - │(GlobalLog │ │ (LogLevel) │ - │ Buffer) │ └──────────────┘ - └──────┬───────┘ - │ 依赖 - ▼ - ┌──────────────┐ - │ entry.rs │ - │ (LogEntry) │ - └──────┬───────┘ - │ 依赖 - │ - ┌────┴─────┬──────────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────┐ ┌────────┐ ┌──────────┐ -│context │ │level.rs│ │config.rs │ -│ .rs │ │ │ │ │ -└─────────┘ └────────┘ └──────────┘ -``` - -### 依赖说明 - -1. **macros.rs → mod.rs**:宏调用 `mod.rs` 导出的 `log_impl()` 和 `is_level_enabled()` 函数 -2. **mod.rs → log_core.rs**:全局单例 `GLOBAL_LOG` 的类型是 `LogCore`,所有公共 API 委托给 `LogCore` 的方法 -3. **log_core.rs → buffer.rs**:`LogCore` 包含 `GlobalLogBuffer` 字段,用于缓存日志 -4. **log_core.rs → level.rs**:`LogCore` 使用 `LogLevel` 进行级别比较和过滤 -5. **buffer.rs → entry.rs**:环形缓冲区存储 `LogEntry` 类型的数据 -6. **entry.rs → context.rs**:创建日志条目时需要收集上下文信息 -7. **entry.rs → level.rs**:日志条目包含级别信息,用于格式化显示 -8. **entry.rs → config.rs**:消息缓冲区大小由 `MAX_MESSAGE_LEN` 常量定义 -9. **所有模块 → config.rs**:配置常量被多个模块引用 - -### 关键数据流路径 - -#### 路径 1:写入日志 - -用户代码 → 宏 (`macros.rs`) → `is_level_enabled()` (`mod.rs`) → `log_impl()` (`mod.rs`) → `LogCore::log()` (`log_core.rs`) → `GlobalLogBuffer::write()` (`buffer.rs`) → 原子操作写入 `LogEntry` (`entry.rs`) - -#### 路径 2:控制台输出 - -`LogCore::log()` → 检查 `console_level` → `console::Stdout::write_fmt()` → 立即打印到控制台 - -#### 路径 3:读取日志 - -用户代码 → `read_log()` (`mod.rs`) → `LogCore::read()` (`log_core.rs`) → `GlobalLogBuffer::read()` (`buffer.rs`) → 返回 `LogEntry` - -#### 路径 4:配置级别 - -用户代码 → `set_global_level()` (`mod.rs`) → `LogCore::set_global_level()` (`log_core.rs`) → 原子写入 `global_level` - -## 双路输出策略 - -Log 子系统的核心特点是双路输出策略,日志同时经过两条路径处理: - -``` - 用户调用 pr_info!("message") - │ - │ - ┌───────────▼───────────┐ - │ 宏展开 + 早期过滤 │ - │ is_level_enabled()? │ - └───────────┬───────────┘ - │ (通过) - ┌───────────▼───────────┐ - │ log_impl(level, msg) │ - │ 创建 LogEntry │ - │ 收集上下文信息 │ - └───────────┬───────────┘ - │ - ┌───────────────┴───────────────┐ - │ │ - │ │ - ┌───────────▼──────────┐ ┌──────────▼──────────┐ - │ 路径 1: 缓冲区路径 │ │ 路径 2: 控制台路径 │ - │ │ │ │ - │ 检查 global_level │ │ 检查 console_level │ - │ (默认 Info) │ │ (默认 Warning) │ - └──────────┬───────────┘ └──────────┬──────────┘ - │ │ - │ (level >= global_level) │ (level >= console_level) - │ │ - ┌──────────▼───────────┐ ┌─────────▼───────────┐ - │ 写入环形缓冲区 │ │ 格式化并打印 │ - │ GlobalLogBuffer │ │ 带 ANSI 颜色 │ - │ 使用原子操作 │ │ 立即输出 │ - └──────────────────────┘ └─────────────────────┘ - │ │ - │ │ - ▼ ▼ - ┌──────────────────────┐ ┌─────────────────────┐ - │ 后续可通过 │ │ 开发者实时看到 │ - │ read_log() 读取 │ │ 关键信息 │ - └──────────────────────┘ └─────────────────────┘ -``` - -### 双路输出的优势 - -1. **日志完整性**:所有达到全局级别的日志都被缓存,确保不会丢失重要信息 -2. **实时监控**:关键级别的日志(如 Error、Warning)立即显示,便于快速发现问题 -3. **灵活配置**:两个级别过滤器独立配置,适应不同场景 -4. **性能平衡**:缓冲区写入是无锁的快速路径,控制台输出只处理重要日志,避免性能瓶颈 - -### 级别过滤矩阵 - -不同级别配置下日志的处理方式: - -| 日志级别 | global_level=Info, console_level=Warning | global_level=Debug, console_level=Info | global_level=Error, console_level=Error | -|---------|------------------------------------------|----------------------------------------|------------------------------------------| -| Debug | 不缓存,不显示 | 缓存,不显示 | 不缓存,不显示 | -| Info | 缓存,不显示 | 缓存,显示 | 不缓存,不显示 | -| Warning | 缓存,显示 | 缓存,显示 | 不缓存,不显示 | -| Error | 缓存,显示 | 缓存,显示 | 缓存,显示 | - -## 同步机制 - -Log 子系统采用无锁设计,通过原子操作和票号系统实现多核并发安全。 - -### 票号系统(Ticket System) - -环形缓冲区使用票号系统协调多个写入者: - -``` -写入流程: - -时刻 T0:初始状态 -┌────────────────────────────────────┐ -│ write_seq: 0 │ 写序列号 -│ read_seq: 0 │ 读序列号 -│ slots: [empty, empty, empty, ...] │ 环形槽位 -└────────────────────────────────────┘ - -时刻 T1:CPU0 和 CPU1 同时写入 -┌────────────────────────────────────┐ -│ CPU0: seq = fetch_add(1) → 0 │ 获得票号 0 -│ CPU1: seq = fetch_add(1) → 1 │ 获得票号 1 -│ write_seq: 2 │ 序列号已推进 -└────────────────────────────────────┘ - -时刻 T2:CPU0 和 CPU1 各自写入对应槽位 -┌────────────────────────────────────┐ -│ CPU0: 写 slot[0 % MAX_ENTRIES] │ 无需等待 -│ CPU1: 写 slot[1 % MAX_ENTRIES] │ 无需等待 -│ 两者并行,互不干扰 │ -└────────────────────────────────────┘ - -时刻 T3:发布数据(Release 语义) -┌────────────────────────────────────┐ -│ CPU0: slot[0].seq.store(0, Release) │ 发布票号 0 -│ CPU1: slot[1].seq.store(1, Release) │ 发布票号 1 -│ 读取者使用 Acquire 可见这些数据 │ -└────────────────────────────────────┘ -``` - -### 写入五步流程 - -每次写入日志遵循固定的五步流程(实现位于 `os/src/log/buffer.rs:128-177`): - -1. **获取票号**:使用 `write_seq.fetch_add(1, Relaxed)` 原子地获取序列号,这是线程私有的票号,保证每个写入者有唯一的槽位 -2. **计算槽位**:通过 `seq % MAX_ENTRIES` 计算目标槽位的索引 -3. **检测溢出**:比较 `write_seq` 和 `read_seq` 的距离,如果超过缓冲区容量,使用 CAS 循环推进 `read_seq` 并增加 `dropped` 计数 -4. **拷贝数据**:将日志内容拷贝到槽位,除了 `seq` 字段外的所有字段 -5. **发布数据**:使用 `Release` 语义写入 `seq` 字段,标志该槽位已就绪,读取者可以安全读取 - -### 读取同步 - -读取日志时,读取者检查槽位的 `seq` 字段(实现位于 `os/src/log/buffer.rs:179-201`): - -1. **加载当前读序列号**:`read_seq.load(Relaxed)` -2. **计算槽位索引**:`read_seq % MAX_ENTRIES` -3. **检查槽位就绪**:使用 `Acquire` 语义加载槽位的 `seq` 字段,如果 `seq == read_seq`,表示数据已发布 -4. **拷贝数据**:从槽位拷贝日志条目到栈上的临时变量 -5. **推进读序列号**:`read_seq.fetch_add(1, Relaxed)` - -### 内存序(Memory Ordering) - -Log 子系统严格遵循内存序规则保证并发安全: - -| 操作 | 内存序 | 原因 | -|------|--------|------| -| `write_seq.fetch_add(1)` | Relaxed | 仅需原子性,不需要同步其他内存 | -| `read_seq.load()` | Relaxed | 仅读取序列号,数据同步由 `seq` 字段保证 | -| `read_seq.fetch_add(1)` | Relaxed | 单消费者,无竞争 | -| `read_seq.store()` (溢出) | Relaxed | CAS 循环已保证同步 | -| `dropped.fetch_add(1)` | Relaxed | 仅需原子递增,不需要同步 | -| `slot.seq.store()` (发布) | Release | 发布数据,保证之前的写入对后续读取可见 | -| `slot.seq.load()` (检查) | Acquire | 获取数据,保证能看到之前的所有写入 | - -**关键点**:Release-Acquire 配对保证写入者发布的数据对读取者可见,这是无锁环形缓冲区正确性的核心。 - -## 初始化流程 - -Log 子系统采用编译期初始化,无需显式的运行时初始化步骤: - -``` -编译期: -┌─────────────────────────────────────────┐ -│ 1. 定义全局单例 GLOBAL_LOG │ -│ pub static GLOBAL_LOG: LogCore = │ -│ LogCore::new(); │ -│ │ -│ 2. LogCore::new() 是 const fn │ -│ 编译器在编译期完成初始化 │ -│ │ -│ 3. 所有字段都是零开销的 │ -│ · buffer: GlobalLogBuffer::new() │ -│ (所有原子变量初始化为 0) │ -│ · global_level: AtomicU8::new(6) │ -│ (Info 级别,编译期常量) │ -│ · console_level: AtomicU8::new(4) │ -│ (Warning 级别,编译期常量) │ -└─────────────────────────────────────────┘ - │ - ▼ -运行时启动: -┌─────────────────────────────────────────┐ -│ 1. 内核启动,执行 rust_main() │ -│ (os/src/main.rs) │ -│ │ -│ 2. GLOBAL_LOG 已经可用 │ -│ 无需任何初始化调用 │ -│ │ -│ 3. 直接使用日志宏 │ -│ pr_info!("Kernel started"); │ -│ │ -│ 4. 日志系统在启动早期即可工作 │ -│ 甚至可以在 MMU 初始化前使用 │ -└─────────────────────────────────────────┘ -``` - -### 零运行时开销的实现 - -Log 子系统通过以下设计实现零运行时开销: - -1. **const fn 初始化**:`LogCore::new()`、`GlobalLogBuffer::new()` 等都是 `const fn`,编译器在编译期计算所有初始值 -2. **静态分配**:环形缓冲区的槽位数组 `[MaybeUninit; MAX_ENTRIES]` 是静态分配的,不使用堆内存 -3. **原子变量零初始化**:`AtomicUsize::new(0)` 在编译期展开为简单的零值,无运行时开销 -4. **无依赖初始化**:Log 子系统不依赖其他子系统的初始化,可以在内核启动的最早期使用 - -### 为什么可以在启动早期使用? - -Log 子系统的设计使其可以在几乎任何阶段使用: - -- **不依赖堆分配**:完全使用静态内存,不需要 `global_allocator` 初始化 -- **不依赖 MMU**:可以在页表初始化前使用(尽管控制台输出可能需要基本的 MMIO 映射) -- **不依赖中断**:无锁设计不需要禁用中断,可以在中断处理程序中安全使用 -- **不依赖多核同步**:原子操作由硬件直接支持,不需要软件锁 - -## 设计决策 - -### 为什么采用 MPSC 模型? - -**决策**:环形缓冲区采用多生产者单消费者(MPSC)模型,而不是 MPMC(多生产者多消费者)。 - -**理由**: - -1. **日志的自然特性**:日志系统通常有多个写入者(多个 CPU 核心、多个内核模块),但只有一个或少数几个读取者(日志守护进程、调试工具) -2. **简化同步**:单消费者模型避免了读取端的竞争,读序列号 `read_seq` 可以使用 Relaxed 语义而不需要 CAS 操作,降低了复杂度 -3. **性能优化**:写入是热路径,MPSC 模型将同步开销集中在写入端,而读取是冷路径,可以接受略高的开销 -4. **避免活锁**:多消费者可能导致活锁或优先级反转,单消费者模型更简单可靠 - -**权衡**:如果确实需要多个读取者,可以在用户态实现多个读取线程,让一个主线程从内核读取日志后分发给其他消费者。 - -### 为什么使用双过滤器? - -**决策**:使用独立的 `global_level` 和 `console_level` 两个过滤器,而不是单一过滤器。 - -**理由**: - -1. **不同的关注点**:缓冲区记录所有有价值的日志供事后分析,控制台只显示关键信息避免刷屏 -2. **灵活性**:开发阶段可以降低 `console_level` 查看详细信息,生产环境提高 `console_level` 减少输出 -3. **性能考量**:控制台输出较慢(串口通信),通过独立过滤减少不必要的输出,避免阻塞日志记录 -4. **Linux 内核惯例**:Linux `printk` 也有类似设计,`console_loglevel` 和 `default_message_loglevel` 分别控制控制台和缓冲区 - -**权衡**:两个过滤器增加了配置复杂度,但实际使用中这种灵活性是值得的。 - -### 为什么固定 256 字节消息长度? - -**决策**:日志消息使用固定 256 字节的缓冲区(`MAX_MESSAGE_LEN`),超过则截断。 - -**理由**: - -1. **避免动态分配**:变长消息需要堆分配,在裸机环境中不可靠且有性能开销 -2. **可预测性**:固定大小使得日志条目的内存布局确定,缓冲区容量可以静态计算 -3. **足够的空间**:256 字节对于大多数日志消息足够,可以包含上下文信息和几个参数 -4. **对齐友好**:256 字节是 2 的幂,配合其他字段后日志条目大小仍然对齐良好 - -**权衡**:非常长的日志会被截断,但这种情况相对罕见。如果确实需要记录大量数据,应考虑使用专门的跟踪机制而不是日志系统。 - -**实现细节**:截断时会尊重 UTF-8 字符边界,避免产生无效字符序列(实现位于 `os/src/log/entry.rs:96-114`)。 - -### 为什么在宏展开时进行早期过滤? - -**决策**:日志宏(如 `pr_info!`)在展开时调用 `is_level_enabled()` 检查级别,而不是在 `log_impl()` 内部检查。 - -**理由**: - -1. **避免格式化开销**:如果级别被禁用,格式化参数(`format_args!` 的求值)会被完全跳过,零开销 -2. **减少函数调用**:被禁用的日志不会产生任何函数调用,减少指令缓存压力 -3. **编译器优化**:如果级别在编译期已知禁用,整个日志语句可能被优化掉 - -**实现**: - -宏展开后的伪代码(`os/src/log/macros.rs:62-67`): - -```rust -// pr_info!("value: {}", x) 展开为: -if is_level_enabled(LogLevel::Info) { - log_impl(LogLevel::Info, format_args!("value: {}", x)); -} -``` - -**权衡**:每次日志调用都有一次级别检查的开销,但这个开销远小于格式化开销,并且原子加载操作非常快。 - -### 为什么使用票号系统而不是传统锁? - -**决策**:环形缓冲区使用原子操作的票号系统(`fetch_add`)分配槽位,而不是使用互斥锁保护写入。 - -**理由**: - -1. **无锁性能**:原子操作通常只需几个 CPU 周期,而锁的获取和释放涉及多次原子操作和可能的上下文切换 -2. **避免优先级反转**:在中断处理程序中记录日志时,锁可能导致优先级反转或死锁 -3. **公平性**:票号系统天然保证公平性,先到达的写入者先获得槽位 -4. **可扩展性**:无锁设计在多核环境下扩展性更好,不会因为锁竞争限制并行度 - -**权衡**:无锁算法的正确性验证更困难,需要仔细处理内存序和边界条件。但一旦正确实现,性能和可靠性都优于锁方案。 - -### 为什么实现 syslog 系统调用? - -**决策**:提供与 Linux 兼容的 `syslog(2)` 系统调用,而不是自定义的日志读取接口。 - -**理由**: - -1. **兼容性**:现有的 Unix 工具(如 `dmesg`、`syslogd`)可以直接工作,无需修改 -2. **标准化**:遵循 POSIX 和 Linux 的惯例,降低学习成本 -3. **完整性**:支持破坏性/非破坏性读取、级别控制、缓冲区查询等完整功能 -4. **用户空间可见**:允许用户空间程序访问内核日志,支持日志工具开发 - -**实现要点**: - -- 支持 11 种操作类型(OPEN/CLOSE/READ/READ_ALL/READ_CLEAR/CLEAR/CONSOLE_OFF/ON/LEVEL/SIZE_UNREAD/SIZE_BUFFER) -- 兼容 Linux 的级别映射(1-8 vs 0-7) -- 精确的字节计数(`SIZE_UNREAD` 返回格式化后的实际字节数) -- 非破坏性读取(`READ_ALL` 使用 `peek_log` 实现) -- 权限检查框架(待用户管理系统完善) - -**权衡**:需要维护额外的系统调用接口,但换来的兼容性和功能完整性是值得的。 - -### 为什么需要非破坏性读取? - -**决策**:添加 `peek_log()` 和相关 API 支持非破坏性读取,不移动读指针。 - -**理由**: - -1. **syslog 兼容性**:Linux `SYSLOG_ACTION_READ_ALL` 需要非破坏性读取 -2. **多次查看**:允许用户多次查看相同的日志,不会因为读取而丢失 -3. **监控场景**:日志监控工具可以周期性扫描日志而不影响其他读取者 -4. **调试友好**:调试时可以反复查看相同的日志条目 - -**实现**: - -- `peek_log(index)` 按索引读取,不移动读指针 -- `log_reader_index()` 和 `log_writer_index()` 获取可读范围 -- 并发安全:与 write 和 read 完全并发 -- 环形缓冲区逻辑:正确处理索引越界和覆盖情况 - -**权衡**:增加了 API 复杂度,但提供了更大的灵活性。 - -### 为什么需要精确字节计数? - -**决策**:实时维护未读日志的格式化字节数(`unread_bytes`),而不是运行时计算。 - -**理由**: - -1. **性能优化**:`SIZE_UNREAD` 系统调用需要立即返回,不能遍历所有日志计算 -2. **缓冲区分配**:用户空间可以精确分配缓冲区大小,避免浪费或不足 -3. **实时性**:原子计数器可以 O(1) 时间返回结果 -4. **Linux 兼容**:Linux `SYSLOG_ACTION_SIZE_UNREAD` 也返回精确字节数 +`pr_*` 是分级日志入口, `print!`/`println!` 是原始控制台文本入口。两者都会写入缓冲, 但控制台格式不同。 -**实现**: +## 目标 -- 写入时增加字节数:`unread_bytes.fetch_add(formatted_len)` -- 读取时减少字节数:`unread_bytes.fetch_sub(formatted_len)` -- `calculate_formatted_length()` 精确计算格式化长度,与实际输出保持一致 -- 三处同步:字节计数计算、控制台输出格式、syslog 格式化 +- 常规路径: 可过滤, 可缓冲, 可通过 syslog 读取。 +- 早期/紧急路径: 不依赖堆, 不依赖复杂初始化, 尽量直接输出。 +- 并发路径: 多 CPU 可同时写日志, 读端按序消费。 -**权衡**:需要额外的原子计数器和精确的长度计算,但避免了运行时遍历的开销。 +## 非目标 -**重要维护点**: +- 不提供复杂 sink 插件系统。 +- 不在日志核心里执行阻塞 I/O。 +- 不让用户态直接访问 `LogEntry` 内存布局。 -如果修改日志输出格式,必须同步更新三处: -1. `buffer::calculate_formatted_length` - 字节长度计算 -2. `log_core::direct_print_entry` - 控制台输出格式 -3. `log_core::format_log_entry` - syslog 字符串格式化 +## 双级别过滤 -## 性能考量 +LogCore 有两个阈值: -### 关键优化 +- global level: 决定是否写入环形缓冲。 +- console level: 决定是否即时打印到控制台。 -1. **早期过滤**:宏展开时检查级别,避免格式化被禁用的日志,这是最重要的性能优化 -2. **无锁写入**:多核并发写入无需等待锁,写入延迟取决于原子操作的硬件性能(通常几纳秒) -3. **缓存行对齐**:`WriterData` 和 `ReaderData` 使用 `CachePadded64` 包装,避免伪共享(`os/src/log/buffer.rs:40-52`) -4. **Relaxed 语义**:大部分原子操作使用 Relaxed 语义,只在必要时使用 Release/Acquire,减少内存屏障开销 -5. **固定大小分配**:所有数据结构编译期确定大小,无动态分配的开销和碎片化 +级别数值越小优先级越高, 因此判断逻辑是 `level <= threshold`。默认值由 `config.rs` 给出, 当前全局级别和控制台级别都为 Info。 -### 性能瓶颈 +## 输出路径 -1. **控制台输出**:串口通信速度慢(通常 115200 bps),大量控制台输出会显著拖慢系统 - - **建议**:提高 `console_level`,只输出关键日志 -2. **缓冲区溢出**:频繁溢出时,CAS 循环推进 `read_seq` 可能产生竞争 - - **建议**:增大 `BUFFER_SIZE` 或提高日志读取频率 -3. **格式化开销**:复杂的格式化字符串(如大量参数、嵌套格式化)会增加写入延迟 - - **建议**:日志消息简洁明了,避免在热路径记录过于详细的日志 +### pr_* 日志 -### 预期性能 +`pr_*` 宏先做早期过滤。通过后, LogCore 生成带级别, CPU, task id, timestamp 和消息的 `LogEntry`, 写入缓冲, 再按 console level 选择是否用带前缀格式输出。 -在典型的 RISC-V 平台(如 QEMU 模拟的 virt 机器)上: +### print/println -- **写入单条日志**(未被过滤,不输出到控制台):约 100-200 纳秒 -- **早期过滤的日志**(被过滤,完全跳过):约 5-10 纳秒(一次原子加载的开销) -- **控制台输出**:取决于串口速度,通常几毫秒 -- **读取单条日志**:约 50-100 纳秒 +`print!` 和 `println!` 调用 `print_impl()`。它们保持控制台原文输出, 但同时以 Info 级别写入日志缓冲, 防止普通启动信息绕过 syslog。 -**注意**:实际性能取决于硬件平台、编译器优化级别和系统负载。 +### emergency 输出 -## 安全性分析 +panic 和部分 trap 路径使用 `console::emergency_print()`。它绕开常规日志核心和控制台锁, 适合系统处于不稳定状态时尽快输出诊断。 -### 安全机制 +## syslog 路径 -1. **内存安全**:使用 Rust 的类型系统保证内存安全,槽位使用 `MaybeUninit` 包装,避免未初始化读取 -2. **并发安全**:原子操作和内存序保证多核并发的正确性,无数据竞争 -3. **溢出处理**:缓冲区满时自动覆盖最旧的日志,保证系统不会因日志缓冲区满而挂起 -4. **消息截断**:过长的消息自动截断,避免缓冲区溢出 -5. **固定资源**:所有资源在编译期确定,无动态分配,避免资源耗尽攻击 +`syslog` syscall 把缓冲中的 `LogEntry` 格式化为用户可读字符串并复制到用户缓冲。支持破坏性读取, 非破坏性读取, 读取并清空, 清空, 控制台级别调整和大小查询。 -### 已知限制 +## 并发和生命周期约束 -1. **单消费者**:只能有一个读取者,多个读取者会导致数据竞争和未定义行为 - - **影响**:用户态工具需要协调,避免多个进程同时读取内核日志缓冲区 -2. **有限容量**:缓冲区容量有限(默认 16 KB),高速日志记录可能导致旧日志被覆盖 - - **影响**:突发的大量日志可能丢失早期信息,需要及时读取或增大缓冲区 -3. **消息截断**:超过 256 字节的消息会被截断,可能丢失部分信息 - - **影响**:非常长的日志需要分多条记录或使用其他机制 -4. **时间戳精度**:时间戳依赖 `arch::timer::get_time()`,精度取决于架构实现 - - **影响**:时间戳可能不适合高精度性能分析,应使用专门的性能跟踪工具 -5. **无持久化**:日志只存在内存中,系统崩溃或重启后丢失 - - **影响**:严重错误导致的崩溃可能无法记录崩溃前的日志,未来可考虑持久化机制 +- 全局 LogCore 静态初始化, 生命周期覆盖整个内核运行期。 +- 缓冲区槽位用 seq 字段作为发布标记, 写入数据后再发布 seq。 +- 读端通过 seq 判断槽位是否可读, 读后推进 read sequence。 +- 溢出覆盖是设计行为, 不阻塞生产者。 +- 控制台即时输出尽量用单次格式化写入, 减少多 CPU 输出交错。 -## 扩展可能性 +## 已知限制 -未来可能的扩展方向: +- 当前是单消费者读取模型。 +- 字节计数依赖格式化长度估算, 修改输出格式必须同步更新相关代码。 +- emergency 输出不保证进入日志缓冲。 -1. **多级缓冲区**:增加慢速持久化缓冲区(如磁盘、SPI Flash),定期从内存缓冲区刷新 -2. **日志压缩**:对重复日志进行压缩,记录重复次数而不是完整消息 -3. **结构化日志**:支持结构化字段(如 JSON 格式),便于机器解析和分析 -4. **动态级别**:支持按模块或按文件设置不同的日志级别,更细粒度的控制 -5. **网络日志**:通过网络发送日志到远程服务器,支持分布式系统的集中日志管理 -6. **跟踪集成**:与性能跟踪工具(如 tracing、perf)集成,提供统一的观测性基础设施 +## 源码索引 -这些扩展在不破坏现有 API 的前提下都是可行的,得益于分层架构的良好封装。 +- `os/src/log/log_core.rs`: `LogCore`, 双过滤器, 格式化输出。 +- `os/src/log/mod.rs`: 全局门面。 +- `os/src/log/macros.rs`: `pr_*` 宏。 +- `os/src/log/buffer.rs`: 环形缓冲。 +- `os/src/kernel/syscall/sys.rs`: `syslog()`。 +- `os/src/console.rs`: `Stdout`, `emergency_print()`。 diff --git a/document/log/buffer_and_entry.md b/document/log/buffer_and_entry.md index bb5e523d..a74ad21c 100644 --- a/document/log/buffer_and_entry.md +++ b/document/log/buffer_and_entry.md @@ -1,808 +1,75 @@ -# 环形缓冲区与日志条目 +# 缓冲区和日志条目 -## 概述 +日志缓冲区是固定大小的 MPSC 环形缓冲。它优先保证写入端不阻塞, 因此满时覆盖最旧未读日志。 -本文档详细介绍 Log 子系统的两个核心组件:无锁环形缓冲区(`GlobalLogBuffer`)和日志条目(`LogEntry`)。环形缓冲区负责高效地缓存日志,采用 MPSC(多生产者单消费者)模型支持多核并发写入;日志条目定义了日志数据的内存布局和序列化方式。 +## 当前状态 -## 环形缓冲区(GlobalLogBuffer) +- 全局缓冲大小由 `GLOBAL_LOG_BUFFER_SIZE` 决定, 当前为 16 KiB。 +- 单条消息最大长度由 `MAX_LOG_MESSAGE_LENGTH` 决定, 当前为 256 字节。 +- `LogEntry` 固定布局, 第一个字段是原子 `seq` 发布标记。 +- 写入端使用单调递增 sequence 分配槽位。 +- 读取端是单消费者模型。 +- 非破坏性 `peek` 用于 syslog read-all。 -### 结构概览 +## 目标 -`GlobalLogBuffer` 是一个固定大小的环形缓冲区,位于 `os/src/log/buffer.rs`。其核心设计是无锁 MPSC 模型,通过原子操作和票号系统实现多核并发安全。 +- 多 CPU 写日志时避免互斥锁竞争。 +- 不依赖堆分配。 +- 溢出时保留最新日志, 同时记录 dropped count。 +- 提供未读条目数和格式化字节数给 syslog 查询。 -### 内存布局 +## 非目标 -``` -GlobalLogBuffer 结构体布局: -┌─────────────────────────────────────────────────────────────────┐ -│ GlobalLogBuffer │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ writer: CachePadded64 [64 字节对齐] │ │ -│ │ ┌─────────────────────────────────────────────────┐ │ │ -│ │ │ write_seq: AtomicUsize (写序列号) │ │ │ -│ │ │ 当前值表示下一个可用的票号 │ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ │ [填充至 64 字节,避免伪共享] │ │ -│ └────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ reader: CachePadded64 [64 字节对齐] │ │ -│ │ ┌─────────────────────────────────────────────────┐ │ │ -│ │ │ read_seq: AtomicUsize (读序列号) │ │ │ -│ │ │ dropped: AtomicUsize (丢弃计数) │ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ │ [填充至 64 字节,避免伪共享] │ │ -│ └────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ slots: [MaybeUninit; MAX_ENTRIES] │ │ -│ │ │ │ -│ │ 环形槽位数组,每个槽位存储一个 LogEntry │ │ -│ │ MAX_ENTRIES = BUFFER_SIZE / size_of::() │ │ -│ │ ≈ 16384 / 280 ≈ 58 个槽位 │ │ -│ └────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ unread_bytes: AtomicUsize (未读字节计数) │ │ -│ │ │ │ -│ │ 记录所有未读日志格式化后的总字节数 │ │ -│ │ 用于 SIZE_UNREAD 系统调用 │ │ -│ └────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ +- 不支持多个独立消费者各自维护游标。 +- 不保证日志永不丢失。 +- 不保存超过固定消息长度的完整文本。 -内存对齐说明: -- WriterData 和 ReaderData 各占 64 字节(一个缓存行) -- 避免伪共享:多个 CPU 写 write_seq 不会与读 read_seq 竞争同一缓存行 -- slots 数组紧随其后,每个 LogEntry 约 280 字节 -``` +## 写入流程 -定义位于 `os/src/log/buffer.rs:54-59`。 +1. `write_seq.fetch_add()` 获取唯一序号。 +2. 用序号对容量取模定位槽位。 +3. 若写入会覆盖未读数据, 推进 read sequence 并增加 dropped count。 +4. 拷贝条目数据到槽位, 暂不写 seq。 +5. 用 Release store 发布 seq。 +6. 增加未读格式化字节数。 -### MPSC 并发模型 +## 读取流程 -环形缓冲区采用**多生产者单消费者**(MPSC)模型,这是日志系统的自然选择: +1. 读取当前 read sequence。 +2. 定位槽位并检查 seq 是否匹配。 +3. 匹配则 clone 条目。 +4. 减少未读字节数。 +5. 推进 read sequence。 -``` -并发模型示意图: +## LogEntry 设计 - 多个生产者(写入者) +条目保存: - ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ - │ CPU0 │ │ CPU1 │ │ CPU2 │ │ CPU3 │ - └───┬──┘ └───┬──┘ └───┬──┘ └───┬──┘ - │ │ │ │ - │ │ │ │ - │ pr_info!() │ pr_err!() │ pr_warn!() │ pr_debug!() - │ │ │ │ - └─────────────┴──────┬──────┴─────────────┘ - │ - │ 并发写入(使用原子操作协调) - │ - ┌────────────▼───────────┐ - │ GlobalLogBuffer │ - │ │ - │ write_seq (原子递增) │ - │ slots[...] │ - │ read_seq │ - └────────────┬───────────┘ - │ - │ 单一读取者(顺序读取) - │ - ┌──────▼──────┐ - │ Log Reader │ - │ (用户态工具) │ - └─────────────┘ +- level。 +- CPU id。 +- task id。 +- timestamp。 +- message length。 +- 固定大小 message buffer。 -关键特点: -1. 多个 CPU 可以并发调用 pr_* 宏,无需等待锁 -2. 写入者通过 write_seq.fetch_add() 获取独占的票号 -3. 每个票号对应唯一的槽位,写入者互不干扰 -4. 读取者单独访问 read_seq,无竞争 -5. Release-Acquire 内存序保证数据可见性 -``` +消息写入使用内部 `MessageWriter`, 超长消息截断。`message()` 只暴露有效长度范围。 -### 票号分配系统 +## 并发和生命周期约束 -票号系统是无锁环形缓冲区的核心机制,通过原子递增操作分配唯一的序列号: +- seq 是生产者和消费者之间的可见性边界。 +- 写入发布 seq 前, 读端不能把槽位视为有效。 +- 溢出推进 read sequence 可能让消费者看不到旧日志, 这是预期行为。 +- `unread_bytes` 是为 syslog size 查询服务的运行时计数, 修改格式化逻辑时必须同步更新计算函数。 -``` -票号分配流程: +## 已知限制 -初始状态: -┌────────────────────────────┐ -│ write_seq = 0 │ -│ read_seq = 0 │ -└────────────────────────────┘ +- `peek` 与并发覆盖同时发生时可能返回 None。 +- MPSC 设计只假设一个破坏性读取者。 +- ANSI 格式化长度纳入字节计数。 -时刻 T1:三个 CPU 同时请求写入 -┌────────────────────────────┐ -│ CPU0 执行 fetch_add(1) │ → 返回 0,write_seq 变为 1 -│ CPU1 执行 fetch_add(1) │ → 返回 1,write_seq 变为 2 -│ CPU2 执行 fetch_add(1) │ → 返回 2,write_seq 变为 3 -└────────────────────────────┘ +## 源码索引 -结果: -┌────────────────────────────┐ -│ CPU0 获得票号 0 │ → 写入 slot[0 % MAX_ENTRIES] -│ CPU1 获得票号 1 │ → 写入 slot[1 % MAX_ENTRIES] -│ CPU2 获得票号 2 │ → 写入 slot[2 % MAX_ENTRIES] -│ write_seq = 3 │ -└────────────────────────────┘ - -票号系统的保证: -1. 原子性:fetch_add 保证每个 CPU 获得唯一的票号 -2. 公平性:先到达的 CPU 获得较小的票号 -3. 顺序性:票号单调递增,读取者按顺序消费日志 -4. 无等待:获得票号后立即写入,无需等待其他 CPU -``` - -实现位于 `os/src/log/buffer.rs:128`。 - -### 写入流程详解 - -每次写入日志遵循严格的五步流程,保证并发安全和数据完整性: - -``` -五步写入流程: - -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 1:获取票号 │ -│ ───────────────────────────────────────────────────────── │ -│ seq = write_seq.fetch_add(1, Relaxed) │ -│ │ -│ · 原子地递增 write_seq 并返回旧值 │ -│ · Relaxed 语义足够,因为票号本身是线程私有的 │ -│ · 返回的 seq 是该日志的唯一标识 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 2:计算槽位 │ -│ ───────────────────────────────────────────────────────── │ -│ idx = seq % MAX_ENTRIES │ -│ │ -│ · 将线性序列号映射到环形槽位索引 │ -│ · MAX_ENTRIES 是编译期常量,取模可能优化为位与操作 │ -│ · 多个序列号可能映射到同一槽位(当缓冲区绕一圈后) │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 3:检测并处理溢出 │ -│ ───────────────────────────────────────────────────────── │ -│ current_write = write_seq.load(Relaxed) │ -│ current_read = read_seq.load(Relaxed) │ -│ │ -│ if (current_write - current_read) > MAX_ENTRIES: │ -│ // 缓冲区满,需要覆盖旧数据 │ -│ loop: │ -│ old_read = read_seq.load(Relaxed) │ -│ new_read = current_write - MAX_ENTRIES + 1 │ -│ if read_seq.compare_exchange(old_read, new_read): │ -│ dropped.fetch_add(new_read - old_read) │ -│ break │ -│ │ -│ · CAS 循环推进 read_seq,保证只有一个 CPU 成功 │ -│ · 增加 dropped 计数记录丢弃的日志数量 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 4:拷贝日志数据 │ -│ ───────────────────────────────────────────────────────── │ -│ slot = &slots[idx] │ -│ // 拷贝除 seq 字段外的所有数据: │ -│ slot.level = entry.level │ -│ slot.cpu_id = entry.cpu_id │ -│ slot.timestamp = entry.timestamp │ -│ slot.task_id = entry.task_id │ -│ slot.length = entry.length │ -│ slot.message.copy_from_slice(entry.message) │ -│ │ -│ · 注意:seq 字段暂不写入,它是同步标志 │ -│ · 此时数据尚未"发布",读取者看不到 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 5:发布数据 │ -│ ───────────────────────────────────────────────────────── │ -│ slot.seq.store(seq, Release) │ -│ │ -│ · 使用 Release 语义写入 seq 字段 │ -│ · Release 保证之前的所有写入对后续的 Acquire 读取可见 │ -│ · 写入 seq 是"发布"操作,标志槽位已就绪 │ -│ · 读取者通过检查 seq 字段判断数据是否可读 │ -└─────────────────────────────────────────────────────────────┘ -``` - -完整实现位于 `os/src/log/buffer.rs:128-177`。 - -### 溢出处理机制 - -当写入速度超过读取速度时,环形缓冲区会满。此时采用 FIFO 策略覆盖最旧的日志: - -``` -溢出处理状态转换: - -正常状态: -┌────────────────────────────────────────┐ -│ write_seq: 10 │ -│ read_seq: 5 │ -│ 容量:MAX_ENTRIES = 58 │ -│ 已用:10 - 5 = 5 条日志 │ -│ 可用:58 - 5 = 53 个槽位 │ -└────────────────────────────────────────┘ - -接近满状态: -┌────────────────────────────────────────┐ -│ write_seq: 62 │ -│ read_seq: 5 │ -│ 已用:62 - 5 = 57 条日志 │ -│ 可用:58 - 57 = 1 个槽位 │ -│ 警告:缓冲区即将满 │ -└────────────────────────────────────────┘ - -溢出检测: -┌────────────────────────────────────────┐ -│ write_seq: 64 │ -│ read_seq: 5 │ -│ 已用:64 - 5 = 59 > MAX_ENTRIES │ -│ 判定:缓冲区溢出! │ -└────────────────────────────────────────┘ - │ - ▼ -溢出处理(CAS 循环): -┌────────────────────────────────────────┐ -│ 计算需要推进的读序列号: │ -│ new_read = 64 - 58 + 1 = 7 │ -│ │ -│ 尝试 CAS: │ -│ read_seq.compare_exchange(5, 7) │ -│ ├─ 成功:read_seq = 7 │ -│ │ dropped += (7 - 5) = 2 │ -│ │ 返回,继续写入 │ -│ └─ 失败:其他 CPU 已推进 read_seq │ -│ 重新加载 read_seq,重试 │ -└────────────────────────────────────────┘ - │ - ▼ -恢复正常: -┌────────────────────────────────────────┐ -│ write_seq: 64 │ -│ read_seq: 7 │ -│ 已用:64 - 7 = 57 条日志 │ -│ 可用:58 - 57 = 1 个槽位 │ -│ dropped: 2(记录丢弃了 2 条日志) │ -└────────────────────────────────────────┘ - -关键点: -1. 只有写入者会推进 read_seq(在溢出时) -2. CAS 保证多个写入者中只有一个成功推进 -3. dropped 计数精确记录被覆盖的日志数量 -4. 被覆盖的日志是最旧的日志(FIFO 策略) -``` - -实现位于 `os/src/log/buffer.rs:148-162`。 - -### 读取同步机制 - -读取者通过检查槽位的 `seq` 字段判断数据是否就绪: - -``` -读取流程: - -┌─────────────────────────────────────────────────────────────┐ -│ 1. 加载当前读序列号 │ -│ current_read = read_seq.load(Relaxed) │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 2. 计算槽位索引 │ -│ idx = current_read % MAX_ENTRIES │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 3. 检查槽位就绪(Acquire 同步点) │ -│ slot_seq = slot.seq.load(Acquire) │ -│ │ -│ if slot_seq == current_read: │ -│ // 数据已发布,可以安全读取 │ -│ else: │ -│ // 数据未就绪或已被覆盖,返回 None │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 4. 拷贝日志条目 │ -│ // 从槽位拷贝到栈上的临时变量 │ -│ entry = clone_from_slot(slot) │ -│ │ -│ · Acquire 保证能看到写入者的所有数据 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 5. 推进读序列号 │ -│ read_seq.fetch_add(1, Relaxed) │ -│ │ -│ · 单消费者,无竞争,Relaxed 足够 │ -└─────────────────────────────────────────────────────────────┘ - -为什么检查 slot_seq == current_read? -┌────────────────────────────────────────┐ -│ 情况 1:slot_seq == current_read │ -│ → 数据已发布,匹配期望的序列号 │ -│ → 可以安全读取 │ -├────────────────────────────────────────┤ -│ 情况 2:slot_seq < current_read │ -│ → 数据尚未写入(写入者还未到达) │ -│ → 返回 None,等待写入者 │ -├────────────────────────────────────────┤ -│ 情况 3:slot_seq > current_read │ -│ → 数据已被覆盖(缓冲区绕了一圈) │ -│ → 返回 None,日志已丢失 │ -└────────────────────────────────────────┘ -``` - -实现位于 `os/src/log/buffer.rs:179-201`。 - -### 缓存行填充优化 - -`WriterData` 和 `ReaderData` 使用 `CachePadded64` 包装,避免伪共享: - -``` -伪共享问题(未优化): - -假设缓存行大小为 64 字节: -┌────────────────────────────────────────────────────────────┐ -│ 缓存行 │ -│ ┌──────────────────┐ ┌──────────────────┐ │ -│ │ write_seq (8B) │ │ read_seq (8B) │ [其他数据] │ -│ └──────────────────┘ └──────────────────┘ │ -└────────────────────────────────────────────────────────────┘ - ↑ ↑ - │ │ - CPU0 频繁写入 CPU1 频繁读取 - -问题: -- CPU0 修改 write_seq → 缓存行失效 → CPU1 的缓存行被强制刷新 -- CPU1 读取 read_seq → 导致 CPU0 的缓存行失效 -- 两个 CPU 互相干扰,性能下降(伪共享) - -优化后(CachePadded64): - -┌────────────────────────────────────────┐ -│ 缓存行 1 (64 字节) │ -│ ┌──────────────────┐ │ -│ │ write_seq (8B) │ [填充 56 字节] │ -│ └──────────────────┘ │ -└────────────────────────────────────────┘ - ↑ - │ - CPU0 独占此缓存行 - -┌────────────────────────────────────────┐ -│ 缓存行 2 (64 字节) │ -│ ┌──────────────────┐ │ -│ │ read_seq (8B) │ [填充] │ -│ │ dropped (8B) │ │ -│ └──────────────────┘ │ -└────────────────────────────────────────┘ - ↑ - │ - CPU1 独占此缓存行 - -优势: -- write_seq 和 read_seq 位于不同的缓存行 -- CPU0 写入 write_seq 不会影响 CPU1 的缓存 -- CPU1 读取 read_seq 不会影响 CPU0 的缓存 -- 消除伪共享,提升并发性能 -``` - -定义位于 `os/src/log/buffer.rs:40-52`。 - -### 非破坏性读取 - -除了传统的破坏性读取(`read()`),环形缓冲区还支持非破坏性读取,允许多次查看相同的日志而不删除它们。 - -#### peek 操作 - -``` -非破坏性读取流程: - -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 1:获取可读范围 │ -│ ───────────────────────────────────────────────────────── │ -│ start = read_seq.load(Acquire) │ -│ end = write_seq.load(Acquire) │ -│ │ -│ · 获取当前的读写指针位置 │ -│ · 可读范围为 [start, end) │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 2:验证索引 │ -│ ───────────────────────────────────────────────────────── │ -│ if (index < start || index >= end): │ -│ return None // 索引越界 │ -│ │ -│ if (end >= start + MAX_ENTRIES): │ -│ oldest_valid = end - MAX_ENTRIES │ -│ if (index < oldest_valid): │ -│ return None // 数据已被覆盖 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 3:读取槽位 │ -│ ───────────────────────────────────────────────────────── │ -│ slot_idx = index % MAX_ENTRIES │ -│ slot = &slots[slot_idx] │ -│ │ -│ · 计算环形槽位索引 │ -│ · 获取槽位指针 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 4:验证序列号 │ -│ ───────────────────────────────────────────────────────── │ -│ seq = slot.seq.load(Acquire) │ -│ if (seq != index): │ -│ return None // 数据尚未就绪或已被覆盖 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 步骤 5:克隆并返回 │ -│ ───────────────────────────────────────────────────────── │ -│ entry = (*slot).clone() │ -│ return Some(entry) │ -│ │ -│ · 注意:不修改 read_seq,日志保留在缓冲区中 │ -└─────────────────────────────────────────────────────────────┘ -``` - -**并发安全性**: - -- `peek()` 可以与 `write()` 和 `read()` 并发调用 -- 使用 Acquire 内存序保证能看到已发布的数据 -- 不修改任何共享状态,完全无锁 - -**使用场景**: - -- `SyslogAction::ReadAll` - 非破坏性读取所有日志 -- 日志监控工具 - 周期性扫描而不删除日志 -- 调试分析 - 反复查看相同的日志条目 - -**实现位置**:`os/src/log/buffer.rs:330-381` - -### 精确字节计数 - -环形缓冲区实时维护未读日志的格式化字节数,用于支持 `SyslogAction::SizeUnread` 系统调用。 - -#### 字节计数机制 - -``` -字节计数维护流程: - -写入时: -┌─────────────────────────────────────────────────────────────┐ -│ calculate_formatted_length(entry) │ -│ ↓ │ -│ · 计算日志条目格式化后的精确字节长度 │ -│ · 包括 ANSI 颜色代码、时间戳、上下文信息、消息内容 │ -│ · 必须与 format_log_entry() 和 direct_print_entry() 一致 │ -│ ↓ │ -│ unread_bytes.fetch_add(formatted_len, Release) │ -│ ↓ │ -│ · 原子地增加未读字节计数 │ -└─────────────────────────────────────────────────────────────┘ - -读取时: -┌─────────────────────────────────────────────────────────────┐ -│ entry = read_log() │ -│ ↓ │ -│ calculate_formatted_length(&entry) │ -│ ↓ │ -│ unread_bytes.fetch_sub(formatted_len, Release) │ -│ ↓ │ -│ · 原子地减少未读字节计数 │ -└─────────────────────────────────────────────────────────────┘ - -查询时: -┌─────────────────────────────────────────────────────────────┐ -│ unread_bytes.load(Acquire) │ -│ ↓ │ -│ · O(1) 时间返回结果,无需遍历缓冲区 │ -│ · 用于 syslog SIZE_UNREAD 系统调用 │ -└─────────────────────────────────────────────────────────────┘ -``` - -#### 格式化长度计算 - -`calculate_formatted_length()` 函数精确计算日志条目格式化后的字节长度: - -``` -格式: "{color_code}{level} [{timestamp:12}] [CPU{cpu_id}/T{task_id:3}] {message}{reset}\n" - -组成部分: -- ANSI 颜色代码: entry.level().color_code().len() // 开始 -- 级别标签: entry.level().as_str().len() // "[INFO]" 等 -- 时间戳: 14 字节 // " [{:12}]" = 2 + 12 -- CPU ID: 5 + digit_count(cpu_id) // " [CPU" -- 任务 ID: 2 + digit_count_padded(task_id, 3) + 1 // "/T]" -- 消息内容: entry.message().len() -- ANSI 重置代码: entry.level().reset_color_code().len() // 结束 -- 分隔符和换行: 3 + 1 字节 // 3个空格 + 1个换行 - -总字节数 = 所有部分之和 -``` - -**重要维护点**: - -如果修改日志输出格式,必须同步更新三处: -1. `buffer::calculate_formatted_length()` - 字节长度计算(`os/src/log/buffer.rs:18-102`) -2. `log_core::direct_print_entry()` - 控制台输出格式(`os/src/log/log_core.rs:223-240`) -3. `log_core::format_log_entry()` - syslog 字符串格式化(`os/src/log/log_core.rs:277-286`) - -**性能特性**: - -- 查询字节数:O(1) - 单次原子加载 -- 写入时计算:O(1) - 固定计算,无循环 -- 内存开销:一个 `AtomicUsize`(8 字节) - -**实现位置**: -- 字节计数:`os/src/log/buffer.rs:18-102`, `os/src/log/buffer.rs:236-238`, `os/src/log/buffer.rs:302-305` -- 查询接口:`os/src/log/buffer.rs:323-326` - -## 日志条目(LogEntry) - -### 内存布局 - -`LogEntry` 定义了单条日志的内存布局,位于 `os/src/log/entry.rs:20-30`: - -``` -LogEntry 内存布局(总大小约 280 字节): - -偏移量 字段 类型 大小 对齐 说明 -────────────────────────────────────────────────────────────────── -0x0000 seq AtomicUsize 8 字节 8 同步标志,必须在首位 -0x0008 level LogLevel 1 字节 1 日志级别 (0-7) -0x0009 [padding] - 3 字节 - 对齐填充 -0x000C cpu_id usize 8 字节 8 记录日志的 CPU 核心 ID -0x0014 length usize 8 字节 8 消息实际长度 -0x001C task_id u32 4 字节 4 记录日志的任务 ID -0x0020 timestamp usize 8 字节 8 时间戳(架构相关单位) -0x0028 message [u8; 256] 256字节 1 消息缓冲区 - -总计:约 280 字节(实际取决于编译器对齐) - -关键设计: -┌────────────────────────────────────────────────────────────────┐ -│ seq 字段必须在首位的原因: │ -│ ─────────────────────────────────────────────────────────────│ -│ 1. 作为同步标志,读取者首先检查 seq 判断数据是否就绪 │ -│ 2. Acquire-Release 语义: │ -│ · 写入者最后以 Release 写入 seq │ -│ · 读取者首先以 Acquire 读取 seq │ -│ · 保证 seq 之前的所有字段对读取者可见 │ -│ 3. 固定偏移量 0,便于汇编优化和理解 │ -└────────────────────────────────────────────────────────────────┘ - -内存表示(#[repr(C, align(8))]): -┌────────────────────────────────────────────────────────────────┐ -│ +0x00 ┌───────────────────────────────────────────────────┐ │ -│ │ seq: AtomicUsize │ │ -│ +0x08 ├───┬───────────────────────────────────────────────┤ │ -│ │lv │ [padding 3 bytes] │ │ -│ +0x0C ├───┴───────────────────────────────────────────────┤ │ -│ │ cpu_id: usize │ │ -│ +0x14 ├───────────────────────────────────────────────────┤ │ -│ │ length: usize │ │ -│ +0x1C ├───────────────────────────────────────────────────┤ │ -│ │ task_id: u32 │ │ -│ +0x20 ├───────────────────────────────────────────────────┤ │ -│ │ timestamp: usize │ │ -│ +0x28 ├───────────────────────────────────────────────────┤ │ -│ │ message: [u8; 256] │ │ -│ │ 固定大小的消息缓冲区 │ │ -│ │ UTF-8 编码,超长消息会被截断 │ │ -│ +0x128 └───────────────────────────────────────────────────┘ │ -└────────────────────────────────────────────────────────────────┘ -``` - -### 消息截断策略 - -消息缓冲区固定为 256 字节(`MAX_MESSAGE_LEN`),超过长度的消息会被自动截断: - -**截断规则**: - -1. **尊重 UTF-8 边界**:截断时检查 UTF-8 字符边界,避免产生无效字符序列 - - 如果第 256 字节是 UTF-8 多字节字符的中间位置,向前查找完整字符的起始位置 - - 保证截断后的字符串是有效的 UTF-8 - -2. **记录实际长度**:`length` 字段记录消息的实际字节数(截断前的长度) - - 读取者可以通过比较 `length` 和 `MAX_MESSAGE_LEN` 判断是否被截断 - -3. **静默截断**:截断不会报错或警告,保证日志记录的可靠性 - - 设计哲学:宁可记录部分信息,也不能因为消息过长而丢失整条日志 - -**UTF-8 截断示例**: - -假设消息是 "Hello 世界!Extra text"(UTF-8 编码): - -``` -字节序列: -H e l l o 世 界 ! E x t r a ... -48 65 6c 6c 6f 20 e4b896 e7958c efbc81 45 78 74 72 61 ... - -假设 MAX_MESSAGE_LEN = 20: -- 朴素截断:截取前 20 字节 → 可能在"界"字的中间(e7958c 被截断) -- 智能截断:检测到 0xe7 是三字节字符的开始,向前推到"世"字的结束位置 -- 最终截断:"Hello 世界!E" (完整的 UTF-8 字符) -``` - -实现位于 `os/src/log/entry.rs:96-114`。 - -### 显示格式 - -日志条目实现了 `core::fmt::Display` trait,格式化输出为可读的字符串: - -**标准格式**: - -``` -[时间戳] [级别] [CPU核心/任务ID] 消息内容 -``` - -**示例输出**: - -``` -[000012345678] [INFO ] [CPU0/Task1] Kernel initialized successfully -[000012350123] [ERROR] [CPU2/Task5] Failed to allocate memory -[000012351000] [WARN ] [CPU1/Task3] High memory usage: 95% -[000012352456] [DEBUG] [CPU3/Task0] Entering function foo() -``` - -**字段说明**: - -| 字段 | 格式 | 说明 | -|------|------|------| -| 时间戳 | `[%012d]` | 12 位十进制数,左填充零,单位取决于架构(通常是 CPU 周期或纳秒) | -| 级别 | `[%-5s]` | 5 字符宽,左对齐,使用 ANSI 颜色(如果控制台支持) | -| CPU核心 | `CPU%d` | CPU 核心 ID | -| 任务ID | `Task%d` | 任务 ID(如果未设置则为 0) | -| 消息 | UTF-8 字符串 | 实际的日志消息,可能被截断 | - -**颜色映射**: - -| 级别 | 颜色 | ANSI 代码 | -|------|------|-----------| -| Emergency | 亮红 | `\x1b[91m` | -| Alert | 亮红 | `\x1b[91m` | -| Critical | 亮红 | `\x1b[91m` | -| Error | 红色 | `\x1b[31m` | -| Warning | 黄色 | `\x1b[33m` | -| Notice | 青色 | `\x1b[36m` | -| Info | 绿色 | `\x1b[32m` | -| Debug | 默认 | 无颜色 | - -实现位于 `os/src/log/entry.rs:116-137`。 - -## 性能特性 - -### 环形缓冲区性能 - -| 操作 | 时间复杂度 | 说明 | -|------|-----------|------| -| 写入 | O(1) | fetch_add + 槽位写入,常数时间 | -| 读取 | O(1) | 槽位读取 + fetch_add,常数时间 | -| 溢出处理 | O(k) | k 为 CAS 循环次数,通常 1-3 次 | -| 级别检查 | O(1) | 原子加载,几纳秒 | - -### 日志条目性能 - -| 操作 | 时间复杂度 | 说明 | -|------|-----------|------| -| 创建条目 | O(n) | n 为消息长度,需要格式化和拷贝 | -| 拷贝条目 | O(1) | 固定大小(280 字节),memcpy 优化 | -| 格式化显示 | O(n) | n 为消息长度,需要格式化输出 | - -### 内存使用 - -**环形缓冲区总内存**: - -``` -总大小 = WriterData (64B) + ReaderData (64B) + slots数组 - -slots数组大小 = MAX_ENTRIES × sizeof(LogEntry) - ≈ 58 × 280 字节 - ≈ 16240 字节 - -总大小 ≈ 64 + 64 + 16240 ≈ 16368 字节 ≈ 16 KB -``` - -符合配置文件中定义的 `BUFFER_SIZE = 16384` 字节(`os/src/log/config.rs:9`)。 - -## 设计权衡 - -### 固定 vs 动态大小 - -**当前设计**:固定大小的消息缓冲区(256 字节) - -**优势**: -- 日志条目大小确定,环形缓冲区容量可静态计算 -- 无需堆分配,适合裸机环境 -- 槽位对齐良好,访问效率高 - -**劣势**: -- 长消息会被截断 -- 短消息浪费空间(平均日志可能只有几十字节) - -**权衡分析**:对于内核日志系统,可预测性和可靠性优先于灵活性。固定大小设计更符合实时系统的需求。 - -### MPSC vs MPMC - -**当前设计**:MPSC(多生产者单消费者) - -**优势**: -- 简化读取端同步,read_seq 无竞争 -- 避免多消费者的活锁和优先级反转 -- 性能更好(读取是冷路径,可接受单线程) - -**劣势**: -- 只能有一个读取者 -- 多个工具需要读取日志时需要额外协调 - -**权衡分析**:日志的自然特性是多写少读,MPSC 是最佳选择。如需多消费者,可在用户态实现分发。 - -### Release-Acquire vs SeqCst - -**当前设计**:Release-Acquire 语义用于 `seq` 字段同步 - -**优势**: -- 比 SeqCst 更弱的内存序,性能更好(特别是在 ARM/RISC-V 上) -- 足以保证无锁环形缓冲区的正确性 - -**劣势**: -- 需要更仔细的推理和验证 -- 错误使用可能导致难以调试的并发 bug - -**权衡分析**:经过仔细验证,Release-Acquire 是正确的选择。SeqCst 会带来不必要的性能开销。 - -## 未来改进方向 - -### 动态缓冲区大小 - -当前缓冲区大小在编译期固定。未来可考虑: -- 启动参数配置缓冲区大小 -- 运行时动态扩展(需要复杂的迁移逻辑) - -### 持久化支持 - -当前日志只存在内存中。未来可增加: -- 慢速持久化层(SPI Flash、磁盘) -- 后台线程定期刷新内存日志到持久化存储 -- 崩溃恢复时读取持久化日志 - -### 压缩和去重 - -对于重复的日志,可以压缩: -- 记录重复次数而不是完整消息 -- 使用哈希值检测重复 -- 节省缓冲区空间 - -### 多缓冲区分区 - -针对不同模块使用不同的缓冲区: -- 减少竞争,提高并发性能 -- 支持按模块过滤和分析 -- 隔离故障(一个模块的日志洪水不影响其他模块) - -这些改进在不破坏当前 API 的前提下都是可行的,得益于良好的模块化设计。 +- `os/src/log/buffer.rs`: `GlobalLogBuffer`, sequence, overflow, read/peek。 +- `os/src/log/entry.rs`: `LogEntry`, message writer, publish/readiness。 +- `os/src/log/config.rs`: 缓冲和消息长度常量。 +- `os/src/log/log_core.rs`: 格式化输出和字节计数一致性要求。 diff --git a/document/log/level.md b/document/log/level.md index 2b7d33d7..c40b56c1 100644 --- a/document/log/level.md +++ b/document/log/level.md @@ -1,702 +1,65 @@ -# 日志级别与宏接口 +# 日志级别 -## 概述 +日志级别兼容 Linux printk 的 0 到 7 优先级模型。数值越小表示越严重, 阈值比较使用 `level <= threshold`。 -Log 子系统采用 8 级日志分类系统,模仿 Linux 内核的 `printk` 级别设计。每个级别对应不同的严重程度,从最高优先级的 Emergency(系统不可用)到最低优先级的 Debug(调试信息)。本文档详细介绍日志级别的语义、宏接口的使用、颜色映射以及双过滤器的工作原理。 +## 当前状态 -## 日志级别枚举 +| 值 | 级别 | 宏 | 用途 | +| --- | --- | --- | --- | +| 0 | Emergency | `pr_emerg!` | 系统不可用 | +| 1 | Alert | `pr_alert!` | 必须立即处理 | +| 2 | Critical | `pr_crit!` | 关键错误 | +| 3 | Error | `pr_err!` | 普通错误 | +| 4 | Warning | `pr_warn!` | 可恢复风险 | +| 5 | Notice | `pr_notice!` | 正常但重要 | +| 6 | Info | `pr_info!` | 常规信息 | +| 7 | Debug | `pr_debug!` | 调试信息 | -`LogLevel` 是一个 8 级枚举,定义在 `os/src/log/level.rs:7-18`: +## 目标 -| 级别值 | 级别名称 | 宏接口 | 语义 | -|-------|---------|--------|------| -| 0 | Emergency | `pr_emerg!()` | 系统不可用,需要立即采取行动 | -| 1 | Alert | `pr_alert!()` | 必须立即采取行动的严重情况 | -| 2 | Critical | `pr_crit!()` | 临界错误,系统功能受到严重影响 | -| 3 | Error | `pr_err!()` | 错误条件,功能无法正常工作 | -| 4 | Warning | `pr_warn!()` | 警告条件,可能导致问题 | -| 5 | Notice | `pr_notice!()` | 正常但重要的信息 | -| 6 | Info | `pr_info!()` | 信息性消息 | -| 7 | Debug | `pr_debug!()` | 调试级别的详细信息 | +- 给内核日志提供统一严重度语言。 +- 让宏层能在格式化前做早期过滤。 +- 让缓冲和控制台输出可以使用不同阈值。 -**级别排序**:数值越小,优先级越高。Emergency(0)是最高级别,Debug(7)是最低级别。 +## 非目标 -**枚举表示**:使用 `#[repr(u8)]` 保证枚举值与底层整数对应,便于原子存储和比较。 +- 不规定每个子系统必须用某个具体级别。 +- 不把日志级别当成错误处理或审计等级。 +- 不在文档维护宏展开实现清单。 -## 各级别详细说明 +## 过滤设计 -### Emergency(紧急) +- global level 控制是否进入环形缓冲。 +- console level 控制是否即时输出到控制台。 +- `pr_*` 宏先调用 `is_level_enabled()`, 被过滤时不求值 `format_args!` 后续路径。 +- `print!`/`println!` 不按 `pr_*` 级别过滤, 它们按原始输出语义打印, 同时写入 Info 缓冲记录。 -**数值**:0 -**宏**:`pr_emerg!()` -**语义**:系统完全不可用,即将崩溃或已经崩溃 +## 颜色和格式 -**使用场景**: -- 内核 panic 前的最后一条消息 -- 严重的硬件故障(如内存控制器失败) -- 无法恢复的系统状态(如栈溢出) +当前 `LogLevel` 为控制台和 syslog 格式提供级别标签和 ANSI 颜色码。格式大致包含: -**示例情况**: -- "Kernel panic: unable to continue" -- "Hardware failure: memory controller not responding" -- "Critical resource exhausted: cannot allocate kernel stack" +- 级别标签。 +- timestamp。 +- CPU id。 +- task id。 +- message。 -### Alert(警报) +颜色码会出现在 `format_log_entry()` 的输出中。消费 syslog 的用户态工具如果不希望显示颜色, 需要自行剥离 ANSI escape。 -**数值**:1 -**宏**:`pr_alert!()` -**语义**:必须立即采取行动的严重情况 +## 使用约束 -**使用场景**: -- 文件系统损坏 -- 关键设备故障 -- 资源即将耗尽(但还有机会恢复) +- 热路径调试信息使用 `pr_debug!`, 依赖默认过滤降低开销。 +- 可恢复但需要注意的情况使用 `pr_warn!`, 不要滥用 `pr_err!`。 +- panic 或 trap 中无法信任普通路径时使用 emergency 输出, 而不是提高日志级别。 -**示例情况**: -- "Filesystem corruption detected, immediate repair required" -- "Critical device failure: disk controller error" -- "System temperature critical, shutting down soon" +## 已知限制 -### Critical(严重) +- 默认 console level 当前为 Info, 和部分旧文档描述的 Warning 不一致。 +- `LogLevel::from_u8()` 对未知值回落到默认日志级别。 -**数值**:2 -**宏**:`pr_crit!()` -**语义**:临界错误,系统功能受到严重影响,但系统可能还能运行 +## 源码索引 -**使用场景**: -- 主要功能失败(但系统未完全崩溃) -- 安全相关的严重问题 -- 重要资源分配失败 - -**示例情况**: -- "Unable to initialize network subsystem" -- "Security violation: unauthorized memory access attempt" -- "Failed to allocate memory for critical kernel structure" - -### Error(错误) - -**数值**:3 -**宏**:`pr_err!()` -**语义**:错误条件,某个功能无法正常工作 - -**使用场景**: -- 系统调用失败 -- 设备驱动错误 -- 无法完成用户请求 - -**示例情况**: -- "Failed to open file: permission denied" -- "Device driver error: invalid ioctl command" -- "Unable to create process: resource limit exceeded" - -### Warning(警告) - -**数值**:4 -**宏**:`pr_warn!()` -**语义**:警告条件,当前没有错误但可能导致未来问题 - -**使用场景**: -- 资源使用率高 -- 检测到异常但可恢复的情况 -- 配置问题(非致命) - -**示例情况**: -- "Memory usage high: 95% of physical memory in use" -- "Retrying operation after transient failure" -- "Deprecated API called, please update code" - -### Notice(通知) - -**数值**:5 -**宏**:`pr_notice!()` -**语义**:正常但重要的信息,值得注意但不是错误 - -**使用场景**: -- 系统状态变化 -- 重要操作完成 -- 配置变更 - -**示例情况**: -- "Network interface eth0 link up" -- "User root logged in" -- "System entering suspend mode" - -### Info(信息) - -**数值**:6 -**宏**:`pr_info!()` -**语义**:信息性消息,记录系统的正常操作 - -**使用场景**: -- 子系统初始化 -- 常规操作日志 -- 统计信息 - -**示例情况**: -- "Filesystem mounted: /dev/sda1 on /" -- "Process 1234 started: /bin/bash" -- "Cache statistics: 1000 hits, 50 misses" - -### Debug(调试) - -**数值**:7 -**宏**:`pr_debug!()` -**语义**:调试级别的详细信息,仅供开发和问题诊断使用 - -**使用场景**: -- 函数进入/退出跟踪 -- 中间变量值 -- 详细的状态转换 - -**示例情况**: -- "Entering function: allocate_frame()" -- "Page table entry: PTE[123] = 0x80001001" -- "State transition: RUNNING -> BLOCKED" - -## 与 Linux 内核的对比 - -Comix Log 子系统的级别设计直接借鉴 Linux 内核的 `printk` 级别: - -| Comix 级别 | Linux 级别 | Linux 宏 | 数值 | 说明 | -|-----------|-----------|----------|------|------| -| Emergency | KERN_EMERG | `pr_emerg()` | 0 | 完全一致 | -| Alert | KERN_ALERT | `pr_alert()` | 1 | 完全一致 | -| Critical | KERN_CRIT | `pr_crit()` | 2 | 完全一致 | -| Error | KERN_ERR | `pr_err()` | 3 | 完全一致 | -| Warning | KERN_WARNING | `pr_warn()` | 4 | 完全一致 | -| Notice | KERN_NOTICE | `pr_notice()` | 5 | 完全一致 | -| Info | KERN_INFO | `pr_info()` | 6 | 完全一致 | -| Debug | KERN_DEBUG | `pr_debug()` | 7 | 完全一致 | - -**一致性优势**: -- 熟悉 Linux 内核开发的人可以无缝迁移 -- 宏名称和语义保持一致,降低学习成本 -- 遵循成熟的最佳实践,避免重复设计 - -**差异**: -- Comix 使用 Rust 的 `format_args!` 宏处理格式化,而 Linux 使用 C 的可变参数 -- Comix 实现了早期过滤优化,禁用级别的日志完全零开销 -- Comix 的双过滤器设计更灵活(独立的 global_level 和 console_level) - -## 颜色映射 - -控制台输出根据日志级别使用不同的 ANSI 颜色,提高可读性。颜色映射定义在 `os/src/log/level.rs:44-58`。 - -| 级别 | 颜色 | ANSI 转义码 | 效果 | -|------|------|------------|------| -| Emergency | 亮红色(Bright Red) | `\x1b[91m` | 高优先级,极其显眼 | -| Alert | 亮红色(Bright Red) | `\x1b[91m` | 高优先级,极其显眼 | -| Critical | 亮红色(Bright Red) | `\x1b[91m` | 高优先级,极其显眼 | -| Error | 红色(Red) | `\x1b[31m` | 错误信息,醒目 | -| Warning | 黄色(Yellow) | `\x1b[33m` | 警告信息,引起注意 | -| Notice | 青色(Cyan) | `\x1b[36m` | 重要信息,区别于普通 | -| Info | 绿色(Green) | `\x1b[32m` | 正常信息,表示成功 | -| Debug | 默认颜色 | 无 | 调试信息,不突出显示 | - -**颜色分组**: -- **亮红色(Emergency/Alert/Critical)**:最高三级使用相同的亮红色,表示极其严重的情况 -- **红色(Error)**:普通错误,醒目但不如亮红 -- **黄色(Warning)**:警告,引起注意但不表示错误 -- **青色(Notice)**:重要的正常信息,有别于普通信息 -- **绿色(Info)**:正常操作,绿色通常表示"正常"或"成功" -- **默认色(Debug)**:调试信息不特别突出,避免干扰 - -**控制台兼容性**: -- ANSI 颜色码在大多数现代终端和串口工具中支持(如 minicom、screen、PuTTY) -- 不支持颜色的终端会忽略转义码,显示为普通文本 -- 可以通过环境变量或配置禁用颜色(未来可扩展) - -**颜色效果示例**: - -``` -[000012345678] [EMERG] [CPU0/Task1] Kernel panic! ← 亮红色 -[000012345679] [ALERT] [CPU0/Task1] Disk failure! ← 亮红色 -[000012345680] [CRIT ] [CPU1/Task2] Out of memory! ← 亮红色 -[000012345681] [ERROR] [CPU2/Task3] File not found ← 红色 -[000012345682] [WARN ] [CPU0/Task1] High temperature ← 黄色 -[000012345683] [NOTIC] [CPU1/Task2] Network connected ← 青色 -[000012345684] [INFO ] [CPU2/Task3] Process started ← 绿色 -[000012345685] [DEBUG] [CPU3/Task4] Function entry ← 默认色 -``` - -## 宏接口说明 - -Log 子系统提供 8 个宏,对应 8 个日志级别。所有宏定义在 `os/src/log/macros.rs`。 - -### 宏列表 - -| 宏名称 | 级别 | 定义位置 | -|--------|------|---------| -| `pr_emerg!()` | Emergency | `os/src/log/macros.rs:60-67` | -| `pr_alert!()` | Alert | `os/src/log/macros.rs:79-86` | -| `pr_crit!()` | Critical | `os/src/log/macros.rs:98-105` | -| `pr_err!()` | Error | `os/src/log/macros.rs:118-127` | -| `pr_warn!()` | Warning | `os/src/log/macros.rs:139-148` | -| `pr_notice!()` | Notice | `os/src/log/macros.rs:158-167` | -| `pr_info!()` | Info | `os/src/log/macros.rs:178-187` | -| `pr_debug!()` | Debug | `os/src/log/macros.rs:199-208` | - -### 宏的功能 - -所有宏接口具有相同的行为模式: - -1. **早期级别检查**:展开时调用 `is_level_enabled()` 判断级别是否启用 -2. **条件格式化**:只有级别启用时才格式化参数 -3. **调用核心函数**:调用 `log_impl()` 传递级别和格式化的参数 -4. **零开销抽象**:被禁用的日志完全不产生运行时开销 - -### 宏的基本形式 - -所有宏支持类似 `format!` 的语法: - -- **无参数**:`pr_info!("message")` -- **带参数**:`pr_info!("value: {}", x)` -- **多参数**:`pr_info!("x={}, y={}", x, y)` -- **格式化选项**:`pr_info!("hex: {:#x}", value)` - -### 各宏的使用建议 - -#### pr_emerg!() - Emergency - -**何时使用**: -- 系统即将崩溃,这是最后的消息 -- 严重的硬件故障使系统无法继续 -- panic 前记录原因 - -**使用频率**:极少(理想情况下从不使用) - -**注意事项**: -- Emergency 日志应该简洁明了,说明问题的本质 -- 这可能是系统记录的最后一条日志 - -#### pr_alert!() - Alert - -**何时使用**: -- 检测到需要立即人工干预的情况 -- 系统还能运行但功能严重受损 -- 关键资源即将耗尽 - -**使用频率**:很少 - -**注意事项**: -- Alert 应触发管理员通知(如果有监控系统) -- 记录足够的上下文帮助快速定位问题 - -#### pr_crit!() - Critical - -**何时使用**: -- 主要功能失败但系统未完全崩溃 -- 安全相关的严重问题 -- 重要资源初始化失败 - -**使用频率**:少 - -**注意事项**: -- Critical 表示系统处于不稳定状态 -- 应考虑降级服务或限制功能 - -#### pr_err!() - Error - -**何时使用**: -- 系统调用失败 -- 用户请求无法完成 -- 设备或驱动错误 - -**使用频率**:中等 - -**注意事项**: -- Error 应包含错误码或原因 -- 帮助用户理解为什么操作失败 -- 常见的错误级别,但不应滥用 - -#### pr_warn!() - Warning - -**何时使用**: -- 检测到可能导致问题的情况 -- 使用了不推荐的功能 -- 资源使用率高 - -**使用频率**:中等 - -**注意事项**: -- Warning 不应泛滥,避免"狼来了"效应 -- 应指出潜在的问题和解决方案 - -#### pr_notice!() - Notice - -**何时使用**: -- 系统状态变化(网络连接、设备插拔) -- 重要操作完成 -- 安全相关事件(登录、权限变更) - -**使用频率**:中等 - -**注意事项**: -- Notice 和 Info 的界限有时模糊 -- 如果事件值得管理员关注,使用 Notice - -#### pr_info!() - Info - -**何时使用**: -- 子系统初始化 -- 常规操作日志 -- 统计信息和进度报告 - -**使用频率**:高 - -**注意事项**: -- Info 是最常用的级别 -- 生产环境通常默认启用 Info 及以上级别 -- 应保持日志简洁,避免过于冗长 - -#### pr_debug!() - Debug - -**何时使用**: -- 开发和调试时跟踪代码执行 -- 记录中间变量和状态 -- 详细的函数调用跟踪 - -**使用频率**:非常高(开发时),极低(生产时) - -**注意事项**: -- Debug 日志在生产环境通常被禁用 -- 可以自由使用,不担心性能(感谢早期过滤) -- 应使用描述性的日志,帮助理解代码流程 - -## 级别过滤配置 - -Log 子系统使用双过滤器设计:`global_level` 和 `console_level`。 - -### 双过滤器架构 - -``` -日志写入流程中的双重过滤: - -用户调用 pr_info!("message") - │ - │ - ▼ -┌─────────────────────────┐ -│ 早期过滤(宏展开时) │ -│ is_level_enabled()? │ ← 检查 global_level -│ (避免格式化被禁用的日志) │ -└──────────┬──────────────┘ - │ (通过) - ▼ -┌─────────────────────────┐ -│ 创建 LogEntry │ -│ 格式化消息 │ -└──────────┬──────────────┘ - │ - ├───────────────────┬───────────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ - │ 过滤器 1 │ │ 过滤器 2 │ │ (其他处理) │ - │ global_level │ │console_level │ │ │ - └──────┬───────┘ └──────┬───────┘ └──────────────┘ - │ │ - │ (Info >= global) │ (Info >= console) - │ │ - ▼ ▼ - ┌──────────────┐ ┌──────────────┐ - │ 写入缓冲区 │ │ 打印到控制台 │ - └──────────────┘ └──────────────┘ -``` - -### global_level(全局级别) - -**作用**:控制哪些日志被缓存到环形缓冲区 - -**默认值**:`Info`(级别 6) - -**影响**: -- 低于 global_level 的日志会被完全忽略(宏展开时就跳过) -- 达到或超过 global_level 的日志会被缓存 - -**配置函数**: -- `set_global_level(level: LogLevel)` - 设置全局级别(位于 `os/src/log/mod.rs:86`) -- `get_global_level() -> LogLevel` - 获取当前全局级别(位于 `os/src/log/mod.rs:91`) - -**使用场景**: -- **开发阶段**:设置为 `Debug`,捕获所有日志 -- **测试阶段**:设置为 `Info`,记录正常操作 -- **生产环境**:设置为 `Warning` 或 `Error`,只记录问题 - -### console_level(控制台级别) - -**作用**:控制哪些日志立即打印到控制台 - -**默认值**:`Warning`(级别 4) - -**影响**: -- 低于 console_level 的日志不会打印到控制台(但仍可能被缓存) -- 达到或超过 console_level 的日志会立即打印 - -**配置函数**: -- `set_console_level(level: LogLevel)` - 设置控制台级别(位于 `os/src/log/mod.rs:96`) -- `get_console_level() -> LogLevel` - 获取当前控制台级别(位于 `os/src/log/mod.rs:101`) - -**使用场景**: -- **开发阶段**:设置为 `Info` 或 `Debug`,实时查看所有日志 -- **演示阶段**:设置为 `Notice`,显示重要操作 -- **生产环境**:设置为 `Error`,只显示错误信息 - -### 级别过滤矩阵 - -不同配置下各级别日志的处理方式: - -| 日志级别 | global=Debug, console=Debug | global=Info, console=Warning | global=Error, console=Error | -|---------|----------------------------|------------------------------|------------------------------| -| Emergency (0) | 缓存 + 显示 | 缓存 + 显示 | 缓存 + 显示 | -| Alert (1) | 缓存 + 显示 | 缓存 + 显示 | 缓存 + 显示 | -| Critical (2) | 缓存 + 显示 | 缓存 + 显示 | 缓存 + 显示 | -| Error (3) | 缓存 + 显示 | 缓存 + 显示 | 缓存 + 显示 | -| Warning (4) | 缓存 + 显示 | 缓存 + 显示 | 不缓存,不显示 | -| Notice (5) | 缓存 + 显示 | 缓存,不显示 | 不缓存,不显示 | -| Info (6) | 缓存 + 显示 | 缓存,不显示 | 不缓存,不显示 | -| Debug (7) | 缓存 + 显示 | 不缓存,不显示 | 不缓存,不显示 | - -**关键观察**: -- global_level 是"第一道防线",决定日志是否被记录 -- console_level 是"第二道防线",决定日志是否显示 -- console_level 必须 >= global_level 才有意义(否则缓存的日志不会被显示) - -### 推荐配置 - -#### 开发调试配置 - -```rust -set_global_level(LogLevel::Debug); // 缓存所有日志 -set_console_level(LogLevel::Info); // 显示 Info 及以上级别 -``` - -**效果**:Debug 日志被缓存但不显示,减少控制台刷屏,需要时可以读取缓冲区查看。 - -#### 正常运行配置 - -```rust -set_global_level(LogLevel::Info); // 缓存常规日志 -set_console_level(LogLevel::Warning); // 只显示警告和错误 -``` - -**效果**:平衡日志完整性和控制台清洁度,这是默认配置。 - -#### 生产环境配置 - -```rust -set_global_level(LogLevel::Warning); // 只缓存问题 -set_console_level(LogLevel::Error); // 只显示错误 -``` - -**效果**:最小化日志开销,只记录和显示真正的问题。 - -#### 调试特定问题配置 - -```rust -set_global_level(LogLevel::Debug); // 缓存所有日志 -set_console_level(LogLevel::Debug); // 显示所有日志 -``` - -**效果**:最大程度的可见性,用于诊断难以重现的问题。注意:可能产生大量输出。 - -## 双过滤器工作原理 - -双过滤器在不同阶段发挥作用,优化性能和灵活性: - -``` -时间线上的过滤阶段: - -┌─────────────────────────────────────────────────────────────────┐ -│ 阶段 1:编译/宏展开时 - 早期过滤 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ pr_info!("value: {}", expensive_calculation()) │ -│ ↓ │ -│ if is_level_enabled(LogLevel::Info) { ← 检查 global_level │ -│ log_impl(LogLevel::Info, format_args!("value: {}", ...)) │ -│ } │ -│ │ -│ 如果 Info < global_level: │ -│ · expensive_calculation() 不会被调用 │ -│ · format_args! 不会被求值 │ -│ · 整个 if 块被跳过,零开销 │ -└─────────────────────────────────────────────────────────────────┘ - │ - │ (通过 global_level 过滤) - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ 阶段 2:运行时 - LogCore::log() 内部 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ 创建 LogEntry,收集上下文信息(CPU ID、时间戳等) │ -│ 格式化消息到 entry.message │ -│ │ -│ 分支 1:写入缓冲区(无需再检查 global_level,已通过) │ -│ buffer.write(entry) → 总是执行 │ -│ │ -│ 分支 2:控制台输出(需要检查 console_level) │ -│ if entry.level <= self.console_level.load(Acquire) { │ -│ println!("{}", entry); // 带颜色的格式化输出 │ -│ } │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 为什么需要两个过滤器? - -**设计理由**: - -1. **不同的关注点**: - - global_level:哪些日志值得保留?(完整性) - - console_level:哪些日志需要立即看到?(实时性) - -2. **性能考量**: - - 控制台输出慢(串口通信),减少输出量避免阻塞 - - 缓冲区写入快(无锁内存操作),可以记录更多日志 - -3. **灵活性**: - - 开发时:console_level=Info,实时查看常规操作 - - 生产时:console_level=Error,只显示严重问题 - - global_level 保持不变,保证日志完整性 - -4. **避免刷屏**: - - Debug 日志缓存但不显示,需要时读取缓冲区 - - 控制台保持清洁,不被大量 Debug 信息淹没 - -### 单过滤器 vs 双过滤器 - -| 特性 | 单过滤器设计 | 双过滤器设计(当前) | -|------|-------------|---------------------| -| 配置复杂度 | 简单,只有一个级别 | 略复杂,两个级别 | -| 灵活性 | 低,缓存和显示必须同步 | 高,独立控制 | -| 控制台清洁度 | 差,要么全显示要么全不显示 | 好,可以只显示重要日志 | -| 性能 | 中等 | 优,减少控制台输出 | -| Linux 兼容性 | 低 | 高,类似 console_loglevel | - -**结论**:双过滤器的额外复杂度是值得的,提供了更好的灵活性和性能。 - -## 最佳实践 - -### 选择合适的级别 - -**决策树**: - -``` -是否导致系统崩溃或即将崩溃? -├─ 是 → Emergency -└─ 否 → 是否需要立即人工干预? - ├─ 是 → Alert - └─ 否 → 是否严重影响系统功能? - ├─ 是 → Critical - └─ 否 → 是否导致操作失败? - ├─ 是 → Error - └─ 否 → 是否可能导致未来问题? - ├─ 是 → Warning - └─ 否 → 是否值得管理员关注? - ├─ 是 → Notice - └─ 否 → 是否常规操作信息? - ├─ 是 → Info - └─ 否 → Debug -``` - -### 避免常见错误 - -#### 错误 1:过度使用高级别 - -**不好的做法**: -- 将所有错误都标记为 Critical -- 将所有警告都标记为 Error - -**问题**: -- 级别失去意义,无法区分严重程度 -- 产生"狼来了"效应,真正严重的问题被淹没 - -**正确做法**: -- 严格按照语义使用级别 -- Critical 只用于真正严重影响系统的情况 - -#### 错误 2:日志过于冗长 - -**不好的做法**: -- 在日志中包含大量上下文信息 -- 日志消息超过 256 字节被截断 - -**问题**: -- 浪费缓冲区空间 -- 重要信息可能被截断 -- 控制台输出缓慢 - -**正确做法**: -- 日志简洁明了,通常一行足够 -- 复杂信息分多条日志记录 -- 使用结构化的格式(如 key=value) - -#### 错误 3:在热路径使用 Debug 日志 - -**不好的做法**: -- 在循环中记录 Debug 日志 -- 在中断处理程序中大量使用日志 - -**问题**: -- 即使 Debug 被禁用,早期过滤也有微小开销 -- 大量日志调用影响性能 - -**正确做法**: -- 热路径使用条件编译(`#[cfg(debug_assertions)]`) -- 或者使用专门的性能跟踪工具而不是日志 - -### 日志的可读性 - -**好的日志示例**: - -- `pr_info!("Filesystem mounted: {} on {}", device, mountpoint)` -- `pr_err!("Failed to allocate memory: size={} bytes, error={}", size, err)` -- `pr_warn!("Memory usage high: {}% ({}MB / {}MB)", percent, used, total)` - -**特点**: -- 包含关键信息(设备名、大小、错误码) -- 简洁明了,一眼能看懂 -- 使用结构化格式,便于解析 - -**不好的日志示例**: - -- `pr_info!("Operation completed")` ← 太模糊 -- `pr_err!("Error occurred")` ← 没有上下文 -- `pr_debug!("Value: {:?}", huge_structure)` ← 可能非常长 - -## 未来扩展 - -### 按模块过滤 - -当前只有全局级别,未来可支持按模块设置不同级别: - -``` -log::mm::set_level(LogLevel::Debug); // MM 子系统使用 Debug -log::fs::set_level(LogLevel::Info); // FS 子系统使用 Info -``` - -### 动态级别调整 - -支持运行时通过 debugfs 或系统调用动态调整级别,无需重启系统。 - -### 结构化日志 - -支持结构化字段(如 JSON),便于机器解析: - -``` -pr_info_struct!( - "event" => "process_start", - "pid" => pid, - "name" => name -); -``` - -### 日志分类标签 - -支持给日志添加标签(如模块名、子系统),便于过滤和分析: - -``` -pr_info!(tag="mm", "Allocated {} frames", count); -``` - -这些扩展在不破坏现有 API 的前提下都是可行的。 +- `os/src/log/level.rs`: 级别枚举, 标签, 颜色。 +- `os/src/log/config.rs`: 默认 global/console level。 +- `os/src/log/macros.rs`: 宏入口和早期过滤。 +- `os/src/log/log_core.rs`: 阈值检查和控制台输出。 diff --git a/document/log/usage.md b/document/log/usage.md index 29097570..89da5ba9 100644 --- a/document/log/usage.md +++ b/document/log/usage.md @@ -1,1219 +1,65 @@ -# Log 子系统使用指南 +# Log 使用方法 -## 概述 +本文只保留使用边界和约束。具体函数签名和宏定义以 rustdoc 和源码为准。 -本文档提供 Log 子系统的实用指南,包括基本使用、配置方法、格式化技巧、性能最佳实践、多核并发场景、常见陷阱和调试技巧。通过丰富的代码示例,帮助开发者快速掌握日志系统的使用。 +## 常规写日志 -## 基本使用 - -### 引入宏 - -在需要使用日志的模块中引入对应的宏: - -```rust -use log::{pr_info, pr_err, pr_warn, pr_debug}; -``` - -或者引入所有宏: - -```rust -use log::*; -``` - -### 记录简单日志 - -最基本的用法是记录字符串消息: - -```rust -pr_info!("System initialization started"); -pr_warn!("Low memory warning"); -pr_err!("Failed to initialize device"); -``` - -### 格式化日志 - -使用 Rust 的格式化语法,类似于 `println!` 和 `format!`: - -```rust -let pid = 42; -let name = "init"; -pr_info!("Process started: pid={}, name={}", pid, name); - -let count = 100; -pr_debug!("Allocated {} frames", count); - -let addr = 0x80001000usize; -pr_info!("Page table at {:#x}", addr); // 十六进制格式 -``` - -### 不同级别的使用示例 - -```rust -// Emergency: 系统即将崩溃 -pr_emerg!("Kernel panic: unable to handle page fault"); - -// Alert: 需要立即采取行动 -pr_alert!("Filesystem corruption detected"); - -// Critical: 严重错误 -pr_crit!("Failed to initialize memory subsystem"); - -// Error: 普通错误 -pr_err!("Cannot open file: {}", filename); - -// Warning: 警告 -pr_warn!("Memory usage: {}%", usage_percent); - -// Notice: 重要信息 -pr_notice!("Network interface {} is up", interface); - -// Info: 常规信息 -pr_info!("Loading module: {}", module_name); - -// Debug: 调试信息 -pr_debug!("Entering function: allocate_frame()"); -``` - -## 配置级别过滤器 - -### 查询当前级别 - -```rust -use log::{get_global_level, get_console_level, LogLevel}; - -let global = get_global_level(); -let console = get_console_level(); - -pr_info!("Current levels: global={:?}, console={:?}", global, console); -``` - -### 设置全局级别 - -控制哪些日志被缓存: - -```rust -use log::{set_global_level, LogLevel}; - -// 缓存所有日志(包括 Debug) -set_global_level(LogLevel::Debug); - -// 只缓存 Info 及以上级别(默认) -set_global_level(LogLevel::Info); - -// 只缓存警告和错误 -set_global_level(LogLevel::Warning); - -// 只缓存错误 -set_global_level(LogLevel::Error); -``` - -### 设置控制台级别 - -控制哪些日志立即打印: - -```rust -use log::{set_console_level, LogLevel}; - -// 显示所有日志(包括 Debug) -set_console_level(LogLevel::Debug); - -// 显示 Info 及以上级别 -set_console_level(LogLevel::Info); - -// 只显示警告和错误(默认) -set_console_level(LogLevel::Warning); - -// 只显示错误 -set_console_level(LogLevel::Error); -``` - -### 典型配置场景 - -#### 开发调试配置 - -```rust -// 缓存所有日志,但只显示 Info 及以上 -// 这样 Debug 日志被保留,需要时可以读取缓冲区查看 -set_global_level(LogLevel::Debug); -set_console_level(LogLevel::Info); - -pr_debug!("This will be buffered but not shown"); -pr_info!("This will be buffered and shown"); -``` - -#### 正常运行配置 - -```rust -// 默认配置:缓存常规信息,只显示警告和错误 -set_global_level(LogLevel::Info); -set_console_level(LogLevel::Warning); - -pr_info!("Normal operation"); // 缓存,不显示 -pr_warn!("Warning condition"); // 缓存,显示 -``` - -#### 生产环境配置 - -```rust -// 最小化开销:只记录和显示问题 -set_global_level(LogLevel::Warning); -set_console_level(LogLevel::Error); - -pr_info!("This will be ignored"); // 完全跳过 -pr_warn!("Warning"); // 缓存,不显示 -pr_err!("Error"); // 缓存,显示 -``` - -#### 临时启用详细日志 - -```rust -// 调试特定问题时,临时启用所有日志 -let old_global = get_global_level(); -let old_console = get_console_level(); - -set_global_level(LogLevel::Debug); -set_console_level(LogLevel::Debug); - -// ... 执行需要调试的代码 ... - -// 恢复原来的配置 -set_global_level(old_global); -set_console_level(old_console); -``` - -## 读取日志缓冲区 - -### 读取单条日志 - -```rust -use log::read_log; - -if let Some(entry) = read_log() { - // 使用 Display trait 格式化输出 - println!("{}", entry); - - // 或者访问字段 - println!("Level: {:?}", entry.level()); - println!("CPU: {}", entry.cpu_id()); - println!("Timestamp: {}", entry.timestamp()); - println!("Message: {}", entry.message()); -} -``` - -### 读取所有日志 - -```rust -use log::read_log; - -// 顺序读取所有日志(FIFO 顺序) -while let Some(entry) = read_log() { - println!("{}", entry); -} -``` - -### 检查缓冲区状态 - -```rust -use log::{log_len, log_dropped_count}; - -// 检查有多少条日志等待读取 -let buffered_count = log_len(); -println!("Buffered logs: {}", buffered_count); - -// 检查是否有日志被丢弃(缓冲区溢出) -let dropped = log_dropped_count(); -if dropped > 0 { - pr_warn!("Warning: {} logs were dropped due to buffer overflow", dropped); -} -``` - -### 定期读取日志(避免溢出) - -```rust -use log::{read_log, log_len, log_dropped_count}; - -// 日志读取任务(可以在内核线程中运行) -fn log_reader_task() { - loop { - // 定期检查缓冲区 - let count = log_len(); - if count > 0 { - println!("=== Reading {} buffered logs ===", count); - - while let Some(entry) = read_log() { - // 处理日志(打印、写入文件、发送到网络等) - process_log_entry(entry); - } - } - - // 检查溢出 - let dropped = log_dropped_count(); - if dropped > 0 { - println!("WARNING: {} logs were dropped", dropped); - } - - // 休眠一段时间 - sleep_ms(1000); - } -} - -fn process_log_entry(entry: LogEntry) { - // 示例:写入到文件或发送到远程服务器 - // file.write_fmt(format_args!("{}\n", entry)).ok(); - println!("{}", entry); -} -``` - -### 非破坏性读取 - -使用 `peek_log()` 可以读取日志而不删除它们: - -```rust -use log::{peek_log, log_reader_index, log_writer_index}; - -// 获取可读范围 -let start = log_reader_index(); -let end = log_writer_index(); - -println!("Available logs: {}", end - start); - -// 遍历所有日志(不删除) -for index in start..end { - if let Some(entry) = peek_log(index) { - println!("Log #{}: {}", index, entry); - } -} - -// 可以再次读取相同的日志 -for index in start..end { - if let Some(entry) = peek_log(index) { - // 处理日志,但它们仍保留在缓冲区中 - if entry.level() <= LogLevel::Error { - send_alert(&entry); - } - } -} -``` - -### 查询缓冲区状态 - -```rust -use log::{log_len, log_unread_bytes, log_dropped_count}; - -// 查询未读日志数量和字节数 -let count = log_len(); -let bytes = log_unread_bytes(); -let dropped = log_dropped_count(); - -println!("Buffered logs: {} entries, {} bytes", count, bytes); -println!("Dropped logs: {}", dropped); - -// 检查缓冲区使用率 -let capacity = 58; // 约58条 -let usage_percent = (count * 100) / capacity; -if usage_percent > 80 { - pr_warn!("Log buffer is {}% full", usage_percent); -} -``` - -## 使用 syslog 系统调用 - -用户空间程序可以通过 `syslog` 系统调用读取和控制内核日志。 - -### 基本用法 - -```c -#include -#include -#include -#include - -int main() { - char buf[8192]; - - // 读取内核日志(破坏性) - int len = syscall(SYS_syslog, 2, buf, sizeof(buf)); - if (len > 0) { - write(STDOUT_FILENO, buf, len); - } - - return 0; -} -``` - -### 非破坏性读取 - -```c -#include -#include -#include -#include - -#define SYSLOG_ACTION_READ_ALL 3 -#define SYSLOG_ACTION_SIZE_UNREAD 9 - -int main() { - // 1. 查询需要多少空间 - int size = syscall(SYS_syslog, SYSLOG_ACTION_SIZE_UNREAD, NULL, 0); - if (size < 0) { - perror("syslog"); - return 1; - } - - printf("Unread logs: %d bytes\n", size); - - // 2. 分配足够的缓冲区 - char *buf = malloc(size + 1); - if (!buf) { - perror("malloc"); - return 1; - } - - // 3. 读取所有日志(非破坏性) - int len = syscall(SYS_syslog, SYSLOG_ACTION_READ_ALL, buf, size); - if (len < 0) { - perror("syslog"); - free(buf); - return 1; - } - - // 4. 显示日志 - buf[len] = '\0'; - printf("%s", buf); - - free(buf); - return 0; -} -``` - -### 控制控制台输出 - -```c -#define SYSLOG_ACTION_CONSOLE_OFF 6 -#define SYSLOG_ACTION_CONSOLE_ON 7 -#define SYSLOG_ACTION_CONSOLE_LEVEL 8 - -// 禁用控制台输出(只记录到缓冲区) -syscall(SYS_syslog, SYSLOG_ACTION_CONSOLE_OFF, NULL, 0); - -// 启用控制台输出 -syscall(SYS_syslog, SYSLOG_ACTION_CONSOLE_ON, NULL, 0); - -// 设置控制台级别为 Warning (4) -int old_level = syscall(SYS_syslog, SYSLOG_ACTION_CONSOLE_LEVEL, NULL, 5); -printf("Old console level: %d\n", old_level); -``` - -### 清空日志缓冲区 - -```c -#define SYSLOG_ACTION_CLEAR 5 - -// 清空所有日志 -int ret = syscall(SYS_syslog, SYSLOG_ACTION_CLEAR, NULL, 0); -if (ret < 0) { - perror("syslog"); -} -``` - -### 实现 dmesg 工具 - -完整的 `dmesg` 工具实现示例: - -```c -// dmesg.c - 简化的 dmesg 实现 -#include -#include -#include -#include -#include -#include - -#define SYSLOG_ACTION_READ 2 -#define SYSLOG_ACTION_READ_ALL 3 -#define SYSLOG_ACTION_READ_CLEAR 4 -#define SYSLOG_ACTION_CLEAR 5 -#define SYSLOG_ACTION_CONSOLE_LEVEL 8 -#define SYSLOG_ACTION_SIZE_UNREAD 9 -#define SYSLOG_ACTION_SIZE_BUFFER 10 - -static void usage(const char *prog) { - fprintf(stderr, "Usage: %s [options]\n", prog); - fprintf(stderr, "Options:\n"); - fprintf(stderr, " -c Clear the ring buffer\n"); - fprintf(stderr, " -C Clear after reading\n"); - fprintf(stderr, " -r Print raw (do not consume)\n"); - fprintf(stderr, " -n Set console log level (1-8)\n"); - fprintf(stderr, " -s Show buffer size\n"); - exit(1); -} - -int main(int argc, char *argv[]) { - int opt; - int action = SYSLOG_ACTION_READ_ALL; // 默认非破坏性读取 - int clear_only = 0; - int show_size = 0; - int set_level = 0; - int level = 0; - - // 解析命令行参数 - while ((opt = getopt(argc, argv, "cCrn:s")) != -1) { - switch (opt) { - case 'c': - clear_only = 1; - break; - case 'C': - action = SYSLOG_ACTION_READ_CLEAR; - break; - case 'r': - action = SYSLOG_ACTION_READ_ALL; - break; - case 'n': - set_level = 1; - level = atoi(optarg); - if (level < 1 || level > 8) { - fprintf(stderr, "Invalid level: %d (must be 1-8)\n", level); - return 1; - } - break; - case 's': - show_size = 1; - break; - default: - usage(argv[0]); - } - } - - // 设置控制台级别 - if (set_level) { - int ret = syscall(SYS_syslog, SYSLOG_ACTION_CONSOLE_LEVEL, NULL, level); - if (ret < 0) { - perror("syslog"); - return 1; - } - printf("Console level set to %d (was %d)\n", level, ret); - if (!show_size && !clear_only && action == SYSLOG_ACTION_READ_ALL) { - return 0; // 只设置级别,不读取日志 - } - } - - // 显示缓冲区大小 - if (show_size) { - int size = syscall(SYS_syslog, SYSLOG_ACTION_SIZE_BUFFER, NULL, 0); - int unread = syscall(SYS_syslog, SYSLOG_ACTION_SIZE_UNREAD, NULL, 0); - if (size < 0 || unread < 0) { - perror("syslog"); - return 1; - } - printf("Buffer size: %d bytes\n", size); - printf("Unread: %d bytes\n", unread); - return 0; - } - - // 清空缓冲区 - if (clear_only) { - int ret = syscall(SYS_syslog, SYSLOG_ACTION_CLEAR, NULL, 0); - if (ret < 0) { - perror("syslog"); - return 1; - } - return 0; - } - - // 查询未读字节数 - int size = syscall(SYS_syslog, SYSLOG_ACTION_SIZE_UNREAD, NULL, 0); - if (size < 0) { - perror("syslog"); - return 1; - } - - if (size == 0) { - // 没有日志 - return 0; - } - - // 分配缓冲区 - char *buf = malloc(size + 1); - if (!buf) { - perror("malloc"); - return 1; - } - - // 读取日志 - int len = syscall(SYS_syslog, action, buf, size); - if (len < 0) { - perror("syslog"); - free(buf); - return 1; - } - - // 输出日志 - if (len > 0) { - buf[len] = '\0'; - printf("%s", buf); - } - - free(buf); - return 0; -} -``` - -**编译和使用**: - -```bash -# 编译 -gcc -o dmesg dmesg.c - -# 查看内核日志 -./dmesg - -# 查看并清空 -./dmesg -C - -# 只清空 -./dmesg -c - -# 显示缓冲区状态 -./dmesg -s - -# 设置控制台级别 -./dmesg -n 5 # 设置为 Notice -``` - -### syslog 操作类型完整列表 - -| 值 | 宏定义 | 描述 | 参数 | -|----|--------|------|------| -| 0 | SYSLOG_ACTION_CLOSE | 关闭日志(NOP) | - | -| 1 | SYSLOG_ACTION_OPEN | 打开日志(NOP) | - | -| 2 | SYSLOG_ACTION_READ | 破坏性读取 | buf, len | -| 3 | SYSLOG_ACTION_READ_ALL | 非破坏性读取 | buf, len | -| 4 | SYSLOG_ACTION_READ_CLEAR | 读取并清空 | buf, len | -| 5 | SYSLOG_ACTION_CLEAR | 清空缓冲区 | - | -| 6 | SYSLOG_ACTION_CONSOLE_OFF | 禁用控制台 | - | -| 7 | SYSLOG_ACTION_CONSOLE_ON | 启用控制台 | - | -| 8 | SYSLOG_ACTION_CONSOLE_LEVEL | 设置级别 | len (1-8) | -| 9 | SYSLOG_ACTION_SIZE_UNREAD | 查询未读字节 | - | -| 10 | SYSLOG_ACTION_SIZE_BUFFER | 查询缓冲区大小 | - | - -## 格式化复杂数据 - -### 基本格式化选项 +优先使用 `pr_*` 宏: ```rust -let value = 42; - -// 十进制 -pr_info!("Value: {}", value); // Value: 42 - -// 十六进制 -pr_info!("Value: {:#x}", value); // Value: 0x2a - -// 二进制 -pr_info!("Value: {:#b}", value); // Value: 0b101010 - -// 指定宽度 -pr_info!("Value: {:08x}", value); // Value: 0000002a +pr_info!("mounted root filesystem"); +pr_warn!("retrying transient operation"); +pr_err!("failed to open device: {}", name); +pr_debug!("state={:?}", state); ``` -### 格式化指针和地址 - -```rust -let addr = 0x80000000usize; -let ptr: *const u8 = 0x80001000 as *const u8; +这些宏会先检查 global level, 被过滤的日志不会进入后续格式化和缓冲路径。 -pr_info!("Physical address: {:#x}", addr); -pr_info!("Pointer: {:p}", ptr); -pr_debug!("Page table entry: PTE[{}] = {:#018x}", index, pte_value); -``` +## 原始控制台输出 -### 格式化多个参数 +`print!` 和 `println!` 适合启动流程或测试输出。它们保持原始控制台文本, 同时把文本作为 Info 日志写入缓冲, 方便 syslog 读取。 -```rust -let start_addr = 0x80000000usize; -let end_addr = 0x80001000usize; -let size = end_addr - start_addr; +不要把 `println!` 当成绕过日志系统的调试通道。若信息有明确严重度, 使用 `pr_*`。 -pr_info!("Memory region: {:#x} - {:#x}, size = {} bytes", - start_addr, end_addr, size); -``` +## 紧急输出 -### 使用 Debug trait +panic, trap 异常或锁状态不可信时使用 `console::emergency_print()` 或架构 trap 中的 emergency helper。它绕开常规日志核心, 目标是尽量把诊断打印出来。 -```rust -use core::fmt::Debug; +## 调整级别 -#[derive(Debug)] -struct Frame { - ppn: usize, - flags: u8, -} +- global level: 控制进入缓冲区的日志。 +- console level: 控制即时打印的日志。 -let frame = Frame { ppn: 0x80000, flags: 0x7 }; +开发时可以临时调低阈值观察更多日志。提交前应避免把全局 Debug 输出留在高频路径。 -// 使用 {:?} 格式化 -pr_debug!("Allocated frame: {:?}", frame); -// 输出:Allocated frame: Frame { ppn: 524288, flags: 7 } +## 读取日志 -// 使用 {:#?} 格式化(多行美化) -pr_debug!("Frame details: {:#?}", frame); -// 输出:Frame details: Frame { -// ppn: 524288, -// flags: 7, -// } -``` +内核内可通过读取门面消费 `LogEntry`。用户态通过 `syslog` syscall 读取格式化文本: -### 条件格式化 +- destructive read: 读取并推进 read sequence。 +- read all: 非破坏性 peek。 +- read clear/clear: 读取后或直接清空剩余日志。 +- size unread/size buffer: 查询未读格式化字节数或缓冲容量。 +- console level: 调整控制台输出阈值。 -```rust -let result: Result = Err("out of memory"); +## 使用约束 -match result { - Ok(value) => pr_info!("Operation succeeded: value = {}", value), - Err(e) => pr_err!("Operation failed: {}", e), -} - -// 或者使用更简洁的方式 -pr_info!("Result: {:?}", result); -``` - -### 格式化字符串切片 - -```rust -let name = "hello.txt"; -let message = b"Hello, world!"; - -pr_info!("Filename: {}", name); -pr_debug!("Message: {:?}", message); // 字节数组 - -// UTF-8 字符串 -let utf8_str = core::str::from_utf8(message).unwrap(); -pr_info!("Content: {}", utf8_str); -``` - -## 性能最佳实践 - -### 避免格式化被禁用的日志 - -早期过滤会自动处理,但了解其工作原理有助于编写高效代码: - -```rust -// 好:使用宏,自动早期过滤 -pr_debug!("Value: {}", expensive_calculation()); -// 如果 Debug 被禁用,expensive_calculation() 不会被调用 - -// 坏:手动调用 log_impl,无早期过滤 -use log::{log_impl, LogLevel}; -log_impl(LogLevel::Debug, format_args!("Value: {}", expensive_calculation())); -// expensive_calculation() 总是被调用,即使 Debug 被禁用 -``` - -### 热路径中的日志 - -在性能关键的代码路径中,即使是早期过滤也有微小开销: - -```rust -// 方案 1:使用条件编译(推荐) -#[cfg(debug_assertions)] -pr_debug!("Processing item {}", i); - -// 方案 2:减少日志频率 -if i % 1000 == 0 { - pr_debug!("Processed {} items", i); -} - -// 方案 3:使用更低的级别 -// 如果日志不是必需的,考虑完全移除 -``` - -### 避免在中断处理程序中大量记录日志 - -中断处理程序应该快速完成,避免阻塞系统: - -```rust -// 中断处理程序 -fn timer_interrupt_handler() { - // 好:只记录关键错误 - if critical_error { - pr_err!("Timer interrupt error"); - } - - // 坏:记录每次中断(会严重影响性能) - // pr_debug!("Timer interrupt fired"); // 不要这样做! -} -``` - -### 控制台输出的性能影响 - -控制台输出(串口通信)比缓冲区写入慢得多: - -```rust -// 性能测试示例 -use arch::timer::get_time; - -let start = get_time(); - -// 1000 次缓冲区写入(不输出到控制台) -set_console_level(LogLevel::Emergency); // 禁用控制台 -for i in 0..1000 { - pr_info!("Message {}", i); -} - -let buffered_time = get_time() - start; - -// 1000 次控制台输出 -set_console_level(LogLevel::Info); // 启用控制台 -let start = get_time(); -for i in 0..1000 { - pr_info!("Message {}", i); -} - -let console_time = get_time() - start; - -pr_info!("Buffered: {} cycles, Console: {} cycles", - buffered_time, console_time); -// 预期:console_time >> buffered_time(可能是 100 倍以上) -``` - -### 消息长度优化 - -避免超过 256 字节的消息: - -```rust -// 好:简洁的日志 -pr_info!("File opened: {}", filename); - -// 坏:过长的日志(会被截断) -pr_info!("File opened with following properties: name={}, size={}, \ - permissions={}, owner={}, group={}, created={}, modified={}, \ - accessed={}, ... [very long message]", ...); - -// 更好:分多条日志 -pr_info!("File opened: {}", filename); -pr_debug!("File size: {} bytes", size); -pr_debug!("File owner: uid={}, gid={}", uid, gid); -``` - -## 多核并发场景 - -### 多核并发写入 - -Log 子系统是并发安全的,多个 CPU 可以同时记录日志: - -```rust -// CPU 0 -fn task_on_cpu0() { - pr_info!("[CPU0] Starting task A"); - // ... 执行任务 ... - pr_info!("[CPU0] Task A completed"); -} - -// CPU 1 -fn task_on_cpu1() { - pr_info!("[CPU1] Starting task B"); - // ... 执行任务 ... - pr_info!("[CPU1] Task B completed"); -} - -// 两个 CPU 可以同时调用 pr_info!,无需担心竞争条件 -// 日志会按照时间戳顺序记录到缓冲区 -``` - -### 日志中包含 CPU ID - -日志条目自动包含 CPU ID,帮助追踪多核执行: - -```rust -// 在不同 CPU 上执行 -for i in 0..100 { - pr_debug!("Processing item {}", i); -} - -// 输出示例: -// [000012345678] [DEBUG] [CPU0/Task1] Processing item 0 -// [000012345679] [DEBUG] [CPU1/Task2] Processing item 1 -// [000012345680] [DEBUG] [CPU0/Task1] Processing item 2 -// [000012345681] [DEBUG] [CPU2/Task3] Processing item 3 -``` - -### 使用时间戳分析并发行为 - -```rust -pr_info!("Task started"); -// ... 执行任务 ... -pr_info!("Task completed"); - -// 读取日志后,通过时间戳计算执行时间 -// [000012000000] [INFO] [CPU0/Task1] Task started -// [000012005000] [INFO] [CPU0/Task1] Task completed -// 执行时间:5000 个时钟周期 -``` - -### 竞态条件的调试 - -```rust -// 使用日志追踪竞态条件 -static SHARED_COUNTER: AtomicUsize = AtomicUsize::new(0); - -fn increment_counter() { - let old = SHARED_COUNTER.fetch_add(1, Ordering::SeqCst); - pr_debug!("Counter: {} -> {}", old, old + 1); -} - -// 多个 CPU 并发调用 increment_counter() -// 日志会显示每个 CPU 看到的值和顺序 -``` - -## 常见陷阱 - -### 陷阱 1:消息被截断 - -**问题**:消息超过 256 字节会被截断 - -```rust -// 错误示例:超长消息 -let long_string = "a".repeat(300); -pr_info!("Data: {}", long_string); -// 只会记录前 256 字节,后面的内容丢失 -``` - -**解决方案**:分多条日志记录 - -```rust -// 正确做法:分段记录 -let data = vec![1, 2, 3, /* ... 很多数据 */]; -pr_info!("Data (total {} items):", data.len()); -for (i, chunk) in data.chunks(10).enumerate() { - pr_debug!(" Chunk {}: {:?}", i, chunk); -} -``` - -### 陷阱 2:忘记读取日志导致缓冲区溢出 - -**问题**:日志写入速度超过读取速度,旧日志被覆盖 - -```rust -// 持续写入日志,但从不读取 -for i in 0..1000 { - pr_info!("Message {}", i); -} - -// 缓冲区只能容纳约 60 条日志 -// 早期的日志(0-940)会被覆盖,只能读取到后 60 条 -``` - -**解决方案**:定期读取日志 - -```rust -// 创建日志读取任务 -fn log_reader() { - loop { - while let Some(entry) = read_log() { - // 处理日志(打印、存储等) - handle_log(entry); - } - sleep_ms(100); // 每 100ms 读取一次 - } -} -``` - -### 陷阱 3:在日志中使用昂贵的计算 - -**问题**:即使有早期过滤,但如果计算在宏参数中,仍然会执行 - -```rust -// 错误示例:昂贵的计算 -pr_debug!("Hash: {}", compute_expensive_hash(&data)); -// 即使 Debug 被禁用,compute_expensive_hash 仍然会被调用! -``` - -**解决方案**:先检查级别再计算 - -```rust -// 正确做法:条件计算 -use log::is_level_enabled; -if is_level_enabled(LogLevel::Debug) { - let hash = compute_expensive_hash(&data); - pr_debug!("Hash: {}", hash); -} - -// 或者使用条件编译 -#[cfg(debug_assertions)] -{ - let hash = compute_expensive_hash(&data); - pr_debug!("Hash: {}", hash); -} -``` - -**注意**:这个陷阱是 Rust 宏的特性决定的,宏参数在宏展开前求值。 - -### 陷阱 4:日志级别配置不当 - -**问题**:global_level 低于 console_level,导致部分日志无法显示 - -```rust -// 错误配置 -set_global_level(LogLevel::Warning); // 只缓存 Warning 及以上 -set_console_level(LogLevel::Info); // 期望显示 Info 及以上 - -pr_info!("This message will not appear!"); -// Info < Warning,不会被缓存,console_level 无效 -``` - -**解决方案**:确保 global_level <= console_level - -```rust -// 正确配置 -set_global_level(LogLevel::Info); // 缓存 Info 及以上 -set_console_level(LogLevel::Warning); // 显示 Warning 及以上 - -pr_info!("This will be buffered but not shown"); -pr_warn!("This will be buffered and shown"); -``` - -### 陷阱 5:在 panic handler 中记录日志 - -**问题**:panic handler 可能在不稳定状态下运行,日志系统可能无法正常工作 - -```rust -#[panic_handler] -fn panic_handler(info: &PanicInfo) -> ! { - // 谨慎使用日志,此时系统状态可能不一致 - // pr_emerg! 是最安全的选择 - pr_emerg!("Kernel panic: {}", info); - - // 不要尝试读取日志缓冲区或复杂操作 - // 直接 shutdown 或进入死循环 - loop {} -} -``` - -## 调试技巧 - -### 追踪函数调用 - -```rust -fn allocate_frame() -> Result { - pr_debug!(">>> Entering allocate_frame()"); - - let result = do_allocate(); - - match &result { - Ok(frame) => pr_debug!("<<< allocate_frame() -> Ok(Frame {{ ppn: {:#x} }})", frame.ppn), - Err(e) => pr_debug!("<<< allocate_frame() -> Err({:?})", e), - } - - result -} -``` - -### 使用条件日志 - -```rust -// 只在特定条件下记录日志 -if unlikely_condition { - pr_warn!("Rare condition occurred: {}", details); -} - -// 使用断言 + 日志 -debug_assert!({ - pr_debug!("Assertion check: value = {}", value); - value > 0 -}); -``` - -### 性能分析 - -```rust -use arch::timer::get_time; - -fn performance_critical_function() { - let start = get_time(); - - // ... 执行代码 ... - - let elapsed = get_time() - start; - pr_debug!("Function took {} cycles", elapsed); -} -``` - -### 状态转换日志 - -```rust -#[derive(Debug)] -enum State { - Idle, - Running, - Blocked, - Terminated, -} - -fn set_task_state(task: &mut Task, new_state: State) { - pr_debug!("Task {} state: {:?} -> {:?}", task.id, task.state, new_state); - task.state = new_state; -} -``` - -### 使用日志分析死锁 - -```rust -// 记录锁的获取和释放 -pr_debug!("Trying to acquire lock: {}", lock_name); -lock.acquire(); -pr_debug!("Acquired lock: {}", lock_name); - -// ... 临界区代码 ... - -pr_debug!("Releasing lock: {}", lock_name); -lock.release(); -pr_debug!("Released lock: {}", lock_name); - -// 如果系统挂起,查看日志可以发现哪个锁未被释放 -``` - -### 内存泄漏追踪 - -```rust -static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0); -static FREE_COUNT: AtomicUsize = AtomicUsize::new(0); - -fn allocate() -> *mut u8 { - let ptr = do_allocate(); - let count = ALLOC_COUNT.fetch_add(1, Ordering::Relaxed) + 1; - pr_debug!("Allocated: {:p}, total allocations: {}", ptr, count); - ptr -} - -fn deallocate(ptr: *mut u8) { - let count = FREE_COUNT.fetch_add(1, Ordering::Relaxed) + 1; - pr_debug!("Freed: {:p}, total frees: {}", ptr, count); - do_free(ptr); -} - -// 定期检查 -fn check_memory_leaks() { - let allocs = ALLOC_COUNT.load(Ordering::Relaxed); - let frees = FREE_COUNT.load(Ordering::Relaxed); - if allocs != frees { - pr_warn!("Potential memory leak: {} allocations, {} frees", allocs, frees); - } -} -``` - -### 使用日志辅助 GDB 调试 - -```rust -// 在关键点记录日志 -pr_info!("Checkpoint A: value = {}", value); - -// 在 GDB 中设置断点: -// (gdb) break os/src/module.rs:123 -// (gdb) condition 1 value == 42 - -// 结合日志查看执行流程 -``` - -## 示例:完整的日志使用场景 - -### 文件系统操作日志 - -```rust -fn open_file(path: &str, flags: u32) -> Result { - pr_info!("Opening file: {}, flags: {:#x}", path, flags); - - // 查找文件 - pr_debug!("Looking up inode for: {}", path); - let inode = match lookup_inode(path) { - Ok(inode) => { - pr_debug!("Found inode: {}", inode.number); - inode - } - Err(e) => { - pr_err!("Failed to lookup file {}: {:?}", path, e); - return Err(e); - } - }; - - // 检查权限 - pr_debug!("Checking permissions for inode {}", inode.number); - if !check_permissions(&inode, flags) { - pr_warn!("Permission denied: {}, uid={}", path, current_uid()); - return Err(Error::PermissionDenied); - } - - // 分配文件描述符 - let fd = allocate_fd(inode)?; - pr_info!("File opened successfully: {} -> fd {}", path, fd); - - Ok(fd) -} -``` - -### 进程调度日志 - -```rust -fn schedule() -> ! { - loop { - // 选择下一个任务 - let next_task = scheduler::pick_next_task(); - - pr_debug!("Scheduling: CPU{} switching to task {} ({})", - current_cpu_id(), next_task.id, next_task.name); - - // 上下文切换 - let prev_task = current_task(); - pr_debug!("Context switch: task {} -> task {}", - prev_task.id, next_task.id); - - context_switch(prev_task, next_task); - - // 任务恢复执行后 - pr_debug!("Task {} resumed", current_task().id); - } -} -``` - -### 内存管理日志 - -```rust -fn allocate_pages(count: usize) -> Result { - pr_debug!("Allocating {} pages", count); - - // 检查可用内存 - let free_pages = get_free_page_count(); - pr_debug!("Free pages: {}, requested: {}", free_pages, count); - - if free_pages < count { - pr_warn!("Low memory: {} pages free, {} requested", - free_pages, count); - - // 尝试回收内存 - pr_info!("Attempting memory reclaim"); - reclaim_pages(); - - let free_pages = get_free_page_count(); - if free_pages < count { - pr_err!("Out of memory: {} pages free, {} requested", - free_pages, count); - return Err(Error::OutOfMemory); - } - } - - let addr = frame_allocator::allocate(count)?; - pr_debug!("Allocated pages: {:#x}, count: {}", addr, count); - - Ok(addr) -} -``` +- 日志消息保持简短, 超过固定上限会截断。 +- 不要在持有关键锁时写大量日志, 即使缓冲无锁, 控制台输出仍可能拖慢路径。 +- 不要依赖日志作为同步机制。 +- 用户态读取 syslog 时要能处理 ANSI 颜色码。 +- 热路径中优先用 `pr_debug!`, 让默认过滤承担开销控制。 -## 总结 +## 已知限制 -Log 子系统提供了强大而灵活的日志功能: +- 当前 syslog 权限检查尚未完整接入 capability。 +- 多个用户态读取者会竞争同一个破坏性 read sequence。 +- emergency 输出可能不进入缓冲, 只保证尽量打印。 -- **8 个宏**覆盖不同的严重程度 -- **双过滤器**平衡日志完整性和实时性 -- **无锁设计**支持多核并发 -- **早期过滤**保证性能 -- **丰富的格式化**支持复杂数据 +## 源码索引 -遵循本文档的最佳实践,可以有效利用日志系统进行开发、调试和问题诊断。 +- `os/src/log/macros.rs`: 日志宏。 +- `os/src/log/mod.rs`: 读取和级别门面。 +- `os/src/kernel/syscall/sys.rs`: `syslog()`。 +- `os/src/uapi/log.rs`: syslog action。 +- `os/src/console.rs`: `print!`, `println!`, emergency 输出。 diff --git a/document/mm/README.md b/document/mm/README.md index 212a90fd..f4c9f674 100644 --- a/document/mm/README.md +++ b/document/mm/README.md @@ -1,234 +1,118 @@ # MM 子系统文档 -## 简介 - -MM(Memory Management,内存管理)子系统是 Comix 内核的核心组件,负责管理系统的物理和虚拟内存。该子系统采用清晰的分层架构,将架构无关的抽象层与架构特定的实现层分离,支持 RISC-V 和 LoongArch 等多种硬件平台。 - -### 主要功能 - -- **物理内存管理**:物理帧的分配、回收和跟踪 -- **虚拟内存管理**:地址空间管理、页表操作、内存映射 -- **内核堆分配**:支持动态内存分配的全局分配器 -- **架构抽象**:统一的接口支持多种硬件架构 - -## 模块结构 - -``` -os/src/mm/ # 架构无关的内存管理层 -│ -├── mod.rs ........................ MM 子系统初始化入口 -│ -├── address/ ...................... 地址抽象层 -│ ├── address.rs ............... 物理/虚拟地址类型 (Paddr/Vaddr) -│ ├── page_num.rs .............. 物理/虚拟页号类型 (Ppn/Vpn) -│ └── operations.rs ............ 地址运算 trait 定义 -│ -├── frame_allocator/ .............. 物理帧分配器 -│ └── frame_allocator.rs ....... 核心分配器及 RAII 包装器 -│ -├── global_allocator/ ............. 内核堆分配器 -│ ├── global_allocator.rs ...... talc 全局分配器实现 -│ └── heap.rs .................. C 风格 kmalloc 接口(未实现) -│ -├── page_table/ ................... 页表抽象层 -│ ├── page_table.rs ............ PageTableInner trait 定义 -│ └── page_table_entry.rs ...... PTE trait 及通用标志位 -│ -└── memory_space/ ................. 地址空间管理 - ├── memory_space.rs .......... MemorySpace 结构及空间创建 - └── mapping_area.rs .......... MappingArea 映射区域管理 - -os/src/arch/{riscv,loongarch}/mm/ # 架构特定实现层 -│ -├── mod.rs ........................ 地址转换函数 -├── page_table.rs ................. PageTableInner 实现 -└── page_table_entry.rs ........... PageTableEntry 实现 +MM 子系统负责 Comix 内核的物理帧分配, 内核堆, 页表抽象和进程地址空间管理. 当前实现把架构无关的策略放在 `os/src/mm/`, 把页表格式, TLB 刷新和地址转换放在 `os/src/arch/{riscv,loongarch}/mm/`. + +本文档聚焦设计边界和关键流程. 具体函数签名, 字段和错误分支以 rustdoc 与源码为准. + +## 当前状态 + +- 支持 RISC-V 和 LoongArch64 的页表后端. +- 地址和页号使用强类型包装: `PA`, `VA`, `UA`, `Ppn`, `Vpn`. +- 物理帧由全局 `SpinLock` 保护, 已分配帧通过 RAII tracker 回收. +- 全局堆使用 `talc::Talck`. +- 地址空间由 `MemorySpace` 维护页表和 `MappingArea` 列表, 支持 `brk`, `mmap`, `munmap`, `mprotect`, ELF 加载, fork 克隆, 文件映射和 SysV shared memory 映射. +- TLB 批处理上下文由架构后端提供: RISC-V 会合并跨核 shootdown, LoongArch 当前只保证本地刷新. + +## 模块边界 + +```text +os/src/mm/ ++-- mod.rs ++-- address/ +| +-- mod.rs +| +-- operations.rs +| +-- page_num.rs +| +-- types.rs ++-- frame_allocator/ +| +-- mod.rs +| +-- allocator.rs ++-- global_allocator/ +| +-- mod.rs +| +-- talc_alloc.rs ++-- page_table/ +| +-- mod.rs +| +-- inner.rs +| +-- page_table_entry.rs ++-- memory_space/ + +-- mod.rs + +-- mmap_file.rs + +-- mapping_area/ + | +-- mod.rs + | +-- map_ops.rs + | +-- split_ops.rs + | +-- resize_ops.rs + | +-- file_ops.rs + +-- space/ + +-- mod.rs + +-- address_space.rs + +-- kernel_space.rs + +-- elf_loader.rs + +-- mmap_ops.rs + +-- tests.rs ``` -## 文档导航 - -### 核心概念 - -- **[整体架构](architecture.md)** - MM 子系统的分层设计、模块依赖关系和初始化流程 - -### 子模块详解 - -- **[地址抽象层](address.md)** - Paddr/Vaddr/Ppn/Vpn 类型、地址运算和范围操作(**左闭右开区间**) -- **[物理帧分配器](frame_allocator.md)** - 水位线 + 回收栈分配策略、FrameTracker RAII 机制 -- **[全局堆分配器](global_allocator.md)** - talc 全局分配器实现和动态内存分配 -- **[页表抽象层](page_table.md)** - PageTableInner trait、RISC-V SV39 实现、UniversalPTEFlag - -### 地址空间管理 - -- **[地址空间管理](memory_space.md)** - 地址空间管理、MemorySpace 结构、MappingArea 映射区域及系统调用支持 - -### API 参考 - -- **[API 索引](api_reference.md)** - 完整的公共 API 列表及文件位置 - -## 设计原则 - -### 1. 架构抽象 - -MM 子系统使用 trait 系统实现架构抽象,架构特定代码必须实现以下接口: - -- `vaddr_to_paddr()` / `paddr_to_vaddr()` - 地址转换函数 -- `PageTableInner` trait - 页表操作接口 -- `PageTableEntry` trait - 页表项操作接口 - -### 2. 安全性保障 - -- **RAII 模式**:物理帧通过 `FrameTracker` 自动管理生命周期 -- **类型安全**:物理地址和虚拟地址使用不同类型,防止混用 -- **所有权系统**:利用 Rust 的所有权机制防止内存泄漏 - -### 3. 性能优化 - -- **帧回收优化**:回收栈自动合并连续帧,减少碎片 -- **直接映射**:内核空间使用直接映射,避免页表查找开销 -- **对齐分配**:支持对齐的连续帧分配,优化 DMA 等场景 - -## 重要约定 - -### Range 语义 - -所有 range 类型均遵循 **左闭右开区间** 语义: - -- `AddressRange::new(start, end)` 表示区间 **[start, end)** -- `PageNumRange::new(start, end)` 表示区间 **[start, end)** -- 迭代器遍历时包含 `start`,不包含 `end` +架构相关目录: -### 内存布局 +```text +os/src/arch/riscv/mm/ ++-- mod.rs ++-- page_table.rs ++-- page_table_entry.rs -Comix 采用**高半核(Higher Half Kernel)**设计,虚拟地址空间分为两个主要区域: - -``` -虚拟地址空间布局(从高地址到低地址): - -═══════════════════════════════════════════════════════════════════ - 高半核(内核空间) -═══════════════════════════════════════════════════════════════════ -0xFFFF_FFFF_FFFF_FFFF - | - ... (向高地址扩展的物理内存直接映射区) - | -[可用物理内存帧] ← [ekernel, MEMORY_END) 直接映射 -[内核堆 Heap] ← sheap ~ eheap (16MB,talc 分配器) -[内核 BSS 段 .bss] ← sbss ~ ebss -[内核数据段 .data] ← sdata ~ edata -[内核只读数据段 .rodata] ← srodata ~ erodata -[内核代码段 .text] ← stext ~ etext - | -0xFFFF_FFC0_8020_0000 ← VIRTUAL_BASE (内核加载地址) - | -地址上半底部 ← VADDR_START (内核空间基址) - -物理地址映射: vaddr = paddr | 0xFFFF_FFC0_0000_0000 - -═══════════════════════════════════════════════════════════════════ - 低半核(用户空间) -═══════════════════════════════════════════════════════════════════ -地址下半顶部 - | -[USER_STACK] ← 用户栈区域 (4MB) - | - ... (动态扩展空间) - | -[USER_HEAP] ← 用户堆区域(可动态扩展,最大 64MB) -[USER_DATA] ← 用户数据段 (.data, .bss) -[USER_TEXT] ← 用户代码段 (.text) - | -0x0000_0000_0000_0000 -``` - -**关键地址常量**: - -*内核空间*: -- `VADDR_START = 0xFFFF_FFC0_0000_0000` - 内核虚拟地址空间基址(RISC-V) -- `VIRTUAL_BASE = 0xFFFF_FFC0_8020_0000` - 内核实际加载地址 -- `PHYSICAL_BASE = 0x8020_0000` - 内核物理加载地址 -- `MEMORY_END = 0x8800_0000` - 物理内存结束地址(128MB,QEMU virt) - -*用户空间*: -- `USER_STACK_TOP - 用户栈顶 - -**地址转换规则**(RISC-V SV39): -- 物理地址 → 虚拟地址:`vaddr = paddr | VADDR_START` -- 虚拟地址 → 物理地址:`paddr = vaddr & 0x0000_003F_FFFF_FFFF` - -## 快速开始 - -### 初始化流程 - -MM 子系统在 `mm::init()` 中按以下顺序初始化: - -1. **物理帧分配器初始化** - 管理 `[ekernel, MEMORY_END)` 物理内存区域 -2. **内核堆分配器初始化** - 初始化全局堆分配器 -3. **内核地址空间创建** - 创建并激活内核页表 - -```rust -// os/src/mm/mod.rs:33 -pub fn init() { - // 1. 初始化物理帧分配器 - let ekernel_paddr = unsafe { vaddr_to_paddr(ekernel as usize) }; - init_frame_allocator(Ppn::from_addr_ceil(ekernel_paddr), - Ppn::from_addr_floor(MEMORY_END)); - - // 2. 初始化堆分配器 - init_heap(); - - // 3. 创建并激活内核地址空间 - #[cfg(target_arch = "riscv64")] { - let root_ppn = with_kernel_space(|space| space.root_ppn()); - crate::arch::mm::PageTableInner::activate(root_ppn); - } -} +os/src/arch/loongarch/mm/ ++-- mod.rs ++-- page_table.rs ++-- page_table_entry.rs ``` -### 常见操作示例 - -#### 分配物理帧 +## 初始化流程 -```rust -use crate::mm::frame_allocator::alloc_frame; - -// 分配单个物理帧(自动释放) -let frame = alloc_frame().expect("Failed to allocate frame"); -let ppn = frame.ppn(); -``` +`mm::init()` 是启动入口: -#### 创建地址映射 +1. 把链接器符号 `ekernel` 从内核虚拟地址转换为物理地址, 并按页向上对齐. +2. 优先从设备树读取真实 DRAM 范围, 读取失败时回退到编译期 `MEMORY_END`. +3. LoongArch64 对可用物理范围设 1GiB 上限, 与内核直接映射窗口保持一致; RISC-V 覆盖设备树报告的 DRAM. +4. 初始化物理帧分配器. +5. 在启用 `alloc` 时初始化 talc 堆. +6. 创建最终内核 `MemorySpace`, 保存到全局内核空间句柄, 供其他 CPU 使用同一份内核页表. -```rust -use crate::mm::memory_space::MemorySpace; - -// 创建用户地址空间 -let mut space = MemorySpace::from_elf(elf_data); - -// 映射匿名内存 -space.mmap(start_vaddr, len, prot); -``` +`mm::init()` 创建但不主动激活页表. 激活由调用者在合适的启动阶段通过 `mm::activate(root_ppn)` 完成. -#### 地址转换 +## 核心设计约定 -```rust -use crate::mm::address::{Vaddr, Paddr}; +- 所有 `Range`, `AddressRange`, `PageNumRange`, `VpnRange`, `PpnRange` 都采用 `[start, end)` 语义. +- 内核映射和用户映射共享同一页表结构: 用户页表包含用户私有映射和内核共享映射, 通过 PTE 的用户访问位隔离权限. +- `MappingArea` 是 VMA 级别的元数据和帧所有权边界. 页表只保存硬件可见映射, 不负责记录映射来源. +- `MapType::Reserved` 表示占用虚拟地址但不建立叶子 PTE, 用于 `PROT_NONE` 和 guard page. +- 当前页表路径只启用 4K 页. `PageSize` 目前只有 `Size4K`. +- 架构后端必须把 `UniversalPTEFlag` 翻译成自己的 PTE 标志, 不能把 RISC-V 位语义直接泄漏到上层. -// 虚拟地址转物理地址 -let vaddr = Vaddr::new(0xffff_ffc0_8000_0000); -let paddr = vaddr.to_paddr(); - -// 物理地址转虚拟地址 -let vaddr_back = paddr.to_vaddr(); -``` - -## 相关资源 - -- **源代码位置**:`os/src/mm/` 和 `os/src/arch/{riscv,loongarch}/mm/` -- **配置常量**:`os/src/config.rs` - -## 版本信息 +## 文档导航 -- **Rust 版本**:nightly-2025-01-13 -- **支持架构**:RISC-V (SV39), LoongArch (TODO) -- **页面大小**:4KB(大页支持已暂时禁用) +- [整体架构](architecture.md) +- [地址抽象层](address.md) +- [物理帧分配器](frame_allocator.md) +- [全局堆分配器](global_allocator.md) +- [页表抽象层](page_table.md) +- [地址空间管理](memory_space.md) +- [源码索引](api_reference.md) + +## 已知限制 + +- `MemorySpace::areas` 仍是线性 `Vec`, 区域查找和重叠检测是线性扫描. +- fork 当前对 `Framed` 区域做深拷贝, 不是真正 COW. +- RISC-V 后端有跨核 TLB shootdown, 但 LoongArch 后端的批处理上下文当前是本地刷新占位实现. +- 4K 页是唯一启用路径, 大页映射还未作为正式能力暴露. + +## 源码索引 + +- `os/src/mm/mod.rs:36` - MM 初始化, DRAM 范围选择, 内核空间全局句柄. +- `os/src/mm/address/` - 地址, 页号, 范围和转换 trait. +- `os/src/mm/frame_allocator/allocator.rs:96` - 全局帧分配器和 RAII tracker. +- `os/src/mm/global_allocator/talc_alloc.rs:22` - talc 全局堆分配器. +- `os/src/mm/page_table/inner.rs:8` - 架构页表 trait. +- `os/src/mm/page_table/page_table_entry.rs:12` - 通用 PTE 标志和转换接口. +- `os/src/mm/memory_space/mapping_area/mod.rs:16` - VMA 类型, 映射策略和所有权字段. +- `os/src/mm/memory_space/space/address_space.rs:3` - `MemorySpace` 基本操作和 fork 克隆. +- `os/src/mm/memory_space/space/kernel_space.rs:30` - 内核映射构建. +- `os/src/mm/memory_space/space/mmap_ops.rs:10` - `brk`, `mmap`, `munmap`, `mprotect`. diff --git a/document/mm/address.md b/document/mm/address.md index f18dba43..85ae683d 100644 --- a/document/mm/address.md +++ b/document/mm/address.md @@ -1,353 +1,67 @@ # 地址抽象层 -## 概述 +地址抽象层把裸 `usize` 拆成带语义的类型, 让 MM 代码在接口边界上区分物理地址, 内核虚拟地址, 用户地址和页号. -地址抽象层提供了类型安全的物理地址和虚拟地址抽象,以及页号(Page Number)的相关操作。通过 `repr(transparent)` 实现零成本抽象,同时提供编译期类型安全保证。 +## 当前状态 -### 设计目标 +- `PA`, `VA`, `UA` 来自架构地址模块, 在 `os/src/mm/address/types.rs` 中实现统一 trait. +- `Ppn` 和 `Vpn` 是 MM 层定义的页号类型. +- `AddressRange` 和 `PageNumRange` 统一使用 `[start, end)`. +- `ConvertablePA` 和 `ConvertableVA` 只表达直接映射地址转换能力, 不替代页表翻译. -1. **类型安全**:物理地址和虚拟地址使用不同类型,防止混用 -2. **零成本抽象**:通过 `repr(transparent)` 保证与 usize 相同的内存布局 -3. **便捷操作**:提供丰富的算术运算、对齐操作和范围查询 -4. **架构无关**:地址转换逻辑委托给架构特定层 +## 目标 -### 核心类型 +- 防止把物理地址和虚拟地址作为同一种整数随意传递. +- 统一页对齐, 页号转换和范围遍历. +- 把架构地址格式保留在 `arch` 层, 让 MM 上层只依赖少量 trait. -```rust -// 物理地址和虚拟地址 -#[repr(transparent)] -pub struct Paddr(usize); +## 非目标 -#[repr(transparent)] -pub struct Vaddr(usize); +- 不负责检查用户指针合法性.用户地址最终仍要经过当前进程页表或专门的用户拷贝路径验证. +- 不负责页表权限判断.`VA::to_pa()` 只适用于架构直接映射窗口, 普通用户地址必须走 `MemorySpace::translate()`. +- 不承诺所有算术都做溢出保护.调用方仍需在外部边界做长度和地址空间检查. -// 物理页号和虚拟页号 -#[repr(transparent)] -pub struct Ppn(usize); +## 模块边界 -#[repr(transparent)] -pub struct Vpn(usize); +- `types.rs` 定义 `Address`, `AddressRange`, `PA/VA/UA` 的统一行为以及直接映射转换 trait. +- `page_num.rs` 定义 `PageNum`, `Ppn`, `Vpn`, `PpnRange`, `VpnRange`. +- `operations.rs` 定义 `UsizeConvert`, `CalcOps`, `AlignOps` 以及地址/页号算术宏. +- `mod.rs` 只负责重导出, 让其他 MM 模块通过 `crate::mm::address::*` 使用这些类型. -// 地址范围(左闭右开区间) -pub struct AddressRange { - start: T, // 包含 - end: T, // 不包含 -} +## 关键流程 -pub type PaddrRange = AddressRange; -pub type VaddrRange = AddressRange; -pub type PpnRange = AddressRange; -pub type VpnRange = AddressRange; -``` +### 地址到页号 -## 地址类型 (Paddr/Vaddr) +页号转换分为 floor 和 ceil: -### 基本操作 +- floor 用于查找地址所在页, 例如页表翻译非页对齐地址. +- ceil 用于把字节区间尾部扩展到完整页, 例如映射 `[start, start + len)`. -```rust -// 创建地址 -let paddr = Paddr::new(0x8000_0000); -let vaddr = Vaddr::new(0xffff_ffc0_8000_0000); +这两个方向不能混用.`translate()` 一类路径必须用 floor, 否则页内地址会错误落到下一页. -// 转换为 usize -let addr_val: usize = paddr.as_usize(); +### 物理地址到内核虚拟地址 -// 地址转换 -let vaddr = paddr.to_vaddr(); // 物理 → 虚拟 -let paddr = vaddr.to_paddr(); // 虚拟 → 物理(unsafe) -``` +`ConvertablePA::to_va()` 调用架构直接映射函数.它适用于已经确认位于内核直接映射窗口内的物理页, 例如清零物理帧或读写页表页. -### 算术运算 +### 虚拟地址到物理地址 -```rust -// 基于类型大小的运算 -let addr = Vaddr::new(0x1000); -let addr2 = addr.add::(3); // 0x1000 + 3 * 8 = 0x1018 +`ConvertableVA::to_pa()` 只面向架构直接映射地址.用户地址和一般 VMA 地址必须通过当前 `MemorySpace` 的页表翻译. -// 单步前进/后退 -let next = addr.step(); // 0x1001 -let prev = addr.step_back(); // 0x0fff +## 生命周期与安全约束 -// 运算符重载 -let sum = addr1 + addr2; // 加法 -let diff = addr1 - addr2; // 减法 -let aligned = addr1 & mask; // 位与(用于对齐) -``` +- `Ppn` 本身不拥有物理帧.帧所有权由 `FrameTracker`, `FrameRangeTracker` 或 `MappingArea.frames` 管理. +- `VA` 本身不说明地址可访问.它只是一种数值语义, 访问前仍需页表映射和权限保证. +- Range 迭代只表达页号/地址序列, 不表达内存已经映射. -### 对齐操作 +## 已知限制 -```rust -let addr = Vaddr::new(0x1234); +- 地址算术主要服务内核内部路径, 对恶意输入的完整溢出防护在系统调用层或调用方完成. +- `AddressRange::from_slices()` 这类辅助构造不应作为 VMA 合法性判断依据. -// 按任意对齐值对齐 -let up = addr.align_up(16); // 0x1240 -let down = addr.align_down(16); // 0x1230 -assert!(addr.is_aligned(16)); // false +## 源码索引 -// 按页对齐(PAGE_SIZE = 4096) -let page_aligned = addr.align_up_to_page(); // 0x2000 -assert!(page_aligned.is_page_aligned()); // true -``` - -### 地址范围 - -```rust -// 创建范围 [0x1000, 0x5000) -let range = VaddrRange::new( - Vaddr::new(0x1000), - Vaddr::new(0x5000) -); - -// 长度和边界 -assert_eq!(range.len(), 0x4000); -assert_eq!(range.start(), Vaddr::new(0x1000)); -assert_eq!(range.end(), Vaddr::new(0x5000)); - -// 包含关系(注意:左闭右开) -assert!(range.contains(&Vaddr::new(0x1000))); // 包含 start -assert!(!range.contains(&Vaddr::new(0x5000))); // 不包含 end - -// 区间运算 -let r1 = VaddrRange::new(Vaddr::new(0x1000), Vaddr::new(0x3000)); -let r2 = VaddrRange::new(Vaddr::new(0x2000), Vaddr::new(0x4000)); - -if r1.intersects(&r2) { - let inter = r1.intersection(&r2).unwrap(); // [0x2000, 0x3000) -} - -let union = r1.union(&r2).unwrap(); // [0x1000, 0x4000) -``` - -## 页号类型 (Ppn/Vpn) - -### 地址与页号转换 - -```rust -// 地址 → 页号 -let addr = Paddr::new(0x8000_1234); -let ppn_floor = Ppn::from_addr_floor(addr); // 向下取整:0x8000_1234 / 4096 -let ppn_ceil = Ppn::from_addr_ceil(addr); // 向上取整 - -// 页号 → 地址 -let vpn = Vpn::new(0x100); -let start_addr = vpn.start_addr(); // 页起始地址:0x100 * 4096 -let end_addr = vpn.end_addr(); // 下一页起始地址:0x101 * 4096 -``` - -### 页号运算 - -```rust -let ppn = Ppn::new(0x8000_1); - -// 步进 -let next_ppn = ppn.step(); // 0x8000_2 -let prev_ppn = ppn.step_back(); // 0x8000_0 - -// 偏移 -let offset_ppn = ppn.offset(10); // 0x8000_b -``` - -### 页号范围 - -```rust -// 创建范围 [0x10, 0x20) -// 注意:左闭右开 -let range = VpnRange::new(Vpn::new(0x10), Vpn::new(0x20)); - -assert_eq!(range.len(), 0x10); // 包含 16 个页 - -// 迭代页号 -for vpn in range { - println!("VPN: {:#x}", vpn.as_usize()); -} -// 输出:0x10, 0x11, ..., 0x1f(不包含 0x20) -``` - -## 地址运算 Trait - -地址类型通过以下 trait 提供统一的操作接口: - -### UsizeConvert - -```rust -pub trait UsizeConvert { - fn as_usize(&self) -> usize; - fn from_usize(value: usize) -> Self; -} -``` - -### CalcOps - -提供算术和位运算: - -```rust -// 支持的运算符 -addr1 + addr2 // 加法 -addr1 - addr2 // 减法 -addr1 & mask // 位与 -addr1 | mask // 位或 -addr1 ^ mask // 位异或 -addr1 >> n // 右移 -addr1 << n // 左移 -``` - -### AlignOps - -提供对齐操作: - -```rust -pub trait AlignOps { - fn is_aligned(&self, align: usize) -> bool; - fn align_up(&self, align: usize) -> Self; - fn align_down(&self, align: usize) -> Self; - fn is_page_aligned(&self) -> bool; - fn align_up_to_page(&self) -> Self; - fn align_down_to_page(&self) -> Self; -} -``` - -## 使用场景 - -### 场景 1:内核地址空间映射 - -```rust -extern "C" { - fn stext(); - fn etext(); -} - -// 获取 .text 段地址范围 -let text_start = Vaddr::new(stext as usize); -let text_end = Vaddr::new(etext as usize); - -// 创建映射区域 -space.push(MappingArea::new( - VaddrRange::new(text_start, text_end), - MapType::Direct, - UniversalPTEFlag::kernel_r() | UniversalPTEFlag::X, - AreaType::KernelText, -)); -``` - -### 场景 2:物理帧分配器初始化 - -```rust -// 计算可用物理内存范围 -let ekernel_paddr = unsafe { vaddr_to_paddr(ekernel as usize) }; -let start = Ppn::from_addr_ceil(Paddr::new(ekernel_paddr)); -let end = Ppn::from_addr_floor(Paddr::new(MEMORY_END)); - -// 初始化分配器 -init_frame_allocator(start, end); -``` - -### 场景 3:拷贝数据到物理页 - -```rust -pub fn copy_data(&self, page_table: &ActivePageTableInner, data: &[u8]) { - let mut offset = 0; - for vpn in self.vpn_range { - // 翻译虚拟地址到物理地址 - let paddr = page_table.translate(vpn.start_addr()).unwrap(); - - // 转换为可访问的虚拟地址 - let vaddr = paddr.to_vaddr(); - - // 拷贝数据 - let dst = unsafe { - core::slice::from_raw_parts_mut(vaddr.as_usize() as *mut u8, PAGE_SIZE) - }; - let len = core::cmp::min(PAGE_SIZE, data.len() - offset); - dst[..len].copy_from_slice(&data[offset..offset + len]); - offset += len; - } -} -``` - -## 常见错误 - -### 错误 1:忘记 Range 是左闭右开 - -```rust -// ❌ 错误:期望包含结束地址 -let range = VaddrRange::new(start_addr, end_addr); -assert!(range.contains(&end_addr)); // 断言失败! - -// ✅ 正确:end 应为 end_addr.step() -let range = VaddrRange::new(start_addr, end_addr.step()); -assert!(range.contains(&end_addr)); // 通过 -``` - -### 错误 2:混用物理地址和虚拟地址 - -```rust -// ❌ 编译错误 -let paddr = Paddr::new(0x8000_0000); -let vaddr: Vaddr = paddr; // 类型不匹配! - -// ✅ 正确:显式转换 -let vaddr = paddr.to_vaddr(); -``` - -### 错误 3:未检查对齐 - -```rust -// ❌ 页表根地址未对齐可能导致硬件异常 -let root_ppn = Ppn::from_addr_floor(paddr); - -// ✅ 应先检查对齐 -assert!(paddr.is_page_aligned()); -let root_ppn = Ppn::from_addr_floor(paddr); -``` - -### 错误 4:对齐值不是 2 的幂 - -```rust -// ❌ 错误:对齐值必须是 2 的幂 -let addr = Vaddr::new(0x1234); -let aligned = addr.align_up(15); // 15 不是 2 的幂 - -// ✅ 正确:使用 2 的幂作为对齐值 -let aligned = addr.align_up(16); // 16 = 2^4 -``` - -**原因**:对齐算法 `(value + align - 1) & !(align - 1)` 仅对 2 的幂有效。 - -## 性能考量 - -### 零成本抽象 - -```rust -use core::mem::{size_of, align_of}; - -// 大小和对齐与 usize 相同 -assert_eq!(size_of::(), size_of::()); -assert_eq!(align_of::(), align_of::()); -``` - -### 内联优化 - -关键方法标记为 `#[inline]`,在 `release` 模式下会被内联,编译为零成本的机器指令: - -```rust -#[inline] -pub fn new(value: usize) -> Self { Self(value) } - -#[inline] -pub fn as_usize(&self) -> usize { self.0 } - -#[inline] -pub const fn align_up(&self, align: usize) -> Self { /* ... */ } -``` - -## 相关文档 - -- [整体架构](architecture.md) - MM 子系统分层设计 -- [物理帧分配器](frame_allocator.md) - Ppn 的实际使用 -- [页表抽象层](page_table.md) - Vpn/Ppn 的页表映射 -- [API 参考](api_reference.md) - 快速 API 查询 - -## 参考实现 - -- **源代码**:`os/src/mm/address/` -- **架构接口**:`os/src/arch/*/mm/mod.rs`(地址转换函数) +- `os/src/mm/address/mod.rs:1` - 模块说明和重导出. +- `os/src/mm/address/types.rs:12` - `Address` trait 与 `AddressRange`. +- `os/src/mm/address/types.rs:103` - `ConvertablePA` 和 `ConvertableVA`. +- `os/src/mm/address/page_num.rs:13` - `PageNum`, `Ppn`, `Vpn` 和页号范围. +- `os/src/mm/address/operations.rs:12` - `UsizeConvert`, 算术和对齐 trait. diff --git a/document/mm/api_reference.md b/document/mm/api_reference.md index 64719ca4..a9e62a44 100644 --- a/document/mm/api_reference.md +++ b/document/mm/api_reference.md @@ -1,550 +1,110 @@ -# MM 子系统 API 参考手册 - -## 概述 - -本文档提供 MM 子系统所有公共 API 的快速参考,按模块分类并标注源代码位置。 - -## 初始化 API - -### mm::init() - -```rust -pub fn init() -``` - -**功能**:初始化整个 MM 子系统 - -**调用顺序**: -1. 初始化物理帧分配器 -2. 初始化内核堆分配器 -3. 创建并激活内核地址空间 - -**源代码**:`os/src/mm/mod.rs:33` - -**示例**: -```rust -fn rust_main() { - mm::init(); // 第一个调用 - // 此后可以使用所有内存管理功能 -} -``` - ---- - -## 地址抽象层 API (address/) - -### Paddr / Vaddr - -#### 创建 - -```rust -impl Paddr { - pub fn new(value: usize) -> Self - pub fn as_usize(&self) -> usize -} -``` - -**源代码**:`os/src/mm/address/address.rs:20-53` - -#### 转换 - -```rust -impl Paddr { - pub fn to_vaddr(self) -> Vaddr // 物理→虚拟 -} - -impl Vaddr { - pub fn to_paddr(self) -> Paddr // 虚拟→物理(unsafe) -} -``` - -#### 对齐 - -```rust -impl AlignOps for Paddr/Vaddr { - fn is_aligned(&self, align: usize) -> bool - fn align_up(&self, align: usize) -> Self - fn align_down(&self, align: usize) -> Self - fn is_page_aligned(&self) -> bool - fn align_up_to_page(&self) -> Self - fn align_down_to_page(&self) -> Self -} -``` - -**源代码**:`os/src/mm/address/operations.rs:44-75` - -### Ppn / Vpn - -#### 创建 - -```rust -impl Ppn/Vpn { - pub fn new(value: usize) -> Self - pub fn from_addr_floor(addr: Paddr/Vaddr) -> Self // 向下取整 - pub fn from_addr_ceil(addr: Paddr/Vaddr) -> Self // 向上取整 -} -``` - -**源代码**:`os/src/mm/address/page_num.rs:7-64` - -#### 地址转换 - -```rust -impl PageNum for Ppn/Vpn { - fn start_addr(self) -> Paddr/Vaddr // 页起始地址 - fn end_addr(self) -> Paddr/Vaddr // 页结束地址(下一页起始) - fn step(self) -> Self // 前进一页 - fn step_back(self) -> Self // 后退一页 - fn offset(self, offset: isize) -> Self // 偏移多页 -} -``` - -### AddressRange / PageNumRange - -#### 创建 - -```rust -impl AddressRange/PageNumRange { - pub fn new(start: T, end: T) -> Self // [start, end) 左闭右开 - pub fn start(&self) -> T - pub fn end(&self) -> T - pub fn len(&self) -> usize - pub fn is_empty(&self) -> bool -} -``` - -**源代码**:`os/src/mm/address/address.rs:141-233`、`os/src/mm/address/page_num.rs:93-185` - -#### 区间运算 - -```rust -pub fn contains(&self, item: &T) -> bool -pub fn intersects(&self, other: &Self) -> bool -pub fn intersection(&self, other: &Self) -> Option -pub fn union(&self, other: &Self) -> Option -``` - -**重要**:所有 Range 类型均为**左闭右开区间 [start, end)** - ---- - -## 物理帧分配器 API (frame_allocator/) - -### 分配 - -```rust -pub fn alloc_frame() -> FrameAllocResult -pub fn alloc_frames(n: usize) -> FrameAllocResult> -pub fn alloc_contig_frames(n: usize) -> FrameAllocResult -pub fn alloc_contig_frames_aligned(n: usize, align: usize) -> FrameAllocResult -``` - -**源代码**:`os/src/mm/frame_allocator/frame_allocator.rs:219-236` - -**示例**: -```rust -// 单帧 -let frame = alloc_frame()?; - -// 10个非连续帧 -let frames = alloc_frames(10)?; - -// 256个连续帧(1MB) -let contig = alloc_contig_frames(256)?; - -// 512个连续帧,2MB对齐 -let aligned = alloc_contig_frames_aligned(512, 512)?; -``` - -### FrameTracker - -```rust -impl FrameTracker { - pub fn ppn(&self) -> Ppn - pub fn start_paddr(&self) -> Paddr - pub fn as_slice(&self) -> &[T] - pub fn as_slice_mut(&mut self) -> &mut [T] -} -``` - -**源代码**:`os/src/mm/frame_allocator/frame_allocator.rs:24-71` - -**RAII**:自动释放(`Drop`) - -### FrameRangeTracker - -```rust -impl FrameRangeTracker { - pub fn start_ppn(&self) -> Ppn - pub fn end_ppn(&self) -> Ppn - pub fn start_paddr(&self) -> Paddr - pub fn end_paddr(&self) -> Paddr - pub fn iter(&self) -> impl Iterator -} -``` - -**源代码**:`os/src/mm/frame_allocator/frame_allocator.rs:74-132` - -### 错误类型 - -```rust -pub enum FrameAllocError { - OutOfMemory, - InvalidAddress, - AlignmentError, -} -``` - ---- - -## 内核堆分配器 API (global_allocator/) - -### 初始化 - -```rust -pub fn init_heap() -``` - -**功能**:初始化全局堆分配器(16 MB) - -**源代码**:`os/src/mm/global_allocator/global_allocator.rs:35` - -### 使用 - -初始化后自动支持 `alloc` crate: - -```rust -use alloc::vec::Vec; -use alloc::boxed::Box; -use alloc::string::String; -use alloc::collections::BTreeMap; - -let v = Vec::new(); -let b = Box::new(42); -let s = String::from("hello"); -let m = BTreeMap::new(); -``` - ---- - -## 页表抽象层 API (page_table/) - -### PageTableInner trait - -```rust -pub trait PageTableInner { - // 常量 - const LEVELS: usize; - const MAX_VA_BITS: usize; - const MAX_PA_BITS: usize; - - // 生命周期 - fn new() -> Self; - fn from_ppn(ppn: Ppn) -> Self; - fn activate(ppn: Ppn); - fn root_ppn(&self) -> Ppn; - - // TLB管理 - fn tlb_flush(vpn: Vpn); - fn tlb_flush_all(); - - // 映射操作 - fn map(&mut self, vpn: Vpn, ppn: Ppn, page_size: PageSize, - flags: UniversalPTEFlag) -> PagingResult<()>; - fn unmap(&mut self, vpn: Vpn) -> PagingResult<()>; - fn remap(&mut self, vpn: Vpn, new_ppn: Ppn, page_size: PageSize, - flags: UniversalPTEFlag) -> PagingResult<()>; - - // 查询 - fn translate(&self, vaddr: Vaddr) -> Option; - fn walk(&self, vpn: Vpn) -> PagingResult<(Ppn, PageSize, UniversalPTEFlag)>; -} -``` - -**源代码**:`os/src/mm/page_table/page_table.rs:5-52` - -### UniversalPTEFlag - -```rust -impl UniversalPTEFlag { - // 基础标志 - pub const V: Self; // Valid - pub const R: Self; // Readable - pub const W: Self; // Writable - pub const X: Self; // Executable - pub const U: Self; // User - pub const G: Self; // Global - pub const A: Self; // Accessed - pub const D: Self; // Dirty - - // 预定义组合 - pub fn user_read() -> Self; // U | R | V - pub fn user_rw() -> Self; // U | R | W | V - pub fn user_rx() -> Self; // U | R | X | V - pub fn kernel_r() -> Self; // R | V - pub fn kernel_rw() -> Self; // R | W | V -} -``` - -**源代码**:`os/src/mm/page_table/page_table_entry.rs:4-58` - -### PageSize - -```rust -pub enum PageSize { - Size4K = 0x1000, - Size2M = 0x20_0000, // 暂时禁用 - Size1G = 0x4000_0000, // 暂时禁用 -} -``` - -### PagingError - -```rust -pub enum PagingError { - NotMapped, - AlreadyMapped, - InvalidAddress, - InvalidPageSize, - PermissionDenied, - PageTableFull, - FrameAllocationFailed, - // ... 更多 -} -``` - -**源代码**:`os/src/mm/page_table/mod.rs:23-44` - ---- - -## 地址空间管理 API (memory_space/) - -### MemorySpace - -#### 创建 - -```rust -impl MemorySpace { - pub fn new_kernel() -> Self // 创建内核地址空间 - pub fn from_elf(elf_data: &[u8]) -> Self // 从ELF创建用户地址空间 -} -``` - -**源代码**:`os/src/mm/memory_space/memory_space.rs:203-458` - -#### 系统调用支持 - -```rust -pub fn brk(&mut self, new_end: Vaddr) -> SyscallResult -pub fn mmap(&mut self, start: Vaddr, len: usize, prot: usize) -> SyscallResult -pub fn munmap(&mut self, start: Vaddr, len: usize) -> SyscallResult<()> -``` - -#### 进程管理 - -```rust -pub fn clone_for_fork(&self) -> Self // fork时深拷贝 -pub fn activate(&self) // 激活地址空间 -pub fn root_ppn(&self) -> Ppn // 获取根页表页号 -``` - -### MappingArea - -#### 创建 - -```rust -impl MappingArea { - pub fn new( - vaddr_range: VaddrRange, - map_type: MapType, - permission: UniversalPTEFlag, - area_type: AreaType, - ) -> Self -} -``` - -**源代码**:`os/src/mm/memory_space/mapping_area.rs:62-100` - -#### 映射类型 - -```rust -pub enum MapType { - Direct, // 直接映射(内核) - Framed, // 帧映射(用户) -} - -pub enum AreaType { - KernelText, - KernelData, - UserText, - UserData, - UserStack, - UserHeap, - // ... -} -``` - -#### 操作 - -```rust -pub fn map(&mut self, page_table: &mut ActivePageTableInner) -> PagingResult<()> -pub fn unmap(&mut self, page_table: &mut ActivePageTableInner) -> PagingResult<()> -pub fn copy_data(&self, page_table: &ActivePageTableInner, data: &[u8]) -pub fn extend(&mut self, page_table: &mut ActivePageTableInner, - new_end_vpn: Vpn) -> PagingResult<()> -pub fn shrink(&mut self, page_table: &mut ActivePageTableInner, - new_end_vpn: Vpn) -> PagingResult<()> -``` - -**源代码**:`os/src/mm/memory_space/mapping_area.rs:113-626` - ---- - -## 架构特定 API (arch/*/mm/) - -### RISC-V (arch/riscv/mm/) - -#### 地址转换 - -```rust -pub const unsafe fn vaddr_to_paddr(vaddr: usize) -> usize -pub const fn paddr_to_vaddr(paddr: usize) -> usize -``` - -**源代码**:`os/src/arch/riscv/mm/mod.rs:8-15` - -#### 常量 - -```rust -pub const VADDR_START: usize = 0xffff_ffc0_0000_0000; -pub const PADDR_MASK: usize = 0x0000_003f_ffff_ffff; -``` - -#### SV39 PageTableInner - -```rust -impl PageTableInner for PageTableInner { - const LEVELS: usize = 3; - const MAX_VA_BITS: usize = 39; - const MAX_PA_BITS: usize = 56; - // ... trait实现 -} -``` - -**源代码**:`os/src/arch/riscv/mm/page_table.rs:21-286` - ---- - -## 配置常量 (config.rs) - -```rust -// 基础配置 -pub const PAGE_SIZE: usize = 4096; -pub const KERNEL_HEAP_SIZE: usize = 16 * 1024 * 1024; // 16 MB -pub const USER_STACK_SIZE: usize = 4 * 1024 * 1024; // 4 MB -pub const MAX_USER_HEAP_SIZE: usize = 64 * 1024 * 1024; // 64 MB - -// 内存布局 -pub const TRAMPOLINE: usize = usize::MAX - PAGE_SIZE + 1; -pub const TRAP_CONTEXT: usize = TRAMPOLINE - 2 * PAGE_SIZE; -pub const USER_STACK_TOP: usize = TRAP_CONTEXT - PAGE_SIZE; - -// 平台相关 -pub const MEMORY_END: usize = 0x88000000; // 128 MB -``` - -**源代码**:`os/src/config.rs` - ---- - -## 常用模式 - -### 模式 1:分配并映射页面 - -```rust -use crate::mm::frame_allocator::alloc_frame; -use crate::mm::page_table::PageSize; - -let frame = alloc_frame()?; -let ppn = frame.ppn(); -page_table.map(vpn, ppn, PageSize::Size4K, UniversalPTEFlag::user_rw())?; -frames.insert(vpn, TrackedFrames::Single(frame)); -``` - -### 模式 2:创建用户地址空间 - -```rust -use crate::mm::memory_space::MemorySpace; - -let elf_data = load_elf_from_disk(path)?; -let mut space = MemorySpace::from_elf(&elf_data); -space.activate(); -``` - -### 模式 3:扩展堆区域 - -```rust -let new_end = current_end + size; -let new_end_vaddr = Vaddr::new(new_end); -space.brk(new_end_vaddr)?; -``` - -### 模式 4:地址转换 - -```rust -// 虚拟地址 → 物理地址 -let vaddr = Vaddr::new(0xffff_ffc0_8000_1000); -let paddr = page_table.translate(vaddr).unwrap(); - -// 页号 → 地址 -let vpn = Vpn::new(0x100); -let vaddr = vpn.start_addr(); -``` - ---- - -## 快速索引 - -| 功能 | API | 源文件 | -|------|-----|--------| -| 初始化MM | `mm::init()` | `mm/mod.rs:33` | -| 分配单帧 | `alloc_frame()` | `frame_allocator/frame_allocator.rs:219` | -| 分配连续帧 | `alloc_contig_frames(n)` | `frame_allocator/frame_allocator.rs:223` | -| 地址对齐 | `addr.align_up_to_page()` | `address/operations.rs:69` | -| 页号转换 | `Ppn::from_addr_floor(paddr)` | `address/page_num.rs:34` | -| 创建页表 | `PageTableInner::new()` | `arch/riscv/mm/page_table.rs:56` | -| 映射页面 | `page_table.map(vpn, ppn, ...)` | `page_table/page_table.rs:35` | -| 创建用户空间 | `MemorySpace::from_elf(data)` | `memory_space/memory_space.rs:353` | -| 扩展堆 | `space.brk(new_end)` | `memory_space/memory_space.rs:461` | -| 地址转换 | `vaddr.to_paddr()` | `address/address.rs:50` | - ---- - -## 版本信息 - -- **文档版本**:1.0 -- **Rust工具链**:nightly-2025-01-13 -- **支持架构**:RISC-V (SV39), LoongArch (TODO) -- **页面大小**:4KB(大页支持已暂时禁用) - ---- - -## 相关文档 - -- **[总览](README.md)** - MM子系统简介和导航 -- **[架构设计](architecture.md)** - 分层架构和设计决策 -- **[地址抽象层](address/overview.md)** - Paddr/Vaddr/Ppn/Vpn详解 -- **[物理帧分配器](frame_allocator/overview.md)** - FrameAllocator详解 -- **[内核堆分配器](global_allocator/heap_allocator.md)** - talc全局分配器 -- **[页表抽象层](page_table/overview.md)** - PageTableInner trait -- **[地址空间管理](memory_space/overview.md)** - MemorySpace详解 - -## 在线文档 - -完整的 rustdoc 文档: - -```bash -cd os && cargo doc --open -``` - -生成的文档位于:`target/doc/os/mm/index.html` +# MM 源码索引 + +本页不维护完整 API 清单.公共函数签名, 参数和错误分支以 rustdoc 与源码为准.这里按设计职责索引源码入口, 方便从文档跳到实现. + +## 初始化 + +- `os/src/mm/mod.rs:36` - `mm::init()`, 负责帧分配器, 堆和内核地址空间创建. +- `os/src/mm/mod.rs:131` - `mm::activate(root_ppn)`, 通过架构页表后端激活地址空间. +- `os/src/mm/mod.rs:145` - 全局内核空间句柄, 供多 CPU 使用最终内核页表. + +## 地址和页号 + +- `os/src/mm/address/types.rs:12` - `Address` trait. +- `os/src/mm/address/types.rs:103` - 直接映射地址转换 trait. +- `os/src/mm/address/types.rs:132` - `AddressRange`. +- `os/src/mm/address/page_num.rs:13` - `PageNum` trait. +- `os/src/mm/address/page_num.rs:104` - `Ppn` 和 `Vpn`. +- `os/src/mm/address/page_num.rs:116` - `PageNumRange`. +- `os/src/mm/address/operations.rs:12` - `UsizeConvert` 与算术/对齐 trait. + +## 物理帧 + +- `os/src/mm/frame_allocator/mod.rs:15` - 对外分配入口. +- `os/src/mm/frame_allocator/allocator.rs:14` - 单帧 RAII tracker. +- `os/src/mm/frame_allocator/allocator.rs:45` - 连续帧 RAII tracker. +- `os/src/mm/frame_allocator/allocator.rs:96` - 全局 `FRAME_ALLOCATOR`. +- `os/src/mm/frame_allocator/allocator.rs:100` - `FrameAllocator` 状态. +- `os/src/mm/frame_allocator/allocator.rs:124` - 单帧分配. +- `os/src/mm/frame_allocator/allocator.rs:143` - 多个非连续帧分配. +- `os/src/mm/frame_allocator/allocator.rs:163` - 连续帧分配. +- `os/src/mm/frame_allocator/allocator.rs:180` - 对齐连续帧分配. +- `os/src/mm/frame_allocator/allocator.rs:222` - 单帧回收. +- `os/src/mm/frame_allocator/allocator.rs:259` - 连续帧回收. + +## 全局堆 + +- `os/src/mm/global_allocator/mod.rs:1` - feature gated 导出. +- `os/src/mm/global_allocator/talc_alloc.rs:22` - `Talck` 全局 allocator. +- `os/src/mm/global_allocator/talc_alloc.rs:37` - `init_heap()`. + +## 页表通用接口 + +- `os/src/mm/page_table/mod.rs:11` - 活动页表类型别名. +- `os/src/mm/page_table/mod.rs:15` - `PageSize`. +- `os/src/mm/page_table/mod.rs:21` - `PagingError`. +- `os/src/mm/page_table/inner.rs:8` - `PageTableInner` trait. +- `os/src/mm/page_table/page_table_entry.rs:12` - `UniversalPTEFlag`. +- `os/src/mm/page_table/page_table_entry.rs:48` - 通用 flag 构造辅助. +- `os/src/mm/page_table/page_table_entry.rs:61` - `UniversalConvertableFlag`. +- `os/src/mm/page_table/page_table_entry.rs:70` - `PageTableEntry` trait. + +## 页表架构后端 + +- `os/src/arch/riscv/mm/page_table.rs:13` - RISC-V 页表状态和 root frame 所有权. +- `os/src/arch/riscv/mm/page_table.rs:21` - SV39 trait 实现. +- `os/src/arch/riscv/mm/page_table.rs:403` - 带批处理的 map. +- `os/src/arch/riscv/mm/page_table.rs:425` - 带批处理的 unmap. +- `os/src/arch/riscv/mm/page_table.rs:444` - 带批处理的权限更新. +- `os/src/arch/riscv/mm/page_table.rs:467` - RISC-V `TlbBatchContext`. +- `os/src/arch/riscv/mm/page_table_entry.rs:7` - SV39 PTE flags. +- `os/src/arch/loongarch/mm/page_table.rs:25` - LoongArch64 页表状态. +- `os/src/arch/loongarch/mm/page_table.rs:34` - LoongArch64 trait 实现. +- `os/src/arch/loongarch/mm/page_table.rs:399` - 带批处理的 map. +- `os/src/arch/loongarch/mm/page_table.rs:413` - 带批处理的 unmap. +- `os/src/arch/loongarch/mm/page_table.rs:424` - 带批处理的权限更新. +- `os/src/arch/loongarch/mm/page_table.rs:440` - LoongArch64 `TlbBatchContext`. +- `os/src/arch/loongarch/mm/page_table_entry.rs:35` - LoongArch64 PTE flags. + +## 地址空间和 VMA + +- `os/src/mm/memory_space/mod.rs:1` - 模块组织. +- `os/src/mm/memory_space/mmap_file.rs:8` - 文件映射元数据. +- `os/src/mm/memory_space/mapping_area/mod.rs:16` - `MapType`. +- `os/src/mm/memory_space/mapping_area/mod.rs:33` - `AreaType`. +- `os/src/mm/memory_space/mapping_area/mod.rs:52` - `MappingArea`. +- `os/src/mm/memory_space/mapping_area/map_ops.rs:50` - 创建 VMA. +- `os/src/mm/memory_space/mapping_area/map_ops.rs:86` - 单页映射. +- `os/src/mm/memory_space/mapping_area/map_ops.rs:137` - 整区映射. +- `os/src/mm/memory_space/mapping_area/map_ops.rs:150` - 单页解除映射. +- `os/src/mm/memory_space/mapping_area/map_ops.rs:178` - 整区解除映射. +- `os/src/mm/memory_space/mapping_area/map_ops.rs:191` - 已映射区域数据复制. +- `os/src/mm/memory_space/mapping_area/split_ops.rs:4` - 元数据克隆. +- `os/src/mm/memory_space/mapping_area/split_ops.rs:26` - 帧映射深拷贝. +- `os/src/mm/memory_space/mapping_area/split_ops.rs:121` - VMA 拆分. +- `os/src/mm/memory_space/mapping_area/split_ops.rs:217` - 局部权限修改. +- `os/src/mm/memory_space/mapping_area/split_ops.rs:429` - 局部解除映射. +- `os/src/mm/memory_space/mapping_area/resize_ops.rs:8` - 尾部扩展. +- `os/src/mm/memory_space/mapping_area/resize_ops.rs:31` - 尾部收缩. +- `os/src/mm/memory_space/mapping_area/file_ops.rs:9` - 文件映射读入. +- `os/src/mm/memory_space/mapping_area/file_ops.rs:73` - 文件映射写回. + +## MemorySpace + +- `os/src/mm/memory_space/space/mod.rs:38` - 内核 token/root 辅助. +- `os/src/mm/memory_space/space/mod.rs:58` - `MemorySpace` 字段. +- `os/src/mm/memory_space/space/address_space.rs:3` - 空地址空间创建. +- `os/src/mm/memory_space/space/address_space.rs:52` - 创建带内核映射的用户空间. +- `os/src/mm/memory_space/space/address_space.rs:208` - 插入 VMA 并检查重叠. +- `os/src/mm/memory_space/space/address_space.rs:338` - fork 克隆. +- `os/src/mm/memory_space/space/address_space.rs:379` - drop 时写回文件映射. +- `os/src/mm/memory_space/space/kernel_space.rs:30` - 内核映射构建. +- `os/src/mm/memory_space/space/kernel_space.rs:175` - 创建内核空间. +- `os/src/mm/memory_space/space/kernel_space.rs:217` - 映射 MMIO 区域. +- `os/src/mm/memory_space/space/kernel_space.rs:284` - 解除 MMIO 映射. +- `os/src/mm/memory_space/space/elf_loader.rs:22` - ELF 装载. +- `os/src/mm/memory_space/space/mmap_ops.rs:10` - `brk`. +- `os/src/mm/memory_space/space/mmap_ops.rs:104` - 用户 mmap 空洞搜索. +- `os/src/mm/memory_space/space/mmap_ops.rs:205` - `mmap`. +- `os/src/mm/memory_space/space/mmap_ops.rs:291` - `munmap`. +- `os/src/mm/memory_space/space/mmap_ops.rs:375` - `mprotect`. diff --git a/document/mm/architecture.md b/document/mm/architecture.md index e083335a..d007f179 100644 --- a/document/mm/architecture.md +++ b/document/mm/architecture.md @@ -1,534 +1,113 @@ # MM 子系统整体架构 -## 概述 +MM 子系统采用三层结构: 类型和资源所有权在通用 MM 层, 硬件页表在架构层, 进程语义在 `MemorySpace` 层. -Comix 内核的 MM(Memory Management)子系统采用分层架构设计,将架构无关的通用抽象与架构特定的实现清晰分离。这种设计使得内核能够在不修改核心逻辑的情况下支持多种硬件架构(RISC-V、LoongArch 等)。 - -## 分层架构 - -### 架构层次图 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 应用层 │ -│ (系统调用: mmap/munmap/brk 等) │ -└─────────────────────────┬───────────────────────────────────┘ - │ -┌─────────────────────────┴───────────────────────────────────┐ -│ 架构无关层 (os/src/mm/) │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ memory_space/ - MemorySpace 地址空间管理 │ │ -│ │ - MappingArea 映射区域管理 │ │ -│ └────────────────┬─────────────────┬───────────────────┘ │ -│ │ │ │ -│ ┌────────────────▼─────────────┐ ┌▼──────────────────┐ │ -│ │ page_table/ │ │ frame_allocator/ │ │ -│ │ - PageTableInner trait │ │ - FrameAllocator │ │ -│ │ - PageTableEntry trait │ │ - FrameTracker │ │ -│ │ - UniversalPTEFlag │ └───────────────────┘ │ -│ └──────────────┬───────────────┘ │ -│ │ ┌──────────────────────┐ │ -│ ┌──────────────▼──────────────┐ │ global_allocator/ │ │ -│ │ address/ │ │ - talc Allocator │ │ -│ │ - Paddr/Vaddr │ └──────────────────────┘ │ -│ │ - Ppn/Vpn │ │ -│ │ - 地址运算 trait │ │ -│ └─────────────────────────────┘ │ -└─────────────────────────┬───────────────────────────────────┘ - │ -┌─────────────────────────┴───────────────────────────────────┐ -│ 架构特定层 (os/src/arch/{riscv,loongarch}/mm/) │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ - vaddr_to_paddr() / paddr_to_vaddr() │ │ -│ │ - PageTableInner 实现 (如 RISC-V SV39) │ │ -│ │ - PageTableEntry 实现 (如 SV39 PTE 格式) │ │ -│ └──────────────────────────────────────────────────────┘ │ -└─────────────────────────┬───────────────────────────────────┘ - │ -┌─────────────────────────┴───────────────────────────────────┐ -│ 硬件层 │ -│ (MMU, TLB, 物理内存, SATP/PGDH 寄存器等) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 各层职责 - -#### 1. 架构无关层 (os/src/mm/) - -提供通用的内存管理抽象,不包含任何架构特定代码。 - -**职责**: -- 定义统一的地址类型和页号类型 -- 提供物理帧分配和回收算法 -- 实现地址空间管理和映射区域管理 -- 定义页表操作的 trait 接口 -- 管理内核堆分配器 - -**关键设计**: -- 使用 trait 定义架构无关接口 -- 通过条件编译引用架构特定实现 -- 所有公共 API 均在此层暴露 - -#### 2. 架构特定层 (os/src/arch/{riscv,loongarch}/mm/) - -为特定硬件架构提供具体实现。 - -**职责**: -- 实现虚拟地址与物理地址的转换逻辑 -- 实现 PageTableInner trait(页表遍历、映射、TLB 管理等) -- 实现 PageTableEntry trait(PTE 标志位操作) -- 提供架构特定的常量和配置 - -**当前支持架构**: -- **RISC-V**:完整实现 SV39 三级页表 -- **LoongArch**:TODO(占位符已预留) - -## 模块依赖关系 - -``` - ┌──────────────┐ - │ mm/mod.rs │ - │ (初始化器) │ - └───────┬──────┘ - │ init() - ┌───────────────┼───────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌───────────┐ ┌──────────┐ ┌──────────────┐ - │frame_ │ │ global_ │ │memory_space │ - │allocator │ │allocator │ │(内核空间) │ - └───────────┘ └──────────┘ └──────┬───────┘ - │ │ - │ ┌────────────────────┘ - │ │ - ▼ ▼ - ┌─────────────────────────┐ - │ page_table/ │ - │ (PageTableInner trait) │ - └────────────┬────────────┘ - │ - ▼ - ┌─────────────────────────┐ - │ address/ │ - │ (Paddr/Vaddr/Ppn/Vpn) │ - └────────────┬────────────┘ - │ - ▼ - ┌─────────────────────────┐ - │ arch/*/mm/ │ - │ - vaddr_to_paddr() │ - │ - paddr_to_vaddr() │ - │ - PageTable 实现 │ - └─────────────────────────┘ -``` - -### 关键依赖路径 - -1. **内存分配路径**: - ``` - memory_space → mapping_area → frame_allocator → FrameTracker - ``` - -2. **地址转换路径**: - ``` - Vaddr/Paddr → arch::mm::{vaddr_to_paddr, paddr_to_vaddr} - ``` - -3. **页表操作路径**: - ``` - MemorySpace → PageTableInner trait → arch::mm::PageTableInner - ``` - -4. **初始化路径**: - ``` - mm::init() → frame_allocator::init() → global_allocator::init() → MemorySpace::new_kernel() - ``` - -## 初始化流程 - -### 启动序列 +## 分层设计 +```text +系统调用和进程管理 + | + v +MemorySpace + - VMA 列表 + - brk/mmap/munmap/mprotect + - ELF/fork/file/shared mapping + | + v +MappingArea + - 映射策略 + - 帧所有权 + - 文件和共享段元数据 + | + v +PageTableInner trait + UniversalPTEFlag + | + v +arch/riscv/mm 或 arch/loongarch/mm + - PTE 格式 + - 页表 walker + - activate + - TLB flush + | + v +FrameAllocator + talc heap + address types ``` -┌────────────────────────────────────────────────────────────┐ -│ 1. 内核入口 (rust_main) │ -└─────────────────────┬──────────────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────────────────────────┐ -│ 2. mm::init() │ -│ ├─ 获取可用物理内存范围 [ekernel, MEMORY_END) │ -│ ├─ 初始化物理帧分配器 │ -│ ├─ 初始化内核堆分配器 (talc) │ -│ └─ 创建并激活内核地址空间 │ -└─────────────────────┬──────────────────────────────────────┘ - │ - ┌───────────────┼───────────────┐ - │ │ │ - ▼ ▼ ▼ -┌──────────┐ ┌──────────────┐ ┌──────────────────┐ -│ 物理帧 │ │ 内核堆 │ │ 内核页表 │ -│ 分配器 │ │ 分配器 │ │ 创建与激活 │ -│ 就绪 │ │ 就绪 │ │ 就绪 │ -└──────────┘ └──────────────┘ └──────────────────┘ -``` - -### 详细步骤 - -#### 第一步:物理帧分配器初始化 - -```rust -// os/src/mm/mod.rs:36-41 -let ekernel_paddr = unsafe { vaddr_to_paddr(ekernel as usize) }; -let start = Ppn::from_addr_ceil(Paddr::new(ekernel_paddr)); -let end = Ppn::from_addr_floor(Paddr::new(MEMORY_END)); -init_frame_allocator(start, end); -``` - -**作用**: -- 计算内核结束地址到物理内存结束的可用区域 -- 初始化全局帧分配器 `FRAME_ALLOCATOR` -- 此时可以开始分配物理帧 - -#### 第二步:内核堆分配器初始化 - -```rust -// os/src/mm/mod.rs:44 -init_heap(); -``` - -**作用**: -- 初始化 talc 全局堆分配器 -- 注册内核堆区域 `[sheap, eheap)`(由链接脚本定义) -- 此时可以使用 `alloc` crate 进行动态内存分配(Vec、Box 等) - -#### 第三步:内核地址空间创建 - -```rust -// os/src/mm/mod.rs:47-51 -#[cfg(target_arch = "riscv64")] { - let root_ppn = with_kernel_space(|space| space.root_ppn()); - crate::arch::mm::PageTableInner::activate(root_ppn); -} -``` - -**作用**: -- 调用 `MemorySpace::new_kernel()` 创建内核地址空间 -- 映射内核各段(text/rodata/data/bss/heap) -- 直接映射所有物理内存到高半核 -- 写入 SATP 寄存器并刷新 TLB,启用分页 - -### 内核地址空间构建细节 - -``` -MemorySpace::new_kernel() 执行以下映射: - -1. 跳板页 (Trampoline) - [usize::MAX-PAGE_SIZE+1, usize::MAX+1) → 跳板代码物理页 - -2. 内核代码段 (.text) - [stext, etext) → 对应物理地址,权限: R+X - -3. 内核只读数据段 (.rodata) - [srodata, erodata) → 对应物理地址,权限: R - -4. 内核数据段 (.data) - [sdata, edata) → 对应物理地址,权限: R+W - -5. 内核栈段 (.bss.stack) - [boot_stack, boot_stack_top) → 对应物理地址,权限: R+W - -6. 内核 BSS 段 (.bss) - [sbss, ebss) → 对应物理地址,权限: R+W - -7. 内核堆段 - [sheap, eheap) → 对应物理地址,权限: R+W - -8. 直接映射物理内存 - [ekernel, MEMORY_END) → 对应物理地址,权限: R+W - (用于访问用户进程的物理页面) -``` - -## 架构抽象模式 - -### 1. 条件编译导出 - -通过 `#[cfg(target_arch = "...")]` 实现架构选择: - -```rust -// os/src/arch/mod.rs:6-10 -#[cfg(target_arch = "loongarch64")] -pub use self::loongarch::*; - -#[cfg(target_arch = "riscv64")] -pub use riscv::*; -``` - -### 2. Trait 接口契约 - -架构特定代码必须实现以下 trait: - -```rust -// PageTableInner trait (os/src/mm/page_table/page_table.rs:5-52) -pub trait PageTableInner { - const LEVELS: usize; // 页表级数(如 SV39 为 3) - const MAX_VA_BITS: usize; // 虚拟地址位宽(如 SV39 为 39) - const MAX_PA_BITS: usize; // 物理地址位宽(如 SV39 为 56) - - // TLB 管理 - fn tlb_flush(vpn: Vpn); - fn tlb_flush_all(); - - // 生命周期 - fn new() -> Self; - fn from_ppn(ppn: Ppn) -> Self; - fn activate(ppn: Ppn); - - // 核心操作 - fn map(&mut self, vpn: Vpn, ppn: Ppn, page_size: PageSize, - flags: UniversalPTEFlag) -> PagingResult<()>; - fn unmap(&mut self, vpn: Vpn) -> PagingResult<()>; - fn translate(&self, vaddr: Vaddr) -> Option; - // ...更多方法 -} -``` - -### 3. 通用标志位转换 - -通过 `UniversalPTEFlag` 实现架构无关的权限表示: - -```rust -// os/src/mm/page_table/page_table_entry.rs:4-11 -pub struct UniversalPTEFlag(u8); - -impl UniversalPTEFlag { - // 低 8 位兼容 RISC-V SV39 格式 - pub const V: Self = Self(1 << 0); // Valid - pub const R: Self = Self(1 << 1); // Readable - pub const W: Self = Self(1 << 2); // Writable - pub const X: Self = Self(1 << 3); // Executable - // ... -} -``` - -架构特定的 PTE 标志位通过 `UniversalConvertableFlag` trait 转换: - -```rust -// os/src/arch/riscv/mm/page_table_entry.rs:142-146 -impl UniversalConvertableFlag for SV39PTEFlags { - fn from_universal(flag: UniversalPTEFlag) -> Self { - Self::from_bits(flag.bits() & 0xff).unwrap() - } -} -``` - -### 4. 地址转换函数 - -每个架构必须提供地址转换函数: - -```rust -// RISC-V 实现 (os/src/arch/riscv/mm/mod.rs:8-15) -pub const VADDR_START: usize = 0xffff_ffc0_0000_0000; -pub const PADDR_MASK: usize = 0x0000_003f_ffff_ffff; - -pub const unsafe fn vaddr_to_paddr(vaddr: usize) -> usize { - vaddr & PADDR_MASK // 提取低 38 位 -} - -pub const fn paddr_to_vaddr(paddr: usize) -> usize { - paddr | VADDR_START // 添加高半核前缀 -} -``` - -## 关键设计决策 - -### 1. 为什么使用直接映射? - -**内核空间的直接映射设计**(物理地址 → 物理地址 + VADDR_START): - -**优势**: -- 访问物理内存无需页表查找,性能优秀 -- 简化内核代码,地址转换仅需位运算 -- 便于访问用户进程的物理页面(用于拷贝数据) - -**代价**: -- 需要占用较大的虚拟地址空间(高半核) -- 仅适用于 64 位架构 - -### 2. 为什么分离 Paddr 和 Vaddr? - -**类型安全设计**: - -```rust -#[repr(transparent)] -pub struct Paddr(usize); - -#[repr(transparent)] -pub struct Vaddr(usize); -``` - -**优势**: -- 编译期防止物理地址和虚拟地址混用 -- 明确表达函数参数的地址类型语义 -- 通过 `repr(transparent)` 保证零成本抽象 - -### 3. 为什么使用 RAII 管理物理帧? - -**FrameTracker 设计**: - -```rust -// os/src/mm/frame_allocator/frame_allocator.rs:24-35 -pub struct FrameTracker { - ppn: Ppn, -} - -impl Drop for FrameTracker { - fn drop(&mut self) { - dealloc_frame(self.ppn); - } -} -``` - -**优势**: -- 自动释放,防止内存泄漏 -- 配合 Rust 所有权系统,编译期检查 -- 支持通过 `clone()` 显式拷贝,避免意外共享 - -### 4. 为什么暂时禁用大页? - -**当前限制**: -- `PageSize::Size2M` 和 `PageSize::Size1G` 枚举存在但未启用 -- 映射区域的 extend/shrink 仅支持 4K 页 - -**原因**: -- 大页扩展/收缩逻辑复杂,需要拆分/合并页表项 -- 帧分配器需要支持对齐的大块分配 -- 需要更多测试验证正确性 - -**未来计划**: -- 代码中已预留大页相关逻辑(已注释) -- 完善后可启用,优化 TLB 性能 - -### 5. 为什么使用左闭右开区间? - -**Range 语义统一**: - -```rust -// AddressRange/PageNumRange 均为 [start, end) -let range = VpnRange::new(start_vpn, end_vpn); -// 包含 start_vpn,不包含 end_vpn -``` - -**优势**: -- 符合 Rust 标准库惯例(`a..b` 即 `[a, b)`) -- 便于计算长度:`len = end - start` -- 避免边界处理歧义 - -## 架构扩展指南 - -### 添加新架构支持 - -**步骤**: - -1. **创建架构目录**: - ``` - os/src/arch/{新架构}/mm/ - ├── mod.rs - ├── page_table.rs - └── page_table_entry.rs - ``` - -2. **实现地址转换函数**(`mod.rs`): - ```rust - pub const unsafe fn vaddr_to_paddr(vaddr: usize) -> usize; - pub const fn paddr_to_vaddr(paddr: usize) -> usize; - ``` -3. **实现 PageTableEntry trait**(`page_table_entry.rs`): - - 定义 PTE 结构体(如 `struct RV64PTE(u64)`) - - 实现 `PageTableEntry` trait 的所有方法 - - 实现 `UniversalConvertableFlag` 转换 +## 关键设计 -4. **实现 PageTableInner trait**(`page_table.rs`): - - 定义页表结构体(如 `struct PageTableInner`) - - 实现所有必需方法(map/unmap/translate/walk 等) - - 实现 TLB 管理和页表激活 +### 地址类型先于页表 -5. **更新架构选择器**(`os/src/arch/mod.rs`): - ```rust - #[cfg(target_arch = "新架构")] - pub use self::新架构::*; - ``` +MM 代码不直接传递裸整数表示地址.`PA`, `VA`, `UA`, `Ppn`, `Vpn` 把"这个数是什么"写进类型系统.这样页表, 帧分配器和地址空间管理可以共享对齐和范围语义. -6. **添加测试**: - - 单元测试验证地址转换正确性 - - 集成测试验证页表操作 +### 页表后端只做硬件映射 -### 注意事项 +页表后端负责从 VPN 到 PPN 的硬件可见映射, 不记录映射来自匿名页, 文件页还是共享内存.来源和所有权在 `MappingArea` 中维护. -- 确保 `MAX_VA_BITS` 和 `MAX_PA_BITS` 常量正确 -- TLB 刷新操作必须正确实现(错误可能导致诡异 bug) -- 大页支持可选,但需在 `is_huge()` 中正确检测 -- 参考 RISC-V 实现(`os/src/arch/riscv/mm/`)作为范例 +### VMA 是所有权边界 -## 性能考量 +`MappingArea` 同时记录虚拟范围和映射策略.对于 `Framed` 区域, 它还拥有物理帧 tracker.拆分, 解除映射和权限修改必须先处理 VMA 所有权, 再处理页表. -### 关键优化 +### 内核映射被复制到用户页表 -1. **帧分配器回收优化**: - - 回收时自动合并栈顶连续帧 - - 减少碎片,提高分配连续帧的成功率 +用户地址空间包含用户私有区域和内核共享区域.陷入内核时不需要切换到另一张内核页表, 但用户态不能访问 U=0 的内核映射. -2. **直接映射避免 TLB miss**: - - 内核访问物理内存时无需查页表 - - 减少 TLB 压力 +### PROT_NONE 不是无权限叶子 PTE -3. **BTreeMap 存储帧映射**: - - `MappingArea` 使用 `BTreeMap` - - O(log n) 查找性能,支持范围查询 +RISC-V 不接受没有 R/W/X 的普通叶子 PTE 作为可访问映射.当前设计用 `MapType::Reserved` 表示地址占位, 避免在硬件页表中创建这种条目. -4. **零拷贝地址转换**: - - `repr(transparent)` 确保地址类型无运行时开销 - - 地址转换函数标记为 `const` 和 `inline` +## 初始化顺序 -### 潜在瓶颈 +1. `mm::init()` 计算可用物理内存范围. +2. 初始化全局帧分配器. +3. 初始化 talc 堆. +4. 创建最终内核 `MemorySpace`. +5. 保存全局内核空间句柄. +6. 调用方在合适阶段激活根页表. -- **全局锁**:`FRAME_ALLOCATOR` 使用 `Mutex` 保护,高并发时可能成为瓶颈 -- **TLB 刷新**:频繁的 `unmap` 操作导致 TLB 失效 -- **页表遍历**:三级页表查找需要 3 次内存访问(可通过 TLB 缓存缓解) +这个顺序保证页表创建所需的物理帧已可分配, 堆分配器可服务后续动态结构, 多 CPU 最终共享 CPU0 建好的内核映射. -## 安全性 +## 架构扩展边界 -### 关键安全机制 +新增架构需要提供: -1. **类型系统防护**: - - 物理地址和虚拟地址类型隔离 - - 页表项权限通过 `UniversalPTEFlag` 显式指定 +- 地址类型和直接映射转换函数. +- `PageTableInner` 实现. +- `PageTableEntry` 实现. +- `UniversalPTEFlag` 到架构 PTE flags 的转换. +- 页表激活和 TLB 刷新机制. -2. **RAII 资源管理**: - - `FrameTracker` 自动释放物理帧 - - `MappingArea` 在 Drop 时自动取消映射 +通用层不应依赖某个后端的 CSR, PTE 位布局或 TLB 指令. -3. **所有权检查**: - - 页表所有权明确(`MemorySpace` 拥有页表) - - 帧所有权通过 `FrameTracker` 转移 +## 并发与生命周期 -4. **保护页机制**: - - 用户栈和 trap 上下文间插入未映射页 - - 栈溢出时触发缺页异常而非静默覆盖 +- 全局帧分配器由 `SpinLock` 保护. +- 全局堆由 `RawSpinLock` 保护. +- 全局内核空间句柄由 `SpinLock>>>` 保存. +- 进程地址空间由外层内核对象负责加锁; `MemorySpace` 修改路径自身不是无锁并发结构. +- 页表页和普通物理页都通过 RAII tracker 释放, 但它们的所有者不同: 页表后端拥有页表页, VMA 拥有用户数据页. -### 已知限制 +## 性能取舍 -- **Unsafe 代码**:地址转换函数标记为 `unsafe`,调用者需保证地址有效性 -- **直接映射风险**:内核可直接访问所有物理内存,需小心处理指针 -- **未实现 ASLR**:地址空间布局固定,存在安全隐患 +- 线性 VMA 列表实现简单, 但大量映射下查找成本高. +- fork 深拷贝简单可靠, 但比 COW 更耗时和耗内存. +- RISC-V TLB 批处理减少 IPI 数量, LoongArch64 仍是较保守的本地刷新. +- 连续物理帧分配不做复杂碎片整理, 保持实现简单. -## 参考资料 +## 已知限制 -- **RISC-V 特权架构规范**:SV39 页表格式定义 -- **LoongArch 架构手册**:待实现架构的参考 -- **Rust 嵌入式书**:裸机编程最佳实践 -- **xv6-riscv**:经典教学操作系统,内存管理参考 +- 大页, COW, VMA 树, per-CPU frame cache 和可增长内核堆都不是当前正式能力. +- LoongArch64 和 RISC-V 的 TLB shootdown 能力不完全对齐. +- 设备树 DRAM 信息缺失时仍会回退到编译期 `MEMORY_END`. -## 总结 +## 源码索引 -Comix 的 MM 子系统通过清晰的分层架构和 trait 抽象,实现了高度模块化和可扩展的设计。架构无关层提供统一接口,架构特定层提供硬件适配,二者通过 trait 系统和条件编译无缝集成。该设计既保证了代码的可维护性,又为未来扩展(如 LoongArch 支持、大页功能)奠定了坚实基础。 +- `os/src/mm/mod.rs:36` - 初始化编排. +- `os/src/mm/address/` - 地址与页号类型系统. +- `os/src/mm/frame_allocator/allocator.rs:100` - 帧分配器状态. +- `os/src/mm/global_allocator/talc_alloc.rs:22` - talc 堆. +- `os/src/mm/page_table/inner.rs:8` - 页表 trait 边界. +- `os/src/mm/memory_space/mapping_area/mod.rs:52` - VMA 所有权边界. +- `os/src/mm/memory_space/space/address_space.rs:338` - fork 克隆策略. +- `os/src/mm/memory_space/space/kernel_space.rs:30` - 内核映射策略. +- `os/src/arch/riscv/mm/page_table.rs:21` - RISC-V 页表后端. +- `os/src/arch/loongarch/mm/page_table.rs:34` - LoongArch64 页表后端. diff --git a/document/mm/frame_allocator.md b/document/mm/frame_allocator.md index fbfdea66..a8e677df 100644 --- a/document/mm/frame_allocator.md +++ b/document/mm/frame_allocator.md @@ -1,441 +1,71 @@ # 物理帧分配器 -## 概述 +物理帧分配器管理可分配 DRAM 的页帧, 并把分配结果包装成 RAII tracker.它是页表页, 用户页, 内核按需页和连续 DMA 缓冲区的基础来源. -物理帧分配器(Frame Allocator)负责管理可用物理内存页面(帧)的分配和回收。采用**水位线 + 回收栈**的混合策略,平衡了分配效率和内存利用率。 +## 当前状态 -### 设计目标 +- 全局分配器是 `FRAME_ALLOCATOR: SpinLock`. +- 初始化范围来自 `mm::init()` 计算出的 `[start_ppn, end_ppn)`. +- 单页分配优先使用回收栈, 回收栈为空时从水位线 `cur` 向上分配. +- 连续页分配只从水位线之后的连续区域分配, 不在回收栈中做复杂拼接. +- `FrameTracker` 和 `FrameRangeTracker` 在创建时清零物理页, 在 drop 时自动归还. -1. **高效分配**:O(1) 时间复杂度分配单帧和连续帧 -2. **自动回收**:通过 RAII 机制防止内存泄漏 -3. **减少碎片**:回收栈自动合并连续帧 -4. **支持对齐**:满足 DMA 等场景的对齐需求 +## 目标 -### 核心组件 +- 提供早期内核可用的简单物理帧管理. +- 用所有权表达"谁负责归还物理页". +- 支持单页, 多个非连续页, 连续页和带页数对齐的连续页. -- **FrameAllocator**:全局分配器,管理物理帧池 -- **FrameTracker**:单帧 RAII 包装器 -- **FrameRangeTracker**:连续帧 RAII 包装器 -- **TrackedFrames**:统一的帧枚举类型 +## 非目标 -## 分配器原理 +- 不提供伙伴系统或 slab 级别的长期碎片治理. +- 不维护每页引用计数, 也不实现 COW. +- 不处理 NUMA, zone, DMA mask 或 cache attribute. -### 数据结构 +## 关键流程 -```rust -pub struct FrameAllocator { - start: Ppn, // 可分配区域起始页号 - end: Ppn, // 可分配区域结束页号(左闭右开) - cur: Ppn, // 当前分配水位线 - recycled: Vec, // 回收栈(按升序存储已释放的页号) -} -``` +### 初始化 -### 分配策略 +`init_frame_allocator(start_addr, end_addr)` 把物理地址转换成页号范围: -``` -物理内存布局: +- 起点用 ceil, 避免分配覆盖内核镜像尾部的非完整页. +- 终点用 floor, 避免分配超出可用物理内存尾部的非完整页. -0x8000_0000 MEMORY_END - │ │ - ▼ ▼ - ┌──────────────┬───────────────────────────────┬─────┐ - │ 内核占用 │ 可分配区域 [start, end) │未用 │ - └──────────────┴───────────────────────────────┴─────┘ - ↑ ↑ ↑ - start cur end +### 单页分配 -分配顺序: -1. 优先从回收栈分配(LIFO) -2. 回收栈为空时从水位线分配 -3. 水位线递增 -``` +1. 先从 `recycled` 弹出被归还的页. +2. 如果没有可回收页, 且 `cur < end`, 使用 `cur` 并前移水位线. +3. 新 tracker 创建时清零整页. +4. 无页可用时返回 `None`. -### 回收优化 +### 回收与合并 -回收时自动检测并合并栈顶连续帧: +归还页会被加入 `recycled` 并排序.如果回收栈末尾正好贴近 `cur`, 分配器会把连续尾部并回水位线, 让未来连续分配重新利用这段空间. -``` -场景:按相反顺序释放连续帧 +### 连续分配 -初始: cur = 105, recycled = [] +连续分配只检查当前水位线之后是否有足够页.带对齐的连续分配会把水位线向上对齐, 中间跳过的页加入回收栈. -1. dealloc_frame(104): - recycled = [104] - 104 + 1 == 105 (cur) → 合并! - recycled = [], cur = 104 +## 并发与生命周期约束 -2. dealloc_frame(103): - recycled = [103] - 103 + 1 == 104 (cur) → 合并! - recycled = [], cur = 103 +- 全局入口都通过 `FRAME_ALLOCATOR.lock()` 串行化. +- tracker 不应被 `mem::forget` 泄漏, 否则对应物理页不会回收. +- `Ppn` 不是所有权凭据.只有 tracker 或拥有 tracker 的结构可以决定何时释放页. +- `MappingArea` 中的帧所有权必须随 VMA split/unmap/mprotect 一起移动或释放. -最终: cur = 102, recycled = [] // 完全回收 -``` +## 已知限制 -## 核心 API +- 回收栈排序是简单实现, 在大量回收时成本会升高. +- 连续分配不从回收栈组合碎片, 长时间运行后连续大块可能更难获得. +- 调试检查依赖 `debug_assert!`, release 构建下不会阻止错误归还. -### 单帧分配 +## 源码索引 -```rust -// 分配单个物理帧 -let frame = alloc_frame()?; -let ppn = frame.ppn(); - -// 访问帧内存 -let bytes = frame.as_slice_mut::(); -bytes[0] = 0xff; - -// FrameTracker 离开作用域时自动释放 -``` - -### 多帧分配(非连续) - -```rust -// 分配 5 个帧(可能非连续) -let frames = alloc_frames(5)?; - -for frame in &frames { - println!("Allocated PPN: {:#x}", frame.ppn().as_usize()); -} -// frames 离开作用域时批量释放 -``` - -### 连续帧分配 - -```rust -// 分配 256 个连续帧(1MB) -let contig = alloc_contig_frames(256)?; - -assert_eq!(contig.len(), 256); -let start_ppn = contig.start_ppn(); -let end_ppn = contig.end_ppn(); // 左闭右开 - -// 遍历连续帧 -for ppn in contig.iter() { - println!("PPN: {:#x}", ppn.as_usize()); -} -``` - -### 对齐连续帧分配 - -```rust -// 分配 512 个 4KB 页(2MB),起始地址 2MB 对齐 -let ppn_per_2mb = 512; -let huge_page = alloc_contig_frames_aligned(512, ppn_per_2mb)?; - -// 验证对齐 -assert_eq!(huge_page.start_ppn().as_usize() % ppn_per_2mb, 0); -``` - -## RAII 机制 - -### FrameTracker - -单帧的 RAII 包装器,离开作用域时自动释放: - -```rust -pub struct FrameTracker { - ppn: Ppn, -} - -impl FrameTracker { - pub fn ppn(&self) -> Ppn { self.ppn } - - // 访问帧内存 - pub fn as_slice(&self) -> &[T] { /* ... */ } - pub fn as_slice_mut(&mut self) -> &mut [T] { /* ... */ } -} - -impl Drop for FrameTracker { - fn drop(&mut self) { - dealloc_frame(self.ppn); // 自动释放 - } -} - -impl Clone for FrameTracker { - fn clone(&self) -> Self { - // 克隆时分配新帧并拷贝内容 - alloc_frame().unwrap() - } -} -``` - -**使用示例**: - -```rust -{ - let frame = alloc_frame()?; - // 使用 frame -} // 自动释放 - -// 避免过早释放 -fn wrong_usage() -> Result<(), FrameAllocError> { - // ❌ 错误:过早释放 - let ppn = { - let frame = alloc_frame()?; - frame.ppn() - }; // frame 被释放 - page_table.map(vpn, ppn, ...)?; // 映射已释放的帧! - - Ok(()) -} - -fn correct_usage() -> Result<(), FrameAllocError> { - // ✅ 正确:延长生命周期 - let frame = alloc_frame()?; - let ppn = frame.ppn(); - page_table.map(vpn, ppn, ...)?; - frames.push(frame); // 存储以保持所有权 - - Ok(()) -} -``` - -### FrameRangeTracker - -连续帧的 RAII 包装器: - -```rust -pub struct FrameRangeTracker { - start_ppn: Ppn, - end_ppn: Ppn, // 左闭右开 -} - -impl FrameRangeTracker { - pub fn start_ppn(&self) -> Ppn { self.start_ppn } - pub fn end_ppn(&self) -> Ppn { self.end_ppn } - pub fn len(&self) -> usize { /* ... */ } - - // 迭代所有页号 - pub fn iter(&self) -> impl Iterator { /* ... */ } -} - -impl Drop for FrameRangeTracker { - fn drop(&mut self) { - // 批量释放所有连续帧 - for ppn in self.iter() { - dealloc_frame(ppn); - } - } -} -``` - -### TrackedFrames 枚举 - -统一的帧枚举类型,用于映射区域: - -```rust -pub enum TrackedFrames { - Single(FrameTracker), - Multiple(Vec), - Contiguous(FrameRangeTracker), -} - -impl TrackedFrames { - pub fn count(&self) -> usize { - match self { - Self::Single(_) => 1, - Self::Multiple(v) => v.len(), - Self::Contiguous(r) => r.len(), - } - } - - pub fn ppns(&self) -> impl Iterator + '_ { - match self { - Self::Single(f) => /* ... */, - Self::Multiple(v) => /* ... */, - Self::Contiguous(r) => r.iter(), - } - } -} -``` - -**使用场景**: - -```rust -// MappingArea 中存储不同类型的帧 -pub struct MappingArea { - vpn_range: VpnRange, - frames: BTreeMap, // 灵活存储 - // ... -} - -impl MappingArea { - pub fn push_single(&mut self, vpn: Vpn) { - let frame = alloc_frame().unwrap(); - self.frames.insert(vpn, TrackedFrames::Single(frame)); - } - - pub fn push_contig(&mut self, vpn_range: VpnRange) { - let contig = alloc_contig_frames(vpn_range.len()).unwrap(); - let start_vpn = vpn_range.start(); - self.frames.insert(start_vpn, TrackedFrames::Contiguous(contig)); - } -} -``` - -## 初始化 - -```rust -// os/src/mm/mod.rs:36-41 -pub fn init() { - // 计算可用物理内存范围 - let ekernel_paddr = unsafe { vaddr_to_paddr(ekernel as usize) }; - let start = Ppn::from_addr_ceil(Paddr::new(ekernel_paddr)); - let end = Ppn::from_addr_floor(Paddr::new(MEMORY_END)); - - // 初始化全局帧分配器 - init_frame_allocator(start, end); -} -``` - -## 错误处理 - -```rust -#[derive(Debug)] -pub enum FrameAllocError { - OutOfMemory, // 物理内存耗尽 - InvalidAddress, // 地址无效 - AlignmentError, // 对齐错误 -} - -pub type FrameAllocResult = Result; -``` - -**常见错误场景**: - -```rust -// OutOfMemory:物理内存耗尽 -match alloc_frame() { - Ok(frame) => { /* 使用 frame */ }, - Err(FrameAllocError::OutOfMemory) => { - panic!("Physical memory exhausted!"); - } -} - -// AlignmentError:对齐值不是 2 的幂 -let result = alloc_contig_frames_aligned(10, 15); // 15 不是 2 的幂 -assert!(matches!(result, Err(FrameAllocError::AlignmentError))); -``` - -## 使用场景 - -### 场景 1:页表创建 - -```rust -// 分配页表根页面 -let root_frame = alloc_frame()?; -let root_ppn = root_frame.ppn(); - -// 初始化页表 -let page_table = PageTableInner::from_ppn(root_ppn); - -// root_frame 需要保持所有权,直到页表销毁 -``` - -### 场景 2:用户程序加载 - -```rust -pub fn load_elf(&mut self, elf_data: &[u8]) -> Result<(), ElfError> { - let elf = xmas_elf::ElfFile::new(elf_data)?; - - for ph in elf.program_iter() { - if ph.get_type() != ProgramHeaderType::Load { - continue; - } - - let start_vpn = Vpn::from_addr_floor(Vaddr::new(ph.virtual_addr() as usize)); - let end_vpn = Vpn::from_addr_ceil(Vaddr::new( - (ph.virtual_addr() + ph.mem_size()) as usize - )); - - // 为每个页分配物理帧 - for vpn in VpnRange::new(start_vpn, end_vpn) { - let frame = alloc_frame()?; - let ppn = frame.ppn(); - - // 映射 - self.page_table.map(vpn, ppn, PageSize::Size4K, flags)?; - - // 拷贝数据 - let dst = ppn.start_addr().to_vaddr().as_usize() as *mut u8; - // ... - - // 存储 frame 以保持所有权 - self.frames.insert(vpn, TrackedFrames::Single(frame)); - } - } - - Ok(()) -} -``` - -### 场景 3:DMA 缓冲区 - -```rust -// 分配 4MB DMA 缓冲区(1024 个 4KB 页,4MB 对齐) -let dma_pages = 1024; -let alignment = 1024; // 4MB = 1024 * 4KB - -let dma_buffer = alloc_contig_frames_aligned(dma_pages, alignment)?; - -// 传递物理地址给 DMA 控制器 -let dma_paddr = dma_buffer.start_ppn().start_addr(); -configure_dma(dma_paddr.as_usize()); -``` - -## 常见陷阱 - -### 陷阱 1:忘记 forget - -```rust -// ❌ 错误:重复释放 -pub fn manual_dealloc(frame: FrameTracker) { - dealloc_frame(frame.ppn()); - // frame Drop 时会再次释放! -} - -// ✅ 正确:手动释放后 forget -pub fn manual_dealloc(frame: FrameTracker) { - dealloc_frame(frame.ppn()); - core::mem::forget(frame); // 防止 Drop -} -``` - -### 陷阱 2:过早释放 - -参见前文 FrameTracker 使用示例。 - -### 陷阱 3:Clone 语义误解 - -```rust -// Clone 会分配新帧并拷贝内容 -let frame1 = alloc_frame()?; -let frame2 = frame1.clone(); // 分配新帧! - -assert_ne!(frame1.ppn(), frame2.ppn()); // 不同的物理帧 -``` - -## 调试技巧 - -```rust -// 查看分配器状态 -with_frame_allocator(|allocator| { - println!("Total frames: {}", allocator.end.as_usize() - allocator.start.as_usize()); - println!("Allocated: {}", allocator.cur.as_usize() - allocator.start.as_usize()); - println!("Recycled: {}", allocator.recycled.len()); -}); -``` - -## 相关文档 - -- [地址抽象层](address.md) - Ppn/Paddr 类型 -- [整体架构](architecture.md) - MM 子系统架构 -- [页表抽象层](page_table.md) - 帧的映射使用 -- [API 参考](api_reference.md) - 完整 API 列表 - -## 参考实现 - -- **源代码**:`os/src/mm/frame_allocator/` -- **初始化**:`os/src/mm/mod.rs:36-41` +- `os/src/mm/frame_allocator/mod.rs:15` - 公共分配入口. +- `os/src/mm/frame_allocator/allocator.rs:14` - `FrameTracker`. +- `os/src/mm/frame_allocator/allocator.rs:45` - `FrameRangeTracker`. +- `os/src/mm/frame_allocator/allocator.rs:96` - 全局 `FRAME_ALLOCATOR`. +- `os/src/mm/frame_allocator/allocator.rs:100` - `FrameAllocator` 状态. +- `os/src/mm/frame_allocator/allocator.rs:124` - 单页分配策略. +- `os/src/mm/frame_allocator/allocator.rs:163` - 连续帧分配. +- `os/src/mm/frame_allocator/allocator.rs:222` - 回收与尾部合并. diff --git a/document/mm/global_allocator.md b/document/mm/global_allocator.md index 726d63d5..b719badb 100644 --- a/document/mm/global_allocator.md +++ b/document/mm/global_allocator.md @@ -1,237 +1,49 @@ # 全局堆分配器 -## 概述 +全局堆分配器为 `alloc` 生态提供内核动态内存.当前实现使用 `talc` 的 `Talck` 包装器, 锁类型是内核自己的 `RawSpinLock`. -全局堆分配器为内核提供动态内存分配能力,支持 Rust 标准库的 `alloc` crate(Vec、Box、String 等)。Comix 使用 **talc** 作为全局分配器实现。 +## 当前状态 -### 为什么选择 talc? +- 只有启用 `alloc` feature 时编译 `global_allocator` 实现. +- `#[global_allocator]` 是 `Talck`. +- 初始 span 为空, `init_heap()` 在启动阶段用链接器符号 `sheap` 和 `eheap` claim 真实堆区. +- `RawSpinLock` 实现 `lock_api::RawMutex`, 因此 talc 可以通过同一套锁协议保护内部元数据. -- **无锁设计**:单核环境下性能优秀 -- **零依赖**:适合裸机环境 -- **灵活配置**:支持多种分配策略 -- **稳定性好**:经过充分测试 +## 目标 -## 实现 +- 在 `no_std` 内核环境中支持 `Vec`, `Box`, `Arc`, `BTreeMap` 等动态分配. +- 复用 `RawSpinLock` 的中断保护, 避免在本地中断重入分配器时破坏 allocator 状态. +- 把堆区边界交给链接器脚本统一定义. -### 全局分配器定义 +## 非目标 -```rust -use talc::{Talc, Span}; +- 不提供用户态堆.用户态 `brk` 和 `mmap` 由 `MemorySpace` 管理. +- 不提供 per-CPU cache 或 slab allocator. +- 不把 OOM 恢复策略放在 MM 文档中展开.具体行为以 allocator 与 panic/OOM handler 源码为准. -#[global_allocator] -static ALLOCATOR: Talck, ClaimOnOom> = Talc::new(unsafe { - ClaimOnOom::new(Span::empty()) -}).lock(); -``` +## 初始化流程 -### 初始化流程 +1. `mm::init()` 在物理帧分配器初始化后调用 `init_heap()`. +2. `init_heap()` 读取 `sheap` 和 `eheap`. +3. 使用 talc `claim()` 把这段连续虚拟地址区声明为可分配堆. +4. 后续所有 `alloc` 分配通过全局 allocator 进入 talc. -```rust -// os/src/mm/global_allocator/global_allocator.rs:30-40 -pub fn init_heap() { - extern "C" { - fn sheap(); - fn eheap(); - } +## 并发与生命周期约束 - let heap_start = sheap as usize; - let heap_size = eheap as usize - heap_start; +- `init_heap()` 必须早于任何堆分配. +- `init_heap()` 只应在启动阶段调用一次. +- allocator 的锁保护范围应尽量短.持有 allocator 锁时不应主动触发可能再次分配的复杂路径. +- 中断保护只能解决本 CPU 中断重入问题, 跨 CPU 互斥仍由 `RawSpinLock` 原子状态保证. - unsafe { - ALLOCATOR - .lock() - .claim(Span::new(heap_start as *mut u8, heap_start + heap_size as *mut u8)) - .expect("Failed to initialize heap"); - } -} -``` +## 已知限制 -**链接脚本定义**(`linker.ld`): +- 堆大小固定由链接器脚本给出, 当前没有向物理帧分配器动态扩容的路径. +- 没有独立的堆统计和碎片观测接口. +- 长时间持锁分配会影响中断延迟, 调用方应避免在中断上下文做复杂分配. -```ld -sheap = .; -. = . + 16M; // KERNEL_HEAP_SIZE = 16MB -eheap = .; -``` +## 源码索引 -### 内存布局 - -``` -内核虚拟地址空间: - -0xFFFF_FFC0_8020_0000 ← 内核加载地址 - ↓ -[.text] -[.rodata] -[.data] -[.bss] - ↓ -sheap ──────────┐ - │ - [Heap] │ 16MB(KERNEL_HEAP_SIZE) - │ -eheap ──────────┘ - ↓ -[物理内存直接映射区] -``` - -## 基本使用 - -### Vec 动态数组 - -```rust -use alloc::vec::Vec; - -let mut v = Vec::new(); -for i in 0..100 { - v.push(i); -} -assert_eq!(v.len(), 100); -``` - -### Box 堆分配 - -```rust -use alloc::boxed::Box; - -// 分配单个值 -let b = Box::new(42); -assert_eq!(*b, 42); - -// 分配数组 -let arr = Box::new([0u8; 4096]); -``` - -### String 字符串 - -```rust -use alloc::string::String; - -let mut s = String::from("Hello, "); -s.push_str("Comix!"); -assert_eq!(s, "Hello, Comix!"); -``` - -### BTreeMap 有序映射 - -```rust -use alloc::collections::BTreeMap; - -let mut map = BTreeMap::new(); -map.insert("key1", "value1"); -map.insert("key2", "value2"); - -assert_eq!(map.get("key1"), Some(&"value1")); -``` - -## OOM 处理 - -当堆内存耗尽时,`alloc_error_handler` 会被调用: - -```rust -// os/src/mm/global_allocator/global_allocator.rs:50-55 -#[alloc_error_handler] -fn alloc_error_handler(layout: core::alloc::Layout) -> ! { - panic!( - "Heap allocation failed: size = {}, align = {}", - layout.size(), - layout.align() - ); -} -``` - -## 常见错误 - -### 错误 1:未初始化就使用 - -```rust -// ❌ 错误:在 mm::init() 之前使用 alloc -pub fn rust_main() { - let v = Vec::new(); // panic: heap not initialized! - mm::init(); -} - -// ✅ 正确:先初始化 -pub fn rust_main() { - mm::init(); - let v = Vec::new(); // OK -} -``` - -### 错误 2:堆溢出 - -```rust -// 内核堆仅 16MB,注意避免过大分配 -let huge_vec: Vec = Vec::with_capacity(32 * 1024 * 1024); // OOM! -``` - -**建议**: -- 大块内存使用物理帧分配器 -- 控制动态数据结构的增长 -- 必要时增加 `KERNEL_HEAP_SIZE` - -### 错误 3:忘记 no_std 环境 - -```rust -// ❌ 错误:std::vec 在 no_std 中不可用 -use std::vec::Vec; // 编译错误! - -// ✅ 正确:使用 alloc::vec -extern crate alloc; -use alloc::vec::Vec; -``` - -## 调试技巧 - -### 追踪堆分配 - -使用静态计数器追踪分配/释放: - -```rust -use core::sync::atomic::{AtomicUsize, Ordering}; - -static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0); - -// 在分配器中 -unsafe impl GlobalAlloc for MyAllocator { - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - ALLOC_COUNT.fetch_add(1, Ordering::Relaxed); - // ... - } - - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - ALLOC_COUNT.fetch_sub(1, Ordering::Relaxed); - // ... - } -} - -// 检查内存泄漏 -assert_eq!(ALLOC_COUNT.load(Ordering::Relaxed), 0, "Memory leak detected!"); -``` - -## 性能考量 - -### talc 特点 - -- **分配策略**:First-fit with splitting -- **时间复杂度**:O(n)(n 为空闲块数量) -- **碎片管理**:自动合并相邻空闲块 -- **锁开销**:使用 `spin::Mutex`,单核下性能优秀 - -### 优化建议 - -1. **批量分配**:使用 `Vec::with_capacity` 预分配 -2. **对象池**:频繁分配/释放的对象考虑使用对象池 -3. **栈优先**:小对象优先使用栈分配 - -## 相关文档 - -- [物理帧分配器](frame_allocator.md) - 大块内存分配 -- [整体架构](architecture.md) - MM 子系统初始化流程 -- [配置常量](../../os/src/config.rs) - KERNEL_HEAP_SIZE - -## 参考资料 - -- **talc 文档**:https://docs.rs/talc -- **GlobalAlloc trait**:https://doc.rust-lang.org/core/alloc/trait.GlobalAlloc.html -- **源代码**:`os/src/mm/global_allocator/` +- `os/src/mm/global_allocator/mod.rs:1` - feature gated 模块导出. +- `os/src/mm/global_allocator/talc_alloc.rs:11` - `RawSpinLock` 作为 allocator lock. +- `os/src/mm/global_allocator/talc_alloc.rs:22` - 全局 `Talck`. +- `os/src/mm/global_allocator/talc_alloc.rs:37` - `init_heap()` 读取链接器堆边界并 claim span. diff --git a/document/mm/memory_space.md b/document/mm/memory_space.md index 2c0dbbcd..fed37832 100644 --- a/document/mm/memory_space.md +++ b/document/mm/memory_space.md @@ -1,448 +1,130 @@ # 地址空间管理 -## 概述 +地址空间管理是 MM 子系统的策略层.它把页表后端, 物理帧所有权, VMA 元数据和系统调用语义组合成 `MemorySpace`. -地址空间管理是 MM 子系统的最高抽象层,负责管理整个虚拟地址空间的布局、映射区域和页表操作。每个进程拥有独立的虚拟地址空间,支持内核和用户态的内存隔离。 +## 当前状态 -### 设计目标 +- `MemorySpace` 持有一个 `ActivePageTableInner`, 一个 `Vec` 和用户堆起点 `heap_start`. +- `MappingArea` 持有 VMA 范围, 区域类型, 映射策略, 权限, 私有帧, 文件映射信息和共享内存段信息. +- 映射策略包括 `Direct`, `Framed`, `Reserved`, `Shared`. +- `memory_space` 当前按职责拆成: + - `mapping_area/mod.rs` - VMA 元数据类型. + - `mapping_area/map_ops.rs` - 单页/整区映射和复制数据. + - `mapping_area/split_ops.rs` - fork 克隆, split, mprotect 局部修改, munmap 局部解除. + - `mapping_area/resize_ops.rs` - brk 场景下尾部扩缩. + - `mapping_area/file_ops.rs` - 文件映射加载和脏页写回. + - `space/address_space.rs` - `MemorySpace` 基本操作和 fork. + - `space/kernel_space.rs` - 内核地址空间和 MMIO 映射. + - `space/elf_loader.rs` - ELF 用户程序装载. + - `space/mmap_ops.rs` - `brk`, `mmap`, `munmap`, `mprotect`. -1. **地址空间隔离**:每个进程拥有独立的虚拟地址空间 -2. **灵活的内存布局**:支持代码段、数据段、堆、栈等多种区域 -3. **按需分配**:延迟分配物理内存,节省资源 -4. **系统调用支持**:实现 brk、mmap、munmap 等内存管理系统调用 +## 目标 -## 核心结构 +- 用 VMA 记录虚拟地址空间布局, 用页表记录硬件映射. +- 支持内核共享映射和用户私有映射共存. +- 为 `brk`, `mmap`, `munmap`, `mprotect`, ELF 加载和 fork 提供一致的区域操作. +- 让帧所有权跟随 VMA 生命周期自动释放. -### MemorySpace +## 非目标 -```rust -pub struct MemorySpace { - page_table: ActivePageTableInner, // 页表(管理虚拟→物理映射) - areas: Vec, // 映射区域列表 - heap_top: Option, // 用户堆顶(brk) -} -``` +- 不实现完整 Linux VMA 红黑树或 mmap policy. +- fork 不是 COW, `Framed` 区域会深拷贝. +- 不在文档中列出所有 syscall 参数检查和错误分支, 这些细节看 rustdoc 和源码. -### MappingArea +## 映射策略 -```rust -pub struct MappingArea { - vpn_range: VpnRange, // 虚拟页号范围 [start, end) - area_type: AreaType, // 区域类型(代码/数据/堆/栈) - map_type: MapType, // 映射策略(Direct/Framed) - permission: UniversalPTEFlag, // 权限标志(R/W/X/U) - frames: BTreeMap, // 物理帧映射(Framed 类型使用) -} -``` +### Direct -## 映射策略 +用于内核直接映射和内核段映射.`MappingArea` 不持有物理帧, 映射时从虚拟地址通过架构直接映射函数得到物理页. + +### Framed + +用于用户私有页和匿名映射.每个虚拟页分配 `FrameTracker`, tracker 放在 `MappingArea.frames` 中.VMA 被移除或拆分时, tracker 的所有权同步移动或释放. + +### Reserved + +用于占位但不可访问的区域, 例如 `PROT_NONE`.这种区域参与 VMA 重叠检查, 但不建立叶子 PTE. -### Direct 直接映射 - -用于内核空间,虚拟地址直接对应物理地址: - -``` -虚拟地址 物理地址 -0xFFFF_FFC0_8000_0000 ←→ 0x8000_0000 -0xFFFF_FFC0_8000_1000 ←→ 0x8000_1000 - -特点: -✓ 无需分配物理帧 -✓ 访问物理内存无需查页表 -✓ 仅用于内核空间 -``` - -```rust -// 实现 -for vpn in self.vpn_range { - let vaddr = vpn.start_addr(); - let paddr = vaddr.to_paddr(); - let ppn = Ppn::from_addr_floor(paddr); - page_table.map(vpn, ppn, PageSize::Size4K, self.permission)?; -} -``` - -### Framed 帧映射 - -用于用户空间,每个虚拟页分配独立的物理帧: - -``` -虚拟页号 物理页号 -VPN 0x1000 ←→ PPN 0x8234_5 (分配) -VPN 0x1001 ←→ PPN 0x8456_7 (分配) - -特点: -✓ 每个虚拟页分配独立物理帧 -✓ 物理内存可能不连续 -✓ 自动管理物理帧生命周期(RAII) -``` - -```rust -// 实现 -for vpn in self.vpn_range { - let frame = alloc_frame()?; - let ppn = frame.ppn(); - page_table.map(vpn, ppn, PageSize::Size4K, self.permission)?; - self.frames.insert(vpn, TrackedFrames::Single(frame)); -} -``` - -## 地址空间创建 +### Shared + +用于 SysV shared memory.VMA 保存共享段引用和段内页偏移, 映射时从共享段取 PPN, 不拥有私有帧. + +## 关键流程 ### 内核地址空间 -```rust -pub fn new_kernel() -> Self { - let mut space = Self { - page_table: ActivePageTableInner::new(), - areas: Vec::new(), - heap_top: None, - }; - - // 1. 映射跳板页 - space.map_trampoline(); - - // 2. 映射内核各段(Direct 映射) - space.map_kernel_text(); // .text R+X - space.map_kernel_rodata(); // .rodata R - space.map_kernel_data(); // .data R+W - space.map_kernel_bss(); // .bss R+W - space.map_kernel_heap(); // heap R+W - - // 3. 直接映射物理内存 - space.map_physical_memory(); - - space -} -``` - -**内核段映射示例**: - -```rust -// .text 段(只读可执行) -let text_start = Vaddr::new(stext as usize); -let text_end = Vaddr::new(etext as usize); -space.push(MappingArea::new( - VaddrRange::new(text_start, text_end), - MapType::Direct, - UniversalPTEFlag::kernel_r() | UniversalPTEFlag::X, - AreaType::KernelText, -)); -``` +`MemorySpace::new_kernel()` 创建空页表后调用 `map_kernel_space()`: + +1. 按链接器符号映射 `.text`, `.rodata`, `.data`, `.bss.stack`, `.bss`, 堆. +2. 直接映射可用物理内存范围.RISC-V 覆盖设备树 DRAM, LoongArch64 对页表直映射窗口做 1GiB cap. +3. RISC-V 在需要时额外确保 DTB 所在页可访问. +4. MMIO 自动映射目前不是默认路径, 显式 MMIO 映射通过 `map_mmio` 系列接口管理. ### 用户地址空间 -```rust -pub fn from_elf(elf_data: &[u8]) -> Self { - let mut space = Self { - page_table: ActivePageTableInner::new(), - areas: Vec::new(), - heap_top: None, - }; - - // 1. 解析 ELF 文件,映射各段(Framed 映射) - let elf = xmas_elf::ElfFile::new(elf_data).unwrap(); - for program_header in elf.program_iter() { - if program_header.get_type() == Ok(xmas_elf::program::Type::Load) { - // 创建映射区域 - let area = MappingArea::new( - vaddr_range, - MapType::Framed, - permission, - area_type, - ); - // 拷贝数据到物理页 - area.copy_data(&space.page_table, program_header.get_data(&elf).unwrap()); - space.areas.push(area); - } - } - - // 2. 映射用户栈 - space.map_user_stack(); - - // 3. 映射 trap 上下文 - space.map_trap_context(); - - // 4. 初始化堆 - space.heap_top = Some(space.infer_heap_start()); - - space -} -``` - -## 系统调用支持 - -### brk - 堆扩展 - -```rust -pub fn brk(&mut self, new_end: Vaddr) -> SyscallResult { - let new_end_vpn = Vpn::from_addr_ceil(new_end); - let current_end_vpn = self.heap_top.unwrap(); - - if new_end_vpn > current_end_vpn { - // 扩展堆 - let heap_area = self.find_heap_area_mut()?; - heap_area.extend(&mut self.page_table, new_end_vpn)?; - self.heap_top = Some(new_end_vpn); - } else if new_end_vpn < current_end_vpn { - // 收缩堆 - let heap_area = self.find_heap_area_mut()?; - heap_area.shrink(&mut self.page_table, new_end_vpn)?; - self.heap_top = Some(new_end_vpn); - } - - Ok(new_end) -} -``` - -**使用场景**: - -```rust -// C 标准库 malloc 底层调用 -let old_brk = process.memory_space.brk(Vaddr::new(0))?; // 获取当前堆顶 -let new_brk = old_brk + Vaddr::new(size); -process.memory_space.brk(new_brk)?; // 扩展堆 -``` - -### mmap - 匿名内存映射 - -```rust -pub fn mmap(&mut self, start: Vaddr, len: usize, prot: usize) -> SyscallResult { - let start_vpn = Vpn::from_addr_floor(start); - let end_vpn = Vpn::from_addr_ceil(start + Vaddr::new(len)); - - // 检查地址范围是否可用 - self.check_range_available(VpnRange::new(start_vpn, end_vpn))?; - - // 创建新的映射区域 - let permission = Self::prot_to_pte_flags(prot); - let area = MappingArea::new( - VaddrRange::new(start, start + Vaddr::new(len)), - MapType::Framed, - permission, - AreaType::UserAnonymous, - ); - - // 映射到页表 - area.map(&mut self.page_table)?; - self.areas.push(area); - - Ok(start) -} -``` - -**使用场景**: - -```rust -// 用户程序请求匿名内存 -let addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, - MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); -``` - -### munmap - 取消映射 - -```rust -pub fn munmap(&mut self, start: Vaddr, len: usize) -> SyscallResult<()> { - let start_vpn = Vpn::from_addr_floor(start); - let end_vpn = Vpn::from_addr_ceil(start + Vaddr::new(len)); - let unmap_range = VpnRange::new(start_vpn, end_vpn); - - // 找到重叠的映射区域并取消映射 - let mut areas_to_remove = Vec::new(); - for (idx, area) in self.areas.iter().enumerate() { - if area.vpn_range.intersects(&unmap_range) { - areas_to_remove.push(idx); - } - } - - // 取消映射并释放资源 - for idx in areas_to_remove.iter().rev() { - let area = self.areas.remove(*idx); - area.unmap(&mut self.page_table)?; - } - - Ok(()) -} -``` - -## 进程管理 - -### fork 时的地址空间复制 - -```rust -pub fn clone_for_fork(&self) -> Self { - let mut new_space = Self { - page_table: ActivePageTableInner::new(), - areas: Vec::new(), - heap_top: self.heap_top, - }; - - // 深拷贝所有映射区域 - for area in &self.areas { - let mut new_area = area.clone_structure(); - - // 拷贝物理页内容 - for vpn in area.vpn_range { - if let Some(old_frame) = area.frames.get(&vpn) { - let new_frame = old_frame.clone(); // 分配新帧并拷贝数据 - new_area.frames.insert(vpn, new_frame); - } - } - - // 映射到新页表 - new_area.map(&mut new_space.page_table)?; - new_space.areas.push(new_area); - } - - new_space -} -``` - -**写时复制(COW)优化**(未来改进): - -fork 时共享物理页并标记为只读,写入时触发缺页异常再复制,可显著提升性能并减少内存占用。 - -### 激活地址空间 - -```rust -pub fn activate(&self) { - let root_ppn = self.page_table.root_ppn(); - ActivePageTableInner::activate(root_ppn); -} -``` - -**使用场景**: - -```rust -// 进程切换 -fn switch_to_process(process: &mut Process) { - process.memory_space.activate(); // 切换页表 - // 跳转到用户态 -} -``` - -## 区域类型 - -```rust -pub enum AreaType { - KernelText, // 内核代码段 - KernelData, // 内核数据段 - KernelHeap, // 内核堆 - UserText, // 用户代码段 - UserData, // 用户数据段 - UserHeap, // 用户堆 - UserStack, // 用户栈 - UserAnonymous, // 用户匿名映射(mmap) - Trampoline, // 跳板页 - TrapContext, // Trap 上下文 -} -``` - -## 使用场景 - -### 场景 1:创建新进程 - -```rust -// 从 ELF 文件加载程序 -let elf_data = load_elf_from_disk("/bin/hello")?; -let memory_space = MemorySpace::from_elf(&elf_data); - -let process = Process { - memory_space, - // ... 其他字段 -}; -``` - -### 场景 2:进程 fork - -```rust -fn sys_fork() -> SyscallResult { - let parent = current_process(); - let child_space = parent.memory_space.clone_for_fork(); +用户地址空间会复制当前内核映射的元数据并重新建立直接映射, 然后装入用户私有区域: + +- `from_elf()` 解析 loadable segment, 建立 `Framed` 用户段. +- ET_DYN 使用固定 load bias, 并处理当前支持的最小重定位集合. +- 用户栈, sigreturn trampoline 和 heap 起点按内核配置设置. + +### brk - let child = Process { - memory_space: child_space, - parent: Some(parent.pid()), - // ... 其他字段 - }; - - Ok(child.pid()) -} -``` - -### 场景 3:动态内存分配(brk) - -```rust -fn sys_brk(new_brk: usize) -> SyscallResult { - let process = current_process_mut(); - let new_end = Vaddr::new(new_brk); - process.memory_space.brk(new_end)?; - Ok(new_brk) -} -``` +`brk` 以 `heap_start` 为下界: -### 场景 4:内存映射(mmap) - -```rust -fn sys_mmap(start: usize, len: usize, prot: usize) -> SyscallResult { - let process = current_process_mut(); - let start_vaddr = Vaddr::new(start); - let mapped_addr = process.memory_space.mmap(start_vaddr, len, prot)?; - Ok(mapped_addr.as_usize()) -} -``` +- 第一次扩展时创建 `UserHeap` 区域. +- 后续扩展只允许向未占用区域增长. +- 收缩会解除尾部映射, 收缩到起点则移除整个 heap VMA. -## 常见问题 +### mmap 与地址选择 -### Q1: 内核和用户地址空间如何隔离? +匿名 `mmap` 在无 hint 时从用户堆顶和用户栈 guard 之间自顶向下找洞, 避免和向上增长的 brk 冲突.hint 会先向下页对齐, 如果冲突则回退到自动找洞. -**A**: 通过虚拟地址范围和 U 标志位: -- 内核空间:高半核(0xffff_ffc0_0000_0000 以上),U=0 -- 用户空间:低地址(0x0 开始),U=1 -- CPU 在用户态无法访问 U=0 的页面 +### munmap 和 mprotect -### Q2: fork 时为什么要深拷贝? - -**A**: 当前实现保证父子进程完全独立。未来可改用写时复制(COW)优化性能。 - -### Q3: 如何防止用户程序访问内核内存? - -**A**: 两层保护: -1. 页表权限:内核页面设置 U=0 -2. 地址检查:系统调用参数验证用户指针合法性 +`munmap` 和 `mprotect` 都先收集受影响 VMA 下标, 再倒序处理, 避免修改 `areas` 时下标失效. -### Q4: mmap 分配的地址范围如何选择? +- `munmap` 对中间区间解除映射时可能把一个 VMA 拆成左右两个 VMA. +- `mprotect(PROT_NONE)` 会把 `Framed` 区间转为 `Reserved` 并释放中间帧. +- 从 `Reserved` 改回可访问权限会重新分配帧并建立映射. +- `Direct` 映射不允许通过用户 mprotect 路径修改. -**A**: 当前实现要求用户指定地址。未来可实现地址分配器自动选择空闲区域。 - -## 性能优化 +### fork -### TLB 刷新优化 +`clone_for_fork()` 按映射策略处理: -```rust -// ✅ 高效:批量映射后一次性刷新 -for vpn in vpn_range { - area.map_single_page(vpn, &mut page_table)?; -} -PageTableInner::tlb_flush_all(); -``` +- `Direct` 只克隆元数据并重新映射. +- `Framed` 分配新帧并复制页内容. +- `Reserved` 只克隆元数据. +- `Shared` 复制共享段引用并重新建立共享映射. -### 内存占用优化 +## 并发与生命周期约束 -- 共享只读页面(代码段、只读数据) -- 写时复制(COW) -- 大页支持(减少页表级数) -- 延迟分配(按需分配物理帧) +- 进程级 `MemorySpace` 通常由外层锁保护.文档不假设 `MemorySpace` 本身可无锁并发修改. +- `MappingArea.frames` 是私有帧所有权边界.split, mprotect, munmap 必须移动或释放 tracker, 不能只改页表. +- 文件映射在 `munmap` 前和 `MemorySpace::drop()` 时尽力写回脏页. +- 页表修改经 `map_with_batch`, `unmap_with_batch`, `update_flags_with_batch` 进入架构 TLB 刷新策略. -## 相关文档 +## 已知限制 -- [地址抽象层](address.md) - Vaddr/Vpn 类型 -- [页表抽象层](page_table.md) - 页表操作接口 -- [物理帧分配器](frame_allocator.md) - 物理内存分配 -- [整体架构](architecture.md) - MM 子系统架构 -- [API 参考](api_reference.md) - API 快速查询 +- VMA 容器是线性 `Vec`, 地址空间碎片多时查找成本会上升. +- 文件映射和共享映射能力仍是基础实现, 与 Linux 完整 mmap 语义存在差距. +- `mmap` hint 冲突时不会做复杂的邻近搜索. +- 当前没有完整 COW, fork 成本随私有页数量线性增长. -## 参考实现 +## 源码索引 -- **源代码**:`os/src/mm/memory_space/` -- **初始化**:`os/src/mm/mod.rs:47-51`(内核地址空间) +- `os/src/mm/memory_space/mod.rs:1` - 模块组织和重导出. +- `os/src/mm/memory_space/mmap_file.rs:8` - 文件映射元数据. +- `os/src/mm/memory_space/mapping_area/mod.rs:16` - `MapType`, `AreaType`, `MappingArea`. +- `os/src/mm/memory_space/mapping_area/map_ops.rs:86` - 单页和整区映射. +- `os/src/mm/memory_space/mapping_area/split_ops.rs:4` - VMA 元数据克隆和 fork 数据复制. +- `os/src/mm/memory_space/mapping_area/split_ops.rs:234` - `mprotect` 局部权限修改. +- `os/src/mm/memory_space/mapping_area/split_ops.rs:444` - `munmap` 局部解除映射. +- `os/src/mm/memory_space/mapping_area/resize_ops.rs:8` - 区域尾部扩缩. +- `os/src/mm/memory_space/mapping_area/file_ops.rs:9` - 文件映射读入. +- `os/src/mm/memory_space/mapping_area/file_ops.rs:73` - 文件映射写回. +- `os/src/mm/memory_space/space/address_space.rs:3` - `MemorySpace` 基本操作. +- `os/src/mm/memory_space/space/kernel_space.rs:30` - 内核空间构建. +- `os/src/mm/memory_space/space/elf_loader.rs:22` - ELF 装载. +- `os/src/mm/memory_space/space/mmap_ops.rs:10` - 用户内存系统调用支持. diff --git a/document/mm/page_table.md b/document/mm/page_table.md index ada1f5bf..b6901954 100644 --- a/document/mm/page_table.md +++ b/document/mm/page_table.md @@ -1,439 +1,97 @@ # 页表抽象层 -## 概述 +页表抽象层把 MM 策略和硬件页表格式隔离开.上层只处理 `Vpn`, `Ppn`, `PageSize` 和 `UniversalPTEFlag`, 具体页表级数, PTE 位布局, 激活寄存器和 TLB 刷新由架构后端实现. -页表抽象层提供架构无关的页表操作接口,通过 trait 系统将通用逻辑与硬件特定实现分离。目前已实现 RISC-V SV39 三级页表。 +## 当前状态 -### 设计目标 +- 架构无关接口位于 `os/src/mm/page_table/`. +- 当前活动页表类型别名是 `ActivePageTableInner = crate::arch::mm::PageTableInner`. +- RISC-V 后端实现 SV39 三级页表. +- LoongArch64 后端实现 4 级页表, 匹配 48 位虚拟地址. +- 当前只启用 4K 页路径, `PageSize` 只有 `Size4K`. -1. **架构抽象**:统一的 trait 接口支持多种架构 -2. **类型安全**:通过 Rust 类型系统防止错误操作 -3. **灵活标志位**:UniversalPTEFlag 屏蔽架构差异 -4. **易于扩展**:新增架构只需实现 trait +## 目标 -## 核心接口 +- 统一上层页表操作接口. +- 让 `MemorySpace` 不依赖某个架构的 PTE 格式. +- 把跨 CPU TLB 刷新策略留给架构后端. +- 用 `UniversalPTEFlag` 表达用户/内核权限和 R/W/X/D 等通用含义. -### PageTableInner Trait +## 非目标 -页表的核心操作接口: +- 不在通用层承诺每个架构标志位一一对应. +- 不提供大页稳定接口. +- 不在通用层处理 ASID 生命周期. -```rust -pub trait PageTableInner { - // 架构常量 - const LEVELS: usize; // 页表级数(SV39 为 3) - const MAX_VA_BITS: usize; // 虚拟地址位宽(SV39 为 39) - const MAX_PA_BITS: usize; // 物理地址位宽(SV39 为 56) +## 模块边界 - // 生命周期管理 - fn new() -> Self; - fn from_ppn(root_ppn: Ppn) -> Self; - fn activate(ppn: Ppn); +- `page_table/inner.rs` 定义 `PageTableInner`. +- `page_table/page_table_entry.rs` 定义通用 PTE flag, flag 转换 trait 和 PTE trait. +- `arch/riscv/mm/page_table.rs` 负责 SV39 页表创建, walk, map, unmap, update flags, satp 激活和 TLB shootdown. +- `arch/riscv/mm/page_table_entry.rs` 负责 SV39 PTE 位布局. +- `arch/loongarch/mm/page_table.rs` 负责 LoongArch64 页表创建, CSR 配置, PGDL/PGDH 激活和本地 TLB 刷新. +- `arch/loongarch/mm/page_table_entry.rs` 负责 LoongArch PTE 位布局和反逻辑 NR/NX 翻译. - // TLB 管理 - fn tlb_flush(vpn: Vpn); - fn tlb_flush_all(); +## 关键流程 - // 核心操作 - fn map(&mut self, vpn: Vpn, ppn: Ppn, page_size: PageSize, - flags: UniversalPTEFlag) -> PagingResult<()>; - fn unmap(&mut self, vpn: Vpn) -> PagingResult<()>; - fn translate(&self, vaddr: Vaddr) -> Option; +### 建立映射 - // 查询操作 - fn walk(&self, vpn: Vpn) -> PagingResult<(Ppn, PageSize, UniversalPTEFlag)>; - fn root_ppn(&self) -> Ppn; -} -``` +1. 上层传入 VPN, PPN, 4K 页大小和通用权限. +2. 后端校验叶子 PTE 至少具备 R/W/X 之一.`PROT_NONE` 不应走叶子 PTE, 上层用 `MapType::Reserved` 表示. +3. 页表 walk 从根层向下查找, 必要时分配中间页表帧. +4. 叶子 PTE 写入架构格式. +5. 后端刷新本地 TLB, RISC-V 在非批处理模式下通知其他 CPU. -### UniversalPTEFlag +### 解除映射 -架构无关的页表项标志位: +后端 walk 到叶子 PTE 后清空条目.中间页表帧当前由 `PageTableInner.frames` 持有并随整个页表释放, 不在每次 unmap 时做空表回收. -```rust -pub struct UniversalPTEFlag(u8); +### 批量刷新 -impl UniversalPTEFlag { - // 基础标志 - pub const VALID: Self = Self(1 << 0); // 页表项有效 - pub const READABLE: Self = Self(1 << 1); // 可读 - pub const WRITABLE: Self = Self(1 << 2); // 可写 - pub const EXECUTABLE: Self = Self(1 << 3); // 可执行 - pub const USER_ACCESSIBLE: Self = Self(1 << 4); // 用户态可访问 - pub const GLOBAL: Self = Self(1 << 5); // 全局映射 - pub const ACCESSED: Self = Self(1 << 6); // 已访问 - pub const DIRTY: Self = Self(1 << 7); // 已修改 +`MappingArea` 批量 map/unmap/update 时通过 `TlbBatchContext::execute()` 包住循环: - // 组合标志 - pub fn kernel_r() -> Self { - Self::VALID | Self::READABLE - } +- RISC-V: 单页操作仍刷新本地页, 批处理结束后合并一次全局刷新和 IPI 通知. +- LoongArch64: 当前批处理上下文是本地全量刷新占位实现, 多核 IPI 尚未接入. - pub fn kernel_rw() -> Self { - Self::VALID | Self::READABLE | Self::WRITABLE - } +### 激活页表 - pub fn kernel_rx() -> Self { - Self::VALID | Self::READABLE | Self::EXECUTABLE - } +- RISC-V 把根 PPN 写入 `satp` 的 SV39 模式并执行 `sfence.vma`. +- LoongArch64 配置页表 walker CSR, 写 PGDL/PGDH, 设置 ASID, 打开分页并刷新 TLB.高半内核根可由 `set_kernel_root_ppn()` 提供. - pub fn user_r() -> Self { - Self::VALID | Self::READABLE | Self::USER_ACCESSIBLE - } +## 架构差异 - pub fn user_rw() -> Self { - Self::VALID | Self::READABLE | Self::WRITABLE | Self::USER_ACCESSIBLE - } +### RISC-V - pub fn user_rwx() -> Self { - Self::VALID | Self::READABLE | Self::WRITABLE | - Self::EXECUTABLE | Self::USER_ACCESSIBLE - } -} -``` +SV39 PTE 的低 8 位和 `UniversalPTEFlag` 基本同构.`sfence.vma` 用于本地刷新, 多核通过 IPI 请求其他 CPU 刷新. -## RISC-V SV39 实现 +### LoongArch64 -### SV39 页表结构 +LoongArch PTE 使用 PLV 表达权限级, 使用 NR/NX 表示不可读/不可执行, 因此 flag 转换不是简单位拷贝.页表激活还需要配置 PGDL/PGDH 和 page walk CSR. -``` -39 位虚拟地址: -┌─────────┬──────────┬──────────┬──────────┬────────────┐ -│ 63...39 │ 38...30 │ 29...21 │ 20...12 │ 11...0 │ -│ (符号位) │ VPN[2] │ VPN[1] │ VPN[0] │ offset │ -└─────────┴──────────┴──────────┴──────────┴────────────┘ - 25 位 9 位 9 位 9 位 12 位 +## 并发与生命周期约束 -56 位物理地址: -┌─────────┬──────────────────────────────┬────────────┐ -│ 55...44 │ 43...12 │ 11...0 │ -│ (保留) │ PPN │ offset │ -└─────────┴──────────────────────────────┴────────────┘ - 12 位 32 位 12 位 +- 页表修改必须由上层地址空间锁序列化.页表结构本身不提供内部锁. +- 页表页帧由 `PageTableInner.frames` 持有, 根页和中间页表随 `PageTableInner` 生命周期释放. +- 硬件 TLB 不会自动感知软件页表修改, 所有修改路径必须经过带刷新逻辑的后端方法. +- 通用 `UniversalPTEFlag` 只描述意图, 不应在上层假设某个架构的原始 PTE 位. -页表项 (PTE): -┌────────┬──────────────────────┬───────┬─┬─┬─┬─┬─┬─┬─┬─┐ -│ 63...54│ 53...10 │ 9...8 │D│A│G│U│X│W│R│V│ -│ (保留) │ PPN │ RSW │ │ │ │ │ │ │ │ │ -└────────┴──────────────────────┴───────┴─┴─┴─┴─┴─┴─┴─┴─┘ - 10 位 44 位 2 位 标志位(8位) -``` +## 已知限制 -### 地址转换 +- `PageSize` 仅 `Size4K`. +- RISC-V shootdown 是异步通知, 不等待远端 CPU 确认. +- LoongArch64 批处理刷新和跨核 shootdown 仍未达到 RISC-V 后端同等能力. -Comix 使用**直接映射**方式: +## 源码索引 -```rust -// 物理地址 → 虚拟地址 -pub const fn paddr_to_vaddr(paddr: usize) -> usize { - paddr | 0xffff_ffc0_0000_0000 -} - -// 虚拟地址 → 物理地址 -pub const unsafe fn vaddr_to_paddr(vaddr: usize) -> usize { - vaddr & 0x0000_003f_ffff_ffff -} -``` - -### 页表遍历 - -``` -三级页表查找流程: - -Virtual Address: VPN[2] | VPN[1] | VPN[0] | offset - ↓ -┌──────────────────────────────────────┐ -│ Level 2 (Root Page Table) │ -│ Entry[VPN[2]] → PPN of Level 1 │──┐ -└──────────────────────────────────────┘ │ - ↓ -┌──────────────────────────────────────┐ -│ Level 1 Page Table │ -│ Entry[VPN[1]] → PPN of Level 0 │──┐ -└──────────────────────────────────────┘ │ - ↓ -┌──────────────────────────────────────┐ -│ Level 0 Page Table (Leaf) │ -│ Entry[VPN[0]] → PPN of Data Page │──┐ -└──────────────────────────────────────┘ │ - ↓ - Physical Page + offset -``` - -### TLB 管理 - -TLB(Translation Lookaside Buffer)缓存虚拟地址到物理地址的翻译结果。修改页表后必须刷新 TLB,确保硬件使用最新映射。 - -#### 单核 TLB 刷新 - -```rust -// 刷新单个页 -pub fn tlb_flush(vpn: Vpn) { - unsafe { - asm!("sfence.vma {0}, zero", in(reg) vpn.as_usize()); - } -} - -// 刷新所有页 -pub fn tlb_flush_all() { - unsafe { - asm!("sfence.vma"); - } -} -``` - -#### 多核 TLB Shootdown - -在多核系统中,修改页表后需要通知所有 CPU 刷新 TLB。**Comix 的页表实现会自动处理这个过程**。 - -**自动 TLB Shootdown**: - -页表的 `map()`, `unmap()`, `update_flags()` 操作会自动触发 TLB shootdown: - -```rust -// 映射页面 - 自动刷新所有 CPU 的 TLB -page_table.map(vpn, ppn, PageSize::Size4K, UniversalPTEFlag::user_rw())?; -// ✓ 不需要手动调用 tlb_flush! - -// 解除映射 - 自动刷新所有 CPU 的 TLB -page_table.unmap(vpn)?; -// ✓ 不需要手动调用 tlb_flush! - -// 更新权限 - 自动刷新所有 CPU 的 TLB -page_table.update_flags(vpn, UniversalPTEFlag::kernel_r())?; -// ✓ 不需要手动调用 tlb_flush! -``` - -**工作原理**: - -`tlb_flush_all_cpus()` 方法执行以下操作: -1. 刷新当前 CPU 的 TLB(使用 `sfence.vma`) -2. 检查是否为多核环境(`NUM_CPU > 1`) -3. 如果是多核,通过 IPI 通知所有其他 CPU 刷新 TLB - -**单核/多核行为**: - -- **单核环境**(`NUM_CPU = 1`):只刷新本地 TLB,无 IPI 开销 -- **多核环境**(`NUM_CPU > 1`):刷新本地 TLB + 发送 IPI 到其他 CPU -- **测试模式**:自动检测环境,确保测试正常运行 - -**性能影响**: - -- 单核环境:无额外开销(~10 CPU 周期) -- 多核环境:每次页表操作增加约 0.5 微秒(4 核系统) -- 批量操作:建议使用更大的页面或预分配页表以减少 IPI 频率 - -详见 [IPI 文档 - 页表自动 TLB Shootdown](../arch/riscv/ipi.md#521-页表自动-tlb-shootdown)。 - -## 基本使用 - -### 创建页表 - -```rust -// 创建新页表(自动分配根页表帧) -let mut page_table = ActivePageTableInner::new(); - -// 从已有根页号创建 -let page_table = PageTableInner::from_ppn(root_ppn); -``` - -### 映射页面 - -```rust -// 映射单个 4K 页 -let vpn = Vpn::new(0x1000); -let ppn = Ppn::new(0x8000_1); -page_table.map( - vpn, - ppn, - PageSize::Size4K, - UniversalPTEFlag::user_rw() -)?; -// TLB 已自动刷新(单核/多核环境均适用) -``` - -### 取消映射 - -```rust -page_table.unmap(vpn)?; -// TLB 已自动刷新 -``` - -### 地址翻译 - -```rust -let vaddr = Vaddr::new(0x1000_0000); -if let Some(paddr) = page_table.translate(vaddr) { - println!("VA {:#x} → PA {:#x}", vaddr.as_usize(), paddr.as_usize()); -} else { - println!("Page fault: unmapped address"); -} -``` - -### 查询映射信息 - -```rust -match page_table.walk(vpn) { - Ok((ppn, size, flags)) => { - println!("Mapped: VPN {:#x} → PPN {:#x}", vpn.as_usize(), ppn.as_usize()); - println!("Page size: {:?}", size); - println!("Flags: {:?}", flags); - } - Err(e) => println!("Walk failed: {:?}", e), -} -``` - -## 使用场景 - -### 场景 1:内核地址空间创建 - -```rust -pub fn new_kernel() -> Self { - let mut space = MemorySpace::new(); - - // 映射跳板页 - space.map_trampoline(); - - // 映射内核段 - space.push(MappingArea::new( - VaddrRange::new(Vaddr::new(stext as usize), Vaddr::new(etext as usize)), - MapType::Direct, - UniversalPTEFlag::kernel_rx(), - AreaType::KernelText, - )); - - // 映射内核数据段 - space.push(MappingArea::new( - VaddrRange::new(Vaddr::new(sdata as usize), Vaddr::new(edata as usize)), - MapType::Direct, - UniversalPTEFlag::kernel_rw(), - AreaType::KernelData, - )); - - // 直接映射物理内存 - let phys_mem_end = paddr_to_vaddr(MEMORY_END); - space.push(MappingArea::new( - VaddrRange::new(Vaddr::new(ekernel as usize), Vaddr::new(phys_mem_end)), - MapType::Direct, - UniversalPTEFlag::kernel_rw(), - AreaType::PhysicalMemory, - )); - - space -} -``` - -### 场景 2:用户程序加载 - -```rust -pub fn from_elf(elf_data: &[u8]) -> Result { - let elf = xmas_elf::ElfFile::new(elf_data)?; - let mut space = MemorySpace::new(); - - for ph in elf.program_iter() { - if ph.get_type() != ProgramHeaderType::Load { - continue; - } - - let start_va = ph.virtual_addr() as usize; - let end_va = (ph.virtual_addr() + ph.mem_size()) as usize; - let flags = ph_flags_to_universal(ph.flags()); - - // 创建映射区域 - let area = MappingArea::new( - VaddrRange::new(Vaddr::new(start_va), Vaddr::new(end_va)), - MapType::Framed, // 为每页分配物理帧 - flags, - AreaType::UserData, - ); - - space.push(area); - } - - Ok(space) -} -``` - -## 错误处理 - -```rust -#[derive(Debug)] -pub enum PagingError { - PageFault, // 页面不存在 - AlreadyMapped, // 页面已映射 - InvalidFlags, // 标志位无效 - FrameAllocFailed, // 物理帧分配失败 -} - -pub type PagingResult = Result; -``` - -## 常见问题 - -### Q1: 为什么需要刷新 TLB? - -**A**: TLB 缓存虚拟地址到物理地址的翻译结果。修改页表后,必须刷新 TLB 以保证硬件使用最新映射。 - -### Q2: 什么时候使用 tlb_flush_all? - -**A**: -- 切换页表(如进程切换) -- 批量修改映射时 -- 单个 `tlb_flush` 适用于修改少量页面 - -### Q3: translate 和 walk 的区别? - -**A**: -- `translate(vaddr)`:快速翻译,仅返回物理地址 -- `walk(vpn)`:返回完整映射信息(PPN、大小、标志),用于调试 - -### Q4: 为什么 VADDR_START 是 0xffff_ffc0_0000_0000? - -**A**: 这是 SV39 高半核的起始地址: -- Bit 38 = 1,符号扩展后 bits [63:39] 全为 1 -- 低 38 位全为 0 -- 结果:`0xffff_ffc0_0000_0000` - -### Q5: 多核环境下如何处理 TLB? - -**A**: Comix 的页表实现会自动处理多核 TLB 同步: -- 页表操作(`map`/`unmap`/`update_flags`)会自动刷新所有 CPU 的 TLB -- 单核环境下无额外开销 -- 多核环境下通过 IPI(核间中断)通知其他 CPU 刷新 TLB -- 详见 [TLB 管理](#tlb-管理) 章节 - -### Q6: 为什么不需要手动刷新 TLB? - -**A**: 从 SMP 分支开始,页表操作内部集成了自动 TLB shootdown 机制: -- **自动化**:`map`/`unmap`/`update_flags` 会自动调用 `tlb_flush_all_cpus()` -- **正确性**:确保所有 CPU 看到一致的内存映射 -- **性能**:单核环境下无 IPI 开销,多核环境下自动优化 -- **简化**:用户代码无需关心 TLB 刷新细节 - -如果需要手动控制 TLB 刷新(如批量优化),可以直接操作页表项后调用 `send_tlb_flush_ipi_all()`。 - -## 性能考量 - -### TLB 性能 - -- TLB 命中率通常 > 95% -- TLB Miss 惩罚:~100 CPU 周期 -- 合理规划映射可提高 TLB 命中率 - -### 大页支持 - -当前实现中大页已暂时禁用。未来启用时: -- 2MB 大页:减少 TLB 压力 -- 1GB 巨页:适用于大块连续内存 - -## 相关文档 - -- [地址抽象层](address.md) - Vpn/Ppn 类型 -- [物理帧分配器](frame_allocator.md) - 页表帧的分配 -- [整体架构](architecture.md) - MM 子系统分层设计 -- [API 参考](api_reference.md) - 完整 API 列表 - -## 参考实现 - -- **架构无关层**:`os/src/mm/page_table/` -- **RISC-V 实现**:`os/src/arch/riscv/mm/page_table.rs` -- **RISC-V 规范**:SV39 Paging Scheme +- `os/src/mm/page_table/mod.rs:11` - 活动页表别名和公共类型. +- `os/src/mm/page_table/inner.rs:8` - `PageTableInner` trait. +- `os/src/mm/page_table/page_table_entry.rs:12` - `UniversalPTEFlag`. +- `os/src/mm/page_table/page_table_entry.rs:61` - `PageTableEntry` trait. +- `os/src/arch/riscv/mm/page_table.rs:13` - RISC-V `PageTableInner` 状态. +- `os/src/arch/riscv/mm/page_table.rs:403` - RISC-V 批处理 map/unmap/update. +- `os/src/arch/riscv/mm/page_table.rs:467` - RISC-V `TlbBatchContext`. +- `os/src/arch/riscv/mm/page_table_entry.rs:7` - SV39 PTE flags. +- `os/src/arch/loongarch/mm/page_table.rs:25` - LoongArch64 `PageTableInner` 状态. +- `os/src/arch/loongarch/mm/page_table.rs:399` - LoongArch64 批处理接口. +- `os/src/arch/loongarch/mm/page_table.rs:440` - LoongArch64 `TlbBatchContext`. +- `os/src/arch/loongarch/mm/page_table_entry.rs:35` - LoongArch64 PTE flags. diff --git a/document/net/README.md b/document/net/README.md index 08c2ffc6..38fabddc 100644 --- a/document/net/README.md +++ b/document/net/README.md @@ -1,21 +1,47 @@ # 网络模块概览 -网络模块负责把内核网卡设备、接口配置、smoltcp 协议栈、VFS socket 文件和网络 syscall 连接起来。 +网络模块把 `NetDevice`, 接口控制面, smoltcp runtime, VFS socket 文件和网络 syscall 连接起来。当前仍是单 active runtime 设计, 但边界已经集中到 `NetworkStack` 门面。 -## 源码入口 +## 当前状态 -- `os/src/device/net/`:网卡设备抽象和 VirtIO/loopback/null 设备。 -- `os/src/net/interface.rs`:接口注册表、接口配置和 smoltcp interface 兼容工厂。 -- `os/src/net/stack.rs`:`NetworkStack` 状态对象和协议栈运行时实现。 -- `os/src/net/socket.rs`:`SocketFile`、fd/socket 映射、UDP per-fd 队列和公开 socket 包装 API。 -- `os/src/kernel/syscall/network.rs`:网络 syscall ABI 层。 +- 真实网卡通过 `register_net_device()` 注册为 `NetworkInterface`。 +- 默认配置确保 `lo` 存在, loopback-only 场景使用 `127.0.0.1/8` 且没有默认网关。 +- AF_INET TCP/UDP socket 由 smoltcp 驱动, 状态集中在 `NetworkStack`。 +- AF_UNIX socket 是内核本地 IPC transport, 不进入 smoltcp。 +- `SocketFile` 和 `UnixSocketFile` 都实现 VFS `File`, 由 syscall 层通过 fd table 暴露给用户态。 + +## 目标 + +- 设备层只提供收发帧能力, 不理解 socket 或 syscall。 +- 接口层保存控制面配置, 不直接管理协议栈 socket set。 +- 协议栈运行时由 `NetworkStack` 统一持有和推进。 +- syscall 层只处理用户 ABI, fd table, sockaddr 和 errno。 + +## 非目标 + +- 当前不是多 active interface runtime。 +- 不在文档列出每个 socket syscall 的参数和错误分支。 +- 不把 AF_UNIX 混入 smoltcp 数据路径。 ## 文档导航 -- `architecture.md`:整体分层和依赖方向。 -- `device_and_interface.md`:设备注册、接口对象和中断兼容桥。 -- `stack_runtime.md`:smoltcp runtime、poll、socket set 和 loopback link。 -- `socket_syscall.md`:SocketFile、fd 映射、syscall errno 边界。 -- `loopback_poll.md`:显式 loopback 和 poll/select 唤醒。 -- `testing.md`:验证矩阵和常见排查。 -- `network_implementation_guide.md`:维护指南。 +- [整体架构](architecture.md) +- [设备与接口](device_and_interface.md) +- [协议栈运行时](stack_runtime.md) +- [Socket 与 syscall](socket_syscall.md) +- [Loopback 与 poll](loopback_poll.md) +- [测试与排查](testing.md) +- [网络实现指南](network_implementation_guide.md) +- [netperf / netserver 测试说明](netperf.md) + +## 源码索引 + +- `os/src/device/net/`: 网卡设备抽象和 VirtIO/loopback/null 设备。 +- `os/src/net/mod.rs`: 网络模块入口和 `register_net_device()`。 +- `os/src/net/interface.rs`: `NetworkInterface`, registry, 中断兼容桥。 +- `os/src/net/config.rs`: 默认接口配置和 loopback 初始化。 +- `os/src/net/stack/mod.rs`: `NetworkStack` 和 smoltcp runtime。 +- `os/src/net/stack/adapter.rs`: `NetDeviceAdapter` 和 loopback frame 回灌。 +- `os/src/net/socket.rs`: AF_INET `SocketFile`, fd/socket mapping, poll 门面。 +- `os/src/net/unix_socket.rs`: AF_UNIX socket。 +- `os/src/kernel/syscall/network/`: socket syscall ABI 层。 diff --git a/document/net/architecture.md b/document/net/architecture.md index 775139a5..3a47a19f 100644 --- a/document/net/architecture.md +++ b/document/net/architecture.md @@ -1,34 +1,78 @@ # 网络架构 -## 目标模型 +网络架构的核心是把控制面, 数据面和用户 ABI 分开。设备驱动提供帧收发, 接口层保存配置, `NetworkStack` 拥有协议栈运行时, syscall 层只做用户边界。 + +## 当前状态 ```text -device/net NetDevice - | - v -net::register_net_device() - | - v +NetDevice + | + v +register_net_device() + | + v NetworkInterface registry - | - v -NetworkStack runtime - | - v -SocketFile / syscall ABI + | + v +NetworkStack + | + v +SocketFile / UnixSocketFile + | + v +network syscall ``` -依赖方向只能自上而下或通过明确门面调用。设备层不理解 socket,syscall 层不理解 smoltcp 的内部 socket set。 +AF_INET 走 `NetworkStack` 和 smoltcp。AF_UNIX 走 `UnixSocketFile` 内部队列和绑定表, 是同一个 syscall family 下的本地 IPC transport。 + +## 目标 + +- 设备层和 syscall 层都不直接持有 smoltcp socket set。 +- `SocketFile` 保存 fd 局部状态, 协议栈状态由 `NetworkStack` 持有。 +- `NetworkInterface` 是控制面对象, 不承担 `Driver` 职责, 中断兼容由 `NetDriverHandle` 处理。 +- loopback 流量在当前单 runtime 下通过内部 frame queue 回灌。 + +## 非目标 + +- 不实现完整路由表和多接口 runtime 调度。 +- 不在网络核心中放架构特定逻辑。 +- 不把测试兼容路径提升为长期抽象, 如 bounded loopback drain。 + +## 模块边界 + +- `os/src/device/net/`: `NetDevice` 数据面。 +- `os/src/net/interface.rs`: 接口 registry, IP/gateway 配置, smoltcp interface 工厂。 +- `os/src/net/stack/mod.rs`: socket set, active interface, UDP dispatcher, TCP close 回收。 +- `os/src/net/socket.rs`: AF_INET socket 文件和公开门面。 +- `os/src/net/unix_socket.rs`: AF_UNIX 本地 socket。 +- `os/src/kernel/syscall/network/`: 用户 ABI 和 fd table。 + +## 关键流程 + +1. 设备驱动创建 `Arc`。 +2. `register_net_device()` 创建 `NetworkInterface`, 加入 registry, 注册 `NetDriverHandle`。 +3. `NetworkConfigManager::init_default_interface()` 选择 active interface, 设置 IP/gateway, 初始化 `NetworkStack`。 +4. `socket()` 为 AF_INET 创建 smoltcp handle 和 `SocketFile`, 为 AF_UNIX 创建 `UnixSocketFile`。 +5. connect/send/recv/poll 等 syscall 通过文件对象或 `NetworkStack` 门面推进协议状态。 + +## 并发和生命周期约束 + +- `NetworkStack` 内部状态用 `SpinLock` 分区保护。 +- `SocketFile::drop()` 必须释放或关闭对应 stack handle, 防止 fd 生命周期结束后 socket set 泄漏。 +- UDP per-port socket 可能被多个 fd 共享, per-fd 接收队列在 `SocketFile` 中隔离。 +- 网络中断不直接推进完整协议栈, 而是唤醒 poll waiters 或请求工作队列 poll。 -## 当前边界 +## 已知限制 -- `NetworkStack` 是协议栈状态宿主,持有 smoltcp socket set、active interface runtime、loopback link,并提供 TCP/UDP/socket 文件级 API。 -- `NetworkInterface` 是控制面接口对象,保存名称、MAC、IP 地址、网关和兼容中断状态。 -- `SocketFile` 是 VFS 文件对象,保存 fd 相关逻辑状态,如 local/remote endpoint、flags、shutdown 状态、UDP per-fd 接收队列。 -- `kernel::syscall::network` 只负责用户参数、fd table、sockaddr 编解码和 errno。 +- 单 active smoltcp runtime 限制了多 NIC 路由能力。 +- `SocketHandle` 仍包装 smoltcp handle, 是迁移中的兼容类型。 +- IPv6 支持受构建 feature 和 smoltcp 路径限制。 -## 兼容点 +## 源码索引 -- 当前仍是单 active smoltcp runtime,多接口 runtime 需要后续引入真正的 `StackSocketId` 和接口路由。 -- `SocketHandle` 仍包装 smoltcp handle,作为旧路径兼容类型。 -- `NetworkInterface::create_smoltcp_interface()` 暂时保留为 runtime 初始化工厂。 +- `os/src/net/mod.rs`: `register_net_device()`。 +- `os/src/net/interface.rs`: `NetworkInterface`, `NetworkInterfaceManager`, `NetDriverHandle`。 +- `os/src/net/config.rs`: 默认接口配置。 +- `os/src/net/stack/mod.rs`: `NetworkStack`。 +- `os/src/net/socket.rs`: `SocketFile`。 +- `os/src/net/unix_socket.rs`: AF_UNIX transport。 diff --git a/document/net/device_and_interface.md b/document/net/device_and_interface.md index 5a029e4c..389c9189 100644 --- a/document/net/device_and_interface.md +++ b/document/net/device_and_interface.md @@ -1,43 +1,73 @@ # 设备与接口 -## 设备层 +设备层和接口层是网络控制面的入口。`NetDevice` 提供帧收发, `NetworkInterface` 保存名称, MAC, IP 和网关等控制面状态。 -`os/src/device/net/` 只负责网卡数据面: +## 当前状态 -- `net_device.rs` 定义 `NetDevice`、`NetDeviceError` 和 VirtIO net 设备实现。 -- `virtio_net.rs` 初始化 VirtIO net 设备,并调用网络子系统注册入口。 -- `loopback.rs` 提供显式 loopback 设备。 -- `null_net.rs` 仅作为空设备/占位实现,不表示 loopback。 +- VirtIO net 初始化成功后调用 `crate::net::register_net_device(device)`。 +- 注册入口根据设备 id 生成 `ethN` 名称。 +- `NetworkInterface` 被加入 `NETWORK_INTERFACE_MANAGER`。 +- `NetDriverHandle` 作为兼容 driver 注册到设备框架, 用于中断分发。 +- 默认配置会确保 `lo` 存在。 -设备层不得直接依赖 IP、网关、socket、syscall 或 smoltcp socket set。 +## 目标 -## 注册路径 +- 保持设备驱动只依赖 `NetDevice` 抽象。 +- 让接口对象负责控制面配置, 不直接成为设备框架 driver。 +- 通过兼容桥处理旧中断注册路径, 避免污染接口抽象。 -真实网卡初始化后调用: +## 非目标 -```rust -crate::net::register_net_device(device) -``` +- `NetworkInterface` 不直接操作 socket set。 +- 设备驱动不配置 IP, 网关或 loopback 策略。 +- `NullNetDevice` 不代表 loopback。 -该入口负责: +## register_net_device 流程 -- 注册底层 `NetDevice`。 -- 创建 `NetworkInterface`。 -- 把接口加入 `NETWORK_INTERFACE_MANAGER`。 -- 通过 `NetDriverHandle` 注册中断兼容桥。 +1. 用设备 id 构造接口名, 如 `eth0`。 +2. 创建 `NetworkInterface` 并保存底层 `NetDevice` 引用。 +3. 调用设备子系统注册底层 net device。 +4. 把 interface 加入 `NETWORK_INTERFACE_MANAGER`。 +5. 创建 `NetDriverHandle` 并注册为设备框架 driver, 供中断路径调用。 -## 接口层 +## NetworkInterface 边界 -`NetworkInterface` 保存控制面状态: +`NetworkInterface` 保存: -- interface name,如 `eth0`、`lo`。 +- name。 - MAC address。 +- 底层 `NetDevice`。 - IP CIDR 列表。 - IPv4 gateway。 -- 兼容中断开关和最后中断时间。 +- 中断兼容状态和最后中断时间。 -`NetworkInterface` 不应直接实现 `Driver`。中断体系仍需要 `Driver` 时,使用 `NetDriverHandle`。 +它可以创建 `SmoltcpInterface` wrapper, 但这个工厂是当前单 runtime 初始化的兼容边界, 不是多接口运行时模型。 -## Loopback +## Loopback 初始化 -默认网络初始化会确保 `lo` 存在。无真实 NIC 时,`lo` 作为 active runtime 接口,配置 `127.0.0.1/8` 且不配置默认网关。 +`NetworkConfigManager::ensure_loopback_interface()` 确保 `lo` 存在并配置 `127.0.0.1/8`。如果没有真实 NIC, 默认配置选择 `lo` 作为 active runtime, 且不设置默认网关。若存在真实 NIC, 当前会在选中接口上同时配置默认地址和 loopback CIDR。 + +## 中断协作 + +`NetDriverHandle::try_handle_interrupt()`: + +- 检查接口中断开关。 +- 记录中断事件和时间。 +- 尝试从底层设备读取一帧。 +- 唤醒 poll waiters。 + +它不维护第二份协议栈状态。真正协议推进仍在进程上下文中的 `NetworkStack::poll()` 或网络 I/O 路径完成。 + +## 已知限制 + +- `last_interrupt_time` 当前用简单递增时间模拟。 +- 接口 registry 是简单 Vec, 没有 namespace 或复杂路由策略。 +- 默认配置是测试友好的静态配置, 不是通用 DHCP/用户配置系统。 + +## 源码索引 + +- `os/src/net/mod.rs`: `register_net_device()`。 +- `os/src/net/interface.rs`: 接口对象, registry, `NetDriverHandle`。 +- `os/src/net/config.rs`: 默认配置和 loopback 保证。 +- `os/src/device/net/virtio_net.rs`: VirtIO net 注册入口。 +- `os/src/device/net/loopback.rs`: 显式 loopback 设备。 diff --git a/document/net/loopback_poll.md b/document/net/loopback_poll.md index 6ea23e2b..6af4fe7f 100644 --- a/document/net/loopback_poll.md +++ b/document/net/loopback_poll.md @@ -1,23 +1,71 @@ # Loopback 与 poll -## Loopback 模型 +Loopback 和 poll 是当前网络栈能稳定跑本机 TCP/UDP 测试的关键边界。loopback 负责 127/8 流量回灌, poll 负责推进 smoltcp 和唤醒等待者。 -`lo` 是显式接口,不再由 `NullNetDevice` 隐式承担。无真实 NIC 时,默认配置创建 `LoopbackNetDevice`,接口名为 `lo`,地址为 `127.0.0.1/8`。 +## 当前状态 -当前仍是单 active smoltcp runtime,因此有真实 NIC 时,127/8 frame 通过 `net::stack` 内部 loopback link 消费;后续多 runtime 可以把 `lo` 升级为独立 smoltcp interface。 +- `lo` 是显式接口, 默认配置会确保它存在。 +- 无真实 NIC 时, active runtime 使用 `LoopbackNetDevice` 和 `127.0.0.1/8`。 +- 有真实 NIC 时, 当前单 runtime 下仍通过 `NetTxToken` 检测 127/8 frame 并放入 `loopback_link`。 +- 网络 poll 可由 I/O 路径直接调用, 也可由 timer/中断路径请求工作队列执行。 -## Poll 模型 +## 目标 -协议栈推进集中在 `NetworkStack::poll()`: +- loopback 不依赖 `NullNetDevice` 的副作用。 +- poll/select 看到的是已 dispatch 的 TCP/UDP socket 状态。 +- 硬中断只做轻量通知, 协议推进在可控上下文中完成。 -- smoltcp poll。 -- loopback link bounded drain。 +## 非目标 + +- 当前不提供独立 loopback smoltcp runtime。 +- bounded drain 不是通用网络调度策略。 +- 不在本文描述 pollfd 的每个事件位分支。 + +## Loopback 数据流 + +1. socket write/sendto 调用 smoltcp 发送。 +2. `NetTxToken` 检查以太网帧中的 IPv4/ARP 地址。 +3. 如果源或目的地址属于 127/8, 帧进入 `NetworkStack::loopback_link`。 +4. 下一次 `NetDeviceAdapter::receive()` 优先从 loopback queue 取帧。 +5. `NetworkStack::poll()` 驱动 smoltcp 接收并更新 socket 状态。 + +## Poll 流程 + +`NetworkStack::poll()` 执行: + +- smoltcp interface poll。 +- loopback extra poll。 - UDP per-port dispatch 到 per-fd queue。 - TCP pending close reaping。 - poll/select waiter wakeup。 -`poll_until_empty()` 仅作为 loopback/netperf 兼容包装保留,不应成为新的主路径。 +`poll_network_and_dispatch()` 是 syscall I/O 层使用的门面。`request_network_poll()` 用原子 pending 位把中断/timer 侧请求合并到 work queue, 避免重复排队。 + +## 与 syscall I/O 的关系 + +- `read/write` 遇到 socket `WouldBlock` 时, 会先 poll 网络再 yield。 +- `ppoll/poll/select` 在等待循环中推进网络状态。 +- socket 文件的 `readable/writable` 查询只看当前可观察状态, 不复制用户缓冲区。 +- 状态变化统一调用 `wake_poll_waiters()`。 + +## 并发和生命周期约束 + +- loopback queue 受 `NetworkStack` 内部锁保护。 +- work queue pending 位避免多个中断重复安排网络 poll。 +- poll 唤醒不能假设所有 waiter 都还持有原 fd, fd table 需重新验证。 +- bounded drain 有固定次数上限, 避免写路径无限自旋。 + +## 已知限制 + +- 多接口 runtime 到来前, loopback 和真实 NIC 仍共享 active smoltcp interface。 +- 对 127/8 的检测基于当前帧解析逻辑, 不是完整路由表。 +- netperf 中和 `EINTR` 相关的现象更多来自 signal/syscall restart, 见 `netperf.md`。 -## 中断协作 +## 源码索引 -网络中断兼容桥只记录事件和唤醒 waiter,不直接维护另一份协议栈状态。需要处理收包时,由进程上下文中的 poll/read/write/connect 等路径推进 `NetworkStack`。 +- `os/src/net/config.rs`: loopback interface 保证和默认配置。 +- `os/src/device/net/loopback.rs`: 显式 loopback device。 +- `os/src/net/stack/adapter.rs`: `NetTxToken` loopback 判定和 frame 回灌。 +- `os/src/net/stack/mod.rs`: `poll()`, `poll_until_empty()`, loopback queue。 +- `os/src/net/socket.rs`: `poll_network_and_dispatch()`, `request_network_poll()`。 +- `os/src/kernel/syscall/io.rs`: poll/select 等待和唤醒。 diff --git a/document/net/netperf.md b/document/net/netperf.md index 021b823e..61c6ed2b 100644 --- a/document/net/netperf.md +++ b/document/net/netperf.md @@ -1,51 +1,51 @@ -# netperf / netserver 测试说明(已知现象) +# netperf / netserver 测试说明 -本页用于记录在 ComixOS 上运行 `netperf/netserver` 时的测试方法与当前已知现象,便于后续回归与排查。 +本页记录 netperf 场景和当前内核边界的关系。它不是网络实现指南, 也不要求修改用户态测试脚本。 -## 如何运行 +## 当前状态 -仓库内提供了脚本(不建议修改脚本本身): +- 仓库内脚本 `data/netperf_testcode.sh` 会启动 `netserver`, 再运行 UDP/TCP stream 和 request-response 测试。 +- 主要测试目标是 loopback TCP/UDP, socket syscall, poll/select 和 signal 交互。 +- 测试可能在尾部看到 `Interrupted system call (errno 4)` 相关输出。 -- `data/netperf_testcode.sh` +## 目标 -在系统内启动后执行: +- 用 netperf 覆盖本机网络数据面和 syscall 等待路径。 +- 把已知 `EINTR` 输出和真实网络失败区分开。 +- 保持脚本输出格式稳定, 便于回归比较。 -```sh -./netperf_testcode.sh -``` +## 非目标 -该脚本会: +- 不在文档中维护 netperf 参数大全。 +- 不通过修改脚本掩盖内核 syscall restart 限制。 +- 不把 netperf 结果当作完整网络兼容性证明。 -1. 后台启动 `netserver`(`-D` 守护模式),监听 `127.0.0.1:12865` -2. 依次运行 `netperf` 的 `UDP_STREAM/TCP_STREAM/UDP_RR/TCP_RR/TCP_CRR` -3. 结束时 `kill -9` 掉 `netserver` 进程 +## 关键流程 -## 预期结果 +1. `netserver` 在 loopback 地址监听。 +2. `netperf` 发起 TCP/UDP 流量。 +3. socket syscall 进入 AF_INET `SocketFile`。 +4. `NetworkStack::poll()` 推进 smoltcp, loopback queue 和 UDP dispatch。 +5. poll/select waiter 被网络状态变化唤醒。 +6. 子进程退出或信号到达时, syscall 可能返回 `EINTR`。 -脚本应当能够完整跑完并返回 shell,且各测试段落会打印 `end: success`。 +## 已知现象 -## 已知现象:`accept_connections: select failure: Interrupted system call (errno 4)` +`accept_connections: select failure: Interrupted system call (errno 4)` 可能出现。它表示 `netserver` 的 select 被信号打断并看到了 `EINTR`。 -在脚本尾部(或某些测试段落结束后)可能看到如下输出: +当前内核尚未完整实现 `SA_RESTART` 和 syscall restart, 因此该输出不一定表示网络栈失败。判断测试是否失败应同时看脚本是否完整跑完, 各测试段落是否输出 success, 以及是否存在真实连接/收发错误。 -``` -accept_connections: select failure: Interrupted system call (errno 4) -``` +## 排查边界 -说明: +- 如果只出现 `EINTR` 文案, 先查 signal 和 syscall restart, 不要直接改网络栈。 +- 如果 TCP accept 或 recv 卡住, 查 `NetworkStack::poll()`, listener queue 和 `wake_poll_waiters()`。 +- 如果 UDP 测试无数据, 查 UDP per-port dispatch 和 per-fd queue。 +- 如果 loopback 发送后没有接收, 查 `NetTxToken` 127/8 判定和 `loopback_link` drain。 -- 该信息来自 `netserver`:其内部 `select()` 被信号打断,返回 `EINTR (errno=4)` 后打印该告警。 -- **这不代表脚本失败**:脚本仍可能全部测试通过并正常返回 shell。 -- 触发时机通常与 `netserver` 的守护/回收子进程逻辑相关(例如收到 `SIGCHLD` 等),而 `netserver` 对 `EINTR` 的处理方式是直接打印错误并退出/回到外层循环。 - -### 为什么暂时不在脚本层修复 - -用户侧脚本已固定(且多处依赖其输出格式),修改脚本容易引入额外差异,不利于对内核兼容性的验证。 - -### 后续若要消除该输出(不改脚本)的方向 - -需要在内核或用户态二进制中做其一: - -1. **内核实现更完整的 `SA_RESTART` / syscall restart 语义**(让 `select/poll` 在特定信号到来后自动重启,尽量不向用户态暴露 `EINTR`)。 -2. **修改/替换 `netserver`**:将 `select()` 返回 `EINTR` 视为可重试(不打印错误、直接继续)。 +## 源码索引 +- `os/src/net/stack/mod.rs`: loopback drain, TCP/UDP runtime, poll。 +- `os/src/net/stack/adapter.rs`: loopback frame 回灌。 +- `os/src/kernel/syscall/io.rs`: poll/select 和 `EINTR` 交互。 +- `os/src/ipc/signal.rs`: `signal_interrupts_syscall()`。 +- `os/src/kernel/syscall/network/`: socket syscall。 diff --git a/document/net/network_implementation_guide.md b/document/net/network_implementation_guide.md index 602f1278..aed8566e 100644 --- a/document/net/network_implementation_guide.md +++ b/document/net/network_implementation_guide.md @@ -1,46 +1,56 @@ -# 网络子系统实现指南 +# 网络实现指南 -本文档描述 `new-main` 上网络模块的当前实现边界和维护方式。网络模块以 `NetDevice -> NetworkInterface -> NetworkStack -> SocketFile/syscall` 为主线,保留少量兼容壳体以维持现有接口稳定。 +本文给维护网络代码时的边界规则。细节 API 以源码和 rustdoc 为准。 ## 当前分层 -- 设备层:`os/src/device/net/` 定义和实现网卡数据面,核心抽象是 `NetDevice`。 -- 接口层:`os/src/net/interface.rs` 维护接口名称、MAC、IP、网关和接口枚举。 -- 栈运行时:`os/src/net/stack.rs` 持有 smoltcp runtime、socket set、loopback link 和 poll 推进入口。 -- Socket 层:`os/src/net/socket.rs` 保留 `SocketFile`、fd 映射和 UDP per-fd 队列,协议栈操作通过 `NetworkStack` 门面执行。 -- Syscall 层:`os/src/kernel/syscall/network.rs` 只处理用户 ABI、fd table、sockaddr 编解码和 errno 映射。 - -## 关键规则 - -- 设备驱动只注册 `NetDevice`,不直接创建接口、配置 IP 或操作 socket。 -- `SOCKET_SET`、`NET_IFACE`、smoltcp socket 类型不得暴露给 syscall 层。 -- `NetworkStack::poll()` 是协议栈推进和 poll/select 唤醒的统一入口。 -- `lo` 是显式 loopback 语义;不要用 `NullNetDevice` 伪装 loopback。 -- 架构差异只放在 `document/arch/*` 和 `os/src/arch/*`,不要进入网络核心实现。 - -## 维护入口 - -- 总体架构见 `document/net/architecture.md`。 -- 设备和接口见 `document/net/device_and_interface.md`。 -- 栈运行时见 `document/net/stack_runtime.md`。 -- Socket 和 syscall 见 `document/net/socket_syscall.md`。 -- Loopback 与 poll 见 `document/net/loopback_poll.md`。 -- 测试和排查见 `document/net/testing.md`、`document/net/netperf.md`。 - -## 回归要求 - -每次修改网络边界后至少执行: - -```bash -cd os -cargo fmt -cargo check -``` - -涉及行为修改时,还需要覆盖: - -- 接口枚举能看到 `lo`,有真实网卡时还能看到 `ethN`。 -- `set_network_interface_config()` 能正确设置和查询 IP、mask、gateway。 -- `socket/bind/listen/accept/connect/send/recv/sendto/recvfrom` 基础路径可用。 -- `ppoll/select` 能观察 TCP accept、TCP recv、UDP recv 可读事件。 -- netperf/netserver 已知 EINTR 行为不要误判为网络功能不可用。 +- 设备层: `os/src/device/net/` 定义和实现 `NetDevice`。 +- 接口层: `os/src/net/interface.rs` 维护接口 registry 和控制面配置。 +- 配置层: `os/src/net/config.rs` 初始化默认 IP, gateway 和 loopback。 +- 栈运行时: `os/src/net/stack/mod.rs` 持有 smoltcp runtime 和 poll 推进。 +- Socket 层: `os/src/net/socket.rs` 和 `os/src/net/unix_socket.rs` 暴露 VFS `File`。 +- Syscall 层: `os/src/kernel/syscall/network/` 处理用户 ABI。 + +## 维护规则 + +- 设备驱动只注册 `NetDevice`, 不配置 IP 或操作 socket。 +- syscall 层不要 import smoltcp socket 类型, 也不要直接访问 socket set。 +- 新的 AF_INET 行为优先加到 `NetworkStack` 门面, 再由 `SocketFile` 或 syscall 调用。 +- AF_UNIX 行为留在 `unix_socket.rs`, 不进入 smoltcp runtime。 +- 用户指针复制只能在 syscall 边界完成。 +- 状态变化后调用 `wake_poll_waiters()`, 但不要在硬中断中推进完整协议栈。 + +## 修改 checklist + +- 改设备注册: 检查 `register_net_device()` 和 `NetworkInterfaceManager`。 +- 改默认网络: 检查 `NetworkConfigManager::init_default_interface()` 和 loopback-only 分支。 +- 改 TCP/UDP: 检查 `NetworkStack`, `SocketFile`, network syscall ops 是否保持边界。 +- 改 poll: 检查 `io.rs`, `NetworkStack::poll()`, UDP dispatch 和 wakeup。 +- 改 fd 生命周期: 检查 `SocketFile::drop()`, exit cleanup 和 fd/socket map。 + +## 回归建议 + +- `cd os && cargo fmt` +- `cd os && cargo check` +- 启动后确认接口枚举能看到 `lo`。 +- 有真实 VirtIO NIC 时确认可见 `ethN`。 +- 覆盖 `socket/bind/listen/accept/connect/send/recv/sendto/recvfrom` 基础路径。 +- 覆盖 `poll/ppoll/select` 对 TCP accept, TCP recv, UDP recv 的可读观察。 +- 对 netperf/netserver 输出按 `netperf.md` 判断, 不把已知 `EINTR` 文案误判为网络失效。 + +## 已知限制 + +- 单 active runtime 是最大结构限制。 +- UDP per-port dispatcher 是兼容当前 smoltcp demux 的设计, 后续多 socket demux 可能重构。 +- loopback bounded drain 是测试兼容路径, 不能替代真实调度和中断模型。 + +## 源码索引 + +- `os/src/net/mod.rs` +- `os/src/net/interface.rs` +- `os/src/net/config.rs` +- `os/src/net/stack/mod.rs` +- `os/src/net/stack/adapter.rs` +- `os/src/net/socket.rs` +- `os/src/net/unix_socket.rs` +- `os/src/kernel/syscall/network/` diff --git a/document/net/socket_syscall.md b/document/net/socket_syscall.md index 7832e23a..6701e25b 100644 --- a/document/net/socket_syscall.md +++ b/document/net/socket_syscall.md @@ -1,35 +1,90 @@ # Socket 与 syscall -## SocketFile +Socket 层把网络 transport 暴露为 VFS `File`。AF_INET 使用 `SocketFile` 和 `NetworkStack`, AF_UNIX 使用 `UnixSocketFile` 的内核本地队列。 -`SocketFile` 是 VFS 文件对象,保存 fd 相关逻辑状态: +## 当前状态 -- stack socket handle。 -- listener backlog 和 accept 队列。 +- `socket(AF_INET, SOCK_STREAM/SOCK_DGRAM)` 创建 smoltcp TCP/UDP handle 和 `SocketFile`。 +- `socket(AF_UNIX, ...)` 创建 `UnixSocketFile`。 +- `socketpair()` 当前支持 AF_UNIX。 +- `bind/listen/connect/accept/send/recv/sendto/recvfrom/getsockname/getpeername/shutdown` 分散在 `network/**` ops 文件。 +- `setsockopt/getsockopt` 同时识别 AF_INET 和 AF_UNIX socket 文件。 + +## 目标 + +- syscall 层只处理 ABI: 用户指针, sockaddr 编解码, fd table 和 errno。 +- socket 文件保存 fd 局部状态, 如 flags, endpoint, shutdown 和 per-fd queue。 +- 协议栈行为通过 `NetworkStack` 或 `UnixSocketFile` 方法进入。 + +## 非目标 + +- 不在 syscall 层匹配 smoltcp TCP 内部状态。 +- 不把用户传入的 sockaddr 或 buffer 指针保存到 socket 对象。 +- 不在本文维护完整 errno 表。 + +## SocketFile 边界 + +`SocketFile` 保存: + +- optional `SocketHandle`。 +- listener backlog 和 listen queue。 - local/remote endpoint。 - UDP per-fd receive queue。 -- shutdown、flags、socket options。 +- shutdown 状态。 +- status flags 和 socket options。 + +`SocketFile` 实现 VFS `File`, 但实际 read/write/readable/writable/drop 都委托给 `NetworkStack`。 -`SocketFile::read()`、`write()`、`readable()`、`writable()`、`recvfrom()` 和 drop 路径通过 `NetworkStack` 方法进入协议栈。 +## UnixSocketFile 边界 + +AF_UNIX 不使用 smoltcp。它保存: + +- path 或 abstract address binding。 +- stream connection buffer。 +- datagram queue。 +- listener pending queue。 +- shutdown 状态和 socket options。 + +路径绑定会在 VFS 中创建 socket node, abstract binding 只在内核表中注册。 ## Syscall 边界 -`os/src/kernel/syscall/network.rs` 负责: +`os/src/kernel/syscall/network/` 负责: + +- 解析 domain/type/protocol 和 `SOCK_NONBLOCK`, `SOCK_CLOEXEC`。 +- 从 fd table 取得文件对象并 downcast 到 `SocketFile` 或 `UnixSocketFile`。 +- 复制用户 sockaddr, buffer 和 optval。 +- 写回 fd, sockaddr 和 optlen。 +- 将 `FsError`/`NetworkError` 转换成 Linux errno。 + +## I/O 和 poll 协作 + +通用 `read/write` 在 `io.rs` 中处理 `WouldBlock` 重试。对于网络 socket: + +- 阻塞路径会先请求 `poll_network_and_dispatch()`。 +- 让出 CPU 后检查可投递信号, 需要时返回 `EINTR`。 +- `file_read_ready()` 和 `file_write_ready()` 使用 socket 文件的 `readable/writable`。 +- 网络状态变化通过 `wake_poll_waiters()` 唤醒等待者。 + +## 并发和生命周期约束 -- 解析 syscall 参数。 -- 使用 `SumGuard` 访问用户指针。 -- 编解码 sockaddr。 -- 操作 fd table。 -- 将网络行为转发给 `NetworkStack` 或 socket 文件对象。 -- 返回 Linux errno。 +- fd 生命周期结束时, AF_INET `SocketFile::drop()` 必须释放 stack handle。 +- exit cleanup 关闭 fd 时会清理 `(tid, fd) -> SocketHandle` 映射。 +- UDP per-port 共享 socket 用 weak fd list, 避免 fd drop 后悬挂强引用。 +- AF_UNIX binding 在 `Drop` 中移除, stream peer shutdown 会唤醒等待者。 -syscall 层禁止: +## 已知限制 -- import `SOCKET_SET` 或 `NET_IFACE`。 -- import `smoltcp::socket::{tcp, udp}`。 -- 匹配 smoltcp TCP state。 -- 持有协议栈锁时复制用户缓冲区。 +- AF_INET 只覆盖当前测试所需 TCP/UDP 路径。 +- AF_UNIX 是最小本地 socket 实现, 不是完整 Linux unix socket。 +- 网络 syscall 的阻塞重试依赖当前 `io.rs` 的 yield/poll 模型, 尚非完整调度等待队列模型。 -## Errno +## 源码索引 -网络 syscall 应使用 `uapi::errno` 或 `FsError::to_errno()`。`set_network_interface_config()` 这类接口配置 syscall 不返回私有 `-1/-2/-3` 错误码。 +- `os/src/net/socket.rs`: AF_INET `SocketFile`, fd/socket map, sockaddr_in helper。 +- `os/src/net/unix_socket.rs`: AF_UNIX socket。 +- `os/src/kernel/syscall/network/socket_ops.rs`: socket, bind, listen, accept。 +- `os/src/kernel/syscall/network/connection_ops.rs`: connect, send, recv, shutdown。 +- `os/src/kernel/syscall/network/addr_ops.rs`: sendto, recvfrom, getsockname, getpeername。 +- `os/src/kernel/syscall/network/sockopt_ops.rs`: sockopt。 +- `os/src/kernel/syscall/io.rs`: read/write/poll retry 边界。 diff --git a/document/net/stack_runtime.md b/document/net/stack_runtime.md index 2ec16ef5..b9bd26e3 100644 --- a/document/net/stack_runtime.md +++ b/document/net/stack_runtime.md @@ -1,43 +1,75 @@ # 协议栈运行时 -`os/src/net/stack.rs` 是 smoltcp runtime 的归属层。 +`NetworkStack` 是 AF_INET 协议栈运行时的唯一宿主。它持有 smoltcp socket set, active interface runtime, loopback link, UDP dispatcher 和 TCP 延迟关闭队列。 -## NetworkStack +## 当前状态 -`NetworkStack` 对外提供稳定状态对象: +- `NetworkStack::init_network()` 安装当前 active smoltcp interface。 +- TCP/UDP socket 创建都通过 `NetworkStack` 向 socket set 添加 smoltcp socket。 +- `SocketFile` 的 read/write/readable/writable/drop 都回调 `NetworkStack`。 +- UDP bind 使用 per-port smoltcp socket 加 per-fd queue 的分发模型。 +- loopback frame 通过 `NetTxToken` 回灌到 `loopback_link`。 -- 创建 TCP/UDP socket。 -- TCP connect/listen/state/close/endpoints。 -- UDP dispatch 和 per-port attach。 -- SocketFile read/write/readable/writable/drop/sendto/recvfrom。 -- poll 和 bounded loopback drain。 +## 目标 -调用者不应直接访问 smoltcp socket set。 +- 不把 smoltcp `SocketSet` 暴露给 syscall 层。 +- 统一协议推进入口, 让 poll/select 和 I/O 路径看到一致状态。 +- 把 TCP listener pool, UDP per-port dispatch 和 close reaping 集中管理。 + +## 非目标 + +- 不支持多个 active `Interface` 同时运行。 +- 不在 `SocketFile` 中保存 smoltcp socket 本体。 +- 不在中断上下文直接执行完整 smoltcp poll。 ## 内部状态 -`net::stack` 持有: +- `socket_set`: smoltcp sockets。 +- `net_iface`: active `NetIfaceWrapper`, 拥有 `NetDeviceAdapter` 和 smoltcp `Interface`。 +- `loopback_link`: 127/8 发送帧的内部回灌队列。 +- `udp_ports`: UDP local port 到共享 smoltcp UDP socket 和 fd weak list 的映射。 +- `pending_tcp_close`: 等待 graceful close 后回收的 TCP handle。 + +## Poll 流程 + +`NetworkStack::poll()` 是主推进路径: + +1. 对 active interface 执行 smoltcp poll。 +2. 如果 loopback queue 产生新帧, 做有限额外 poll。 +3. 从共享 UDP socket drain datagram, 投递到匹配 fd 的 per-fd queue。 +4. 回收 pending TCP close。 +5. 如状态变化, 唤醒 poll/select waiters。 + +写入 loopback 目标后, `socket_write()` 和 `socket_sendto()` 会执行 bounded drain, 让本机测试不必等外部中断。 + +## UDP per-port dispatch + +smoltcp UDP demux 按 port 工作, 但 Linux 兼容场景允许多个 fd 绑定同一端口。当前实现为: + +- 每个 local port 使用一个共享 smoltcp UDP socket。 +- 每个 `SocketFile` 保存自己的 local/remote endpoint 和 rx queue。 +- poll 时从共享 socket drain datagram, 按 endpoint 投递到对应 fd 队列。 + +## TCP listener 和 close -- `socket_set`:smoltcp socket set。 -- `net_iface`:当前 active interface runtime。 -- `loopback_link`:当前单 runtime 下的 loopback frame queue。 -- `udp_ports`:UDP per-port dispatcher。 -- `pending_tcp_close`:等待 graceful close 回收的 TCP socket。 +`SocketFile` 保存 listener 状态, backlog 和 listen queue。`NetworkStack` 负责从队列中取出 established child socket, 维护备用 listener, 并在 `SocketFile::drop()` 时关闭或移除 TCP handle。 -这些状态都是 `NetworkStack` 字段,不作为裸全局符号暴露给 syscall 层或 socket 层。 +## 并发和生命周期约束 -## Poll 顺序 +- 调用者应通过 `NetworkStack` 方法访问 runtime 状态。 +- drop 路径必须处理 listener queue 中的子 handle。 +- UDP 共享 socket 的生命周期不能因某个 fd drop 而提前释放。 +- poll 唤醒和 socket 可读/可写查询必须避免持锁后复制用户缓冲区。 -`NetworkStack::poll()` 推进协议栈,并集中处理: +## 已知限制 -- smoltcp interface poll。 -- bounded loopback extra poll。 -- UDP datagram dispatch。 -- TCP graceful close reaping。 -- poll/select waiter wakeup。 +- 单 runtime 架构限制多接口路由。 +- bounded loopback drain 是兼容路径, 不应扩展成通用调度机制。 +- TCP buffer 大小和 listener pool 限制以源码常量为准。 -锁顺序保持为: +## 源码索引 -```text -NetworkStack -> interface runtime -> SocketSet -> SocketFile local state -``` +- `os/src/net/stack/mod.rs`: `NetworkStack`, poll, TCP/UDP runtime。 +- `os/src/net/stack/adapter.rs`: smoltcp `Device` adapter 和 loopback frame 回灌。 +- `os/src/net/socket.rs`: `SocketFile` 对 `NetworkStack` 的门面调用。 +- `os/src/kernel/syscall/io.rs`: poll/select waiter 和网络 poll 协作。 diff --git a/document/net/testing.md b/document/net/testing.md index bb726097..7a32c6b1 100644 --- a/document/net/testing.md +++ b/document/net/testing.md @@ -1,5 +1,7 @@ # 网络测试与排查 +本页只记录当前网络边界的验证点, 不作为完整测试计划。 + ## 基础检查 ```bash @@ -12,13 +14,31 @@ cargo check - 启动后接口枚举能看到 `lo`。 - 有真实 VirtIO NIC 时接口枚举能看到 `ethN`。 -- loopback-only 场景能使用 `127.0.0.1/8`。 -- `socket/bind/listen/accept/connect/send/recv/sendto/recvfrom` 基础路径可用。 -- `ppoll/select` 能观察 TCP accept、TCP recv 和 UDP recv 可读事件。 -- netperf/netserver 结果与 `netperf.md` 中记录的 EINTR 现象一致。 +- loopback-only 场景使用 `127.0.0.1/8`, 且没有错误默认网关。 +- AF_INET TCP: socket, bind, listen, accept, connect, send, recv。 +- AF_INET UDP: bind, sendto, recvfrom, connected UDP send/recv。 +- AF_UNIX: socketpair, stream read/write, datagram queue, path/abstract bind。 +- poll/select: TCP listener 可读, TCP recv 可读, UDP recv 可读。 +- fd 生命周期: close/exit 后 socket handle 不应被复用 fd 命中。 + +## 排查路径 + +- 接口缺失: 查 `register_net_device()` 是否被驱动调用, `NETWORK_INTERFACE_MANAGER` 是否已有接口。 +- loopback 不通: 查 `NetTxToken` 127/8 判定和 `NetworkStack::loopback_link` 是否有帧。 +- UDP poll 不醒: 查 `NetworkStack::udp_dispatch_drain_locked()` 是否在 poll 路径执行, per-fd queue 是否收到 datagram。 +- TCP accept 卡住: 查 listener queue, spare listener pool 和 `NetworkStack::poll()` 是否推进。 +- `EINTR` 暴露: 查 signal pending 和 `signal_interrupts_syscall()`, 不要先假设网络栈丢包。 +- syscalls 返回 `ENOTSOCK`: 查 fd table 中对象类型, 以及 `(tid, fd) -> SocketHandle` mapping 是否清理过早或遗漏。 + +## 已知现象 + +- netperf/netserver 的部分 `Interrupted system call` 输出是已知 signal/syscall restart 限制, 见 `netperf.md`。 +- loopback 写路径存在 bounded drain, 因此本机测试可能比外部设备路径更快观察到状态变化。 -## 常见问题 +## 源码索引 -- 如果 syscall 层出现 `SOCKET_SET`、`NET_IFACE` 或 `smoltcp::socket` import,说明边界回退了。 -- 如果有真实 NIC 时 127.0.0.1 流量尝试走设备发送,检查 `NetTxToken` 的 loopback frame 判定和 stack loopback link。 -- 如果 UDP poll/select 不醒,检查 `udp_dispatch_drain_locked()` 是否在 poll 路径执行,并确认 per-fd queue 是否收到 datagram。 +- `os/src/net/config.rs`: 默认接口和 loopback-only 配置。 +- `os/src/net/stack/mod.rs`: poll, UDP dispatch, TCP close。 +- `os/src/net/stack/adapter.rs`: loopback frame 回灌。 +- `os/src/kernel/syscall/io.rs`: poll/select 和 WouldBlock 重试。 +- `os/src/kernel/syscall/network/`: socket syscall。 diff --git a/document/sync/README.md b/document/sync/README.md index c55ee128..6d3c0b53 100644 --- a/document/sync/README.md +++ b/document/sync/README.md @@ -1,79 +1,109 @@ -# 同步与锁 (Synchronization & Locking) +# 同步与锁 -本文档概述了 `comix` 内核中用于处理并发和防止竞争条件的同步原语。 +`os/src/sync` 提供 Comix 内核当前使用的同步原语. 本文档只描述现行实现和设计边界, 不把未接入源码的历史方案当作可用 API. -## 1. 简介 +## 当前状态 -在多核或可抢占的内核中,当多个执行流(如不同核上的任务,或中断处理程序与被中断的任务)同时访问共享数据时,若不加协调,就会产生竞争条件(Race Condition),导致数据损坏和系统崩溃。 +当前导出的核心原语: -`sync` 模块提供了一系列同步原语(Synchronization Primitives),通过确保在任何时刻只有一个执行流能够访问临界区(Critical Section),来保证共享数据的完整性。 +- `RawSpinLock` +- `SpinLock` +- `RwLock` +- `Mutex` +- `IntrGuard` +- `PreemptGuard` +- `PerCpu` -### 导航 +当前 `os/src/sync` 中没有 `ticket_lock.rs` 或 `sleep_lock.rs`. 对应文档页仅用于说明历史或未实现状态, 不进入主导航. -- **[自旋锁 (`SpinLock`)](./spin_lock.md)**: 用于保护短临界区的互斥锁。 -- **[读写锁 (`RwLock`)](./rwlock.md)**: 允许多个读者并发访问或单个写者独占访问。 -- **[票号锁 (`TicketLock`)](./ticket_lock.md)**: 提供公平性保证的自旋锁,按 FIFO 顺序获取。 -- **[睡眠锁 (`SleepLock`)](./sleep_lock.md)**: 用于保护长临界区,会使等待者任务睡眠。 -- **[中断屏蔽 (`IntrGuard`)](./intr_guard.md)**: 用于在单核上实现临界区的底层机制。 -- **[Per-CPU 变量 (`PerCpu`)](./per_cpu.md)**: 每个 CPU 维护独立数据副本,避免锁竞争。 -- **[抢占控制 (`PreemptGuard`)](./preempt.md)**: 防止任务在访问 Per-CPU 数据期间被迁移。 -- **[锁顺序与死锁预防](./deadlock.md)**: 内核中必须遵守的锁获取规则。 -- **[SMP内核的中断与并发问题](./smp_interrupts.md)**: SMP系统中的中断和并发挑战。 +## 设计目标 -## 2. 核心概念 +- 用 RAII guard 绑定锁生命周期, 避免遗漏释放. +- 在短临界区内同时处理跨 CPU 竞争和本 CPU 中断重入. +- 为较长临界区提供会让出 CPU 的 `Mutex`. +- 为 per-CPU 数据提供抢占保护约束. +- 让底层锁可以作为 `lock_api::RawMutex` 服务第三方组件, 例如 talc allocator. -本内核主要采用三种策略来解决并发问题: +## 非目标 -1. **中断屏蔽 (Interrupt Disabling)**: 在单核处理器上,禁用中断可以防止当前代码被中断处理程序打断,从而避免了任务代码与中断代码之间的竞争。这是实现其他更复杂锁的底层基础。 -2. **原子操作与自旋 (Atomic Operations & Spinning)**: 在多核处理器上,仅屏蔽本地核心的中断是不够的,因为其他核心仍然可以访问共享数据。自旋锁利用CPU提供的原子操作(如 `amoswap`)来循环检查并获取锁。如果锁已被占用,它会"自旋"(在一个紧凑循环中等待),直到锁被释放。 -3. **Per-CPU 数据与抢占控制 (Per-CPU Data & Preemption Control)**: 为每个 CPU 核心维护独立的数据副本,完全避免锁竞争。通过禁用抢占防止任务在访问期间被迁移到其他核心,确保数据一致性。 +- 不提供严格 FIFO 公平锁. +- 不提供独立的无数据 `SleepLock`. +- 不提供用户态同步 ABI. +- 不保证所有锁可在中断上下文使用. 会调度或睡眠的路径不能在中断上下文使用. -## 3. 同步原语概览 +## 原语分层 -`comix` 提供了多种同步原语,适用于不同的场景: +```text +IntrGuard + - 禁用和恢复本 CPU 中断 + - 支持 per-CPU 嵌套深度 -| 原语 | 源码链接 | 核心机制 | 适用场景 | -|------------------|---------------------------------------------------------------------------------------|------------------------------------------------|--------------------------------------------------------------------------| -| **`SpinLock`**| [`os/src/sync/spin_lock.rs`](/os/src/sync/spin_lock.rs) | 屏蔽中断 + 原子操作自旋 | 保护访问耗时**极短**的共享数据,例如修改一个计数器或链表指针。 | -| **`RwLock`** | [`os/src/sync/rwlock.rs`](/os/src/sync/rwlock.rs) | 原子操作 + 读写分离 | 读多写少场景,允许多个读者并发访问。 | -| **`TicketLock`**| [`os/src/sync/ticket_lock.rs`](/os/src/sync/ticket_lock.rs) | 原子操作 + 票号机制 | 需要严格公平性的场景,防止饥饿。 | -| **`SleepLock`** | [`os/src/sync/sleep_lock.rs`](/os/src/sync/sleep_lock.rs) | 原子操作 + 任务睡眠 (`WaitQueue`) | 保护访问耗时**较长**的共享数据,例如执行I/O操作或复杂的计算。 | -| **`IntrGuard`** | [`os/src/sync/intr_guard.rs`](/os/src/sync/intr_guard.rs) | 屏蔽/恢复中断 (RAII) | 作为其他锁的底层实现,或在确定为单核且无需锁的场景下临时屏蔽中断。 | -| **`PerCpu`** | [`os/src/sync/per_cpu.rs`](/os/src/sync/per_cpu.rs) | 每核独立数据副本 + 抢占控制 | 频繁访问的统计计数器、Per-CPU 运行队列、对象缓存等无锁场景。 | -| **`PreemptGuard`**| [`os/src/sync/preempt.rs`](/os/src/sync/preempt.rs) | 禁用抢占 (RAII) | 访问 Per-CPU 变量时防止任务迁移,保护短临界区。 | -| `RawSpinLock` | [`os/src/sync/raw_spin_lock.rs`](/os/src/sync/raw_spin_lock.rs) | 纯粹的原子操作自旋 | `SpinLock` 和 `SleepLock` 的内部构件,不推荐直接使用。 | +RawSpinLock + - AtomicBool 互斥 + - 进入时持有 IntrGuard + - 也实现 lock_api::RawMutex -## 4. 设计哲学:RAII 与锁守卫 +SpinLock + - RawSpinLock + UnsafeCell + - 短临界区互斥 -为了防止因忘记释放锁而导致的死锁,本模块广泛采用了 **RAII (Resource Acquisition Is Initialization)** 设计模式。 +RwLock + - AtomicUsize 状态 + - 多读或单写 + - 持 guard 期间禁用本 CPU 中断 -- 当调用 `lock()` 方法时,会返回一个**锁守卫 (Lock Guard)** 对象(例如 `SpinLockGuard`)。 -- 这个守卫对象在其生命周期内持有锁,并提供对受保护数据的安全访问(通过 `Deref` 和 `DerefMut`)。 -- 当守卫对象离开其作用域时,它的 `drop()` 方法会自动被调用,从而**自动释放锁**。 +Mutex + - AtomicBool + RawSpinLock + WaitQueue + - 竞争时入队并 yield -这种设计极大地提升了锁使用的安全性。 - -```rust -// 示例: -let data = SpinLock::new(0); - -// lock() 返回一个守卫对象 guard -let mut guard = data.lock(); - -// 通过 guard 安全地访问被保护的数据 -*guard += 1; - -// 当 guard 离开作用域时,锁会自动释放,无需手动调用 unlock() +PreemptGuard + PerCpu + - 防止访问当前 CPU 数据时任务迁移 ``` -## 5. 注意事项:死锁 - -使用锁时必须警惕**死锁 (Deadlock)**。一个常见的死锁场景是锁顺序反转: - -- 任务A: `lock(L1); lock(L2);` -- 任务B: `lock(L2); lock(L1);` - -如果任务A持有L1并等待L2,而任务B持有L2并等待L1,两个任务将永远等待下去。 - -**规则**: 在整个内核中,如果需要同时获取多个锁,必须始终**按照相同的顺序**获取它们。 -详见[锁顺序与死锁预防](./deadlock.md) \ No newline at end of file +## 选择建议 + +| 场景 | 当前原语 | +| --- | --- | +| 极短共享数据修改 | `SpinLock` | +| 读多写少且临界区短 | `RwLock` | +| allocator 或底层锁适配 | `RawSpinLock` | +| 可能等待较久的任务上下文互斥 | `Mutex` | +| 单 CPU 中断重入屏蔽 | `IntrGuard` | +| 访问当前 CPU 本地数据 | `PreemptGuard` + `PerCpu` | + +## 并发约束 + +- `SpinLock`, `RawSpinLock`, `RwLock` 都会屏蔽本 CPU 中断, 但仍依赖原子操作处理跨 CPU 竞争. +- 自旋锁类临界区必须短, 不能主动 sleep 或长期等待调度. +- `Mutex` 可能调用调度相关路径, 只适合任务上下文. +- `PerCpu::get_mut()` 从共享引用返回当前 CPU 的可变引用, 调用方必须用 `PreemptGuard` 或等价机制保证期间不会迁移. + +## 文档导航 + +当前设计页: + +- [RawSpinLock](raw_spin_lock.md) +- [SpinLock](spin_lock.md) +- [RwLock](rwlock.md) +- [Mutex](mutex.md) +- [IntrGuard](intr_guard.md) +- [PreemptGuard](preempt.md) +- [PerCpu](per_cpu.md) +- [死锁预防](deadlock.md) +- [SMP 中断与并发](smp_interrupts.md) + +历史状态页: + +- [TicketLock 状态](ticket_lock.md) +- [SleepLock 状态](sleep_lock.md) + +## 源码索引 + +- `os/src/sync/mod.rs:5` - 当前模块列表和导出集合. +- `os/src/sync/raw_spin_lock.rs:20` - `RawSpinLock`. +- `os/src/sync/spin_lock.rs:30` - `SpinLock`. +- `os/src/sync/rwlock.rs:24` - `RwLock`. +- `os/src/sync/mutex.rs:21` - `Mutex`. +- `os/src/sync/intr_guard.rs:44` - `IntrGuard`. +- `os/src/sync/preempt.rs:95` - `PreemptGuard`. +- `os/src/sync/per_cpu.rs:36` - `PerCpu`. diff --git a/document/sync/deadlock.md b/document/sync/deadlock.md index 9649167e..a47c86da 100644 --- a/document/sync/deadlock.md +++ b/document/sync/deadlock.md @@ -1,78 +1,57 @@ -# 锁顺序与死锁预防 (Lock Ordering & Deadlock Prevention) +# 锁顺序与死锁预防 -本文档详细阐述了死锁(Deadlock)的成因,并为 `comix` 内核建立了一套必须严格遵守的锁获取顺序规则,以从根本上预防死锁的发生。 +死锁预防依赖一致的锁顺序和对上下文的限制.当前同步原语中, `SpinLock`, `RawSpinLock`, `RwLock` 属于自旋类短临界区, `Mutex` 属于任务上下文等待类互斥. -## 1. 死锁问题详解 +## 当前规则 -死锁,或称“死锁拥抱”(Deadly Embrace),是多任务系统中一个经典且致命的问题。当两个或更多的执行流(任务或中断)各自持有一个锁,并试图获取对方持有的锁时,它们将陷入无限的等待循环,导致系统部分或全部功能瘫痪。 +1. 不要在持有自旋类锁时等待会调度的资源. +2. 不要在中断上下文获取 `Mutex`. +3. 多把锁嵌套时保持全局一致顺序. +4. 持有 `IntrGuard` 或自旋类 guard 的代码必须短. +5. 访问 `PerCpu` 当前 CPU 可变副本时持有 `PreemptGuard`; 如果中断也访问同一数据, 额外处理中断重入. -### 典型死锁场景 +## 推荐锁顺序 -假设我们有两个任务(任务A,任务B)和两个锁(锁L1,锁L2)。 +从低层到高层: -1. **任务A** 获取了 **锁L1**。 -2. **任务B** 获取了 **锁L2**。 -3. 此时,**任务A** 尝试获取 **锁L2**,但因为任务B持有该锁,任务A进入等待状态。 -4. 接着,**任务B** 尝试获取 **锁L1**,但因为任务A持有该锁,任务B也进入等待状态。 +1. CPU 本地状态 guard: `PreemptGuard`, `IntrGuard`. +2. 底层自旋锁: `RawSpinLock`. +3. 数据自旋锁: `SpinLock`, `RwLock`. +4. 等待队列和任务上下文互斥: `Mutex` 及其内部 `WaitQueue`. +5. 文件系统, 进程, 设备等更高层对象锁. -至此,任务A在等待任务B释放L2,而任务B在等待任务A释放L1。两者都无法继续执行来释放自己持有的锁,从而形成永久的僵局。 +实际代码如果需要更细锁序, 应在所属子系统文档中声明. -## 2. 解决方案:建立严格的锁顺序 +## 常见风险 -预防死锁最简单有效的方法是**资源排序(Resource Ordering)**。我们为系统中的所有锁定义一个全局的、唯一的获取顺序。任何代码,在任何时候,如果需要获取多个锁,都**必须**按照这个预定义的顺序来获取。 +### 自旋锁内等待 Mutex -通过强制执行这个规则,我们打破了死锁形成的循环等待条件。在上面的例子中,如果规定必须先获取L1再获取L2,那么任务B的执行路径 `lock(L2); lock(L1);` 将是不被允许的,它必须改为 `lock(L1); lock(L2);`。这样,当任务A持有L1时,任务B会直接在尝试获取L1时阻塞,而不会先获取L2,从而避免了死锁。 +`Mutex` 竞争时会把任务入队并 `yield_task()`.如果当前已经持有自旋锁或关闭中断, 调度和唤醒路径可能无法前进. -## 3. `comix` 内核锁顺序规则 +### 中断上下文等待任务资源 -根据 `comix` 内核的现有实现,我们定义以下从 **高到低** 的锁获取层级。获取锁时,**必须从高层级的锁向低层级的锁获取**。严禁在持有低层级锁的情况下,尝试获取一个更高层级的锁。 +中断处理程序没有普通任务睡眠语义, 只能使用不会 sleep 的同步路径, 并且临界区必须短. -| 层级 | 锁 | 保护对象 | 备注 | -|------|----------------------------------|-----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------| -| **1**| `TASK_MANAGER` 全局锁 | 全局任务列表 `tasks` 和 `tid_allocator` | 最高级别的锁,用于任务的创建和全局查找。应极力缩短持有时间。 | -| **2**| `WaitQueue` 内部锁 (`queue.lock`) | `WaitQueue` 中的任务队列 `tasks` | 由于需要修改任务调度状态,必须在内部持有SCHEDULER锁。 | -| **3**| `SCHEDULER` 全局锁 | 调度器的运行队列 `run_queue` 和其他调度状态 | 负责任务的调度、睡眠和唤醒。持有此锁时可以修改任务状态。 | -| **4**| `CPU` 本地数据锁 (`current_cpu().lock()`) | `Cpu` 结构,主要是 `current_task` 指针 | 用于安全地获取或修改当前CPU正在运行的任务。 | -| **5**| 单个 `Task` 实例锁 (`task.lock()`) | `Task` 结构体的独占内部字段(如 `state`, `context`) | 用于修改单个任务的内部状态。 | -| **6**| `Task` 字段锁 (`children.lock`) | `Task` 中的可变共享字段 `tasks` | 用于修改线程间共享的状态。 +### Per-CPU 数据迁移 -### 核心规则详解 +没有 `PreemptGuard` 时, 任务可能先根据 CPU A 的 ID 取到引用, 随后迁移到 CPU B 继续访问, 破坏 per-CPU 语义. -1. **严禁逆序**:最核心的规则。例如,你**不能**在持有 `task.lock()` (层级4) 的情况下,去尝试获取 `SCHEDULER` 锁 (层级2)。 +### RwLock 升级 -2. **`TASK_MANAGER` 优先**:任何需要遍历全局任务列表的操作,都必须首先获取 `TASK_MANAGER` 锁。通常,操作完成后应尽快释放它。 +当前 `RwLock` 不支持读锁升级写锁.持读锁时再尝试写锁可能等待自己释放. -3. **调度器 (`SCHEDULER`) 锁**:当需要修改运行队列(如添加、移除任务)或批量改变任务状态时,应获取此锁。调度器在持有此锁时,可以进一步获取单个 `Task` 的锁 (层级4) 来修改其 `state` 字段。 - * **正确示例 (`rr_scheduler.rs::wake_up`)**: - ```rust - // 1. 获取 SCHEDULER 锁 (隐式地,因为在 &mut self 方法内) - // 2. 获取 task 锁 (层级4) - task.lock().state = TaskState::Running; - // 3. 将 task 添加到 run_queue - self.run_queue.add_task(task); - ``` +## 已知限制 -4. **`SleepLock` 与 `WaitQueue` 的特殊模式**: - - `SleepLock` 在判断需要让任务睡眠时,会获取 `WaitQueue` 的内部锁 (层级5),将任务添加到等待队列,然后 **立即释放** `WaitQueue` 锁。 - - 在释放了所有锁之后,才会调用 `sleep_task()` 和 `schedule()` 等可能引起任务调度的函数。 - - **这是至关重要的模式**:**决不能在持有任何自旋锁的情况下调用会导致当前任务睡眠或调度的函数**,否则会造成持有锁的CPU被切换走,其他CPU或任务将永远无法获得该锁,导致系统死锁。 +- 当前锁实现没有统一的 owner 追踪和死锁检测. +- `Mutex` 唤醒等待者后仍由任务重新竞争, 不提供严格公平顺序. +- `TicketLock` 不是当前实现能力, 不能依赖 FIFO 锁顺序解决饥饿. -### 实践中的例子 +## 源码索引 -#### 场景:终止一个任务 (`terminate_task`) - -1. `terminate_task` 首先通过 `current_cpu().lock()` (层级3) 获取到当前任务的句柄 `task`。 -2. 然后获取 `task.lock()` (层级4) 来修改任务状态为 `Stopped`。 -3. 释放 `task.lock()`。 -4. 最后调用 `schedule()`,此时已不持有任何锁。`schedule()` 内部会获取 `SCHEDULER` 锁 (层级2) 来将该任务从调度系统中移除。 - -这个流程严格遵守了从高到低(虽然这里没有跨层级获取)的顺序和不在持锁状态下调度的原则。 - -#### 场景:创建一个内核线程 (`kthread_spawn`) - -1. `kthread_spawn` 调用 `TASK_MANAGER.lock()` (层级1) 来分配 `tid` 和创建 `Task` 对象。 -2. 在持有 `TASK_MANAGER` 锁期间,它可能会初始化 `Task` 的部分数据。 -3. 创建完成后,它调用 `SCHEDULER.lock()` (层级2) 的 `add_task` 方法,将新任务加入运行队列。 - * 注意:这里是先释放了层级1的锁,再获取层级2的锁,是安全的。如果需要同时持有,必须先获取 `TASK_MANAGER` 再获取 `SCHEDULER`。 - -通过在整个内核中强制实施这套简单的层级规则,可以从设计上根除绝大多数死锁问题,极大地提升系统的稳定性和可维护性。 \ No newline at end of file +- `os/src/sync/raw_spin_lock.rs:80` - 自旋类底层加锁. +- `os/src/sync/spin_lock.rs:53` - 带数据自旋锁. +- `os/src/sync/rwlock.rs:51` - 读锁获取. +- `os/src/sync/rwlock.rs:79` - 写锁获取. +- `os/src/sync/mutex.rs:41` - 等待式互斥获取. +- `os/src/sync/preempt.rs:95` - 抢占 guard. +- `os/src/sync/intr_guard.rs:44` - 中断 guard. diff --git a/document/sync/intr_guard.md b/document/sync/intr_guard.md index 79d31674..5c33474a 100644 --- a/document/sync/intr_guard.md +++ b/document/sync/intr_guard.md @@ -1,27 +1,50 @@ -# 中断屏蔽 (`IntrGuard`) +# IntrGuard -`IntrGuard` 是一个基于RAII模式的工具,用于在代码的特定作用域内安全地禁用和恢复CPU中断。它是实现其他同步原语(如 `SpinLock`)的底层基石。 +`IntrGuard` 是本 CPU 中断屏蔽的 RAII guard.它解决的是本 CPU 上"任务代码被中断处理程序打断"造成的重入问题, 不是跨 CPU 互斥. -**源码链接**: [`os/src/sync/intr_guard.rs`](/os/src/sync/intr_guard.rs) +## 当前状态 -## 1. 工作原理 +- 通过 `CpuOps` 禁用和恢复中断. +- 使用 per-CPU 嵌套深度 `INTR_DEPTH`. +- 只有最外层 guard 保存进入前 flags 并在 drop 时恢复. +- 内层 guard drop 不会提前打开中断. -在单核或不考虑多核并发的场景下,禁用中断是实现原子操作的最简单有效的方法。当中断被禁用时,当前CPU核心不会响应任何外部中断(如时钟中断),因此当前执行的代码流不会被中断处理程序打断,从而保证了操作的原子性。 +## 目标 -`IntrGuard` 的实现非常直接: +- 为 `RawSpinLock`, `SpinLock`, `RwLock` 提供本 CPU 中断保护. +- 允许嵌套使用而不破坏外层临界区. +- 用 RAII 避免异常返回路径遗漏恢复中断状态. -1. **创建 (`new`)**: 当一个 `IntrGuard` 对象被创建时,它会读取 `sstatus` 寄存器中当前的中断使能位(`SIE`),将其保存起来,然后清除 `SIE` 位以禁用中断。 -2. **销毁 (`drop`)**: 当 `IntrGuard` 对象离开作用域时,其 `Drop` 实现会自动被调用。它会根据创建时保存的原始状态,恢复 `sstatus` 寄存器中的 `SIE` 位,从而恢复中断。 +## 非目标 -## 2. 核心接口 +- 不阻止其他 CPU 并发访问同一数据. +- 不表示抢占禁用语义.访问 per-CPU 数据应使用 `PreemptGuard` 或等价机制. +- 不应包裹长时间运行或可能调度的代码. -- `pub fn new() -> Self`: 创建一个 `IntrGuard` 实例,立即禁用中断。 +## 关键流程 -## 3. 适用场景 +1. 读取当前 CPU 的中断保护深度. +2. 如果深度为 0, 调用 `CPU::disable_interrupts()` 并保存 flags. +3. 增加 per-CPU 深度并建立 acquire fence. +4. drop 时建立 release fence, 减少深度. +5. 如果 drop 的是最外层 guard, 恢复保存的 flags. -- **作为其他锁的构件**: `SpinLock` 在获取锁时会创建一个 `IntrGuard`,以防止在持有自旋锁的同时被本地中断打断,这是一种标准的组合用法。 -- **临时的、短小的临界区**: 在某些非常底层的代码中,如果可以确定操作极快且不会与其他核心冲突,可以临时使用 `IntrGuard` 来保证原子性。 +## 并发约束 -**警告**: -- `IntrGuard` **无法阻止来自其他CPU核心的并发访问**。在多核环境下,它必须与自旋锁等其他机制结合使用,才能保证真正的互斥。 -- 滥用中断屏蔽会导致系统响应延迟(例如,无法及时响应时钟中断和I/O中断),因此应尽可能缩短禁用中断的时间。 \ No newline at end of file +- guard 必须在创建它的 CPU 上 drop.迁移会破坏 per-CPU 深度和 flags 对应关系. +- 嵌套 guard 的 `was_enabled()` 返回 false, 因为它进入时中断已经由外层关闭. +- `IntrGuard` 可组合原子锁形成 SMP 安全锁, 但单独使用只适合 CPU 本地临界区. + +## 已知限制 + +- 深度数组大小由 `MAX_CPU_COUNT` 固定. +- 不记录调用栈或所有者, 不能诊断中断长期关闭的来源. + +## 源码索引 + +- `os/src/sync/intr_guard.rs:23` - per-CPU 缓存行对齐计数器. +- `os/src/sync/intr_guard.rs:33` - `INTR_DEPTH`. +- `os/src/sync/intr_guard.rs:37` - `SAVED_INTR_FLAGS`. +- `os/src/sync/intr_guard.rs:44` - `IntrGuard`. +- `os/src/sync/intr_guard.rs:55` - 创建和嵌套处理. +- `os/src/sync/intr_guard.rs:85` - drop 恢复中断状态. diff --git a/document/sync/mutex.md b/document/sync/mutex.md new file mode 100644 index 00000000..36896561 --- /dev/null +++ b/document/sync/mutex.md @@ -0,0 +1,53 @@ +# Mutex + +`Mutex` 是当前同步模块中的睡眠式互斥原语.和 `SpinLock` 不同, 它在竞争时会把当前任务加入等待队列并让出 CPU. + +## 当前状态 + +- 源码文件是 `os/src/sync/mutex.rs`. +- 状态由 `AtomicBool locked` 表示. +- 内部 `RawSpinLock` 保护检查和入队过程. +- 等待者保存在 `SpinLock` 中. +- 成功持锁时返回 `MutexGuard`, guard drop 时清除 `locked` 并唤醒等待者. + +## 目标 + +- 为任务上下文中的较长临界区提供互斥. +- 竞争时避免长时间忙等. +- 用 guard 绑定解锁和唤醒. + +## 非目标 + +- 不适合中断上下文. +- 不提供严格 FIFO 唤醒语义. +- 不替代所有短临界区自旋锁.热路径仍应优先考虑 `SpinLock` 或更细粒度设计. + +## 关键流程 + +1. 获取内部 `RawSpinLock`. +2. 如果 `locked` 原来为 false, 设置为 true 并返回 `MutexGuard`. +3. 如果锁已占用, 获取当前任务, 把任务放入等待队列. +4. 释放内部 raw lock, 调用 `yield_task()` 让出 CPU. +5. 被唤醒后重新尝试. +6. guard drop 时把 `locked` 置 false 并唤醒等待队列. + +## 并发与生命周期约束 + +- `Mutex` 依赖调度器和 `WaitQueue`, 只能在有当前任务的上下文使用. +- 不要在持有 `SpinLock` 或禁用中断的长路径中等待 `Mutex`. +- guard 当前保存了内部 raw lock guard, 因此持有 mutex 数据期间也会保持 raw lock 的中断保护语义.调用方仍应避免长时间关中断的工作. +- 唤醒使用 `wake_up_all()`, 被唤醒任务会重新竞争 `locked`. + +## 已知限制 + +- 当前实现更接近初版睡眠互斥, 等待队列公平性和惊群控制仍可改进. +- 没有 `try_lock()`. +- 没有所有者检查或死锁诊断. + +## 源码索引 + +- `os/src/sync/mutex.rs:21` - `Mutex` 字段. +- `os/src/sync/mutex.rs:31` - 构造. +- `os/src/sync/mutex.rs:41` - 竞争和等待流程. +- `os/src/sync/mutex.rs:60` - `MutexGuard`. +- `os/src/sync/mutex.rs:79` - guard drop 解锁和唤醒. diff --git a/document/sync/per_cpu.md b/document/sync/per_cpu.md index d3686cde..071c27a2 100644 --- a/document/sync/per_cpu.md +++ b/document/sync/per_cpu.md @@ -1,360 +1,61 @@ -# Per-CPU 变量机制 +# PerCpu -## 简介 +`PerCpu` 为每个 CPU 保存一份独立数据.它用空间换取低竞争访问, 适合统计计数器, 当前 CPU 缓存和 CPU 本地状态. -Per-CPU 变量机制允许每个 CPU 核心维护独立的数据副本,从而避免多核环境下的锁竞争。这是一种重要的无锁并发优化技术,特别适用于频繁访问的共享数据。 +## 当前状态 -在多核系统中,如果多个 CPU 核心频繁访问同一个共享变量,即使使用自旋锁保护,也会因为缓存一致性协议(如 MESI)导致严重的性能开销。Per-CPU 变量通过为每个核心分配独立的数据副本,使得每个核心只访问自己的副本,从而完全消除了锁竞争和缓存行抖动(Cache Line Bouncing)。 +- 数据存储为 `Vec>`. +- 每个元素用 64 字节对齐减少 false sharing. +- CPU ID 来自 `CpuOps::id()`. +- `get()` 和 `get_mut()` 访问当前 CPU 副本. +- `get_of(cpu_id)` 读取指定 CPU 副本. -## 核心概念 +## 目标 -### 数据隔离 +- 避免所有 CPU 频繁争用同一把锁. +- 给 CPU 本地数据提供简单容器. +- 和 `PreemptGuard` 配合保证访问当前 CPU 副本期间不会迁移. -Per-CPU 变量的核心思想是**数据隔离**: -- 每个 CPU 核心拥有独立的数据副本 -- 核心只访问自己的副本,不与其他核心共享 -- 避免了缓存一致性开销和锁竞争 +## 非目标 -### 抢占保护 +- 不自动禁用抢占. +- 不提供跨 CPU 汇总一致性快照. +- 不保证 `T` 内部操作原子.需要跨 CPU 读取或汇总时, `T` 自身仍可能需要原子类型或锁. -访问 Per-CPU 变量时必须禁用抢占,原因是: -- 如果任务在访问 Per-CPU 变量期间被抢占并迁移到另一个核心 -- 它会访问到错误核心的数据副本,导致数据不一致 -- 通过禁用抢占,确保任务在访问期间不会被迁移 +## 关键流程 -## 数据结构 +### 创建 -### PerCpu +`PerCpu::new()` 从 `kernel::num_cpu()` 获取 CPU 数量, 为每个 CPU 调用初始化闭包.`new_with_id()` 把 CPU ID 传给闭包.测试或特殊路径可用 `new_with_id_and_count()` 指定数量. -```rust -pub struct PerCpu { - data: Vec>, -} -``` +### 当前 CPU 访问 -`PerCpu` 是 Per-CPU 变量的容器,内部维护一个 `Vec`,每个元素对应一个 CPU 核心的数据副本。 +`get()` 和 `get_mut()` 使用当前 CPU ID 索引数据.`get_mut()` 从共享引用返回可变引用, 因此调用方必须保证当前任务不会迁移, 且同一 CPU 上没有并发可变访问. -**缓存行对齐优化**:每个数据副本使用 `CacheAligned` 包装,确保独占一个缓存行(64 字节)。这避免了伪共享(False Sharing)问题——当多个 CPU 核心修改位于同一缓存行内的不同数据时,会导致缓存行频繁失效和同步,严重影响性能。通过缓存行对齐,每个核心的数据副本互不干扰,充分发挥 Per-CPU 变量的性能优势。 +### 指定 CPU 读取 -**源码位置**:`os/src/sync/per_cpu.rs:32` +`get_of(cpu_id)` 只做范围检查并返回指定副本引用.它不提供快照一致性, 适合诊断或调用方已有同步的场景. -## API 接口 +## 并发与生命周期约束 -### 创建 Per-CPU 变量 +- 访问当前 CPU 可变副本前应持有 `PreemptGuard`. +- 如果中断处理程序也访问同一 per-CPU 数据, 还需要考虑中断屏蔽或内部原子性. +- `PerCpu` 的 `Send`/`Sync` 约束要求 `T: Send`, 但这不代表任意访问模式都无竞争. +- CPU 数量必须在创建前初始化, 否则会断言失败. -```rust -pub fn new T>(init: F) -> Self -``` +## 已知限制 -创建一个 Per-CPU 变量,为每个 CPU 核心调用初始化函数 `init` 创建独立的数据副本。 +- CPU 热插拔不在当前设计内. +- 数据副本数量创建后固定. +- 没有内置遍历汇总 API. -**参数**: -- `init`: 初始化函数,为每个 CPU 创建一个数据副本 +## 源码索引 -**Panics**: -- 如果 `NUM_CPU` 未设置或为 0,会 panic - -**示例**: -```rust -use sync::PerCpu; -use core::sync::atomic::AtomicUsize; - -// 为每个 CPU 创建一个独立的计数器 -let counter = PerCpu::new(|| AtomicUsize::new(0)); -``` - -**源码位置**:`os/src/sync/per_cpu.rs:44` - -### 获取当前 CPU 的数据(只读) - -```rust -pub fn get(&self) -> &T -``` - -获取当前 CPU 核心的数据副本的只读引用。 - -**Safety 要求**: -1. 当前 CPU ID 必须有效(< NUM_CPU) -2. 访问期间抢占必须已禁用(防止任务迁移) - -**示例**: -```rust -use sync::{PerCpu, preempt_disable, preempt_enable}; - -let counter = PerCpu::new(|| 0usize); - -preempt_disable(); -let value = counter.get(); -println!("Current CPU counter: {}", value); -preempt_enable(); -``` - -**源码位置**:`os/src/sync/per_cpu.rs:63` - -### 获取当前 CPU 的数据(可变) - -```rust -pub fn get_mut(&self) -> &mut T -``` - -获取当前 CPU 核心的数据副本的可变引用。 - -**Safety 要求**: -1. 当前 CPU ID 必须有效 -2. 访问期间抢占必须已禁用 -3. 没有其他引用指向同一数据 - -**设计说明**: - -此方法从 `&self` 返回 `&mut T`,这是 Per-CPU 变量的标准实现模式: -- Per-CPU 变量通常作为全局 `static` 使用,只能通过 `&self` 访问 -- 每个 CPU 访问不同的数据副本,通过抢占控制保证独占访问 -- 使用 `UnsafeCell` 提供内部可变性,类似于 `RefCell` 或 `Mutex` - -这种设计允许 Per-CPU 变量作为静态全局变量使用,同时保持无锁的高性能特性。 - -**示例**: -```rust -use sync::{PerCpu, preempt_disable, preempt_enable}; - -let counter = PerCpu::new(|| 0usize); - -preempt_disable(); -let value = counter.get_mut(); -*value += 1; -preempt_enable(); -``` - -**源码位置**:`os/src/sync/per_cpu.rs:86` - -### 获取指定 CPU 的数据(只读) - -```rust -pub fn get_of(&self, cpu_id: usize) -> &T -``` - -获取指定 CPU 核心的数据副本的只读引用。用于跨核访问,例如负载均衡时查看其他 CPU 的队列长度。 - -**参数**: -- `cpu_id`: 目标 CPU 的 ID - -**Panics**: -- 如果 `cpu_id` 无效(>= NUM_CPU),会 panic - -**示例**: -```rust -use sync::PerCpu; - -let counter = PerCpu::new(|| 0usize); - -// 查看 CPU 0 的计数器值 -let value = counter.get_of(0); -println!("CPU 0 counter: {}", value); -``` - -**源码位置**:`os/src/sync/per_cpu.rs:96` - -## 使用场景 - -### 1. 统计计数器 - -Per-CPU 变量最常见的用途是实现高性能的统计计数器: - -```rust -use sync::PerCpu; -use core::sync::atomic::{AtomicUsize, Ordering}; - -// 每个 CPU 维护独立的中断计数器 -static INTERRUPT_COUNT: PerCpu = PerCpu::new(|| AtomicUsize::new(0)); - -fn handle_interrupt() { - // 中断处理程序中,抢占已自动禁用 - INTERRUPT_COUNT.get().fetch_add(1, Ordering::Relaxed); -} - -fn get_total_interrupts() -> usize { - let num_cpu = unsafe { crate::kernel::NUM_CPU }; - let mut total = 0; - for cpu_id in 0..num_cpu { - total += INTERRUPT_COUNT.get_of(cpu_id).load(Ordering::Relaxed); - } - total -} -``` - -### 2. Per-CPU 运行队列 - -调度器可以为每个 CPU 维护独立的运行队列,避免锁竞争: - -```rust -use sync::PerCpu; -use alloc::collections::VecDeque; - -struct RunQueue { - tasks: VecDeque, -} - -static RUN_QUEUES: PerCpu> = PerCpu::new(|| { - SpinLock::new(RunQueue { tasks: VecDeque::new() }) -}); - -fn enqueue_task(task: TaskRef) { - preempt_disable(); - let queue = RUN_QUEUES.get().lock(); - queue.tasks.push_back(task); - preempt_enable(); -} -``` - -### 3. Per-CPU 缓存 - -为每个 CPU 维护独立的对象缓存,减少分配器竞争: - -```rust -use sync::PerCpu; - -struct ObjectCache { - free_list: Vec, -} - -static CACHE: PerCpu> = PerCpu::new(|| { - ObjectCache { free_list: Vec::new() } -}); - -fn alloc_object() -> Option { - preempt_disable(); - let cache = CACHE.get_mut(); - let obj = cache.free_list.pop(); - preempt_enable(); - obj -} -``` - -## 设计考量 - -### 优势 - -1. **零锁开销**:完全避免锁竞争,每个核心独立访问自己的数据 -2. **缓存友好**:数据副本通常位于核心的本地缓存中,访问延迟低 -3. **可扩展性**:性能随核心数线性扩展,不会因核心增加而退化 - -### 劣势 - -1. **内存开销**:每个核心都有独立副本,内存使用量是单副本的 N 倍(N 为核心数) -2. **聚合成本**:如果需要获取全局视图(如总计数),需要遍历所有核心的副本 -3. **抢占限制**:访问期间必须禁用抢占,可能影响实时性 - -### 适用场景 - -Per-CPU 变量适用于: -- 频繁写入的统计计数器 -- 每个核心独立维护的数据结构(如运行队列) -- 需要高并发性能的场景 - -不适用于: -- 需要频繁聚合的数据 -- 内存受限的环境 -- 数据副本很大的情况 - -## 线程安全性 - -`PerCpu` 实现了 `Send` 和 `Sync` trait: - -```rust -unsafe impl Send for PerCpu {} -unsafe impl Sync for PerCpu {} -``` - -这是安全的,因为: -- 每个 CPU 访问不同的数据副本,不存在数据竞争 -- 通过抢占控制保证任务不会在访问期间迁移 -- 跨核访问(`get_of`)只提供只读引用,不会产生竞争 - -## 与抢占控制的配合 - -Per-CPU 变量必须与抢占控制配合使用: - -```rust -use sync::{PerCpu, preempt_disable, preempt_enable}; - -let data = PerCpu::new(|| 0); - -// 正确用法:禁用抢占 -preempt_disable(); -let value = data.get_mut(); -*value += 1; -preempt_enable(); - -// 错误用法:未禁用抢占(可能导致数据不一致) -// let value = data.get_mut(); // 危险! -// *value += 1; -``` - -更推荐使用 RAII 守卫: - -```rust -use sync::{PerCpu, PreemptGuard}; - -let data = PerCpu::new(|| 0); - -{ - let _guard = PreemptGuard::new(); // 自动禁用抢占 - let value = data.get_mut(); - *value += 1; -} // 守卫销毁时自动启用抢占 -``` - -## 实现细节 - -### CPU ID 获取 - -Per-CPU 变量通过 `arch::kernel::cpu::cpu_id()` 获取当前 CPU 的 ID: - -```rust -let cpu_id = crate::arch::kernel::cpu::cpu_id(); -``` - -在 RISC-V 架构中,CPU ID 通常存储在 `tp`(Thread Pointer)寄存器中,可以快速读取。 - -**注意**:当前实现暂时只支持单核,`cpu_id()` 始终返回 0。多核支持正在开发中。 - -### 内存布局 - -`PerCpu` 使用 `Vec>` 存储数据副本: -- 每个元素对应一个 CPU 核心 -- 索引即为 CPU ID -- 使用 `CacheAligned` 包装,确保每个数据副本独占一个缓存行(64 字节对齐) -- 内部使用 `UnsafeCell` 提供内部可变性 - -**缓存行对齐的重要性**:在多核系统中,如果多个 CPU 的数据位于同一缓存行,即使访问不同的数据,也会因为缓存一致性协议导致性能下降。通过确保每个 Per-CPU 数据独占一个缓存行,可以完全避免这种伪共享问题。 - -### 初始化时机 - -Per-CPU 变量在创建时需要知道 CPU 核心数量(`NUM_CPU`): -- `NUM_CPU` 在内核启动早期由引导代码设置 -- 如果在 `NUM_CPU` 设置前创建 Per-CPU 变量,会 panic - -## 测试 - -Per-CPU 模块包含以下测试: - -1. **基本功能测试**(`test_per_cpu_basic`):测试创建和访问 Per-CPU 变量 -2. **跨核访问测试**(`test_per_cpu_get_of`):测试 `get_of` 方法 -3. **可变访问测试**(`test_per_cpu_get_mut`):测试 `get_mut` 方法 - -运行测试: -```bash -cd os && make test -``` - -## 相关资源 - -- **源码位置**:`os/src/sync/per_cpu.rs` -- **抢占控制**:[抢占控制文档](./preempt.md) -- **同步机制概述**:[同步机制文档](./README.md) - -## 未来改进 - -1. **NUMA 感知**:在 NUMA 架构中,优化数据副本的内存分配位置 -2. **动态核心数**:支持运行时动态调整核心数量 -3. **统计聚合优化**:提供高效的聚合接口,减少遍历开销 +- `os/src/sync/per_cpu.rs:16` - cache line 对齐包装. +- `os/src/sync/per_cpu.rs:36` - `PerCpu`. +- `os/src/sync/per_cpu.rs:42` - `new()`. +- `os/src/sync/per_cpu.rs:56` - `new_with_id()`. +- `os/src/sync/per_cpu.rs:70` - `new_with_id_and_count()`. +- `os/src/sync/per_cpu.rs:85` - 当前 CPU 只读访问. +- `os/src/sync/per_cpu.rs:93` - 当前 CPU 可变访问. +- `os/src/sync/per_cpu.rs:100` - 指定 CPU 访问. diff --git a/document/sync/preempt.md b/document/sync/preempt.md index 9534e688..5a735dfc 100644 --- a/document/sync/preempt.md +++ b/document/sync/preempt.md @@ -1,421 +1,57 @@ -# 抢占控制 +# PreemptGuard -## 简介 +`PreemptGuard` 用 RAII 方式禁用抢占.它主要服务 `PerCpu`: 访问当前 CPU 副本期间不能让任务迁移到其他 CPU. -抢占控制(Preemption Control)是多核操作系统中的关键机制,用于防止任务在访问 Per-CPU 数据期间被调度器迁移到其他 CPU 核心。通过禁用抢占,可以确保任务在访问期间始终运行在同一个核心上,从而保证数据一致性。 +## 当前状态 -在可抢占的内核中,调度器可以在任何时刻中断当前任务并切换到另一个任务。如果任务在访问 Per-CPU 变量期间被抢占并迁移到另一个核心,它会访问到错误核心的数据副本,导致数据不一致。抢占控制通过临时禁用调度器的抢占功能,确保任务在关键区域内不会被迁移。 +- 抢占计数是 per-CPU `PREEMPT_COUNT` 数组. +- 提供泛型版本 `preempt_disable_generic::()` 等, 便于测试或显式架构选择. +- 生产路径 `PreemptGuard` 使用 `ArchImpl`. +- 支持嵌套, 计数大于 0 即视为抢占已禁用. -## 核心概念 +## 目标 -### 抢占与任务迁移 +- 防止访问 per-CPU 数据时任务迁移. +- 用 acquire/release fence 给临界区建立基本的编译器和 CPU 重排边界. +- 以 guard 形式减少手动 disable/enable 不配对的风险. -在多核系统中,调度器可能会: -1. **抢占当前任务**:中断正在运行的任务,切换到另一个任务 -2. **任务迁移**:将任务从一个 CPU 核心迁移到另一个核心(负载均衡) +## 非目标 -如果任务在访问 Per-CPU 数据期间被迁移: -```rust -// 假设任务最初在 CPU 0 上运行 -let cpu_id = cpu_id(); // 返回 0 -let data = per_cpu_data[cpu_id]; // 访问 CPU 0 的数据 +- 不屏蔽硬件中断. +- 不提供跨 CPU 互斥. +- 不负责调度器完整策略, 只维护当前 CPU 的抢占计数. -// 此时任务被抢占并迁移到 CPU 1 -// 但 cpu_id 仍然是 0,导致访问错误的数据副本 -data.modify(); // 错误:修改了 CPU 0 的数据,但任务在 CPU 1 上运行 -``` +## 关键流程 -### 抢占计数器 +1. `PreemptGuard::new()` 调用 `preempt_disable()`. +2. `preempt_disable()` 增加当前 CPU 的计数并执行 acquire fence. +3. guard drop 调用 `preempt_enable()`. +4. `preempt_enable()` 先执行 release fence, 再减少当前 CPU 的计数. -抢占控制使用**嵌套计数器**机制: -- 每个 CPU 维护一个抢占计数器 -- 计数器 > 0 表示抢占已禁用 -- 支持嵌套调用:每次 `preempt_disable()` 增加计数器,每次 `preempt_enable()` 减少计数器 -- 只有当计数器降为 0 时,抢占才真正启用 +## 与 IntrGuard 的区别 -这种设计允许嵌套的临界区: -```rust -preempt_disable(); // 计数器: 0 -> 1 -{ - preempt_disable(); // 计数器: 1 -> 2 - // 内层临界区 - preempt_enable(); // 计数器: 2 -> 1 -} -// 外层临界区仍然受保护 -preempt_enable(); // 计数器: 1 -> 0,抢占启用 -``` +- `IntrGuard` 处理本 CPU 中断重入. +- `PreemptGuard` 处理任务迁移. +- 访问 per-CPU 数据通常需要 `PreemptGuard`, 不一定需要关中断. +- 保护跨 CPU 共享数据需要锁, 不是单纯禁用抢占. -## 数据结构 +## 并发约束 -### 抢占计数器数组 +- guard 必须在创建它的 CPU 上 drop. +- 计数没有下溢恢复机制, 手动 enable/disable 必须严格配对. +- 长时间禁用抢占会影响调度延迟. -```rust -use crate::config::MAX_CPU_COUNT; +## 已知限制 -/// 创建 Per-CPU 抢占计数器数组的辅助宏 -macro_rules! create_preempt_count_array { - ($size:expr) => {{ - const SIZE: usize = $size; - const INIT: AtomicUsize = AtomicUsize::new(0); - [INIT; SIZE] - }}; -} +- 当前只维护计数, 调度器何时检查该计数由调度路径决定. +- 没有调试所有者或超时诊断. -static PREEMPT_COUNT: [AtomicUsize; MAX_CPU_COUNT] = create_preempt_count_array!(MAX_CPU_COUNT); -``` +## 源码索引 -使用静态数组为每个 CPU 维护独立的抢占计数器。数组大小由 `MAX_CPU_COUNT` 配置常量决定(默认为 8),可在 `config.rs` 中修改以支持更多 CPU 核心。 - -**实现说明**:使用宏来生成数组初始化代码,确保数组元素数量与 `MAX_CPU_COUNT` 一致。由于 `AtomicUsize::new(0)` 是 `const fn`,可以在编译期完成所有初始化。 - -**源码位置**:`os/src/sync/preempt.rs:10` - -## API 接口 - -### 禁用抢占 - -```rust -pub fn preempt_disable() -``` - -禁用当前 CPU 的抢占功能。可以嵌套调用,每次调用增加抢占计数器。 - -**实现细节**: -1. 获取当前 CPU ID -2. 原子地增加该 CPU 的抢占计数器 -3. 插入 Acquire 内存屏障,确保后续访问不会被重排到此之前 - -**示例**: -```rust -use sync::preempt_disable; - -preempt_disable(); -// 临界区:访问 Per-CPU 数据 -// 任务不会被迁移到其他核心 -``` - -**源码位置**:`os/src/sync/preempt.rs:25` - -### 启用抢占 - -```rust -pub fn preempt_enable() -``` - -启用当前 CPU 的抢占功能。必须与 `preempt_disable()` 配对使用。 - -**实现细节**: -1. 插入 Release 内存屏障,确保之前的访问不会被重排到此之后 -2. 获取当前 CPU ID -3. 原子地减少该 CPU 的抢占计数器 - -**示例**: -```rust -use sync::{preempt_disable, preempt_enable}; - -preempt_disable(); -// 临界区 -preempt_enable(); -``` - -**注意**:必须确保每个 `preempt_disable()` 都有对应的 `preempt_enable()`,否则抢占将永久禁用,导致系统无法调度。 - -**源码位置**:`os/src/sync/preempt.rs:36` - -### 检查抢占状态 - -```rust -pub fn preempt_disabled() -> bool -``` - -检查当前 CPU 的抢占是否已禁用。 - -**返回值**: -- `true`:抢占已禁用(计数器 > 0) -- `false`:抢占已启用(计数器 = 0) - -**示例**: -```rust -use sync::preempt_disabled; - -if preempt_disabled() { - println!("Preemption is disabled"); -} else { - println!("Preemption is enabled"); -} -``` - -**源码位置**:`os/src/sync/preempt.rs:45` - -### RAII 守卫 - -```rust -pub struct PreemptGuard; - -impl PreemptGuard { - pub fn new() -> Self -} -``` - -抢占保护的 RAII 守卫。创建时自动禁用抢占,销毁时自动启用抢占。 - -**优势**: -- 自动管理抢占状态,避免忘记调用 `preempt_enable()` -- 即使发生 panic,守卫的 `drop()` 也会被调用,确保抢占被正确恢复 -- 代码更简洁,意图更清晰 - -**示例**: -```rust -use sync::PreemptGuard; - -{ - let _guard = PreemptGuard::new(); // 抢占已禁用 - // 临界区:访问 Per-CPU 数据 -} // 守卫销毁,抢占自动启用 -``` - -**源码位置**:`os/src/sync/preempt.rs:53` - -## 使用场景 - -### 1. 访问 Per-CPU 变量 - -这是抢占控制最主要的用途: - -```rust -use sync::{PerCpu, PreemptGuard}; - -static COUNTER: PerCpu = PerCpu::new(|| 0); - -fn increment_counter() { - let _guard = PreemptGuard::new(); - let counter = COUNTER.get_mut(); - *counter += 1; -} -``` - -### 2. 保护短临界区 - -对于不需要锁但需要保证原子性的短操作: - -```rust -use sync::PreemptGuard; - -fn update_local_state() { - let _guard = PreemptGuard::new(); - // 更新当前 CPU 的本地状态 - // 确保操作不会被中断 -} -``` - -### 3. 嵌套临界区 - -支持嵌套的抢占禁用: - -```rust -use sync::PreemptGuard; - -fn outer_function() { - let _guard1 = PreemptGuard::new(); // 计数器: 0 -> 1 - // 外层临界区 - inner_function(); - // 外层临界区继续 -} - -fn inner_function() { - let _guard2 = PreemptGuard::new(); // 计数器: 1 -> 2 - // 内层临界区 -} // 计数器: 2 -> 1 -``` - -## 内存屏障 - -抢占控制使用内存屏障确保正确的内存顺序: - -### Acquire 屏障(preempt_disable) - -```rust -core::sync::atomic::fence(Ordering::Acquire); -``` - -在禁用抢占后插入 Acquire 屏障,确保: -- 后续的内存访问不会被重排到屏障之前 -- 可以安全地读取 Per-CPU 数据 - -### Release 屏障(preempt_enable) - -```rust -core::sync::atomic::fence(Ordering::Release); -``` - -在启用抢占前插入 Release 屏障,确保: -- 之前的内存访问不会被重排到屏障之后 -- Per-CPU 数据的修改对其他核心可见 - -### 为什么需要屏障? - -考虑以下场景: -```rust -preempt_disable(); -let data = per_cpu_data.get_mut(); -data.value = 42; // 写入 -preempt_enable(); -``` - -如果没有内存屏障: -1. 编译器或 CPU 可能将 `data.value = 42` 重排到 `preempt_disable()` 之前 -2. 此时任务可能被迁移,导致写入错误的数据副本 - -内存屏障防止了这种重排,确保所有 Per-CPU 访问都在抢占禁用期间完成。 - -## 设计考量 - -### 优势 - -1. **轻量级**:相比锁,抢占控制的开销极小(只是原子操作和内存屏障) -2. **嵌套支持**:通过计数器机制支持嵌套调用 -3. **RAII 友好**:提供守卫类型,自动管理抢占状态 - -### 劣势 - -1. **实时性影响**:禁用抢占期间,高优先级任务无法抢占当前任务 -2. **不保护中断**:抢占控制不影响中断处理,中断仍然可以打断当前任务 -3. **单核无效**:在单核系统中,抢占控制无法防止中断处理程序的并发访问 - -### 与中断屏蔽的区别 - -| 特性 | 抢占控制 | 中断屏蔽 | -|------|---------|---------| -| 防止任务抢占 | ✓ | ✓ | -| 防止中断 | ✗ | ✓ | -| 开销 | 低 | 中等 | -| 实时性影响 | 中等 | 高 | -| 适用场景 | Per-CPU 数据访问 | 中断与任务共享数据 | - -## 实现细节 - -### CPU ID 获取 - -抢占控制通过 `arch::kernel::cpu::cpu_id()` 获取当前 CPU 的 ID: - -```rust -let cpu_id = crate::arch::kernel::cpu::cpu_id(); -``` - -在 RISC-V 架构中,CPU ID 通常存储在 `tp`(Thread Pointer)寄存器中。 - -**注意**:当前实现暂时只支持单核,`cpu_id()` 始终返回 0。多核支持正在开发中。 - -### 原子操作 - -抢占计数器使用 `AtomicUsize` 和 `Relaxed` 内存顺序: - -```rust -PREEMPT_COUNT[cpu_id].fetch_add(1, Ordering::Relaxed); -``` - -使用 `Relaxed` 是安全的,因为: -- 每个 CPU 只访问自己的计数器,不存在跨核竞争 -- 内存顺序由显式的 `fence()` 保证 - -### 数组大小配置 - -抢占计数器使用固定大小的数组,大小由 `config::MAX_CPU_COUNT` 常量决定(默认为 8)。 - -**配置方法**:在 `os/src/config.rs` 中修改 `MAX_CPU_COUNT` 常量: - -```rust -// 支持最多 16 个 CPU 核心 -pub const MAX_CPU_COUNT: usize = 16; -``` - -**设计权衡**: -- 使用固定数组而非动态分配(`Vec`)是为了性能考虑 -- 抢占控制是极高频访问的底层机制,固定数组提供零开销访问 -- 如需支持更多核心,只需修改配置常量并重新编译 - -## 与调度器的集成 - -抢占控制需要与调度器紧密集成: - -1. **调度器检查**:在调度点,调度器必须检查 `preempt_disabled()` -2. **延迟调度**:如果抢占已禁用,调度器应延迟调度,直到抢占启用 -3. **抢占标志**:可以设置"需要调度"标志,在 `preempt_enable()` 时触发调度 - -```rust -pub fn preempt_enable() { - core::sync::atomic::fence(Ordering::Release); - let cpu_id = crate::arch::kernel::cpu::cpu_id(); - let count = PREEMPT_COUNT[cpu_id].fetch_sub(1, Ordering::Relaxed); - - // 如果计数器降为 0 且有待处理的调度请求 - if count == 1 && need_resched() { - schedule(); // 触发调度 - } -} -``` - -**注意**:当前实现尚未完全集成调度器,这是未来的改进方向。 - -## 测试 - -抢占控制模块包含以下测试: - -1. **基本功能测试**(`test_preempt_disable_enable`):测试禁用和启用抢占 -2. **守卫测试**(`test_preempt_guard`):测试 RAII 守卫的自动管理 -3. **嵌套守卫测试**(`test_nested_preempt_guard`):测试嵌套的抢占禁用 - -运行测试: -```bash -cd os && make test -``` - -## 使用建议 - -### 最佳实践 - -1. **优先使用守卫**:使用 `PreemptGuard` 而不是手动调用 `preempt_disable/enable` -2. **保持临界区短小**:禁用抢占期间应尽快完成操作,避免影响实时性 -3. **避免阻塞操作**:禁用抢占期间不应进行可能阻塞的操作(如 I/O) -4. **配对使用**:确保每个 `preempt_disable()` 都有对应的 `preempt_enable()` - -### 常见陷阱 - -1. **忘记启用抢占**: -```rust -// 错误:忘记调用 preempt_enable() -preempt_disable(); -// 临界区 -// 缺少 preempt_enable(),抢占永久禁用 -``` - -2. **在禁用抢占期间睡眠**: -```rust -// 错误:禁用抢占期间不应睡眠 -preempt_disable(); -sleep(); // 错误!可能导致死锁 -preempt_enable(); -``` - -3. **过长的临界区**: -```rust -// 不推荐:临界区过长,影响实时性 -preempt_disable(); -for i in 0..1000000 { - // 大量计算 -} -preempt_enable(); -``` - -## 相关资源 - -- **源码位置**:`os/src/sync/preempt.rs` -- **Per-CPU 变量**:[Per-CPU 文档](./per_cpu.md) -- **同步机制概述**:[同步机制文档](./README.md) - -## 未来改进 - -1. **调度器集成**:完善与调度器的集成,支持延迟调度 -2. **动态核心数**:支持运行时动态调整核心数量 -3. **抢占延迟统计**:记录抢占禁用的时长,用于性能分析 -4. **调试支持**:在调试模式下检测过长的抢占禁用,帮助发现问题 +- `os/src/sync/preempt.rs:21` - `PREEMPT_COUNT`. +- `os/src/sync/preempt.rs:31` - 泛型 disable. +- `os/src/sync/preempt.rs:39` - 泛型 enable. +- `os/src/sync/preempt.rs:47` - 泛型状态检查. +- `os/src/sync/preempt.rs:53` - 泛型 guard. +- `os/src/sync/preempt.rs:78` - 生产 `preempt_disable()`. +- `os/src/sync/preempt.rs:95` - `PreemptGuard`. diff --git a/document/sync/raw_spin_lock.md b/document/sync/raw_spin_lock.md new file mode 100644 index 00000000..f54656c8 --- /dev/null +++ b/document/sync/raw_spin_lock.md @@ -0,0 +1,56 @@ +# RawSpinLock + +`RawSpinLock` 是同步模块的底层互斥构件.它只管理锁状态和中断保护, 不直接暴露受保护数据. + +## 当前状态 + +- 锁状态是 `AtomicBool`. +- 普通 guard 路径在加锁前创建 `IntrGuard`, 加锁成功后由 `RawSpinLockGuard` 保存该 guard. +- `try_lock()` 获取失败时会让临时 `IntrGuard` drop, 立即恢复进入前中断状态. +- 同时实现 `lock_api::RawMutex`, 供 talc 等外部抽象使用. + +## 目标 + +- 作为 `SpinLock` 的底层互斥. +- 为全局 allocator 提供 `lock_api` 兼容锁. +- 在本 CPU 中断上下文可能重入相同数据时, 通过禁用中断降低死锁风险. + +## 非目标 + +- 不保护具体数据, 上层必须自己组合 `UnsafeCell` 或其他容器. +- 不提供公平性保证. +- 不可重入.持有后再次获取同一把锁会自旋等待自己释放. + +## 关键流程 + +### 普通 guard 路径 + +1. 创建 `IntrGuard`, 禁用本 CPU 中断. +2. 用 acquire CAS 把锁状态从 false 改成 true. +3. 返回 `RawSpinLockGuard`. +4. guard drop 时先 release 存储 false, 然后字段 drop 恢复中断状态. + +### lock_api 路径 + +`lock_api::RawMutex` 接口没有显式 guard 字段保存 `IntrGuard`, 因此实现会保存进入锁前的中断 flags.unlock 时释放锁并恢复该 flags. + +## 并发约束 + +- 临界区必须短. +- 不能在持锁期间主动调度或执行可能长期等待的操作. +- `RawSpinLock` 可跨 CPU 共享, 互斥靠原子 acquire/release 语义. +- 中断状态只针对当前 CPU, 不能替代跨 CPU 原子互斥. + +## 已知限制 + +- `lock_api` 适配路径用锁内单个 `saved_intr_flags` 保存 flags, 设计上要求同一把 raw lock 不可重入, 且 unlock 与成功 lock 配对. +- 没有队列或退避策略, 高竞争时会持续自旋. + +## 源码索引 + +- `os/src/sync/raw_spin_lock.rs:20` - 锁状态. +- `os/src/sync/raw_spin_lock.rs:61` - acquire 自旋循环. +- `os/src/sync/raw_spin_lock.rs:80` - 普通 `lock()`. +- `os/src/sync/raw_spin_lock.rs:93` - `try_lock()`. +- `os/src/sync/raw_spin_lock.rs:118` - `RawSpinLockGuard`. +- `os/src/sync/raw_spin_lock.rs:132` - `lock_api::RawMutex` 实现. diff --git a/document/sync/rwlock.md b/document/sync/rwlock.md index 6fde5476..c6e7aa1c 100644 --- a/document/sync/rwlock.md +++ b/document/sync/rwlock.md @@ -1,370 +1,61 @@ -# 读写锁 (RwLock) +# RwLock -## 1. 概述 +`RwLock` 允许多个读者并发访问或一个写者独占访问.它适合读多写少且临界区仍然很短的内核数据. -**读写锁 (Read-Write Lock)** 是一种允许多个读者同时访问共享数据,但写者独占访问的同步原语。它特别适用于**读多写少**的场景,能够显著提升并发性能。 +## 当前状态 -### 1.1 核心特性 +- 状态由一个 `AtomicUsize` 编码. +- 高位 `WRITER_BIT` 表示写者持有. +- 低位 `READER_MASK` 表示读者数量. +- 读写 guard 都持有 `IntrGuard`, 因此 guard 生命周期内本 CPU 中断被禁用. -- **多读者并发**:允许多个线程同时持有读锁,并发读取数据 -- **写者独占**:写锁是独占的,持有写锁时不允许其他读者或写者 -- **中断安全**:自动禁用中断,防止死锁 -- **RAII 模式**:通过 Guard 自动管理锁的生命周期 +## 目标 -### 1.2 与 SpinLock 的区别 +- 降低读多写少场景的无谓互斥. +- 维持与自旋锁相似的中断安全约束. +- 用 RAII guard 管理读者计数和写者位. -| 特性 | SpinLock | RwLock | -|------|----------|--------| -| 并发读 | ❌ 不支持 | ✅ 支持多读者 | -| 性能(读多) | 较低 | 高 | -| 性能(写多) | 较高 | 较低 | -| 实现复杂度 | 简单 | 中等 | -| 适用场景 | 通用 | 读多写少 | +## 非目标 -### 1.3 适用场景 +- 不提供写者优先或公平调度. +- 不支持读锁升级为写锁, 也不支持写锁降级为读锁. +- 不适合长临界区. -✅ **推荐使用**: -- 全局配置数据(频繁读取,偶尔更新) -- 设备驱动注册表(启动时注册,运行时查询) -- 缓存数据结构(高频读取,低频更新) -- 读写比 > 5:1 的场景 +## 关键流程 -❌ **不推荐使用**: -- 写操作频繁的场景(读写比 < 3:1) -- 临界区极短的场景(使用 SpinLock 更高效) -- 需要锁升级/降级的场景(不支持) +### 读锁 -## 2. 设计原理 +1. 创建 `IntrGuard`. +2. 读取状态.如果写者位存在, 自旋等待. +3. CAS 把读者计数加一. +4. 读 guard drop 时 release 减一. -### 2.1 状态编码 +### 写锁 -RwLock 使用单个 `AtomicUsize` 来编码锁状态: +1. 创建 `IntrGuard`. +2. 只有状态为 0 时 CAS 设置写者位. +3. 写 guard 提供可变访问. +4. 写 guard drop 时 release 清零状态. -``` -状态位:[WRITER (1bit)] [READERS (31bits)] - │ │ - │ └─ 读者计数 (0 ~ 2^31-1) - └─ 写者标志 (0=无写者, 1=有写者) -``` +## 并发约束 -**常量定义**: -```rust -const WRITER_BIT: usize = 1 << 31; // 0x80000000 -const READER_MASK: usize = WRITER_BIT - 1; // 0x7FFFFFFF -``` +- 读者和写者都在持 guard 期间禁用本 CPU 中断. +- 写者可能被源源不断的新读者延迟, 当前实现没有饥饿防护. +- 读者计数达到 mask 上限会 panic, 这是不应出现的内核错误. +- 临界区不能主动 sleep 或等待调度. -### 2.2 状态转换 +## 已知限制 -**读锁获取**: -1. 检查 `state & WRITER_BIT == 0`(无写者) -2. 检查 `readers < READER_MASK`(未溢出) -3. 原子地将 state 加 1(`compare_exchange_weak`) +- 无公平性. +- 无锁升级/降级. +- 状态编码固定使用 `usize` 的 bit layout, 文档不承诺外部依赖这些常量. -**写锁获取**: -1. 先用 `load` 检查 state 是否为 0(test-and-test-and-set 优化) -2. 原子地将 state 从 0 设置为 `WRITER_BIT`(`compare_exchange_weak`) +## 源码索引 -**锁释放**: -- 读锁:`fetch_sub(1, Release)` -- 写锁:`store(0, Release)` - -### 2.3 内存序保证 - -| 操作 | 内存序 | 说明 | -|------|--------|------| -| 获取锁(成功) | `Acquire` | 与释放锁的 Release 同步 | -| 释放锁 | `Release` | 发布数据修改 | -| 自旋检查 | `Relaxed` | 无需同步,仅检查状态 | -| CAS 失败 | `Relaxed` | 失败时无需同步 | - -## 3. API 参考 - -### 3.1 创建读写锁 - -```rust -pub const fn new(data: T) -> Self -``` - -创建一个新的读写锁,包装给定的数据。 - -**示例**: -```rust -use crate::sync::RwLock; - -let lock = RwLock::new(vec![1, 2, 3]); -``` - -### 3.2 获取读锁 - -```rust -pub fn read(&self) -> RwLockReadGuard<'_, T> -``` - -获取读锁,返回 RAII 保护器。允许多个读者同时持有。如果有写者持有锁,则自旋等待。 - -**特性**: -- 自动禁用中断 -- 支持多读者并发 -- 离开作用域时自动释放 - -**示例**: -```rust -let lock = RwLock::new(42); - -// 多个读者可以同时访问 -let reader1 = lock.read(); -let reader2 = lock.read(); -println!("Value: {}", *reader1); // 输出: Value: 42 -``` - -### 3.3 获取写锁 - -```rust -pub fn write(&self) -> RwLockWriteGuard<'_, T> -``` - -获取写锁,返回 RAII 保护器。独占访问,等待所有读者和写者退出。 - -**特性**: -- 自动禁用中断 -- 独占访问(排斥所有读者和写者) -- 离开作用域时自动释放 -- 使用 test-and-test-and-set 优化减少总线争用 - -**示例**: -```rust -let lock = RwLock::new(vec![1, 2, 3]); - -let mut writer = lock.write(); -writer.push(4); // 独占修改 -// writer 离开作用域时自动释放锁 -``` - -### 3.4 尝试获取读锁 - -```rust -pub fn try_read(&self) -> Option> -``` - -非阻塞版本,如果当前有写者则立即返回 `None`。 - -**示例**: -```rust -let lock = RwLock::new(42); - -if let Some(guard) = lock.try_read() { - println!("Got read lock: {}", *guard); -} else { - println!("Lock is held by writer"); -} -``` - -### 3.5 尝试获取写锁 - -```rust -pub fn try_write(&self) -> Option> -``` - -非阻塞版本,如果当前有读者或写者则立即返回 `None`。 - -**示例**: -```rust -let lock = RwLock::new(vec![1, 2, 3]); - -if let Some(mut guard) = lock.try_write() { - guard.push(4); -} else { - println!("Lock is busy"); -} -``` - -## 4. 使用示例 - -### 4.1 基本读写操作 - -```rust -use crate::sync::RwLock; - -let data = RwLock::new(0); - -// 读操作 -{ - let reader = data.read(); - println!("Current value: {}", *reader); -} - -// 写操作 -{ - let mut writer = data.write(); - *writer += 1; -} -``` - -### 4.2 多读者并发 - -```rust -let data = RwLock::new(vec![1, 2, 3, 4, 5]); - -// 多个读者可以同时访问 -let reader1 = data.read(); -let reader2 = data.read(); -let reader3 = data.read(); - -// 所有读者都能看到相同的数据 -assert_eq!(reader1.len(), 5); -assert_eq!(reader2.len(), 5); -assert_eq!(reader3.len(), 5); -``` - -### 4.3 与 lazy_static 配合使用 - -```rust -use lazy_static::lazy_static; -use crate::sync::RwLock; -use alloc::vec::Vec; - -lazy_static! { - /// 全局设备注册表 - static ref DEVICE_REGISTRY: RwLock> = RwLock::new(Vec::new()); -} - -// 注册设备(写操作,低频) -pub fn register_device(device: Device) { - let mut registry = DEVICE_REGISTRY.write(); - registry.push(device); -} - -// 查询设备(读操作,高频) -pub fn find_device(name: &str) -> Option { - let registry = DEVICE_REGISTRY.read(); - registry.iter().find(|d| d.name == name).cloned() -} -``` - -## 5. 性能特性 - -### 5.1 性能对比 - -**场景:读多写少(90% 读,10% 写)** - -| 锁类型 | 吞吐量 | 相对性能 | -|--------|--------|----------| -| SpinLock | 2.0M ops/s | 1.0x | -| RwLock | 5.6M ops/s | 2.8x ✅ | - -**场景:写操作频繁(50% 读,50% 写)** - -| 锁类型 | 吞吐量 | 相对性能 | -|--------|--------|----------| -| SpinLock | 8.3M ops/s | 1.0x ✅ | -| RwLock | 7.1M ops/s | 0.85x | - -### 5.2 性能优化 - -**test-and-test-and-set 模式**: -```rust -// 优化前:每次都执行昂贵的 CAS -loop { - if compare_exchange_weak(0, WRITER_BIT).is_ok() { ... } -} - -// 优化后:先用便宜的 load 检查 -loop { - if load() == 0 && compare_exchange_weak(0, WRITER_BIT).is_ok() { ... } -} -``` - -这个优化在锁竞争激烈时能显著减少总线流量。 - -### 5.3 适用场景分析 - -**✅ 高效场景**: -- 读写比 > 5:1 -- 读操作耗时较长 -- 多核并发读取 - -**❌ 低效场景**: -- 读写比 < 3:1 -- 临界区极短(< 10 条指令) -- 单核环境 - -## 6. 注意事项 - -### 6.1 写者饥饿 - -**问题**:连续的读者可能导致写者永远无法获取锁。 - -```rust -// CPU 0: 持续读取 -loop { - let _reader = data.read(); - // 读操作 -} - -// CPU 1: 写者饥饿 -let _writer = data.write(); // 可能永远等待 -``` - -**缓解方法**: -- 限制读锁持有时间 -- 在应用层实现写者优先策略 -- 对于关键写操作,考虑使用 SpinLock - -### 6.2 不支持锁升级/降级 - -**错误示例**: -```rust -let reader = data.read(); -// ❌ 错误:尝试升级为写锁会死锁 -let writer = data.write(); // 死锁! -``` - -**正确做法**: -```rust -// 先释放读锁 -{ - let reader = data.read(); - // 读操作 -} - -// 再获取写锁 -{ - let mut writer = data.write(); - // 写操作 -} -``` - -### 6.3 中断安全性 - -RwLock 通过 `IntrGuard` 自动禁用中断,保证在持锁期间不会被中断处理程序打断,避免死锁。 - -```rust -pub fn read(&self) -> RwLockReadGuard<'_, T> { - let intr_guard = IntrGuard::new(); // 禁用中断 - // 获取锁... - RwLockReadGuard { lock: self, intr_guard } - // Guard drop 时自动恢复中断 -} -``` - -### 6.4 读者溢出 - -理论上,如果同时有超过 2^31-1 个读者,会触发 panic。但在实际内核环境中,这种情况不可能发生。 - -```rust -if (state & READER_MASK) == READER_MASK { - panic!("RwLock: 读者数量溢出"); -} -``` - -## 7. 源码链接 - -- **实现**: [`os/src/sync/rwlock.rs`](/os/src/sync/rwlock.rs) -- **测试**: 同文件中的 `#[cfg(test)] mod tests` - -## 8. 相关文档 - -- [自旋锁 (SpinLock)](./spin_lock.md) -- [票号锁 (TicketLock)](./ticket_lock.md) -- [中断保护 (IntrGuard)](./intr_guard.md) -- [死锁预防](./deadlock.md) +- `os/src/sync/rwlock.rs:12` - 状态位定义. +- `os/src/sync/rwlock.rs:24` - `RwLock`. +- `os/src/sync/rwlock.rs:51` - `read()`. +- `os/src/sync/rwlock.rs:79` - `write()`. +- `os/src/sync/rwlock.rs:98` - `try_read()`. +- `os/src/sync/rwlock.rs:125` - `try_write()`. +- `os/src/sync/rwlock.rs:153` - guard 访问和 drop. diff --git a/document/sync/sleep_lock.md b/document/sync/sleep_lock.md index 4e2c3a38..fe543f9f 100644 --- a/document/sync/sleep_lock.md +++ b/document/sync/sleep_lock.md @@ -1,39 +1,26 @@ -# 睡眠锁 (`SleepLock`) +# SleepLock 状态 -`SleepLock` 是一种互斥锁,当任务尝试获取一个已被占用的锁时,它不会自旋空等,而是会将任务置于**睡眠状态**,并让出CPU给其他任务执行。 +当前代码库没有独立的 sleep lock 模块, `sync` 模块也没有导出 `SleepLock`. 本页保留用于说明历史文档状态, 不应被当作现行实现说明. -**源码链接**: [`os/src/sync/sleep_lock.rs`](/os/src/sync/sleep_lock.rs) +## 当前替代 -## 1. 工作原理 +需要"竞争时让出 CPU"的任务上下文互斥, 请查看 [Mutex](mutex.md). 当前 `Mutex` 使用 `AtomicBool`, `RawSpinLock` 和 `WaitQueue` 组合实现, 并通过 `MutexGuard` 管理解锁和唤醒. -`SleepLock` 的实现依赖于调度器和 `WaitQueue` 机制。 +## 下线原因 -1. **内部状态**: `SleepLock` 内部包含一个布尔值 `locked` 表示锁状态,一个 `RawSpinLock` 用于保护 `locked` 字段本身,以及一个 `WaitQueue` 用于管理等待此锁的睡眠任务。 +旧文档描述了一个独立无数据 `SleepLock`, 但当前源码中的正式类型是带数据的 `Mutex`: -2. **获取锁 (`lock`)**: - a. 任务尝试获取锁。它首先获取内部的 `RawSpinLock`。 - b. 检查 `locked` 字段。如果锁未被占用 (`false`),则将 `locked` 设置为 `true`,释放 `RawSpinLock`,获取锁成功。 - c. 如果锁已被占用 (`true`),任务会将自己加入到 `WaitQueue` 中,然后调用 `sleep_task()` 进入睡眠状态。在睡眠前,它会释放内部的 `RawSpinLock`。 +- `os/src/sync/mod.rs` 导出 `mutex::*`, 没有 `sleep_lock` 模块. +- `os/src/sync/mutex.rs` 是当前睡眠式互斥实现. +- `SpinLock` 和 `RawSpinLock` 文档中的"长临界区用 SleepLock"说法已改为指向 `Mutex`. -3. **释放锁 (`unlock`)**: - a. 持有锁的任务完成操作后调用 `unlock()`。 - b. 它获取内部的 `RawSpinLock`,将 `locked` 设置为 `false`。 - c. 调用 `WaitQueue` 的 `wake_up_one()` 方法,唤醒一个正在等待队列中睡眠的任务。 - d. 释放 `RawSpinLock`。被唤醒的任务将有机会在下一次调度时运行,并再次尝试获取锁。 +## 对使用者的影响 -## 2. 核心接口 +- 不要引用 `crate::sync::SleepLock`. +- 不要链接历史 sleep lock 模块路径. +- 文档导航后续应由主 agent 决定是否移除本页或移动到历史区. -- `pub fn new() -> Self`: 创建一个新的 `SleepLock`。注意它不直接包裹数据,通常用于保护一段代码逻辑。 -- `pub fn lock(&mut self)`: 获取锁。如果锁被占用,将阻塞当前任务(使其睡眠)。 -- `pub fn unlock(&mut self)`: 释放锁,并唤醒一个等待者。 +## 源码索引 -## 3. 适用场景 - -**优点**: -- 当锁的争用激烈或临界区执行时间较长时,它不会像自旋锁那样浪费CPU资源,而是通过任务调度提高了系统整体的吞吐量。 - -**缺点**: -- 涉及任务的睡眠和唤醒,有上下文切换的开销,因此比 `SpinLock` 更“重”。 -- **`SleepLock` 只能在任务上下文中使用,绝对不能在中断处理程序中使用**,因为中断处理程序没有任务上下文,无法被调度或睡眠。 - -**结论**: `SleepLock` **适用于保护那些访问时间较长或可能发生阻塞的临界区**。例如,文件系统操作、复杂的设备I/O等。 +- `os/src/sync/mod.rs:5` - 当前同步模块列表. +- `os/src/sync/mutex.rs:21` - 当前睡眠式互斥 `Mutex`. diff --git a/document/sync/smp_interrupts.md b/document/sync/smp_interrupts.md index 1e2f4a06..71b8e847 100644 --- a/document/sync/smp_interrupts.md +++ b/document/sync/smp_interrupts.md @@ -1,88 +1,64 @@ -# SMP 内核的中断与并发问题 +# SMP 中断与并发 -本文档详细阐述了在对称多处理(SMP)系统中中断的来源,以及由此引入的、比单核系统更为复杂的并发问题和解决方案。 +SMP 内核中的并发来源不止任务之间的抢占, 还包括本 CPU 中断重入和其他 CPU 同时执行.同步原语必须明确自己覆盖哪一种并发. -## 1. SMP 系统中的中断源 +## 并发来源 -在 SMP 系统中,每个 CPU 核心都是一个独立的执行单元。中断不再仅仅是外部设备与单个 CPU 之间的事情,而是可以来自多个源,并作用于多个核心。 +- 同一 CPU 上任务被中断处理程序打断. +- 多个 CPU 同时运行任务代码. +- 多个 CPU 同时处理共享中断或 IPI. +- 调度迁移导致任务访问不同 CPU 的 per-CPU 数据. -### 1.1. 本地中断 (Local Interrupts) +## 当前原语覆盖范围 -这类中断与特定的 CPU 核心绑定,只会被该核心响应。 +| 原语 | 覆盖本 CPU 中断重入 | 覆盖跨 CPU 竞争 | 可能调度 | +| --- | --- | --- | --- | +| `IntrGuard` | 是 | 否 | 否 | +| `RawSpinLock` | 是 | 是 | 否 | +| `SpinLock` | 是 | 是 | 否 | +| `RwLock` | 是 | 是 | 否 | +| `Mutex` | 否, 不适合中断上下文 | 是 | 是 | +| `PreemptGuard` | 否 | 否 | 否 | +| `PerCpu` | 否 | 通过分片减少共享 | 否 | -- **时钟中断 (Timer Interrupts)**: 每个核心都有自己独立的本地定时器。这对于实现核本地的抢占式调度至关重要。当核心A的时钟中断触发时,它只会中断核心A,而不会影响核心B。 -- **软件中断 (Software Interrupts)**: 一个核心可以给自己发送软件中断,用于处理延迟的或低优先级的任务。 +## 关键规则 -### 1.2. 处理器间中断 (Inter-Processor Interrupts, IPIs) +### IntrGuard 不是 SMP 锁 -IPI 是 SMP 系统独有的核心机制,允许一个 CPU 核心向另一个或所有其他核心发送中断。这是实现多核协作的基础。 +禁用本 CPU 中断不能阻止其他 CPU 访问同一全局变量.跨 CPU 共享数据需要 `RawSpinLock`, `SpinLock`, `RwLock` 或更高层同步. -- **TLB 刷落 (TLB Shootdown)**: 当核心A修改了一个共享的页表项(例如,取消一个页的映射)后,其他核心(如核心B)的 TLB (Translation Lookaside Buffer) 中可能还缓存着旧的、无效的映射。核心A必须向核心B发送一个 IPI,通知它刷新其 TLB 中对应的条目,以保证内存视图的一致性。 -- **调度协作**: 当一个高优先级的任务在核心A上被唤醒,但核心A正在运行一个不可抢占的内核任务时,调度器可以向一个正在运行低优先级任务的空闲核心B发送 IPI,请求它立即重新调度,以便高优先级任务能够尽快运行。 -- **系统停机/Panic**: 当一个核心检测到无法恢复的致命错误时,它可以向所有其他核心广播一个 IPI,命令它们停止所有活动并进入停机状态,以防止进一步的数据损坏。 +### 自旋类锁必须短 -### 1.3. 全局/共享中断 (Global/Shared Interrupts) +自旋锁持有期间本 CPU 中断被关闭.临界区越长, 中断延迟越大, 其他 CPU 自旋浪费也越多. -这是来自外部物理设备(如网卡、磁盘、键盘)的中断。在 SMP 系统中,这些中断通过一个高级中断控制器(如 RISC-V 中的 PLIC)被路由到**某一个当前可用的 CPU 核心**。 +### Mutex 只能在任务上下文 -这意味着,同个设备(如网卡)的两次中断,第一次可能由核心A处理,而第二次可能由核心B处理。 +`Mutex` 竞争时会使用 `WaitQueue`, `current_task()` 和 `yield_task()`.中断上下文不能依赖这些语义. -## 2. SMP 中的并发来源与挑战 +### Per-CPU 数据需要防迁移 -在单核(UP)系统中,并发主要来源于**任务代码**与**中断处理程序**之间的竞争。通过禁用中断(`IntrGuard`),我们就可以阻止这种并发。 +`PerCpu` 把共享数据拆成每 CPU 副本, 但访问当前 CPU 副本期间必须避免迁移.通常使用 `PreemptGuard`. -但在 SMP 系统中,**真正的并行(Parallelism)** 带来了全新的、更复杂的并发来源。**禁用本地核心的中断,完全无法阻止其他核心的并行执行**。 +## TLB 与 IPI -### 来源一:任务 vs. 任务 +MM 页表后端也受 SMP 约束: -- **描述**: 两个或多个任务在不同的CPU核心上同时执行。 -- **问题**: 如果这些任务访问任何共享的全局数据(如全局计数器、共享缓冲区),就会发生数据竞争。 -- **解决方案**: 必须使用 `SpinLock` 或其他原子同步原语来保护共享数据。 +- RISC-V 修改页表后会刷新本地 TLB, 多核时通过 IPI 通知其他 CPU. +- 批处理上下文用于减少多页操作中的 IPI 次数. +- LoongArch64 当前批处理上下文只保证本地刷新, 跨核 shootdown 仍是限制. -### 来源二:任务 vs. 中断 +## 已知限制 -- **描述**: 一个任务在核心A上执行,而一个中断处理程序(可以是外部设备中断或IPI)在核心B上并行执行。 -- **问题**: 这是最常见的并发场景之一。即使核心A上的任务禁用了本地中断,也无法阻止核心B上的中断处理程序访问共享数据。 -- **示例**: - - **核心A** 上的任务正在访问一个全局共享数据 `G`。 - - 同时,一个外部设备中断被路由到 **核心B**,其中断处理程序也需要访问 `G`。 - - 核心A即使禁用了自己的本地中断,也无法阻止核心B并行地执行中断处理程序。它们会同时访问 `G`,导致数据竞争。 -- **解决方案**: 必须使用**自旋锁 (`SpinLock`)** 来保护 `G`。`SpinLock` 利用跨核心同步的原子指令,确保无论代码运行在哪个核心上,只有一个执行流能进入临界区。 +- 同步模块没有统一的中断上下文检查. +- 没有跨 CPU 锁依赖图或运行时死锁检测. +- `Mutex` 等待队列公平性仍是基础实现. -### 来源三:中断 vs. 中断 +## 源码索引 -- **描述**: 两个中断处理程序在不同的核心上并行执行。这可能是两个不同的外部设备中断,或一个外部中断和一个IPI。 -- **问题**: 如果这两个中断处理程序访问了共同的内核数据结构(例如,设备驱动程序中的共享状态),就会发生竞争。 -- **示例**: - - 一个网卡中断被路由到 **核心A**,其处理程序开始访问网卡驱动的共享数据结构 `N`。 - - 几乎同时,另一个磁盘中断被路由到 **核心B**,其处理程序也需要访问 `N`(例如,一个通用的设备管理结构)。 - - 两个中断处理程序在两个不同的核心上并行执行,产生了竞争。 -- **解决方案**: 同样,必须使用 `SpinLock` 来保护共享数据 `N`。 - -### 来源四:IPI 引入的复杂同步 - -- **描述**: 处理器间中断(IPI)本身就是一种并发事件,它要求发送方和接收方之间有精确的同步协议,以避免状态不一致。 -- **问题**: 在核心A发送 IPI 和核心B处理 IPI 之间,核心B可能正在使用即将失效的旧状态。 -- **示例 (TLB Shootdown 协议)**: - 1. 核心A 获取一个用于保护该页表的 `SpinLock`。 - 2. 核心A 修改页表项。 - 3. 核心A 向核心B发送 IPI。 - 4. 核心A **自旋等待**,直到核心B确认 IPI 已处理完毕。 - 5. 核心B 收到 IPI,执行 `sfence.vma` 指令刷新其 TLB。 - 6. 核心B 通过原子变量等方式通知核心A,它已完成刷新。 - 7. 核心A 收到确认后,才释放页表锁,并继续执行。 - -## 3. SMP 环境下的锁使用规则 - -1. **`IntrGuard` 不足以保证多核安全** - - `IntrGuard` 或直接屏蔽中断,只能阻止**本地核心**的并发(即任务与本地中断的竞争)。它对于来自其他核心的并行访问是完全无效的。 - - **规则**: 任何可能在多核环境下被并行访问的数据,**必须**使用 `SpinLock` 或其他更高级的同步原语来保护。 - -2. **中断处理程序中的锁使用限制** - - 中断处理程序(包括 IPI 处理程序)的执行上下文是特殊的,它没有关联的任务,不能被调度。 - - **规则**: 中断处理程序**绝对不能**获取任何可能导致睡眠的锁(如 `SleepLock`),也不能执行任何可能触发内存分配或任务调度的操作。否则,整个系统将死锁或崩溃。 - - **结论**: 中断处理程序中唯一可以安全使用的锁就是 `SpinLock`。 - -3. **保持中断处理程序简短快速** - - 当一个核心在中断处理程序中持有一个 `SpinLock` 时,其他核心如果也想获取这个锁,就会一直自旋等待。如果中断处理程序执行时间过长,会严重影响整个系统的性能和响应能力。 - - **规则**: 中断处理程序应尽可能快地完成其工作。对于耗时较长的任务,应采用“上半部/下半部”模型:在中断处理程序(上半部)中只完成紧急的操作(如从硬件读取数据、应答中断),然后将耗时的工作注册为一个延迟任务(下半部),交由正常的任务上下文去执行。 \ No newline at end of file +- `os/src/sync/intr_guard.rs:44` - 本 CPU 中断保护. +- `os/src/sync/raw_spin_lock.rs:20` - 中断保护加原子互斥. +- `os/src/sync/mutex.rs:41` - 任务上下文等待式互斥. +- `os/src/sync/preempt.rs:95` - 抢占保护. +- `os/src/sync/per_cpu.rs:36` - per-CPU 数据. +- `os/src/arch/riscv/mm/page_table.rs:467` - RISC-V TLB 批处理. +- `os/src/arch/loongarch/mm/page_table.rs:440` - LoongArch64 TLB 批处理. diff --git a/document/sync/spin_lock.md b/document/sync/spin_lock.md index 47d096fc..0e440c6c 100644 --- a/document/sync/spin_lock.md +++ b/document/sync/spin_lock.md @@ -1,37 +1,48 @@ -# 自旋锁 (`SpinLock`) +# SpinLock -`SpinLock` 是一个基于原子操作和中断屏蔽的互斥锁,用于保护在多核和抢占环境下被并发访问的共享数据。 +`SpinLock` 是带数据的短临界区互斥锁.它把 `RawSpinLock` 和 `UnsafeCell` 组合起来, 通过 guard 提供 `Deref`/`DerefMut` 访问. -**源码链接**: [`os/src/sync/spin_lock.rs`](/os/src/sync/spin_lock.rs) +## 当前状态 -## 1. 工作原理 +- 内部锁是 `RawSpinLock`. +- 成功加锁后返回 `SpinLockGuard`. +- guard 生命周期内可访问被保护数据. +- `try_lock()` 在竞争时返回 `None`. -`SpinLock` 的核心机制结合了中断屏蔽和原子自旋,以应对两种并发来源: +## 目标 -1. **本地核心中断**: 在获取锁之前,`SpinLock` 会**禁用当前CPU核心的中断**。这可以防止在持有锁时,被一个中断处理程序打断,从而避免了当前任务与中断处理程序之间的竞争。 -2. **多核并发**: `SpinLock` 内部使用一个 `RawSpinLock`,它依赖CPU的原子指令(如 `amoswap`)来实现互斥。如果另一个CPU核心已经持有了该锁,当前核心将在一个循环中“自旋”,不断尝试获取锁,直到成功为止。 +- 保护极短的共享数据修改. +- 同时处理跨 CPU 竞争和本 CPU 中断重入. +- 作为全局状态和内核基础设施的简单锁. -当锁被释放时(通过 `SpinLockGuard` 的 `drop`),它会先释放原子锁,然后**恢复之前的中断状态**。 +## 非目标 -## 2. 核心接口 +- 不提供公平性. +- 不适合可能 sleep, yield 或长时间等待的代码. +- 不可重入. -- `pub fn new(data: T) -> Self`: 创建一个新的 `SpinLock`,包裹需要保护的数据 `data`。 -- `pub fn lock(&self) -> SpinLockGuard`: 获取锁。此方法会阻塞(自旋),直到成功获取锁为止,并返回一个锁守卫 `SpinLockGuard`。 +## 关键流程 -## 3. 锁守卫 (`SpinLockGuard`) +1. `SpinLock::lock()` 进入 `RawSpinLock::lock()`. +2. `RawSpinLock` 禁用本 CPU 中断并自旋获取原子锁. +3. `SpinLockGuard` 保存 raw guard 和 `&mut T`. +4. guard drop 时 raw guard 释放锁并恢复中断状态. -`SpinLockGuard` 是 `SpinLock` 安全性的关键。 +## 并发约束 -- 它通过实现 `Deref` 和 `DerefMut` Trait,使得用户可以像直接访问裸指针一样方便地访问被保护的数据。 -- 当 `SpinLockGuard` 离开作用域时,其 `Drop` 实现会自动调用 `unlock()`,释放锁并恢复中断,从而避免了忘记解锁导致的死锁。 +- 持锁代码必须短小. +- 持锁期间不要调用可能调度的路径, 包括会等待任务队列的 `Mutex`. +- 在中断上下文中只能使用不会睡眠的路径. +- 多把锁嵌套时必须遵守全局锁顺序, 避免死锁. -## 4. 适用场景 +## 已知限制 -**优点**: -- 实现简单,开销小,获取锁和释放锁的速度非常快(如果锁未被争用)。 +- 高竞争下会消耗 CPU 周期. +- 没有所有者记录, 不能检测当前 CPU 是否重复加锁. -**缺点**: -- 持锁等待期间会占用CPU时间进行空转(自旋),浪费CPU资源。 -- **严禁在持有 `SpinLock` 的情况下进行任何可能导致任务睡眠或调度的操作**(如申请内存、等待I/O、获取 `SleepLock`),否则可能导致整个系统死锁。 +## 源码索引 -**结论**: `SpinLock` **只适用于保护那些访问时间极短的临界区**。如果临界区内的操作耗时较长,应使用 `SleepLock`。 +- `os/src/sync/spin_lock.rs:30` - `SpinLock`. +- `os/src/sync/spin_lock.rs:53` - `lock()`. +- `os/src/sync/spin_lock.rs:62` - `try_lock()`. +- `os/src/sync/spin_lock.rs:77` - `SpinLockGuard`. diff --git a/document/sync/ticket_lock.md b/document/sync/ticket_lock.md index c436e11a..a87c4254 100644 --- a/document/sync/ticket_lock.md +++ b/document/sync/ticket_lock.md @@ -1,346 +1,31 @@ -# 票号锁 (TicketLock) +# TicketLock 状态 -## 1. 概述 +当前代码库没有独立的 ticket lock 模块, `sync` 模块也没有导出 `TicketLock`. 本页保留用于说明历史或未实现状态, 不应被当作现行实现说明. -**票号锁 (Ticket Lock)** 是一种提供公平性保证的自旋锁,确保线程按照 **FIFO (先进先出)** 顺序获取锁。它通过票号机制避免了传统自旋锁可能出现的饥饿问题。 +## 当前替代 -### 1.1 核心特性 +需要短临界区互斥时, 使用 [SpinLock](spin_lock.md) 或 [RawSpinLock](raw_spin_lock.md). 需要读多写少时, 使用 [RwLock](rwlock.md). -- **公平性保证**:严格按照请求顺序授予锁,防止饥饿 -- **FIFO 顺序**:先请求锁的线程先获得锁 -- **中断安全**:自动禁用中断,防止死锁 -- **RAII 模式**:通过 Guard 自动管理锁的生命周期 +当前这些锁不提供严格 FIFO 公平性: -### 1.2 与 SpinLock 的区别 +- `RawSpinLock` 使用 `AtomicBool` CAS 自旋. +- `SpinLock` 只是 `RawSpinLock` 加数据. +- `RwLock` 使用读者计数和写者位, 没有写者优先或公平队列. -| 特性 | SpinLock | TicketLock | -|------|----------|------------| -| 公平性 | ❌ 无保证 | ✅ 严格 FIFO | -| 饥饿问题 | 可能发生 | 不会发生 | -| 性能开销 | 低 | 中等 | -| 缓存一致性 | 较好 | 较差 | -| 实现复杂度 | 简单 | 中等 | +## 未实现内容 -### 1.3 适用场景 +旧文档曾描述基于 `next_ticket` 和 `serving_ticket` 的 FIFO 自旋锁, 但当前源码没有对应类型, guard 或测试. 任何需要公平锁的设计都应先补实现和测试, 再恢复正式文档. -✅ **推荐使用**: -- 需要严格公平性的场景 -- 防止饥饿至关重要的系统 -- 锁持有时间较长的场景 -- 调度器、资源分配器等核心组件 +## 对使用者的影响 -❌ **不推荐使用**: -- 对性能极度敏感的热路径 -- 锁竞争非常激烈的场景 -- 临界区极短的场景(使用 SpinLock 更高效) +- 不要引用 `crate::sync::TicketLock`. +- 不要链接历史 ticket lock 模块路径. +- 不要在设计文档中把 FIFO 公平性列为当前同步模块能力. +- 文档导航后续应由主 agent 决定是否移除本页或移动到历史区. -## 2. 设计原理 +## 源码索引 -### 2.1 票号机制 - -TicketLock 使用两个原子计数器实现公平性: - -``` -┌─────────────────┐ ┌──────────────────┐ -│ next_ticket │ │ serving_ticket │ -│ (下一个票号) │ │ (当前服务票号) │ -└─────────────────┘ └──────────────────┘ - │ │ - │ │ - ▼ ▼ - 每次请求递增 每次释放递增 -``` - -**工作流程**: -1. 线程请求锁时,原子地获取 `next_ticket` 并递增 -2. 线程自旋等待,直到 `serving_ticket` 等于自己的票号 -3. 线程释放锁时,原子地递增 `serving_ticket` - -### 2.2 公平性保证 - -**示例场景**: -``` -时刻 T0: next_ticket=0, serving_ticket=0 (锁空闲) - -时刻 T1: 线程 A 请求锁 - - A 获得票号 0,next_ticket=1 - - serving_ticket=0,A 立即获得锁 - -时刻 T2: 线程 B 请求锁 - - B 获得票号 1,next_ticket=2 - - serving_ticket=0,B 自旋等待 - -时刻 T3: 线程 C 请求锁 - - C 获得票号 2,next_ticket=3 - - serving_ticket=0,C 自旋等待 - -时刻 T4: 线程 A 释放锁 - - serving_ticket=1 - - B 的票号匹配,B 获得锁(C 继续等待) - -时刻 T5: 线程 B 释放锁 - - serving_ticket=2 - - C 的票号匹配,C 获得锁 -``` - -**不变量**: -- `serving_ticket <= next_ticket` -- 持有锁时 `serving_ticket == 某个线程的票号` -- 释放锁时 `serving_ticket` 递增 1 - -### 2.3 内存序保证 - -| 操作 | 内存序 | 说明 | -|------|--------|------| -| 获取票号 | `Relaxed` | 仅需原子性,无需同步 | -| 检查服务票号 | `Acquire` | 与释放锁的 Release 同步 | -| 释放锁 | `Release` | 发布数据修改 | - -## 3. API 参考 - -### 3.1 创建票号锁 - -```rust -pub const fn new(data: T) -> Self -``` - -创建一个新的票号锁,包装给定的数据。 - -**示例**: -```rust -use crate::sync::TicketLock; - -let lock = TicketLock::new(vec![1, 2, 3]); -``` - -### 3.2 获取锁 - -```rust -pub fn lock(&self) -> TicketLockGuard<'_, T> -``` - -获取锁,返回 RAII 保护器。按 FIFO 顺序获取,如果锁被占用则自旋等待。 - -**特性**: -- 自动禁用中断 -- 严格 FIFO 顺序 -- 离开作用域时自动释放 - -**示例**: -```rust -let lock = TicketLock::new(42); - -let guard = lock.lock(); -println!("Value: {}", *guard); // 输出: Value: 42 -// guard 离开作用域时自动释放锁 -``` - -### 3.3 尝试获取锁 - -```rust -pub fn try_lock(&self) -> Option> -``` - -非阻塞版本,如果当前无法立即获取锁则返回 `None`。 - -**示例**: -```rust -let lock = TicketLock::new(42); - -if let Some(guard) = lock.try_lock() { - println!("Got lock: {}", *guard); -} else { - println!("Lock is busy"); -} -``` - -## 4. 使用示例 - -### 4.1 基本加锁/解锁 - -```rust -use crate::sync::TicketLock; - -let data = TicketLock::new(0); - -// 获取锁并修改数据 -{ - let mut guard = data.lock(); - *guard += 1; -} - -// 读取数据 -{ - let guard = data.lock(); - println!("Value: {}", *guard); // 输出: Value: 1 -} -``` - -### 4.2 公平性演示 - -```rust -use crate::sync::TicketLock; - -let lock = TicketLock::new(0); - -// 线程 A 获取锁 -let guard_a = lock.lock(); -println!("Thread A got lock"); - -// 线程 B 请求锁(会等待) -// let guard_b = lock.lock(); // 阻塞直到 A 释放 - -drop(guard_a); // A 释放锁 - -// 现在 B 可以获取锁 -let guard_b = lock.lock(); -println!("Thread B got lock"); -``` - -### 4.3 与 lazy_static 配合使用 - -```rust -use lazy_static::lazy_static; -use crate::sync::TicketLock; -use alloc::vec::Vec; - -lazy_static! { - /// 全局任务队列(需要公平调度) - static ref TASK_QUEUE: TicketLock> = TicketLock::new(Vec::new()); -} - -// 添加任务 -pub fn enqueue_task(task: Task) { - let mut queue = TASK_QUEUE.lock(); - queue.push(task); -} - -// 取出任务(按 FIFO 顺序) -pub fn dequeue_task() -> Option { - let mut queue = TASK_QUEUE.lock(); - queue.pop() -} -``` - -## 5. 性能特性 - -### 5.1 性能对比 - -**场景:中等锁竞争(4 个线程)** - -| 锁类型 | 吞吐量 | 公平性 | 最大等待时间 | -|--------|--------|--------|--------------| -| SpinLock | 10.2M ops/s | 无保证 | 不确定 | -| TicketLock | 8.7M ops/s | 严格 FIFO | 可预测 | - -**场景:高锁竞争(16 个线程)** - -| 锁类型 | 吞吐量 | 公平性 | 最大等待时间 | -|--------|--------|--------|--------------| -| SpinLock | 3.1M ops/s | 无保证 | 不确定 | -| TicketLock | 2.8M ops/s | 严格 FIFO | 可预测 | - -### 5.2 性能特点 - -**优势**: -- 公平性保证,无饥饿问题 -- 可预测的等待时间 -- 适合锁持有时间较长的场景 - -**劣势**: -- 性能略低于 SpinLock(约 10-15%) -- 缓存一致性开销较大(两个原子变量) -- 高竞争时性能下降明显 - -### 5.3 适用场景分析 - -**✅ 高效场景**: -- 需要严格公平性 -- 防止饥饿至关重要 -- 锁持有时间较长(> 100 个时钟周期) -- 调度器、资源分配器 - -**❌ 低效场景**: -- 对性能极度敏感的热路径 -- 临界区极短(< 20 个时钟周期) -- 锁竞争非常激烈(> 16 个线程) - -## 6. 注意事项 - -### 6.1 性能开销 - -TicketLock 的性能开销主要来自: -1. **两个原子变量**:增加缓存一致性流量 -2. **自旋等待**:每次检查都需要读取 `serving_ticket` -3. **顺序约束**:无法利用缓存局部性 - -**缓解方法**: -- 仅在需要公平性的场景使用 -- 避免在热路径使用 -- 考虑使用 SpinLock 替代(如果不需要公平性) - -### 6.2 票号溢出 - -**理论问题**:`next_ticket` 在 `usize::MAX` 时回绕,可能导致死锁。 - -```rust -// 极端情况(实际不可能发生) -next_ticket = usize::MAX -serving_ticket = 0 - -// 下一次请求会回绕到 0 -next_ticket.fetch_add(1) => 0 - -// 可能与正在服务的票号冲突 -``` - -**实际情况**: -- 64 位系统:需要 2^64 次锁操作才会溢出 -- 假设每秒 10^9 次操作,需要约 584 年 -- 内核环境中实际不可能发生 - -### 6.3 中断安全性 - -TicketLock 通过 `IntrGuard` 自动禁用中断,保证在持锁期间不会被中断处理程序打断,避免死锁。 - -```rust -pub fn lock(&self) -> TicketLockGuard<'_, T> { - let intr_guard = IntrGuard::new(); // 禁用中断 - // 获取锁... - TicketLockGuard { lock: self, intr_guard } - // Guard drop 时自动恢复中断 -} -``` - -### 6.4 不支持锁升级/降级 - -与 RwLock 类似,TicketLock 不支持锁升级或降级。尝试在持有锁时再次获取会导致死锁。 - -**错误示例**: -```rust -let guard1 = lock.lock(); -// ❌ 错误:尝试再次获取会死锁 -let guard2 = lock.lock(); // 死锁! -``` - -### 6.5 公平性 vs 性能权衡 - -TicketLock 牺牲了一定的性能来换取公平性。在选择锁类型时需要权衡: - -| 需求 | 推荐锁类型 | -|------|-----------| -| 性能优先 | SpinLock | -| 公平性优先 | TicketLock | -| 读多写少 | RwLock | - -## 7. 源码链接 - -- **实现**: [`os/src/sync/ticket_lock.rs`](/os/src/sync/ticket_lock.rs) -- **测试**: 同文件中的 `#[cfg(test)] mod tests` - -## 8. 相关文档 - -- [自旋锁 (SpinLock)](./spin_lock.md) -- [读写锁 (RwLock)](./rwlock.md) -- [中断保护 (IntrGuard)](./intr_guard.md) -- [死锁预防](./deadlock.md) +- `os/src/sync/mod.rs:5` - 当前同步模块列表. +- `os/src/sync/raw_spin_lock.rs:20` - 当前底层自旋锁. +- `os/src/sync/spin_lock.rs:30` - 当前带数据自旋锁. +- `os/src/sync/rwlock.rs:24` - 当前读写锁. diff --git a/document/syscall/README.md b/document/syscall/README.md index 5d4cff3e..002a61b6 100644 --- a/document/syscall/README.md +++ b/document/syscall/README.md @@ -1,135 +1,123 @@ -# Syscall 概览 - -本文档收录了两百个左右Linux x86_64 的主干 syscall 接口,用于在实现系统调用时速查。 - -说明:作用按领域分组;参数一般遵循 (int fd, const char *buf, size_t len)、(const struct TimeSpec *req, struct TimeSpec *rem)、(int pid, int sig)、(void *addr, size_t len, int prot, int flags, int fd, off_t off) 等常见模式。下面按类别简述关键功能与典型参数。未逐条展开,保持速览。 - -1. 基础文件与 IO -- read/write/pread/pwrite/readv/writev: (fd, buf/vec, count, offset) -- open/openat/close/creat: (path/dirfd, flags, mode) -- lseek: (fd, offset, whence) -- fstat/stat/lstat/newfstatat: (path/fd, statbuf) -- fsync/fdatasync/fallocate/truncate/ftruncate: (fd, len) -- getdents/getdents64: (fd, dirent_buf, size) -- access/faccessat/faccessat2: (path/dirfd, mode, flags) -- chmod/fchmod/fchmodat/fchmodat2: (path/fd, mode) -- chown/fchown/lchown/fchownat: (path/fd, uid, gid) -- link/symlink/unlink/[at]、readlink[at]: (old, new)/(path, buf, size) -- rename/renameat/renameat2: (old,new[,flags]) -- mkdir/mkdirat/rmdir/mknod/mknodat: (path[,mode,type]) -- utime/utimensat/utimes: (path, times) -- statx: (dirfd, path, flags, mask, statxbuf) - -2. 进程与线程 -- fork/vfork/clone/clone3: (flags, stack, parent_tid, child_tid, tls) -- execve/execveat: (path, argv, envp[,flags]) -- exit/exit_group: (code) -- wait4/waitid: (pid, status, options, rusage) -- getpid/getppid/gettid: 无参或返回当前 ID -- setuid/setgid/setreuid/...: (uid/gid 组合) -- setpgid/getsid/setsid/getpgid: (pid, pgid) -- prctl/personality: (option, arg1..) -- sched_yield: 无参;sched_set/getparam/scheduler/affinity/attr: (pid, param/attr/mask) -- set_tid_address: (tidptr) 线程退出清理 -- restart_syscall: 内核透明重启阻塞的调用 - -3. 内存管理 -- mmap/mmap2/mremap/munmap: (addr, len, prot, flags, fd, off) -- mprotect: (addr, len, prot) -- brk: (addr) -- madvise/mincore: (addr, len, advice)/查询驻留 -- mlock/mlockall/munlock/munlockall: (addr, len) -- memfd_create: (name, flags) -- mlock2/pkey_alloc/pkey_free/pkey_mprotect: 内存保护键 -- process_madvise: (pidfd, iov, advice) -- map_shadow_stack/mseal: 安全栈/内存封印 -- set_mempolicy/get_mempolicy/mbind: NUMA 策略 -- migrate_pages/move_pages: (pid, list) - -4. 信号与计时 -- rt_sigaction/rt_sigprocmask/rt_sigpending/rt_sigsuspend/rt_sigqueueinfo/rt_tgsigqueueinfo: (signum, act, mask, size) -- kill/tgkill/tkill: (pid[/tgid], tid, sig) -- rt_sigreturn: 用户态返回栈恢复 -- timer_create/timer_settime/timer_gettime/timerfd_*: POSIX/FD 定时器 -- nanosleep/clock_nanosleep: (req, rem[, clock, flags]) -- getitimer/setitimer/alarm: 定时器 -- gettimeofday/clock_gettime/clock_settime/clock_getres/adjtimex: 时间与校时 -- times: (tmsbuf) -- getrusage: (who, rusage) -- sched_rr_get_interval: (pid, TimeSpec) - -5. IPC -- pipe/pipe2: (fds[2]) -- socket/socketpair/bind/listen/accept/accept4/connect: 套接字基本 -- sendto/recvfrom/sendmsg/recvmsg/sendmmsg/recvmmsg/shutdown: 数据收发 (fd, buf/msg, len, flags, addr) -- setsockopt/getsockopt: (fd, level, optname, optval) -- epoll_create/epoll_wait/epoll_ctl/epoll_pwait/epoll_create1/poll/ppoll/select/pselect: 事件复用 -- eventfd/eventfd2/signalfd/signalfd4/timerfd_create/inotify_*: 各类 FD 通知 -- futex/futex_waitv/futex_wake/futex_wait/futex_requeue: (uaddr, op, val, timeout, uaddr2, val3) -- msgget/msgsnd/msgrcv/msgctl: System V 消息队列 -- semget/semop/semctl/semtimedop: System V 信号量 -- shmget/shmat/shmdt/shmctl: 共享内存 -- memfd_secret: 私密匿名内存 -- mq_open/mq_timedsend/mq_timedreceive/mq_getsetattr/mq_unlink/mq_notify: POSIX 消息队列 -- add_key/request_key/keyctl: Key 管理 -- process_vm_readv/writev: 跨进程内存访问 -- pidfd_open/pidfd_getfd/pidfd_send_signal: 稳定 PID 引用 - -6. 文件系统与挂载 -- mount/umount2/move_mount/open_tree/openat2/fsopen/fsconfig/fsmount/fspick/quotactl/quotactl_fd: 挂载与文件系统管理 -- statfs/fstatfs: 文件系统状态 -- sync/syncfs: 刷写 -- renameat2: 原子重命名 -- open_tree_attr/file_getattr/file_setattr: 树与属性 - -7. 安全与权限 -- capget/capset: 进程能力 -- seccomp: 沙箱过滤 -- bpf: 加载 BPF 程序 -- setns: 进入命名空间 -- setxattr/getxattr/listxattr/removexattr 及 *at 变体:扩展属性 -- chroot: 改根 -- ptrace: (request, pid, addr, data) -- landlock_*: Landlock 安全沙箱 -- lsm_*: LSM 自省接口 -- process_mrelease: 释放僵尸进程资源 - -8. 性能与异步 IO -- readahead/fadvise64: 预读与访问提示 -- io_setup/io_submit/io_getevents/io_destroy/io_cancel/io_pgetevents: AIO -- splice/tee/vmsplice: 零拷贝管道操作 -- copy_file_range: 零拷贝文件段传输 -- perf_event_open: 性能监控 - -9. 资源与限制 -- getrlimit/setrlimit/prlimit64: 资源限制 -- getpriority/setpriority/nice: 调度优先级 -- rlimit 相关参数: (which, rlimit struct) - -10. 进程凭据与组 -- getuid/geteuid/getgid/getegid/getgroups/setgroups/setresuid/getresuid/setresgid/getresgid/setfsuid/setfsgid: 用户/组 ID 操作 - -11. 其它杂项 -- uname: (utsname) -- sysinfo: (info) -- reboot: (magic, magic2, cmd, arg) -- adjtimex: (timex) -- kexec_load/kexec_file_load: 内核热重启 -- rseq: (rseq_area, len, flags, sig) -- cachestat: 文件缓存统计 -- statmount/listmount: 挂载枚举 -- map_shadow_stack: 安全栈映射 - -参数模式速记 -- 路径相关:dirfd + path + flags + mode -- IO 向量:iovec 数组 + count(readv/writev/preadv/pwritev) -- 时间:TimeSpec/timeval + 可选剩余/精度结构 -- 结构读写:用户指针传入,内核填充(stat, rusage, utsname) -- 标志位:按 OR 组合(O_*、MAP_*、EPOLL*、SOCK_*) - -获取详细参数/返回值 -- man 2 -- 内核源码:include/uapi/asm-generic/ 或 arch/x86/include/asm/ -- 返回值约定:负 errno 放入 -Exxx,用户态转换为 errno。 - -如需具体某组 syscall 详细参数/错误码,再指定名称列表。 \ No newline at end of file +# Syscall 子系统设计 + +Syscall 层是用户态 ABI 到内核子系统的边界。当前文档只描述 Comix 当前实现的分发, frame 抽象, 用户指针和子系统入口, 不维护庞大的 Linux syscall 清单。 + +## 当前状态 + +- `dispatch_syscall()` 按 syscall number 分发到 `sys_*` 包装函数。 +- `SyscallFrame` trait 抽象寄存器读取和返回值写回, 让分发逻辑不绑定具体架构。 +- `impl_syscall!` 宏负责从 frame 提取最多 6 个参数, 转换为 Rust 函数签名, 并把返回值写回 frame。 +- 真正实现按领域拆到 `fs`, `io`, `task`, `mm`, `signal`, `ipc`, `network`, `sys`, `cred` 等模块。 +- 未识别 syscall 返回 `-ENOSYS`。 + +## 目标 + +- 让架构相关 trap frame 和通用 syscall 实现解耦。 +- 让 syscall 层承担 ABI 边界职责: 参数解码, 用户指针复制, fd 查找, errno 映射。 +- 让具体业务逻辑留在对应内核子系统, syscall 模块只做薄适配。 + +## 非目标 + +- 不在文档维护完整 syscall 号码表。号码以 `numbers.rs` 为准。 +- 不在文档列出每个 syscall 的参数和错误分支。细节以源码和 rustdoc 为准。 +- 不把 syscall 层当作跨子系统共享状态的宿主。 + +## Dispatch 边界 + +系统调用入口在架构 trap handler 中取得当前 trap frame 后调用 `dispatch_syscall(frame)`。dispatch 只做三件事: + +1. 读取 syscall id 和参数用于调试日志。 +2. 根据 `numbers.rs` 中的常量选择对应 `sys_*` wrapper。 +3. 对未知号码写回 `-ENOSYS`。 + +dispatch 不直接读取用户内存, 不操作 fd table, 不进入 VFS 或协议栈内部状态。 + +## SyscallFrame 抽象 + +`SyscallFrame` 是架构无关寄存器接口: + +- `syscall_id()` 读取 syscall number。 +- `arg0()` 到 `arg5()` 读取 ABI 参数寄存器。 +- `set_ret()` 写回返回值。 + +RISC-V, LoongArch 等架构只需要把自己的 trap frame 适配到这个 trait, 通用 syscall 模块就可以复用同一套分发和 wrapper。 + +## impl_syscall wrapper + +`impl_syscall!` 生成 `sys_*` 函数。生成代码负责: + +- 从 frame 取原始 `usize` 参数。 +- 按声明转换为指针, 整数或 ABI 结构指针。 +- 调用实际内核实现函数。 +- 对普通返回 syscall 写回 `isize` 返回值。 +- 对 `noreturn` syscall 直接转入不返回路径, 如 `exit_group` 或 `rt_sigreturn`。 + +这层不做深度验证。验证必须在具体实现函数中完成, 因为只有实现函数知道参数含义和所需锁顺序。 + +## Errno 和返回值 + +当前约定是内核实现返回 `isize` 或 `c_int`, 成功返回非负结果, 失败返回负 errno。常见来源: + +- VFS 错误通过 `FsError::to_errno()` 转换。 +- 网络错误通过 `NetworkError::to_errno()` 或具体 syscall 映射。 +- UAPI errno 常量来自 `uapi::errno`。 +- `brk` 这类 Linux 兼容接口按自身 ABI 返回当前 brk, 而不是负 errno。 + +新增 syscall 时应优先使用已有错误类型转换, 避免在多个模块散落私有错误码。 + +## 用户指针边界 + +用户指针只能在 syscall 边界或明确的用户缓冲工具中解引用。当前主要路径: + +- 字符串路径: `util::get_path_safe()` 和 `copy_str_from_user()`。 +- argv/envp: `get_args_safe()`。 +- I/O 缓冲: `kernel/syscall/io.rs` 中先复制到内核 `Vec`, 再调用 `File`。 +- sockaddr: `kernel/syscall/network/*` 负责解析和写回。 +- shm/stat/signal/syslog: 各自 syscall 模块用 `read_from_user`, `write_to_user` 或架构 copy helper。 + +设计要求: + +- 不把用户指针保存到内核对象中。 +- 持有协议栈, VFS, MemorySpace 等关键锁时避免长时间复制用户缓冲区。 +- 复制失败返回 `-EFAULT` 或对应子系统错误。 + +## 子系统入口 + +- FS: `fs/**`, `fcntl.rs`, `ioctl.rs` 处理路径, fd, mount, stat, rename 等。 +- IO: `io.rs` 处理 read/write/readv/writev/poll/ppoll/pselect 等通用 fd I/O。 +- Task: `task/**` 处理 clone, exec, exit, wait, futex, sched, time。 +- MM: `mm.rs` 处理 brk, mmap, munmap, mprotect。 +- Signal: `signal.rs` 处理 rt_sigaction, rt_sigprocmask, sigtimedwait, sigreturn 等。 +- IPC: `ipc.rs` 处理 pipe2, dup, SysV shm。 +- Network: `network/**` 处理 socket, bind, connect, accept, send/recv, sockopt, ifaddrs。 +- System/log: `sys.rs` 处理 uname, sysinfo, syslog, reboot 等系统级接口。 +- Credentials: `cred.rs` 处理 uid/gid 相关接口。 + +## 并发和生命周期约束 + +- syscall wrapper 不持有锁跨越实际实现调用。 +- fd 操作必须考虑 close/dup/fork/exec 的共享表语义。 +- exec 会先执行 close-on-exec, detach SysV shm, 再切换地址空间和 trap frame。 +- exit_group 走进程级资源清理, 包括 fd, socket fd mapping, shm attachment 和地址空间。 +- poll/select waiters 和网络 poll 通过 `io.rs` 与 `net::socket` 协作, 避免在硬中断中推进 smoltcp。 + +## 已知限制 + +- syscall 支持范围由 `numbers.rs` 和 `dispatch.rs` 的匹配分支决定, 并不等价于完整 Linux ABI。 +- 部分 syscall 为兼容测试提供最小语义, 不代表完整内核实现。 +- `SA_RESTART` 等高级 syscall restart 语义尚不完整, 阻塞调用可能返回 `EINTR`。 + +## 源码索引 + +- `os/src/kernel/syscall/mod.rs`: 模块组织和 `impl_syscall!` 注册。 +- `os/src/kernel/syscall/dispatch.rs`: 架构无关分发和 wrapper 宏。 +- `os/src/kernel/syscall/syscall_frame.rs`: `SyscallFrame` trait。 +- `os/src/kernel/syscall/numbers.rs`: 当前处理的 syscall number。 +- `os/src/kernel/syscall/util.rs`: 路径, argv/envp, syslog 参数辅助。 +- `os/src/kernel/syscall/fs/`: 文件系统 syscall。 +- `os/src/kernel/syscall/io.rs`: 通用 fd I/O 和 poll。 +- `os/src/kernel/syscall/network/`: socket syscall。 +- `os/src/kernel/syscall/task/`: 任务和进程 syscall。 +- `os/src/kernel/syscall/mm.rs`: 内存 syscall。 +- `os/src/kernel/syscall/signal.rs`: 信号 syscall。 +- `os/src/kernel/syscall/ipc.rs`: pipe 和 SysV shm。 diff --git a/document/vfs/README.md b/document/vfs/README.md index 36d6c997..c0e16e48 100644 --- a/document/vfs/README.md +++ b/document/vfs/README.md @@ -1,251 +1,117 @@ -# VFS 子系统文档 +# VFS 子系统 -## 简介 +VFS 是 Comix 内核的统一文件命名空间和打开文件模型. 它把系统调用看到的路径, 文件描述符, 设备文件和具体文件系统实现分开, 让 ext4, tmpfs, procfs, sysfs, VFAT 以及设备节点都能通过同一组抽象接入. -VFS (Virtual File System) 是 Comix 内核的虚拟文件系统层,提供统一的文件系统抽象接口。该系统采用分层设计,支持多种文件类型和文件系统,为上层系统调用和下层具体文件系统实现之间搭建了桥梁。 +本文档只描述设计边界和关键生命周期. 具体 trait 方法, 字段和错误分支以 `os/src/vfs/` rustdoc 和源码为准. -VFS 的核心特点是**分层抽象设计**:将文件访问分为会话层和存储层,会话层维护打开文件的状态(如偏移量、标志),存储层提供无状态的随机访问接口。这种设计使得相同的底层 Inode 可以被多个进程以不同的方式访问,既保证了数据共享,又维护了各自独立的会话状态。 +## 当前状态 -作为内核文件系统的核心基础设施,VFS 子系统支持路径解析、挂载管理、[目录项缓存和文件描述符管理等完整功能。它采用目录项缓存加速路径查找,使用挂载表实现灵活的文件系统组织,并通过文件描述符表为每个进程提供独立的文件视图。 +- `File` 是打开文件会话, 保存 offset, open flags 和具体读写语义. +- `Inode` 是文件系统对象, 提供无状态的随机访问, 目录操作和元数据. +- `Dentry` 是路径层缓存节点, 连接名字, 父子关系和 `Inode`. +- `MountTable` 维护全局挂载点栈, 路径解析会自动跨越挂载边界. +- `FDTable` 是进程可见的 fd 空间, fd 指向 `Arc`. +- `dev` 和 `devno` 把 POSIX 设备号映射到字符/块设备驱动. -### 主要功能 +## 目标 -- **分层文件抽象**:会话层 (File trait) 和存储层 (Inode trait) 分离,清晰的职责划分 -- **多文件类型支持**:普通文件、管道、字符设备、块设备、符号链接、FIFO、Socket -- **路径解析**:支持绝对路径和相对路径,自动处理 `.` 和 `..`,符号链接解析 -- **目录项缓存**:Dentry 缓存加速重复路径查找,减少磁盘访问 -- **挂载管理**:支持多文件系统挂载,挂载点栈,灵活的文件系统组织 -- **文件描述符表**:进程级文件描述符管理,支持 dup/dup2/dup3,close-on-exec 标志 -- **文件锁**:支持 POSIX 文件锁 (flock),进程间文件访问同步 -- **设备文件**:字符设备和块设备抽象,设备号管理 -- **标准化接口**:与 POSIX 兼容的文件操作接口,权限管理,元数据访问 +- 为系统调用提供稳定的 POSIX 风格文件访问模型. +- 允许不同文件系统只实现 `FileSystem` 和 `Inode`, 不关心 fd 分配和路径缓存. +- 允许同一个 `Inode` 被多个 `File` 会话共享, 同时保持每次 open 的 offset 独立. +- 允许设备节点作为普通路径出现, 由 VFS 根据 inode 类型和设备号转发到驱动. -### 模块结构 +## 非目标 -``` -os/src/vfs/ -├── mod.rs # 模块入口,导出公共 API -├── file.rs # File trait 定义 (会话层接口) -├── inode.rs # Inode trait 定义 (存储层接口) -├── dentry.rs # 目录项结构和全局缓存 -├── path.rs # 路径解析和查找逻辑 -├── mount.rs # 挂载表和挂载点管理 -├── fd_table.rs # 文件描述符表 -├── file_lock.rs # 文件锁管理器 -├── file_system.rs # FileSystem trait 定义 -├── adapter.rs # 类型转换适配器 -├── dev.rs # 设备号工具函数 -├── devno.rs # 设备驱动注册表 -├── error.rs # VFS 错误类型定义 -└── impls/ # 具体文件类型实现 - ├── reg_file.rs # 普通文件 - ├── pipe_file.rs # 管道文件 - ├── stdio_file.rs # 标准 I/O 文件 - ├── char_dev_file.rs # 字符设备文件 - └── blk_dev_file.rs # 块设备文件 -``` - -### 模块职责 - -- **mod.rs**:模块的统一入口,导出所有公共 API 和类型,提供便利函数如 `vfs_load_elf` -- **file.rs**:定义会话层接口 `File` trait,声明 read、write、lseek 等方法,支持可选方法提供默认实现 -- **inode.rs**:定义存储层接口 `Inode` trait,提供 read_at、write_at、lookup、create、mkdir 等无状态方法 -- **dentry.rs**:实现目录项 (Dentry) 结构,维护文件名到 Inode 的映射,管理父子关系和全局缓存 -- **path.rs**:实现路径解析引擎,处理绝对/相对路径、`.` 和 `..`、符号链接,提供 `vfs_lookup` 等 API -- **mount.rs**:管理文件系统挂载,维护全局挂载表,支持挂载点栈和最长前缀匹配 -- **fd_table.rs**:实现进程级文件描述符表,支持分配、关闭、复制文件描述符,管理 close-on-exec 标志 -- **file_lock.rs**:实现全局文件锁管理器,支持共享锁和排他锁,死锁检测 -- **file_system.rs**:定义文件系统抽象接口 `FileSystem` trait,声明 root_inode、sync、umount 等方法 -- **impls/**:包含各种具体文件类型的实现,如 RegFile (基于 Inode)、PipeFile (环形缓冲区)、StdioFile 等 - -## 文档导航 - -### 核心概念 - -- **[整体架构](architecture.md)**:VFS 子系统的分层架构、模块依赖、数据流转、设计决策和性能考量 - -### 子模块详解 - -- **[Inode与Dentry](inode_and_dentry.md)**:存储层 Inode 接口、目录项 Dentry 结构、缓存机制和生命周期管理 -- **[File与FDTable](file_and_fdtable.md)**:会话层 File 接口、文件描述符表实现、文件类型详解 -- **[路径解析与挂载](path_and_mount.md)**:路径解析算法、挂载表管理、符号链接处理、挂载点查找 -- **[FileSystem与错误处理](filesystem_and_errors.md)**:FileSystem trait 接口、错误类型系统、文件系统实现指南 -- **[文件锁与设备管理](filelock_and_devices.md)**:POSIX 文件锁、设备驱动注册、设备文件操作 - -### 使用指南 - -- **[使用指南](usage.md)**:VFS 系统的基本使用、文件操作、路径查找、挂载管理、最佳实践和常见陷阱 - -## 设计原则 - -VFS 子系统的设计遵循以下核心原则: - -### 1. 分层抽象 - -采用会话层和存储层分离的设计: -- **会话层 (File)**:维护打开文件的状态,如当前偏移量、打开标志,支持有状态的 read/write 操作 -- **存储层 (Inode)**:提供无状态的随机访问接口,所有方法携带 offset 参数,可被多个 File 共享 - -这种分层使得同一个文件可以被多个进程或多次打开,每次打开都有独立的会话状态,但共享底层存储。 - -### 2. 统一接口 - -通过 trait 定义统一的文件操作接口,屏蔽不同文件类型的实现细节: -- 所有文件类型都实现 `File` trait,可以统一存储在 `Arc` 中 -- 所有存储对象都实现 `Inode` trait,可以统一处理文件、目录、设备等 -- 文件描述符表对文件类型一无所知,完全通过 trait 对象操作 - -### 3. 缓存优化 - -使用多级缓存减少重复计算和磁盘访问: -- **Dentry 缓存**:缓存路径到 Dentry 的映射,避免重复路径解析 -- **Dentry 树缓存**:父子关系缓存在 Dentry 内部,加速相对路径查找 -- **挂载点缓存**:每个 Dentry 缓存其挂载点信息,避免每次查挂载表 - -### 4. 引用计数管理 - -使用 Rust 的 `Arc` 和 `Weak` 智能指针管理对象生命周期: -- Dentry 使用 `Arc` 共享所有权,`Weak` 避免父子循环引用 -- Inode 由 Dentry 持有 `Arc`,可被多个 Dentry 共享 (硬链接) -- File 对象由文件描述符表持有 `Arc`,支持 dup 等操作共享 - -## 重要约定 - -### 会话层与存储层的区别 - -| 方面 | File (会话层) | Inode (存储层) | -|------|---------------|----------------| -| 职责 | 维护打开文件的状态 | 提供底层存储访问 | -| 状态 | 有状态 (offset、flags) | 无状态 | -| 方法 | `read(buf)`, `write(buf)` | `read_at(offset, buf)`, `write_at(offset, buf)` | -| 实例 | 每次 open 创建新实例 | 多个 File 可共享同一个 Inode | -| 存储位置 | 文件描述符表 | Dentry 中 | +- 不在正式文档中维护完整 API 清单. +- 不把某个文件系统的内部格式写入 VFS 文档. +- 不承诺完整 Linux VFS 兼容性. 当前实现以 Comix 需要的系统调用语义为准. -### 路径解析规则 +## 模块边界 -- **绝对路径**:以 `/` 开头,从根目录开始解析 -- **相对路径**:不以 `/` 开头,从当前工作目录开始 -- **`.` 组件**:表示当前目录,解析时跳过 -- **`..` 组件**:表示父目录,绝对路径中不能越过根目录,相对路径中累积 `..` -- **符号链接**:`vfs_lookup` 自动跟随,`vfs_lookup_no_follow` 不跟随最后一个组件 - -### 挂载点处理 - -- 挂载表使用**最长前缀匹配**:如果 `/mnt/data` 和 `/mnt` 都是挂载点,访问 `/mnt/data/file` 使用 `/mnt/data` 的挂载点 -- 支持**挂载点栈**:同一路径可以多次挂载,最后挂载的文件系统覆盖之前的 -- **自动跟随挂载点**:`vfs_lookup` 在解析路径时自动切换到挂载点的根 Dentry - -### 文件描述符约定 - -- **FD 0-2 预留**:0 = stdin, 1 = stdout, 2 = stderr -- **最小可用 FD**:alloc() 总是分配最小的可用文件描述符 -- **dup 语义**:dup 复制的 FD 指向同一个 `Arc`,共享偏移量 -- **close-on-exec**:`O_CLOEXEC` 和 `FD_CLOEXEC` 标志控制 exec 时是否关闭文件 - -## 快速开始 - -### 打开和读取文件 - -```rust -use vfs::{vfs_lookup, RegFile, OpenFlags}; -use alloc::sync::Arc; - -// 1. 查找文件路径 -let dentry = vfs_lookup(\"/etc/passwd\")?; - -// 2. 创建 RegFile (普通文件) -let file = Arc::new(RegFile::new(dentry, OpenFlags::O_RDONLY)); - -// 3. 读取数据 -let mut buf = [0u8; 1024]; -let n = file.read(&mut buf)?; -``` - -### 使用文件描述符 - -```rust -use vfs::FDTable; - -// 创建文件描述符表 -let fd_table = FDTable::new(); - -// 分配文件描述符 -let fd = fd_table.alloc(file)?; - -// 从文件描述符读取 -let file = fd_table.get(fd)?; -let n = file.read(&mut buf)?; - -// 关闭文件描述符 -fd_table.close(fd)?; +```text +syscall layer + -> FDTable + -> File + -> Dentry and path + -> Inode + -> FileSystem implementation + -> device layer when backed by hardware ``` -### 路径操作 +- `file.rs`: 打开文件会话接口. +- `inode.rs`: 文件系统对象接口和基础元数据类型. +- `dentry.rs`: 路径节点, 父子缓存, 挂载点反向关系. +- `path.rs`: 绝对/相对路径解析, symlink 跟随, mount crossing. +- `mount.rs`: 全局挂载表, 根挂载, probe 期间根卸载. +- `fd_table.rs`: fd 分配, dup, close, close-on-exec. +- `file_system.rs`: 文件系统实例接口. +- `file_lock.rs`: 全局 advisory 文件锁. +- `dev.rs`, `devno.rs`, `impls/*_dev_file.rs`: 设备号和设备文件. -```rust -use vfs::{normalize_path, split_path, parse_path}; +## 关键流程 -// 规范化路径 -let path = normalize_path(\"/a/b/../c/./d\"); // \"/a/c/d\" +### open -// 分割目录和文件名 -let (dir, name) = split_path(\"/etc/passwd\")?; // (\"/etc\", \"passwd\") +1. 系统调用层解析 flags 和 mode. +2. `path.rs` 把路径解析为 `Dentry`. +3. 根据 inode 类型创建 `RegFile`, `CharDeviceFile`, `BlockDeviceFile` 或其他 `File` 实现. +4. 当前任务的 `FDTable` 分配最小可用 fd. -// 解析路径组件 -let components = parse_path(\"../../foo/bar\"); -``` - -### 挂载文件系统 +### read and write -```rust -use vfs::{MOUNT_TABLE, MountFlags}; -use alloc::sync::Arc; +1. 系统调用层通过 fd 取出 `Arc`. +2. `File` 根据自身类型处理 offset, flags 和流式语义. +3. 普通文件转发到 `Inode::read_at` 或 `write_at`. +4. 设备文件通过设备号转发到字符或块设备驱动. -// 假设已有一个文件系统实现 -let fs: Arc = create_my_fs()?; +### path lookup -// 挂载到 /mnt -MOUNT_TABLE.mount( - fs, - \"/mnt\", - MountFlags::empty(), - Some(String::from(\"/dev/sda1\")) -)?; +1. 绝对路径从当前任务 root 或全局 root 开始, 相对路径从 cwd 开始. +2. 每个组件先查 `Dentry` 子缓存, miss 后调用父 inode 的 lookup. +3. 成功 lookup 后创建子 `Dentry`, 视 inode 的 `cacheable` 策略加入缓存. +4. 每步都会检查 mount point, 命中时切换到挂载文件系统根 dentry. -// 访问挂载点下的文件 -let dentry = vfs_lookup(\"/mnt/data/file.txt\")?; +### mount -// 卸载 -MOUNT_TABLE.umount(\"/mnt\")?; -``` +1. `FileSystem::root_inode` 生成被挂载文件系统根 inode. +2. `MountTable` 为挂载点创建根 `Dentry` 和 `MountPoint`. +3. 同一路径允许形成栈, 栈顶为当前可见文件系统. +4. 卸载时恢复下层挂载或清除挂载缓存. -## 相关资源 +## 并发和生命周期 -### 源代码位置 +- `FDTable`, `Dentry` 子节点, `DENTRY_CACHE`, `MountTable`, 文件锁表使用内核锁保护. +- `File` 和 `Inode` 以 trait object + `Arc` 共享. `dup` 共享同一个 `File`, 因此也共享 offset. +- `Dentry` 到父节点和全局缓存使用 `Weak`, 避免父子环和缓存永久持有对象. +- procfs 这类动态路径可以通过 `Inode::cacheable` 禁止 dentry 缓存, 避免进程退出后的陈旧路径. +- mount root 记录 mounted-on 关系, 使 full path 和 `..` 能跨挂载边界工作. -- **主模块**:`os/src/vfs/mod.rs` -- **核心接口**:`os/src/vfs/file.rs`, `os/src/vfs/inode.rs` -- **路径解析**:`os/src/vfs/path.rs` -- **完整源码**:`os/src/vfs/` 目录 +## 已知限制 -### 配置常量 +- mount flags 主要作为结构化状态保存, 并非所有 flags 都完整强制执行. +- dentry 失效仍较粗粒度, 跨文件系统 rename/unlink 后的一致性依赖各实现配合. +- 文件锁是 advisory lock, 不会自动阻止未遵守锁协议的读写路径. +- 设备文件覆盖的是当前内核已有驱动集合, 不是完整 Linux devtmpfs. -- **最大文件描述符数**:`os/src/config.rs:DEFAULT_MAX_FDS` - -### 依赖模块 - -- **sync**:提供 `SpinLock` 等同步原语 -- **kernel**:提供 `current_task()` 获取当前任务信息 -- **uapi**:定义 POSIX 兼容的类型和常量 (OpenFlags, Stat, etc.) - -### 支持的文件系统 - -- **tmpfs**:内存文件系统 -- **fat32**:FAT32 文件系统 (通过 fatfs crate) -- **devfs**:设备文件系统 (字符设备和块设备) - -### 版本信息 +## 文档导航 -- **Rust 版本**:nightly-2025-01-13 -- **目标架构**:riscv64gc-unknown-none-elf -- **支持架构**:RISC-V (当前),LoongArch (规划中) +- [architecture.md](architecture.md): VFS 分层和生命周期总览. +- [inode_and_dentry.md](inode_and_dentry.md): 存储对象和路径缓存设计. +- [file_and_fdtable.md](file_and_fdtable.md): 打开文件会话和 fd 语义. +- [path_and_mount.md](path_and_mount.md): 路径解析和挂载表. +- [filesystem_and_errors.md](filesystem_and_errors.md): 文件系统接入边界和错误策略. +- [filelock_and_devices.md](filelock_and_devices.md): 文件锁和设备节点. +- [usage.md](usage.md): 子系统协作流程. + +## 源码索引 + +- `os/src/vfs/mod.rs`: VFS 模块入口和公共导出. +- `os/src/vfs/file.rs`: `File` 会话层接口. +- `os/src/vfs/inode.rs`: `Inode` 存储层接口和元数据. +- `os/src/vfs/dentry.rs`: dentry 结构, 缓存和挂载关系. +- `os/src/vfs/path.rs`: 路径解析, symlink, mount crossing. +- `os/src/vfs/mount.rs`: `MountTable`, `MountPoint`, 根挂载. +- `os/src/vfs/fd_table.rs`: fd table 和 dup/exec 生命周期. +- `os/src/vfs/file_lock.rs`: advisory lock 管理. +- `os/src/vfs/dev.rs`, `os/src/vfs/devno.rs`: 设备号和驱动注册表. +- `os/src/vfs/impls/`: 普通文件, 管道, stdio, 字符设备, 块设备文件实现. diff --git a/document/vfs/architecture.md b/document/vfs/architecture.md index 06a09e2f..8ab67ae5 100644 --- a/document/vfs/architecture.md +++ b/document/vfs/architecture.md @@ -1,586 +1,126 @@ -# VFS 子系统架构 +# VFS 架构 -## 概述 +VFS 的核心设计是把"名字", "打开会话", "存储对象", "挂载边界"分开. 系统调用只处理 fd 和路径, 具体文件系统只处理 inode 和数据, 设备层只暴露驱动接口. -本文档详细介绍 VFS 子系统的整体架构、模块依赖关系、数据流转过程、设计决策以及性能和安全性考量。VFS 子系统采用分层架构设计,各层职责清晰,通过目录项缓存和挂载表实现高效的文件访问。 +## 当前状态 -## 分层架构 - -VFS 子系统采用四层架构,从上到下依次为应用层、路径层、会话层和存储层: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 应用层 (Application Layer) │ -│ │ -│ 系统调用: open() read() write() lseek() close() mount() │ -│ getdents64() stat() fstat() chown() chmod() │ -│ │ -│ 文件描述符表 (FDTable) │ -│ 进程级资源,管理打开的文件,支持 dup/dup2/close-on-exec │ -└─────────────────────────────┬───────────────────────────────────┘ - │ - │ 通过 FD 获取 Arc - │ -┌─────────────────────────────▼───────────────────────────────────┐ -│ 会话层 (Session Layer) │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ File trait - 有状态的文件操作接口 │ │ -│ │ │ │ -│ │ · read(buf) / write(buf) - 从当前 offset 读写 │ │ -│ │ · lseek(offset, whence) - 设置偏移量 │ │ -│ │ · metadata() - 获取文件元数据 │ │ -│ │ │ │ -│ │ 实现类型: │ │ -│ │ · RegFile - 普通文件 (基于 Inode,支持 seek) │ │ -│ │ · PipeFile - 管道 (环形缓冲区,流式) │ │ -│ │ · StdioFile - 标准 I/O │ │ -│ │ · CharDevFile - 字符设备文件 │ │ -│ │ · BlkDevFile - 块设备文件 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────┬───────────────────────────────────┘ - │ - │ RegFile 持有 Dentry - │ -┌─────────────────────────────▼───────────────────────────────────┐ -│ 路径层 (Path Layer) │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Dentry (目录项) - 路径组件的缓存 │ │ -│ │ │ │ -│ │ · name: String - 文件名 │ │ -│ │ · inode: Arc - 关联的 Inode │ │ -│ │ · parent: Weak - 父目录 (弱引用避免循环) │ │ -│ │ · children: BTreeMap> - 子项缓存 │ │ -│ │ · mount_point: Option> - 挂载点信息 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ DentryCache - 全局路径缓存 │ │ -│ │ cache: BTreeMap> │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ MountTable - 全局挂载表 │ │ -│ │ mounts: BTreeMap>> │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────┬───────────────────────────────────┘ - │ - │ Dentry 持有 Inode - │ -┌─────────────────────────────▼───────────────────────────────────┐ -│ 存储层 (Storage Layer) │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Inode trait - 无状态的存储访问接口 │ │ -│ │ │ │ -│ │ · read_at(offset, buf) / write_at(offset, buf) │ │ -│ │ · metadata() - 获取文件元数据 │ │ -│ │ · lookup(name) - 在目录中查找子项 │ │ -│ │ · create / mkdir / unlink / rmdir - 目录操作 │ │ -│ │ · readdir() - 列出目录内容 │ │ -│ │ · truncate / sync - 文件管理 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ FileSystem trait - 文件系统抽象 │ │ -│ │ · root_inode() - 获取根 Inode │ │ -│ │ · sync() / umount() - 文件系统操作 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└───────────────────────────────────────────────────────────────────┘ -``` - -### 各层职责 - -#### 应用层 (Application Layer) - -应用层是用户空间和内核空间的接口,通过系统调用提供文件操作功能。文件描述符表 (FDTable) 是进程级资源,每个进程维护独立的文件描述符空间。主要职责: -- 系统调用参数验证和权限检查 -- 文件描述符到 File 对象的映射管理 -- dup/dup2/dup3 文件描述符复制 -- close-on-exec 标志管理 (用于 exec 系统调用) - -#### 会话层 (Session Layer) - -会话层维护打开文件的会话状态,如当前读写偏移量、打开标志 (只读/只写/读写/追加等)。同一个底层 Inode 可以被多个 File 对象引用,每个都有独立的 offset。主要特点: -- 方法不携带 offset 参数,由内部维护 (`read`/`write` vs `read_at`/`write_at`) -- 支持可 seek 文件 (RegFile) 和流式设备 (PipeFile) -- 通过 trait 对象 `Arc` 实现多态,支持异构文件类型 - -#### 路径层 (Path Layer) - -路径层管理文件系统的命名空间,包括目录树结构、路径解析、挂载点管理。Dentry 是核心数据结构,缓存文件名到 Inode 的映射,加速重复路径查找。主要职责: -- 路径解析:支持绝对路径、相对路径、`.` 和 `..` -- 目录项缓存:避免重复的 Inode lookup 操作 -- 挂载点管理:支持多文件系统挂载,最长前缀匹配 -- 符号链接解析:自动跟随符号链接 (可选) - -#### 存储层 (Storage Layer) - -存储层提供无状态的文件存储访问接口,所有方法携带 offset 参数,实现随机访问能力。Inode 是抽象接口,具体实现由各个文件系统提供 (tmpfs、fat32 等)。主要职责: -- 数据读写:read_at/write_at 提供指定偏移量的访问 -- 目录操作:lookup/create/mkdir/unlink/rmdir 管理目录结构 -- 元数据管理:metadata/truncate/chmod/chown 等 -- 文件系统操作:sync/umount 等全局操作 - -## 模块依赖关系 - -VFS 子系统内部模块之间的依赖关系如下图所示: - -``` - ┌──────────────┐ - │ mod.rs │ 模块入口,导出公共 API - └──────┬───────┘ - │ 依赖 - ┌──────────┼──────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────┐ ┌────────┐ ┌──────────┐ -│fd_table │ │path.rs │ │impls/ │ -└────┬────┘ └───┬────┘ └────┬─────┘ - │ │ │ - │ │ │ - └──────────┼───────────┘ - │ 都依赖 - ▼ - ┌──────────────┐ - │ file.rs │ File trait (会话层) - │ │ - └──────┬───────┘ - │ RegFile 等持有 - ▼ - ┌──────────────┐ - │ dentry.rs │ Dentry 结构 - │ │ - └──────┬───────┘ - │ 持有 - ▼ - ┌──────────────┐ - │ inode.rs │ Inode trait (存储层) - └──────────────┘ - -独立模块: -┌────────────┐ ┌──────────────┐ ┌────────────┐ -│ mount.rs │ │ file_lock.rs │ │ error.rs │ -│ (挂载表) │ │ (文件锁) │ │ (错误) │ -└────────────┘ └──────────────┘ └────────────┘ - -┌──────────────┐ ┌───────────────┐ -│ file_system │ │ dev/devno │ -│ (FS trait) │ │ (设备管理) │ -└──────────────┘ └───────────────┘ -``` - -### 依赖说明 - -1. **mod.rs → file.rs / path.rs / fd_table.rs**:模块入口导出所有公共 API,依赖各子模块 -2. **fd_table.rs → file.rs**:FDTable 存储 `Arc`,依赖 File trait -3. **path.rs → dentry.rs / mount.rs**:路径解析需要 Dentry 和挂载表 -4. **impls/* → file.rs / inode.rs**:RegFile 等实现 File trait,依赖 Dentry 和 Inode -5. **dentry.rs → inode.rs**:Dentry 持有 `Arc`,依赖 Inode trait -6. **file.rs ← inode.rs**:File trait 的 metadata() 返回 InodeMetadata,定义在 inode.rs -7. **所有模块 → error.rs**:统一的错误类型 FsError - -### 关键数据流路径 - -#### 路径 1: open 系统调用 +当前实现已经形成稳定的五个层次: -``` -系统调用 sys_open(path, flags) - → vfs_lookup(path) # path.rs - → 解析路径组件,查找 Dentry # 使用 DENTRY_CACHE - → 检查挂载点 # 使用 MOUNT_TABLE - → 返回 Arc - → RegFile::new(dentry, flags) # impls/reg_file.rs - → fd_table.alloc(Arc::new(file)) # fd_table.rs - → 返回文件描述符 fd +```text +syscall and task state + -> FDTable + -> File + -> Dentry tree and MountTable + -> Inode + -> FileSystem and concrete FS ``` -#### 路径 2: read 系统调用 +- `FDTable` 是每个任务可见的 fd 命名空间. +- `File` 是打开文件描述, 可以是普通文件, 管道, stdio, 字符设备或块设备. +- `Dentry` 是路径缓存节点, 保存名字, 父子关系, inode 和 mount 关系. +- `Inode` 是底层文件对象, 由 ext4, tmpfs, procfs, sysfs, VFAT 等实现. +- `FileSystem` 是一次挂载的文件系统实例. -``` -系统调用 sys_read(fd, buf, len) - → fd_table.get(fd) # 获取 Arc - → file.read(buf) # File trait 方法 - → (RegFile) inode.read_at(offset, buf) # 委托给 Inode - → (具体文件系统实现) 从磁盘读取数据 - → 更新 RegFile 内部的 offset - → 返回读取字节数 -``` +## 目标 -#### 路径 3: mount 系统调用 +- 清晰分离 open-time 状态和 storage-time 状态. +- 支持同一路径树中混合多个文件系统. +- 支持 `dup` 共享 open file description 的语义. +- 为动态伪文件系统和设备节点保留扩展点. -``` -系统调用 sys_mount(device, path, fs_type, flags) - → 创建文件系统实例 fs: Arc - → MOUNT_TABLE.mount(fs, path, flags, device) # mount.rs - → 创建 MountPoint,包含 fs 和 root Dentry - → 添加到挂载表 mounts[path].push(mount_point) - → 更新 DENTRY_CACHE 中的挂载点信息 - → 后续 vfs_lookup(path下的文件) 会自动切换到挂载的文件系统 -``` +## 非目标 -#### 路径 4: 路径解析 +- 不复制 Linux VFS 的所有缓存, permission 和 superblock 细节. +- 不在架构文档中列出每个 trait 方法. +- 不把 ext4, tmpfs, VFAT 的内部实现放进 VFS 架构文档. -``` -vfs_lookup(\"/mnt/data/file.txt\") - → parse_path() 解析为 [Root, \"mnt\", \"data\", \"file.txt\"] - → 从根 Dentry 开始 - → resolve_component(\"mnt\") - → base.lookup_child(\"mnt\") # 先查 Dentry 缓存 - → 如果未命中: base.inode.lookup(\"mnt\") # 查 Inode - → 创建新 Dentry,加入缓存 - → check_mount_point() # 检查是否为挂载点 - → resolve_component(\"data\") ... - → resolve_component(\"file.txt\") ... - → 返回最终 Dentry -``` - -## 核心机制 - -### 目录项缓存 (Dentry Cache) +## 分层职责 -Dentry 缓存是 VFS 性能的关键,避免了重复的路径解析和 Inode lookup。 +### FDTable -#### 多级缓存结构 - -``` -┌───────────────────────────────────────────────────────┐ -│ DentryCache (全局缓存) │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ \"/\" → Weak │ │ -│ │ \"/etc\" → Weak │ │ -│ │ \"/etc/passwd\" → Weak │ │ -│ │ \"/mnt/data\" → Weak │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -│ 使用 Weak 避免延长生命周期 │ -│ 当 Dentry 不再被其他地方引用时自动从缓存消失 │ -└───────────────────────────────────────────────────────┘ - -┌───────────────────────────────────────────────────────┐ -│ Dentry 内部缓存 (父子关系) │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ Dentry(\"/etc\") │ │ -│ │ parent: Weak │ │ -│ │ children: { │ │ -│ │ \"passwd\" → Arc, │ │ -│ │ \"hosts\" → Arc, │ │ -│ │ ... │ │ -│ │ } │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -│ 加速相对路径查找,避免每次都查询父 Inode │ -└───────────────────────────────────────────────────────┘ -``` +`FDTable` 保存 fd 到 `Arc` 的映射. fd 是进程视角的整数索引, `File` 是被打开的对象. `dup` 和 `fork` 可以共享同一个 `File`, 因此共享 offset 和 file status flags. fd flags 仍属于 fd slot. -#### 缓存更新策略 +### File -- **插入时机**:路径解析成功后自动插入 `DENTRY_CACHE.insert(&dentry)` -- **失效时机**:Weak 引用自动失效,不需要手动清理 -- **一致性保证**:文件删除时调用 `DENTRY_CACHE.remove(path)` 和 `parent.remove_child(name)` +`File` 表达一次打开会话. 普通文件会保存 offset 并转发到 inode 的随机访问接口. 管道是流式对象, 没有 seek 语义. 设备文件根据 inode 中的设备号进入设备驱动. -### 挂载表 (Mount Table) +### Dentry and MountTable -挂载表支持多文件系统共存,使用最长前缀匹配查找挂载点。 +`Dentry` 把路径组件映射到 inode, 并缓存父子关系. `MountTable` 把路径映射到挂载点栈. 路径解析命中挂载点后切换到挂载文件系统的根 dentry, 调用者不需要知道跨越了哪个文件系统. -#### 挂载点栈 +### Inode -``` -MOUNT_TABLE.mounts: BTreeMap>> - -例如: -{ - \"/\": [MountPoint(tmpfs, \"/\")], - \"/mnt\": [ - MountPoint(fat32, \"/dev/sda1\"), # 第一次挂载 - MountPoint(ext4, \"/dev/sda2\") # 第二次挂载,覆盖 - ], - \"/mnt/data\": [MountPoint(tmpfs, None)] -} - -访问 \"/mnt/data/file\" 时: -1. 查找所有以 \"/mnt\" 开头的挂载点: [\"/\", \"/mnt\", \"/mnt/data\"] -2. 选择最长匹配: \"/mnt/data\" -3. 使用栈顶挂载点: MountPoint(tmpfs) -4. 从该挂载点的 root Dentry 开始解析剩余路径 -``` +`Inode` 是文件系统对象接口. 它不保存打开会话 offset, 因此可以被多个 `File` 共享. 目录 lookup, create, unlink, rename 和 readdir 也在这个层次完成. -#### 挂载点查找算法 - -```rust -// path.rs:check_mount_point() -fn check_mount_point(dentry: Arc) -> Result, FsError> { - // 1. 快速路径:检查 dentry 本地缓存 - if let Some(mounted_root) = dentry.get_mount() { - return Ok(mounted_root); - } - - // 2. 慢速路径:查找挂载表 - let full_path = dentry.full_path(); - if let Some(mount_point) = MOUNT_TABLE.find_mount(&full_path) { - if mount_point.mount_path == full_path { - // 更新 dentry 的挂载缓存 - dentry.set_mount(&mount_point.root); - return Ok(mount_point.root.clone()); - } - } - - Ok(dentry) -} -``` +### FileSystem -### 文件描述符表 (FDTable) +`FileSystem` 表达一次具体文件系统挂载. VFS 只需要根 inode, sync, statfs 和 umount 等挂载级操作. 块设备适配, 内存数据结构或动态生成逻辑由具体 FS 自己负责. -每个进程维护独立的文件描述符表,管理打开的文件。 +## 关键流程 -#### FDTable 结构 +### 打开普通文件 +```text +sys_open + -> vfs_lookup + -> Dentry + -> RegFile + -> FDTable alloc ``` -FDTable { - files: SpinLock>>>, - fd_flags: SpinLock>, - max_fds: usize -} - -FD 分配策略: -- alloc() 总是返回最小的可用 FD -- install_at(fd) 可以指定 FD 编号 (用于 dup2) -- 数组动态扩展,最大 max_fds (通常 1024) - -FD 标志 (fd_flags): -- FD_CLOEXEC: exec 时关闭该文件描述符 -- 独立于文件状态标志 (O_RDONLY/O_WRONLY/O_APPEND 等) -``` - -#### dup 语义 -```rust -// dup: 复制文件描述符,新旧 FD 指向同一个 Arc -let new_fd = fd_table.dup(old_fd)?; -// 共享偏移量: 新旧 FD 的 read/write 会相互影响 offset +路径解析只返回命名空间对象. 打开时才创建 `File`, 这让同一个 dentry 可以被多次打开并拥有不同 offset. -// dup2: 复制到指定 FD,如果目标 FD 已打开则先关闭 -let new_fd = fd_table.dup2(old_fd, target_fd)?; -// 特殊情况: old_fd == target_fd 时,直接返回,不关闭 +### 读取普通文件 -// dup3: dup2 的扩展,支持设置 FD_CLOEXEC -let new_fd = fd_table.dup3(old_fd, target_fd, O_CLOEXEC)?; -// 不允许 old_fd == target_fd (返回 EINVAL) +```text +sys_read + -> FDTable get + -> File read + -> Inode read_at + -> concrete FS ``` -### 引用计数与生命周期 - -VFS 使用 Rust 的智能指针管理对象生命周期,避免内存泄漏和悬空指针。 - -#### Dentry 引用关系 - -``` -┌─────────────────────────────────────┐ -│ Arc │ -│ ↑ │ -│ │ Arc (强引用) │ -│ │ │ -│ ┌─┴─────────────────────┐ │ -│ │ Dentry(\"/etc/passwd\")│ │ -│ │ parent: Weak │ ←──┐ │ -│ │ inode: Arc │ │ │ -│ └───────────────────────┘ │ │ -│ │ │ -│ Weak 避免循环引用: │ │ -│ 父子互相引用会导致内存泄漏 │ │ -└───────────────────────────────┼────┘ - │ - │ Arc (强引用) - ▼ - ┌──────────────┐ - │ Arc │ - │ (可被多个 │ - │ Dentry 共享)│ - └──────────────┘ -``` +`File` 负责会话状态, `Inode` 负责真实数据. 这个分界是 VFS 最重要的生命周期边界. -#### File 和 FDTable 的引用 +### 挂载文件系统 +```text +FileSystem root_inode + -> MountPoint root Dentry + -> MountTable push + -> path lookup crosses mount point ``` -Process { - fd_table: Arc -} - │ - │ Arc - ▼ -FDTable { - files: Vec>> -} - │ - │ Arc - ▼ -RegFile { - dentry: Arc, - offset: AtomicUsize, - flags: OpenFlags -} - │ - │ Arc - ▼ -Dentry { - inode: Arc -} - -dup 后共享 File 对象: -fd[3] ──┐ - ├──> Arc -fd[4] ──┘ - -fork 后共享整个 FDTable: -Parent Process ──┐ - ├──> Arc -Child Process ──┘ -``` - -## 设计决策 - -### 为什么分离 File 和 Inode? - -**决策**:将文件抽象分为会话层 (File) 和存储层 (Inode) 两层。 - -**理由**: - -1. **状态隔离**:同一个文件可被多次打开,每次有独立的状态 (offset、flags),但共享底层存储 -2. **简化实现**:Inode 实现可以完全无状态,不需要考虑并发打开的 offset 管理 -3. **支持硬链接**:多个 Dentry 可以共享同一个 Inode,符合 POSIX 语义 -4. **管道等特殊文件**:PipeFile 不需要 Inode,直接实现 File trait,灵活性更高 - -**权衡**:增加了一层抽象,但换来了清晰的职责划分和更好的扩展性。 - -### 为什么使用 Dentry 缓存? - -**决策**:维护全局 Dentry 缓存和 Dentry 内部的父子关系缓存。 - -**理由**: - -1. **性能优化**:避免重复路径解析,减少 Inode lookup 操作 (磁盘 I/O) -2. **一致性**:所有路径解析返回相同的 Dentry 对象,简化状态管理 -3. **减少内存**:Weak 引用允许不再使用的 Dentry 被自动回收 - -**权衡**:需要在文件删除/重命名时维护缓存一致性,但实际复杂度可控。 - -### 为什么挂载表使用最长前缀匹配? - -**决策**:查找挂载点时使用最长前缀匹配算法,而不是精确匹配。 - -**理由**: - -1. **层次化挂载**:支持 `/` 和 `/mnt` 同时作为挂载点,访问 `/mnt/file` 时自动使用 `/mnt` -2. **Linux 兼容**:Linux VFS 也使用最长前缀匹配 -3. **灵活性**:可以在任意目录挂载新文件系统,无需特殊处理 - -**实现**:遍历所有挂载点,找到路径前缀最长的一个,时间复杂度 O(n),n 为挂载点数量 (通常很小)。 - -### 为什么支持挂载点栈? - -**决策**:同一路径可以多次挂载,维护一个栈,最后挂载的文件系统覆盖之前的。 - -**理由**: - -1. **容器支持**:容器技术需要在同一挂载点多次挂载 (mount namespace) -2. **调试方便**:可以临时挂载新文件系统,卸载后恢复原来的 -3. **Linux 兼容**:Linux 支持 overmounting - -**实现**:每个挂载路径对应一个 `Vec>`,栈顶是当前可见的。 - -### 为什么 FDTable 使用 Vec 而不是 HashMap? - -**决策**:FDTable 内部使用 `Vec>>` 存储文件描述符。 - -**理由**: - -1. **FD 编号连续**:POSIX 要求 alloc() 返回最小可用 FD,Vec 可以 O(n) 时间找到 -2. **内存效率**:大部分进程只打开少量文件,Vec 更紧凑 -3. **缓存友好**:Vec 的内存布局连续,访问 FD 时缓存命中率高 - -**权衡**:如果进程打开大量文件且稀疏分布,Vec 可能浪费空间,但实际场景很少见。 - -### 为什么使用 trait 对象而不是枚举? - -**决策**:File 和 Inode 使用 trait 对象 (`Arc`),而不是枚举 (`enum File { Reg, Pipe, ... }`)。 - -**理由**: - -1. **扩展性**:可以在外部 crate 中添加新的文件类型,无需修改 VFS 核心代码 -2. **代码复用**:不同文件类型共享相同的操作接口,FDTable 等无需关心具体类型 -3. **动态分发**:支持运行时多态,灵活性更高 - -**权衡**:trait 对象有轻微的虚函数调用开销,但在 VFS 场景下可以忽略 (I/O 开销远大于调用开销)。 - -## 性能考量 - -### 关键优化 - -1. **Dentry 缓存**:避免重复路径解析,减少 Inode lookup (磁盘 I/O) - 这是最重要的性能优化 -2. **挂载点缓存**:Dentry 本地缓存挂载点信息,避免每次查挂载表 -3. **父子关系缓存**:Dentry 内部缓存子项,加速相对路径查找 -4. **Weak 引用**:全局缓存使用 Weak,不延长 Dentry 生命周期,减少内存占用 -5. **原子操作**:RegFile 的 offset 使用 AtomicUsize,避免锁开销 - -### 性能瓶颈 - -1. **路径解析**:深层路径需要多次 Inode lookup,即使有缓存第一次访问仍然慢 - - **建议**:尽量使用绝对路径,避免 `../../..` 等复杂相对路径 -2. **挂载点查找**:O(n) 时间复杂度,如果挂载点很多可能变慢 - - **建议**:限制挂载点数量,或使用前缀树优化 (未实现) -3. **全局 Dentry 缓存锁**:并发查找时可能竞争 SpinLock - - **建议**:未来可考虑分片锁或无锁缓存 - -### 预期性能 - -在典型的 RISC-V 平台上: - -- **vfs_lookup 缓存命中**:约 100-500 纳秒 (查 BTreeMap + 克隆 Arc) -- **vfs_lookup 缓存未命中**:约 10-100 微秒 (Inode lookup + 创建 Dentry) -- **read/write 系统调用**:约 1-10 微秒 (不含实际 I/O) -- **open 系统调用**:约 5-50 微秒 (路径解析 + 创建 File 对象) - -**注意**:实际性能取决于底层文件系统实现、磁盘速度、编译器优化级别等。 - -## 安全性分析 - -### 安全机制 -1. **类型安全**:Rust 类型系统保证内存安全,无空指针、无数据竞争 -2. **引用计数**:Arc/Weak 自动管理生命周期,无手动 free,避免 use-after-free -3. **权限检查**:FileMode 提供权限位检查 (当前简化为 root-only,未来支持多用户) -4. **路径规范化**:normalize_path 防止 `../../../` 越过根目录 -5. **挂载隔离**:进程可以有独立的挂载命名空间 (未实现,规划中) +同一个 mount path 可以重复挂载. 栈顶是当前可见挂载, umount 后恢复下层挂载. -### 已知限制 +### 根文件系统探测 -1. **权限系统简化**:当前假设所有操作都是 root 用户,权限检查未完全实现 - - **影响**:无法防止恶意进程访问其他用户文件 -2. **符号链接循环**:vfs_lookup 不检测符号链接循环,可能导致栈溢出 - - **影响**:恶意构造的符号链接可能导致内核崩溃 -3. **缓存一致性**:目录删除后,Dentry 缓存可能残留 - - **影响**:可能访问到已删除的文件,需要手动调用 remove 清理 -4. **挂载点安全**:未限制挂载操作的权限 - - **影响**:任何进程都可以随意挂载文件系统 +当前启动路径由 FS 层驱动, VFS 只提供 mount 和 lookup 能力. `init_rootfs_from_discovered_block_devices` 会遍历块设备和分区, 尝试打开 ext4, 并选择包含 `/bin/sh` 或 `/bin/ash` 的设备作为 `/`. -### 未来改进 +## 并发和生命周期约束 -1. **完整权限系统**:实现 uid/gid 检查,支持多用户 -2. **符号链接限制**:限制解析深度 (如 Linux 的 40 层),检测循环 -3. **mount namespace**:支持进程级挂载命名空间,隔离容器 -4. **capability**:细粒度权限控制,如 CAP_SYS_ADMIN 控制挂载权限 +- VFS 对象广泛使用 `Arc` 共享生命周期, 使用 `Weak` 打断反向引用. +- `Dentry` 的 parent 和 global cache 不应持有强引用到上游对象. +- 挂载表和 dentry cache 的锁粒度较粗, 适合当前内核规模, 不是高度并行文件服务器设计. +- `File` 内部是否需要原子 offset 或锁由具体实现决定, 但 trait 要求 `Send + Sync`. +- procfs 动态路径要谨慎缓存, 当前通过 `cacheable` 让实现决定是否进入 dentry cache. -## 扩展可能性 +## 已知限制 -未来可能的扩展方向: +- permission, namespace, chroot 等策略尚未形成完整安全模型. +- mount propagation, bind mount, lazy umount 等 Linux 高级语义未实现. +- dentry 缓存没有完整的统一 invalidation 协议. +- `FileSystem` 没有完整 superblock 概念, 挂载状态较轻量. -1. **并发优化**:无锁 Dentry 缓存,减少锁竞争 -2. **网络文件系统**:支持 NFS、9P 等远程文件系统协议 -3. **文件系统堆栈**:支持 overlayfs、unionfs 等组合文件系统 -4. **异步 I/O**:支持 io_uring 风格的异步文件操作 -5. **内存映射文件**:实现 mmap 系统调用,支持文件映射到进程地址空间 -6. **文件系统快照**:支持 COW 文件系统 (如 btrfs、zfs) -7. **实时监控**:inotify/fanotify 风格的文件系统事件通知 +## 源码索引 -这些扩展在不破坏现有 API 的前提下都是可行的,得益于分层架构的良好封装。 +- `os/src/vfs/mod.rs`: 模块入口和设计性 rustdoc. +- `os/src/vfs/fd_table.rs`: fd slot 生命周期, dup, close-on-exec. +- `os/src/vfs/file.rs`: 打开文件会话边界. +- `os/src/vfs/impls/reg_file.rs`: 普通文件如何桥接 File 和 Inode. +- `os/src/vfs/impls/pipe_file.rs`: 流式文件模型. +- `os/src/vfs/dentry.rs`: dentry 树, weak cache, mount metadata. +- `os/src/vfs/path.rs`: lookup 状态机. +- `os/src/vfs/mount.rs`: mount stack 和 root mount. +- `os/src/vfs/inode.rs`: 存储对象接口. +- `os/src/vfs/file_system.rs`: 挂载级文件系统接口. diff --git a/document/vfs/file_and_fdtable.md b/document/vfs/file_and_fdtable.md index 80f6be0c..cd825a98 100644 --- a/document/vfs/file_and_fdtable.md +++ b/document/vfs/file_and_fdtable.md @@ -1,804 +1,87 @@ # File 与 FDTable -## 概述 +`File` 表示一次打开的文件会话, `FDTable` 表示进程可见的文件描述符空间. 这两个对象共同实现 POSIX open file description 的核心语义. -本文档详细介绍 VFS 子系统的会话层 (File trait) 和文件描述符表 (FDTable) 的设计与实现。File trait 定义了统一的文件操作接口,支持多种文件类型;FDTable 管理进程级的文件描述符空间。 +## 当前状态 -## File Trait - 会话层接口 +- `FDTable` 内部保存 fd 到 `Arc` 的映射, 并为每个 fd 保存 fd flags. +- `File` trait 由普通文件, 管道, stdio, 字符设备文件和块设备文件实现. +- 普通文件 `RegFile` 持有 `Dentry`, 通过 dentry 的 inode 读写. +- `dup` 系列复制 fd slot, 但共享同一个 `Arc`. +- `O_CLOEXEC` 会转换为 fd flag, exec 前由 fd table 清理. -### 核心概念 +## 目标 -File trait 是 VFS 会话层的核心抽象,定义了有状态的文件操作接口。与存储层的 Inode trait 不同,File 方法不携带 offset 参数,而是在内部维护当前读写位置。 +- fd 分配和打开文件对象解耦. +- 支持最小可用 fd, dup, close-on-exec 等常见 POSIX 行为. +- 支持异构文件类型共存在同一个 fd table. +- 让普通文件 offset 属于 open session, 不是 inode. -#### File 与 Inode 的区别 +## 非目标 -| 方面 | File (会话层) | Inode (存储层) | -|------|---------------|----------------| -| 状态 | 有状态 (维护 offset、flags) | 无状态 | -| 方法签名 | `read(buf)` | `read_at(offset, buf)` | -| 实例数量 | 每次 open 创建新实例 | 多个 File 可共享同一 Inode | -| 存储位置 | FDTable 中 | Dentry 中 | -| 生命周期 | 随文件描述符关闭而结束 | 随 Dentry 释放而结束 | +- 不在文档中维护完整 fd table 方法清单. +- 不描述每个系统调用的参数检查分支. +- 不把 socket 生命周期纳入本页, socket fd 有额外网络层映射. -### File Trait 定义 +## 模块边界 -```rust -pub trait File: Send + Sync { - // 基本属性查询 - fn readable(&self) -> bool; - fn writable(&self) -> bool; - - // 核心 I/O 操作 - fn read(&self, buf: &mut [u8]) -> Result; - fn write(&self, buf: &[u8]) -> Result; - fn metadata(&self) -> Result; - - // 可选方法 (默认返回 NotSupported) - fn lseek(&self, offset: isize, whence: SeekWhence) -> Result { - Err(FsError::NotSupported) - } - - fn offset(&self) -> usize { 0 } - fn flags(&self) -> OpenFlags { OpenFlags::empty() } - - fn dentry(&self) -> Result, FsError> { - Err(FsError::NotSupported) - } - - fn inode(&self) -> Result, FsError> { - Err(FsError::NotSupported) - } - - // 高级操作 - fn set_status_flags(&self, flags: OpenFlags) -> Result<(), FsError> { - Err(FsError::NotSupported) - } - - fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result { - Err(FsError::NotSupported) - } - - fn write_at(&self, offset: usize, buf: &[u8]) -> Result { - Err(FsError::NotSupported) - } - - // 管道特定操作 - fn get_pipe_size(&self) -> Result { - Err(FsError::NotSupported) - } - - fn set_pipe_size(&self, size: usize) -> Result<(), FsError> { - Err(FsError::NotSupported) - } - - // 异步 I/O - fn get_owner(&self) -> Result { - Err(FsError::NotSupported) - } - - fn set_owner(&self, pid: i32) -> Result<(), FsError> { - Err(FsError::NotSupported) - } - - // 设备控制 - fn ioctl(&self, request: u32, arg: usize) -> Result { - Err(FsError::NotSupported) - } -} -``` - -## 文件类型实现 - -### RegFile - 普通文件 - -RegFile 是基于 Inode 的普通文件实现,支持 seek 操作。 - -#### RegFile 结构 - -```rust -pub struct RegFile { - dentry: Arc, - offset: AtomicUsize, - flags: OpenFlags, -} - -impl RegFile { - pub fn new(dentry: Arc, flags: OpenFlags) -> Self { - Self { - dentry, - offset: AtomicUsize::new(0), - flags, - } - } -} -``` - -#### RegFile 实现要点 - -```rust -impl File for RegFile { - fn readable(&self) -> bool { - let mode = self.flags & OpenFlags::O_ACCMODE; - mode == OpenFlags::O_RDONLY || mode == OpenFlags::O_RDWR - } - - fn writable(&self) -> bool { - let mode = self.flags & OpenFlags::O_ACCMODE; - mode == OpenFlags::O_WRONLY || mode == OpenFlags::O_RDWR - } - - fn read(&self, buf: &mut [u8]) -> Result { - if !self.readable() { - return Err(FsError::PermissionDenied); - } - - let offset = self.offset.load(Ordering::Relaxed); - let n = self.dentry.inode.read_at(offset, buf)?; - self.offset.fetch_add(n, Ordering::Relaxed); - Ok(n) - } - - fn write(&self, buf: &[u8]) -> Result { - if !self.writable() { - return Err(FsError::PermissionDenied); - } - - let offset = if self.flags.contains(OpenFlags::O_APPEND) { - // 追加模式:总是写到文件末尾 - self.dentry.inode.metadata()?.size - } else { - self.offset.load(Ordering::Relaxed) - }; - - let n = self.dentry.inode.write_at(offset, buf)?; - - if !self.flags.contains(OpenFlags::O_APPEND) { - self.offset.fetch_add(n, Ordering::Relaxed); - } - - Ok(n) - } - - fn lseek(&self, offset: isize, whence: SeekWhence) -> Result { - let new_offset = match whence { - SeekWhence::SET => offset as usize, - SeekWhence::CUR => { - let cur = self.offset.load(Ordering::Relaxed); - (cur as isize + offset) as usize - } - SeekWhence::END => { - let size = self.dentry.inode.metadata()?.size; - (size as isize + offset) as usize - } - }; - - self.offset.store(new_offset, Ordering::Relaxed); - Ok(new_offset) - } - - fn offset(&self) -> usize { - self.offset.load(Ordering::Relaxed) - } - - fn flags(&self) -> OpenFlags { - self.flags - } - - fn dentry(&self) -> Result, FsError> { - Ok(self.dentry.clone()) - } - - fn inode(&self) -> Result, FsError> { - Ok(self.dentry.inode.clone()) - } - - // 支持 pread/pwrite (不改变 offset) - fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result { - self.dentry.inode.read_at(offset, buf) - } - - fn write_at(&self, offset: usize, buf: &[u8]) -> Result { - self.dentry.inode.write_at(offset, buf) - } -} -``` - -### PipeFile - 管道文件 - -PipeFile 是流式设备,不支持 seek,使用环形缓冲区实现。 - -#### PipeFile 结构 - -```rust -pub struct PipeFile { - pipe: Arc, - mode: PipeMode, -} - -pub enum PipeMode { - Read, - Write, -} - -struct Pipe { - buffer: SpinLock>, - capacity: usize, - read_closed: AtomicBool, - write_closed: AtomicBool, -} -``` - -#### PipeFile 实现要点 - -```rust -impl File for PipeFile { - fn readable(&self) -> bool { - matches!(self.mode, PipeMode::Read) - } - - fn writable(&self) -> bool { - matches!(self.mode, PipeMode::Write) - } - - fn read(&self, buf: &mut [u8]) -> Result { - if !self.readable() { - return Err(FsError::PermissionDenied); - } - - let mut buffer = self.pipe.buffer.lock(); - - // 如果缓冲区为空且写端已关闭,返回 EOF - if buffer.is_empty() && self.pipe.write_closed.load(Ordering::Relaxed) { - return Ok(0); - } - - // 从缓冲区读取数据 - let len = core::cmp::min(buf.len(), buffer.len()); - for i in 0..len { - buf[i] = buffer.pop_front().unwrap(); - } - - Ok(len) - } - - fn write(&self, buf: &[u8]) -> Result { - if !self.writable() { - return Err(FsError::PermissionDenied); - } - - if self.pipe.read_closed.load(Ordering::Relaxed) { - return Err(FsError::BrokenPipe); - } - - let mut buffer = self.pipe.buffer.lock(); - - // 检查容量 - if buffer.len() + buf.len() > self.pipe.capacity { - return Err(FsError::WouldBlock); - } - - for &byte in buf { - buffer.push_back(byte); - } - - Ok(buf.len()) - } - - fn get_pipe_size(&self) -> Result { - Ok(self.pipe.capacity) - } - - fn set_pipe_size(&self, size: usize) -> Result<(), FsError> { - // 简化实现,实际需要检查 MIN_PIPE_SIZE 和 MAX_PIPE_SIZE - self.pipe.capacity = size; - Ok(()) - } - - // 管道不支持 seek - fn lseek(&self, _offset: isize, _whence: SeekWhence) -> Result { - Err(FsError::NotSupported) - } -} -``` - -### StdioFile - 标准 I/O 文件 - -StdioFile 包装控制台输入输出,提供统一的 File 接口。 - -#### StdioFile 实现 - -```rust -pub struct StdinFile; -pub struct StdoutFile; -pub struct StderrFile; - -impl File for StdinFile { - fn readable(&self) -> bool { true } - fn writable(&self) -> bool { false } - - fn read(&self, buf: &mut [u8]) -> Result { - // 从控制台读取(阻塞) - console::stdin().read(buf).map_err(|_| FsError::IoError) - } - - fn write(&self, _buf: &[u8]) -> Result { - Err(FsError::PermissionDenied) - } - - fn metadata(&self) -> Result { - Ok(InodeMetadata { - inode_type: InodeType::CharDevice, - mode: FileMode::S_IFCHR | FileMode::S_IRUSR, - ..Default::default() - }) - } -} - -impl File for StdoutFile { - fn readable(&self) -> bool { false } - fn writable(&self) -> bool { true } - - fn read(&self, _buf: &mut [u8]) -> Result { - Err(FsError::PermissionDenied) - } - - fn write(&self, buf: &[u8]) -> Result { - console::stdout().write(buf).map_err(|_| FsError::IoError) - } - - fn metadata(&self) -> Result { - Ok(InodeMetadata { - inode_type: InodeType::CharDevice, - mode: FileMode::S_IFCHR | FileMode::S_IWUSR, - ..Default::default() - }) - } -} - -// StderrFile 与 StdoutFile 类似 -``` - -### CharDevFile - 字符设备文件 - -字符设备文件通过设备驱动提供 I/O 功能。 - -```rust -pub struct CharDevFile { - dev: u64, - flags: OpenFlags, -} - -impl File for CharDevFile { - fn read(&self, buf: &mut [u8]) -> Result { - let driver = get_chrdev_driver(major(self.dev))?; - driver.read(minor(self.dev), buf) - } - - fn write(&self, buf: &[u8]) -> Result { - let driver = get_chrdev_driver(major(self.dev))?; - driver.write(minor(self.dev), buf) - } - - fn ioctl(&self, request: u32, arg: usize) -> Result { - let driver = get_chrdev_driver(major(self.dev))?; - driver.ioctl(minor(self.dev), request, arg) - } -} -``` - -### BlkDevFile - 块设备文件 - -块设备文件支持随机访问,通常用于磁盘等存储设备。 - -```rust -pub struct BlkDevFile { - dev: u64, - offset: AtomicUsize, - flags: OpenFlags, -} - -impl File for BlkDevFile { - fn read(&self, buf: &mut [u8]) -> Result { - let offset = self.offset.load(Ordering::Relaxed); - let driver = get_blkdev_driver(major(self.dev))?; - let n = driver.read_at(minor(self.dev), offset, buf)?; - self.offset.fetch_add(n, Ordering::Relaxed); - Ok(n) - } - - fn write(&self, buf: &[u8]) -> Result { - let offset = self.offset.load(Ordering::Relaxed); - let driver = get_blkdev_driver(major(self.dev))?; - let n = driver.write_at(minor(self.dev), offset, buf)?; - self.offset.fetch_add(n, Ordering::Relaxed); - Ok(n) - } - - fn lseek(&self, offset: isize, whence: SeekWhence) -> Result { - // 块设备支持 seek - let new_offset = match whence { - SeekWhence::SET => offset as usize, - SeekWhence::CUR => { - let cur = self.offset.load(Ordering::Relaxed); - (cur as isize + offset) as usize - } - SeekWhence::END => { - let size = self.metadata()?.size; - (size as isize + offset) as usize - } - }; - self.offset.store(new_offset, Ordering::Relaxed); - Ok(new_offset) - } -} -``` - -## FDTable - 文件描述符表 - -### 核心概念 - -FDTable (File Descriptor Table) 是进程级资源,管理打开的文件。每个进程有独立的 FDTable,文件描述符是进程特定的整数索引。 - -#### FDTable 的职责 +- `fd_table.rs` 只管理 fd slot 和 fd flags. +- `file.rs` 定义打开会话能力. +- `impls/reg_file.rs` 桥接普通文件和 inode. +- `impls/pipe_file.rs` 管理流式管道缓冲区. +- `impls/char_dev_file.rs` 和 `impls/blk_dev_file.rs` 把设备 inode 转成驱动访问. +- `impls/stdio_file.rs` 绑定标准输入输出. -- **分配文件描述符**: 总是返回最小可用的 FD (POSIX 要求) -- **文件生命周期管理**: 通过 Arc 引用计数自动释放文件 -- **dup 语义**: 支持文件描述符复制,共享 File 对象 -- **close-on-exec**: 管理 FD_CLOEXEC 标志 +## 关键流程 -### FDTable 结构 +### open 后安装 fd -```rust -pub struct FDTable { - /// 文件描述符数组 - files: SpinLock>>>, - - /// FD 标志数组 (与 files 索引对应) - fd_flags: SpinLock>, - - /// 最大文件描述符数量 - max_fds: usize, -} - -bitflags! { - pub struct FdFlags: u32 { - const CLOEXEC = 1; // Close on exec - } -} -``` - -### FDTable 方法 - -#### 分配文件描述符 - -```rust -impl FDTable { - pub fn alloc(&self, file: Arc) -> Result { - self.alloc_with_flags(file, FdFlags::empty()) - } - - pub fn alloc_with_flags(&self, file: Arc, flags: FdFlags) - -> Result { - let mut files = self.files.lock(); - let mut fd_flags = self.fd_flags.lock(); - - // 查找最小可用 FD - for (fd, slot) in files.iter_mut().enumerate() { - if slot.is_none() { - *slot = Some(file); - fd_flags[fd] = flags; - return Ok(fd); - } - } - - // 扩展数组 - let fd = files.len(); - if fd >= self.max_fds { - return Err(FsError::TooManyOpenFiles); - } - - files.push(Some(file)); - fd_flags.push(flags); - Ok(fd) - } - - pub fn install_at(&self, fd: usize, file: Arc) - -> Result<(), FsError> { - self.install_at_with_flags(fd, file, FdFlags::empty()) - } - - pub fn install_at_with_flags(&self, fd: usize, file: Arc, - flags: FdFlags) -> Result<(), FsError> { - let mut files = self.files.lock(); - let mut fd_flags = self.fd_flags.lock(); - - if fd >= self.max_fds { - return Err(FsError::InvalidArgument); - } - - // 扩展数组到指定大小 - while files.len() <= fd { - files.push(None); - fd_flags.push(FdFlags::empty()); - } - - files[fd] = Some(file); - fd_flags[fd] = flags; - Ok(()) - } -} -``` - -#### 访问和关闭 - -```rust -impl FDTable { - pub fn get(&self, fd: usize) -> Result, FsError> { - let files = self.files.lock(); - files.get(fd) - .and_then(|f| f.clone()) - .ok_or(FsError::BadFileDescriptor) - } - - pub fn close(&self, fd: usize) -> Result<(), FsError> { - let mut files = self.files.lock(); - let mut fd_flags = self.fd_flags.lock(); - - if fd >= files.len() || files[fd].is_none() { - return Err(FsError::BadFileDescriptor); - } - - files[fd] = None; - fd_flags[fd] = FdFlags::empty(); - Ok(()) - } -} -``` - -#### dup 系列操作 - -```rust -impl FDTable { - /// dup: 复制文件描述符 - pub fn dup(&self, old_fd: usize) -> Result { - let file = self.get(old_fd)?; - self.alloc(file) - } - - /// dup2: 复制到指定 FD - pub fn dup2(&self, old_fd: usize, new_fd: usize) -> Result { - // 特殊情况: old_fd == new_fd - if old_fd == new_fd { - self.get(old_fd)?; // 检查有效性 - return Ok(new_fd); - } - - let file = self.get(old_fd)?; - let _ = self.close(new_fd); // 忽略错误 - self.install_at(new_fd, file)?; - Ok(new_fd) - } - - /// dup3: dup2 + 支持设置标志 - pub fn dup3(&self, old_fd: usize, new_fd: usize, flags: OpenFlags) - -> Result { - // dup3 不允许 old_fd == new_fd - if old_fd == new_fd { - return Err(FsError::InvalidArgument); - } - - let file = self.get(old_fd)?; - let _ = self.close(new_fd); - - let fd_flags = FdFlags::from_open_flags(flags); - self.install_at_with_flags(new_fd, file, fd_flags)?; - Ok(new_fd) - } -} +```text +Dentry + -> File implementation + -> Arc dyn File + -> FDTable alloc + -> fd ``` -#### fork 和 exec 支持 +fd table 不关心 file 的具体类型. 类型差异都隐藏在 `File` trait object 后面. -```rust -impl FDTable { - /// 克隆整个表 (用于 fork) - pub fn clone_table(&self) -> Self { - let files = self.files.lock().clone(); - let fd_flags = self.fd_flags.lock().clone(); - Self { - files: SpinLock::new(files), - fd_flags: SpinLock::new(fd_flags), - max_fds: self.max_fds, - } - } - - /// 关闭带 CLOEXEC 标志的文件 (用于 exec) - pub fn close_exec(&self) { - let mut files = self.files.lock(); - let mut fd_flags = self.fd_flags.lock(); - - for (slot, flags) in files.iter_mut().zip(fd_flags.iter_mut()) { - if flags.contains(FdFlags::CLOEXEC) { - *slot = None; - *flags = FdFlags::empty(); - } - } - } -} -``` - -#### FD 标志管理 +### dup -```rust -impl FDTable { - pub fn get_fd_flags(&self, fd: usize) -> Result { - let files = self.files.lock(); - let fd_flags = self.fd_flags.lock(); - - if fd >= files.len() || files[fd].is_none() { - return Err(FsError::BadFileDescriptor); - } - - Ok(fd_flags[fd]) - } - - pub fn set_fd_flags(&self, fd: usize, flags: FdFlags) -> Result<(), FsError> { - let files = self.files.lock(); - let mut fd_flags = self.fd_flags.lock(); - - if fd >= files.len() || files[fd].is_none() { - return Err(FsError::BadFileDescriptor); - } - - fd_flags[fd] = flags; - Ok(()) - } -} +```text +fd 3 -> Arc File A +fd 4 -> Arc File A ``` -## 使用示例 - -### 打开和读取文件 - -```rust -// 1. 打开文件 -let dentry = vfs_lookup("/etc/passwd")?; -let file = Arc::new(RegFile::new(dentry, OpenFlags::O_RDONLY)); - -// 2. 安装到 FDTable -let fd_table = current_task().lock().fd_table.clone(); -let fd = fd_table.alloc(file)?; - -// 3. 读取数据 -let file = fd_table.get(fd)?; -let mut buf = [0u8; 1024]; -let n = file.read(&mut buf)?; - -// 4. 关闭文件 -fd_table.close(fd)?; -``` - -### 创建管道 - -```rust -pub fn create_pipe() -> Result<(Arc, Arc), FsError> { - let pipe = Arc::new(Pipe::new(4096)); // 4KB 缓冲区 - - let read_end = Arc::new(PipeFile { - pipe: pipe.clone(), - mode: PipeMode::Read, - }); - - let write_end = Arc::new(PipeFile { - pipe: pipe.clone(), - mode: PipeMode::Write, - }); - - Ok((read_end, write_end)) -} - -// 使用管道 -let (read_file, write_file) = create_pipe()?; -let read_fd = fd_table.alloc(read_file)?; -let write_fd = fd_table.alloc(write_file)?; - -// 写入数据 -let file = fd_table.get(write_fd)?; -file.write(b"Hello, pipe!")?; - -// 读取数据 -let file = fd_table.get(read_fd)?; -let mut buf = [0u8; 128]; -let n = file.read(&mut buf)?; -``` - -### dup 重定向 - -```rust -// 将 stdout 重定向到文件 -let dentry = vfs_lookup("/tmp/output.txt")?; -let file = Arc::new(RegFile::new(dentry, - OpenFlags::O_WRONLY | OpenFlags::O_CREAT | OpenFlags::O_TRUNC)); - -let fd = fd_table.alloc(file)?; -fd_table.dup2(fd, 1)?; // 1 = stdout -fd_table.close(fd)?; - -// 现在 println! 会写到文件 -``` - -## 最佳实践 - -### 实现 File 时的注意事项 - -1. **线程安全**: File 必须实现 `Send + Sync`,内部状态需要原子操作或锁保护 -2. **权限检查**: read/write 前检查 readable()/writable() -3. **错误处理**: 返回准确的 FsError (PermissionDenied/WouldBlock 等) -4. **可选方法**: 不支持的方法返回 `Err(FsError::NotSupported)` - -### 使用 FDTable 时的注意事项 - -1. **及时关闭**: 避免文件描述符泄漏,使用 RAII 模式管理 -2. **检查返回值**: get/close 可能返回错误,必须处理 -3. **dup 语义**: dup 后的 FD 共享 offset,注意并发访问 -4. **fork 后**: 父子进程共享 FDTable,修改会相互影响 - -### 性能优化建议 - -1. **批量 I/O**: 使用较大的缓冲区,减少系统调用次数 -2. **避免 lseek**: 顺序读写不需要 lseek,直接 read/write -3. **管道大小**: 根据使用场景调整管道缓冲区大小 -4. **异步 I/O**: 对于网络文件系统,考虑异步实现 - -## 常见问题 - -### Q: File 和 Inode 都有 read 方法,有什么区别? - -A: -- **File::read(buf)**: 从当前 offset 读取,自动更新 offset -- **Inode::read_at(offset, buf)**: 从指定 offset 读取,不改变状态 -- 一个 Inode 可以被多个 File 共享,各自维护独立的 offset - -### Q: dup 后的文件描述符共享什么? - -A: -- **共享**: File 对象 (包括 offset),文件状态标志 (O_APPEND 等) -- **不共享**: FD 标志 (FD_CLOEXEC) - -### Q: O_CLOEXEC 和 FD_CLOEXEC 有什么区别? +dup 后两个 fd 指向同一个 `File`, 因此普通文件的 offset 共享. 这是和再次 open 同一路径的主要区别. -A: -- **O_CLOEXEC**: open() 时指定,自动设置 FD_CLOEXEC 标志 -- **FD_CLOEXEC**: FD 标志,通过 fcntl(F_SETFD) 设置 +### close -### Q: 管道缓冲区满了怎么办? +close 只清空 fd slot. 如果这是最后一个 `Arc`, 对象才会 drop. 对普通文件而言, 数据同步仍由具体文件系统和 `sync` 路径负责, close 本身不是完整 fsync. -A: -当前实现返回 `WouldBlock` 错误。完整实现应该: -- 如果是阻塞模式,阻塞等待缓冲区有空间 -- 如果是非阻塞模式 (O_NONBLOCK),返回 WouldBlock +### exec -### Q: 如何实现 O_NONBLOCK? +exec 前 fd table 会关闭带 close-on-exec flag 的 fd. open flags 和 fd flags 分开保存, 因为 dup 共享文件状态 flags, 但 fd flags 属于单个 descriptor. -A: -File 实现需要检查 flags,在 read/write 时: -- 阻塞模式: 等待数据/空间可用 -- 非阻塞模式: 立即返回 WouldBlock +## 并发和生命周期约束 -## 相关资源 +- `FDTable` 用锁保护 slot 向量, `File` 自身必须 `Send + Sync`. +- fd table 操作的锁只保护映射关系, 不保护文件内容. +- `RegFile` 的 offset 是会话状态, 共享同一个 `RegFile` 的 fd 会共享 offset. +- 管道和设备文件有自己的同步约束, 不能假设它们支持 seek. -### 源代码位置 +## 已知限制 -- **File trait**: `os/src/vfs/file.rs` -- **FDTable**: `os/src/vfs/fd_table.rs` -- **RegFile**: `os/src/vfs/impls/reg_file.rs` -- **PipeFile**: `os/src/vfs/impls/pipe_file.rs` -- **StdioFile**: `os/src/vfs/impls/stdio_file.rs` -- **设备文件**: `os/src/vfs/impls/char_dev_file.rs`, `os/src/vfs/impls/blk_dev_file.rs` +- fd table 是简单向量结构, 适合当前最大 fd 数, 没有复杂稀疏 fd 管理. +- close 不自动清理所有外部子系统映射, 特殊 fd 需要调用方配合. +- 非阻塞语义和 poll/select 相关能力仍由各 file 实现和系统调用层逐步补齐. -### 参考文档 +## 源码索引 -- [VFS 整体架构](architecture.md) -- [Inode 与 Dentry](inode_and_dentry.md) -- [路径解析与挂载](path_and_mount.md) -- [使用指南](usage.md) +- `os/src/vfs/fd_table.rs`: fd slot, fd flags, dup, close, take_all. +- `os/src/vfs/file.rs`: 打开文件会话接口. +- `os/src/vfs/impls/reg_file.rs`: 普通文件 offset 和 inode 转发. +- `os/src/vfs/impls/pipe_file.rs`: 管道 file. +- `os/src/vfs/impls/stdio_file.rs`: stdin/stdout/stderr. +- `os/src/vfs/impls/char_dev_file.rs`: 字符设备文件. +- `os/src/vfs/impls/blk_dev_file.rs`: 块设备文件. diff --git a/document/vfs/filelock_and_devices.md b/document/vfs/filelock_and_devices.md index 2b7d8fbc..9877b0ee 100644 --- a/document/vfs/filelock_and_devices.md +++ b/document/vfs/filelock_and_devices.md @@ -1,610 +1,92 @@ -# 文件锁与设备管理 +# 文件锁与设备节点 -## 概述 +本页覆盖两个 VFS 边界能力: advisory 文件锁和设备文件. 二者都挂在 VFS 命名空间中, 但真实状态分别由全局锁表和设备驱动层维护. -本文档介绍 VFS 的文件锁机制和设备管理功能。文件锁实现 POSIX advisory locks 语义,支持进程间文件访问同步;设备管理提供字符设备和块设备的统一抽象。 +## 当前状态 -## 文件锁机制 +- `file_lock.rs` 提供全局文件锁管理器, 支持按文件范围记录共享/排他锁. +- 锁语义是 advisory, 只有遵守锁协议的调用路径才会受影响. +- `dev.rs` 提供 major/minor 设备号工具. +- `devno.rs` 保存字符/块设备主设备号约定和驱动注册. +- 字符设备和块设备通过专门的 `File` 实现进入驱动. +- `/dev` 节点由 FS 初始化阶段在根文件系统上创建. -### 核心概念 +## 目标 -文件锁(File Locks)是进程间同步文件访问的机制。VFS 实现 POSIX advisory locks,即建议性锁,不强制执行,需要进程协作遵守。 +- 让文件锁和设备访问通过普通 fd 模型暴露. +- 让 `/dev/null`, `/dev/zero`, `/dev/tty`, `/dev/vda`, `/dev/vda1` 等节点可通过路径打开. +- 让块设备可直接作为文件访问, 也可作为 ext4/VFAT 的底层存储. +- 让锁生命周期能跟随进程退出或 fd 清理释放. -#### 锁类型 +## 非目标 -```rust -pub enum LockType { - Read = 0, // F_RDLCK: 读锁(共享锁) - Write = 1, // F_WRLCK: 写锁(独占锁) - Unlock = 2, // F_UNLCK: 解锁 -} -``` - -#### 锁的语义 - -| 已持有 \ 请求 | 读锁 (共享) | 写锁 (独占) | -|--------------|------------|------------| -| **无锁** | ✅ 允许 | ✅ 允许 | -| **读锁** | ✅ 允许(可共享) | ❌ 冲突 | -| **写锁** | ❌ 冲突 | ❌ 冲突 | - -**特殊规则**: -- 同一进程的锁不冲突(可以升级/降级锁) -- 进程退出时自动释放所有锁 - -### FileLockEntry 结构 - -单个锁的表示: - -```rust -struct FileLockEntry { - /// 锁类型(读/写) - lock_type: LockType, - - /// 起始位置(文件中的绝对偏移) - start: usize, - - /// 长度(0 表示锁定到文件末尾) - len: usize, - - /// 持有锁的进程 PID - pid: i32, -} -``` - -### FileLockManager - -全局文件锁管理器: - -```rust -pub struct FileLockManager { - /// 文件锁表:FileId -> 锁列表 - locks: SpinLock>>, -} - -// 文件标识符 -struct FileId { - dev: u64, // 设备号 - ino: u64, // Inode 号 -} -``` - -### fcntl 文件锁操作 - -#### F_GETLK - 测试锁 - -检查是否有锁会阻塞请求的锁: - -```rust -pub fn test_lock( - &self, - dev: u64, - ino: u64, - start: usize, - len: usize, - flock: &mut Flock, - pid: i32, -) -> Result<(), FsError> { - let file_id = FileId { dev, ino }; - let locks = self.locks.lock(); - - // 构造请求的锁 - let requested_lock = FileLockEntry { - lock_type: LockType::from_raw(flock.l_type).ok_or(FsError::InvalidArgument)?, - start, - len, - pid, - }; - - // 检查是否有冲突的锁 - if let Some(file_locks) = locks.get(&file_id) { - for existing_lock in file_locks { - if existing_lock.conflicts_with(&requested_lock) { - // 找到冲突的锁,填充 flock 结构 - flock.l_type = existing_lock.lock_type as i16; - flock.l_start = existing_lock.start as i64; - flock.l_len = existing_lock.len as i64; - flock.l_pid = existing_lock.pid; - return Ok(()); - } - } - } - - // 没有冲突,设置为 F_UNLCK - flock.l_type = LockType::Unlock as i16; - Ok(()) -} -``` - -#### F_SETLK / F_SETLKW - 设置锁 - -```rust -pub fn set_lock( - &self, - dev: u64, - ino: u64, - start: usize, - len: usize, - lock_type: LockType, - pid: i32, - blocking: bool, // true = F_SETLKW, false = F_SETLK -) -> Result<(), FsError> { - let file_id = FileId { dev, ino }; - let mut locks = self.locks.lock(); - - match lock_type { - LockType::Unlock => { - // 释放锁 - if let Some(file_locks) = locks.get_mut(&file_id) { - file_locks.retain(|lock| - !(lock.pid == pid && lock.overlaps(start, len)) - ); - if file_locks.is_empty() { - locks.remove(&file_id); - } - } - Ok(()) - } - LockType::Read | LockType::Write => { - let file_locks = locks.entry(file_id).or_insert_with(Vec::new); - - let new_lock = FileLockEntry { - lock_type, - start, - len, - pid, - }; - - // 检查冲突 - for existing_lock in file_locks.iter() { - if existing_lock.conflicts_with(&new_lock) { - if blocking { - // TODO: 阻塞等待 - // 当前实现未完成 F_SETLKW - return Err(FsError::WouldBlock); - } else { - return Err(FsError::WouldBlock); - } - } - } - - // 移除同一进程的旧锁 - file_locks.retain(|lock| - !(lock.pid == pid && lock.overlaps(start, len)) - ); - - // 添加新锁 - file_locks.push(new_lock); - Ok(()) - } - } -} -``` - -#### release_all_locks - 进程退出清理 - -```rust -pub fn release_all_locks(&self, pid: i32) { - let mut locks = self.locks.lock(); - for file_locks in locks.values_mut() { - file_locks.retain(|lock| lock.pid != pid); - } - locks.retain(|_, file_locks| !file_locks.is_empty()); -} -``` +- 不提供强制锁. +- 不实现完整 devtmpfs. +- 不在本页记录全部 Linux 设备号. +- 不把具体驱动寄存器操作写入 VFS 文档. -### 使用示例 +## 模块边界 -#### 获取读锁 +- `file_lock.rs`: 锁表, 冲突检测, 释放. +- `dev.rs`: 设备号编码/解码. +- `devno.rs`: 设备主号约定和驱动注册表. +- `impls/char_dev_file.rs`: 字符设备 file 适配. +- `impls/blk_dev_file.rs`: 块设备 file 适配. +- `os/src/fs/mod.rs`: `/dev` 目录和设备节点创建. +- `os/src/device/`: 真实驱动和设备注册表. -```rust -use vfs::file_lock_manager; +## 关键流程 -pub fn acquire_read_lock(file: &Arc) -> Result<(), FsError> { - let dentry = file.dentry()?; - let metadata = dentry.inode.metadata()?; - - let current = current_task(); - let pid = current.lock().pid; - - file_lock_manager().set_lock( - 0, // dev (简化, 实际需要从 metadata 获取) - metadata.inode_no as u64, - 0, // start: 从文件开头 - 0, // len: 0 表示到文件末尾 - LockType::Read, - pid, - false, // 非阻塞 - ) -} -``` - -#### 升级为写锁 - -```rust -pub fn upgrade_to_write_lock(file: &Arc) -> Result<(), FsError> { - let dentry = file.dentry()?; - let metadata = dentry.inode.metadata()?; - - let current = current_task(); - let pid = current.lock().pid; - - // 同一进程可以升级锁 - file_lock_manager().set_lock( - 0, - metadata.inode_no as u64, - 0, - 0, - LockType::Write, - pid, - true, // 阻塞等待 - ) -} -``` - -#### 释放锁 +### advisory lock -```rust -pub fn release_lock(file: &Arc) -> Result<(), FsError> { - let dentry = file.dentry()?; - let metadata = dentry.inode.metadata()?; - - let current = current_task(); - let pid = current.lock().pid; - - file_lock_manager().set_lock( - 0, - metadata.inode_no as u64, - 0, - 0, - LockType::Unlock, - pid, - false, - ) -} +```text +fcntl lock request + -> File identity + -> global lock table + -> conflict check + -> record or report conflict ``` -### 限制与注意事项 - -#### 当前未实现的功能 - -1. **F_SETLKW 阻塞等待**: 当前遇到锁冲突时立即返回 `WouldBlock`,即使指定了阻塞模式 - - 完整实现需要等待队列和任务调度支持 - - 需要处理信号中断(返回 EINTR) - -2. **死锁检测**: 不检测死锁情况 - - 可能导致多个进程相互等待 - -3. **锁的范围合并**: 不自动合并相邻的锁 - - 可能导致锁表膨胀 - -#### Advisory Locks 注意事项 - -- **建议性**: 锁不是强制的,进程可以忽略锁直接读写 -- **协作**: 需要所有进程都遵守锁协议 -- **自动释放**: 进程退出或 exec 时自动释放 - -## 设备管理 +锁不直接改变 inode 读写路径. 它只为系统调用提供协作式同步状态. -### 核心概念 +### mknod and open device -设备文件是访问硬件设备的接口。VFS 支持两种设备类型: -- **字符设备**: 面向流的设备,如串口、终端 -- **块设备**: 面向块的设备,如磁盘 - -### 设备号 - -设备号由主设备号和次设备号组成: - -```rust -// dev.rs - -/// 设备号工具函数 - -/// 从主设备号和次设备号构造设备号 -pub fn makedev(major: u32, minor: u32) -> u64 { - ((major as u64) << 32) | (minor as u64) -} - -/// 提取主设备号 -pub fn major(dev: u64) -> u32 { - (dev >> 32) as u32 -} - -/// 提取次设备号 -pub fn minor(dev: u64) -> u32 { - (dev & 0xFFFFFFFF) as u32 -} +```text +/dev/vda1 dentry + -> inode type BlockDevice + -> rdev major minor + -> BlockDeviceFile + -> BlockDriver ``` -**说明**: -- **主设备号**: 标识设备类型/驱动程序(如 1 = 内存设备,8 = SCSI 磁盘) -- **次设备号**: 标识同类型设备的具体实例(如 /dev/sda1, /dev/sda2) - -### 设备驱动注册 - -#### 字符设备驱动 - -```rust -// devno.rs - -pub trait CharDeviceDriver: Send + Sync { - fn read(&self, minor: u32, buf: &mut [u8]) -> Result; - fn write(&self, minor: u32, buf: &[u8]) -> Result; - fn ioctl(&self, minor: u32, request: u32, arg: usize) - -> Result; -} - -// 全局驱动注册表 -static CHRDEV_DRIVERS: SpinLock>> - = SpinLock::new(BTreeMap::new()); - -/// 注册字符设备驱动 -pub fn register_chrdev(major: u32, driver: Arc) { - CHRDEV_DRIVERS.lock().insert(major, driver); -} - -/// 获取字符设备驱动 -pub fn get_chrdev_driver(major: u32) -> Result, FsError> { - CHRDEV_DRIVERS.lock() - .get(&major) - .cloned() - .ok_or(FsError::NoDevice) -} -``` - -#### 块设备驱动 - -```rust -pub trait BlockDeviceDriver: Send + Sync { - fn block_size(&self) -> usize; - fn total_blocks(&self, minor: u32) -> usize; - - fn read_block(&self, minor: u32, block_no: usize, buf: &mut [u8]) - -> Result; - fn write_block(&self, minor: u32, block_no: usize, buf: &[u8]) - -> Result; - - fn read_at(&self, minor: u32, offset: usize, buf: &mut [u8]) - -> Result; - fn write_at(&self, minor: u32, offset: usize, buf: &[u8]) - -> Result; -} - -static BLKDEV_DRIVERS: SpinLock>> - = SpinLock::new(BTreeMap::new()); - -pub fn register_blkdev(major: u32, driver: Arc) { - BLKDEV_DRIVERS.lock().insert(major, driver); -} - -pub fn get_blkdev_driver(major: u32) -> Result, FsError> { - BLKDEV_DRIVERS.lock() - .get(&major) - .cloned() - .ok_or(FsError::NoDevice) -} -``` - -### 创建设备文件 - -#### mknod 系统调用 - -```rust -pub fn sys_mknod(path: &str, mode: FileMode, dev: u64) - -> Result<(), FsError> { - let (dir, name) = vfs::split_path(path)?; - let parent = vfs::vfs_lookup(&dir)?; - - parent.inode.mknod(&name, mode, dev)?; - Ok(()) -} -``` - -#### 使用示例 - -```rust -// 创建字符设备文件 /dev/null (major=1, minor=3) -sys_mknod("/dev/null", - FileMode::S_IFCHR | FileMode::S_IRUSR | FileMode::S_IWUSR, - makedev(1, 3))?; - -// 创建块设备文件 /dev/sda1 (major=8, minor=1) -sys_mknod("/dev/sda1", - FileMode::S_IFBLK | FileMode::S_IRUSR | FileMode::S_IWUSR, - makedev(8, 1))?; -``` - -### 实现设备驱动示例 - -#### Null 设备驱动 - -```rust -struct NullDevice; - -impl CharDeviceDriver for NullDevice { - fn read(&self, _minor: u32, _buf: &mut [u8]) -> Result { - // 读取总是返回 EOF - Ok(0) - } - - fn write(&self, _minor: u32, buf: &[u8]) -> Result { - // 写入总是成功,数据丢弃 - Ok(buf.len()) - } - - fn ioctl(&self, _minor: u32, _request: u32, _arg: usize) - -> Result { - Err(FsError::NotSupported) - } -} - -// 注册 -pub fn init_null_device() { - register_chrdev(1, Arc::new(NullDevice)); -} -``` - -#### 内存磁盘设备 - -```rust -struct RamDisk { - data: SpinLock>, - block_size: usize, -} - -impl RamDisk { - fn new(size: usize, block_size: usize) -> Self { - Self { - data: SpinLock::new(vec![0; size]), - block_size, - } - } -} - -impl BlockDeviceDriver for RamDisk { - fn block_size(&self) -> usize { - self.block_size - } - - fn total_blocks(&self, _minor: u32) -> usize { - let data = self.data.lock(); - data.len() / self.block_size - } - - fn read_at(&self, _minor: u32, offset: usize, buf: &mut [u8]) - -> Result { - let data = self.data.lock(); - if offset >= data.len() { - return Ok(0); - } - - let len = core::cmp::min(buf.len(), data.len() - offset); - buf[..len].copy_from_slice(&data[offset..offset + len]); - Ok(len) - } - - fn write_at(&self, _minor: u32, offset: usize, buf: &[u8]) - -> Result { - let mut data = self.data.lock(); - if offset >= data.len() { - return Err(FsError::InvalidArgument); - } - - let len = core::cmp::min(buf.len(), data.len() - offset); - data[offset..offset + len].copy_from_slice(&buf[..len]); - Ok(len) - } - - // read_block 和 write_block 实现... -} -``` - -### 常见设备号分配 - -| 主设备号 | 类型 | 设备名 | 说明 | -|---------|------|--------|------| -| 1 | 字符 | mem | 内存设备 (/dev/null, /dev/zero) | -| 4 | 字符 | tty | 终端设备 | -| 5 | 字符 | tty | 控制台 | -| 8 | 块 | sd | SCSI 磁盘 (/dev/sda, /dev/sdb) | -| 11 | 块 | sr | SCSI CD-ROM | - -## 使用场景 - -### 文件锁场景 - -#### 数据库锁 - -```rust -// 数据库文件锁 -pub fn db_transaction() -> Result<(), FsError> { - let db_file = open_db()?; - - // 获取写锁 - acquire_write_lock(&db_file)?; - - // 执行事务 - // ... - - // 释放锁 - release_lock(&db_file)?; - Ok(()) -} -``` - -#### 日志轮转 - -```rust -// 多进程写日志,使用读锁 -pub fn write_log(msg: &str) -> Result<(), FsError> { - let log_file = open_log()?; - - acquire_write_lock(&log_file)?; - log_file.write(msg.as_bytes())?; - release_lock(&log_file)?; - - Ok(()) -} -``` - -### 设备访问场景 - -#### 读取磁盘分区 - -```rust -// 读取 /dev/sda1 第一个扇区 -pub fn read_boot_sector() -> Result, FsError> { - let dentry = vfs_lookup("/dev/sda1")?; - let file = Arc::new(RegFile::new(dentry, OpenFlags::O_RDONLY)); - - let mut buf = vec![0u8; 512]; - file.read(&mut buf)?; - Ok(buf) -} -``` - -#### 写入 /dev/null - -```rust -// 丢弃输出 -pub fn discard_output(data: &[u8]) -> Result<(), FsError> { - let dentry = vfs_lookup("/dev/null")?; - let file = Arc::new(RegFile::new(dentry, OpenFlags::O_WRONLY)); - - file.write(data)?; - Ok(()) -} -``` - -## 最佳实践 - -### 文件锁 +字符设备同理, 但进入字符驱动表. VFS 不关心底层是串口, 控制台还是内存伪设备, 只通过设备号选择驱动. -1. **总是释放锁**: 使用 RAII 模式确保锁被释放 -2. **避免死锁**: 按固定顺序获取多个锁 -3. **最小锁范围**: 只锁定必要的文件范围 -4. **超时机制**: 使用非阻塞模式并重试 +### rootfs 后创建 `/dev` -### 设备驱动 +当前 rootfs 探测成功后, FS 初始化会确保 `/dev` 存在, 再创建固定字符设备节点和从 `list_block_devices` 枚举得到的块设备节点. 分区设备如 `vda1`, `vda2` 会和整盘 `vda` 一起出现. -1. **错误处理**: 硬件操作可能失败,正确处理错误 -2. **同步**: 设备访问需要同步保护 -3. **缓存**: 考虑实现设备缓存提高性能 -4. **中断**: 使用中断驱动而不是轮询 +## 并发和生命周期约束 -## 相关资源 +- 文件锁表是全局状态, 需要按 owner 释放, 防止进程退出后残留. +- 设备 file 持有 dentry/inode, 真实驱动由全局设备注册表持有. +- 块设备读写必须遵守底层 `BlockDriver` 的 block size 和边界. +- 设备节点 inode 的 `rdev` 是 VFS 到驱动层的稳定桥接字段. -### 源代码位置 +## 已知限制 -- **文件锁**: `os/src/vfs/file_lock.rs` -- **设备号工具**: `os/src/vfs/dev.rs` -- **设备驱动注册**: `os/src/vfs/devno.rs` -- **CharDevFile**: `os/src/vfs/impls/char_dev_file.rs` -- **BlkDevFile**: `os/src/vfs/impls/blk_dev_file.rs` +- 文件锁不强制拦截所有读写. +- 设备节点创建是初始化时的静态扫描, 不是真正热插拔 devfs. +- 字符设备集合较小, 主要覆盖内核当前需要的 null/zero/random/tty/console/rtc. +- 分区设备依赖 MBR/GPT 解析和块设备 512 字节扇区假设. -### 参考文档 +## 源码索引 -- [VFS 整体架构](architecture.md) -- [File 与 FDTable](file_and_fdtable.md) -- [使用指南](usage.md) +- `os/src/vfs/file_lock.rs`: advisory lock 管理. +- `os/src/vfs/dev.rs`: 设备号工具. +- `os/src/vfs/devno.rs`: 主设备号和驱动注册. +- `os/src/vfs/impls/char_dev_file.rs`: 字符设备 file. +- `os/src/vfs/impls/blk_dev_file.rs`: 块设备 file. +- `os/src/fs/mod.rs`: `/dev` 节点创建. +- `os/src/device/block/mod.rs`: `BlockDriver`. +- `os/src/device/block/partition.rs`: 分区块设备. +- `os/src/device/console/`, `os/src/device/serial/`, `os/src/device/rtc/`: 字符类设备来源. diff --git a/document/vfs/filesystem_and_errors.md b/document/vfs/filesystem_and_errors.md index a0bdf753..671eb884 100644 --- a/document/vfs/filesystem_and_errors.md +++ b/document/vfs/filesystem_and_errors.md @@ -1,565 +1,84 @@ -# FileSystem Trait 与错误处理 +# FileSystem 与错误策略 -## 概述 +`FileSystem` 是 VFS 对一次挂载文件系统实例的最小抽象. 具体文件系统通过它暴露根 inode, 同步, statfs 和卸载语义. 具体文件和目录行为主要在 `Inode` 层. -本文档详细介绍 VFS 的 FileSystem trait 接口和错误处理机制。FileSystem trait 定义了文件系统的抽象接口,所有具体的文件系统实现(如 tmpfs、fat32)都必须实现此接口。错误处理部分说明了 VFS 的错误类型系统及其与 POSIX errno 的映射。 +## 当前状态 -## FileSystem Trait +- ext4, tmpfs, procfs, sysfs, simple_fs, VFAT 都接入 VFS. +- `FileSystem::fs_type` 用于 mount 列表和 procfs 展示. +- `FileSystem::root_inode` 是挂载接入点. +- `FsError` 统一 VFS 和文件系统错误, 并映射为 Linux errno. +- VFAT 当前源码路径是 `os/src/fs/vfat/`, VFS 文档只引用这个入口. -### 核心概念 +## 目标 -FileSystem trait 是文件系统的顶层抽象,它定义了文件系统级别的操作,而不是单个文件的操作(那是 Inode 的职责)。 +- 让每个文件系统只暴露 VFS 需要的挂载级能力. +- 保持错误类型跨 VFS, FS, 设备适配层可转换. +- 让系统调用层可以稳定把 `FsError` 转成 errno. +- 避免在正式文档中复制每个错误分支. -#### FileSystem 的职责 +## 非目标 -- **提供根 Inode**: 返回文件系统的根目录 Inode -- **同步操作**: 将缓存数据刷新到持久化存储 -- **统计信息**: 提供文件系统使用情况统计 -- **卸载清理**: 执行卸载前的资源清理 +- 不维护一个完整错误码大全. 以 `os/src/vfs/error.rs` 为准. +- 不规定每个具体 FS 的全部内部错误. +- 不在本页提供自定义文件系统教程式代码. -### FileSystem Trait 定义 +## 模块边界 -```rust -pub trait FileSystem: Send + Sync { - /// 文件系统类型名称 - /// - /// 返回文件系统类型的静态字符串,如 "tmpfs"、"fat32"、"ext4" - fn fs_type(&self) -> &'static str; - - /// 获取根 inode - /// - /// 返回文件系统的根目录 inode,用于挂载时创建根 Dentry - fn root_inode(&self) -> Arc; - - /// 同步文件系统 - /// - /// 将所有未写入的数据刷新到持久化存储设备 - fn sync(&self) -> Result<(), FsError>; - - /// 获取文件系统统计信息 - /// - /// 返回磁盘使用情况、inode 数量等统计信息 - fn statfs(&self) -> Result; - - /// 卸载文件系统(可选) - /// - /// 执行卸载前的清理工作,默认实现调用 sync() - fn umount(&self) -> Result<(), FsError> { - self.sync() - } -} -``` - -### StatFs 结构 - -文件系统统计信息: - -```rust -#[derive(Debug, Clone)] -pub struct StatFs { - /// 块大小(单位:字节) - pub block_size: usize, - - /// 总块数 - pub total_blocks: usize, - - /// 空闲块数 - pub free_blocks: usize, - - /// 可用块数(非特权用户可用) - pub available_blocks: usize, - - /// 总 inode 数 - pub total_inodes: usize, - - /// 空闲 inode 数 - pub free_inodes: usize, - - /// 文件系统 ID - pub fsid: u64, - - /// 最大文件名长度 - pub max_filename_len: usize, -} -``` - -## 实现文件系统 - -### TmpFS 示例 - -内存文件系统是最简单的文件系统实现: - -```rust -pub struct TmpFs { - root: Arc, - next_inode_no: AtomicUsize, -} - -impl TmpFs { - pub fn new() -> Self { - // 创建根目录 inode - let root = Arc::new(TmpfsInode::new_dir(1)); - - Self { - root, - next_inode_no: AtomicUsize::new(2), - } - } - - pub fn alloc_inode_no(&self) -> usize { - self.next_inode_no.fetch_add(1, Ordering::Relaxed) - } -} - -impl FileSystem for TmpFs { - fn fs_type(&self) -> &'static str { - "tmpfs" - } - - fn root_inode(&self) -> Arc { - self.root.clone() - } - - fn sync(&self) -> Result<(), FsError> { - // 内存文件系统无需同步 - Ok(()) - } - - fn statfs(&self) -> Result { - Ok(StatFs { - block_size: 4096, - total_blocks: 0, // 内存文件系统无限制 - free_blocks: 0, - available_blocks: 0, - total_inodes: 0, - free_inodes: 0, - fsid: 0, - max_filename_len: 255, - }) - } - - fn umount(&self) -> Result<(), FsError> { - // 内存文件系统卸载时释放所有数据 - // 实际上由 Arc 自动管理 - Ok(()) - } -} -``` - -### Fat32 示例 - -磁盘文件系统需要管理持久化存储: - -```rust -pub struct Fat32Fs { - device: Arc, - root_inode: Arc, - fat_table: SpinLock>, - dirty: AtomicBool, -} - -impl Fat32Fs { - pub fn new(device: Arc) -> Result { - // 读取引导扇区 - let boot_sector = Self::read_boot_sector(&device)?; - - // 加载 FAT 表 - let fat_table = Self::load_fat(&device, &boot_sector)?; - - // 创建根目录 inode - let root_inode = Arc::new(Fat32Inode::new_root(&boot_sector)); - - Ok(Self { - device, - root_inode, - fat_table: SpinLock::new(fat_table), - dirty: AtomicBool::new(false), - }) - } - - fn write_fat(&self) -> Result<(), FsError> { - // 将 FAT 表写回磁盘 - let fat = self.fat_table.lock(); - let buf = fat.as_slice(); - self.device.write(FAT_OFFSET, buf)?; - Ok(()) - } -} - -impl FileSystem for Fat32Fs { - fn fs_type(&self) -> &'static str { - "fat32" - } - - fn root_inode(&self) -> Arc { - self.root_inode.clone() - } - - fn sync(&self) -> Result<(), FsError> { - if self.dirty.load(Ordering::Relaxed) { - // 写回 FAT 表 - self.write_fat()?; - - // 同步所有脏 inode - // ... - - self.dirty.store(false, Ordering::Relaxed); - } - Ok(()) - } - - fn statfs(&self) -> Result { - let total_clusters = self.boot_sector.total_clusters(); - let free_clusters = self.count_free_clusters(); - - Ok(StatFs { - block_size: self.boot_sector.cluster_size(), - total_blocks: total_clusters, - free_blocks: free_clusters, - available_blocks: free_clusters, - total_inodes: 0, // FAT32 无 inode 限制 - free_inodes: 0, - fsid: 0, - max_filename_len: 255, - }) - } - - fn umount(&self) -> Result<(), FsError> { - // 卸载前同步 - self.sync()?; - - // 释放缓存 - // ... - - Ok(()) - } -} -``` - -## 错误处理 - -### FsError 枚举 - -VFS 定义了与 POSIX 兼容的错误类型: - -```rust -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FsError { - // 文件/目录相关 - NotFound, // -ENOENT(2): 文件不存在 - AlreadyExists, // -EEXIST(17): 文件已存在(O_CREAT | O_EXCL) - NotDirectory, // -ENOTDIR(20): 不是目录 - IsDirectory, // -EISDIR(21): 是目录(不能对目录执行文件操作) - DirectoryNotEmpty, // -ENOTEMPTY(39): 目录非空(rmdir) - - // 权限相关 - PermissionDenied, // -EACCES(13): 权限被拒绝 - - // 文件描述符相关 - BadFileDescriptor, // -EBADF(9): 无效的文件描述符 - TooManyOpenFiles, // -EMFILE(24): 进程打开的文件过多 - - // 参数相关 - InvalidArgument, // -EINVAL(22): 无效参数 - NameTooLong, // -ENAMETOOLONG(36): 文件名过长 - - // 文件系统相关 - ReadOnlyFs, // -EROFS(30): 只读文件系统 - NoSpace, // -ENOSPC(28): 设备空间不足 - IoError, // -EIO(5): I/O 错误 - NoDevice, // -ENODEV(19): 设备不存在 - - // 管道相关 - BrokenPipe, // -EPIPE(32): 管道破裂(读端已关闭) - WouldBlock, // -EAGAIN(11): 非阻塞操作将阻塞 - - // 其他 - NotSupported, // -ENOTSUP(95): 操作不支持 - TooManyLinks, // -EMLINK(31): 硬链接过多 -} -``` - -### 错误码转换 - -FsError 可以转换为系统调用错误码(负数): - -```rust -impl FsError { - pub fn to_errno(&self) -> isize { - match self { - FsError::NotFound => -2, - FsError::IoError => -5, - FsError::BadFileDescriptor => -9, - FsError::WouldBlock => -11, - FsError::PermissionDenied => -13, - FsError::AlreadyExists => -17, - FsError::NoDevice => -19, - FsError::NotDirectory => -20, - FsError::IsDirectory => -21, - FsError::InvalidArgument => -22, - FsError::TooManyOpenFiles => -24, - FsError::NoSpace => -28, - FsError::ReadOnlyFs => -30, - FsError::TooManyLinks => -31, - FsError::BrokenPipe => -32, - FsError::NameTooLong => -36, - FsError::DirectoryNotEmpty => -39, - FsError::NotSupported => -95, - } - } -} -``` - -### 错误使用场景 - -#### NotFound - -**使用场景**: 文件或目录不存在 - -```rust -// 打开不存在的文件(没有 O_CREAT) -let dentry = vfs_lookup("/nonexistent")?; // Err(NotFound) - -// 查找目录中不存在的子项 -parent.inode.lookup("missing")?; // Err(NotFound) -``` - -#### AlreadyExists - -**使用场景**: 文件已存在(O_CREAT | O_EXCL) - -```rust -// 创建已存在的文件 -if flags.contains(OpenFlags::O_CREAT | OpenFlags::O_EXCL) { - if file_exists { - return Err(FsError::AlreadyExists); - } -} -``` - -#### PermissionDenied - -**使用场景**: 没有足够的权限 - -```rust -// 尝试写入只读文件 -if !file.writable() { - return Err(FsError::PermissionDenied); -} +- `file_system.rs`: 挂载实例接口和 statfs 结构. +- `error.rs`: VFS 统一错误和 errno 映射. +- `mount.rs`: 消费 `FileSystem` 并创建 `MountPoint`. +- 具体 FS: 负责把内部库或设备错误转换成 `FsError`. -// 尝试访问没有权限的文件 -if !metadata.mode.can_read() { - return Err(FsError::PermissionDenied); -} -``` - -#### IsDirectory / NotDirectory - -**使用场景**: 文件类型不匹配 - -```rust -// 对目录执行文件操作 -let metadata = dentry.inode.metadata()?; -if metadata.inode_type == InodeType::Directory { - return Err(FsError::IsDirectory); -} - -// 对文件执行目录操作 -if metadata.inode_type != InodeType::Directory { - return Err(FsError::NotDirectory); -} -``` - -#### NoSpace - -**使用场景**: 磁盘空间不足 - -```rust -// 写入数据时磁盘满 -if self.free_blocks() == 0 { - return Err(FsError::NoSpace); -} -``` - -#### WouldBlock - -**使用场景**: 非阻塞操作将阻塞 - -```rust -// 非阻塞管道写入时缓冲区满 -if flags.contains(OpenFlags::O_NONBLOCK) && buffer_full() { - return Err(FsError::WouldBlock); -} -``` - -### 错误处理最佳实践 - -#### 1. 总是检查错误 - -```rust -// 错误❌ -let dentry = vfs_lookup(path).unwrap(); - -// 正确✅ -let dentry = vfs_lookup(path) - .map_err(|e| format!("Failed to lookup {}: {:?}", path, e))?; -``` - -#### 2. 提供上下文信息 +## 关键流程 -```rust -pub fn open_file(path: &str) -> Result, String> { - let dentry = vfs_lookup(path) - .map_err(|e| format!("open_file: lookup failed for '{}': {:?}", path, e))?; - - let metadata = dentry.inode.metadata() - .map_err(|e| format!("open_file: metadata failed: {:?}", e))?; - - if metadata.inode_type == InodeType::Directory { - return Err(format!("open_file: '{}' is a directory", path)); - } - - Ok(Arc::new(RegFile::new(dentry, OpenFlags::O_RDONLY))) -} -``` - -#### 3. 区分预期错误和异常错误 - -```rust -pub fn try_create_file(path: &str) -> Result, FsError> { - match vfs_lookup(path) { - Ok(dentry) => { - // 文件已存在,正常情况 - Ok(Arc::new(RegFile::new(dentry, OpenFlags::O_RDWR))) - } - Err(FsError::NotFound) => { - // 文件不存在,创建新文件(预期行为) - let (dir, name) = split_path(path)?; - let parent = vfs_lookup(&dir)?; - let inode = parent.inode.create(&name, - FileMode::S_IFREG | FileMode::S_IRUSR | FileMode::S_IWUSR)?; - let dentry = Dentry::new(name, inode); - Ok(Arc::new(RegFile::new(dentry, OpenFlags::O_RDWR))) - } - Err(e) => { - // 其他错误,异常情况 - Err(e) - } - } -} -``` - -## 使用示例 +### mount integration -### 挂载自定义文件系统 - -```rust -// 创建文件系统实例 -let my_fs = Arc::new(MyCustomFs::new()); - -// 挂载到 /mnt -vfs::MOUNT_TABLE.mount( - my_fs, - "/mnt", - MountFlags::empty(), - Some(String::from("/dev/custom")) -)?; - -// 访问挂载的文件系统 -let dentry = vfs::vfs_lookup("/mnt/file.txt")?; +```text +concrete FS open + -> Arc dyn FileSystem + -> root_inode + -> MountPoint + -> VFS namespace ``` -### 查询文件系统统计信息 - -```rust -pub fn sys_statfs(path: &str) -> Result { - let dentry = vfs_lookup(path)?; - let full_path = dentry.full_path(); - - // 查找挂载点 - let mount_point = vfs::MOUNT_TABLE.find_mount(&full_path) - .ok_or(FsError::NotSupported)?; - - // 获取统计信息 - mount_point.fs.statfs() -} -``` +root inode 一旦进入 mount table, 后续路径解析只和 `Dentry`/`Inode` 交互. `FileSystem` 主要服务于挂载级操作. -### 同步文件系统 +### error conversion -```rust -pub fn sys_sync() { - // 同步所有挂载的文件系统 - let mounts = vfs::MOUNT_TABLE.list_all(); - for (_, mount_point) in mounts { - let _ = mount_point.fs.sync(); - } -} +```text +device or fs error + -> FsError + -> syscall errno ``` -## 常见问题 - -### Q: FileSystem 和 Inode 有什么区别? - -A: -- **FileSystem**: 文件系统级别的操作(根 inode、同步、统计) -- **Inode**: 单个文件/目录的操作(读写、查找、创建) - -### Q: 为什么 tmpfs 的 sync() 是空操作? - -A: -tmpfs 是内存文件系统,数据只存在内存中,没有持久化存储,因此无需同步。 - -### Q: 如何实现自己的文件系统? - -A: -1. 实现 `Inode` trait 定义单个文件/目录的行为 -2. 实现 `FileSystem` trait 提供文件系统级别的接口 -3. 在 `sys_mount` 中添加对新文件系统类型的支持 - -### Q: 错误处理中如何选择合适的错误类型? - -A: -参考 POSIX 标准的 errno 定义,选择最接近的错误类型。如果没有合适的,使用 `IoError` 或 `NotSupported`。 - -### Q: umount 失败会怎样? - -A: -卸载失败通常是因为: -- 文件系统正在使用(有打开的文件) -- sync() 失败(磁盘错误) +ext4 和 VFAT 都有内部库错误. 文档只描述转换边界, 具体映射由源码保存, 避免文档和实现漂移. -卸载失败后挂载点仍然保留,可以稍后重试。 +### statfs and sync -## 相关资源 +`statfs` 汇总文件系统容量和能力信息. `sync` 是挂载级同步入口, 对内存或动态文件系统可能是空操作, 对块设备文件系统会下推到设备 flush 或库 unmount/sync 路径. -### 源代码位置 +## 并发和生命周期约束 -- **FileSystem trait**: `os/src/vfs/file_system.rs` -- **FsError**: `os/src/vfs/error.rs` -- **tmpfs 实现**: `os/src/fs/tmpfs/` -- **fat32 实现**: `os/src/fs/fat32/` +- `FileSystem` 必须 `Send + Sync`, 因为 mount table 全局共享. +- 是否需要内部大锁由具体 FS 决定. VFAT 由于底层 `fatfs` 访问模型, 当前使用挂载状态锁串行化操作. +- `umount` 不能破坏仍被 `Arc` 持有的 open file. 当前 VFS 的 umount 语义较轻量, 调用方要避免卸载忙碌文件系统. +- 错误类型不要携带短生命周期引用, 需要能跨层返回. -### 参考文档 +## 已知限制 -- [VFS 整体架构](architecture.md) -- [Inode 与 Dentry](inode_and_dentry.md) -- [路径解析与挂载](path_and_mount.md) -- [使用指南](usage.md) +- busy mount 检测不完整. +- mount flags 对错误策略的影响尚不完整. +- `FsError` 是内核内部通用错误, 不能表达每个底层库的所有细节. +- statfs 字段对伪文件系统和 FAT/VFAT 这类无 inode 计数的文件系统会使用近似或 0. -### POSIX 错误码参考 +## 源码索引 -| errno | 值 | VFS 对应 | 说明 | -|-------|---|----------|------| -| ENOENT | 2 | NotFound | 文件不存在 | -| EIO | 5 | IoError | I/O 错误 | -| EBADF | 9 | BadFileDescriptor | 无效文件描述符 | -| EAGAIN | 11 | WouldBlock | 资源暂时不可用 | -| EACCES | 13 | PermissionDenied | 权限被拒绝 | -| EEXIST | 17 | AlreadyExists | 文件已存在 | -| ENOTDIR | 20 | NotDirectory | 不是目录 | -| EISDIR | 21 | IsDirectory | 是目录 | -| EINVAL | 22 | InvalidArgument | 无效参数 | -| EMFILE | 24 | TooManyOpenFiles | 打开文件过多 | -| ENOSPC | 28 | NoSpace | 设备空间不足 | -| EROFS | 30 | ReadOnlyFs | 只读文件系统 | +- `os/src/vfs/file_system.rs`: `FileSystem` 和 `StatFs`. +- `os/src/vfs/error.rs`: `FsError` 和 errno 映射. +- `os/src/vfs/mount.rs`: 挂载表如何消费文件系统实例. +- `os/src/fs/ext4/mod.rs`, `os/src/fs/ext4/inode.rs`: ext4 接入. +- `os/src/fs/tmpfs/tmpfs.rs`, `os/src/fs/tmpfs/inode.rs`: tmpfs 接入. +- `os/src/fs/proc/proc.rs`, `os/src/fs/proc/inode.rs`: procfs 接入. +- `os/src/fs/sysfs/sysfs.rs`, `os/src/fs/sysfs/inode.rs`: sysfs 接入. +- `os/src/fs/vfat/fs.rs`, `os/src/fs/vfat/inode.rs`, `os/src/fs/vfat/adapter.rs`: VFAT/FAT 接入. diff --git a/document/vfs/inode_and_dentry.md b/document/vfs/inode_and_dentry.md index 6e4b14c6..3cd7f563 100644 --- a/document/vfs/inode_and_dentry.md +++ b/document/vfs/inode_and_dentry.md @@ -1,507 +1,80 @@ # Inode 与 Dentry -## 概述 +`Inode` 和 `Dentry` 是 VFS 中最容易混淆的两个对象. 简单说, inode 表示"文件系统里的对象", dentry 表示"路径树里某个名字指向这个对象". -本文档详细介绍 VFS 子系统的存储层 (Inode) 和路径层 (Dentry) 的设计与实现。Inode 提供无状态的文件存储访问接口,Dentry 管理文件系统的目录树结构和路径缓存。 +## 当前状态 -## Inode - 存储层接口 +- `Inode` 由具体文件系统实现, 比如 ext4 inode, tmpfs inode, proc inode, sysfs inode, VFAT inode. +- `Dentry` 由 VFS 创建和缓存, 保存 name, parent, children, inode 和 mount 关系. +- `DENTRY_CACHE` 保存 full path 到 `Weak` 的映射. +- `Inode::cacheable` 允许动态文件系统拒绝路径缓存. +- ext4 inode 使用 weak dentry 反向引用, 需要路径时从 dentry 计算 full path. -### 核心概念 +## 目标 -Inode (Index Node) 是文件系统中文件或目录的底层表示,提供无状态的随机访问能力。与会话层的 File trait 不同,Inode 的所有读写方法都携带 offset 参数,不维护任何会话状态。 +- 让路径缓存不污染具体文件系统实现. +- 让一个 inode 可以被多个名字引用, 支持硬链接和 mount root 等关系. +- 让路径解析能跨 mount point 并保留合理的 `..` 行为. +- 让动态文件系统能避免陈旧 dentry. -#### Inode 的职责 +## 非目标 -- **数据访问**: `read_at(offset, buf)` 和 `write_at(offset, buf)` 提供指定偏移量的读写 -- **目录操作**: `lookup(name)` 查找子项,`create/mkdir/unlink/rmdir` 管理目录结构 -- **元数据管理**: `metadata()` 获取文件信息,`truncate/chmod/chown` 修改文件属性 -- **符号链接**: `symlink/readlink` 创建和读取符号链接 -- **设备文件**: `mknod` 创建设备文件节点 -- **同步**: `sync()` 将数据刷新到持久化存储 +- 不在这里列出 `Inode` 的每个方法和元数据字段. +- 不描述具体磁盘 inode 格式. +- 不承诺 dentry cache 是强一致缓存. -### Inode Trait 定义 +## 模块边界 -```rust -pub trait Inode: Send + Sync + Any { - // 元数据访问 - fn metadata(&self) -> Result; - - // 数据访问 (无状态,携带 offset) - fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result; - fn write_at(&self, offset: usize, buf: &[u8]) -> Result; - - // 目录操作 - fn lookup(&self, name: &str) -> Result, FsError>; - fn create(&self, name: &str, mode: FileMode) -> Result, FsError>; - fn mkdir(&self, name: &str, mode: FileMode) -> Result, FsError>; - fn unlink(&self, name: &str) -> Result<(), FsError>; - fn rmdir(&self, name: &str) -> Result<(), FsError>; - fn readdir(&self) -> Result, FsError>; - - // 链接操作 - fn symlink(&self, name: &str, target: &str) -> Result, FsError>; - fn link(&self, name: &str, target: &Arc) -> Result<(), FsError>; - fn readlink(&self) -> Result; - - // 文件管理 - fn truncate(&self, size: usize) -> Result<(), FsError>; - fn sync(&self) -> Result<(), FsError>; - fn chmod(&self, mode: FileMode) -> Result<(), FsError>; - fn chown(&self, uid: u32, gid: u32) -> Result<(), FsError>; - fn set_times(&self, atime: Option, mtime: Option) - -> Result<(), FsError>; - - // 设备文件 - fn mknod(&self, name: &str, mode: FileMode, dev: u64) - -> Result, FsError>; - - // Dentry 关联 (可选) - fn set_dentry(&self, _dentry: Weak) {} - fn get_dentry(&self) -> Option> { None } - - // 向下转型支持 - fn as_any(&self) -> &dyn Any; -} -``` - -### InodeMetadata 结构 - -```rust -pub struct InodeMetadata { - pub inode_no: usize, // Inode 编号 - pub inode_type: InodeType, // 文件类型 - pub mode: FileMode, // 权限位 - pub uid: u32, // 用户 ID - pub gid: u32, // 组 ID - pub size: usize, // 文件大小 (字节) - pub atime: TimeSpec, // 访问时间 - pub mtime: TimeSpec, // 修改时间 - pub ctime: TimeSpec, // 状态改变时间 - pub nlinks: usize, // 硬链接数 - pub blocks: usize, // 占用的块数 (512B 为单位) - pub rdev: u64, // 设备号 (仅设备文件有效) -} -``` - -### 文件类型 InodeType - -```rust -pub enum InodeType { - File, // 普通文件 - Directory, // 目录 - Symlink, // 符号链接 - CharDevice, // 字符设备 - BlockDevice, // 块设备 - Fifo, // 命名管道 - Socket, // 套接字 -} -``` - -### 文件权限 FileMode - -```rust -bitflags! { - pub struct FileMode: u32 { - // 文件类型掩码 - const S_IFMT = 0o170000; - const S_IFREG = 0o100000; // 普通文件 - const S_IFDIR = 0o040000; // 目录 - const S_IFLNK = 0o120000; // 符号链接 - const S_IFCHR = 0o020000; // 字符设备 - const S_IFBLK = 0o060000; // 块设备 - - // 用户权限 - const S_IRUSR = 0o400; // 用户读 - const S_IWUSR = 0o200; // 用户写 - const S_IXUSR = 0o100; // 用户执行 - - // 组权限 - const S_IRGRP = 0o040; - const S_IWGRP = 0o020; - const S_IXGRP = 0o010; - - // 其他用户权限 - const S_IROTH = 0o004; - const S_IWOTH = 0o002; - const S_IXOTH = 0o001; - - // 特殊位 - const S_ISUID = 0o4000; // Set UID - const S_ISGID = 0o2000; // Set GID - const S_ISVTX = 0o1000; // Sticky bit - } -} -``` - -### Inode 实现示例 - -不同文件系统需要实现自己的 Inode 类型。以下是 tmpfs (内存文件系统) 的简化示例: - -```rust -pub struct TmpfsInode { - metadata: SpinLock, - data: SpinLock>, // 文件数据 - children: SpinLock>>, // 目录子项 - dentry: SpinLock>, // 关联的 Dentry -} - -impl Inode for TmpfsInode { - fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result { - let data = self.data.lock(); - if offset >= data.len() { - return Ok(0); - } - let len = core::cmp::min(buf.len(), data.len() - offset); - buf[..len].copy_from_slice(&data[offset..offset + len]); - Ok(len) - } - - fn write_at(&self, offset: usize, buf: &[u8]) -> Result { - let mut data = self.data.lock(); - let end = offset + buf.len(); - if end > data.len() { - data.resize(end, 0); - } - data[offset..end].copy_from_slice(buf); - - // 更新元数据 - let mut metadata = self.metadata.lock(); - metadata.size = data.len(); - metadata.mtime = get_current_time(); - - Ok(buf.len()) - } - - fn lookup(&self, name: &str) -> Result, FsError> { - self.children.lock() - .get(name) - .cloned() - .ok_or(FsError::NotFound) - } - - // ... 其他方法实现 -} -``` - -## Dentry - 路径层结构 - -### 核心概念 - -Dentry (Directory Entry) 是路径组件的缓存,表示目录树中的一个节点。Dentry 缓存文件名到 Inode 的映射,避免重复的路径解析和 Inode 查找。 - -#### Dentry 的职责 +- `inode.rs` 定义对象能力: 元数据, 读写, 目录操作, 链接, 设备节点, 时间戳等. +- `dentry.rs` 定义命名空间缓存: parent/children, mount point, full path, global cache. +- `path.rs` 是两者的主要协调者: miss 时调用 inode lookup, hit 时复用 dentry. +- 具体 FS 只应返回 inode, 不应自己维护 VFS 路径树. -- **路径缓存**: 缓存从根目录到文件的完整路径 -- **父子关系**: 维护目录树的层次结构 -- **Inode 关联**: 持有对应的 `Arc` -- **挂载点标记**: 标识该 Dentry 是否为挂载点 +## 关键流程 -### Dentry 结构 +### lookup miss -```rust -pub struct Dentry { - /// 文件名 (不含路径) - pub name: String, - - /// 关联的 inode - pub inode: Arc, - - /// 父目录 dentry (弱引用避免循环) - parent: SpinLock>, - - /// 子 dentry 映射 (文件名 -> dentry) - children: SpinLock>>, - - /// 如果此 dentry 是挂载点,指向挂载的根 dentry - mount_point: SpinLock>>, -} -``` - -### Dentry 方法 - -```rust -impl Dentry { - /// 创建新的 dentry - pub fn new(name: String, inode: Arc) -> Arc; - - /// 设置父 dentry - pub fn set_parent(&self, parent: &Arc); - - /// 获取父 dentry - pub fn parent(&self) -> Option>; - - /// 查找子 dentry (从缓存) - pub fn lookup_child(&self, name: &str) -> Option>; - - /// 添加子 dentry - pub fn add_child(self: &Arc, child: Arc); - - /// 删除子 dentry - pub fn remove_child(&self, name: &str) -> Option>; - - /// 获取完整路径 - pub fn full_path(&self) -> String; - - /// 挂载点操作 - pub fn set_mount(&self, mounted_root: &Arc); - pub fn clear_mount(&self); - pub fn get_mount(&self) -> Option>; -} -``` - -### 完整路径生成 - -`full_path()` 方法通过向上遍历父节点生成完整路径: - -```rust -pub fn full_path(&self) -> String { - let mut components = Vec::new(); - let mut current = self as *const Dentry; - - loop { - let dentry = unsafe { &*current }; - - if dentry.name == "/" { - break; // 到达根目录 - } - - components.push(dentry.name.clone()); - - match dentry.parent() { - Some(parent) => current = Arc::as_ptr(&parent), - None => break, - } - } - - components.reverse(); - - if components.is_empty() { - String::from("/") - } else { - String::from("/") + &components.join("/") - } -} -``` - -### 全局 Dentry 缓存 - -VFS 维护一个全局 Dentry 缓存,加速重复路径查找: - -```rust -pub struct DentryCache { - /// 路径 -> dentry 的弱引用映射 - cache: SpinLock>>, -} - -impl DentryCache { - /// 查找缓存 - pub fn lookup(&self, path: &str) -> Option> { - let cache = self.cache.lock(); - let weak = cache.get(path)?; - weak.upgrade() // Weak -> Arc,失败说明已被回收 - } - - /// 插入缓存 - pub fn insert(&self, dentry: &Arc) { - let path = dentry.full_path(); - self.cache.lock().insert(path, Arc::downgrade(dentry)); - } - - /// 删除缓存 - pub fn remove(&self, path: &str) { - self.cache.lock().remove(path); - } - - /// 清空缓存 - pub fn clear(&self) { - self.cache.lock().clear(); - } -} - -// 全局单例 -lazy_static! { - pub static ref DENTRY_CACHE: DentryCache = DentryCache::new(); -} -``` - -## 引用计数与生命周期 - -### Dentry 引用关系 - -Dentry 使用 `Arc` 和 `Weak` 智能指针管理生命周期: - -``` -┌──────────────────────────────────────┐ -│ Arc │ 根目录 (强引用) -│ ├─ name: "/" │ -│ ├─ parent: Weak::new() │ 根目录无父目录 -│ ├─ children: {"etc" -> Arc<...>} │ 强引用子目录 -│ └─ inode: Arc │ 强引用 Inode -└──────────┬───────────────────────────┘ - │ - │ Arc (强引用) - ▼ -┌──────────────────────────────────────┐ -│ Arc │ -│ ├─ name: "etc" │ -│ ├─ parent: Weak │ 弱引用父目录 (避免循环) -│ ├─ children: {"passwd" -> Arc} │ -│ └─ inode: Arc │ -└──────────┬───────────────────────────┘ - │ - │ Arc - ▼ -┌──────────────────────────────────────┐ -│ Arc │ -│ ├─ parent: Weak │ -│ └─ inode: Arc │ -└──────────────────────────────────────┘ +```text +parent Dentry + -> parent Inode lookup name + -> child Inode + -> Dentry new + -> parent children cache + -> optional global cache ``` -### 为什么使用 Weak 引用? - -1. **避免循环引用**: 父节点持有子节点的 Arc,子节点持有父节点的 Weak,打破循环 -2. **自动回收**: 当没有外部引用时,Dentry 自动被释放,无需手动清理 -3. **缓存失效**: 全局缓存使用 Weak,不延长 Dentry 生命周期 - -### Inode 共享 (硬链接) - -多个 Dentry 可以共享同一个 Inode,实现硬链接: - -``` -Dentry("/home/user/file.txt") ────┐ - ├──> Arc -Dentry("/tmp/link_to_file") ────┘ - -metadata.nlinks = 2 # 硬链接计数 -``` - -## 缓存一致性 - -### 缓存更新时机 - -| 操作 | Dentry 缓存 | Dentry 树 | -|------|-------------|-----------| -| lookup 成功 | 插入 `DENTRY_CACHE.insert()` | 插入父节点 `parent.add_child()` | -| create/mkdir | 插入新 Dentry | 插入父节点 | -| unlink/rmdir | 删除 `DENTRY_CACHE.remove()` | 删除父节点 `parent.remove_child()` | -| rename | 更新路径缓存 | 从旧父节点移除,加入新父节点 | - -### 缓存失效策略 - -#### 自动失效 (Weak 引用) - -全局缓存使用 `Weak`,当 Dentry 不再被使用时自动失效: - -```rust -let dentry = DENTRY_CACHE.lookup("/tmp/file"); // 返回 None (已被回收) -``` - -#### 手动失效 - -文件删除时需要手动清理缓存: - -```rust -// sys_unlink 实现 -pub fn sys_unlink(path: &str) -> Result<(), FsError> { - let (dir, name) = split_path(path)?; - let parent = vfs_lookup(&dir)?; - - // 删除 Inode - parent.inode.unlink(&name)?; - - // 删除 Dentry 缓存 - parent.remove_child(&name); - DENTRY_CACHE.remove(path); - - Ok(()) -} -``` - -## DirEntry - 轻量级目录项 - -`readdir` 系统调用返回轻量级的 `DirEntry`,不持有 Arc 引用: - -```rust -pub struct DirEntry { - pub name: String, // 文件名 - pub inode_no: usize, // Inode 编号 - pub inode_type: InodeType, // 文件类型 -} - -// 使用示例 -let entries = inode.readdir()?; -for entry in entries { - println!("{:?} {} (inode {})", - entry.inode_type, entry.name, entry.inode_no); -} -``` - -## 最佳实践 - -### 实现 Inode 时的注意事项 - -1. **线程安全**: Inode 必须实现 `Send + Sync`,所有可变状态需要用锁保护 -2. **错误处理**: 返回准确的 FsError 类型 (NotFound/IsDirectory/PermissionDenied 等) -3. **元数据更新**: write_at/truncate 等操作后更新 mtime/ctime -4. **原子操作**: rename 等操作应该是原子的,使用文件系统级锁保证 - -### 使用 Dentry 时的注意事项 - -1. **优先查缓存**: 总是先查 `DENTRY_CACHE.lookup()`,未命中再查 Inode -2. **及时清理**: 文件删除后立即删除缓存,避免访问到已删除的文件 -3. **避免长时间持有**: Dentry 的 Arc 不应该在系统调用之外长期持有 -4. **挂载点检查**: 路径解析时检查 `check_mount_point()`,自动跟随挂载 - -### 性能优化建议 - -1. **批量 readdir**: 一次 readdir 返回所有子项,避免多次 lookup -2. **预加载子项**: 访问目录时可以预先将子项加入 Dentry 树 -3. **限制缓存大小**: 如果内存紧张,可以实现 LRU 淘汰策略 -4. **异步 I/O**: 对于网络文件系统,Inode 操作可以异步实现 - -## 常见问题 - -### Q: Dentry 和 Inode 有什么区别? - -A: -- **Dentry** 是路径层的缓存,可能有多个 Dentry 指向同一个 Inode (硬链接) -- **Inode** 是存储层的实体,代表物理文件,与路径无关 -- 删除 Dentry 不影响 Inode,只有当 nlinks 为 0 时 Inode 才被删除 - -### Q: 为什么 Inode 方法是 `read_at` 而不是 `read`? - -A: -Inode 是无状态的,不维护 offset。多个进程可以共享同一个 Inode,各自有独立的 offset (在 File 层维护)。 - -### Q: Dentry 缓存会不会无限增长? +缓存策略的关键点是 `Weak`. 全局缓存不会延长 dentry 生命周期, 因此长期不用的路径可以自然释放. -A: -不会。全局缓存使用 `Weak`,当 Dentry 不再被外部引用时,`Weak::upgrade()` 返回 None,缓存自动失效。 +### full path -### Q: 如何实现符号链接? +`Dentry::full_path` 沿 parent 链向上拼接路径. 如果当前 dentry 是某个挂载文件系统的根, 它会通过 mounted-on 关系回到外层挂载点, 使 `/mnt/file` 这类路径保持用户可见形式. -A: -1. 创建 InodeType::Symlink 类型的 Inode -2. 实现 `readlink()` 返回目标路径 -3. 路径解析时检测到符号链接,递归解析目标路径 +### mutation -### Q: 硬链接和符号链接有什么区别? +创建, 删除, rename 等修改由父 inode 执行. VFS 的 dentry 子缓存需要随操作更新或失效. 当前实现依赖调用路径主动移除局部缓存, 不是完整 Linux dcache invalidation 模型. -A: -- **硬链接**: 多个 Dentry 共享同一个 Inode,`link()` 操作,删除一个不影响其他 -- **符号链接**: 创建新的 Inode,存储目标路径字符串,`symlink()` 操作 +## 并发和生命周期约束 -## 相关资源 +- `Dentry` 持有 `Arc`, inode 生命周期至少覆盖该路径节点. +- parent 使用 `Weak`, children 使用 `Arc`, 避免父子强引用环. +- global cache 使用 `Weak`, 只作为加速索引. +- inode 到 dentry 的反向关系必须是可选或 weak, 否则容易形成泄漏. +- 动态 inode, 尤其 `/proc/[pid]`, 不应无条件缓存. -### 源代码位置 +## 已知限制 -- **Inode trait**: `os/src/vfs/inode.rs` -- **Dentry 结构**: `os/src/vfs/dentry.rs` -- **示例实现**: `os/src/fs/tmpfs/` (tmpfs Inode 实现) +- dentry cache 没有版本号或统一失效事件. +- hard link 的多个 dentry 共享 inode 语义依赖具体 FS 正确实现. +- symlink 的最终解析在 `path.rs`, inode 只负责返回 link target. +- 跨文件系统 rename 等复杂语义仍由上层约束. -### 参考文档 +## 源码索引 -- [VFS 整体架构](architecture.md) -- [File 与 FDTable](file_and_fdtable.md) -- [路径解析与挂载](path_and_mount.md) +- `os/src/vfs/inode.rs`: `Inode`, `InodeMetadata`, `DirEntry`, `FileMode`. +- `os/src/vfs/dentry.rs`: `Dentry`, `DentryCache`, mount relation. +- `os/src/vfs/path.rs`: lookup miss/hit, symlink 跟随, mount crossing. +- `os/src/fs/ext4/inode.rs`: 持久化 inode 实现和 dentry weak 反向引用. +- `os/src/fs/tmpfs/inode.rs`: 内存 inode 和目录树. +- `os/src/fs/proc/inode.rs`: 动态 inode 和 cacheable 策略. +- `os/src/fs/sysfs/inode.rs`: 属性文件, 目录和 symlink inode. +- `os/src/fs/vfat/inode.rs`: FAT/VFAT inode 适配. diff --git a/document/vfs/path_and_mount.md b/document/vfs/path_and_mount.md index 8d19b97d..aaad9f66 100644 --- a/document/vfs/path_and_mount.md +++ b/document/vfs/path_and_mount.md @@ -1,740 +1,88 @@ -# 路径解析与挂载管理 +# 路径解析与挂载 -## 概述 +路径解析把用户传入的字符串转换为 `Dentry`. 挂载管理把多个文件系统拼接到同一个路径树中. 这两个机制共同决定命名空间中"看到的文件"来自哪里. -本文档详细介绍 VFS 子系统的路径解析机制和挂载管理功能。路径解析将字符串路径转换为 Dentry 对象,支持绝对路径、相对路径、符号链接等;挂载管理实现多文件系统共存,支持挂载点栈和动态挂载/卸载。 +## 当前状态 -## 路径解析 +- 支持绝对路径和相对路径. +- 支持 `.`, `..`, symlink 跟随和 no-follow 变体. +- 路径解析过程中会检查 dentry 本地 mount cache 和全局 `MountTable`. +- `MountTable` 对同一路径保存挂载点栈, 栈顶可见. +- 根文件系统由 FS 初始化代码探测后挂载到 `/`. -### 核心概念 +## 目标 -路径解析是将字符串路径(如 `/etc/passwd`)转换为 Dentry 对象的过程。VFS 支持: -- **绝对路径**: 以 `/` 开头,从根目录解析 -- **相对路径**: 不以 `/` 开头,从当前工作目录解析 -- **特殊组件**: `.` (当前目录) 和 `..` (父目录) -- **符号链接**: 自动跟随符号链接(可选) +- 让路径查找对调用者隐藏文件系统边界. +- 让 mount/umount 只更新挂载表和 dentry mount cache. +- 让 lookup 可以在动态文件系统中选择不缓存. +- 支持 probe rootfs 时临时挂载和回滚. -### 路径组件 +## 非目标 -```rust -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PathComponent { - Root, // "/" - Current, // "." - Parent, // ".." - Normal(String), // 普通文件名 -} -``` - -### parse_path - 路径解析 - -将路径字符串分解为组件列表: - -```rust -pub fn parse_path(path: &str) -> Vec { - let mut components = Vec::new(); - - // 绝对路径以 Root 开始 - if path.starts_with('/') { - components.push(PathComponent::Root); - } - - // 分割并解析每个部分 - for part in path.split('/').filter(|s| !s.is_empty()) { - let component = match part { - "." => PathComponent::Current, - ".." => PathComponent::Parent, - name => PathComponent::Normal(String::from(name)), - }; - components.push(component); - } - - components -} -``` - -**示例**: -```rust -parse_path("/etc/passwd") // [Root, Normal("etc"), Normal("passwd")] -parse_path("../foo/./bar") // [Parent, Normal("foo"), Current, Normal("bar")] -parse_path("/") // [Root] -``` - -### normalize_path - 路径规范化 - -处理 `.` 和 `..`,生成规范化路径: - -```rust -pub fn normalize_path(path: &str) -> String { - let components = parse_path(path); - let mut stack: Vec = Vec::new(); - let mut is_absolute = false; - - for component in components { - match component { - PathComponent::Root => { - is_absolute = true; - } - PathComponent::Current => { - // "." 不做任何操作 - } - PathComponent::Parent => { - if is_absolute { - // 绝对路径:不能越过根目录 - if !stack.is_empty() { - stack.pop(); - } - } else { - // 相对路径:处理 ".." - if let Some(last) = stack.last() { - if last == ".." { - stack.push(String::from("..")); - } else { - stack.pop(); - } - } else { - stack.push(String::from("..")); - } - } - } - PathComponent::Normal(name) => { - stack.push(name); - } - } - } - - // 构造结果 - if stack.is_empty() { - if is_absolute { - String::from("/") - } else { - String::from(".") - } - } else if is_absolute { - String::from("/") + &stack.join("/") - } else { - stack.join("/") - } -} -``` - -**示例**: -```rust -normalize_path("/a/b/../c/./d") // "/a/c/d" -normalize_path("../../foo") // "../../foo" -normalize_path("/a/b/../../") // "/" -normalize_path("./foo/./bar") // "foo/bar" -``` - -### split_path - 路径分割 - -将路径分割为目录部分和文件名: - -```rust -pub fn split_path(path: &str) -> Result<(String, String), FsError> { - // 路径以斜杠结尾表示目录 - if path.ends_with('/') && path.len() > 1 { - return Err(FsError::InvalidArgument); - } - - let normalized = normalize_path(path); - - if let Some(pos) = normalized.rfind('/') { - let dir = if pos == 0 { - String::from("/") - } else { - String::from(&normalized[..pos]) - }; - let filename = String::from(&normalized[pos + 1..]); - - if filename.is_empty() { - return Err(FsError::InvalidArgument); - } - - Ok((dir, filename)) - } else { - // 相对路径 - Ok((String::from("."), String::from(normalized))) - } -} -``` - -**示例**: -```rust -split_path("/etc/passwd") // Ok(("/etc", "passwd")) -split_path("/passwd") // Ok(("/", "passwd")) -split_path("foo/bar") // Ok(("foo", "bar")) -split_path("file.txt") // Ok((".", "file.txt")) -``` - -### vfs_lookup - 路径查找 - -将路径转换为 Dentry,这是路径解析的核心函数: - -```rust -pub fn vfs_lookup(path: &str) -> Result, FsError> { - let components = parse_path(path); - - // 确定起始 dentry - let mut current_dentry = if components.first() == Some(&PathComponent::Root) { - get_root_dentry()? // 绝对路径:从根目录开始 - } else { - get_cur_dir()? // 相对路径:从当前工作目录开始 - }; - - // 逐个解析路径组件 - for component in components { - current_dentry = resolve_component(current_dentry, component)?; - } - - Ok(current_dentry) -} -``` - -### resolve_component - 组件解析 - -解析单个路径组件,包括缓存查找、Inode lookup、挂载点检查: - -```rust -fn resolve_component(base: Arc, component: PathComponent) - -> Result, FsError> { - match component { - PathComponent::Root => { - get_root_dentry() - } - PathComponent::Current => { - Ok(base) - } - PathComponent::Parent => { - match base.parent() { - Some(parent) => check_mount_point(parent), - None => Ok(base), // 根目录的父目录是自己 - } - } - PathComponent::Normal(name) => { - // 1. 先检查 dentry 缓存 - if let Some(child) = base.lookup_child(&name) { - return check_mount_point(child); - } - - // 2. 缓存未命中,通过 inode 查找 - let child_inode = base.inode.lookup(&name)?; - - // 3. 创建新的 dentry 并加入缓存 - let child_dentry = Dentry::new(name.clone(), child_inode); - base.add_child(child_dentry.clone()); - - // 4. 加入全局缓存 - DENTRY_CACHE.insert(&child_dentry); - - // 5. 检查是否有挂载点 - check_mount_point(child_dentry) - } - } -} -``` - -### check_mount_point - 挂载点检查 - -检查 Dentry 是否是挂载点,如果是则返回挂载的根 Dentry: - -```rust -fn check_mount_point(dentry: Arc) -> Result, FsError> { - // 快速路径:检查 dentry 本地缓存 - if let Some(mounted_root) = dentry.get_mount() { - return Ok(mounted_root); - } - - // 慢速路径:查找挂载表 - let full_path = dentry.full_path(); - if let Some(mount_point) = MOUNT_TABLE.find_mount(&full_path) { - if mount_point.mount_path == full_path { - // 更新 dentry 的挂载缓存 - dentry.set_mount(&mount_point.root); - return Ok(mount_point.root.clone()); - } - } - - Ok(dentry) -} -``` - -### vfs_lookup_from - 从指定 Dentry 查找 - -从给定的 base Dentry 开始查找路径: - -```rust -pub fn vfs_lookup_from(base: Arc, path: &str) - -> Result, FsError> { - let components = parse_path(path); - let mut current_dentry = base; - - for component in components { - if component == PathComponent::Root { - continue; // 忽略根组件 - } - current_dentry = resolve_component(current_dentry, component)?; - } - - Ok(current_dentry) -} -``` - -### vfs_lookup_no_follow - 不跟随符号链接 - -查找路径但不跟随最后一个组件的符号链接(用于 lstat、unlink 等): - -```rust -pub fn vfs_lookup_no_follow(path: &str) -> Result, FsError> { - let components = parse_path(path); - - if components.is_empty() { - return Err(FsError::InvalidArgument); - } - - let mut current_dentry = if components.first() == Some(&PathComponent::Root) { - get_root_dentry()? - } else { - get_cur_dir()? - }; - - if components.len() == 1 && components[0] == PathComponent::Root { - return Ok(current_dentry); - } - - // 解析除最后一个组件外的所有组件 - let len = components.len(); - for i in 0..len - 1 { - current_dentry = resolve_component(current_dentry, components[i].clone())?; - } - - // 解析最后一个组件,但不跟随符号链接 - let last_component = &components[len - 1]; - match last_component { - PathComponent::Root => get_root_dentry(), - PathComponent::Current => Ok(current_dentry), - PathComponent::Parent => { - match current_dentry.parent() { - Some(parent) => Ok(parent), - None => Ok(current_dentry), - } - } - PathComponent::Normal(name) => { - // 查找但不跟随符号链接 - if let Some(child) = current_dentry.lookup_child(name) { - return Ok(child); - } - - let child_inode = current_dentry.inode.lookup(name)?; - let child_dentry = Dentry::new(name.clone(), child_inode); - current_dentry.add_child(child_dentry.clone()); - DENTRY_CACHE.insert(&child_dentry); - - Ok(child_dentry) - } - } -} -``` - -## 挂载管理 - -### 核心概念 - -挂载管理允许多个文件系统共存于同一目录树中。挂载点是文件系统的接入点,访问挂载点下的路径时会自动切换到挂载的文件系统。 - -#### 关键特性 - -- **挂载点栈**: 同一路径可以多次挂载,最后挂载的文件系统覆盖之前的 -- **最长前缀匹配**: 查找挂载点时使用最长前缀匹配算法 -- **动态挂载/卸载**: 支持运行时挂载和卸载文件系统 - -### MountFlags - 挂载标志 - -```rust -bitflags! { - pub struct MountFlags: u32 { - const READ_ONLY = 1 << 0; // 只读挂载 - const NO_EXEC = 1 << 1; // 禁止执行 - const NO_SUID = 1 << 2; // 忽略 SUID/SGID 位 - const SYNC = 1 << 3; // 同步写入 - const NO_DEV = 1 << 4; // 禁止设备文件 - } -} -``` - -### MountPoint - 挂载点结构 - -```rust -pub struct MountPoint { - /// 挂载的文件系统 - pub fs: Arc, - - /// 挂载点的根 dentry - pub root: Arc, - - /// 挂载标志 - pub flags: MountFlags, - - /// 设备路径 (如果有) - pub device: Option, - - /// 挂载路径 - pub mount_path: String, -} - -impl MountPoint { - pub fn new(fs: Arc, mount_path: String, - flags: MountFlags, device: Option) -> Arc { - let root_inode = fs.root_inode(); - let root = Dentry::new(String::from("/"), root_inode); - - Arc::new(Self { - fs, - root, - flags, - device, - mount_path, - }) - } -} -``` - -### MountTable - 挂载表 - -```rust -pub struct MountTable { - /// 挂载路径 -> 挂载点栈 (最后一个是当前可见的) - mounts: SpinLock>>>, -} - -lazy_static! { - pub static ref MOUNT_TABLE: MountTable = MountTable::new(); -} -``` - -### mount - 挂载文件系统 - -```rust -impl MountTable { - pub fn mount(&self, fs: Arc, path: &str, - flags: MountFlags, device: Option) - -> Result<(), FsError> { - let normalized_path = normalize_path(path); - - // 创建挂载点 - let mount_point = MountPoint::new(fs, normalized_path.clone(), - flags, device); - - // 添加到挂载栈 - let mut mounts = self.mounts.lock(); - mounts.entry(normalized_path.clone()) - .or_insert_with(Vec::new) - .push(mount_point.clone()); - - // 更新 dentry 缓存中的挂载信息 - if let Some(dentry) = DENTRY_CACHE.lookup(&normalized_path) { - dentry.set_mount(&mount_point.root); - } - - Ok(()) - } -} -``` - -### umount - 卸载文件系统 - -```rust -impl MountTable { - pub fn umount(&self, path: &str) -> Result<(), FsError> { - let normalized_path = normalize_path(path); - - // 不允许卸载根文件系统 - if normalized_path == "/" { - return Err(FsError::NotSupported); - } - - let mut mounts = self.mounts.lock(); - let stack = mounts.get_mut(&normalized_path) - .ok_or(FsError::NotFound)?; - - // 弹出栈顶的挂载点 - let mount_point = stack.pop().ok_or(FsError::NotFound)?; - - // 如果栈为空,移除整个条目 - if stack.is_empty() { - mounts.remove(&normalized_path); - } - - drop(mounts); // 释放锁 - - // 同步文件系统 - mount_point.fs.sync()?; - - // 执行卸载清理 - mount_point.fs.umount()?; - - // 更新 dentry 缓存 - if let Some(dentry) = DENTRY_CACHE.lookup(&normalized_path) { - let mounts = self.mounts.lock(); - if let Some(stack) = mounts.get(&normalized_path) { - if let Some(underlying_mount) = stack.last() { - dentry.set_mount(&underlying_mount.root); - } else { - dentry.clear_mount(); - } - } else { - dentry.clear_mount(); - } - } - - Ok(()) - } -} -``` - -### find_mount - 查找挂载点 - -使用最长前缀匹配查找挂载点: - -```rust -impl MountTable { - pub fn find_mount(&self, path: &str) -> Option> { - let normalized_path = normalize_path(path); - let mounts = self.mounts.lock(); - - // 查找最长匹配的挂载点 - let mut best_match = None; - let mut best_len = 0; - - for (mount_path, stack) in mounts.iter() { - if normalized_path.starts_with(mount_path) - && mount_path.len() > best_len { - // 返回栈顶的挂载点 (当前可见的) - if let Some(mp) = stack.last() { - best_match = Some(mp.clone()); - best_len = mount_path.len(); - } - } - } - - best_match - } -} -``` - -**示例**: -```rust -// 挂载情况: -// "/" -> tmpfs -// "/mnt" -> fat32 -// "/mnt/data" -> ext4 - -find_mount("/etc/passwd") // Some(tmpfs at "/") -find_mount("/mnt/config") // Some(fat32 at "/mnt") -find_mount("/mnt/data/file") // Some(ext4 at "/mnt/data") -``` - -### root_mount - 获取根挂载点 - -```rust -impl MountTable { - pub fn root_mount(&self) -> Option> { - self.mounts.lock() - .get("/") - .and_then(|stack| stack.last()) - .cloned() - } -} - -pub fn get_root_dentry() -> Result, FsError> { - MOUNT_TABLE.root_mount() - .map(|mp| mp.root.clone()) - .ok_or(FsError::NotSupported) -} -``` - -### list_mounts - 列出所有挂载点 - -```rust -impl MountTable { - pub fn list_mounts(&self) -> Vec<(String, String)> { - let mounts = self.mounts.lock(); - mounts.iter() - .flat_map(|(path, stack)| { - stack.iter() - .map(|mp| (path.clone(), String::from(mp.fs.fs_type()))) - }) - .collect() - } -} -``` - -## 使用示例 - -### 路径查找 - -```rust -// 绝对路径查找 -let dentry = vfs_lookup("/etc/passwd")?; - -// 相对路径查找 -let dentry = vfs_lookup("../foo/bar")?; - -// 查找但不跟随符号链接 -let dentry = vfs_lookup_no_follow("/path/to/symlink")?; - -// 从指定 dentry 查找 -let base = vfs_lookup("/mnt")?; -let dentry = vfs_lookup_from(base, "data/file.txt")?; -``` - -### 挂载文件系统 - -```rust -// 创建文件系统实例 -let fs: Arc = create_tmpfs()?; - -// 挂载到 /tmp -MOUNT_TABLE.mount( - fs, - "/tmp", - MountFlags::empty(), - None -)?; - -// 访问挂载点下的文件 -let dentry = vfs_lookup("/tmp/test.txt")?; - -// 卸载 -MOUNT_TABLE.umount("/tmp")?; -``` +- 不实现 Linux mount namespace, bind mount, propagation. +- 不在本文维护 path parser 的完整分支. +- 不保证所有 mount flags 已经执行到每个访问路径. -### 挂载点栈 +## 模块边界 -```rust -// 第一次挂载 -let fs1 = create_tmpfs()?; -MOUNT_TABLE.mount(fs1, "/mnt", MountFlags::empty(), None)?; +- `path.rs`: 路径规范化, split, lookup, symlink, mount crossing. +- `mount.rs`: mount point, mount stack, root dentry, probe umount. +- `dentry.rs`: mount point cache 和 mounted-on 反向关系. +- FS 层负责决定挂载哪个文件系统, VFS 只负责把它接入路径树. -// 第二次挂载 (覆盖) -let fs2 = create_fat32()?; -MOUNT_TABLE.mount(fs2, "/mnt", MountFlags::empty(), - Some(String::from("/dev/sda1")))?; +## 关键流程 -// 访问 /mnt 会使用 fs2 -let dentry = vfs_lookup("/mnt")?; +### 普通 lookup -// 卸载第二次挂载 -MOUNT_TABLE.umount("/mnt")?; - -// 现在访问 /mnt 会使用 fs1 +```text +path string + -> start dentry + -> component loop + -> child cache or inode lookup + -> optional symlink follow + -> mount point check + -> final dentry ``` -### 多级挂载 - -```rust -// 挂载根文件系统 -let tmpfs = create_tmpfs()?; -MOUNT_TABLE.mount(tmpfs, "/", MountFlags::empty(), None)?; +`vfs_lookup` 默认跟随最终 symlink. `vfs_lookup_no_follow` 用于 unlink, lstat 等需要操作 link 本身的路径. -// 挂载 /mnt -let fat32 = create_fat32()?; -MOUNT_TABLE.mount(fat32, "/mnt", MountFlags::empty(), - Some(String::from("/dev/sda1")))?; +### mount crossing -// 挂载 /mnt/data -let ext4 = create_ext4()?; -MOUNT_TABLE.mount(ext4, "/mnt/data", MountFlags::empty(), - Some(String::from("/dev/sda2")))?; - -// 路径解析会自动切换到对应的文件系统 -vfs_lookup("/etc/passwd") // 在 tmpfs 中查找 -vfs_lookup("/mnt/config") // 在 fat32 中查找 -vfs_lookup("/mnt/data/file") // 在 ext4 中查找 +```text +/mnt dentry + -> get_mount hit + -> mounted root dentry + -> continue lookup inside mounted fs ``` -## 性能优化 - -### 缓存策略 - -1. **Dentry 全局缓存**: 避免重复路径解析 -2. **Dentry 树缓存**: 父子关系缓存,加速相对路径查找 -3. **挂载点本地缓存**: Dentry 缓存挂载点信息,避免每次查挂载表 - -### 路径解析优化 - -1. **短路径优先**: 尽量使用绝对路径,避免复杂的 `../..` 等相对路径 -2. **批量操作**: 在同一目录下操作多个文件时,先查找目录 Dentry,再使用 `vfs_lookup_from` -3. **缓存预热**: 启动时预加载常用路径到缓存 - -## 最佳实践 - -### 路径处理 - -1. **总是规范化路径**: 使用 `normalize_path` 处理用户输入 -2. **检查路径有效性**: 使用 `split_path` 验证路径格式 -3. **选择合适的查找函数**: - - 普通查找: `vfs_lookup` - - 不跟随符号链接: `vfs_lookup_no_follow` - - 从指定位置查找: `vfs_lookup_from` - -### 挂载管理 - -1. **先挂载根目录**: 系统启动时先挂载根文件系统 -2. **检查挂载点**: 挂载前确保目录存在 -3. **优雅卸载**: 卸载前确保没有进程使用该文件系统 -4. **错误处理**: 挂载/卸载失败时正确清理资源 - -### 安全考虑 - -1. **路径越界检查**: 防止 `../../../` 越过根目录 -2. **权限验证**: 检查用户是否有权限访问路径 -3. **符号链接循环**: 限制符号链接解析深度(当前未实现) - -## 常见问题 - -### Q: 绝对路径和相对路径有什么区别? - -A: -- **绝对路径**: 以 `/` 开头,从根目录解析,如 `/etc/passwd` -- **相对路径**: 不以 `/` 开头,从当前工作目录解析,如 `../foo/bar` - -### Q: 挂载点栈有什么用? - -A: -支持同一路径多次挂载,常用于容器技术。最后挂载的文件系统覆盖之前的,卸载后恢复为下层挂载。 - -### Q: 最长前缀匹配如何工作? +如果 dentry 还没有本地 mount cache, `path.rs` 会查询 `MountTable`, 命中后回填 dentry 以加速后续解析. -A: -访问 `/mnt/data/file` 时,如果 `/`、`/mnt` 和 `/mnt/data` 都是挂载点,则选择 `/mnt/data`(最长匹配)。 +### umount -### Q: 如何实现符号链接? +umount 从指定路径的挂载栈弹出栈顶. 如果下面还有挂载, dentry 指向下层根. 如果没有, 清除挂载标记. 根挂载不允许普通 umount, rootfs probe 使用专门路径回滚临时根. -A: -1. 创建 `InodeType::Symlink` 类型的 Inode -2. 实现 `readlink()` 返回目标路径 -3. 路径解析时检测到符号链接,递归调用 `vfs_lookup` 解析目标路径 +### rootfs probe -### Q: 为什么需要 vfs_lookup_no_follow? +`init_rootfs_from_discovered_block_devices` 遍历 sysfs 设备注册表看到的块设备和分区, 优先分区设备. 每个候选设备会被临时作为 ext4 挂载到 `/`, 然后检查 `/bin/sh` 或 `/bin/ash`. 不符合条件的候选会卸载并清空当前任务 root/cwd 和 dentry cache. -A: -某些操作需要操作符号链接本身而不是目标,如: -- `unlink`: 删除符号链接文件 -- `lstat`: 获取符号链接的元数据 -- `readlink`: 读取符号链接目标 +## 并发和生命周期约束 -## 相关资源 +- `MountTable` 由锁保护, mount/umount 是全局命名空间操作. +- 挂载点根 dentry 持有 root inode, 其 mounted-on 关系用 weak 指向外层 dentry. +- dentry 的 mount cache 是加速路径, 真正来源仍是 mount table. +- path lookup 过程中不要长期持有不必要的锁后调用文件系统实现, 否则容易放大锁竞争. -### 源代码位置 +## 已知限制 -- **路径解析**: `os/src/vfs/path.rs` -- **挂载管理**: `os/src/vfs/mount.rs` -- **Dentry 缓存**: `os/src/vfs/dentry.rs` +- mount flags 如 read-only, noexec, nodev 的执行仍不完整. +- 没有 per-task mount namespace. +- dentry cache 清理较粗, rootfs probe 直接清空全局缓存. +- symlink 解析有递归深度限制, 具体限制以源码为准. -### 参考文档 +## 源码索引 -- [VFS 整体架构](architecture.md) -- [Inode 与 Dentry](inode_and_dentry.md) -- [File 与 FDTable](file_and_fdtable.md) -- [使用指南](usage.md) +- `os/src/vfs/path.rs`: lookup 主流程, symlink, mount check. +- `os/src/vfs/mount.rs`: mount stack, root mount, probe rollback. +- `os/src/vfs/dentry.rs`: mount cache, mounted-on, full path. +- `os/src/fs/mod.rs`: rootfs probe 和初始化挂载顺序. +- `os/src/fs/sysfs/device_registry.rs`: 块设备和分区枚举来源. diff --git a/document/vfs/usage.md b/document/vfs/usage.md index dc1814cd..f4fd2e62 100644 --- a/document/vfs/usage.md +++ b/document/vfs/usage.md @@ -1,831 +1,104 @@ -# VFS 使用指南 +# VFS 协作流程 -## 概述 +本页不是 API 使用手册, 而是说明系统调用, VFS, FS 和设备层如何协作. 具体调用方式请读 rustdoc 和对应 syscall 实现. -本文档提供 VFS 子系统的实用指南,包括常见使用场景、代码示例、最佳实践和故障排查。适合开发者快速上手 VFS API,了解如何在内核中进行文件操作。 +## 当前状态 -## 快速开始 +VFS 已覆盖常见文件系统调用所需的核心路径: -### 初始化 VFS +- path lookup, open, read, write, lseek, close. +- mkdir, unlink, rmdir, rename, link, symlink, readlink. +- getdents/stat/statfs/chmod/chown/utimens 类元数据操作. +- pipe, stdio, 设备文件, 文件锁. +- mount/umount 的基础命名空间能力. -系统启动时需要挂载根文件系统: +## 目标 -```rust -// 在 os/src/main.rs 中 -pub fn init_vfs() -> Result<(), FsError> { - // 1. 创建根文件系统 (tmpfs) - let tmpfs = TmpFs::new(); - let fs: Arc = Arc::new(tmpfs); - - // 2. 挂载到根目录 - vfs::MOUNT_TABLE.mount( - fs, - "/", - MountFlags::empty(), - None - )?; - - // 3. 创建基本目录结构 - let root = vfs::get_root_dentry()?; - root.inode.mkdir("dev", FileMode::S_IFDIR | FileMode::S_IRWXU)?; - root.inode.mkdir("etc", FileMode::S_IFDIR | FileMode::S_IRWXU)?; - root.inode.mkdir("tmp", FileMode::S_IFDIR | FileMode::S_IRWXU)?; - root.inode.mkdir("mnt", FileMode::S_IFDIR | FileMode::S_IRWXU)?; - - Ok(()) -} -``` - -### 初始化进程文件描述符 - -每个进程启动时初始化标准 I/O: - -```rust -pub fn init_stdio(task: &Task) -> Result<(), FsError> { - let fd_table = &task.fd_table; - - // 创建标准 I/O 文件 - let (stdin, stdout, stderr) = vfs::create_stdio_files(); - - // 安装到文件描述符 0, 1, 2 - fd_table.install_at(0, stdin)?; // stdin - fd_table.install_at(1, stdout)?; // stdout - fd_table.install_at(2, stderr)?; // stderr - - Ok(()) -} -``` - -## 常见操作 - -### 文件操作 - -#### 打开文件 - -```rust -use vfs::{vfs_lookup, RegFile, OpenFlags}; - -pub fn sys_open(path: &str, flags: OpenFlags, mode: FileMode) - -> Result { - // 1. 解析路径 - let dentry = if flags.contains(OpenFlags::O_CREAT) { - // 创建文件 - let (dir, name) = vfs::split_path(path)?; - let parent = vfs::vfs_lookup(&dir)?; - - match parent.inode.lookup(&name) { - Ok(inode) => { - if flags.contains(OpenFlags::O_EXCL) { - return Err(FsError::AlreadyExists); - } - Dentry::new(name, inode) - } - Err(FsError::NotFound) => { - let inode = parent.inode.create(&name, mode)?; - let dentry = Dentry::new(name, inode); - parent.add_child(dentry.clone()); - dentry - } - Err(e) => return Err(e), - } - } else { - vfs::vfs_lookup(path)? - }; - - // 2. 创建 File 对象 - let file = Arc::new(RegFile::new(dentry, flags)); - - // 3. 如果是 O_TRUNC,截断文件 - if flags.contains(OpenFlags::O_TRUNC) { - file.inode()?.truncate(0)?; - } - - // 4. 分配文件描述符 - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - let fd_flags = FdFlags::from_open_flags(flags); - let fd = fd_table.alloc_with_flags(file, fd_flags)?; - - Ok(fd) -} -``` - -#### 读取文件 - -```rust -pub fn sys_read(fd: usize, buf: &mut [u8]) -> Result { - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - - let file = fd_table.get(fd)?; - if !file.readable() { - return Err(FsError::PermissionDenied); - } - - file.read(buf) -} -``` - -#### 写入文件 - -```rust -pub fn sys_write(fd: usize, buf: &[u8]) -> Result { - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - - let file = fd_table.get(fd)?; - if !file.writable() { - return Err(FsError::PermissionDenied); - } - - file.write(buf) -} -``` - -#### 关闭文件 - -```rust -pub fn sys_close(fd: usize) -> Result<(), FsError> { - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - fd_table.close(fd) -} -``` - -#### Seek 操作 - -```rust -pub fn sys_lseek(fd: usize, offset: isize, whence: SeekWhence) - -> Result { - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - - let file = fd_table.get(fd)?; - file.lseek(offset, whence) -} -``` - -### 目录操作 - -#### 创建目录 - -```rust -pub fn sys_mkdir(path: &str, mode: FileMode) -> Result<(), FsError> { - let (dir, name) = vfs::split_path(path)?; - let parent = vfs::vfs_lookup(&dir)?; - parent.inode.mkdir(&name, mode | FileMode::S_IFDIR)?; - Ok(()) -} -``` - -#### 删除目录 - -```rust -pub fn sys_rmdir(path: &str) -> Result<(), FsError> { - let (dir, name) = vfs::split_path(path)?; - let parent = vfs::vfs_lookup(&dir)?; - - // 删除 inode - parent.inode.rmdir(&name)?; - - // 清理缓存 - parent.remove_child(&name); - vfs::DENTRY_CACHE.remove(path); - - Ok(()) -} -``` - -#### 读取目录 - -```rust -pub fn sys_getdents64(fd: usize, buf: &mut [u8]) -> Result { - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - - let file = fd_table.get(fd)?; - let dentry = file.dentry()?; - - // 获取目录项列表 - let entries = dentry.inode.readdir()?; - - // 序列化到缓冲区 - let mut offset = 0; - for entry in entries { - let dirent = LinuxDirent64 { - d_ino: entry.inode_no as u64, - d_off: offset as i64, - d_reclen: /* 计算记录长度 */, - d_type: inode_type_to_d_type(entry.inode_type), - d_name: entry.name, - }; - - // 写入缓冲区 - offset += dirent.write_to(&mut buf[offset..])?; - } - - Ok(offset) -} -``` - -#### 切换工作目录 - -```rust -pub fn sys_chdir(path: &str) -> Result<(), FsError> { - let dentry = vfs::vfs_lookup(path)?; - - // 检查是否是目录 - let metadata = dentry.inode.metadata()?; - if metadata.inode_type != InodeType::Directory { - return Err(FsError::NotDirectory); - } - - // 更新当前工作目录 - let current = current_task(); - current.lock().fs.lock().cwd = Some(dentry); - - Ok(()) -} -``` - -### 链接操作 - -#### 创建硬链接 - -```rust -pub fn sys_link(oldpath: &str, newpath: &str) -> Result<(), FsError> { - // 查找源文件 - let old_dentry = vfs::vfs_lookup(oldpath)?; - - // 解析目标路径 - let (dir, name) = vfs::split_path(newpath)?; - let parent = vfs::vfs_lookup(&dir)?; - - // 创建硬链接 - parent.inode.link(&name, &old_dentry.inode)?; - - Ok(()) -} -``` - -#### 删除链接 - -```rust -pub fn sys_unlink(path: &str) -> Result<(), FsError> { - let (dir, name) = vfs::split_path(path)?; - let parent = vfs::vfs_lookup(&dir)?; - - // 删除链接 - parent.inode.unlink(&name)?; - - // 清理缓存 - parent.remove_child(&name); - vfs::DENTRY_CACHE.remove(path); - - Ok(()) -} -``` - -#### 创建符号链接 - -```rust -pub fn sys_symlink(target: &str, linkpath: &str) -> Result<(), FsError> { - let (dir, name) = vfs::split_path(linkpath)?; - let parent = vfs::vfs_lookup(&dir)?; - - parent.inode.symlink(&name, target)?; - Ok(()) -} -``` - -#### 读取符号链接 - -```rust -pub fn sys_readlink(path: &str, buf: &mut [u8]) -> Result { - let dentry = vfs::vfs_lookup_no_follow(path)?; - - let target = dentry.inode.readlink()?; - let len = core::cmp::min(buf.len(), target.len()); - buf[..len].copy_from_slice(&target.as_bytes()[..len]); - - Ok(len) -} -``` - -### 管道操作 - -#### 创建管道 - -```rust -pub fn sys_pipe() -> Result<(usize, usize), FsError> { - let (read_file, write_file) = vfs::create_pipe()?; - - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - - let read_fd = fd_table.alloc(read_file)?; - let write_fd = fd_table.alloc(write_file)?; - - Ok((read_fd, write_fd)) -} -``` - -#### 使用管道通信 - -父子进程通过管道通信: - -```rust -pub fn pipe_example() -> Result<(), FsError> { - // 创建管道 - let (read_fd, write_fd) = sys_pipe()?; - - // fork 子进程 - if sys_fork()? == 0 { - // 子进程:关闭写端,读取数据 - sys_close(write_fd)?; - - let mut buf = [0u8; 128]; - let n = sys_read(read_fd, &mut buf)?; - // 处理数据... - - sys_exit(0); - } else { - // 父进程:关闭读端,写入数据 - sys_close(read_fd)?; - - sys_write(write_fd, b"Hello, child!")?; - sys_close(write_fd)?; - - sys_wait()?; - } - - Ok(()) -} -``` - -### 文件描述符操作 - -#### dup/dup2 - -```rust -// 重定向标准输出到文件 -pub fn redirect_stdout(path: &str) -> Result<(), FsError> { - // 打开目标文件 - let fd = sys_open(path, - OpenFlags::O_WRONLY | OpenFlags::O_CREAT | OpenFlags::O_TRUNC, - FileMode::S_IRUSR | FileMode::S_IWUSR)?; - - // 复制到 stdout (fd 1) - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - fd_table.dup2(fd, 1)?; - fd_table.close(fd)?; - - Ok(()) -} -``` - -#### fcntl 操作 - -```rust -pub fn sys_fcntl(fd: usize, cmd: u32, arg: usize) -> Result { - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - - match cmd { - F_GETFD => { - // 获取 FD 标志 - let flags = fd_table.get_fd_flags(fd)?; - Ok(flags.bits() as isize) - } - F_SETFD => { - // 设置 FD 标志 - let flags = FdFlags::from_bits_truncate(arg as u32); - fd_table.set_fd_flags(fd, flags)?; - Ok(0) - } - F_GETFL => { - // 获取文件状态标志 - let file = fd_table.get(fd)?; - Ok(file.flags().bits() as isize) - } - F_SETFL => { - // 设置文件状态标志 - let file = fd_table.get(fd)?; - let flags = OpenFlags::from_bits_truncate(arg as u32); - file.set_status_flags(flags)?; - Ok(0) - } - _ => Err(FsError::NotSupported) - } -} -``` - -### 挂载操作 - -#### 挂载文件系统 - -```rust -pub fn sys_mount(device: &str, path: &str, fstype: &str, flags: u32) - -> Result<(), FsError> { - // 创建文件系统实例 - let fs: Arc = match fstype { - "tmpfs" => Arc::new(TmpFs::new()), - "fat32" => { - let dev = vfs::vfs_lookup(device)?; - Arc::new(Fat32Fs::new(dev)?) - } - _ => return Err(FsError::NotSupported), - }; - - // 挂载 - let mount_flags = MountFlags::from_bits_truncate(flags); - vfs::MOUNT_TABLE.mount( - fs, - path, - mount_flags, - Some(String::from(device)) - )?; - - Ok(()) -} -``` - -#### 卸载文件系统 - -```rust -pub fn sys_umount(path: &str) -> Result<(), FsError> { - vfs::MOUNT_TABLE.umount(path) -} -``` - -## 最佳实践 - -### 资源管理 - -#### 使用 RAII 模式 - -```rust -struct FileGuard { - fd: usize, - fd_table: Arc, -} - -impl FileGuard { - fn new(path: &str, flags: OpenFlags) -> Result { - let fd = sys_open(path, flags, FileMode::empty())?; - let current = current_task(); - let fd_table = current.lock().fd_table.clone(); - Ok(Self { fd, fd_table }) - } -} - -impl Drop for FileGuard { - fn drop(&mut self) { - let _ = self.fd_table.close(self.fd); - } -} - -// 使用 -{ - let file = FileGuard::new("/tmp/test", OpenFlags::O_RDONLY)?; - // 使用文件... -} // 自动关闭 -``` - -#### 批量操作 - -对同一目录下的多个文件,先查找目录 Dentry: - -```rust -pub fn batch_create_files(dir: &str, names: &[&str]) - -> Result<(), FsError> { - // 一次查找目录 - let parent = vfs::vfs_lookup(dir)?; - - // 批量创建文件 - for name in names { - parent.inode.create(name, - FileMode::S_IFREG | FileMode::S_IRUSR | FileMode::S_IWUSR)?; - } - - Ok(()) -} -``` - -### 错误处理 - -#### 正确处理错误 - -```rust -pub fn robust_file_read(path: &str) -> Result, String> { - // 打开文件 - let dentry = vfs::vfs_lookup(path) - .map_err(|e| format!("Failed to lookup {}: {:?}", path, e))?; - - let file = Arc::new(RegFile::new(dentry, OpenFlags::O_RDONLY)); - - // 获取文件大小 - let metadata = file.metadata() - .map_err(|e| format!("Failed to get metadata: {:?}", e))?; - - // 分配缓冲区 - let mut buf = vec![0u8; metadata.size]; - - // 读取数据 - let mut offset = 0; - while offset < metadata.size { - let n = file.read(&mut buf[offset..]) - .map_err(|e| format!("Failed to read at {}: {:?}", offset, e))?; - - if n == 0 { - break; // EOF - } - offset += n; - } - - buf.truncate(offset); - Ok(buf) -} -``` - -### 性能优化 - -#### 大文件读写 - -使用大缓冲区减少系统调用: - -```rust -pub fn copy_file(src: &str, dst: &str) -> Result<(), FsError> { - const BUF_SIZE: usize = 64 * 1024; // 64KB 缓冲区 - - let src_fd = sys_open(src, OpenFlags::O_RDONLY, FileMode::empty())?; - let dst_fd = sys_open(dst, - OpenFlags::O_WRONLY | OpenFlags::O_CREAT | OpenFlags::O_TRUNC, - FileMode::S_IRUSR | FileMode::S_IWUSR)?; - - let mut buf = vec![0u8; BUF_SIZE]; - - loop { - let n = sys_read(src_fd, &mut buf)?; - if n == 0 { - break; - } - - sys_write(dst_fd, &buf[..n])?; - } - - sys_close(src_fd)?; - sys_close(dst_fd)?; - - Ok(()) -} -``` - -#### 使用 pread/pwrite - -避免 lseek + read/write 的竞争条件: - -```rust -pub fn read_at_offset(file: &Arc, offset: usize, buf: &mut [u8]) - -> Result { - // 一次调用,不改变文件 offset - file.read_at(offset, buf) -} -``` - -## 常见陷阱 - -### 1. 忘记关闭文件描述符 - -**错误**: -```rust -for i in 0..1000 { - let fd = sys_open("/tmp/test", OpenFlags::O_RDONLY, FileMode::empty())?; - // 忘记 close,导致 fd 泄漏 -} -``` - -**正确**: -```rust -for i in 0..1000 { - let fd = sys_open("/tmp/test", OpenFlags::O_RDONLY, FileMode::empty())?; - // 使用文件... - sys_close(fd)?; -} -``` - -### 2. dup 后的竞争条件 - -**错误**: -```rust -let fd1 = sys_open("/tmp/file", OpenFlags::O_RDWR, FileMode::empty())?; -let fd2 = sys_dup(fd1)?; - -// 两个线程同时使用 fd1 和 fd2,共享 offset,导致读写混乱 -``` +- 帮助贡献者判断改动应该放在 syscall, VFS, FS 还是 device 层. +- 避免把具体实现细节复制进正式文档. +- 明确常见路径的状态归属和生命周期. -**正确**: -```rust -// 如果需要独立的 offset,重新打开文件 -let fd1 = sys_open("/tmp/file", OpenFlags::O_RDWR, FileMode::empty())?; -let fd2 = sys_open("/tmp/file", OpenFlags::O_RDWR, FileMode::empty())?; -``` +## 非目标 -### 3. 路径越界 +- 不提供完整代码示例集合. +- 不替代系统调用文档. +- 不承诺每个 POSIX corner case 都已实现. -**错误**: -```rust -// 可能越过根目录 -let path = "/../../../etc/passwd"; -``` +## 常见改动定位 -**正确**: -```rust -// 使用 normalize_path 规范化 -let path = vfs::normalize_path("/../../../etc/passwd"); // "/" -``` +- 修改 fd 分配, dup, close-on-exec: `fd_table.rs`. +- 修改普通文件 offset 或 append 语义: `impls/reg_file.rs`. +- 修改路径解析, symlink, cwd/root 行为: `path.rs`. +- 修改 mount/umount: `mount.rs` 和 syscall 层. +- 修改文件系统内部目录或数据行为: 对应 `os/src/fs//`. +- 修改块设备读写或分区: `os/src/device/block/`. +- 修改 `/dev` 节点或设备号: `fs/mod.rs`, `vfs/devno.rs`, device driver. -### 4. 缓存不一致 +## 关键流程 -**错误**: -```rust -let dentry = vfs::vfs_lookup("/tmp/file")?; -dentry.inode.unlink("subfile")?; -// 忘记清理缓存,后续查找可能仍然找到已删除的文件 -``` +### 用户路径到 fd -**正确**: -```rust -let dentry = vfs::vfs_lookup("/tmp/file")?; -dentry.inode.unlink("subfile")?; -dentry.remove_child("subfile"); -vfs::DENTRY_CACHE.remove("/tmp/file/subfile"); +```text +syscall args + -> normalize and lookup path + -> dentry and inode type + -> file implementation + -> fd table slot ``` -## 故障排查 - -### 常见错误 - -#### NotFound - -**原因**: 文件或目录不存在 - -**解决**: 检查路径是否正确,父目录是否存在 - -#### PermissionDenied - -**原因**: 没有读/写权限 - -**解决**: 检查文件 mode 和打开标志 (O_RDONLY/O_WRONLY/O_RDWR) - -#### IsDirectory - -**原因**: 对目录执行了文件操作 - -**解决**: 使用 metadata() 检查文件类型 - -#### NotDirectory - -**原因**: 对文件执行了目录操作 - -**解决**: 确保操作对象是目录 +判断 bug 时先确认问题发生在路径命名空间, file 会话, inode 操作, 还是 fd table slot. -#### TooManyOpenFiles +### fd 到数据 -**原因**: 超过最大文件描述符限制 - -**解决**: 关闭不需要的文件,或增加 `DEFAULT_MAX_FDS` - -#### FileExists - -**原因**: 文件已存在 (O_CREAT | O_EXCL) - -**解决**: 检查是否应该使用 O_TRUNC 覆盖 - -### 调试技巧 - -#### 打印文件描述符表 - -```rust -pub fn dump_fd_table() { - let current = current_task(); - let fd_table = ¤t.lock().fd_table; - println!("{:?}", fd_table); -} +```text +fd + -> Arc dyn File + -> file-specific state + -> inode or driver ``` -#### 列出挂载点 - -```rust -pub fn list_mounts() { - let mounts = vfs::MOUNT_TABLE.list_mounts(); - for (path, fstype) in mounts { - println!("{} on {} type {}", path, path, fstype); - } -} -``` +如果两个 fd 来自 `dup`, 它们共享 file-specific state. 如果来自两次 open, 它们通常共享 inode, 但不共享普通文件 offset. -#### 跟踪路径解析 +### mount 后访问 -在 `vfs_lookup` 中添加日志: - -```rust -pr_debug!(\"Looking up: {}\", path); -pr_debug!(\"Current dentry: {}\", current_dentry.name); -``` - -## 进阶主题 - -### 实现自定义文件系统 - -参考 tmpfs 实现自己的文件系统: - -```rust -pub struct MyFs { - // 文件系统状态... -} - -impl FileSystem for MyFs { - fn root_inode(&self) -> Arc { - // 返回根 Inode - } - - fn sync(&self) -> Result<(), FsError> { - // 同步数据到持久化存储 - } - - fn umount(&self) -> Result<(), FsError> { - // 卸载清理 - } - - fn fs_type(&self) -> &str { - "myfs" - } -} +```text +mount fs at /mnt + -> /mnt dentry points to mounted root + -> /mnt/a lookup enters mounted fs ``` -### 实现自定义文件类型 - -实现 File trait 创建特殊文件类型: +调用者不需要在每次访问时指定 fs type. fs type 只在 mount 创建文件系统实例时有意义. -```rust -pub struct MyFile { - // 文件状态... -} +### rootfs 初始化后访问设备 -impl File for MyFile { - fn readable(&self) -> bool { true } - fn writable(&self) -> bool { true } - - fn read(&self, buf: &mut [u8]) -> Result { - // 自定义读取逻辑 - } - - fn write(&self, buf: &[u8]) -> Result { - // 自定义写入逻辑 - } - - fn metadata(&self) -> Result { - // 返回元数据 - } -} +```text +discover block devices + -> pick ext4 rootfs containing /bin/sh or /bin/ash + -> create /dev + -> expose vda and partitions + -> mount procfs sysfs tmpfs ``` -## 相关资源 +VFAT 分区当前用于 mount/umount 和 FAT 兼容路径测试, 不作为默认 rootfs. -### 源代码示例 +## 并发和生命周期约束 -- **系统调用实现**: `os/src/kernel/syscall/fs.rs` -- **tmpfs 实现**: `os/src/fs/tmpfs/` -- **fat32 实现**: `os/src/fs/fat32/` +- 不要把 fd 生命周期和 inode 生命周期混为一谈. fd close 只释放 fd slot 引用. +- 不要让具体 FS 保存 VFS 的强 dentry 引用形成环. +- 不要在持有全局 mount/dentry 锁时做长时间块设备 I/O. +- 动态文件系统默认应谨慎使用缓存, 尤其是基于进程生命周期的路径. -### 参考文档 +## 已知限制 -- [VFS 整体架构](architecture.md) -- [Inode 与 Dentry](inode_and_dentry.md) -- [File 与 FDTable](file_and_fdtable.md) -- [路径解析与挂载](path_and_mount.md) +- 文档中的流程是当前设计目标, 具体 syscall 的兼容性仍以测试和源码为准. +- mount flags, permission checks, namespace 隔离仍有不完整处. +- 设备热插拔路径尚未形成自动 `/dev` 更新机制. -### 相关系统调用 +## 源码索引 -| 系统调用 | 功能 | 对应 VFS API | -|---------|------|--------------| -| open | 打开文件 | `vfs_lookup` + `RegFile::new` | -| read | 读取 | `File::read` | -| write | 写入 | `File::write` | -| close | 关闭 | `FDTable::close` | -| lseek | 定位 | `File::lseek` | -| stat | 获取元数据 | `Inode::metadata` | -| mkdir | 创建目录 | `Inode::mkdir` | -| rmdir | 删除目录 | `Inode::rmdir` | -| link | 硬链接 | `Inode::link` | -| unlink | 删除文件 | `Inode::unlink` | -| symlink | 符号链接 | `Inode::symlink` | -| readlink | 读链接 | `Inode::readlink` | -| mount | 挂载 | `MountTable::mount` | -| umount | 卸载 | `MountTable::umount` | -| dup | 复制 FD | `FDTable::dup` | -| dup2 | 复制到指定 FD | `FDTable::dup2` | -| pipe | 创建管道 | `create_pipe` | -| chdir | 切换目录 | 更新 `cwd` | +- `os/src/vfs/`: VFS 核心. +- `os/src/fs/`: 具体文件系统实现和初始化. +- `os/src/device/`: 设备驱动和块设备分区. +- `os/src/vfs/tests/`: VFS 行为测试. +- `os/src/fs/tests/`: FS 行为测试. +- `os/src/device/tests/`: 块设备和分区测试. diff --git a/os/src/arch/arch.rs b/os/src/arch/arch.rs index 653c4167..b8be3190 100644 --- a/os/src/arch/arch.rs +++ b/os/src/arch/arch.rs @@ -3,7 +3,7 @@ //! 组合 `CpuOps + VirtualMemory`,并添加进程管理、信号处理、 //! 用户/内核内存复制等高层 CPU/MMU 操作。 //! -//! 平台级操作(控制台 I/O、电源管理、地址映射)已移至 [`crate::arch::platform::Platform`]。 +//! 平台级操作(控制台 I/O、电源管理、地址映射)已移至 [`crate::arch::Platform`]。 //! //! 注意:此 trait 使用关联类型来避免直接引用内核数据结构, //! 确保架构层与内核其余部分的解耦。 @@ -57,7 +57,7 @@ pub trait HwTrapFrame: Copy + Debug + Send + Sync + SyscallFrame + 'static { /// 这是移植新架构时需要实现的第三个 trait(在 `CpuOps` 和 `VirtualMemory` 之后)。 /// [`Platform`] 应同时实现以覆盖控制台、电源等平台操作。 /// -/// [`Platform`]: crate::arch::platform::Platform +/// [`Platform`]: crate::arch::Platform pub trait Arch: CpuOps + VirtualMemory { /// 用户上下文类型(保存/恢复寄存器状态) type UserContext: Sized + Send + Sync + Clone; diff --git a/os/src/fs/mod.rs b/os/src/fs/mod.rs index 2623dee6..f76241fe 100644 --- a/os/src/fs/mod.rs +++ b/os/src/fs/mod.rs @@ -4,7 +4,7 @@ //! //! ## 支持的文件系统 //! -//! - **[tmpfs](tmpfs)**: 临时文件系统(纯内存) +//! - **[tmpfs]**: 临时文件系统(纯内存) //! - 高性能,所有数据存储在内存中 //! - 支持配置最大容量限制 //! - 适用于`/tmp`、缓存等临时存储 @@ -14,7 +14,7 @@ //! - 标准Linux `/proc` 接口 //! - 只读,使用Generator模式 //! -//! - **[sysfs](sysfs)**: 系统设备伪文件系统 +//! - **[sysfs]**: 系统设备伪文件系统 //! - 导出设备树和属性 //! - 标准Linux `/sys` 接口 //! - 使用Builder模式构建设备树 diff --git a/os/src/fs/proc/generators/process/maps.rs b/os/src/fs/proc/generators/process/maps.rs index 5eae9762..80d6db44 100644 --- a/os/src/fs/proc/generators/process/maps.rs +++ b/os/src/fs/proc/generators/process/maps.rs @@ -10,7 +10,7 @@ use crate::{ vfs::FsError, }; -/// /proc/[pid]/maps (simplified): list user VMAs and their sizes. +/// `/proc/[pid]/maps` (simplified): list user VMAs and their sizes. pub struct MapsGenerator { task: Weak>, } diff --git a/os/src/fs/sysfs/builders/block.rs b/os/src/fs/sysfs/builders/block.rs index b3ed21ee..59ced1b7 100644 --- a/os/src/fs/sysfs/builders/block.rs +++ b/os/src/fs/sysfs/builders/block.rs @@ -11,7 +11,7 @@ use crate::vfs::{FsError, Inode}; /// 构建块设备 sysfs 树 /// -/// 在 /sys/class/block/ 下创建指向 /sys/devices/platform// 的符号链接 +/// 在 `/sys/class/block/` 下创建指向 `/sys/devices/platform//` 的符号链接 pub fn build_block_devices(root: &Arc) -> Result<(), FsError> { // 获取 /sys/class/block/ let class_inode = root.lookup("class")?; diff --git a/os/src/fs/sysfs/builders/input.rs b/os/src/fs/sysfs/builders/input.rs index 7ffa2fe3..e3b1d489 100644 --- a/os/src/fs/sysfs/builders/input.rs +++ b/os/src/fs/sysfs/builders/input.rs @@ -11,7 +11,7 @@ use crate::vfs::{FsError, Inode}; /// 构建输入设备 sysfs 树 /// -/// 在 /sys/class/input/ 下创建指向 /sys/devices/platform// 的符号链接 +/// 在 `/sys/class/input/` 下创建指向 `/sys/devices/platform//` 的符号链接 pub fn build_input_devices(root: &Arc) -> Result<(), FsError> { let class_inode = root.lookup("class")?; let class = class_inode diff --git a/os/src/fs/sysfs/builders/net.rs b/os/src/fs/sysfs/builders/net.rs index fe85db4b..e66a1a62 100644 --- a/os/src/fs/sysfs/builders/net.rs +++ b/os/src/fs/sysfs/builders/net.rs @@ -11,7 +11,7 @@ use crate::vfs::{FsError, Inode}; /// 构建网络设备 sysfs 树 /// -/// 在 /sys/class/net/ 下创建指向 /sys/devices/platform// 的符号链接 +/// 在 `/sys/class/net/` 下创建指向 `/sys/devices/platform//` 的符号链接 pub fn build_net_devices(root: &Arc) -> Result<(), FsError> { let class_inode = root.lookup("class")?; let class = class_inode diff --git a/os/src/fs/sysfs/builders/rtc.rs b/os/src/fs/sysfs/builders/rtc.rs index 9ab3ae18..f7fd4966 100644 --- a/os/src/fs/sysfs/builders/rtc.rs +++ b/os/src/fs/sysfs/builders/rtc.rs @@ -11,7 +11,7 @@ use crate::vfs::{FsError, Inode}; /// 构建 RTC 设备 sysfs 树 /// -/// 在 /sys/class/rtc/ 下创建指向 /sys/devices/platform// 的符号链接 +/// 在 `/sys/class/rtc/` 下创建指向 `/sys/devices/platform//` 的符号链接 pub fn build_rtc_devices(root: &Arc) -> Result<(), FsError> { let class_inode = root.lookup("class")?; let class = class_inode diff --git a/os/src/fs/sysfs/builders/tty.rs b/os/src/fs/sysfs/builders/tty.rs index dee7ed72..0bda977c 100644 --- a/os/src/fs/sysfs/builders/tty.rs +++ b/os/src/fs/sysfs/builders/tty.rs @@ -11,7 +11,7 @@ use crate::vfs::{FsError, Inode}; /// 构建 TTY 设备 sysfs 树 /// -/// 在 /sys/class/tty/ 下创建指向 /sys/devices/platform// 的符号链接 +/// 在 `/sys/class/tty/` 下创建指向 `/sys/devices/platform//` 的符号链接 pub fn build_tty_devices(root: &Arc) -> Result<(), FsError> { let class_inode = root.lookup("class")?; let class = class_inode diff --git a/os/src/fs/tmpfs/mod.rs b/os/src/fs/tmpfs/mod.rs index dbc39761..97f71377 100644 --- a/os/src/fs/tmpfs/mod.rs +++ b/os/src/fs/tmpfs/mod.rs @@ -5,7 +5,7 @@ //! # 组件 //! //! - [`TmpFs`] - 文件系统结构,实现 `FileSystem` trait -//! - [`TmpfsInode`] - Inode 实现,管理文件/目录数据 +//! - [`TmpfsInode`](inode::TmpfsInode) - Inode 实现,管理文件/目录数据 //! //! # 设计概览 //! diff --git a/os/src/kernel/task/task_struct.rs b/os/src/kernel/task/task_struct.rs index dc277a77..f4302fb8 100644 --- a/os/src/kernel/task/task_struct.rs +++ b/os/src/kernel/task/task_struct.rs @@ -91,7 +91,7 @@ pub struct Task { /// 任务的所属进程id /// NOTE: 由于采用了统一的任务模型,一个任务组内任务的 pid 是相同的,等于父任务的 pid 而父任务的 pid 等于自己的 tid pub pid: u32, - /// 当前正在执行的可执行文件路径(用于 /proc/[pid]/exe 等) + /// 当前正在执行的可执行文件路径(用于 `/proc/[pid]/exe` 等) /// /// 由 execve/kernel_execve 在切换到新程序前更新。 pub exe_path: Option, diff --git a/os/src/log/log_core.rs b/os/src/log/log_core.rs index 6d5f058a..da4c0df2 100644 --- a/os/src/log/log_core.rs +++ b/os/src/log/log_core.rs @@ -285,12 +285,12 @@ unsafe impl Sync for LogCore {} /// - `buffer::calculate_formatted_length` - 用于精确字节计数 /// /// # 格式 -/// ``` +/// ```text /// [LEVEL] [timestamp] [CPU/T] message /// ``` /// /// # 示例 -/// ``` +/// ```text /// \x1b[37m[INFO] [ 123456] [CPU0/T 1] Kernel initialized\x1b[0m /// \x1b[31m[ERR] [ 789012] [CPU0/T 5] Failed to mount /dev/sda1\x1b[0m /// ``` diff --git a/os/src/vfs/dev.rs b/os/src/vfs/dev.rs index 66eeb9ef..8ebe4557 100644 --- a/os/src/vfs/dev.rs +++ b/os/src/vfs/dev.rs @@ -41,7 +41,7 @@ pub const fn decode_linux_dev(dev: u64) -> u64 { makedev(major as u32, minor as u32) } -/// ext2/3/4 旧格式设备号编码,保存在 inode 的 i_block[0]。 +/// ext2/3/4 旧格式设备号编码,保存在 inode 的 `i_block[0]`。 #[inline] pub const fn encode_ext4_old_dev(dev: u64) -> u32 { let major = major(dev); @@ -49,7 +49,7 @@ pub const fn encode_ext4_old_dev(dev: u64) -> u32 { ((major & 0xff) << 8) | (minor & 0xff) } -/// ext2/3/4 新格式设备号编码,保存在 inode 的 i_block[1]。 +/// ext2/3/4 新格式设备号编码,保存在 inode 的 `i_block[1]`。 #[inline] pub const fn encode_ext4_new_dev(dev: u64) -> u32 { let major = major(dev); diff --git a/os/src/vfs/file.rs b/os/src/vfs/file.rs index e879648c..c4ddb2a2 100644 --- a/os/src/vfs/file.rs +++ b/os/src/vfs/file.rs @@ -33,7 +33,7 @@ //! //! - [`RegFile`](crate::vfs::RegFile) - 普通文件,基于 Inode,支持 seek //! - [`PipeFile`](crate::vfs::PipeFile) - 管道,环形缓冲区,流式设备 -//! - [`StdinFile`](crate::vfs::StdinFile) / [`StdoutFile`](crate::vfs::StdoutFile) / [`StderrFile`](crate::vfs::StderrFile) - 标准 I/O +//! - `StdinFile` / `StdoutFile` / `StderrFile` - 标准 I/O //! - `CharDevFile` - 字符设备文件(串口、终端等) //! - `BlkDevFile` - 块设备文件(磁盘等) //! diff --git a/os/src/vfs/mod.rs b/os/src/vfs/mod.rs index 86eed4f0..b1892e77 100644 --- a/os/src/vfs/mod.rs +++ b/os/src/vfs/mod.rs @@ -78,7 +78,7 @@ //! //! - [`RegFile`]: 普通文件 - 基于 Inode,支持 seek //! - [`PipeFile`]: 管道文件 - 环形缓冲区,流式设备 -//! - [`StdinFile`]/[`StdoutFile`]/[`StderrFile`]: 标准 I/O 文件 +//! - `StdinFile`/`StdoutFile`/`StderrFile`: 标准 I/O 文件 //! - `CharDevFile`: 字符设备文件(串口、终端等) //! - `BlkDevFile`: 块设备文件(磁盘等) //!