Table of contents
Open Table of contents
TL;DR
Asio 通过编译期条件选择 reactor 实现(epoll_reactor / kqueue_reactor / win_iocp_io_context),每种实现共享统一的内部接口(register_descriptor、start_op、cancel_ops、run、interrupt)。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
核心设计
- 使用
EPOLLET(边缘触发)模式 - 每个注册的 fd 关联一个
descriptor_state,包含三个操作队列:read、write、except - 唤醒机制:
eventfd(可用时)或 pipe
边缘触发 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
投机执行在以下场景有效:
- 对端已经发送了数据,socket 缓冲区有数据
- 高吞吐场景,数据到达速率高于处理速率
定时器集成
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
核心设计
- 使用
EVFILT_READ和EVFILT_WRITEkevent 过滤器 - 注册时使用
EV_ADD | EV_CLEAR(等效于边缘触发) descriptor_state中的num_kevents_跟踪每个 fd 注册了几个事件(读=1,读+写=2)
与 epoll 的区别
| 维度 | epoll | kqueue |
|---|---|---|
| 事件注册 | 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 管理)
工作流程
- 每个
io_context创建/关联一个 I/O Completion Port - 异步操作使用 Windows 的重叠 I/O 函数:
AcceptEx、ReadFile(带OVERLAPPED)、WriteFile(带OVERLAPPED) - 操作系统内核在后台执行 I/O 并将完成包投递到 IOCP
run()调用GetQueuedCompletionStatus出队完成事件
没有模拟层——Asio 的 Proactor API 直接映射到 IOCP 语义。
特殊情况
Windows 上有一个后台 select_reactor 线程,用于处理不支持重叠 I/O 的操作(如 connect() 在某些场景下)。这是 Asio 在 Windows 上唯一隐式创建的线程。
io_uring:第四种后端(Boost 1.78+)
启用方式:定义 ASIO_HAS_IO_URING 和 ASIO_DISABLE_EPOLL,链接 -luring
为什么 io_uring 值得关注
io_uring 是 Linux 内核提供的真正的异步 I/O 接口——不再是 Reactor,而是原生 Proactor:
| 维度 | epoll | io_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 通知。
当前限制
- 需要 Linux 5.10+ 内核
- 需要 liburing 库
- 不是所有 Asio 操作都支持 io_uring(部分仍回退到 epoll)
- 安全审计仍在进行(io_uring 在内核中的攻击面较大)
线程安全保证总结
| 操作 | 线程安全 |
|---|---|
多线程调用 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 reactor | asio/detail/epoll_reactor.hpp |
| kqueue reactor | asio/detail/kqueue_reactor.hpp |
| IOCP context | asio/detail/win_iocp_io_context.hpp |
| io_uring service | asio/detail/io_uring_service.hpp |
| select reactor | asio/detail/select_reactor.hpp |
| scheduler | asio/detail/scheduler.hpp |
| timer queue | asio/detail/timer_queue.hpp |
| descriptor state | asio/detail/reactor_op.hpp |