Rust in Everything
基于 tokio 1.54.1

IO 驱动

调度器那一部分讲了任务怎么被取出来 poll。这一部分讲任务在 IO 上返回 Pending 之后,是谁、在什么时候把它叫醒的。

源码主要在 runtime/io/,加上 io-uring 部分和测试约 2000 行:driver.rs 是反应器本体,scheduled_io.rs 是每个 socket 一份的就绪状态,registration.rs 和 io/poll_evented.rs 是 TcpStream 这类类型和反应器之间的胶水。底下的系统调用全交给 mio:Linux 上是 epoll,macOS 上是 kqueue,Windows 上是 IOCP 加 AFD。

  1. 反应器:Driver 和 Handle 各拿着什么;turn() 一次做哪几步;token 怎么变回 ScheduledIo 的指针;注销的 ScheduledIo 为什么要等到下一次 turn() 才释放;忙的 worker 多久查一次 IO。
  2. ScheduledIo 与就绪状态:一个 usize 里装着 16 位就绪位、15 位 tick 和 1 位关闭标志;tick 防的是哪一种竞争;读、写各一个 waker 槽,其余等待者放进侵入式链表。
  3. 从 TcpStream::read 到挂起:顺着真实代码走一遍 IO 主线,配一个能看到 Pending、wake、再次 poll 顺序的实验。
  4. (unstable)io-uring:它要解决什么,1.54.1 做到了哪一步。

整条 IO 主线:

flowchart TB
  read["TcpStream::poll_read"] --> ready{"readiness 里有可读位?"}
  ready -->|有| sys["read 系统调用"]
  sys -->|读到数据| done[返回 Ready]
  sys -->|WouldBlock| clear["clear_readiness"]
  clear --> ready
  ready -->|没有| store["waker 存进 reader 槽"]
  store --> pending[返回 Pending]
  pending --> park[worker 空闲,park 在驱动上]
  park --> poll["mio poll 返回事件"]
  poll --> set["set_readiness:tick 加 1"]
  set --> wake["wake:任务进队列"]
  wake --> read

图里上半部分在任务的线程上,属于任务自己的 poll;下半部分在拿到驱动的那个 worker 上,属于驱动的 turn()。两边只通过 ScheduledIo 里的一个原子变量和一把小锁交换信息。

前置阅读:任务被唤醒后进哪个队列,见 worker 主循环;worker 怎么抢到驱动、睡在 epoll_wait 上,见 任务窃取与空闲 和 驱动栈与 park。

💬 关联讨论

发起讨论
加载中...