普通 CQE · one-shot read/write
凑满后 wakeIOU_F_TWQ_LAZY_WAKE
cq_wait_nr--;只有减到 0 才 wake
从 FAST_POLL 的唤醒回调出发,追踪 work 挂在哪里、谁来执行, 直到 DEFER_TASKRUN 与新的 MPSC FIFO。
recv 返回 -EAGAIN,请求进入等待路径。
io-wq work item 保存请求;独立 worker task 调用阻塞式 recv,睡在 socket 等待队列上。
每个正在等待的请求都占用 worker 的调度实体和内核栈。FAST_POLL 用 socket wait entry 保存等待状态。
命中 io-wq 后,阻塞只是从用户线程转移给内核 worker,仍会占用调度实体和内核栈,真异步的优势没有发挥出来。倘若能像 epoll 那样 arm poll 呢?
func(req)。
那么,是谁来执行这些填充 CQE 的回调?
T.task_works
task_work_run(),逐项调用 func(req)
task_work 是目标 task 上的回调队列。 “消费队列”指摘下待执行节点并逐个调用,直到队列为空。
callback 已经挂到 T.task_works。T 仍在远端 CPU 的用户态运行时,内核怎样提醒它回来执行?
poll callback 把 callback 节点链接到 T.task_works。此时 work 已经入队,尚未执行。
task_work_add(..., true) 设置 notify-resume 标志。T 正在远端 CPU 运行时,kick_process(T) 发送 reschedule IPI;IPI 本身不调用 callback。
T 进入内核并经过返回路径,随后调用 task_work_run():摘下待执行链表,逐个调用 callback。
T 正在运行时,通知路径包含 reschedule IPI。T 已经阻塞时,callback 队列保持不变,通知动作改为显式唤醒。
如果 task 此时处于挂起状态呢?
callback 的承载位置仍为 T.task_works。
notify-resume 标志已经设置。阻塞中的 T 不满足 task_curr(T),因此 kick_process 不发送 reschedule IPI;io_uring 随后调用 wake_up_process(T)。
T 变为 runnable。调度器若要唤醒空闲 CPU,可能发送调度器自己的 wakeup IPI;这条 IPI 用于安排调度,不是强制打断正在运行的 T。
Linux 5.7 的两个分支都会尽快调度 T。Linux 5.19 的 COOP_TASKRUN 保留 T.task_works,同时跳过强制 reschedule IPI。
默认 task_work 总想尽快唤回 T。完成频繁时,running T 会反复收到 reschedule IPI,额外的 CPU 打断也会成为开销。
callback 继续由 T.task_works 承载,执行者仍为 T。
TWA_SIGNAL_NO_IPI 设置通知标志并跳过 kick_process。远端完成不会为这批 callback 强制打断 T。
T 下一次进入内核时消费 callback 队列。T 停留在用户态的时间会直接计入完成延迟。
COOP_TASKRUN 仍依赖 task_work 的运行时机:callback 要等 T 下一次进入内核才能执行,这段等待缺乏可预测性。
completion CPU 把 callback 节点追加到 io_ring_ctx 的本地队列。
更多完成可以继续入队;ring 设置 IORING_SQ_TASKRUN,期间不触发 task_work 通知或 reschedule IPI。
single issuer 观察到标志并调用 io_uring_enter(GETEVENTS),随后进入 ring 的消费路径。
issuer T 依次取出 work 1、2、3,在自己的内核上下文中调用 callback,并发布对应 CQE。
队列消费完毕后清除 TASKRUN 状态。callback 的承载者是 ring,运行者仍为 single issuer T。
进入 io_cqring_wait() 后,先消费已经挂在 ring 上的 local task_work。
drain 完才读取 CQ:已经达到 min_complete 就直接返回,不进入睡眠。
仍不足时计算 nr_wait,写入 cq_wait_nr,把当前 task 设为 TASK_INTERRUPTIBLE 后 schedule。
外部完成上下文只负责入队;lazy 计数归零或 non-lazy 抢到本轮 wake 权后,调用 wake_up_state()。
schedule 返回后先恢复 TASK_RUNNING,解除本轮计数,再次 drain ring-local task_work。
最后重新检查真实 CQ 数量:够了返回 userspace;不够则更新剩余数量并进入下一轮。
read #1 的 completion 入队,atomic_dec_and_test(3) 得到 2:还不 wake。
write #2 再扣一次,2 → 1:issuer 继续睡,producer 立即返回自己的完成路径。
read #3 令 1 → 0:这个 producer 赢得本轮唯一一次 wake,唤醒 single issuer。
issuer 醒来后 drain 三个 callback,各发布一个 CQE;连同原有 1 个,真实 CQ 数达到 4。
IOU_F_TWQ_LAZY_WAKE
cq_wait_nr--;只有减到 0 才 wake
poll_exclusive → EPOLLEXCLUSIVE
REQ_F_POLL_NO_LAZY
普通 poll wake 仍带 LAZY_WAKE
一次 task_work add 只扣 1
IO_REQ_LINK_FLAGS
入队前主动 clear LAZY_WAKE
cq_wait_nr 初值 = 欠缺 CQE 数;每个 lazy task_work add 扣 1。
立刻 wake 只叫醒 issuer;producer 不跑 callback。
wait 策略解决“何时叫醒 consumer”;接下来再看多个 completion CPU 如何把 task_work 并发写入同一个 FIFO。
锁少,但消费者每批都要反转链表恢复 FIFO。
生产者争用一次 tail;消费者按 FIFO 前进,不再聚合后 reverse。
算法把固定的 O(n) 反转成本拿掉,但交换 tail 与发布 prev->next 之间出现一个短暂窗口。
MPSC 拿掉了 reverse,但更新 tail 和发布 prev->next 分成两步。consumer 恰好撞进这两步之间,会把队列误判为空吗?
① producer 准备 B:先令 B.next = NULL。
② xchg(tail, B) 已完成;consumer 此刻看到 A.next == NULL,但 head != tail。
③ producer 用 release-store 发布 A.next = B;consumer 的 acquire-load 现在能看见 B。
④ consumer 弹出 A 并前移 head。发布窗口只会短暂重试,不能误判 empty。