Rust in Everything
基于 mio 1.2.4

常见坑

每一章对着一个报错或一个现象:先用最小的程序把它复现出来,Linux(epoll,容器里跑,必要时带 strace)和 macOS(kqueue)各跑一遍,再到源码里找它从哪一行来,最后给出修法、检查办法,以及 tokio 在这里替你做了什么。机制在前面几部分讲过的,这里只链过去。

mio 的坑大多有同一个来源:它只做「把就绪从内核取回来」这一件事(mio 解决什么问题),文档里写成「unspecified」的地方,结果就是平台 selector 的结果。所以同一个误用,在 debug 和 release、Linux 和 macOS 上常常表现不同。

  1. 没读到 WouldBlock 就回去 poll,程序卡住:边沿触发只报一次,一次事件只读一块、只 accept 一个,剩下的数据和连接就再也等不到事件;两个平台都卡住,poll(2) 后备更严。
  2. I/O source already registered with a Registry:同一个 source 注册两次或注册到两个 Poll,debug 下是 mio 自己的报错,Linux release 是 File exists (os error 17),macOS release 返回 Ok;tokio 的 AsyncFd 绕过了这道检查。
  3. from_std 之后事件循环卡在 read 里:from_std 不设非阻塞,读干净后的那次 read 阻塞住整个循环;tokio 1.44.0 起在 debug 下直接 panic。
  4. Socket not connected (os error 107):connect 返回 Ok 不等于连上;先 peer_addr 后 take_error,Linux 上被拒的连接会永远「还在连」;只注册 READABLE 等不到成功。
  5. drop 之后还收到旧 token 的事件:fd 被 dup 过时,Linux 上旧 token 继续触发,fd 号复用后一次 poll 里新旧 token 同时出现;macOS 关 fd 即移除。
  6. Only a single Waker can be active per Poll instance:debug 下 panic;release 下 epoll 两个都能用,kqueue 和 poll(2) 只剩后一个。
  7. wake() 了,poll 却等到超时:Linux 上这次唤醒丢了,macOS 还在;poll(2) 后备里 register 会吞掉没被取走的 wake()。
  8. 可读事件读到 0 字节,或者事件里只有错误位:对端关闭、连着的 UDP 发到关闭的端口、只关注写时对端 shutdown(Write),两个平台给的位不一样,都要 take_error。
  9. register 报 Invalid argument,事件照样来:kqueue 逐项处理 changelist、不回滚,失败的 register 可能已经装上了一半。
  10. 同一个 token 一次 poll 来两个事件:同一个 socket 关注读写,Linux 1 个、macOS 2 个;Events::with_capacity(0) 在 Linux 上是 EINVAL、在 macOS 上是忙等;poll(2) 后备不看容量。

按现象找:

flowchart TB
  s{看到了什么} -->|"poll 一直不返回"| h{线程在哪}
  h -->|"在 epoll_wait 或 kevent"| p1["1 没读到 WouldBlock"]
  h -->|"在 read 或 recvfrom"| p3["3 from_std 阻塞"]
  h -->|"等 Waker 的事件"| p7["6 和 7 Waker"]
  s -->|"register 返回错误"| r{哪个错误}
  r -->|"already registered"| p2["2 注册两次"]
  r -->|"kqueue 上的 Err"| p9["9 半截注册"]
  s -->|"连接状态不对"| p4["4 connect 未完成"]
  s -->|"事件对不上"| e{哪种}
  e -->|"旧 token 还来"| p5["5 没 deregister"]
  e -->|"读到 0 或只有错误位"| p8["8 关闭与错误"]
  e -->|"个数和预想不同"| p10["10 事件数"]

💬 关联讨论

发起讨论
加载中...