跳转到内容

架构决策记录

本文档记录 OpenOS 开发过程中的重大架构决策。

每条 ADR 遵循以下格式:

  • Status: Proposed | Accepted | Deprecated | Superseded
  • Context: What is the issue that motivates this decision?
  • Decision: What is the change being proposed?
  • Consequences: What are the resulting implications?

Status: Accepted Date: 2026-07-23

内核必须直接管理硬件——内存、CPU、I/O。传统选择是 C 和汇编。C 缺乏内存安全;汇编易错且不可移植。我们需要一种语言提供:

  1. 无运行时开销的内存安全
  2. 硬件交互的零成本抽象
  3. #![no_std] 支持(无标准库依赖)
  4. 强类型系统用于状态机建模

使用 Rust nightly 工具链,配合 #![no_std]#![no_main]

使用的 nightly 特性:

  • abi_x86_interrupt — 允许 extern "x86-interrupt" 用于 ISR 函数
  • alloc_error_handler — 自定义分配失败处理函数
  • build-std — 为裸机目标重新构建 corealloc

Positive:

  • 借用检查器在编译期捕获 use-after-free、数据竞争
  • #[repr(C)] 结构体直接映射到硬件寄存器
  • 模式匹配确保 CPU 异常的穷尽处理
  • unsafe 边界明确标记硬件交互点

Negative:

  • 仅 nightly 的特性可能在工具链更新时中断
  • 硬件访问仍然需要 unsafe
  • OS 开发的生态系统成熟度低于 C

Status: Accepted Date: 2026-07-23

宏内核(Linux、Windows NT)将所有服务运行在内核态——驱动中的 bug 可以导致整个系统崩溃。微内核(L4、seL4、MINIX)仅在内核态运行核心服务,隔离故障。

采用微内核架构。内核仅处理:

服务内核空间用户空间(未来)
内存管理
任务调度
IPC
中断分发
设备驱动
文件系统
网络栈
显示服务器

Positive:

  • 更小的可信计算基(TCB)
  • 驱动 bug 不会导致内核崩溃
  • 服务可以独立重启
  • 更容易进行形式化验证(参见 seL4)

Negative:

  • 每个系统调用的 IPC 开销
  • 更复杂的启动序列(用户空间服务必须加载)
  • 更难实现与宏内核的性能对等

Status: Accepted Date: 2026-07-23

x86_64 虚拟地址空间为 48 位(256 TiB)。内核可以放置在地址空间底部(0x0)或顶部(0xFFFF…)。放置在顶部(高半)是现代操作系统的标准做法。

高半内核映射在 0xFFFFFFFF80100000

KERNEL_OFFSET = 0xFFFFFFFF80000000
Physical load address: 0x100000 (1 MiB, 常规)
Virtual base: KERNEL_OFFSET + 0x100000 = 0xFFFFFFFF80100000

链接脚本使用 AT(ADDR(.section) - KERNEL_OFFSET) 在二进制中输出物理地址,而内核在虚拟地址运行。

Positive:

  • 低 2 GiB 空闲用于用户空间程序(可以使用 32 位偏移量的 calljmp
  • 内核受页表保护——用户空间页错误不会触及内核页面
  • 与 Linux、Windows 和大多数现代 OS 布局匹配

Negative:

  • 在内核可以运行在其虚拟地址之前需要工作的页表
  • 引导加载器必须将内核的物理页面映射到高半区域

ADR-004: 构建目标 — x86_64-unknown-none

Section titled “ADR-004: 构建目标 — x86_64-unknown-none”

Status: Accepted Date: 2026-07-23

裸机 Rust 需要一个禁用操作系统特性的目标规范(无 libc、无线程、无文件系统)。选项:自定义 JSON 目标 vs 内置目标。

使用内置 x86_64-unknown-none 目标,而非自定义 JSON 文件。

.cargo/config.toml 中的配置:

[build]
target = "x86_64-unknown-none"
[unstable]
build-std = ["core", "compiler_builtins", "alloc"]
build-std-features = ["compiler-builtins-mem"]

Positive:

  • 无需维护自定义目标 JSON
  • 设置 .cargo/config.toml 后可与 cargo build 配合工作
  • Rust 团队维护目标规范

Negative:

  • 无法自定义特性(例如,无法在没有自定义 JSON 的情况下禁用 SSE)
  • 未使用的 x86_64-openos.json 中的 -mmx,-sse,-sse2 特性需要自定义规范——但内置目标正确处理了这一点

Status: Accepted Date: 2026-07-23

内核需要对共享数据结构(调度器队列、IPC 端口注册表、VGA 缓冲区)进行互斥访问。选项:自旋锁、票据锁、MCS 锁或禁中断锁。

使用 spin crate (v0.9) 的 spin::Mutex

对于 VGA 输出,配合 x86_64::instructions::interrupts::without_interrupts 使用,防止中断处理函数尝试打印时死锁。

Positive:

  • 简单,易于理解
  • 不依赖操作系统特性(在 no_std 中工作)
  • 确定性——不睡眠,不与调度器交互

Negative:

  • 忙等待——浪费 CPU 周期
  • 不公平——没有等待者的排序保证
  • 如果在中断在同一 CPU 上触发时持有自旋锁可能死锁(通过打印锁的 without_interrupts 缓解)

ADR-006: IPC 模型 — 基于端口的消息传递

Section titled “ADR-006: IPC 模型 — 基于端口的消息传递”

Status: Accepted Date: 2026-07-23

微内核需要高效的进程间通信。两种主要模型:共享内存和消息传递。L4(微内核 IPC 的黄金标准)使用同步消息传递。

实现基于端口的消息传递:

  • 每个服务拥有一个端口(由 u64 ID 标识)
  • 消息为 Message { sender, receiver, data: MessageData }
  • MessageData 枚举支持:TextBytesRequest { id, data }Response { id, data }
  • 端口存储在 IpcManager 内的 BTreeMap<u64, Port>
  • 发送是异步的(消息排队在接收方的收件箱中)
  • 接收是同步的(阻塞直到消息可用——尚未实现)

Positive:

  • 发送方和接收方之间的清晰分离
  • 请求/响应模式内置于消息类型中
  • 可扩展——以后可以添加基于能力的访问控制

Negative:

  • 基于复制(尚无零复制)——大消息开销大
  • 无共享内存用于批量数据传输(例如帧缓冲区)
  • BTreeMap 查找是 O(log n)——可以使用数组用于已知端口数量

Status: Accepted Date: 2026-07-23

Rust panic 可以选择栈展开(默认)或立即中止。展开需要大量运行时支持(着陆垫、异常表),在裸机内核中实现复杂。

[profile.dev][profile.release] 中均设置 panic = "abort"

[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "s"
lto = true

Positive:

  • 无展开表——更小的二进制
  • 无需实现 eh_personality 或着陆垫
  • 更简单的 panic 处理函数——只需打印并停机

Negative:

  • 无法使用 catch_unwind 捕获 panic
  • 任何 panic 都是致命的——无优雅降级
  • 依赖展开的库无法正常工作

ADR-008: 堆分配器 — linked_list_allocator

Section titled “ADR-008: 堆分配器 — linked_list_allocator”

Status: Accepted Date: 2026-07-23

内核需要动态内存分配(BoxVecString)。选项:bump 分配器、链表分配器、slab 分配器、伙伴分配器。

使用 linked_list_allocator crate (v0.10) 作为全局分配器。

#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();
pub fn init_heap() {
unsafe {
ALLOCATOR.lock().init(HEAP_START as *mut u8, HEAP_SIZE);
}
}

堆:100 KiB,起始于虚拟地址 0x4444_4444_0000

Positive:

  • 简单,无外部依赖
  • 首次适配分配——适合内核使用
  • 线程安全(包装在 spin::Mutex 中)

Negative:

  • 随时间产生碎片——不适合长期运行的生产内核
  • 无每 CPU 缓存——高分配负载下的锁竞争
  • 固定堆大小(100 KiB)——必须通过页分配器使其动态化

ADR-009: VGA 输出 — 直接缓冲区访问

Section titled “ADR-009: VGA 输出 — 直接缓冲区访问”

Status: Accepted Date: 2026-07-23

内核需要输出用于调试和用户反馈。VGA 文本模式在物理地址 0xB8000 提供 80×25 字符缓冲区。替代方案:串口、帧缓冲、VESA。

同时实现 VGA 文本缓冲区和串口输出:

  • VGA (drivers/vga.rs):通过 Volatile<ScreenChar> 直接写入 0xB8000,提供 print!/println!
  • 串口 (drivers/serial.rs):0x3F8 的 UART 16550,提供 serial_print!/serial_println!

VGA 使用 volatile crate 防止编译器优化掉写操作。

Positive:

  • 启动期间的即时视觉反馈
  • 使用 -serial stdio 在 QEMU 中可见的串口输出
  • Volatile 包装器防止重排/消除

Negative:

  • 没有传统支持的现代硬件上不可用 VGA
  • 直接缓冲区访问绕过任何抽象——以后更难替换
  • 除了简单的行移位外没有滚动缓冲区管理

Status: Accepted Date: 2026-07-23

内核代码必须正确——bug 是灾难性的。Rust 的 clippy 检查器捕获常见错误。我们需要在严格性和脚手架代码(dead code、unused variables)之间取得平衡。

src/main.rs 中启用严格 clippy:

#![warn(missing_docs)]
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(
clippy::module_inception,
dead_code,
unused_imports,
unused_variables,
clippy::missing_const_for_fn,
clippy::used_underscore_items
)]

格式化由 rustfmt.toml 强制执行,group_imports = "StdExternalCrate"

CI 门控:make check 运行 fmt → clippy (-D warnings) → build。

Positive:

  • 及早捕获 bug(例如 uninlined_format_argsuse_self
  • 贡献者之间一致的代码风格
  • missing_docs 确保所有公共 API 被记录

Negative:

  • crate 级别允许 dead_code——某些警告被抑制
  • clippy::nursery 有不稳定的 lint,可能改变行为
  • 提交前必须运行 cargo fmt(在设置 CI 之前是手动步骤)