跳转到正文
zeno's blog
返回

asio(五):操作系统I/O多路复用-epoll、kqueue、IOCP如何被统一

专题: Asio

Table of contents

Open Table of contents

TL;DR

Asio 通过编译期条件选择 reactor 实现(epoll_reactor / kqueue_reactor / win_iocp_io_context),每种实现共享统一的内部接口(register_descriptorstart_opcancel_opsruninterrupt)。Linux/macOS 是 Reactor 之上模拟 Proactor,Windows 是原生 Proactor 直通 IOCP。io_uring 作为第四种后端在 Boost 1.78+ 可选启用。


平台选择机制

config.hpp 中的宏在编译期决定使用哪个后端:

编译期选择链:

#if defined(BOOST_ASIO_HAS_IOCP)
  → win_iocp_io_context (Windows)
#elif defined(BOOST_ASIO_HAS_IO_URING) && !defined(BOOST_ASIO_DISABLE_EPOLL)
  → io_uring_service (Linux, 需要手动启用)
#elif defined(BOOST_ASIO_HAS_EPOLL)
  → epoll_reactor (Linux)
#elif defined(BOOST_ASIO_HAS_KQUEUE)
  → kqueue_reactor (macOS/BSD)
#else
  → select_reactor (兜底)
#endif

所有 reactor 实现共享统一接口

方法职责
register_descriptor(fd)注册文件描述符到多路复用器
start_op(fd, op_type, handler)发起异步操作
cancel_ops(fd)取消某个 fd 上的所有操作
run(timeout)阻塞等待事件,分发完成事件
interrupt()唤醒阻塞的 run()
schedule_timer(...)注册定时器

Linux:epoll_reactor

头文件asio/detail/epoll_reactor.hpp

核心设计

边缘触发 vs 水平触发

模式触发条件Asio 的选择
水平触发(LT)只要 fd 可读就反复通知不用
边缘触发(ET)仅在状态变化时通知一次✓ 使用

边缘触发减少了 epoll_wait 返回的事件数量,但要求用户在收到通知后读完所有数据(否则不会再通知)。Asio 内部处理了这个复杂性——它在收到 ET 通知后会循环读取直到 EAGAIN

投机执行(Speculative Execution)

async_read_some(socket, buffer, handler) 的内部路径:

1. 检查 descriptor_state.try_speculative_
   ├─ true → 先尝试直接 read()
   │         ├─ 成功 → 直接入队完成事件,跳过 epoll 注册
   │         └─ EAGAIN → 注册到 epoll,等下次通知
   └─ false → 直接注册到 epoll

投机执行在以下场景有效:

定时器集成

epoll_reactor 的 run() 循环:

1. 调用 timer_queue_.get_ready_timers(),取出所有到期 timer
2. 计算下一个 timer 的超时时间
3. 用这个超时作为 epoll_wait 的 timeout 参数
4. epoll_wait 返回(I/O 事件或超时)
5. 处理 I/O 事件
6. 回到步骤 1

在可用时,timerfd_create 会被用于更精确的定时器集成。

唤醒(Interrupt)机制

当需要唤醒阻塞在 epoll_wait 上的线程时(比如新的 handler 被 post 到了队列),Asio 通过 eventfd(或 pipe)发出通知:

interrupt():
  eventfd_write(interrupter_fd_, 1)  // 写入一个事件
  → epoll_wait 返回
  → 检查任务队列中的新 handler

macOS/BSD:kqueue_reactor

头文件asio/detail/kqueue_reactor.hpp

核心设计

与 epoll 的区别

维度epollkqueue
事件注册epoll_ctl 添加/修改/删除kevent 同时用于注册和等待
事件类型位掩码(EPOLLIN | EPOLLOUT)独立过滤器(EVFILT_READ, EVFILT_WRITE)
边缘触发EPOLLET 标志EV_CLEAR 标志
信号处理需要 signalfd原生 EVFILT_SIGNAL
文件系统不支持(仅 socket/pipe)EVFILT_VNODE 监控文件变化

kqueue 比 epoll 更通用(支持文件系统事件、进程事件等),但 Asio 主要使用它的网络 I/O 能力。

Windows:IOCP 原生 Proactor

实现detail::win_iocp_io_context

与 Linux/macOS 完全不同的架构

Linux (模拟 Proactor):
  io_context → scheduler → epoll_reactor → epoll

  handler 在 scheduler 的任务队列中等待

Windows (原生 Proactor):
  io_context → win_iocp_io_context → IOCP

  handler 在 IOCP 的完成队列中等待(OS 管理)

工作流程

  1. 每个 io_context 创建/关联一个 I/O Completion Port
  2. 异步操作使用 Windows 的重叠 I/O 函数:AcceptExReadFile(带 OVERLAPPED)、WriteFile(带 OVERLAPPED
  3. 操作系统内核在后台执行 I/O 并将完成包投递到 IOCP
  4. run() 调用 GetQueuedCompletionStatus 出队完成事件

没有模拟层——Asio 的 Proactor API 直接映射到 IOCP 语义。

特殊情况

Windows 上有一个后台 select_reactor 线程,用于处理不支持重叠 I/O 的操作(如 connect() 在某些场景下)。这是 Asio 在 Windows 上唯一隐式创建的线程。

io_uring:第四种后端(Boost 1.78+)

启用方式:定义 ASIO_HAS_IO_URINGASIO_DISABLE_EPOLL,链接 -luring

为什么 io_uring 值得关注

io_uring 是 Linux 内核提供的真正的异步 I/O 接口——不再是 Reactor,而是原生 Proactor:

维度epollio_uring
模型Reactor(就绪通知)Proactor(完成通知)
I/O 执行者应用程序内核
系统调用次数每次 I/O 至少 2 次(epoll_wait + read)可以 0 系统调用(SQPOLL 模式)
Asio 映射模拟 Proactor原生 Proactor,零模拟开销
io_uring 工作原理:

  应用程序              共享内存环              内核
  ┌─────┐          ┌─────────────┐         ┌─────┐
  │     │───SQE──→│ Submission  │───────→│     │
  │     │          │ Queue       │         │ I/O │
  │     │          ├─────────────┤         │执行  │
  │     │←──CQE──│ Completion  │←───────│     │
  │     │          │ Queue       │         │     │
  └─────┘          └─────────────┘         └─────┘

SQE = Submission Queue Entry(提交请求)
CQE = Completion Queue Entry(完成结果)

当 Asio 使用 io_uring 后端时,不再需要模拟 Proactor——async_read 直接提交 SQE,内核完成后通过 CQE 通知。

当前限制

线程安全保证总结

操作线程安全
多线程调用 io_context::run()✓ 安全
多线程操作同一个 socket✗ 不安全(需要 strand)
多线程操作不同 socket✓ 安全
io_context::stop()✓ 任意线程调用安全
io_context::restart()✗ 不能与 run() 并发
io_context 构造/析构✗ 不是线程安全的
strand 分发的 handler✓ 保证不并发执行

Asio 的基本线程安全规则:不同的 I/O 对象可以从不同线程并发操作;同一个 I/O 对象的并发访问需要 strand 保护。

关键源文件索引

组件头文件
平台检测asio/detail/config.hpp
epoll reactorasio/detail/epoll_reactor.hpp
kqueue reactorasio/detail/kqueue_reactor.hpp
IOCP contextasio/detail/win_iocp_io_context.hpp
io_uring serviceasio/detail/io_uring_service.hpp
select reactorasio/detail/select_reactor.hpp
schedulerasio/detail/scheduler.hpp
timer queueasio/detail/timer_queue.hpp
descriptor stateasio/detail/reactor_op.hpp

分享这篇文章:

上一篇
asio(六):十大陷阱与生产实践
下一篇
asio(四):异步模型演进-从回调到C++20协程