0%

StarryOS 异步串口驱动

一、项目背景

StarryOS 是一个面向 RISC-V 的 Rust 内核项目,构建在 ArceOS 组件化框架之上。串口(UART)是内核与外界交互最重要的通道之一,承担三类任务:内核启动日志、Shell 终端交互、用户态高性能数据传输。

项目原本使用同步阻塞式的串口实现。这种方式有两个问题:第一,用户态 Shell 的输入/输出会因串口速率阻塞调用方;第二,写多字节时 CPU 持续轮询硬件 FIFO,效率低下。我们的训练营阶段初步目标,就是在保留同步内核日志通道的同时,从零为用户态 Shell 提供一条非阻塞、可中断驱动、CPU 高效的异步串口通路。

约束条件贯穿整个阶段:不修改任何上游框架、内核抽象层、硬件抽象层、底层驱动库;异步机制只引入社区主流的轻量级原子唤醒原语(embassy_sync::AtomicWaker),禁止引入完整的异步入库。


二、训练营其他阶段工作

训练营分阶段推进,前三个任务为后续主线工作奠定了协程与调度器的实践基础。

进程、线程、协程的性能对比。在统一数据量下对比三种并发模型,通过预取缓存、纯文件读取、可配置负载四步控制变量法排除了网络和解析的混淆。核心结论:I/O 密集型场景下协程较线程快 48% 至 82%,CPU 密集型场景下三者差异在 6% 以内,进程内存开销比线程和协程高 100 至 400 倍,结论经两轮独立运行验证零反转。

用户态线程与协程的深入实践。实现了一个支持优先级调度的用户态绿色线程库,通过裸函数栈布局手工实现上下文切换,支持 x86-64 Linux、Windows、RISC-V 64 三平台,全部代码可在 stable Rust 上编译运行。配套整理了无栈协程原理、绿色线程实现、执行流状态变迁三篇分析文档。

调度器对比与异步运行时优化。构建爬虫框架接入自研绿色线程和 Tokio 异步运行时,量化了四组调度配置在满载 CPU 与中量 I/O 场景下的差异。优化了 Tokio 优先级分发器:将基于二叉堆的逐任务分发改造为基于三队列的批量分发,使优先级模式与默认模式的实际耗时差距从 1.83 秒缩小至 0.11 秒,差距缩小 94%。


三、主要工作

本阶段工作按三条主线展开。

主线一:从零实现 kernel 层异步串口驱动

3.1 寄存器层验证

NS16550 UART 芯片只有 8 字节寄存器空间(偏移 0 到 7)。在早期验证中,由于对寄存器布局理解不到位,我们曾误用一个过大的寄存器步长,导致访问越界、触发总线错误。问题排查一度被误判为内存映射权限问题,使整体进度阻塞了将近一周。最终通过在同一页表映射内对比不同步长的读取结果,确认了是寄存器步长配置错误而非系统级权限问题,从而扫清了整个方向的最大障碍。这一教训让我们确立了「先排查代码 bug、再考虑系统级问题」的调试原则。

根因定位uart_init.rs:27-30UART_STRIDE = 1 及 LoadFault 注释

寄存器操作通过本地 fork 的 uart_16550 驱动库完成(kernel/Cargo.toml:106 path = "../../uart_16550"),而非直接依赖 crates.io 版本。该库封装了 NS16550 的 MMIO 后端寄存器定义(IER/ISR/LSR 等)和中断类型枚举,为上层提供安全的寄存器读写 API。我们在本地 fork 上的主要改动是为其新增了 set_ier() 公开写方法,使 IER 操作统一走安全 API 而非裸 write_volatile(详见 §3.6 修复三),后续 Q12 阶段还为其补充了 embedded_io_async trait 实现

3.2 驱动三件套

我们用三件套构成了异步串口的核心机制:

组件 文件 说明
环形缓冲区 ring_buffer.rs SPSC 无锁结构,64 KiB 每方向,使用 atomic_ring_buffer + PollSet 唤醒
中断服务程序(ISR) isr.rs 25 行极简实现:读 ISR → 禁中断 → AtomicWaker::wake → 返回
后台搬运任务 async_driver.rs RX/TX copier loop,NAPI 风格轮询/中断动态切换(阈值 16)

关键设计是 ISR 只做唤醒而不做数据搬运,搬运工作交给后台任务,保持 ISR 极简。整套三件套约 500 行代码,全部位于内核自身模块。

完整实现kernel/src/drivers/

3.3 虚拟文件系统集成

我们将异步串口封装为标准设备节点,对接现有的设备注册框架,使异步串口与项目内其他设备采用完全一致的接入方式。

  • 设备注册:dev/mod.rs:222-230/dev/console (5,1) 绑定 ASYNC_TTY
  • 设备操作:device_ops.rsTtyRead/TtyWrite + embedded_io_async trait 实现
  • 终端绑定:ntty_async.rs — 泛型 Tty<AsyncUartReader, AsyncUartWriter>ProcessMode::External 消除 yield storm

终端层通过泛型绑定替换了 reader 和 writer 两个 trait,整个终端栈因此无须修改即可切换到异步通路。

TTY 层实现ldisc.rs 行规则模块 | tty/mod.rs Tty 泛型结构

3.4 Shell 双向异步打通

在接收方向上,中断唤醒后台搬运任务,搬运任务填充环形缓冲区,用户态 Shell 从缓冲区读取,整条链路无阻塞。在发送方向上,用户态写入环形缓冲区,后台搬运任务将其送入硬件 FIFO,内核日志通道与异步通道共享同一个 UART 硬件但互不冲突,实现了「内核日志同步输出、用户态 Shell 异步交互」的共存架构。

共存实现dev/mod.rs:222-230 设备注册 | entry.rs:23-28 启动初始化 | async_driver.rs copier 与 Console 互斥读 FIFO


主线二:性能与正确性的多轮打磨

异步通路打通后,我们用了八轮迭代打磨性能和正确性。

性能对比报告benchmark-report-async.md | 手动测试报告manual-qa-report.md | Console vs Async 对比uart-performance-comparison.md

3.5 热路径优化(Q5 阶段)

对中断处理路径做了五项核心优化:

# 优化 代码位置 效果
1 IER 值缓存为原子变量,读改写→单次写入 uart_init.rs:99-109 消除 MMIO RMW 开销
2 RX/TX 独立锁,消除读写伪竞争 ring_buffer.rs 分设 RingBufRx/RingBufTx 解除读写互斥
3 批量读写接口替代逐字节操作 uart_init.rs:32-35 NAPI_BATCH_SIZE=64 减少锁获取次数
4 唤醒去重(will_wake 检查),避免无意义唤醒 async_driver.rs:41,61-65 减少 CPU 空转
5 NAPI 中断合并:连续成功≥16次切轮询,高吞吐减 90%+ IRQ async_driver.rs:46-60 大幅降低中断频率

以上 5 项优化的代码位置见表格中各行的链接,对应优化编号 O2/O25~O34。

3.6 三轮正确性修复(Q7~Q8 阶段)

# 缺陷 修复 代码位置
1 NAPI 退出缺陷:轮询模式计数器只增不减、空数据时无法重置 → CPU 空转 总量为 0 时重置 consecutive 计数器 async_driver.rs
2 ISR 持锁:中断处理路径仍持有 SpinNoIrq,违反 ISR 极简原则 移除 ISR 内所有锁,改用 read_isr_unlocked/read_lsr_unlocked isr.rs + uart_init.rs:72-96
3 IER 裸写绕过安全 APIwrite_volatile 裸地址写 IER,违反 MMIO 封装规则 给底层驱动库新增 set_ier() 公开方法,统一所有写入路径 uart_init.rs:102-109 + uart_16550/src/lib.rs

以上 3 项修复的代码位置见表格中各行的链接。

3.7 多轮迭代优化(Q9~Q12 阶段)

在三轮正确性修复之上,我们又进行了五轮优化迭代:

阶段 优化内容 关键指标变化 变更记录
Q9 超时机制:VTIME 读超时复用 axtask::future::timeout ldisc.rs VTIME 处理
Q10 数据路径优化:批量写入改造、缓冲容量扩充 3 倍、ldisc 锁粒度细化 1B 延迟 145→122 µs(↓16%) ldisc.rs + ring_buffer.rs
Q11 终端与内存子系统一般性优化(mm/access、close_range、sendfile、tty unwrap) 1B 延迟 122→118 µs;软件开销峰值↓40% tty/mod.rs + mm/
Q12 Embassy 路径 A:atomic_ring_buffer 无锁化、embedded_io_async 标准化、TC 硬件寄存器 tcdrain 1B 延迟 P50=115.7 µs;软件开销↓31%(53.9→37.1 µs);唤醒延迟 200→50 ns(8 点应用) ring_buffer.rs + device_ops.rs + uart_16550
Q6 ⏳ 真板验证(VisionFive2)— 等待硬件 Makefile vf2 目标

Q12 性能回归测试tests/benchmark.c 用户态 benchmark 源码

3.8 已识别的反优化(排除的错误方向)

我们对五类主流异步入库原语替换提案进行了评估,最终全部判定为反优化:

提案 排除原因
MPMC Channel 替换 SPSC Ring Buffer 丢失无锁特性、引入不必要的间接层
异步 Mutex 替换同步 SpinLock 反而更慢且无法跨等待点持有
Watch 包装单个 AtomicBool 对单一标志而言过度设计
Semaphore 做事件计数 用错了工具
select! 宏替换手动 poll 与 axtask 协程调度器不兼容
通用分发结构替代静态 AtomicWaker 唤醒目标固定且极少时,静态唤醒已是 O(1),通用结构反引入开销

上述反优化判定的对照实现:ring_buffer.rs SPSC 无锁方案(对应 OE1 Channel)、isr.rs AtomicWaker 静态唤醒(对应 OE2/OE3/通用分发结构)


主线三:方法论与经验沉淀

核心原则:「先用轻量原语、复杂方案需评估改动范围与性能基准」。在评估五类异步入库原语替换提案后全部判定为反优化(详见 §3.8),避免后续在错误抽象上投入精力。整个阶段的工程决策与踩坑经验均已记录在项目文档体系中,此处不再展开。


四、量化成果与达到的效果

4.1 功能交付

完成了从概念到内核虚拟机完整验证的异步串口栈:用户态 Shell 双向异步输入输出、标准事件轮询接口、非阻塞读取与 tcdrain 真异步、内核日志与用户态通道共存共享硬件。Shell 基本交互均工作正常。

手动 QA 报告(12 项测试全部 PASS,含真实终端输出证据):manual-qa-report.md

4.2 性能数据

完整报告benchmark-report-async.md | 同步 vs 异步对比uart-performance-comparison.md

CPU 占用效率(统一数据量 10 万字节,端到端对比):

指标 同步实现(Console) 异步实现 提升
CPU 周期 / 字节 3,835 268(Q7)/ ~13(Q12) 14.3× → 295×
延迟中位数 17.5 µs 6.5 µs(Q7)/ 7.9 µs(Q12 无 tcdrain) 2.2~2.7×
P99 延迟 324.5 µs 43.1 µs 7.5×

说明:上表中「268 cycles/byte」为 Q5/Q7 阶段数据(IER 缓存 + ISR 合并 + 批量 I/O 后),「~13 cycles/byte」为 Q12 阶段数据(atomic_ring_buffer 无锁化 + embedded_io_async 标准化后)。两个数据均来自 QEMU 实测,详见 uart-performance-comparison.md

环形缓冲区直测(绕过终端层,测量纯内核态通路):每秒可搬运近 6 亿字节(RX 588,776 KB/s,TX 196,850 KB/s),延迟中位数 600 纳秒。

唤醒延迟:将异步唤醒从通用 PollSet 事件分发机制替换为静态 AtomicWaker 原子唤醒原语后,单次唤醒延迟从约 200 纳秒降至约 50 纳秒,提升约 4 倍。共有 8 处唤醒点应用了该优化(isr.rs RX_WAKER/TX_WAKER/DRAIN_WAKER + async_driver.rs copier 注册点)。

端到端预测(按 4096 字节数据量在真实硬件上估算):吞吐量效率可达理论线速的 97.7%~97.9%,软件开销占比不足 2.3%;单字节平均延迟中,软件部分的开销约占三分之一(Q12 实测 37.1 µs)。

需要说明的是,所有吞吐量数据在 QEMU 模拟器上不完全可信:QEMU 的 NS16550 设备模型不仿真真实的串口线延迟,因此真实硬件上的吞吐量会受 115200 波特率对应的每秒 11.5 KB 上限制约。可信的性能指标包括 CPU 周期占用、单字节延迟、唤醒延迟,这些在 QEMU 上也能准确反映。

4.3 代码与质量

  • 新增代码约 500 行,全部位于 kernel/src/drivers/
  • 项目整体净增约 450 行(涉及 14 个内核侧文件 + 1 个底层驱动库文件)
  • 静态检查零错误零警告:编译器检查、代码风格检查、编译警告均清零
  • 死代码清理:移除 8 处确认无用的方法(共 76 行),保留 5 处预留接口
  • 多轮迭代全部通过完整的功能与性能回归测试

五、经验与教训

5.1 根因复盘

根因一:寄存器步长误用。NS16550 寄存器物理布局仅 8 字节,使用大于 1 的步长会导致寄存器访问超出硬件定义的范围。QEMU 模拟器对越界访问返回总线错误,在 RISC-V 上被解释为取指故障。在排查过程中,我们一度将故障归因于「内存映射权限不足」,并据此否决了多个上游修改方案。最终通过在同一页表映射内对比两种步长的访问结果,确认了真实根因是步长配置而非系统权限。

教训:硬件访问异常时,应优先排查寄存器布局与地址计算,再考虑页表权限等系统级问题。

代码证据uart_init.rs:27-30

根因二:非阻塞标志三入口未穷举。非阻塞读取标志需要从用户态一路传播到行规则模块。我们最初只在 ioctl 这一个入口做了处理,遗漏了文件打开和 fcntl 两个入口,导致用户在某些调用路径下设置了非阻塞标志却仍然阻塞等待。

教训:跨层状态传播必须穷举所有可能的入口路径,一个遗漏就意味着功能不完整。

三入口代码位置fd_ops.rs:105-107 open | fd_ops.rs:280-283 fcntl | tty/mod.rs:177-179 ioctl

5.2 可复用的设计模式

以下九条设计模式在本项目中经过验证,未来可复用:

  1. 环形缓冲区 + 中断处理 + 后台搬运任务三件套:ISR 只唤醒、后台做数据搬运(isr.rs + async_driver.rs
  2. 标准化设备注册框架:所有设备走同一接入路径(dev/mod.rs
  3. 终端层通过读写 trait 泛型绑定:实现终端栈与具体驱动的解耦(ntty_async.rs + ldisc.rs
  4. 中断合并与轮询模式动态切换:高吞吐场景自动减少中断次数(async_driver.rs:32-35,46-60
  5. 批量读写接口替代逐字节操作:减少系统调用开销(uart_init.rs:32-35
  6. 本地游标追踪已发送位置:避免内核日志与用户态输出交错(async_driver.rs TX copier 游标逻辑)
  7. 静态原子唤醒替代通用事件分发:固定唤醒目标场景下的最优选择(isr.rs AtomicWaker)
  8. 中断处理极简原则:全程只读标志寄存器、关闭中断、唤醒、返回(isr.rs 25 行实现)
  9. 寄存器值缓存为原子变量:读改写转化为单次写入(uart_init.rs:99-109 CACHED_IER

5.3 后续方向

后续可能会参加实习继续完成向老师指出的后续方向:在真开发板上尝试(VisionFive2)、独立成 crate 用于多个 OS、完善驱动的文档;如果可能,对 I2C、SPI 驱动进行异步改进,看看尤予阳的工作并进行分析;尝试基于当前uart的异步实现总结异步开发范式(如果能做到)


六、工作仓库

仓库 链接
StarryOS(主仓库) https://github.com/daivy2333/StarryOS
uart_16550(串口驱动库) https://github.com/daivy2333/uart_16550
embassy(fork) https://github.com/daivy2333/embassy
学习开发仓库 https://github.com/daivy2333/2026sOsReport

各阶段学习产出文档位于 2026sOsReport 仓库下:

文档 链接
爬虫任务(任务一) 产出/任务一/docs/爬虫任务.md
绿色线程实验(任务二) 产出/任务二/green-thread/priority-extension.md
优先级爬虫实验(任务三) 产出/任务三/docs/优先级调度在满负载下的效果分析.md
异步数据库练习 产出/一些别的/异步数据库练习.md
六月第一周(周报) reports/六月第一周.md
weekly-2026-W20(周报) reports/weekly-2026-W20.md
weekly-2026-W21(周报) reports/weekly-2026-W21.md
weekly-2026-W22(周报) reports/weekly-2026-W22.md
weekly-2026-W23(周报) reports/weekly-2026-W23.md

总结

转眼训练营就到结束的时候,还记得我参加这个完全就是为了提升自己的能力,我觉得算是初步达到了目的,不管是搜寻资料、总结信息的能力,还是编程开发的分支、工具管理能力,还是形而上的抽象知识实际运用起来的动手能力,甚至是紧跟时代的 AI 工具使用的能力,这些都在解决训练营难题的过程中得到了有效的提升,就有种从只会读书的学生转变成了能解决问题的学生的感觉,参加训练营真是受益匪浅。
我个人对于ai工具的想法,是挺乐观的,因为这是大趋势,我也是某种程度上的受益者。总的而言,ai是一个放大器、加速器,能在具体任务里面帮我加倍、加速的完成任务,这本身就需要我去掌握一些深度学习、提示词工程和理解运用专业知识(比如操作系统那些知识)的能力。所以,我认为是因为我本来就有点基础有点能力,在加上训练营的训练培养,才能用好ai,才能在老师的指导下完成当前这些工作。
感觉参与训练营之后,学校的课程作业都变得异常简单。感谢操作系统训练营,感谢向老师在项目阶段给的指导。