OS 抽象具体实现¶
日期:2026-07-04 标签:rust, os, abstraction, trait, async
来源:StarryOS
crates/uart_16550/src/os/mod.rs、kernel/src/drivers/os_arceos.rs、openspec/specs/architecture/spec.md:352-388、openspec/specs/learned/spec.md:226-228,657,188-200,765。 范围:2 trait 最小化设计、ArceOS 适配、跨平台扩展、与 DWMAC HAL 对比、AtomicWaker vs OsWakerSet。
答案¶
OS 抽象层只暴露 OsRuntime + OsWakerSet。从 5 trait 缩减到 2 trait(ADR-036),删了未被 driver 调用的 3 个。
设计动机:5→2 演化¶
ADR-035(2026-06-17)最初定义 5 个 OS 抽象 trait。包括 OsRuntime、OsIrq、OsMmio、OsSpinNoIrq、OsWakerSet。号称「最小完备接口集」。
ADR-036 后续审计发现:OsIrq、OsMmio、OsSpinNoIrq 三个 trait 从未被 driver 代码调用。IRQ 注册、MMIO 映射、锁获取都在 OS 适配层处理。
YAGNI 原则:删除未使用 trait。tombstone 见 openspec/specs/architecture/spec.md:352-388。
trait 完整定义¶
// crates/uart_16550/src/os/mod.rs:21-38
pub trait OsRuntime {
fn spawn<F>(future: F, name: &str)
where
F: Future + Send + 'static,
F::Output: Send;
fn block_on<F>(future: F) -> F::Output
where
F: Future;
}
// crates/uart_16550/src/os/mod.rs:49-60
pub trait OsWakerSet: Send + Sync {
fn new() -> Self;
fn register(&self, waker: &Waker);
fn wake(&self) -> u32;
}
两个 trait 涵盖的职责:
| Trait | 职责 | 驱动调用场景 |
|---|---|---|
OsRuntime |
启动后台 task + 阻塞等 future | 启动 rx_copier、tx_copier;syscall 阻塞 read/write |
OsWakerSet |
多等待者 waker 集合 | RX/TX/DRAIN 三个 waker;ring buffer 的 poll |
ArceOS 适配¶
// kernel/src/drivers/os_arceos.rs:18-39
impl OsRuntime for ArceOsRuntime {
fn spawn<F>(future: F, name: &str)
where
F: Future + Send + 'static,
F::Output: Send,
{
let name = name.to_string();
axtask::spawn_with_name(
move || {
axtask::future::block_on(future);
},
name,
);
}
fn block_on<F>(future: F) -> F::Output
where
F: Future,
{
axtask::future::block_on(future)
}
}
// kernel/src/drivers/os_arceos.rs:48-61
impl OsWakerSet for ArceOsWakerSet {
fn new() -> Self {
Self {
inner: axpoll::PollSet::new(),
}
}
fn register(&self, waker: &Waker) {
self.inner.register(waker);
}
fn wake(&self) -> u32 {
self.inner.wake() as u32
}
}
选型理由¶
| 选择 | 理由 |
|---|---|
axtask::spawn_with_name |
axtask 是 ArceOS 内核 task 抽象,spawn 出独立 task |
axtask::future::block_on |
axtask 自带 future 阻塞执行器,复用不引 embassy-executor |
axpoll::PollSet |
axpoll 提供 PollSet,register/wake 是标准多等待者模式 |
move \|\| 闭包 |
future move 进闭包,闭包调 block_on 跑该 future |
Send + 'static 约束确保 future 能跨 task 边界 move。
跨平台扩展接口¶
OsRuntime 与 OsWakerSet 是公开 trait。其他 OS 可写适配:
// 假设为 Linux 裸机写适配
struct LinuxRuntime;
impl OsRuntime for LinuxRuntime {
fn spawn<F>(future: F, name: &str) where F: Future + Send + 'static, F::Output: Send {
// 例如用 tokio::spawn 替代 axtask
}
fn block_on<F>(future: F) -> F::Output where F: Future {
futures::executor::block_on(future)
}
}
struct LinuxWakerSet { /* ... */ }
impl OsWakerSet for LinuxWakerSet {
fn new() -> Self { /* ... */ }
fn register(&self, waker: &Waker) { /* ... */ }
fn wake(&self) -> u32 { /* ... */ }
}
实现后 AsyncUartDriver<R=LinuxRuntime, W=LinuxWakerSet, U> 即可在 Linux 上跑。R 与 W 是泛型参数,由适配层注入。
与其他抽象的对比¶
learned/spec.md:657 记录:DWMAC HAL trait 7 个方法(dma_alloc、dma_dealloc、mmio_phys_to_virt、mmio_virt_to_phys、wait_until、configure_platform、cache_flush_range)。
我们 UartPort(4 方法)+ OsRuntime/OsWakerSet(2 trait,共 4 方法)= 6 接口。比 DWMAC HAL 7 方法更精简,正交性更好(ADR-036 已印证)。
正交性解释:
- UartPort 解决硬件访问(与具体 UART 型号有关)
- OsRuntime 解决 task 抽象(与具体 OS 有关)
- OsWakerSet 解决 waker 集合(与具体 OS 有关)
DWMAC 把硬件访问与平台依赖(DMA、MMIO 转换、cache flush)混在一个 trait 里,违反单一职责。
AtomicWaker vs OsWakerSet¶
learned/spec.md:226 提到:AtomicWaker 需要 critical-section crate v1.0 的 _critical_section_1_0_acquire/release 符号。在 lib.rs 中用 disable_irqs/enable_irqs 实现。
ISR 上下文 → AtomicWaker::wake → 跨任务边界
↓
critical-section 提供的 acquire/release
↓
disable_irqs / enable_irqs
AtomicWaker 是单槽(embassy_sync::waitqueue::AtomicWaker),适合 ISR↔copier 唯一注册者场景。
OsWakerSet::register/wake 是多等待者容器(axpoll::PollSet),适合多消费者场景(epoll 多个等待者)。
两者不能互相替代。驱动用 AtomicWaker 处理 ISR↔copier 唯一通道;用 OsWakerSet 处理 copier↔消费者多等待者场景。
易错点¶
| 误判 | 真相 |
|---|---|
| trait 越多越灵活 | 越多越难跨平台;YAGNI 原则 |
OsRuntime::spawn 启协程 |
启的是内核 task,独立栈 |
OsWakerSet 等同 AtomicWaker |
多等待者 vs 单槽,不同场景 |
driver 该知道 axtask / axpoll |
driver 只知道 trait,适配层桥接 |
| 改 trait 就能改 OS 行为 | trait 是契约,OS 行为靠适配层 |
| 5 trait 比 2 trait 更完备 | 完备 = 满足需求,未用的 trait 是负担 |
经验¶
- 抽象层的设计标尺是「驱动真正调用的方法」
- YAGNI 原则:未用的 trait 是负担
- 跨平台靠 trait 抽象,OS 行为靠适配层注入
- 单槽 waker 与多等待者 waker 是不同场景的两种工具
- trait 是契约,方法是接口,调用方按契约写
问题解答¶
问题 1:RTOS 适配需要什么新 trait?¶
axtask::future::block_on 假设「当前 task 能阻塞等 future」。RTIC 等 RTOS 任务不能阻塞(协作式或事件驱动),需要换思路。
可能新增 trait:
- OsTime::now():当前时间(用于超时)
- OsSpawn::spawn(future, name):RTIC 风格的 spawn(无 block_on)
- OsWfi::wait_for(event):等待事件(替代 block_on)
但「不能 block_on」对 UART 驱动影响很大。syscall 层大量用 block_on(poll_io(...)) 阻塞 read/write。RTIC 适配下 syscall 阻塞路径全要改。
实际结论:RTIC 等不能 block_on 的 RTOS 不适合套用这个 trait 集。axtask 是「支持 block_on 的最小 OS」,axtask::future 是「最小 async 运行时」。换 OS 意味着换 syscall 阻塞实现,不是简单加 trait。
问题 2:OsWakerSet::wake 返回 u32(被 wake 的 waker 数),为什么签名要 as u32 强转?¶
axpoll::PollSet::wake 返回 usize(被 wake 的 waker 数)。trait 签名固定 u32 是设计选择:
u32足够:实际 waker 数不会超过 4Gu32跨平台稳定:与平台字长无关- 适配层
as u32强转在 32 位/64 位平台都安全
如果用 usize,trait 跨平台时会暴露平台字长差异。u32 是「足够表达 + 跨平台稳定」的折中。
wake == 0 的语义:没有 waker 注册时返回 0,调用方据此判断「无人等待」(flush 在 recheck 阶段可见这种状态)。