Token、Interest 与 Event
主线位置:两条主线上流动的数据。注册主线把
Token和Interest交给内核(Interest变成 epoll 的事件掩码或 kqueue 的过滤器,Token存进内核的那一项里);事件主线从内核取回一批平台事件结构,装在Events里,用户通过Event的方法读出 token 和就绪状态。调用这两步的register和poll在 Poll 与 Registry。
flowchart TB tok["Token(usize)"] --> ctl["epoll_ctl / kevent"] int["Interest 位集合"] -->|"掩码或过滤器"| ctl ctl --> kern["内核里的一项:u64 / udata"] kern -->|"epoll_wait / kevent"| evs["Events:Vec 平台结构"] evs --> ev["Event:透明包装"] ev --> tk2["token()"] ev --> flags["is_readable() 等:查标志位"]
涉及的文件都很短:token.rs 140 行(几乎全是文档)、interest.rs 202 行、event/event.rs 和 event/events.rs 各 230 行。真正干活的是 epoll 和 kqueue selector 里各自的 event 模块。
Token:一个 usize,原样进出内核
Token 的全部定义:
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Token(pub usize);
impl From<Token> for usize {
fn from(val: Token) -> usize {
val.0
}
}
字段是 pub 的,没有保留值,没有校验。它怎么进内核,看两个 selector 的 register。epoll(register):
let mut event = libc::epoll_event {
events: interests_to_epoll(interests),
u64: usize::from(token) as u64,
#[cfg(target_os = "redox")]
_pad: 0,
};
epoll_event 的第二个字段是一个 64 位的联合体,内核不解释它,事件回来时原样带回。mio 用的是其中的 u64。kqueue 用 kevent 的 udata(一个 *mut c_void),由 kevent! 宏填进去:
macro_rules! kevent {
($id: expr, $filter: expr, $flags: expr, $data: expr) => {
libc::kevent {
ident: $id as libc::uintptr_t,
filter: $filter as Filter,
flags: $flags,
udata: $data as UData,
..unsafe { mem::zeroed() }
}
};
}
register 调它时传的 $data 是 token.0。取回来的两个函数各一行,epoll 的是 Token(event.u64 as usize),kqueue 的是 Token(event.0.udata as usize)。usize 放进 u64 或指针宽度的 udata 都不会丢位。
所以 mio 里没有 fd 到 token 的表,也没有 token 到 source 的表。用户挑 token、自己维护 token 到 source 的映射(文档推荐 slab);tokio 直接把 ScheduledIo 的地址当 token(见 tokio 卷的反应器)。
实验:在 strace 和 kevent 里看见 token
src/bin/token.rs 注册两个 source,一个用 Token(42),一个用占满 64 位的 Token(0x1234_5678_9abc_def0),然后 poll 两次。raw(e) 是实验 crate 里的一个辅助函数:Event 是 #[repr(transparent)] 包着平台结构的(下一节),所以可以把 &Event 转成 &libc::epoll_event 或 &libc::kevent 读出原始字段。这依赖私有布局,只用于观察。
use std::time::Duration;
use exp_mio_poll::raw;
use mio::net::{TcpListener, TcpStream};
use mio::{Events, Interest, Poll, Token};
fn main() -> std::io::Result<()> {
let mut poll = Poll::new()?;
let mut listener = TcpListener::bind("127.0.0.1:0".parse().unwrap())?;
poll.registry().register(&mut listener, Token(42), Interest::READABLE)?;
let mut stream = TcpStream::connect(listener.local_addr()?)?;
let big = Token(0x1234_5678_9abc_def0);
poll.registry().register(&mut stream, big, Interest::READABLE | Interest::WRITABLE)?;
let mut events = Events::with_capacity(8);
for round in 1..=2 {
poll.poll(&mut events, Some(Duration::from_millis(100)))?;
for e in &events {
println!("poll #{round}: Token({:#x}) {}", e.token().0, raw(e));
}
}
Ok(())
}
Linux 6.12.28(Apple container 里的 Alpine,aarch64 musl 静态链接)、rustc 1.99.0,strace -e trace=epoll_ctl,epoll_pwait:
epoll_ctl(3, EPOLL_CTL_ADD, 4, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x2a}) = 0
epoll_ctl(3, EPOLL_CTL_ADD, 5, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data=0x123456789abcdef0}) = 0
epoll_pwait(3, [{events=EPOLLIN, data=0x2a}, {events=EPOLLOUT, data=0x123456789abcdef0}], 8, 100, NULL, 8) = 2
poll #1: Token(0x2a) events=IN data=0x2a
poll #1: Token(0x123456789abcdef0) events=OUT data=0x123456789abcdef0
epoll_pwait(3, [], 8, 100, NULL, 8) = 0
+++ exited with 0 +++
M2 Max、macOS、rustc 1.99.0,--release,跑了 3 次,输出相同:
poll #1: Token(0x123456789abcdef0) filter=WRITE flags=CLEAR fflags=0 data=146988 udata=0x123456789abcdef0
poll #2: Token(0x2a) filter=READ flags=CLEAR fflags=0 data=1 udata=0x2a
data=0x2a 就是 42。epoll_ctl 写进去什么,epoll_pwait 就带回什么,udata 同理。两个平台上事件的分批不同(Linux 一次拿到两个,macOS 分两次),这和 token 无关,是两边内核什么时候认为「监听 socket 有连接」的时序不同。kqueue 事件里的 data 是过滤器自己的附加信息:EVFILT_WRITE 的 146988 是发送缓冲区的剩余空间,监听 socket 上 EVFILT_READ 的 1 是待 accept 的连接数,mio 不用它们。
Interest:非空的位集合
Interest 是一个 NonZeroU8,每种关注占一位:
#[derive(Copy, PartialEq, Eq, Clone, PartialOrd, Ord)]
pub struct Interest(NonZeroU8);
// These must be unique.
const READABLE: u8 = 0b0001;
const WRITABLE: u8 = 0b0010;
// The following are not available on all platforms.
const AIO: u8 = 0b0100;
const LIO: u8 = 0b1000;
const PRIORITY: u8 = 0b10000;
用 NonZeroU8 而不是 u8,表达的是「空的关注集合不存在」:不关心任何事件的注册没有意义,要停止接收就 deregister。这条约束落在 API 上:
- 公开的常量只有单个位,
add(BitOr也转给它)是两个非零值按位或,结果不可能为零,所以里面可以放心地NonZeroU8::new_unchecked。add是const fn,可以写const RW: Interest = Interest::READABLE.add(Interest::WRITABLE);。 remove可能减成零,所以返回Option<Interest>:
pub fn remove(self, other: Interest) -> Option<Interest> {
NonZeroU8::new(self.0.get() & !other.0.get()).map(Interest)
}
Debug实现末尾有一句debug_assert!(one, "printing empty interests");,正常途径构造不出空集合,这句只是防御。
NonZeroU8 还顺带让 Option<Interest> 也只占一个字节(零值留给 None)。src/bin/interest.rs 在两个平台上的输出相同:
size_of::<Interest>() = 1
size_of::<Option<Interest>>() = 1
READABLE | WRITABLE = READABLE | WRITABLE
rw.remove(READABLE) = Some(WRITABLE)
rw.remove(rw) = None
READABLE < WRITABLE = true
最后一行来自派生的 Ord:按那个 u8 比较,没有别的含义。
平台专有的三位
五个位常量在所有平台上都定义,公开的关联常量却按平台 cfg:
| 常量 | 位 | 在哪些 target_os 上存在 | selector 里谁看它 |
|---|---|---|---|
Interest::READABLE | 0b0001 | 全部 | 各平台 register |
Interest::WRITABLE | 0b0010 | 全部 | 各平台 register |
Interest::AIO | 0b0100 | dragonfly、freebsd、ios、macos、tvos、visionos、watchos | kqueue 的 register 不看;Event::is_aio 认 EVFILT_AIO 事件 |
Interest::LIO | 0b1000 | freebsd | kqueue 的 register 不看;Event::is_lio 认 EVFILT_LIO 事件 |
Interest::PRIORITY | 0b10000 | linux、android | epoll 的 interests_to_epoll,加 EPOLLPRI |
is_aio、is_lio、is_priority 这三个查询方法在所有平台上都有,只是在没有对应常量的平台上永远返回 false。常量本身被 cfg 掉了,写错平台是编译错误而不是运行时的静默失效。src/bin/platform_interest.rs 里同时用了 Interest::PRIORITY 和 Interest::AIO,在 macOS 上编译:
error[E0599]: no associated function or constant named `PRIORITY` found for struct `Interest` in the current scope
--> src/bin/platform_interest.rs:4:42
|
4 | let _ = Interest::READABLE | Interest::PRIORITY;
| ^^^^^^^^ associated function or constant not found in `Interest`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `exp-mio-poll` (bin "platform_interest") due to 1 previous error
交叉编译到 aarch64-unknown-linux-musl:
error[E0599]: no associated function or constant named `AIO` found for struct `Interest` in the current scope
--> src/bin/platform_interest.rs:5:42
|
5 | let _ = Interest::READABLE | Interest::AIO;
| ^^^ associated function or constant not found in `Interest`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `exp-mio-poll` (bin "platform_interest") due to 1 previous error
Interest 到了内核变成什么
epoll 这边是一个函数,interests_to_epoll:
fn interests_to_epoll(interests: Interest) -> u32 {
let mut kind = EPOLLET;
if interests.is_readable() {
kind = kind | EPOLLIN | EPOLLRDHUP;
}
if interests.is_writable() {
kind |= EPOLLOUT;
}
if interests.is_priority() {
kind |= EPOLLPRI;
}
kind as u32
}
EPOLLET 无条件加上,这就是「mio 是边沿触发」在 Linux 上的全部实现;关注可读时顺带要 EPOLLRDHUP,下面 is_read_closed 要用它。kqueue 这边没有掩码,register 只看 is_writable() 和 is_readable(),各自对应一个 EVFILT_WRITE、EVFILT_READ 过滤器,带 EV_CLEAR(kqueue 的边沿触发)。AIO / LIO 位在这里被忽略。为什么要 EPOLLRDHUP、kqueue 一次提交两项时怎么拿错误,见 epoll 和 kqueue。
is_priority 比 Interest::PRIORITY 早出现了好几个版本Event::is_priority 和 epoll 的「查 EPOLLPRI」早就在了,但注册时没有任何办法让内核报告 EPOLLPRI。issue #1645(「A way to add EPOLLPRI event type to the epoll selector」)的提问者在用 Video4Linux2 时需要等 POLLPRI,直接问:「Why is the is_priority function there? It is of no use at the moment, right?」PR #1647(「Add Interest::PRIORITY」,首次发布于 0.8.6)加上了 PRIORITY 位、Linux / Android 上的公开常量和 interests_to_epoll 里的 if interests.is_priority() { kind |= EPOLLPRI; },说明是「To trigger Event::is_priority on Linux and Android.」同一段说明还写了 kqueue 为什么没有:「In the future we might want to include EV_OOBAND for kqueue, but that seems to be Apple only (FreeBSD and OpenBSD don't implement it at least).」到 1.2.4,kqueue 的 is_priority 仍然是「kqueue doesn't have priority indicators」并返回 false。
Event:平台结构的透明包装
Event 只有一个字段:
#[derive(Clone)]
#[repr(transparent)]
pub struct Event {
inner: sys::Event,
}
sys::Event 在 epoll 上是 pub type Event = libc::epoll_event;,在 kqueue 上是一个同样 #[repr(transparent)] 的新类型 Event(libc::kevent)。Events 内部的 Vec 存的就是这些平台结构,迭代时 from_sys_event_ref 把 &sys::Event 直接转成 &Event:
pub(crate) fn from_sys_event_ref(sys_event: &sys::Event) -> &Event {
unsafe {
// This is safe because the memory layout of `Event` is
// the same as `sys::Event` due to the `repr(transparent)` attribute.
&*(sys_event as *const sys::Event as *const Event)
}
}
内核写进 Vec 的那块内存就是用户读的 Event,取事件的路上没有任何转换或拷贝。代价是每次调用 is_readable() 之类的方法都要现查一次标志位,而它们都只是几次按位与。
Event 直到 1.1.0 才是 Send1.1.0 之前,kqueue 的 sys::Event 就是 pub type Event = libc::kevent;。kevent 的 udata 字段是 *mut c_void,裸指针既不 Send 也不 Sync,于是 mio::event::Event 在 macOS / BSD 上不能跨线程传递,而在 Linux(epoll_event 只有整数字段)上可以。issue #1905 报告的正是这种情况:一个事件循环库在 Linux 和 Windows 上没问题,到 macOS 上编译报「*mut c_void cannot be sent between threads safely」。PR #1906(「fix: make Event thread-safe with kevent」,首次发布于 1.1.0,CHANGELOG 写作「Make Event Send and Sync on platforms using kqueue」)把它换成 #[repr(transparent)] 的新类型 Event(libc::kevent),加上 unsafe impl Send / unsafe impl Sync 和 Deref,event 模块里的 event.filter 等也都改成了 event.0.filter。理由和文件里 Events 早就有的那段注释一样:mio 只把 udata 当整数(token)读,从不解引用。
每个 is_* 方法查哪些位
Event 的方法都是一行转发到 sys::event::*。两个 selector 的实现(epoll 的 event 模块,kqueue 的 event 模块)整理成表:
| 方法 | epoll(epoll_event.events) | kqueue(kevent) |
|---|---|---|
token() | u64 as usize | udata as usize |
is_readable() | EPOLLIN 或 EPOLLPRI | filter == EVFILT_READ;在 FreeBSD 和 Apple 各平台上 EVFILT_USER 也算 |
is_writable() | EPOLLOUT | filter == EVFILT_WRITE |
is_error() | EPOLLERR | flags 含 EV_ERROR;或含 EV_EOF 且 fflags != 0 |
is_read_closed() | EPOLLHUP;或 EPOLLIN 与 EPOLLRDHUP 同时 | filter == EVFILT_READ 且含 EV_EOF |
is_write_closed() | EPOLLHUP;或 EPOLLOUT 与 EPOLLERR 同时;或恰好只有 EPOLLERR | filter == EVFILT_WRITE 且含 EV_EOF |
is_priority() | EPOLLPRI | 恒为 false |
is_aio() | 恒为 false(「Not supported in the kernel, only in libc.」) | filter == EVFILT_AIO(DragonFly、FreeBSD、Apple 各平台) |
is_lio() | 恒为 false | filter == EVFILT_LIO(仅 FreeBSD) |
几处值得对照源码看的地方:
- kqueue 一个事件只有一个方向。 一个
kevent只属于一个过滤器,is_readable和is_writable不可能同时为真;同一个 socket 又可读又可写,就是两个事件、同一个 token。epoll 一个事件可以同时带EPOLLIN|EPOLLOUT。 EVFILT_USER算可读。 这是给Waker用的,注释说「On platforms that useeventfdor a unix pipe it will emit a readable event so we'll fake that here as well.」见 Waker。is_read_closed在 epoll 上要EPOLLIN和EPOLLRDHUP同时出现,只有EPOLLRDHUP不算;EPOLLHUP单独就算。注册时 mio 只在关注可读时要EPOLLRDHUP,没有关注可读的 source 只能靠EPOLLHUP。is_write_closed的「恰好只有EPOLLERR」 一支是 0.7.1 加的(#1350,「Report EPOLLERR as is_write_closed」)。提交说明:管道读端关闭时「we've seen a number of event variations ... including EPOLLHUP and EPOLLOUT|EPOLLERR. And now we've also seen just EPOLLERR, without EPOLLOUT.」它让 tokio 的一个进程测试失败过。- 文档和代码对不上的一处。
Event::is_error文档的表格写 kqueue 检查「EV_ERRORandEV_EOFwithfflagsset to0」,代码是EV_EOF且fflags != 0。以代码为准:EV_EOF时fflags里放的是 socket 的错误码,非零才是错误;下面实验里的 54(ECONNRESET)和 61(ECONNREFUSED)就是这样被判成error的。
所有「closed」方法的文档都写着「best effort」和「it is guaranteed that false-positives will not occur」:可以不报,不会误报。
实验:几种连接状态下的标志位
src/bin/flags.rs 每种场景新建一对连接,服务端的 mio TcpStream 用 READABLE | WRITABLE 注册,先 poll 一次把初始的可写事件取走,再制造一个状态变化,打印下一次 poll 的原始标志位(raw,同上一个实验)和各个 is_* 的结果(bools 把为真的方法名列出来):
use std::io::Write;
use std::net::Shutdown;
use std::os::fd::AsRawFd;
use std::time::Duration;
use exp_mio_poll::{bools, raw};
use mio::net::TcpStream;
use mio::unix::pipe;
use mio::{Events, Interest, Poll, Token};
const RW: Interest = Interest::READABLE.add(Interest::WRITABLE);
fn drain(poll: &mut Poll, title: &str) {
let mut events = Events::with_capacity(16);
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
println!("{title}");
if events.is_empty() {
println!(" (no events)");
}
for e in &events {
println!(" {} => {}", raw(e), bools(e));
}
}
// registered server-side stream + std client, after the initial writable event
fn setup() -> (Poll, TcpStream, std::net::TcpStream) {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let c = std::net::TcpStream::connect(l.local_addr().unwrap()).unwrap();
let (s, _) = l.accept().unwrap();
s.set_nonblocking(true).unwrap();
let mut s = TcpStream::from_std(s);
let mut poll = Poll::new().unwrap();
poll.registry().register(&mut s, Token(1), RW).unwrap();
let mut events = Events::with_capacity(16);
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
(poll, s, c)
}
fn main() -> std::io::Result<()> {
println!("os={}", std::env::consts::OS);
let (mut poll, _s, mut c) = setup();
c.write_all(b"hi")?;
drain(&mut poll, "1. peer writes 2 bytes");
let (mut poll, _s, c) = setup();
c.shutdown(Shutdown::Write)?;
drain(&mut poll, "2. peer shutdown(Write) (FIN)");
let (mut poll, _s, c) = setup();
let linger = libc::linger { l_onoff: 1, l_linger: 0 };
unsafe {
libc::setsockopt(c.as_raw_fd(), libc::SOL_SOCKET, libc::SO_LINGER,
&linger as *const _ as *const libc::c_void, size_of::<libc::linger>() as libc::socklen_t);
}
drop(c);
drain(&mut poll, "3. peer closes with SO_LINGER=0 (RST)");
let (mut poll, s, _c) = setup();
s.shutdown(Shutdown::Write)?;
drain(&mut poll, "4. local shutdown(Write)");
let (mut poll, s, _c) = setup();
s.shutdown(Shutdown::Both)?;
drain(&mut poll, "5. local shutdown(Both)");
// 6. connect to a port nobody listens on
let port = {
let l = std::net::TcpListener::bind("127.0.0.1:0")?;
l.local_addr()?
};
let mut poll = Poll::new()?;
let mut s = TcpStream::connect(port)?;
poll.registry().register(&mut s, Token(1), RW)?;
drain(&mut poll, "6. connect to a closed port");
// 7/8. pipes
let mut poll = Poll::new()?;
let (tx, mut rx) = pipe::new()?;
poll.registry().register(&mut rx, Token(1), Interest::READABLE)?;
drop(tx);
drain(&mut poll, "7. pipe: write end closed, reader registered READABLE");
let mut poll = Poll::new()?;
let (mut tx, rx) = pipe::new()?;
poll.registry().register(&mut tx, Token(1), Interest::WRITABLE)?;
let mut events = Events::with_capacity(16);
poll.poll(&mut events, Some(Duration::from_millis(100)))?;
drop(rx);
drain(&mut poll, "8. pipe: read end closed, writer registered WRITABLE");
#[cfg(target_os = "linux")]
{
let (mut poll, mut s, c) = setup();
poll.registry().reregister(&mut s, Token(1), Interest::READABLE | Interest::PRIORITY)?;
let n = unsafe { libc::send(c.as_raw_fd(), b"!".as_ptr().cast(), 1, libc::MSG_OOB) };
assert_eq!(n, 1);
drain(&mut poll, "9. peer sends 1 byte MSG_OOB, READABLE|PRIORITY");
let mut buf = [0u8; 8];
let _ = std::io::Read::read(&mut s, &mut buf);
}
Ok(())
}
两个平台各跑 3 次,输出逐字节相同。Linux 6.12.28(容器内 Alpine,aarch64 musl):
os=linux
1. peer writes 2 bytes
events=IN|OUT data=0x1 => readable writable
2. peer shutdown(Write) (FIN)
events=IN|OUT|RDHUP data=0x1 => readable writable read_closed
3. peer closes with SO_LINGER=0 (RST)
events=IN|OUT|ERR|HUP|RDHUP data=0x1 => readable writable error read_closed write_closed
4. local shutdown(Write)
events=OUT data=0x1 => writable
5. local shutdown(Both)
events=IN|OUT|HUP|RDHUP data=0x1 => readable writable read_closed write_closed
6. connect to a closed port
events=IN|OUT|ERR|HUP|RDHUP data=0x1 => readable writable error read_closed write_closed
7. pipe: write end closed, reader registered READABLE
events=HUP data=0x1 => read_closed write_closed
8. pipe: read end closed, writer registered WRITABLE
events=OUT|ERR data=0x1 => writable error write_closed
9. peer sends 1 byte MSG_OOB, READABLE|PRIORITY
events=PRI data=0x1 => readable priority
M2 Max、macOS、rustc 1.99.0(第 9 项只在 Linux 上编译):
os=macos
1. peer writes 2 bytes
filter=READ flags=CLEAR fflags=0 data=2 udata=0x1 => readable
2. peer shutdown(Write) (FIN)
filter=READ flags=EOF|CLEAR fflags=0 data=0 udata=0x1 => readable read_closed
3. peer closes with SO_LINGER=0 (RST)
filter=WRITE flags=EOF|CLEAR fflags=54 data=146988 udata=0x1 => writable error write_closed
filter=READ flags=EOF|CLEAR fflags=54 data=0 udata=0x1 => readable error read_closed
4. local shutdown(Write)
filter=WRITE flags=EOF|CLEAR fflags=0 data=146988 udata=0x1 => writable write_closed
5. local shutdown(Both)
filter=READ flags=EOF|CLEAR fflags=0 data=0 udata=0x1 => readable read_closed
filter=WRITE flags=EOF|CLEAR fflags=0 data=146988 udata=0x1 => writable write_closed
6. connect to a closed port
filter=WRITE flags=EOF|CLEAR fflags=61 data=2048 udata=0x1 => writable error write_closed
filter=READ flags=EOF|CLEAR fflags=61 data=0 udata=0x1 => readable error read_closed
7. pipe: write end closed, reader registered READABLE
filter=READ flags=EOF|CLEAR fflags=0 data=0 udata=0x1 => readable read_closed
8. pipe: read end closed, writer registered WRITABLE
filter=WRITE flags=EOF|CLEAR fflags=0 data=0 udata=0x1 => writable write_closed
对着表读:
- 第 1 项:Linux 上数据到达,事件里同时有
IN和OUT。可写状态并没有变,但边沿触发下这个 fd 被唤醒时,内核报告的是它当前全部的就绪位。macOS 只有一个EVFILT_READ事件,data=2是可读字节数,EVFILT_WRITE没有变化就不报。 - 第 2 项(对端 FIN):Linux 是
IN|RDHUP,满足「EPOLLIN与EPOLLRDHUP同时」;macOS 是EVFILT_READ加EV_EOF。两边都只有read_closed,没有error。此时read会返回 0。 - 第 3、6 项(RST、连接被拒):Linux 一次给出全部五个位,所有
is_*都为真;macOS 读写各一个事件,fflags分别是 54(ECONNRESET)和 61(ECONNREFUSED),所以error为真。具体错误要用take_error或下一次读写拿,见 TCP。 - 第 4 项(本端只关写):正是
is_write_closed文档说的平台差异。kqueue 上EVFILT_WRITE带EV_EOF,write_closed为真;epoll 只给一个OUT,write_closed为假。 - 第 7、8 项(管道):Linux 上读端在写端关闭后只收到
HUP,is_readable()为假;macOS 上is_readable()为真。写端在读端关闭后,Linux 是OUT|ERR,走的是「EPOLLOUT与EPOLLERR同时」那一支,所以还带着error。 - 第 9 项:带外数据只产生
EPOLLPRI,没有EPOLLIN,但is_readable()也为真,因为 epoll 的is_readable把EPOLLPRI算在内。Event::is_readable的文档专门提到这一点:带外数据也会触发可读,不读它的程序在水平触发下会陷入死循环,「However because Mio uses edge-triggers it will not result in an infinite loop」。
问: 第 7 项里,Linux 上管道写端关了,读端的事件 is_readable() 却是假。只在 is_readable() 时去读的代码会怎样?
答: 它永远不会去读这个管道,也就永远读不到那个表示 EOF 的 0,而边沿触发下这个 HUP 只报一次。macOS 上同样的代码没问题,因为 kqueue 的 EVFILT_READ 带着 EV_EOF 回来,is_readable() 为真。要在两个平台上都正确,处理事件时把 is_read_closed()(或 is_error())也当成「去读一次」的理由。tokio 就是这么做的:Ready::from_mio 把 is_read_closed() 映射成 READ_CLOSED,而读方向的等待把 READABLE 和 READ_CLOSED 都当成就绪(见 tokio 卷的 ScheduledIo 与就绪状态)。
Events:容量(在 epoll 和 kqueue 上)就是一次 poll 的上限
Events 包着 sys::Events,在 epoll 上是 Vec<libc::epoll_event>,在 kqueue 上是 Events(Vec<Event>)。with_capacity 就是 Vec::with_capacity,capacity() 是 Vec::capacity()。上一页看过 select:每次先 clear(),再把 capacity() 作为 maxevents(kevent 的 nevents)交给内核,最后 set_len(n)。所以:
- 在 epoll 和 kqueue 上,容量就是一次
poll最多取回的条数,poll不会扩容(poll(2)后备实现不看容量、一次全部返回,见 poll(2) 后备)。文档的说法是「At mostevents.capacity()events will be returned. If there are further pending readiness events, they will be returned on the next call topoll.」 - 事件是复用缓冲区,
Events通常和Poll一起建一次、反复用,poll的路上没有分配。 - 迭代器就是下标:
Iter::next对Vec做get(pos)再from_sys_event_ref,size_hint和count直接用len()。
实验:容量 2,五个就绪的 socket
src/bin/capacity.rs 建 5 对连接,每个对端先写 1 字节,让 5 个服务端 socket 在第一次 poll 之前就都可读(也都可写),分别用 Token(0) 到 Token(4) 注册,然后用 Events::with_capacity(2) 反复 poll,直到取不到事件为止。每个事件打印成 token 加 r / w / rw。关注集合分别用 READABLE 和 READABLE | WRITABLE 各做一遍:
use std::io::Write;
use std::time::Duration;
use mio::net::TcpStream;
use mio::{Events, Interest, Poll, Token};
fn main() -> std::io::Result<()> {
println!("os={}", std::env::consts::OS);
let interests = [("READABLE", Interest::READABLE), ("READABLE|WRITABLE", Interest::READABLE | Interest::WRITABLE)];
for (name, interest) in interests {
let l = std::net::TcpListener::bind("127.0.0.1:0")?;
let mut poll = Poll::new()?;
let mut keep = vec![];
for i in 0..5 {
let mut c = std::net::TcpStream::connect(l.local_addr()?)?;
let (s, _) = l.accept()?;
s.set_nonblocking(true)?;
let mut s = TcpStream::from_std(s);
c.write_all(b"x")?; // every socket is readable (and writable) before the first poll
poll.registry().register(&mut s, Token(i), interest)?;
keep.push((s, c));
}
std::thread::sleep(Duration::from_millis(50));
let mut events = Events::with_capacity(2);
println!("{name}, Events::with_capacity(2), capacity()={}", events.capacity());
for round in 1.. {
poll.poll(&mut events, Some(Duration::from_millis(100)))?;
let got: Vec<String> = events.iter().map(|e| {
let rw = match (e.is_readable(), e.is_writable()) { (true, true) => "rw", (true, false) => "r", (false, true) => "w", _ => "-" };
format!("{}{rw}", e.token().0)
}).collect();
println!(" poll #{round}: {} events {:?}", got.len(), got);
if got.is_empty() { break; }
}
}
Ok(())
}
Linux 6.12.28(容器内 Alpine,aarch64 musl),跑了 3 次,输出相同:
os=linux
READABLE, Events::with_capacity(2), capacity()=2
poll #1: 2 events ["0r", "1r"]
poll #2: 2 events ["2r", "3r"]
poll #3: 1 events ["4r"]
poll #4: 0 events []
READABLE|WRITABLE, Events::with_capacity(2), capacity()=2
poll #1: 2 events ["0rw", "1rw"]
poll #2: 2 events ["2rw", "3rw"]
poll #3: 1 events ["4rw"]
poll #4: 0 events []
strace 里对应的系统调用(-e trace=epoll_pwait,只摘 READABLE 那一遍):
epoll_pwait(4, [{events=EPOLLIN, data=0}, {events=EPOLLIN, data=0x1}], 2, 100, NULL, 8) = 2
epoll_pwait(4, [{events=EPOLLIN, data=0x2}, {events=EPOLLIN, data=0x3}], 2, 100, NULL, 8) = 2
epoll_pwait(4, [{events=EPOLLIN, data=0x4}], 2, 100, NULL, 8) = 1
epoll_pwait(4, [], 2, 100, NULL, 8) = 0
第三个参数 maxevents 是 2,内核每次最多交出 2 个,剩下的留在就绪队列里,下一次 epoll_wait 接着交。第一次没取到的 2、3、4 没有丢,也没有因为边沿触发而「错过」:它们的边沿已经被内核记下,只是还没被取走。没有任何报错,代价是要多几次系统调用,排在后面的 socket 晚一点被处理。
Events::with_capacity(0) 是个极端情况,两边的反应不一样(src/bin/zero_cap.rs:注册一个已有连接等待的监听 socket,用容量 0 的 Events 以 500 ms 超时 poll):
macos capacity=0 -> Ok(()), 0 events, 0ms
epoll_pwait(3, 0x8, 0, 500, NULL, 8) = -1 EINVAL (Invalid argument)
linux capacity=0 -> Err(Os { code: 22, kind: InvalidInput, message: "Invalid argument" }), 0 events, 0ms
Linux 的 epoll_wait 要求 maxevents 大于 0,直接 EINVAL;macOS 的 kevent 立刻返回 0 个事件,不等超时。放在循环里,前者每次报错,后者变成忙转。mio 不检查这个参数。
READABLE|WRITABLE 那一遍要几次 poll 才能把事件取完?还是上面的 capacity.rs,M2 Max、macOS、rustc 1.99.0,--release。5 个 socket 都可读也都可写,Events 的容量是 2。Linux 上两遍都是 2、2、1,三次取完。macOS 上 READABLE 那一遍也是三次。READABLE|WRITABLE 那一遍,算上最后返回 0 个事件的那次之前,一共有几次 poll 拿到了事件?
容量设多大是上层的事。tokio 默认 1024,忙的时候另用一个小缓冲区,取舍见 tokio 卷的反应器。
动脑筋答案
os=macos
READABLE, Events::with_capacity(2), capacity()=2
poll #1: 2 events ["0r", "1r"]
poll #2: 2 events ["2r", "3r"]
poll #3: 1 events ["4r"]
poll #4: 0 events []
READABLE|WRITABLE, Events::with_capacity(2), capacity()=2
poll #1: 2 events ["0w", "0r"]
poll #2: 2 events ["1w", "1r"]
poll #3: 2 events ["2w", "2r"]
poll #4: 2 events ["3w", "3r"]
poll #5: 2 events ["4w", "4r"]
poll #6: 0 events []
答案是 5 次。跑了 3 次,输出相同。
- kqueue 上每个 socket 注册了
EVFILT_WRITE和EVFILT_READ两个过滤器,各自产生一个事件,5 个 socket 共 10 个事件,容量 2 就要取 5 次。Linux 上每个 socket 只有一个epoll_event,EPOLLIN|EPOLLOUT合在一起,所以 5 个事件取 3 次。 - 每次取到的恰好是同一个 socket 的写和读两个事件(
"0w", "0r"),同一个 token 在一批Events里出现两次。poll文档说的就是这种情况:一个 source 可能「a single readiness event ... with both readable and writable」,也可能是「two separate events」。处理事件的代码不能假设一个 token 在一批里只出现一次。 - 同样的
with_capacity(n),在 macOS 上能覆盖的 socket 数大约只有 Linux 的一半(对同时关注读写的 source 而言)。
小结
Token(pub usize)没有任何保留值。epoll 存进epoll_event.u64,kqueue 存进kevent.udata,取回时原样转回usize;strace 里能直接看到data=<token>。mio 不维护任何映射表。Interest是NonZeroU8:不存在空集合,remove返回Option,Option<Interest>也只占 1 字节。AIO(DragonFly、FreeBSD、Apple)、LIO(FreeBSD)、PRIORITY(Linux、Android)三个常量按平台 cfg,写错平台是编译错误;PRIORITY是 0.8.6 加的(#1647)。- epoll 的掩码总是带
EPOLLET,可读再加EPOLLIN|EPOLLRDHUP,可写EPOLLOUT,优先EPOLLPRI;kqueue 只为读、写各提交一个带EV_CLEAR的过滤器。 Event是#[repr(transparent)]包着epoll_event/kevent,内核写的内存就是用户读的Event。kqueue 上它 1.1.0 起才是Send(#1906)。- 各个
is_*方法逐个查标志位(见表)。kqueue 一个事件只有一个方向;is_error的文档写反了fflags条件;Linux 上管道写端关闭后读端只有HUP,is_readable()为假,要同时看is_read_closed()。 - 在 epoll 和 kqueue 上,
Events的容量就是一次poll的maxevents:容量 2、5 个就绪 socket,Linux 取 3 次,macOS 同时关注读写时取 5 次,同一个 token 会在一批里出现两次。容量 0 在 Linux 上是EINVAL,在 macOS 上立刻返回。