Rust in Everything
基于 mio 1.2.4

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 各系统FreeBSDDragonFly、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。

📝NOTE
冷知识:一次 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:没有就加,有了就改(所以同一个 fd register 两次在 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(())

逐项看:

  1. 没有回执时,注册顺手把事件取走了。 kevent 返回的 2 项是真实事件:data=146988 是发送缓冲区剩余空间,data=2 是可读字节数。因为带 EV_CLEAR,这两个事件被取走后状态就重置了,紧接着的 select 一个也拿不到。在 mio 里这意味着 register 之后的第一次 poll 永远看不到这个 socket 早已就绪,边沿触发下也不会再来通知。
  2. 有回执时,事件留在队列里。 返回的 2 项都带 ERROR、data=0,是两张「成功」回执;真实事件原封不动,下一次 select 取到了。
  3. 错误是逐项的。 fd 9999 的 EV_ADD 得到 data=9(EBADF);删除一个从没加过的过滤器得到 data=2(ENOENT)。kevent 本身返回 2,并没有失败。对一个无效 fd 做 EV_DELETE,macOS 给的也是 ENOENT,不是 EBADF。
  4. 没有回执又没有空位时只剩 -1。 第一项(fd 9999)失败,整次调用返回 -1 和 EBADF,看不出是哪一项;之后的 select 也拿不到第二项那个 socket 的事件。从结果看,第二项没有被应用(这个 socket 可读,加上了就会有事件)。
  5. 今天的 macOS 不再报 EPIPE。 对端已关闭的 pipe 两端,回执都是 0(下一小节)。
  6. mio 层面。 register 把 EBADF 原样交出来;deregister 拿到的是 ENOENT,被忽略,返回 Ok。
📝NOTE
没有蠢问题

问: kevent_register 里 syscall!(kevent(...)).map(|_| ()) 把返回值直接扔了。kevent 返回 2,不就说明来了两个事件吗?

答: 带 EV_RECEIPT 时返回值是回执的张数,不是事件数,上面第 2、3 项都是 2,一次成功、一次两项全错。changelist 有几项就有几张回执(eventlist 的容量和 changelist 一样大,放得下),所以这个数没有信息量,成败只能看每张回执的 data。真正的事件从不出现在这里,它们留给 select。

忽略哪些错误

三个方法传给 kevent_register 的忽略表各不相同:

方法忽略原因(源码注释)
registerEPIPE老版本 macOS 注册对端已关闭的 pipe 时报 EPIPE,但事件照样会来
reregisterENOENT、EPIPE删一个本来就不在的过滤器;EPIPE 同上
deregisterENOENT同上,删除的目标本来就是「不在」

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 的,这依赖私有布局,只用于观察)。

❗IMPORTANT
动脑筋: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 项)。

📝NOTE
冷知识:去掉一个方向,曾经靠的是 EV_DISABLE

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_readablefilter == EVFILT_READ;FreeBSD 和 Apple 各系统上 EVFILT_USER 也算
is_writablefilter == EVFILT_WRITE
is_errorflags 含 EV_ERROR;或含 EV_EOF 且 fflags != 0
is_read_closedfilter == EVFILT_READ 且含 EV_EOF
is_write_closedfilter == 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()。

💬 关联讨论

发起讨论
加载中...