Source 与 IoSource
主线位置:注册主线的中段。
Registry::register(&mut source, token, interest)什么也不做,直接调source.register(self, token, interest);从这里到平台 selector 之间的那一层就是本页:IoSource在 debug 下查SelectorId,再交给IoSourceState,最后才到epoll_ctl/kevent。事件主线上也有本页的一段:用户读写 mio 的 socket 时都经过IoSource::do_io,它在poll(2)后备实现上负责在WouldBlock之后重新武装。
flowchart TB
reg["Registry::register"] --> src["S::register"]
src --> boxed["Box T:转给内部"]
src --> iosrc["IoSource T"]
src --> srcfd["SourceFd"]
iosrc --> selid{"debug:SelectorId"}
selid -->|已关联| errnode["Err AlreadyExists"]
selid -->|未关联| state{IoSourceState}
state -->|"epoll / kqueue"| stateless["无状态:直接转发"]
state -->|"poll(2) / event ports"| stateful["记下 fd、token、interest"]
srcfd --> sel["selector.register"]
stateless --> sel
stateful --> sel
Poll、Registry 的结构和 register / reregister / deregister 的约定在 Poll 与 Registry,Token 和 Interest 怎么进内核在 Token、Interest 与 Event。这一页讲 Registry 和平台 selector 之间的那一层:event/source.rs(139 行,一大半是文档)、io_source.rs(311 行)、sys/unix/sourcefd.rs,以及 sys/unix/selector/ 下的两个 *_io_source.rs。
event::Source:三个方法
Source 是所有能注册的东西的共同接口:
pub trait Source {
fn register(
&mut self,
registry: &Registry,
token: Token,
interests: Interest,
) -> io::Result<()>;
fn reregister(
&mut self,
registry: &Registry,
token: Token,
interests: Interest,
) -> io::Result<()>;
fn deregister(&mut self, registry: &Registry) -> io::Result<()>;
}
文档要求用户不要直接调这三个方法,而是调 Registry 上的同名方法;实现者「几乎总是」转给一个更底层的句柄。Registry::register 确实只是一层转发,多打一行 trace!:
pub fn register<S>(&self, source: &mut S, token: Token, interests: Interest) -> io::Result<()>
where
S: event::Source + ?Sized,
{
trace!(
"registering event source with poller: token={:?}, interests={:?}",
token,
interests
);
source.register(self, token, interests)
}
方向值得注意:不是 Registry 去问 source 要 fd,而是 Registry 把自己交给 source,由 source 决定怎么注册。这样 Windows 上的 socket(要建 AFD 状态)、Unix 上的裸 fd(直接 epoll_ctl)、一个包着两个 socket 的用户类型,都能用同一个入口。
S: ?Sized 让 &mut dyn Source 也能传进来,所以 mio 不需要给 &mut T 实现 Source。源码里唯一的转发实现是给 Box<T> 的,T: Source + ?Sized,方便把 Box<dyn Source> 放进集合:
impl<T> Source for Box<T>
where
T: Source + ?Sized,
{
fn register(
&mut self,
registry: &Registry,
token: Token,
interests: Interest,
) -> io::Result<()> {
(**self).register(registry, token, interests)
}
// ... reregister / deregister 同样转发
}
问: SourceFd 的 register 什么都不改,epoll 也允许多个线程同时 epoll_ctl,为什么三个方法都要 &mut self?
答: 因为有的平台要在用户态记账。改成 &mut 的 #1180(0.7.0)提交说明写的是:「一般来说事件源不需要被并发地(重新/取消)注册。虽然在多数平台上这样做是可以的,但在需要用户态记账的平台上(比如 Windows)会让事情更复杂。」本页后面的 registered_io_source.rs 就是这种记账:register 要往 self 里写一个 Box<InternalState>。&mut self 让编译器保证同一个 source 不会被两个线程同时注册,实现里就不用加锁。
Box<T> 的这个实现原来更宽。
Source 曾经对所有 DerefMut 类型都有实现0.7.0-alpha.1 里,event/source.rs 有一个覆盖实现:impl<T, S> Source for T where T: DerefMut<Target = S>, S: Source + ?Sized,Box、&mut、任何智能指针都自动是 Source。随后它被换成了现在只针对 Box<T> 的版本,提交说明是:「这会阻止包装类型同时实现 DerefMut 和 event::Source,而内部类型也实现 event::Source 时会导致冲突的 trait 实现。也许等特化稳定之后可以恢复。」以及「这是为 IoSource 实现 DerefMut 所必需的。」也就是说,下一节的 IoSource 既要 DerefMut 到内部的 std socket,又要自己实现 Source,两者在覆盖实现下冲突。这次改动随 #1236 合入,首次发布于 0.7.0。
IoSource:mio 的 socket 都包在里面
IoSource<T> 有三个字段:
pub struct IoSource<T> {
state: IoSourceState,
inner: T,
#[cfg(debug_assertions)]
selector_id: SelectorId,
}
inner 是一个 std 类型(std::net::TcpStream、std::os::unix::net::UnixListener……),state 是平台相关的注册状态(后面讲),selector_id 只在 debug 下存在。mio::net 里的每个类型都是它的薄包装,例如 TcpStream 的字段就是 inner: IoSource<net::TcpStream>,Read 的实现是 self.inner.do_io(|mut inner| inner.read(buf))。
结构体本身是 pub 的,文档也写着「IoSource 让任何 FD 或 socket 的包装都能注册到 Poll」,但 lib.rs 里只有 mod io_source;,没有 pub use,它在 1.x 里是 crate 私有的,用户代码写不出 mio::IoSource。用户自己的 fd 只能走 SourceFd(下文)。
Unix 上的 Source 实现要求 T: AsRawFd,每个方法先做 debug 检查,再交给 state:
impl<T> event::Source for IoSource<T>
where
T: AsRawFd,
{
fn register(
&mut self,
registry: &Registry,
token: Token,
interests: Interest,
) -> io::Result<()> {
#[cfg(debug_assertions)]
self.selector_id.associate(registry)?;
self.state
.register(registry, token, interests, self.inner.as_raw_fd())
}
fn reregister(
&mut self,
registry: &Registry,
token: Token,
interests: Interest,
) -> io::Result<()> {
#[cfg(debug_assertions)]
self.selector_id.check_association(registry)?;
self.state
.reregister(registry, token, interests, self.inner.as_raw_fd())
}
fn deregister(&mut self, registry: &Registry) -> io::Result<()> {
#[cfg(debug_assertions)]
self.selector_id.remove_association(registry)?;
self.state.deregister(registry, self.inner.as_raw_fd())
}
}
Windows 版本要求 T: AsRawSocket,WASI preview 1 版本绕过 state 直接调 selector,结构相同。
SelectorId:debug 下的一道检查
每个 selector 在 debug 下有一个编号。epoll 版本的 Selector::new 从一个全局计数器 NEXT_ID(从 1 开始)取号,try_clone 沿用同一个号(注释:「It's the same selector, so we use the same id.」)。kqueue、poll(2)、event ports 的 selector 都有同样的 id。所以 Registry::try_clone 出来的注册器和原来的 Poll 算同一个 selector,两个 Poll::new() 算两个。
SelectorId 是 IoSource 里记「我注册在哪个 selector 上」的一个 AtomicUsize,0 表示未关联。associate 在 register 时调用:
fn associate(&self, registry: &Registry) -> io::Result<()> {
let registry_id = registry.selector().id();
let previous_id = self.id.swap(registry_id, Ordering::AcqRel);
if previous_id == Self::UNASSOCIATED {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"I/O source already registered with a `Registry`",
))
}
}
它不区分「已经注册在这个 Poll 上」和「已经注册在另一个 Poll 上」,两种都返回同一个 AlreadyExists,而且错误在系统调用之前返回。check_association(reregister 用)区分未注册(NotFound)和注册在别处(AlreadyExists,「already registered with a different Registry」);remove_association(deregister 用)在编号对不上时返回 NotFound。整个类型和三处调用都在 #[cfg(debug_assertions)] 下,release 里什么都不查,交给内核。
mio 自己的测试 tcp_register_multiple_event_loops 就是测这个,并且标了 #[cfg(debug_assertions)] // Check is only present when debug assertions are enabled.。
实验:一个 listener 注册到两个 Poll
依赖只有一行(实验 crate 的 Cargo.toml,后面各实验相同):
mio = { version = "=1.2.4", features = ["os-poll", "net", "os-ext"] }
src/bin/two_polls.rs:同一个 TcpListener 先注册到 poll1,再注册一次到 poll1,再注册到 poll2;另用 SourceFd 把它的 fd 注册到 poll3。然后连一次,三个 Poll 各等 200ms。
use std::net::TcpStream as StdStream;
use std::os::fd::AsRawFd;
use std::time::Duration;
use mio::net::TcpListener;
use mio::unix::SourceFd;
use mio::{Events, Interest, Poll, Token};
fn show(label: &str, r: std::io::Result<()>) {
match r {
Ok(()) => println!("{label}: Ok"),
Err(e) => println!("{label}: Err(kind={:?}) {e}", e.kind()),
}
}
fn drain(name: &str, poll: &mut Poll, events: &mut Events) {
poll.poll(events, Some(Duration::from_millis(200))).unwrap();
let tokens: Vec<usize> = events.iter().map(|e| e.token().0).collect();
println!("{name}: {} event(s), tokens {tokens:?}", tokens.len());
}
fn main() {
println!("debug_assertions = {}", cfg!(debug_assertions));
let mut poll1 = Poll::new().unwrap();
let mut poll2 = Poll::new().unwrap();
let mut poll3 = Poll::new().unwrap();
let mut events = Events::with_capacity(8);
let mut listener = TcpListener::bind("127.0.0.1:0".parse().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let fd = listener.as_raw_fd();
show("register -> poll1", poll1.registry().register(&mut listener, Token(1), Interest::READABLE));
show("register -> poll1 again", poll1.registry().register(&mut listener, Token(1), Interest::READABLE));
show("register -> poll2", poll2.registry().register(&mut listener, Token(2), Interest::READABLE));
show("SourceFd -> poll3", poll3.registry().register(&mut SourceFd(&fd), Token(3), Interest::READABLE));
let _client = StdStream::connect(addr).unwrap();
drain("poll1", &mut poll1, &mut events);
drain("poll2", &mut poll2, &mut events);
drain("poll3", &mut poll3, &mut events);
}
Linux 6.12.28(Apple container 里的 Alpine,aarch64 musl 静态链接)、rustc 1.99.0,debug 构建(cargo build):
debug_assertions = true
register -> poll1: Ok
register -> poll1 again: Err(kind=AlreadyExists) I/O source already registered with a `Registry`
register -> poll2: Err(kind=AlreadyExists) I/O source already registered with a `Registry`
SourceFd -> poll3: Ok
poll1: 1 event(s), tokens [1]
poll2: 0 event(s), tokens []
poll3: 1 event(s), tokens [3]
同一平台,release 构建:
debug_assertions = false
register -> poll1: Ok
register -> poll1 again: Err(kind=AlreadyExists) File exists (os error 17)
register -> poll2: Ok
SourceFd -> poll3: Ok
poll1: 1 event(s), tokens [1]
poll2: 1 event(s), tokens [2]
poll3: 1 event(s), tokens [3]
M2 Max、macOS、rustc 1.99.0:debug 构建的输出和 Linux debug 逐字相同;release 构建只有一行不同:
register -> poll1 again: Ok
用 strace 看两次 Linux 运行的系统调用(strace -e trace=epoll_create1,epoll_ctl,epoll_pwait,socket,bind,listen,connect,程序的标准输出重定向掉了)。debug:
epoll_create1(EPOLL_CLOEXEC) = 3
epoll_create1(EPOLL_CLOEXEC) = 4
epoll_create1(EPOLL_CLOEXEC) = 5
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 6
bind(6, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
listen(6, 128) = 0
epoll_ctl(3, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x1}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x3}) = 0
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP) = 7
connect(7, {sa_family=AF_INET, sin_port=htons(39275), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
epoll_pwait(3, [{events=EPOLLIN, data=0x1}], 8, 200, NULL, 8) = 1
epoll_pwait(4, [], 8, 200, NULL, 8) = 0
epoll_pwait(5, [{events=EPOLLIN, data=0x3}], 8, 200, NULL, 8) = 1
+++ exited with 0 +++
release:
epoll_create1(EPOLL_CLOEXEC) = 3
epoll_create1(EPOLL_CLOEXEC) = 4
epoll_create1(EPOLL_CLOEXEC) = 5
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 6
bind(6, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
listen(6, 128) = 0
epoll_ctl(3, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x1}) = 0
epoll_ctl(3, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x1}) = -1 EEXIST (File exists)
epoll_ctl(4, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x2}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x3}) = 0
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP) = 7
connect(7, {sa_family=AF_INET, sin_port=htons(45471), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
epoll_pwait(3, [{events=EPOLLIN, data=0x1}], 8, 200, NULL, 8) = 1
epoll_pwait(4, [{events=EPOLLIN, data=0x2}], 8, 200, NULL, 8) = 1
epoll_pwait(5, [{events=EPOLLIN, data=0x3}], 8, 200, NULL, 8) = 1
+++ exited with 0 +++
每种构建各跑 3 次,除端口号外相同。逐条看:
- debug 下两次失败都没有系统调用。 两条
register失败的消息完全一样,epoll_ctl只有两次:fd 3(poll1)和 fd 5(poll3)。poll2的 epoll 实例(fd 4)里什么都没有,所以它等满 200ms、0 个事件。 - release 下由内核决定。 对同一个 epoll 实例重复
EPOLL_CTL_ADD得到EEXIST,io::Error把它映射成AlreadyExists,消息是File exists (os error 17);把同一个 fd 加进另一个 epoll 实例是合法的,poll2和poll1各收到一个事件。macOS 上 kqueue 对已存在的过滤器再做一次EV_ADD只是修改,所以连重复注册都返回Ok。 - SourceFd 不受检查。 它没有
SelectorId,debug 下照样把同一个 fd 加进了第三个 epoll 实例。
也就是说,这道检查抓的是「同一个 mio 对象注册了两次」这类用法错误,而且只在 debug 下抓。在 Linux release 下,同一个 fd 进两个 epoll 是合法的,两个 Poll 都会收到事件;但边沿触发加上两个事件循环各自读同一个 socket,几乎一定是 bug。
associate 失败也会改编号
associate 先 swap 再判断,失败时也已经把编号换成了新的 selector。remove_association 也一样,先换成 0 再判断。debug 下一次失败的 register 会留下后遗症(src/bin/quiz_reregister.rs:注册到 poll1,再试着注册到 poll2,然后对 poll1 做 reregister、deregister,最后连一次、看 poll1):
show("register poll1", poll1.registry().register(&mut listener, Token(1), Interest::READABLE));
show("register poll2", poll2.registry().register(&mut listener, Token(2), Interest::READABLE));
show("reregister poll1", poll1.registry().reregister(&mut listener, Token(1), Interest::READABLE));
show("deregister poll1", poll1.registry().deregister(&mut listener));
let _client = StdStream::connect(addr).unwrap();
poll1.poll(&mut events, Some(Duration::from_millis(200))).unwrap();
let tokens: Vec<usize> = events.iter().map(|e| e.token().0).collect();
println!("poll1 after deregister: {} event(s), tokens {tokens:?}", tokens.len());
debug 构建,macOS 和 Linux 输出相同(各 3 次):
debug_assertions = true
register poll1: Ok
register poll2: Err(kind=AlreadyExists) I/O source already registered with a `Registry`
reregister poll1: Err(kind=AlreadyExists) I/O source already registered with a different `Registry`
deregister poll1: Err(kind=NotFound) I/O source not registered with `Registry`
poll1 after deregister: 1 event(s), tokens [1]
release 构建:
debug_assertions = false
register poll1: Ok
register poll2: Ok
reregister poll1: Ok
deregister poll1: Ok
poll1 after deregister: 0 event(s), tokens []
debug 下失败的 register 把编号改成了 poll2 的,于是对真正注册所在的 poll1 做 reregister 被拒(「a different Registry」);deregister 也被拒,并且因为 ? 提前返回,epoll_ctl(EPOLL_CTL_DEL) 根本没发出去,内核里的注册还在,poll1 照样收到事件。release 下 poll2 的注册本来就合法,后面一切正常。这只影响「已经犯了一次错的程序」在 debug 下的后续表现,不是 release 的问题;但它说明这道检查的目的是尽早报错,而不是维护一个精确的状态。
SourceFd:任意 fd
SourceFd 是一个借用 fd 的元组结构(需要 os-ext feature,在 mio::unix 下):
#[derive(Debug)]
pub struct SourceFd<'a>(pub &'a RawFd);
impl<'a> event::Source for SourceFd<'a> {
fn register(
&mut self,
registry: &Registry,
token: Token,
interests: Interest,
) -> io::Result<()> {
registry.selector().register(*self.0, token, interests)
}
// ... reregister / deregister 同样直接调 selector
}
它和 IoSource 有三处不同:
- 不拥有 fd。 文档说它「不管理任何生命周期相关的操作,例如在 drop 时关闭 FD」,预期的用法是每次注册前临时构造:
registry.register(&mut SourceFd(&fd), ...)。 - 没有
SelectorId。 上面的实验里它在 debug 下也能把同一个 fd 注册进第二个Poll。 - 不经过
IoSourceState。 直接调selector().register。在 epoll / kqueue 上没区别;在poll(2)后备实现上,它得不到下一节的重新武装(见文末的动脑筋)。
tokio 的 AsyncFd 就是用它注册的:AsyncFd::register_with_interest 里是 &mut SourceFd(&fd),注册之后的事见 tokio 卷的反应器。
实验:一个 pipe 和一个普通文件
src/bin/sourcefd.rs:用 std::io::pipe() 建一个 pipe,mio 只看到它的读端 fd;再试一个普通文件 /etc/hosts。
use std::fs::File;
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::time::Duration;
use mio::unix::SourceFd;
use mio::{Events, Interest, Poll, Token};
fn show_events(label: &str, events: &Events) {
println!("{label}: {} event(s)", events.iter().count());
for e in events {
println!(
" token={} readable={} writable={} read_closed={}",
e.token().0,
e.is_readable(),
e.is_writable(),
e.is_read_closed()
);
}
}
fn main() {
let mut poll = Poll::new().unwrap();
let mut events = Events::with_capacity(8);
// A pipe from std: mio never sees the PipeReader type, only its fd.
let (mut reader, mut writer) = std::io::pipe().unwrap();
let rfd = reader.as_raw_fd();
let r = poll.registry().register(&mut SourceFd(&rfd), Token(7), Interest::READABLE);
println!("register pipe fd {rfd}: {r:?}");
poll.poll(&mut events, Some(Duration::ZERO)).unwrap();
show_events("before write", &events);
writer.write_all(b"hi").unwrap();
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
show_events("after write", &events);
let mut buf = [0u8; 2];
reader.read_exact(&mut buf).unwrap();
println!("read {:?}", std::str::from_utf8(&buf).unwrap());
drop(writer);
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
show_events("after writer dropped", &events);
let r = poll.registry().deregister(&mut SourceFd(&rfd));
println!("deregister pipe fd: {r:?}");
// A regular file on the local filesystem.
let file = File::open("/etc/hosts").unwrap();
let ffd = file.as_raw_fd();
let r = poll.registry().register(&mut SourceFd(&ffd), Token(8), Interest::READABLE);
println!("register regular file fd {ffd}: {r:?}");
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
show_events("regular file", &events);
}
Linux(release,3 次相同):
register pipe fd 4: Ok(())
before write: 0 event(s)
after write: 1 event(s)
token=7 readable=true writable=false read_closed=false
read "hi"
after writer dropped: 1 event(s)
token=7 readable=false writable=false read_closed=true
deregister pipe fd: Ok(())
register regular file fd 5: Err(Os { code: 1, kind: PermissionDenied, message: "Operation not permitted" })
regular file: 0 event(s)
对应的系统调用(strace -e trace=epoll_ctl,pipe2,openat):
pipe2([4, 5], O_CLOEXEC) = 0
epoll_ctl(3, EPOLL_CTL_ADD, 4, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x7}) = 0
epoll_ctl(3, EPOLL_CTL_DEL, 4, NULL) = 0
openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 5
epoll_ctl(3, EPOLL_CTL_ADD, 5, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x8}) = -1 EPERM (Operation not permitted)
+++ exited with 0 +++
macOS(release,3 次相同):
register pipe fd 4: Ok(())
before write: 0 event(s)
after write: 1 event(s)
token=7 readable=true writable=false read_closed=false
read "hi"
after writer dropped: 1 event(s)
token=7 readable=true writable=false read_closed=true
deregister pipe fd: Ok(())
register regular file fd 5: Ok(())
regular file: 1 event(s)
token=8 readable=true writable=false read_closed=false
pipe 在两个平台上都和 socket 一样工作:std 的 PipeReader 是阻塞的,mio 不关心,它只把 fd 交给内核(这里只读一次、读的是已经到达的 2 字节,所以不会阻塞;真实程序要先把 fd 设成非阻塞)。写端关闭后两边都报 read_closed,但 Linux 上 readable 是 false、macOS 上是 true,这是 EPOLLHUP 和 kqueue EV_EOF 的解释差异,见 Token、Interest 与 Event。
普通文件的结果完全取决于内核:epoll 拒绝普通文件(EPERM),kqueue 接受并且立刻报可读。SourceFd 文档里的「任何能注册到底层 OS selector 的 FD」说的就是这个:mio 不做任何过滤,能不能注册、事件长什么样,都是平台的事。
do_io 和两种 IoSourceState
IoSource 的另一半是 do_io:
pub fn do_io<F, R>(&self, f: F) -> io::Result<R>
where
F: FnOnce(&T) -> io::Result<R>,
{
self.state.do_io(f, &self.inner)
}
文档要求「所有 I/O 操作都必须经过 do_io」,否则在 WouldBlock 之后可能再也收不到事件。mio::net 的类型自己的 read / write / peek 都走它;想直接对 socket 做别的系统调用(比如 libc::recvmsg),要用 TcpStream::try_io 这类方法,它就是 self.inner.do_io(|_| f())。
IoSourceState 有两种 Unix 实现,由各 selector 文件在末尾选择:
| selector | 文件 | IoSourceState | 适用平台(sys/unix/mod.rs 的 cfg) |
|---|---|---|---|
| epoll | selector/epoll.rs | stateless_io_source.rs | android、illumos、linux、redox |
| kqueue | selector/kqueue.rs | stateless_io_source.rs | dragonfly、freebsd、netbsd、openbsd 和 Apple 各系统 |
| event ports | selector/event_ports.rs | registered_io_source.rs | solaris |
poll(2) | selector/poll.rs | registered_io_source.rs | aix、espidf、nuttx、fuchsia、haiku、hermit、hurd、nto、vita、cygwin、horizon、wasi(preview 2 起;preview 1 走 sys/wasip1);或设了 --cfg mio_unsupported_force_poll_poll |
Windows 不用这两个文件,它在 sys/windows/mod.rs 里有自己的 IoSourceState,形状和 registered_io_source.rs 一样:碰到 WouldBlock 就对 sock_state 重新注册,详见 Windows:IOCP 与 AFD。selector 怎么按平台选出来,见源码地图。
无状态:epoll 和 kqueue
文件开头一句话说明了为什么:「kqueue(2) 和 epoll(2) 都不需要持有任何用户态状态。」do_io 就是调用闭包:
pub(crate) struct IoSourceState;
impl IoSourceState {
// ...
pub(crate) fn do_io<T, F, R>(&self, f: F, io: &T) -> io::Result<R>
where
F: FnOnce(&T) -> io::Result<R>,
{
// We don't hold state, so we can just call the function and
// return.
f(io)
}
// register / reregister / deregister: "Pass through, we don't have any state."
}
IoSourceState 是零大小类型,do_io 内联之后什么也不剩。IoSource 是私有的,量不到,但可以量包着它的 mio::net::TcpStream(src/bin/sizes.rs,打印 size_of::<std::net::TcpStream>() 和 size_of::<mio::net::TcpStream>()):
| 构建 | std::net::TcpStream | mio::net::TcpStream |
|---|---|---|
| macOS release | 4 | 4 |
| macOS debug | 4 | 16 |
| Linux release | 4 | 4 |
Linux release,--cfg mio_unsupported_force_poll_poll | 4 | 16 |
release 下的 epoll / kqueue,mio 的 socket 就是一个 fd;debug 多出来的 12 字节是 8 字节的 SelectorId 加对齐;poll(2) 后备下多出来的是 Option<Box<InternalState>> 的一个指针加对齐。
边沿触发的内核自己会在下一次状态变化时再报一次,用户只要读到 WouldBlock(事件主线的要求),不需要 mio 做任何事。
有状态:poll(2) 和 event ports
registered_io_source.rs 开头一句是「poll(2) 和 event_port(2) 都需要持有每个 fd 的状态」:
pub(crate) struct IoSourceState {
inner: Option<Box<InternalState>>,
}
struct InternalState {
selector: Selector,
token: Token,
interests: Interest,
fd: RawFd,
shared_record: Arc<RegistrationRecord>,
}
register 先看 inner 是否已有值(有就 AlreadyExists,这个检查 release 下也在),然后 try_clone 一份 selector,调 register_internal,把 selector、token、interest、fd 都存进 Box。do_io 用它们在 WouldBlock 时重新注册:
pub(crate) fn do_io<T, F, R>(&self, f: F, io: &T) -> io::Result<R>
where
F: FnOnce(&T) -> io::Result<R>,
{
let result = f(io);
if let Err(err) = &result {
if err.kind() == io::ErrorKind::WouldBlock {
self.inner.as_ref().map_or(Ok(()), |state| {
state
.selector
.reregister(state.fd, state.token, state.interests)
})?;
}
}
result
}
原因是这两种机制都不是边沿触发:
poll(2)是水平触发,只要还可读就每次都报。mio 为了给出和 epoll 一样的「边沿」语义,在select里报出一个事件后把这一位从关注集合里摘掉(poll_fd.events &= !poll_fd.revents,旁边的注释写着「IoSourceState的do_io包装会用 reregister 把关注加回去」)。用户读到WouldBlock,说明数据读干净了,do_io把关注加回去,下一次数据到达才会再报。- event ports 是一次性的:
event_ports.rs的注释写着「event ports 在投递之后会解除 fd 的关联」,同样要重新关联。
InternalState 还实现了 Drop:没 deregister 就 drop 时,它自己调一次 selector.deregister(fd),因为 poll(2) 的关注集合在用户态,fd 关闭不会让它自动消失。RegistrationRecord 是和 selector 共享的一个标志,selector 遇到 POLLHUP / POLLERR 时会在内部先注销这个 fd,标志防止 drop 时再注销一次。
摘掉、加回的完整过程、跨线程注册时怎么唤醒正在 poll 的线程、1.2.3 修的 UDS listener 问题,都在 poll(2) 后备实现。这里只需要记住:do_io 是为这两个平台存在的,在 epoll 和 kqueue 上它是空的;而 SourceFd 不经过它。
src/bin/sourcefd_rearm.rs:pipe 读端用 SourceFd 注册 READABLE。每一轮写 1 字节、poll 100ms、数事件、读 1 字节(读完正好读空,但没有再读一次去碰 WouldBlock);第 3 轮开始前手动 reregister 一次。
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::time::Duration;
use mio::unix::SourceFd;
use mio::{Events, Interest, Poll, Token};
fn main() {
let mut poll = Poll::new().unwrap();
let mut events = Events::with_capacity(8);
let (mut reader, mut writer) = std::io::pipe().unwrap();
let fd = reader.as_raw_fd();
poll.registry().register(&mut SourceFd(&fd), Token(7), Interest::READABLE).unwrap();
let mut buf = [0u8; 1];
for round in 1..=3 {
if round == 3 {
poll.registry().reregister(&mut SourceFd(&fd), Token(7), Interest::READABLE).unwrap();
println!("(reregister)");
}
writer.write_all(b"x").unwrap();
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
println!("round {round}: write 1 byte, poll -> {} event(s)", events.iter().count());
reader.read_exact(&mut buf).unwrap(); // the byte is there either way
}
}
跑三种:macOS(kqueue)、Linux(epoll)、Linux 上加 RUSTFLAGS="--cfg mio_unsupported_force_poll_poll" 强制用 poll(2)。都是 release 构建。三种情况下「round 2」那一行各是几个事件?
动脑筋答案
macOS 和 Linux(epoll)输出相同:
round 1: write 1 byte, poll -> 1 event(s)
round 2: write 1 byte, poll -> 1 event(s)
(reregister)
round 3: write 1 byte, poll -> 1 event(s)
Linux,--cfg mio_unsupported_force_poll_poll:
round 1: write 1 byte, poll -> 1 event(s)
round 2: write 1 byte, poll -> 0 event(s)
(reregister)
round 3: write 1 byte, poll -> 1 event(s)
三种各跑 3 次,结果不变。
- epoll / kqueue 是 1。 边沿触发的「边」是新数据到达这个动作,不是「读到过
WouldBlock」。第二轮写入是一次新的到达,内核再报一次。读不到WouldBlock的真正风险在另一种情况:一次事件之后数据没读完,之后又没有新数据来,就再也不会有通知。 poll(2)是 0。 第一轮报出事件后,selector 已经把POLLIN从这个 fd 的关注里摘掉了。SourceFd的读写不经过do_io,没人把它加回去,第二轮的数据到了也不报(字节其实在 pipe 里,read_exact照样读到)。第 3 轮前手动reregister,poll(2)的关注恢复,又能收到事件。
所以在 poll(2) 后备实现上,SourceFd 的用户要自己承担 do_io 的职责:每次读写碰到 WouldBlock(这个例子里是每次处理完事件)就 reregister。用 mio 自己的 TcpStream 等类型就不用管,它们的读写都经过 do_io。
小结
event::Source只有register/reregister/deregister三个方法,都要&mut self(#1180,为了需要用户态记账的平台)。Registry的同名方法只是转发;S: ?Sized让&mut dyn Source直接可用,转发实现只有Box<T>一个(#1236 把对所有DerefMut的覆盖实现收窄成了它)。IoSource<T>包着 std 类型,mio::net的类型都建在它上面;它在 1.x 里是 crate 私有的。- debug 下
SelectorId让同一个IoSource的第二次register在系统调用之前就失败,消息固定是I/O source already registered with a \Registry`,不分同一个还是另一个Poll。release 下不查:Linux 上同一 epoll 重复添加得到File exists (os error 17),加进另一个Poll成功、两边都收到事件;macOS 上两种都成功。associate失败时也改了编号,之后对原Poll的reregister/deregister` 在 debug 下都被拒。 SourceFd(&fd)借用任意 fd,直接调 selector,没有SelectorId也没有do_io。能不能注册由内核决定:普通文件在 epoll 上是EPERM,在 kqueue 上成功且立刻可读。IoSourceState在 epoll / kqueue 上是零大小的直通;在poll(2)和 event ports 上记下 selector、token、interest、fd,do_io碰到WouldBlock就reregister,因为这两种机制报一次之后关注就没了。实测poll(2)后备下SourceFd第二轮收不到事件,epoll / kqueue 上能收到。