VFS 接口 + flush 实现路径¶
日期:2026-07-04 标签:rust, vfs, tty, flush, syscall
来源:StarryOS
kernel/src/syscall/fs/ctl.rs:43-67、kernel/src/syscall/fs/io.rs:125-130、kernel/src/pseudofs/dev/tty/mod.rs:107-220、kernel/src/file/mod.rs:139-181、crates/uart_16550/src/async_/device_ops.rs:128-152、crates/uart_16550/src/tty.rs:236-260。 范围:flush 在 VFS / TTY / syscall 三层的语义差异、TCSBRK 双份实现反模式、Q22 维护性清理候选。
结论¶
flush 不是单一动作。它在「应用层 → 用户库 → syscall → VFS → TTY → driver → 硬件」链上每环语义不同。StarryOS 的 tcdrain 在 syscall 层硬编码拦截 TCSBRK,并复制了一份 register-then-recheck。
五层 flush 语义¶
L1 用户态 C 库:
| 调用 | 含义 |
|---|---|
fflush(stdout) |
把 stdio 缓冲写到内核 fd,不等发送 |
C 库 fflush 只覆盖用户态缓冲。fwrite + fflush 只保证 hello 进内核 TTY 缓冲,不保证离开芯片。
L2 POSIX 系统调用:
| syscall | 语义 |
|---|---|
tcdrain(fd) |
等输出排空到物理介质 |
fsync(fd) |
等所有缓冲(含 metadata)持久化 |
fdatasync(fd) |
等数据持久化(不含 metadata) |
ioctl(TCSBRK, 1) |
发 break,等 drain |
tcdrain 与 fsync 对 TTY 实际等价。
L3 内核 VFS:sys_fsync(kernel/src/syscall/fs/io.rs:125)调 File::from_fd(fd)?.inner().sync(false)。只对 axfs::File 走 PageCache 同步。
TTY 节点不走 axfs::File。flush 走 ioctl 路径:FileLike::ioctl → Location::ioctl → Tty::ioctl。
L4 TTY 抽象:关键发现——Tty<R, W> 与 TtyWrite trait 都没有 flush 方法:
// crates/uart_16550/src/tty.rs:252-260
pub trait TtyWrite: Send + Sync + 'static {
fn write(&self, buf: &[u8]) -> usize;
}
flush 不在 Tty 抽象里。
L5 驱动层:AsyncUartWriter::flush(crates/uart_16550/src/async_/device_ops.rs:128)实现 embedded_io_async::Write::flush,调 tx_completion + register-then-recheck 走四阶段 drain。
实际调用链¶
tcdrain(fd)
↓
syscall ioctl(fd, 0x5409, 1) ← TCSBRK
↓
sys_ioctl (kernel/src/syscall/fs/ctl.rs:43-67)
↓ 硬编码拦截
调 driver.tx_completion + register-then-recheck
↓
block_on → 四阶段 drain
不经过:
- ❌ FileLike::ioctl
- ❌ Tty::ioctl
- ❌ AsyncUartWriter::flush
TCSBRK 硬编码证据¶
sys_ioctl 拦截 TCSBRK(kernel/src/syscall/fs/ctl.rs:43-68):
// TCSBRK (0x5409): tcdrain — wait for all TX stages (ring → copier → FIFO → wire)
if cmd == 0x5409 {
use uart_16550::async_::isr::DRAIN_WAKER;
use crate::drivers::uart_init;
let result = block_on(poll_fn(|cx| {
let driver = uart_init::driver();
let c = driver.tx_completion();
if c.is_drained() {
return Poll::Ready(Ok(0isize));
}
// Register waker before recheck (M1 D3 order: register → check)
if !c.ring_empty || c.copier_active || c.staged_bytes > 0 {
driver.tx.register_waker(cx.waker());
}
DRAIN_WAKER.register(cx.waker());
let c2 = driver.tx_completion();
if c2.is_drained() { Poll::Ready(Ok(0isize)) } else { Poll::Pending }
}));
result
} else {
f.ioctl(cmd, arg)...
}
Tty::ioctl(kernel/src/pseudofs/dev/tty/mod.rs:147-220)处理:TCGETS / TCSETS / TIOCGWINSZ / TIOCSPGRP / TIOCSCTTY / TIOCNOTTY / FIONBIO。不处理 TCSBRK。
driver flush 双份实现反模式¶
两段代码几乎逐字相同:
AsyncUartWriter::flush(device_ops.rs:128-152):
let c = self.driver.tx_completion();
if c.is_drained() { return Poll::Ready(Ok(())); }
if !c.ring_empty || c.copier_active || c.staged_bytes > 0 {
self.driver.tx.register_waker(cx.waker());
}
DRAIN_WAKER.register(cx.waker());
let c2 = self.driver.tx_completion();
if c2.is_drained() { Poll::Ready(Ok(())) } else { Poll::Pending }
syscall 层(ctl.rs:48-67):
let driver = uart_init::driver();
let c = driver.tx_completion();
if c.is_drained() { return Poll::Ready(Ok(0isize)); }
if !c.ring_empty || c.copier_active || c.staged_bytes > 0 {
driver.tx.register_waker(cx.waker());
}
DRAIN_WAKER.register(cx.waker());
let c2 = driver.tx_completion();
if c2.is_drained() { Poll::Ready(Ok(0isize)) } else { Poll::Pending }
M1 D3 顺序、is_drained 判断、waker 注册全重复。
反模式后果¶
| 后果 | 说明 |
|---|---|
| 违反单一事实来源 | driver 的 tx_completion 行为变化时,syscall 不感知 |
| Tty 抽象空洞 | Tty 假装管 tcdrain,实际完全无感 |
| 耦合具体驱动 | use crate::drivers::uart_init——非 uart_init 后端拿不到 tcdrain |
| 不可扩展 | PTY、socket、其他外设要 tcdrain 都得在 syscall 复制 |
为什么变成这样¶
Q19B 真板 userbench 需要 tcdrain 通过。最快打通路径:syscall 硬编码。Tty::ioctl 加分支需改抽象、改 PTY、改所有 TTY 后端。当时为赶真板验证选择硬编码。
这是「实施验证快、架构债留痕」的典型。ADR-051 没明确写「tcdrain 走 syscall 硬编码」,是个隐藏决策。
正确架构¶
如果重做:
Tty::ioctl(TCSBRK) → 调 AsyncUartWriter::flush
↓
embedded_io_async::Write::flush
↓
单一 register-then-recheck 实现
收益:
- 任何 TTY 后端(UART、PTY、socket)实现 embedded_io_async::Write::flush 即自动支持 tcdrain
- syscall 去掉硬编码,所有 ioctl 走通用路径
- flush 逻辑只有一份
Q22 维护性清理候选¶
O48/O49/O50 那组(架构债务清理)有「重复实现去除」一项。这条 TCSBRK 双份实现是典型例子,可列 O50「预留接口评估」或单开 O 条目。
易错点¶
| 误判 | 真相 |
|---|---|
| Tty 抽象处理 tcdrain | Tty::ioctl 不处理 TCSBRK,syscall 层硬编码 |
| tcdrain 走 driver 抽象层 | 走 uart_init 模块,耦合具体驱动 |
| flush 逻辑只一份 | syscall + AsyncUartWriter 各一份 |
| 改 driver 不影响 syscall | 改 tx_completion 行为两边都得改 |
经验¶
- flush 不是单一动作,是 5 层不同语义的串接
- Tty 不实现 flush 是有意的设计选择(推给上层 VFS 与底层 driver)
- syscall 硬编码拦截 = 实施快、架构债重
- 单一事实来源被破坏时,「打补丁 + 留 ADR 标注」是必要的善后
- 评估 tcdrain 实现时,先 grep
0x5409或TCSBRK看是不是硬编码
深挖补充¶
TtyWrite 不实现 flush 的设计意图
TtyWrite trait 注释(crates/uart_16550/src/tty.rs:243-260)明确「non-blocking」。TtyWrite 只承诺「能塞多少塞多少」,不承诺「等发送完成」。
教科书分层:
- TtyWrite 同步非阻塞:当下能写多少
- embedded_io_async::Write 异步:何时写完
Tty 抽象留白是有意。flush 由异步层负责。
为什么不把 flush 加到 TtyWrite
两个问题:
- 破坏 TtyWrite 同步非阻塞契约。flush 该返回
Result<(), _>还是AsyncResult?前者要求阻塞。 - PTY master / slave 需要不同 flush 语义。master 不需要等发送完成;slave 需要。这种差异在 TtyWrite 上不好表达。
embedded_io_async::Write::flush 是 async fn,回调能自然适配。当前只有 AsyncUartWriter 实现了,PTY 还没接 flush。
flush 失败如何处理
UART 错误(overrun、parity、frame)当前被吞:
AsyncUartWriter::flush返回Infallible(永不错误)- syscall 层 TCSBRK 返回
Ok(0isize)
错误在 ISR 处理,不上报到 flush 路径。教科书设计应报告错误给应用层,当前实施简化。这是 Q22 维护性清理候选。
教科书设计 vs 实际实现
教科书「tcdrain 走 TTY 抽象」:
用户 tcdrain(fd)
↓
syscall → FileLike::ioctl
↓
Tty::ioctl(TCSBRK)
↓
embedded_io_async::Write::flush
↓
register-then-recheck
↓
硬件 drain
StarryOS 实际「tcdrain 走 syscall 硬编码」:
用户 tcdrain(fd)
↓
syscall ioctl(0x5409)
↓
sys_ioctl 硬编码拦截
↓(绕过 Tty 抽象)
调 driver.tx_completion
↓
block_on → register-then-recheck
↓
硬件 drain
差异:教科书 5 层抽象层次清晰。StarryOS 4 层(少 Tty::ioctl 翻译层)+ driver 被硬编码。
触发原因:Q19B 真板 tcdrain 必须通过,最快路径。
架构债的识别与偿还
何时偿还 TCSBRK 双份实现?「现在」vs「Q22 维护性清理」:
| 维度 | 现在 | Q22 |
|---|---|---|
| 风险 | 真板验证期间改动可能引入新 bug | 验证已通过,改动风险低 |
| 成本 | 跟当前验证并行 | 单独 milestone |
| 收益 | 立即去除双份实现 | 与其他维护性债务一起处理 |
| 阻塞 | 不阻塞 Q19B | 不阻塞主功能线 |
当前选 Q22 延后是合理工程判断。两个先决条件:
- ADR 标注清楚——记录 TCSBRK 硬编码是临时方案。当前 ADR-051 没明确写这条,需要补。
- grep 找得到。
kernel/src/syscall/fs/ctl.rs:43注释「TCSBRK (0x5409): tcdrain」是充分提示。
加新 TTY 后端(PTY、socket)时必须重新评估。硬编码拦不住新后端,会暴露「非 UART 不能 tcdrain」的问题。
什么时候该重写 vs 保留
「实施快、架构债重」是工程常态。判断何时偿还:
| 信号 | 行动 |
|---|---|
| 同一处债出现 3 次 | 必须重写 |
| 债导致新功能不能加 | 必须重写 |
| 债只在文档里 | 保持 + ADR 标注 |
| 债影响调试效率 | 必须重写 |
TCSBRK 双份实现目前是「债只在文档里」。代码重复但功能正常,调试靠 grep 找得到。Q22 处理合理。
易错点(笔记之外)
| 误判 | 真相 |
|---|---|
| 教科书设计 = 正确设计 | 教科书完整但实施成本高 |
| 实施快 = 债 | 工程常态,需 ADR 标注 |
| flush 永远成功 | UART 硬件错误当前被吞 |
| 抽象层加 flush = 好事 | 破坏 TtyWrite 同步非阻塞契约 |
| 5 层都齐 = 完备 | 完备 = 满足需求,不多不少 |
经验(笔记之外)
- 教科书设计完整但实施成本高,工程取舍优先
- 「实施快 + ADR 标注 + grep 找得到」是工程常态
- 架构纯洁不是每个项目都负担得起
- TtyWrite 同步非阻塞契约是有意设计,flush 不该破坏
- 加新 TTY 后端是 TCSBRK 双份实现必须重写的临界点