跳转到正文
zeno's blog
返回

mio(一):Rust 异步生态的 Reactor 基石

专题: mio

Table of contents

Open Table of contents

TL;DR

mio 是 Rust 生态中对操作系统 I/O 事件通知机制(epoll/kqueue/IOCP)的薄封装,由 Carl Lerche 创建,当前版本 1.2.1。它刻意只做一件事:事件就绪通知。没有 timer、没有 buffer、没有 async/await。tokio 和整个 Rust 异步生态建在 mio 之上。它与 C++ Asio 的核心差异:Asio 选 Proactor 是因为要向 IOCP 对齐;mio 选 Reactor 是因为 Rust 语言特性(ownership + async/await)让 Reactor 模式更自然,而 IOCP 的适配代价由 mio 在内部消化。


从问题出发:Rust 需要什么样的 I/O 层

2014 年,Rust 还没有稳定的 1.0。Carl Lerche(后来成为 tokio 的核心作者)面对的问题是:

mio 诞生就是为了填这个空白——不是做一个完整的异步框架,而是做 最薄的、零开销的、跨平台的事件通知抽象

mio 的设计哲学:Minimal Abstraction over OS Primitives

mio 的官方定义:

A fast, low-level I/O library for Rust focusing on non-blocking APIs and event notification for building high performance I/O apps with as little overhead as possible over the OS abstractions.

关键词是 as little overhead as possible。mio 的态度是:

  1. 不隐藏操作系统的语义:epoll 的边缘触发就暴露为边缘触发,不替你做电平触发的模拟
  2. 不做 buffer 管理:你的 buffer 你自己管,mio 不碰
  3. 不做 timer:定时器不是 I/O 事件通知,不属于 mio 的职责
  4. 不做 async/await 集成:Future/Waker 是上层(tokio)的事
  5. 不做连接管理:accept 了一个连接,怎么管理它的生命周期,是你的事

这和 Asio 的哲学形成鲜明对比:Asio 自称 toolkit,提供 timer、strand、buffer、协程支持;mio 只是一块 砖头——你用它砌墙,但它绝不帮你画图纸。

为什么 Rust 选 Reactor 而 Asio 选 Proactor

这是最核心的设计分歧,理解它才能理解两个生态的思路差异。

回顾:Reactor vs Proactor

维度Reactor (mio)Proactor (Asio)
通知内容「fd 准备好了,你可以读了」「读操作完成了,数据在 buffer 里」
I/O 执行者应用程序自己调 read()/write()OS 内核完成 I/O
缓冲区事件到达后才需要发起操作时就要提交
原生 OS 支持Linux epoll, macOS kqueueWindows IOCP

Asio 选 Proactor 的理由(已在 asio 文档中详述)

IOCP 是原生 Proactor,无法自然暴露为 Reactor API。Asio 向 Proactor 对齐,在 Linux/macOS 上用 Reactor 模拟 Proactor(收到就绪通知后内部立即执行 I/O,封装为完成事件)。

mio 选 Reactor 的理由

  1. Rust 的 ownership 系统让 Reactor 更安全

    Proactor 模式要求在发起异步操作时提交 buffer,操作系统在后台写入这个 buffer。在 C++ 中,这导致 buffer 的生命周期管理极其复杂——你必须保证 buffer 在 OS 完成写入之前不被释放。Asio 用 shared_ptr 和各种生命周期技巧来解决这个问题。

    Reactor 模式没有这个问题:事件通知到达 → 你自己在当前上下文分配 buffer → 你自己调 read() → buffer 的 ownership 完全在你手里。Rust 的 borrow checker 天然适配这个模型。

  2. async/await + Reactor = 自然的 Proactor 语义

    // tokio 在 mio Reactor 之上构建出的使用体验
    let n = stream.read(&mut buf).await?;
    // 看起来像 Proactor(一步完成),
    // 但底层是 Reactor(先等就绪,再读取)

    Rust 的 async/await 让编译器把 Reactor 的两步操作(等就绪 → 执行 I/O)编译成一个 state machine,用户写出来的代码看起来就像 Proactor。语言层面的抽象能力让底层 Reactor 的复杂性消失了。

  3. IOCP 上模拟 Reactor 的代价可以内部消化

    mio 在 Windows 上用 AFD(Auxiliary Function Driver)+ IOCP 来模拟 Reactor 语义。这确实有开销(后面详述),但这个开销被封装在 mio 内部,对上层透明。而且 Windows 的服务端高性能场景本身就少——大多数 Rust 异步服务跑在 Linux 上。

  4. 不需要跨操作 buffer 所有权转移

    Proactor 需要「把 buffer 的控制权交给 OS,等 OS 用完了再还回来」。这在 Rust 的 ownership 模型中极其别扭——你要么用 unsafe,要么引入复杂的 lifetime 标注。Reactor 完全避免了这个问题。

对比总结

决策因素C++ Asio → ProactorRust mio → Reactor
OS 底线必须高效支持 Windows IOCPLinux 是主战场,Windows 可以妥协
语言能力没有 async/await(直到 C++20),Proactor 语义更直接async/await 让 Reactor 也能写出 Proactor 体验
内存安全手动管理 buffer 生命周期ownership 天然契合 Reactor
设计定位完整 toolkit最小化砖头,上层自己搭

mio 的版本演进

mio 0.6(2016.09)

mio 0.7(大规模重构)

mio 0.8(清理和稳定)

mio 1.0 → 1.2.1(当前)

演进的主题

每次大版本都在 做减法:删 timer → 删 channel → 删 PollOpt → 删 Ready → 删 TcpSocket。mio 的演进方向是越来越薄、越来越专注于纯粹的事件通知。这和 Asio 的演进方向相反——Asio 越来越厚(加 strand、加协程支持、加 cancellation slot)。

mio 在 Rust 异步生态中的位置

┌───────────────────────────────────────────┐
│           应用层 (axum, actix-web, ...)    │
├───────────────────────────────────────────┤
│           tokio / async-std               │
│  (task scheduler, timer, async/await,     │
│   buffered I/O, channel, sync primitives) │
├───────────────────────────────────────────┤
│                  mio                      │
│  (Poll, Token, Interest, Events, Waker,   │
│   TcpListener, TcpStream, UdpSocket)      │
├───────────────────────────────────────────┤
│           OS Kernel                       │
│  (epoll / kqueue / IOCP)                  │
└───────────────────────────────────────────┘

mio 只负责中间那一层——把 OS 的事件通知 API 包成 Rust 类型安全的接口。上面所有的东西(定时器、任务调度、async/await 集成、buffered I/O)都不是 mio 的事。

与 C++ 开发者的对应关系

C++ Asio 概念Rust 对应备注
io_contextmio::Poll + tokio runtimemio 只有 Poll,tokio 加上任务调度
async_read / async_writemio 没有,tokio 的 AsyncRead / AsyncWritemio 层面只有 non-blocking read()/write()
strand不需要Rust 的 ownership 保证了单线程内的安全
steady_timermio 没有,tokio 的 tokio::time::sleepmio 刻意不做 timer
ip::tcp::socketmio::net::TcpStreammio 提供但功能最小化
ip::tcp::acceptormio::net::TcpListener同上
completion handler (callback)Token(整数标识)mio 不用回调,原因在下一篇详述

分享这篇文章:

上一篇
mio(二):核心架构-Poll、Token、Interest、Events
下一篇
Go 服务:从接口契约到分层实现