kqueue
主线位置:两条主线在 macOS 和 BSD 上的终点。注册主线的
register/reregister/deregister在这里变成一次带 changelist 的kevent,每个方向一项;事件主线的poll在这里变成一次 changelist 为空的kevent,取回的每个kevent结构只属于一个过滤器,同一个 fd 又可读又可写就是两个事件。
flowchart TB regnode["register"] --> chg["关注的方向各一项"] rereg["reregister"] --> chg2["W、R 两项都交"] dereg["deregister"] --> chg3["W、R 都 EV_DELETE"] chg --> kreg["kevent_register"] chg2 --> kreg chg3 --> kreg kreg --> sys1["kevent 带回执"] sys1 --> chk["逐项查 data"] pollnode["Poll::poll"] --> sel["select"] sel --> sys2["kevent 只取事件"] sys2 --> evs["一个方向一个事件"] wakenode["Waker::wake"] --> sys3["EVFILT_USER 一项"] sys3 --> evs
文件是 sys/unix/selector/kqueue.rs,919 行,真正做事的是前 310 行:Selector 的方法、kevent_register 和 check_errors 两个函数;event 模块里有 400 多行是 Event 的 Debug 输出要列的常量表。Poll → Registry → Selector 的结构和超时换算在 Poll 与 Registry,Token 怎么进出 udata、各个 is_* 查哪些位在 Token、Interest 与 Event,这一页讲 kevent 本身:changelist 怎么拼、错误怎么拿、哪些错误被吞掉、为什么。
哪些系统用这个文件
selector 是在 sys/unix/mod.rs 里用 cfg_attr(path = ...) 选的(规则见源码地图)。没有设 mio_unsupported_force_poll_poll 时,下面九个 target_os 编译 kqueue.rs:
- Apple:
macos、ios、tvos、visionos、watchos; - BSD:
freebsd、dragonfly、netbsd、openbsd。
九个系统的 kevent 结构字段类型不完全一样,文件开头用三个类型别名抹平:Count(nchanges / nevents 参数,NetBSD 是 size_t,其余 c_int)、Filter(Apple 是 i16,DragonFly / FreeBSD / OpenBSD 是 c_short,NetBSD 是 u32)、Flags(Apple u16,三个 BSD c_ushort,NetBSD u32)。udata 统一是 *mut c_void。所有 kevent 结构都由 kevent! 宏造出来,四个字段之外用 mem::zeroed() 填零,所以 fflags 和 data 进内核时总是 0。
除了类型,文件里按平台分叉的地方只有这几处:
| 位置 | Apple 各系统 | FreeBSD | DragonFly、NetBSD、OpenBSD |
|---|---|---|---|
setup_waker | 不编译(「For Darwin (macOS, iOS, etc.) we don't need any setup.」) | 有 | 有 |
is_readable 认 EVFILT_USER | 是 | 是 | 否(1.2.4 为止,见 Waker) |
is_aio 认 EVFILT_AIO | 是 | 是 | 只有 DragonFly |
is_lio 认 EVFILT_LIO | 否 | 是 | 否 |
Waker 用的是 waker/selector.rs,同样排除了 force_poll_poll;IoSourceState 是什么也不存的那个 stateless_io_source.rs(Source 与 IoSource)。下面的实验都在 M2 Max、macOS、rustc 1.99.0 上跑,BSD 上的行为只引源码。
Selector:一个 kqueue fd
Selector 只有一个 OwnedFd,外加 debug 下的 id。new 和 try_clone:
pub fn new() -> io::Result<Selector> {
// SAFETY: `kqueue(2)` ensures the fd is valid.
let kq = unsafe { OwnedFd::from_raw_fd(syscall!(kqueue())?) };
syscall!(fcntl(kq.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC))?;
Ok(Selector {
#[cfg(debug_assertions)]
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
kq,
})
}
pub fn try_clone(&self) -> io::Result<Selector> {
self.kq.try_clone().map(|kq| Selector {
// It's the same selector, so we use the same id.
#[cfg(debug_assertions)]
id: self.id,
kq,
})
}
kqueue() 不接受 flag 参数,没有 epoll 那样的 epoll_create1(EPOLL_CLOEXEC),所以要再补一次 fcntl(F_SETFD, FD_CLOEXEC)。(macOS 手册还说「The queue is not inherited by a child created with fork(2).」)先包成 OwnedFd 再 fcntl,fcntl 失败时 ? 提前返回,OwnedFd 被 drop,fd 随之关闭;0.8.6 之前这里会漏掉这个 fd(PR #1636「Fix a number of potential fd leaks」)。
try_clone 是 std 的 OwnedFd::try_clone,在 macOS 上是 fcntl(fd, F_DUPFD_CLOEXEC, 3)(std 的注释:「Avoid using file descriptors below 3 as they are used for stdio」):复制出的 fd 指向同一个 kqueue,原子地带上 CLOEXEC。复制品沿用同一个 id,因为在 SelectorId 检查看来它就是同一个 selector。调用它的有两处:Registry::try_clone(见 Poll 与 Registry)和 kqueue 平台的 Waker::new。
try_clone 的修复让 Waker::new 在 macOS 上失败了0.7.12 为了让复制出的 fd 也带 CLOEXEC,把 try_clone 从 dup(kq) 改成 fcntl(kq, F_DUPFD_CLOEXEC)(CHANGELOG:「Set FD_CLOEXEC when calling Registry::try_clone」)。F_DUPFD_CLOEXEC 要第三个参数(新 fd 的下限),这里漏了。issue #1497(「F_DUPFD_CLOEXEC doesn't work on macOS 11.3 (v0.7.12)」)报告它返回 Os { code: 22, kind: InvalidInput, message: "Invalid argument" },并指出后果:「tokio (v1.6) cannot run with feature = ["net"], as the Waker depends on cloning the Selector.」修复提交「Fix Selector::try_clone」的说明是:「Because we didn't provide a value it would take whatever value was left in the register from whatever code used it before the system call.」0.7.12 被撤回(CHANGELOG 标作 yanked),修复首次发布于 0.7.13(主线上是 PR #1498,进入 0.8.0)。1.0.0 换成 OwnedFd 时(PR #1745),这行变成了上面的 self.kq.try_clone(),参数交给 std 去填。
select:changelist 为空的 kevent
select 是一次 kevent(kq, NULL, 0, events, capacity, timeout):不改任何注册,只取事件,内核写了几项就 set_len 几项。Duration 换成 timespec、None 传空指针的细节在 Poll 与 Registry 的「kqueue:timespec,不会溢出」。
这里的 EINTR 会原样返回给 Poll::poll 的调用方(同一页的「EINTR:原样交给调用方」)。下面的 kevent_register 对 EINTR 的处理正好相反,原因在那一节。
register:一个方向一项
register 拼一个最多两项的 changelist:
pub fn register(&self, fd: RawFd, token: Token, interests: Interest) -> io::Result<()> {
let flags = libc::EV_CLEAR | libc::EV_RECEIPT | libc::EV_ADD;
// At most we need two changes, but maybe we only need 1.
let mut changes: [MaybeUninit<libc::kevent>; 2] =
[MaybeUninit::uninit(), MaybeUninit::uninit()];
let mut n_changes = 0;
if interests.is_writable() {
let kevent = kevent!(fd, libc::EVFILT_WRITE, flags, token.0);
changes[n_changes] = MaybeUninit::new(kevent);
n_changes += 1;
}
if interests.is_readable() {
let kevent = kevent!(fd, libc::EVFILT_READ, flags, token.0);
changes[n_changes] = MaybeUninit::new(kevent);
n_changes += 1;
}
// ... 关于 EPIPE 的注释,见下文
let changes = unsafe {
// This is safe because we ensure that at least `n_changes` are in
// the array.
slice::from_raw_parts_mut(changes[0].as_mut_ptr(), n_changes)
};
kevent_register(self.kq.as_raw_fd(), changes, &[libc::EPIPE as i64])
}
- 一个方向一个过滤器。 kqueue 里的一项由
(ident, filter)确定,读和写是两个独立的过滤器EVFILT_READ、EVFILT_WRITE,各有各的状态。epoll 一个 fd 只有一项,带一个事件掩码;这里关注两个方向就是两项,WRITABLE在前,READABLE在后。只关注一个方向时 changelist 只有一项,没关注的方向什么也不提交。 - 三个 flag。
EV_ADD:没有就加,有了就改(所以同一个 fdregister两次在 macOS 上成功,第二次换掉了udata)。EV_CLEAR:事件被取走后重置状态,只在状态再次变化时才报,这就是 kqueue 上的边沿触发,对应 epoll 的EPOLLET。EV_RECEIPT:下一节。 - token 在
udata。kevent!把token.0转成指针放进udata,事件回来时event::token原样转回usize。Interest的AIO、LIO位在这里没有人看。 MaybeUninit。 数组最多用两格,只关注一个方向时第二格从不初始化,from_raw_parts_mut只取前n_changes项交给内核。
kevent_register 与 EV_RECEIPT
三个注册方法最后都调 kevent_register,再由 check_errors 逐项检查:
fn kevent_register(
kq: RawFd,
changes: &mut [libc::kevent],
ignored_errors: &[i64],
) -> io::Result<()> {
syscall!(kevent(
kq,
changes.as_ptr(),
changes.len() as Count,
changes.as_mut_ptr(),
changes.len() as Count,
ptr::null(),
))
.map(|_| ())
.or_else(|err| {
// According to the manual page of FreeBSD: "When kevent() call fails
// with EINTR error, all changes in the changelist have been applied",
// so we can safely ignore it.
if err.raw_os_error() == Some(libc::EINTR) {
Ok(())
} else {
Err(err)
}
})
.and_then(|()| check_errors(changes, ignored_errors))
}
/// Check all events for possible errors, it returns the first error found.
fn check_errors(events: &[libc::kevent], ignored_errors: &[i64]) -> io::Result<()> {
for event in events {
// We can't use references to packed structures (in checking the ignored
// errors), so we need copy the data out before use.
let data = event.data as _;
// Check for the error flag, the actual error will be in the `data`
// field.
if (event.flags & libc::EV_ERROR != 0) && data != 0 && !ignored_errors.contains(&data) {
return Err(io::Error::from_raw_os_error(data as i32));
}
}
Ok(())
}
kevent 既提交 changelist,又可以同时取事件。mio 把同一个数组既当 changelist(changes.as_ptr())又当 eventlist(changes.as_mut_ptr()),容量和项数相同,超时是空指针。macOS 的 kevent(2) 手册对 EV_RECEIPT 的说明是:「This flag is useful for making bulk changes to a kqueue without draining any pending events. When passed as input, it forces EV_ERROR to always be returned. When a filter is successfully added, the data field will be zero.」每一项 change 都会在 eventlist 里得到一张「回执」:flags 带 EV_ERROR,data 是这一项的 errno,成功就是 0。check_errors 正是按这个约定读:EV_ERROR 且 data != 0 且不在忽略表里,就是错误,返回第一个。
如果不带 EV_RECEIPT,会发生两件事,手册的「RETURN VALUES」一节写得很清楚:成功的项不产生任何输出,eventlist 里装的是已经就绪的真实事件;出错的项只在 eventlist 还有空位时才以 EV_ERROR 的形式放进去,「Otherwise, -1 will be returned」。前者会把本该由 poll 取走的事件吞掉,后者让调用方分不清是哪一项错了。
EINTR 的那段注释引的是 FreeBSD 手册:kevent 被信号打断时,changelist 已经全部生效,所以当成功处理,之后照常 check_errors。数组里没有被内核改写的项还是 mio 自己填的输入:flags 里没有 EV_ERROR,data 是 0,不会被误判成错误。select 不这样做,是因为它的 EINTR 意味着等待被打断、一个事件也没取到,要交给调用方决定重试还是退出。
实验:有没有 EV_RECEIPT
src/bin/receipt.rs 不经过 mio,直接用 libc 按 kevent_register 的方式调 kevent(changelist 和 eventlist 是同一个数组)。socket 已经可读(对端写了 2 字节)也可写。每次调用之后,再用一次 changelist 为空、超时为 0 的 kevent 看看 select 此刻能取到什么:
// Raw kevent(2) calls, the way kqueue.rs makes them, with and without EV_RECEIPT.
use std::io::Write;
use std::net::{TcpListener, TcpStream};
use std::os::fd::AsRawFd;
use std::{mem, ptr};
use exp_mio_kqueue::fmt_kevent;
fn kev(fd: i32, filter: i16, flags: u16, udata: usize) -> libc::kevent {
libc::kevent {
ident: fd as libc::uintptr_t,
filter,
flags,
udata: udata as *mut libc::c_void,
..unsafe { mem::zeroed() }
}
}
// changelist and eventlist are the same buffer, like kevent_register
fn submit(kq: i32, changes: &mut [libc::kevent], nevents: usize) {
let zero = libc::timespec { tv_sec: 0, tv_nsec: 0 };
let n = unsafe {
libc::kevent(kq, changes.as_ptr(), changes.len() as i32, changes.as_mut_ptr(), nevents as i32, &zero)
};
if n < 0 {
println!(" kevent -> -1, errno={}", std::io::Error::last_os_error());
return;
}
println!(" kevent -> {n}");
for c in &changes[..n as usize] {
println!(" {}", fmt_kevent(c));
}
}
// kevent(kq, NULL, 0, buf, 8, 0): what select would get now
fn pending(kq: i32) {
let mut buf: [libc::kevent; 8] = unsafe { mem::zeroed() };
let zero = libc::timespec { tv_sec: 0, tv_nsec: 0 };
let n = unsafe { libc::kevent(kq, ptr::null(), 0, buf.as_mut_ptr(), 8, &zero) };
println!(" then select -> {n}");
for c in &buf[..n.max(0) as usize] {
println!(" {}", fmt_kevent(c));
}
}
fn main() {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let mut c = TcpStream::connect(l.local_addr().unwrap()).unwrap();
let (s, _) = l.accept().unwrap();
c.write_all(b"hi").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
let fd = s.as_raw_fd();
let base = libc::EV_ADD | libc::EV_CLEAR;
println!("socket fd={fd}, readable (2 bytes) and writable");
println!("1. EV_ADD|EV_CLEAR, no EV_RECEIPT, eventlist = changelist");
let kq = unsafe { libc::kqueue() };
submit(kq, &mut [kev(fd, libc::EVFILT_WRITE, base, 7), kev(fd, libc::EVFILT_READ, base, 7)], 2);
pending(kq);
println!("2. EV_ADD|EV_CLEAR|EV_RECEIPT, eventlist = changelist");
let kq = unsafe { libc::kqueue() };
let r = base | libc::EV_RECEIPT;
submit(kq, &mut [kev(fd, libc::EVFILT_WRITE, r, 7), kev(fd, libc::EVFILT_READ, r, 7)], 2);
pending(kq);
println!("3. EV_RECEIPT: bad fd 9999 (EV_ADD) + EV_DELETE of a filter never added");
let kq = unsafe { libc::kqueue() };
let d = libc::EV_DELETE | libc::EV_RECEIPT;
submit(kq, &mut [kev(9999, libc::EVFILT_READ, r, 7), kev(fd, libc::EVFILT_WRITE, d, 0)], 2);
// and EV_DELETE on the bad fd
submit(kq, &mut [kev(9999, libc::EVFILT_READ, d, 0)], 1);
println!("4. no EV_RECEIPT, nevents=0: bad fd 9999 first, good fd second");
let kq = unsafe { libc::kqueue() };
submit(kq, &mut [kev(9999, libc::EVFILT_READ, base, 7), kev(fd, libc::EVFILT_READ, base, 8)], 0);
pending(kq);
println!("5. EV_RECEIPT: pipe write end (read end closed), pipe read end (write end closed)");
let mut p1 = [0; 2];
let mut p2 = [0; 2];
unsafe { libc::pipe(p1.as_mut_ptr()); libc::close(p1[0]); libc::pipe(p2.as_mut_ptr()); libc::close(p2[1]); }
let kq = unsafe { libc::kqueue() };
submit(kq, &mut [kev(p1[1], libc::EVFILT_WRITE, r, 3), kev(p2[0], libc::EVFILT_READ, r, 2)], 2);
println!("6. mio: SourceFd(&9999) register / deregister");
let poll = mio::Poll::new().unwrap();
let bad = 9999;
let res = poll.registry().register(&mut mio::unix::SourceFd(&bad), mio::Token(7), mio::Interest::READABLE);
println!(" register {res:?}");
let res = poll.registry().deregister(&mut mio::unix::SourceFd(&bad));
println!(" deregister {res:?}");
}
fmt_kevent 是实验 crate 里的辅助函数,按名字打印 kevent 的各个字段。M2 Max、macOS、rustc 1.99.0,--release,跑了 3 次,输出相同:
socket fd=5, readable (2 bytes) and writable
1. EV_ADD|EV_CLEAR, no EV_RECEIPT, eventlist = changelist
kevent -> 2
ident=5 filter=WRITE flags=ADD|CLEAR fflags=0 data=146988 udata=7
ident=5 filter=READ flags=ADD|CLEAR fflags=0 data=2 udata=7
then select -> 0
2. EV_ADD|EV_CLEAR|EV_RECEIPT, eventlist = changelist
kevent -> 2
ident=5 filter=WRITE flags=ADD|CLEAR|RECEIPT|ERROR fflags=0 data=0 udata=7
ident=5 filter=READ flags=ADD|CLEAR|RECEIPT|ERROR fflags=0 data=0 udata=7
then select -> 2
ident=5 filter=WRITE flags=ADD|CLEAR|RECEIPT fflags=0 data=146988 udata=7
ident=5 filter=READ flags=ADD|CLEAR|RECEIPT fflags=0 data=2 udata=7
3. EV_RECEIPT: bad fd 9999 (EV_ADD) + EV_DELETE of a filter never added
kevent -> 2
ident=9999 filter=READ flags=ADD|CLEAR|RECEIPT|ERROR fflags=0 data=9 udata=7
ident=5 filter=WRITE flags=DELETE|RECEIPT|ERROR fflags=0 data=2 udata=0
kevent -> 1
ident=9999 filter=READ flags=DELETE|RECEIPT|ERROR fflags=0 data=2 udata=0
4. no EV_RECEIPT, nevents=0: bad fd 9999 first, good fd second
kevent -> -1, errno=Bad file descriptor (os error 9)
then select -> 0
5. EV_RECEIPT: pipe write end (read end closed), pipe read end (write end closed)
kevent -> 2
ident=11 filter=WRITE flags=ADD|CLEAR|RECEIPT|ERROR fflags=0 data=0 udata=3
ident=10 filter=READ flags=ADD|CLEAR|RECEIPT|ERROR fflags=0 data=0 udata=2
6. mio: SourceFd(&9999) register / deregister
register Err(Os { code: 9, kind: Uncategorized, message: "Bad file descriptor" })
deregister Ok(())
逐项看:
- 没有回执时,注册顺手把事件取走了。
kevent返回的 2 项是真实事件:data=146988是发送缓冲区剩余空间,data=2是可读字节数。因为带EV_CLEAR,这两个事件被取走后状态就重置了,紧接着的select一个也拿不到。在 mio 里这意味着register之后的第一次poll永远看不到这个 socket 早已就绪,边沿触发下也不会再来通知。 - 有回执时,事件留在队列里。 返回的 2 项都带
ERROR、data=0,是两张「成功」回执;真实事件原封不动,下一次select取到了。 - 错误是逐项的。 fd 9999 的
EV_ADD得到data=9(EBADF);删除一个从没加过的过滤器得到data=2(ENOENT)。kevent本身返回 2,并没有失败。对一个无效 fd 做EV_DELETE,macOS 给的也是ENOENT,不是EBADF。 - 没有回执又没有空位时只剩 -1。 第一项(fd 9999)失败,整次调用返回 -1 和
EBADF,看不出是哪一项;之后的select也拿不到第二项那个 socket 的事件。从结果看,第二项没有被应用(这个 socket 可读,加上了就会有事件)。 - 今天的 macOS 不再报
EPIPE。 对端已关闭的 pipe 两端,回执都是 0(下一小节)。 - mio 层面。
register把EBADF原样交出来;deregister拿到的是ENOENT,被忽略,返回Ok。
问: kevent_register 里 syscall!(kevent(...)).map(|_| ()) 把返回值直接扔了。kevent 返回 2,不就说明来了两个事件吗?
答: 带 EV_RECEIPT 时返回值是回执的张数,不是事件数,上面第 2、3 项都是 2,一次成功、一次两项全错。changelist 有几项就有几张回执(eventlist 的容量和 changelist 一样大,放得下),所以这个数没有信息量,成败只能看每张回执的 data。真正的事件从不出现在这里,它们留给 select。
忽略哪些错误
三个方法传给 kevent_register 的忽略表各不相同:
| 方法 | 忽略 | 原因(源码注释) |
|---|---|---|
register | EPIPE | 老版本 macOS 注册对端已关闭的 pipe 时报 EPIPE,但事件照样会来 |
reregister | ENOENT、EPIPE | 删一个本来就不在的过滤器;EPIPE 同上 |
deregister | ENOENT | 同上,删除的目标本来就是「不在」 |
EPIPE 那段注释原文是:「Older versions of macOS (OS X 10.11 and 10.10 have been witnessed) can return EPIPE when registering a pipe file descriptor where the other end has already disappeared. ... It also turns out that kevent will still report events on the file descriptor, telling us that it's readable/hup at least after we've done this registration. As a result we just ignore EPIPE here instead of propagating it.」它来自 issue #582:rust-lang/rust 的 CI 在 OSX 上用 sccache 编译时偶发「broken pipe」,猜测是 register 返回了 EPIPE。PR #584(「kqueue: Ignore EPIPE coming out of kevent」,首次发布于 0.6.7)说明:「On other platforms this is an ok operation which just returns that the pipe is readable/writable (to return an error/eof), so this brings the OSX behavior in line by ignoring the error and moving to the next event.」上面实验第 5 项表明 macOS 27 上这一项已经回 0,这条忽略规则在新系统上不会被触发。
ENOENT 是 mio 自己造出来的:reregister 和 deregister 不知道 fd 当初注册了哪几个方向,干脆两个都删,删到不存在的那个就是 ENOENT。下面两节讲。
一项失败,另一项照样生效
回执是逐项的,处理也是逐项的:changelist 里一项失败,不会让其他项回滚。check_errors 只返回第一个错误,不撤销已经成功的项。所以 kqueue 上 register 返回 Err,不等于什么都没注册。
kqueue.rs 末尾的测试 does_not_register_rw 提供了一个现成的例子:把一个 kqueue fd 注册进另一个 Poll,注释是「Registering kqueue fd will fail if write is requested (On anything but some versions of macOS).」kqueue fd 可以有可读事件(里面有待取的事件),但不支持 EVFILT_WRITE。src/bin/kqfd.rs 用三种 Interest 注册内层 Poll 的 fd,然后用一个 Waker 让内层 kqueue 有一个待取事件,再看外层 poll:
// Register a kqueue fd in another Poll, like the does_not_register_rw test
use std::os::fd::AsRawFd;
use std::time::Duration;
use exp_mio_kqueue::raw;
use mio::unix::SourceFd;
use mio::{Events, Interest, Poll, Token};
fn main() {
for (name, interest) in [
("READABLE", Interest::READABLE),
("WRITABLE", Interest::WRITABLE),
("READABLE|WRITABLE", Interest::READABLE.add(Interest::WRITABLE)),
] {
let mut poll = Poll::new().unwrap();
let inner = Poll::new().unwrap();
let r = poll.registry().register(&mut SourceFd(&inner.as_raw_fd()), Token(1), interest);
println!("{name:<18} register {r:?}");
// make the inner kqueue readable: it gets a pending event
let waker = mio::Waker::new(inner.registry(), Token(5)).unwrap();
waker.wake().unwrap();
let mut events = Events::with_capacity(8);
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
println!("{:<18} then poll: {} event(s)", "", events.iter().count());
for e in &events {
println!("{:<18} token={} {}", "", e.token().0, raw(e));
}
}
}
raw(e) 把 &Event 转成 &libc::kevent 打印字段(Event 是 #[repr(transparent)] 包着 kevent 的,这依赖私有布局,只用于观察)。
register 报错之后,poll 能收到事件吗?在 M2 Max、macOS 上跑上面的程序。WRITABLE 和 READABLE|WRITABLE 两种注册都会返回 Err(EINVAL)。内层 kqueue 随后有了一个待取事件。在 READABLE|WRITABLE 那一轮里,外层 poll 拿到几个事件?
reregister:两项都提交
reregister 不再按需拼 changelist,而是读、写两项都交:
pub fn reregister(&self, fd: RawFd, token: Token, interests: Interest) -> io::Result<()> {
let flags = libc::EV_CLEAR | libc::EV_RECEIPT;
let write_flags = if interests.is_writable() {
flags | libc::EV_ADD
} else {
flags | libc::EV_DELETE
};
let read_flags = if interests.is_readable() {
flags | libc::EV_ADD
} else {
flags | libc::EV_DELETE
};
let mut changes: [libc::kevent; 2] = [
kevent!(fd, libc::EVFILT_WRITE, write_flags, token.0),
kevent!(fd, libc::EVFILT_READ, read_flags, token.0),
];
// Since there is no way to check with which interests the fd was
// registered we modify both readable and write, adding it when required
// and removing it otherwise, ignoring the ENOENT error when it comes
// up. The ENOENT error informs us that a filter we're trying to remove
// wasn't there in first place, but we don't really care since our goal
// is accomplished.
//
// For the explanation of ignoring `EPIPE` see `register`.
kevent_register(
self.kq.as_raw_fd(),
&mut changes,
&[libc::ENOENT as i64, libc::EPIPE as i64],
)
}
要关注的方向 EV_ADD(已有就修改 udata,token 就是这样被换掉的),不要的方向 EV_DELETE。mio 不记录每个 fd 当前注册了什么(IoSourceState 在 kqueue 上是空结构),所以没法只删「原来有、现在不要」的那一个;两项都交,EV_DELETE 落空时的 ENOENT 当成功。代价是每次 reregister 固定两项,副作用是对一个从没注册过的 fd reregister 也会成功(Poll 与 Registry 的误用表)。
deregister 同理:两项都是 EV_DELETE | EV_RECEIPT,udata 填 0,只忽略 ENOENT,于是重复 deregister、deregister 一个无效 fd 都返回 Ok(上面实验第 6 项)。
PR #529(「Assorted kqueue fixes/improvements」)之前,mio 对没关注的方向也提交一项,带 EV_DISABLE:过滤器还在,只是不报。PR 的第一个提交「Do not kevent fds with EV_DISABLE when no interest」说这不等于没注册:「At the very least it makes kernel to still track the event in question, and only prevents it from reporting the event. This means that you cannot register EVFILT_WRITE + EV_DISABLE kevent for file descriptors which are known to always be read-only」,上面 kqueue fd 的 EINVAL 就是这种情况。评审时的疑问是:不用 EV_DISABLE,reregister 从读写改成只读时怎么让可写事件停下来?作者的回答是第二个提交「Do not allocate for kevents + precise errors」改用 EV_DELETE,并且「checks registration errors on a per-kevent basis, so it is able to precisely ignore the errors that arise when event that's being EV_DELETEd does not exist」,EV_RECEIPT 就是这时加进来的。这个 PR 在 GitHub 上是关闭状态,两个提交以 32f34cd、607fb54 直接进了主线,首次发布于 0.6.5。那时 register 和 reregister 还是同一段代码(不要的方向一律 EV_DELETE);PR #1021(「System module cleanup」,0.7.0)的「Optimize Selector::register to pass a single event to kevent when possible」才把 register 改成现在的「只提交关注的方向」。
实验:一个 socket 的一生
src/bin/lifecycle.rs 在同一个 socket 上依次 register、reregister、deregister,每一步之后 poll 一次(超时 100 ms),打印原始字段和 is_* 的结果(bools 列出为真的方法名)。「peer acks」那几步是本端写 1 字节、等 50 ms 让对端的 ACK 回来,发送缓冲区的状态因此变了一次:
// register R|W -> reregister R -> reregister R|W -> deregister, on one socket
use std::io::{Read, Write};
use std::thread::sleep;
use std::time::Duration;
use exp_mio_kqueue::{bools, raw};
use mio::net::TcpStream;
use mio::{Events, Interest, Poll, Token};
const RW: Interest = Interest::READABLE.add(Interest::WRITABLE);
fn show(poll: &mut Poll, title: &str) {
let mut events = Events::with_capacity(16);
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
println!("{title}: {} event(s)", events.iter().count());
for e in &events {
println!(" token={} {} => {}", e.token().0, raw(e), bools(e));
}
}
fn main() {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let mut 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();
let mut buf = [0u8; 64];
let pause = || sleep(Duration::from_millis(50));
c.write_all(b"hi").unwrap();
pause();
poll.registry().register(&mut s, Token(7), RW).unwrap();
show(&mut poll, "1. register R|W (2 bytes waiting)");
println!(" read {} bytes", s.read(&mut buf).unwrap());
s.write_all(b"x").unwrap();
pause();
show(&mut poll, "2. we write 1 byte, peer acks");
let r = poll.registry().reregister(&mut s, Token(8), Interest::READABLE);
println!("3. reregister READABLE, Token(8): {r:?}");
s.write_all(b"y").unwrap();
pause();
show(&mut poll, " we write 1 byte, peer acks");
c.write_all(b"abc").unwrap();
pause();
show(&mut poll, " peer writes 3 bytes");
println!(" read {} bytes", s.read(&mut buf).unwrap());
let r = poll.registry().reregister(&mut s, Token(8), Interest::READABLE);
println!("4. reregister READABLE again: {r:?}");
show(&mut poll, " (no I/O)");
let r = poll.registry().reregister(&mut s, Token(9), RW);
println!("5. reregister R|W, Token(9): {r:?}");
show(&mut poll, " (no I/O)");
let r = poll.registry().deregister(&mut s);
println!("6. deregister: {r:?}");
c.write_all(b"defg").unwrap();
s.write_all(b"z").unwrap();
pause();
show(&mut poll, " both sides write");
let r = poll.registry().deregister(&mut s);
println!("7. deregister again: {r:?}");
}
M2 Max、macOS、rustc 1.99.0,--release,跑了 3 次,输出相同:
1. register R|W (2 bytes waiting): 2 event(s)
token=7 ident=5 filter=WRITE flags=ADD|CLEAR|RECEIPT fflags=0 data=146988 udata=7 => writable
token=7 ident=5 filter=READ flags=ADD|CLEAR|RECEIPT fflags=0 data=2 udata=7 => readable
read 2 bytes
2. we write 1 byte, peer acks: 1 event(s)
token=7 ident=5 filter=WRITE flags=ADD|CLEAR|RECEIPT fflags=0 data=146988 udata=7 => writable
3. reregister READABLE, Token(8): Ok(())
we write 1 byte, peer acks: 0 event(s)
peer writes 3 bytes: 1 event(s)
token=8 ident=5 filter=READ flags=ADD|CLEAR|RECEIPT fflags=0 data=3 udata=8 => readable
read 3 bytes
4. reregister READABLE again: Ok(())
(no I/O): 0 event(s)
5. reregister R|W, Token(9): Ok(())
(no I/O): 1 event(s)
token=9 ident=5 filter=WRITE flags=ADD|CLEAR|RECEIPT fflags=0 data=146988 udata=9 => writable
6. deregister: Ok(())
both sides write: 0 event(s)
7. deregister again: Ok(())
- 第 1 步:两个事件,同一个 token。 先
WRITE后READ,各自只有一个方向的is_*为真。在 Linux 上同样的情况是一个epoll_event带IN|OUT(Token、Interest 与 Event)。事件的flags里还留着注册时的ADD|CLEAR|RECEIPT,那是内核记下的这一项的 flag,is_*不看它们。 - 第 2 步:可写也是边沿。 读掉数据后没有新的
READ。本端写 1 字节、ACK 回来,发送缓冲区变化了一次,EVFILT_WRITE又报了一次。 - 第 3 步:
EV_DELETE真的删掉了写过滤器。 同样的写 + ACK,这次 0 个事件;读事件照常,token 已经换成 8。 - 第 4 步: 再次
reregister(READABLE),写那一项的EV_DELETE落空,ENOENT被忽略,返回Ok。 - 第 5 步: 重新加回写过滤器,socket 本来就可写,新加的这一项立刻报一次
WRITE,token 是 9。 - 第 6、7 步:
deregister之后两边都写,0 个事件;再deregister一次也是Ok。
EVFILT_USER:Waker 的那一项
Waker 的整体设计、多次 wake 合并成一个事件、两个 Waker 互相覆盖,都在 Waker;1.2.3 里 #1987 去掉 setup 的经过在怎么读这一卷。这里只看两个 kevent 调用和上面的注册路径有什么不同。
wake 提交一项 (ident 0, EVFILT_USER),flag 是 EV_ADD | EV_ONESHOT,fflags = NOTE_TRIGGER,token 在 udata:
let kq = self.kq.as_raw_fd();
syscall!(kevent(kq, &kevent, 1, ptr::null_mut(), 0, ptr::null()))?;
Ok(())
- 不走
kevent_register,也没有EV_RECEIPT。 eventlist 传空、容量 0。按手册,出错的项在 eventlist 没有空位时让整个调用返回 -1,所以这一项的错误直接表现为syscall!的Err;成功时也不会把任何事件取出来(没有地方放)。只有一项,不存在「是哪一项错了」的问题。 EV_ONESHOT代替EV_CLEAR。 这一项被select取走一次就从 kqueue 里删除,下次wake再EV_ADD。这正是 #1987 能去掉 setup 的原因:每次都是「加上并触发」,不依赖事先存在的过滤器。- 用的是复制出来的 fd。
Waker持有的是try_clone出来的Selector,dup出来的 fd 指向同一个 kqueue,所以在它上面kevent等于在Poll的 kqueue 上操作。
FreeBSD、DragonFly、NetBSD、OpenBSD 上 Waker::new 还要先调一次 setup_waker:EV_ADD | EV_CLEAR | EV_RECEIPT 加一个 EVFILT_USER 项,自己检查那张回执(EV_ERROR 且 data != 0 才算错),没有经过 check_errors,也没有忽略表。
event 模块:一个 filter 一个方向
event 模块 把一个 kevent 解释成 Event 的各个 is_*。完整的对照表(和 epoll 并列)在 Token、Interest 与 Event 的「每个 is_* 方法查哪些位」,这里只列 kqueue 一侧的依据:
| 方法 | 看什么 |
|---|---|
is_readable | filter == EVFILT_READ;FreeBSD 和 Apple 各系统上 EVFILT_USER 也算 |
is_writable | filter == EVFILT_WRITE |
is_error | flags 含 EV_ERROR;或含 EV_EOF 且 fflags != 0 |
is_read_closed | filter == EVFILT_READ 且含 EV_EOF |
is_write_closed | filter == EVFILT_WRITE 且含 EV_EOF |
is_priority | 恒为 false(「kqueue doesn't have priority indicators.」) |
is_* 都只看 filter、flags、fflags,不看 data。data 里的可读字节数、发送缓冲区剩余空间,mio 不用。
is_error 的条件来自一句注释:「When the read end of the socket is closed, EV_EOF is set on flags, and fflags contains the error if there is one.」Event::is_error 的文档表格写的却是「EV_ERROR and EV_EOF with fflags set to 0」,和代码相反,以代码为准(Token、Interest 与 Event 已经指出过)。手册里 EV_ERROR 用于 changelist 中出错的项和回执;本页实验里 poll 取回的事件从没带过它,连接上的错误都表现为 EV_EOF 加 fflags。
实验:FIN、RST 和关了一端的 pipe
src/bin/eof.rs:服务端 socket 以 READABLE | WRITABLE 注册并取走初始的可写事件,然后对端正常 close()(FIN),或设 SO_LINGER 为 0 再 close()(RST);再把对端已关闭的 pipe 两端用 SourceFd 注册:
// EV_EOF and fflags: peer FIN vs peer RST; then pipes with the other end closed
use std::os::fd::AsRawFd;
use std::thread::sleep;
use std::time::Duration;
use exp_mio_kqueue::{bools, raw};
use mio::net::TcpStream;
use mio::unix::SourceFd;
use mio::{Events, Interest, Poll, Token};
const RW: Interest = Interest::READABLE.add(Interest::WRITABLE);
fn show(poll: &mut Poll, title: &str) {
let mut events = Events::with_capacity(16);
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
println!("{title}: {} event(s)", events.iter().count());
for e in &events {
println!(" {} => {}", raw(e), bools(e));
}
}
fn pair(poll: &mut 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);
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(); // initial WRITE
(s, c)
}
fn main() {
let mut poll = Poll::new().unwrap();
let (_s, c) = pair(&mut poll);
drop(c);
sleep(Duration::from_millis(50));
show(&mut poll, "1. peer close() (FIN)");
let mut poll = Poll::new().unwrap();
let (_s, c) = pair(&mut poll);
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 u32);
}
drop(c);
sleep(Duration::from_millis(50));
show(&mut poll, "2. peer close() with SO_LINGER 0 (RST)");
let mut fds = [0; 2];
unsafe { libc::pipe(fds.as_mut_ptr()) };
unsafe { libc::close(fds[1]) };
let mut poll = Poll::new().unwrap();
let r = poll.registry().register(&mut SourceFd(&fds[0]), Token(2), Interest::READABLE);
println!("3. pipe read end, write end closed: register {r:?}");
show(&mut poll, " poll");
unsafe { libc::pipe(fds.as_mut_ptr()) };
unsafe { libc::close(fds[0]) };
let mut poll = Poll::new().unwrap();
let r = poll.registry().register(&mut SourceFd(&fds[1]), Token(3), Interest::WRITABLE);
println!("4. pipe write end, read end closed: register {r:?}");
show(&mut poll, " poll");
}
M2 Max、macOS、rustc 1.99.0,--release,跑了 3 次,输出相同:
1. peer close() (FIN): 1 event(s)
ident=6 filter=READ flags=ADD|CLEAR|RECEIPT|EOF fflags=0 data=0 udata=1 => readable read_closed
2. peer close() with SO_LINGER 0 (RST): 2 event(s)
ident=8 filter=WRITE flags=ADD|CLEAR|RECEIPT|EOF fflags=54 data=146988 udata=1 => writable error write_closed
ident=8 filter=READ flags=ADD|CLEAR|RECEIPT|EOF fflags=54 data=0 udata=1 => readable error read_closed
3. pipe read end, write end closed: register Ok(())
poll: 1 event(s)
ident=5 filter=READ flags=ADD|CLEAR|RECEIPT|EOF fflags=0 data=0 udata=2 => readable read_closed
4. pipe write end, read end closed: register Ok(())
poll: 1 event(s)
ident=10 filter=WRITE flags=ADD|CLEAR|RECEIPT|EOF fflags=0 data=0 udata=3 => writable write_closed
- FIN 只影响读方向。 一个
READ事件带EV_EOF,fflags=0,于是read_closed而不是error。写过滤器没有报:对端只是不再发,本端仍然可以写。 - RST 两个方向都报,而且是错误。 两个事件都带
EV_EOF,fflags=54,也就是 macOS 的ECONNRESET。按代码的条件(fflags != 0)两个都是error;按文档表格的写法(fflags为 0)就都不是了。同一个连接的一次重置,用户会以同一个 token 收到两个is_error()为真的事件。 - 关了一端的 pipe。 注册成功(今天的 macOS 不报
EPIPE),随后的事件正是 #584 说的「readable/writable (to return an error/eof)」:EV_EOF、fflags=0,只有read_closed或write_closed。
动脑筋答案
前面 kqfd.rs 的输出,M2 Max、macOS、rustc 1.99.0,--release,跑了 3 次,输出相同:
READABLE register Ok(())
then poll: 1 event(s)
token=1 ident=4 filter=READ flags=ADD|CLEAR|RECEIPT fflags=0 data=1 udata=1
WRITABLE register Err(Os { code: 22, kind: InvalidInput, message: "Invalid argument" })
then poll: 0 event(s)
READABLE|WRITABLE register Err(Os { code: 22, kind: InvalidInput, message: "Invalid argument" })
then poll: 1 event(s)
token=1 ident=4 filter=READ flags=ADD|CLEAR|RECEIPT fflags=0 data=1 udata=1
答案是 1 个。
READABLE:一项EVFILT_READ,成功。内层 kqueue 有一个待取事件后,外层收到READ(data=1)。WRITABLE:一项EVFILT_WRITE,回执是EINVAL(kqueue fd 不支持写过滤器),什么也没注册上,0 个事件。READABLE|WRITABLE:changelist 是[WRITE, READ]。WRITE那张回执是EINVAL,check_errors把它作为第一个错误返回;但READ那一项已经成功加进了外层 kqueue,不会被撤销。于是register报错,而这个 fd 实际上以Token(1)注册了读方向,之后照常来事件。
调用方如果把 Err 理解成「没有注册」,转头把这个 fd 关掉或者把 token 给别的 source,这一项就会在 kqueue 里留到 fd 关闭为止,期间带着旧 token 报事件。Linux 上一次 epoll_ctl 要么整个成功要么整个失败,没有这种中间状态。
小结
kqueue.rs供 Apple 五个系统和 FreeBSD、DragonFly、NetBSD、OpenBSD 共用(未设mio_unsupported_force_poll_poll时),平台差异靠Count/Filter/Flags三个类型别名和少数几处 cfg。new是kqueue()加fcntl(FD_CLOEXEC);try_clone是OwnedFd::try_clone(F_DUPFD_CLOEXEC),0.7.12 里漏掉第三个参数曾让 macOS 上的Waker::new失败(#1497)。select是 changelist 为空的kevent。register每个关注的方向一项,EVFILT_WRITE在前,flag 是EV_ADD | EV_CLEAR | EV_RECEIPT;EV_CLEAR是边沿触发,EV_ADD遇到已有项就修改。EV_RECEIPT让每一项都有一张回执(EV_ERROR加data里的 errno),注册不会顺手吞掉待取的事件;check_errors返回第一个不在忽略表里的错误。EINTR在注册时当成功,在select时原样返回。- 忽略表:
register忽略EPIPE(老 macOS 的 pipe 问题,#584),reregister忽略ENOENT和EPIPE,deregister忽略ENOENT。reregister/deregister总是两项都交,不要的方向EV_DELETE(#529 以前用的是EV_DISABLE)。 - 一项失败不会撤销其他项:kqueue 上
register返回Err时,可能已经注册了一个方向。 wake是一次不带回执的kevent:(0, EVFILT_USER)、EV_ADD | EV_ONESHOT、NOTE_TRIGGER,错误直接表现为 -1。- 一个
kevent只有一个方向:读写同时就绪是两个事件、一个 token;FIN 只给READ加EV_EOF,RST 给两个方向都加EV_EOF和fflags=54,两个都是is_error()。