源码地图
主线位置:不在主线上。本页说明两条主线经过的代码分在哪几层、哪些文件,哪个 feature 和
cfg决定编译进来的是哪一份实现,以及仓库用哪些测试和 CI 任务守住十几个平台。
mio 的代码分两层:上面是平台无关的公开类型,下面是 sys 里每个平台各一份的实现。两层之间没有 trait,靠的是「每个平台模块都导出同名的类型和函数」这个约定,由 cfg 在编译期选一份:
flowchart TB lib["lib.rs:入口与 pub use"] --> pub["poll.rs、event/、token.rs、interest.rs、waker.rs"] lib --> ios["io_source.rs:IoSource T"] lib -.-> netm["net/:net feature"] pub --> sysm["sys/mod.rs:按平台选"] ios --> sysm netm --> sysm sysm -->|os-poll 且 Unix| unix["sys/unix"] sysm -->|os-poll 且 Windows| win["sys/windows"] sysm -->|"os-poll 且 WASI p1"| wasi["sys/wasip1"] sysm -->|"没开 os-poll"| shell["sys/shell"] unix --> selm["selector:epoll、kqueue、event ports、poll"]
src/ 的平台分层
v1.2.4 的 src/ 一共 61 个 .rs 文件、13 451 行(含文档注释和测试)。按目录分:
| 部分 | 行数 | 内容 | 本卷 |
|---|---|---|---|
| 平台无关层 | 2 555 | poll.rs、event/、token.rs、interest.rs、waker.rs、io_source.rs、lib.rs、macros.rs | 02 核心类型 |
net/ | 2 311 | TcpListener、TcpStream、UdpSocket、UnixListener、UnixStream、UnixDatagram 的公开 API | 04 网络类型 |
sys/mod.rs | 86 | 选平台模块;debug_detail! 宏 | 本页 |
sys/unix/ | 4 755 | 下面细分 | 03 平台实现 |
sys/windows/ | 3 039 | IOCP、AFD、SockState、named pipe(named_pipe.rs 一个文件就 1 094 行) | Windows:IOCP 与 AFD |
sys/wasip1/ | 369 | WASI preview 1,用 poll_oneoff | 只提一句 |
sys/shell/ | 336 | 没开 os-poll 时的空壳 | 本页 |
平台无关层每个文件的分量:
| 文件 | 行数 | 管什么 |
|---|---|---|
poll.rs | 814 | Poll 和 Registry;一大半是文档,Poll 的文档里有一张「每个系统用哪个 selector」的表 |
io_source.rs | 311 | IoSource<T>:包住一个 std 类型,按平台实现 Source;debug 下的 SelectorId |
lib.rs | 285 | crate 文档、pub use、features 和 guide 两个纯文档模块 |
event/events.rs | 230 | Events 和它的迭代器 |
event/event.rs | 230 | Event,每个 is_* 转给 sys::event |
interest.rs | 202 | Interest:一个 NonZeroU8 |
token.rs | 140 | Token(pub usize) |
event/source.rs | 139 | event::Source trait,以及 Box<T> 的转发实现 |
macros.rs | 98 | cfg_* 宏和日志宏,见下一节 |
waker.rs | 96 | Waker,转给 sys::Waker |
event/mod.rs | 10 | 声明子模块 |
sys/unix/ 的 4 755 行:
| 文件 | 行数 | 管什么 |
|---|---|---|
selector/kqueue.rs | 919 | kqueue:register 拼 changelist,EVFILT_USER 的 wake,Event 的 Debug 要列出各平台的一长串 NOTE_* 常量 |
selector/event_ports.rs | 740 | Solaris 的 event ports(1.2.2 新增) |
selector/poll.rs | 676 | poll(2) 后备实现:用户态维护 fd 列表,模拟边沿触发 |
selector/epoll.rs | 246 | epoll,四个 selector 里最短的一个 |
selector/registered_io_source.rs | 151 | poll(2) 和 event ports 共用的 IoSourceState:do_io 遇到 WouldBlock 时重新武装 |
selector/stateless_io_source.rs | 50 | epoll 和 kqueue 共用的 IoSourceState:什么状态也不存,直接转发 |
pipe.rs | 622 | os-ext 的 pipe::new,以及 pipe 版 Waker 用的 new_raw |
net.rs | 221 | new_socket:各平台怎么建出一个非阻塞、close-on-exec 的 socket |
tcp.rs | 139 | bind、listen、accept(Linux 等用 accept4) |
udp.rs | 36 | UDP 的 bind |
uds/ | 381 | Unix 域套接字 |
waker/ | 265 | eventfd.rs 99、pipe.rs 93、single_threaded.rs 44、selector.rs 29 |
sourcefd.rs | 127 | SourceFd:让任意 fd 参与注册 |
mod.rs | 182 | syscall! 宏;按 target_os 选 selector 和 waker 文件,见后面的表 |
两条主线在 Linux 上经过的代码,几乎全在 poll.rs、io_source.rs、event/、selector/epoll.rs、waker/eventfd.rs 这几个文件里,加起来不到 2 000 行;src/ 的大部分是别的平台和文档。
Cargo.toml:三个 feature
Cargo.toml 第 31 行起:
[features]
# By default Mio only provides a shell implementation.
default = ["log"]
# Enables the `Poll` and `Registry` types.
os-poll = []
# Enables additional OS specific extensions, e.g. Unix `pipe(2)`.
os-ext = [
"os-poll",
"windows-sys/Win32_System_Pipes",
"windows-sys/Win32_Security",
]
# Enables `mio::net` module containing networking primitives.
net = []
[dependencies]
log = { version = "0.4.8", optional = true }
| feature | 默认 | 打开了什么 |
|---|---|---|
log | 是 | 可选依赖 log 的同名 feature。mio 内部的 trace!、warn!、error! 转给 log 宏;关掉时这些宏展开成 if false { let _ = (...); },只为了不报「变量未使用」 |
os-poll | 否 | Poll::new、真正的 sys::Selector 和 sys::Waker;不开时用 sys/shell |
net | 否 | mio::net 模块和 io_source.rs |
os-ext | 否 | Unix 上的 mio::unix::pipe 和 SourceFd,Windows 上的 NamedPipe;它依赖 os-poll |
默认只开 log,所以光写 mio = "1.2.4" 得到的是一个什么都做不了的 crate。tokio 依赖 mio 时关掉默认 feature(default-features = false),由自己的 net feature 打开 mio/os-poll、mio/os-ext、mio/net,所以 tokio 下面的 mio 不带 log。
macros.rs:用宏包住 cfg
源码里很少直接写 #[cfg(feature = "...")],而是用 macros.rs 里的一组宏把一串条目包起来:
/// The `os-poll` feature is enabled.
macro_rules! cfg_os_poll {
($($item:item)*) => {
$(
#[cfg(feature = "os-poll")]
#[cfg_attr(docsrs, doc(cfg(feature = "os-poll")))]
$item
)*
}
}
每个宏给包住的每一项都加上同一个 #[cfg],以及给 docs.rs 用的 doc(cfg(...))(文档里那个「Available on crate feature os-poll only」的标签)。一共六个:
| 宏 | 条件 |
|---|---|
cfg_os_poll! | feature = "os-poll" |
cfg_not_os_poll! | not(feature = "os-poll"),不加 doc(cfg) |
cfg_os_ext! | feature = "os-ext" |
cfg_net! | feature = "net" |
cfg_io_source! | any(feature = "net", all(unix, feature = "os-ext")):需要 IoSource 的场合 |
cfg_any_os_ext! | any(feature = "os-ext", feature = "net") |
后面是 log! 和包着它的 trace!、warn!、error!。平台的选择不走这些宏,而是直接写 target_os,见下一节。
不开 os-poll:Poll 还在,Poll::new 没了
sys/mod.rs 的最后一段是:
cfg_not_os_poll! {
mod shell;
pub(crate) use self::shell::*;
#[cfg(unix)]
cfg_any_os_ext! {
mod unix;
#[cfg(feature = "os-ext")]
pub use self::unix::SourceFd;
}
}
sys/shell 导出和真实平台模块同名的 Selector、Event、Events、event 模块、Waker、tcp、udp、uds,每个函数体都是 os_required!():
macro_rules! os_required {
() => {
panic!("mio must be compiled with `os-poll` to run.")
};
}
于是平台无关层照常编译:lib.rs 无条件地 pub use poll::{Poll, Registry},Poll 这个类型一直存在。被 cfg_os_poll! 包住的只有 Poll::new 一个函数:没有它就造不出 Poll,也就拿不到 Registry,register 和 Waker::new 都无从调用。
只开 net、不开 os-poll 时试两件事。依赖:
mio = { version = "=1.2.4", features = ["net"] }
第一个程序调 Poll::new:
use mio::Poll;
fn main() {
let poll = Poll::new();
println!("{poll:?}");
}
编译不过(M2 Max、rustc 1.99.0):
error[E0599]: no associated function or constant named `new` found for struct `mio::Poll` in the current scope
--> src/bin/poll_new.rs:4:20
|
4 | let poll = Poll::new();
| ^^^ associated function or constant not found in `mio::Poll`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `exp-mio-intro-nopoll` (bin "poll_new") due to 1 previous error
报错说的是「Poll 上没有 new」,不是「找不到 Poll」。第二个程序不碰 Poll,只建一个 TcpListener:
use mio::net::TcpListener;
fn main() {
println!("mio::Poll exists: {}", std::any::type_name::<mio::Poll>());
let listener = TcpListener::bind("127.0.0.1:0".parse().unwrap());
println!("bind returned: {:?}", listener.is_ok());
}
能编译,运行时 panic。跑 3 次,除了线程号都一样(cargo 注册表路径缩写成 .../):
mio::Poll exists: mio::poll::Poll
thread 'main' (2019875) panicked at .../mio-1.2.4/src/sys/shell/tcp.rs:6:5:
mio must be compiled with `os-poll` to run.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
shell/tcp.rs 第 6 行是 new_for_addr 里的 os_required!():bind 的第一步建 socket 就停了。所以 net 不开 os-poll 的组合能编译、类型都在,却一调用就 panic。这样设计的用意见下面的框。
sys/shell 和这套 cfg_* 宏来自「Introduce default API shim and feature flags for net/OS resources」(#1192),首次随 0.7.0 发布。之前依赖 mio 就会拿到全部平台实现和所有网络类型。PR 的动机是:
If a library is interested only in networking primitives but aims to provide an
other strategy for I/O source readiness polling, there is no way to disable the
provided implementation.
改法是默认只给「API shim」:类型都在,用了就 panic;Poll::new 这样的方法直接藏起来。PR 说这些宏「conditionally provide shell types that panic upon use, or hide/expose methods on types an API shim should not provide (e.g. Poll::new)」,正是上面两个程序看到的两种表现。当时的 feature 是 os-poll、os-util、tcp、udp、uds 五个。「Reduce the number of features」(#1380,0.7.6)把 tcp、udp、uds 合成 net,os-util 和 pipe 合成 os-ext,旧名字保留为过渡;0.8.0 的 #1429 删掉了旧名字,成了现在的样子。
每个平台用哪个 selector
sys/mod.rs 先按大类分:unix、target_os = "hermit" 和 WASI preview 2 及以后走 sys/unix,Windows 走 sys/windows,WASI preview 1 走 sys/wasip1。lib.rs 还拒绝其他 wasm 目标:compile_error!("This wasm target is unsupported by mio. If using Tokio, disable the net feature.")。
sys/unix 里没有 selector/mod.rs。sys/unix/mod.rs 只声明一个 mod selector;,前面叠四个 #[cfg_attr(..., path = "...")],条件成立的那个决定这个模块实际是哪个文件:
cfg_os_poll! {
#[cfg_attr(all(
not(mio_unsupported_force_poll_poll),
any(
target_os = "android",
target_os = "illumos",
target_os = "linux",
target_os = "redox",
)
), path = "selector/epoll.rs")]
// ... kqueue.rs、event_ports.rs、poll.rs 三个同样的 cfg_attr
mod selector;
pub(crate) use self::selector::*;
紧接着的下一组 cfg_attr 用同样的办法给 mod waker; 选 Waker 的实现。把两组 cfg_attr 读出来,再加上 Windows 和 WASI p1,合成一张表(不加任何 --cfg 时):
target_os | selector | 文件 | Waker |
|---|---|---|---|
| linux、android | epoll | selector/epoll.rs | eventfd |
| illumos | epoll | selector/epoll.rs | eventfd |
| redox | epoll | selector/epoll.rs | pipe |
| macos、ios、tvos、visionos、watchos | kqueue | selector/kqueue.rs | EVFILT_USER(waker/selector.rs) |
| freebsd、dragonfly、netbsd、openbsd | kqueue | selector/kqueue.rs | EVFILT_USER,创建时先 setup_waker |
| solaris | event ports | selector/event_ports.rs | event ports 的用户事件(waker/selector.rs) |
| fuchsia、hermit、espidf、nuttx | poll(2) | selector/poll.rs | eventfd |
| aix、haiku、hurd、nto、vita、cygwin | poll(2) | selector/poll.rs | pipe |
| horizon、wasi(p2 及以后) | poll(2) | selector/poll.rs | single_threaded.rs,但不导出公开的 Waker |
| wasi(p1) | poll_oneoff | sys/wasip1/mod.rs | 无 |
| windows | IOCP + AFD | sys/windows/selector.rs | sys/windows/waker.rs |
几处值得留意:
- illumos 用的是 epoll,不是 Solaris 的 event ports;
Poll文档里的表也这么写。Redox 用 epoll 但Waker走 pipe。 - 公开的
Waker在lib.rs里是#[cfg(not(any(target_os = "horizon", target_os = "wasi")))]:这两个平台上mio::Waker根本不存在。 Poll和Registry只在有单个 selector fd 的平台上实现AsRawFd/AsFd(impl AsRawFd for Poll前面的cfg排除了全部poll(2)平台):poll(2)实现没有一个能交给别人去等的 fd。- 本站的两个平台,Linux 走第一行,macOS 走第四行。
mio_unsupported_force_poll_poll 与 mio_unsupported_force_waker_pipe
上表里每个 cfg_attr 都带着 not(mio_unsupported_force_poll_poll)。这是两个不属于任何 feature 的 --cfg 标志,要通过 RUSTFLAGS 传。Cargo.toml 最后两行把它们登记进 check-cfg,免得编译器报「unexpected cfg」:
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(mio_unsupported_force_poll_poll)', 'cfg(mio_unsupported_force_waker_pipe)'] }
README 的「Unsupported flags」一节说明了用途和态度:
Current flags:
* `mio_unsupported_force_poll_poll`, uses an implementation based on `poll(2)`
for `mio::Poll`.
* `mio_unsupported_force_waker_pipe`, uses an implementation based on `pipe(2)`
for `mio::Waker`.
**Again, Mio does not officially supports this**. Furthermore these flags may
disappear in the future.
mio_unsupported_force_poll_poll 打开时:所有平台的 selector 都换成 selector/poll.rs;kqueue 平台的 Waker 换成 pipe(sys/unix/mod.rs 的注释:「kqueue(2) based waker doesn't work with poll(2)」);Linux 上 Waker 仍是 eventfd。上面的最小程序用 RUSTFLAGS="... --cfg mio_unsupported_force_poll_poll" 重新编译,在 Linux 容器里跑(Linux 6.12.28(Apple container 里的 Alpine,aarch64 musl 静态链接)、rustc 1.99.0,strace -f -e trace=epoll_create1,epoll_ctl,epoll_pwait,ppoll,poll,eventfd2,pipe2,accept4,read,write,标准输出重定向掉)。3 次除端口和栈地址外逐行相同,其中一次:
ppoll([{fd=0, events=0}, {fd=1, events=0}, {fd=2, events=0}], 3, {tv_sec=0, tv_nsec=0}, NULL, 8) = 0 (Timeout)
eventfd2(0, EFD_CLOEXEC|EFD_NONBLOCK) = 3
write(3, "\1\0\0\0\0\0\0\0", 8) = 8
read(3, "\1\0\0\0\0\0\0\0", 8) = 8
ppoll([{fd=3, events=POLLIN}, {fd=4, events=POLLIN|POLLRDHUP}], 2, NULL, NULL, 8) = 1 ([{fd=4, revents=POLLIN}])
write(1, "poll 1: token=Token(7) readable="..., 52) = 52
accept4(4, {sa_family=AF_INET, sin_port=htons(46236), sin_addr=inet_addr("127.0.0.1")}, [128 => 16], SOCK_CLOEXEC|SOCK_NONBLOCK) = 6
write(1, "accept: ok\n", 11) = 11
accept4(4, 0xffffd21b7538, [128], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
write(3, "\1\0\0\0\0\0\0\0", 8) = 8
read(3, "\1\0\0\0\0\0\0\0", 8) = 8
write(1, "accept: WouldBlock\n", 19) = 19
strace: Process 4 attached
[pid 3] ppoll([{fd=3, events=POLLIN}, {fd=4, events=POLLIN|POLLRDHUP}], 2, NULL, NULL, 8 <unfinished ...>
[pid 4] write(3, "\1\0\0\0\0\0\0\0", 8 <unfinished ...>
[pid 3] <... ppoll resumed>) = 1 ([{fd=3, revents=POLLIN}])
[pid 4] <... write resumed>) = 8
[pid 3] read(3, "\1\0\0\0\0\0\0\0", 8) = 8
[pid 3] write(1, "poll 2: token=Token(9) readable="..., 52) = 52
[pid 4] +++ exited with 0 +++
write(3, "\1\0\0\0\0\0\0\0", 8) = 8
read(3, "\1\0\0\0\0\0\0\0", 8) = 8
+++ exited with 0 +++
程序的标准输出和 epoll 版一样,但系统调用完全换了一套:没有一个 epoll_*;register 没有对应的内核调用,fd 列表由 mio 在用户态维护,每次 poll 都把整个列表交给 ppoll;整个进程只有一个 eventfd(fd 3),既用来叫醒正在 ppoll 的线程,也是用户 Waker 的事件来源;accept4 返回 EAGAIN 之后紧跟着一对 write(3) / read(3),那是 do_io 在把 listener 重新放回关注集合。第一行 ppoll 查的是 fd 0、1、2,那是 Rust 标准库启动时检查标准输入输出是否打开,和 mio 无关。这些机制在 poll(2) 后备一章逐项讲。
这个标志对上层也有影响。tokio 的 PollEvented::poll_read 有一个「读不满就说明已经读空」的优化,只在 epoll 和 kqueue 上成立,它的 cfg 里就写着 not(mio_unsupported_force_poll_poll);来历见从 TcpStream::read 到挂起。tokio 的 Cargo.toml 也把这个 cfg 登记进了自己的 check-cfg。
mio_unsupported_force_waker_pipe 只换 Waker:eventfd 平台和 kqueue 平台都改用 pipe 版。
tests/:17 个测试文件
tests/ 下每个文件编译成一个独立的测试程序。几乎每个文件开头都有 #![cfg(all(feature = "os-poll", feature = "net"))],所以要 cargo test --all-features 才跑得到:
| 文件 | 行数 | #[test] 数 | 测什么 |
|---|---|---|---|
udp_socket.rs | 1 203 | 33 | UdpSocket;Miri 下整个文件不编译(「Miri doesn't support UDP sockets」) |
tcp_stream.rs | 1 099 | 25 | TcpStream:IPv4 / IPv6、TTL 与 nodelay、shutdown、peek、注销后不再有事件、断开时的 hup 事件 |
tcp.rs | 983 | 19 | listener 和 stream 配合:accept、connect、读写、关闭、各种错误、注销后不再有事件 |
poll.rs | 729 | 17 | Poll 和 Registry:零超时、WouldBlock 之后重新就绪、poll 期间从别的线程注册、错误的注册 |
unix_stream.rs | 680 | 14 | UnixStream |
unix_datagram.rs | 429 | 11 | UnixDatagram |
tcp_listener.rs | 308 | 11 | TcpListener |
unix_listener.rs | 283 | 9 | UnixListener;1.2.3 的 #1998 在这里加了「排空之后第二个连接还能收到通知」的回归测试 |
registering.rs | 246 | 5 | register / reregister / deregister 的约定:不 poll 就改 interest、注销后再注册 |
aio.rs | 228 | 2 | Interest::AIO,只在 FreeBSD 和 DragonFly 上编译 |
unix_pipe.rs | 227 | 5 | os-ext 的 pipe |
waker.rs | 162 | 7 | Waker:同线程、跨线程的单次和多次唤醒 |
close_on_drop.rs | 131 | 1 | 服务端 drop 掉连接之后,客户端收到可读事件并读到 0 字节 |
regressions.rs | 130 | 3 | 具体 issue 的回归测试(issue_776、issue_1205 等) |
events.rs | 53 | 3 | Events |
interest.rs | 52 | 4 | Interest 的位运算;文件开头没有 cfg,默认 feature 下也会跑 |
win_named_pipe.rs | 787 | 16 | Windows named pipe;1.2.4 的 #2014 加了 340 多行 |
util/mod.rs | 397 | — | 公用工具:init_with_poll、expect_events、expect_no_events、assert_would_block、assert_socket_non_blocking 等 |
发布的包里没有 tests/:Cargo.toml 的 include 只列了 Cargo.toml、LICENSE、README.md、CHANGELOG.md、src/**/*.rs、examples/**/*.rs。想对照本页读测试,要看 git 仓库。
CI:哪些平台真跑测试,哪些只编译
.github/workflows/ci.yml 在 v1.2.4 有 18 个任务。按「在哪个平台上跑了什么」整理:
| 任务 | 平台 | 做什么 |
|---|---|---|
Test | ubuntu、macos、windows | cargo hack check --feature-powerset;debug 和 release 各 cargo test --all-features |
TestPoll | ubuntu | RUSTFLAGS="--cfg mio_unsupported_force_poll_poll" 下跑全部测试 |
TestWakerPipe | ubuntu | --cfg mio_unsupported_force_waker_pipe 下跑全部测试 |
MinimalVersions | ubuntu、macos、windows | cargo update -Zminimal-versions 之后跑测试 |
MSRV | ubuntu、macos、windows | 1.71 上只 cargo check --all-features(注释:好让测试能用新特性) |
TestWASI | ubuntu + wasmtime | wasm32-wasip2 上跑测试 |
Nightly、Clippy、Docs、Rustfmt | ubuntu | nightly 测试、clippy、cargo doc 不许警告、rustfmt --check |
Miri | ubuntu | 集成测试和文档测试在 Miri 下跑,再用 poll(2) selector 跑一遍集成测试 |
CheckTier1 | 16 个目标 | cargo hack check -Z build-std --feature-powerset:Apple 五个系统、Android、FreeBSD、Linux gnu / musl、NetBSD、OpenBSD、Windows msvc |
CheckTier2 | 24 个目标 | 同上:Fuchsia、Hermit、QNX、Redox、Hurd、ESP-IDF、Solaris、illumos、Haiku、DragonFly、3DS、PS Vita、wasm32-wasip1 等 |
Sanitizer | ubuntu | address、leak、memory、thread 四种 sanitizer 跑 make test_sanitizer |
TestFreeBSD | FreeBSD 虚拟机 | 14.4 和 15.0 的 x86_64,14.4 的 i686 |
TestSolaris | Solaris 11.4 虚拟机 | 跑全部测试 |
Wine | ubuntu 22.04 + Wine | x86_64-pc-windows-gnu 在 Wine 里跑测试 |
合并 PR 要求通过的 Passed 任务只依赖其中 11 个:Test、TestPoll、MinimalVersions、MSRV、Nightly、Clippy、Docs、Rustfmt、Miri、CheckTier1、TestFreeBSD。TestWakerPipe、TestWASI、Sanitizer、TestSolaris、Wine 和 CheckTier2 失败不挡合并;Solaris 是 1.2.3 发版前的提交「Remove testing Solaris from CI pass requirement」从这个列表里拿掉的。
也就是说,真正跑测试的平台是 Linux、macOS、Windows、FreeBSD、Solaris、Wine 和 WASI p2;其余平台只保证编译通过。本站的两个平台都在第一行里,每个 PR 都跑。
小结
src/13 451 行:平台无关层 2.6k、net/2.3k、sys/unix4.8k(四个 selector 占 2.6k)、sys/windows3.0k。Linux 上两条主线经过的代码不到 2 000 行。- 两层之间没有 trait:每个平台模块导出同名的
Selector、Event、Events、Waker、IoSourceState,sys/unix/mod.rs用#[cfg_attr(..., path = "...")]在编译期挑一个文件当mod selector。 - feature 默认只有
log;os-poll决定Poll::new在不在,net给网络类型,os-ext给pipe、SourceFd、NamedPipe。不开os-poll时类型都在,Poll::new编译报 E0599,网络类型落到sys/shell,一调用就 panic「mio must be compiled withos-pollto run.」(#1192,0.7.0 起)。tokio 用default-features = false加os-poll、os-ext、net。 - Linux、Android、illumos 用 epoll,Apple 系统和四个 BSD 用 kqueue,Solaris 用 event ports,其余 Unix 用
poll(2),Windows 用 IOCP + AFD。--cfg mio_unsupported_force_poll_poll把所有平台换成poll(2),strace 里epoll_*全部变成ppoll。 - CI 在 Linux、macOS、Windows、FreeBSD 上跑测试并挡合并,
poll(2)后备和 Miri 也挡合并;Solaris、Wine、WASI 跑但不挡;CheckTier1/CheckTier2的 40 个目标只做编译检查。