非阻塞 I/O、epoll 与 Reactor / Nonblocking I/O, epoll, and Reactor
1. 为什么阻塞模型不够
阻塞 echo 服务适合入门,但一个连接卡在 recv 上,整个线程就无法处理其他连接。要服务大量连接,你需要把“等待某个 fd 就绪”从业务处理里抽出来,交给内核事件通知。
blocking per connection:
thread waits in recv
event driven:
one loop waits for many fd events
each connection advances a small state machine2. 非阻塞是什么意思
非阻塞不是“永远立即成功”,而是“如果当前条件不满足,系统调用不要睡眠等待,而是返回 EAGAIN/EWOULDBLOCK”。
recv(nonblocking fd)
bytes available -> return n
peer closed -> return 0
no bytes now -> -1, errno = EAGAIN
fatal error -> -1, errno = ECONNRESET/...所以非阻塞程序必须维护状态:这次没读完,下次可读事件继续;这次没写完,下次可写事件继续。
3. epoll 解决什么
epoll 是 Linux 的 I/O 就绪通知机制。你把关心的 fd 注册进去,然后 epoll_wait 返回哪些 fd 当前可能可读、可写、出错或关闭。
epoll_create1
-> epoll_ctl ADD listen_fd
-> loop:
epoll_wait
for each event:
if listen_fd readable: accept until EAGAIN
if conn readable: read until EAGAIN or close
if conn writable: flush output buffer注意“可能可读/可写”。事件通知不是业务完成通知。拿到事件后仍要循环读写,直到 EAGAIN、完成或致命错误。
4. 水平触发和边缘触发
Level-triggered(水平触发,LT)像提醒:只要条件仍满足,下次还会提醒。Edge-triggered(边缘触发,ET)像门铃:状态从不可读变成可读时提醒一次,你必须把数据读到 EAGAIN。
LT:
fd has 100 bytes
read 10 bytes
still has 90 bytes
epoll_wait may report again
ET:
fd becomes readable
epoll reports once
if you read only 10 bytes and stop
remaining 90 bytes may not trigger another eventET 模式通常要求 fd 非阻塞,并在 read/write 中循环到 EAGAIN。否则很容易“明明有数据但程序不再收到事件”。
5. Reactor 是什么
Reactor 是一种结构,不是 Linux 专有 API。它把 I/O 就绪事件分发给对应 handler,由 handler 推进连接状态机。
EventLoop
owns epoll fd
waits events
dispatches Channel
Channel
wraps one fd and interested events
Connection
owns input buffer, output buffer, parser state, close state6. 连接状态机
事件驱动代码最怕回调里散落状态。每个连接至少要有明确状态:
Accepting
-> ReadingRequest
-> Processing
-> WritingResponse
-> Closing
-> Closed如果读缓冲已经拿到半条请求,就留在 ReadingRequest;如果输出缓冲没有写完,就注册可写事件进入 WritingResponse。
7. 输入和输出缓冲
非阻塞网络程序不能把 recv 的临时 buffer 当成请求。连接对象需要 input buffer 保存跨事件的数据,也需要 output buffer 保存暂时写不出去的响应。
on_readable:
read bytes into input buffer until EAGAIN
parse zero or more complete frames
keep incomplete frame in buffer
on_writable:
send output buffer until EAGAIN or empty
if empty: disable writable interest8. accept 也要循环
监听 socket 可读表示有新连接可 accept,但可能不止一个。非阻塞服务端通常在事件到来后循环 accept,直到 EAGAIN。
for (;;) {
int fd = accept4(listen_fd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (fd >= 0) { add_connection(fd); continue; }
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
if (errno == EINTR) continue;
log_fatal_accept_error(errno);
break;
}9. 最小 epoll 循环骨架
int ep = epoll_create1(EPOLL_CLOEXEC);
epoll_event ev{};
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(ep, EPOLL_CTL_ADD, listen_fd, &ev);
std::array<epoll_event, 1024> events;
while (running) {
int n = epoll_wait(ep, events.data(), events.size(), timeout_ms);
for (int i = 0; i < n; ++i) {
if (events[i].data.fd == listen_fd) accept_all();
else dispatch_connection(events[i]);
}
}10. 背压
如果对端读得慢,你的 send buffer 和应用 output buffer 会堆积。背压就是不要无限接收新工作,而要限制每个连接和全局的未发送字节数。
read faster than peer can receive
-> output buffer grows
-> memory pressure
-> latency spike
-> process killed or service stalls常见策略:达到高水位线后暂停读取、拒绝大请求、降级响应、关闭慢连接或把计算任务排队限制。
11. 定时器和超时
事件循环不能只等 socket。连接空闲、请求头读取、请求体读取、写响应、优雅关闭都需要 deadline。Linux 下可以用 timerfd,也可以在 event loop 中维护小根堆或时间轮。
12. 多线程 Reactor
常见结构是 one loop per thread:一个 acceptor 接受连接,再把 connected fd 分配给某个 I/O loop。不要让多个线程同时读写同一个 fd,除非你明确设计同步和所有权。
main thread
accept new connection
choose worker loop
handoff fd
worker loop
owns connection state
reads/writes/parses13. 常见错误
- ET 模式没有读到 EAGAIN。
- 忘记把 connected fd 设置成非阻塞。
- 一直注册 EPOLLOUT,导致 busy loop。
- output buffer 无限增长,没有背压。
- 关闭 fd 后没有从 epoll 和连接表移除。
- 在回调中做长时间 CPU 任务,卡住整个 event loop。
- 多线程同时操作同一个 connection 状态。
14. 调试方法
事件循环问题要同时看 fd 状态、事件兴趣、缓冲区长度和连接状态。日志只写“read error”不够,至少要记录 fd、connection id、events mask、errno、input/output buffer size。
strace -e epoll_wait,epoll_ctl,accept4,recvfrom,sendto ./server
ss -antp | grep 8080
perf top
tcpdump -i any port 8080 -nn15. 练习
- 把阻塞 echo 服务改成非阻塞 epoll echo。
- 故意在 ET 模式只读一次,观察连接卡住。
- 给每个连接加 output buffer 高水位线。
- 加入 30 秒 idle timeout。
- 把 CPU 处理移到线程池,I/O loop 只负责读写。
16. 总结
非阻塞 I/O 的核心不是 API,而是状态机。epoll 告诉你 fd 可能就绪,Reactor 把事件分派给连接对象,连接对象用输入缓冲、输出缓冲、协议解析状态、超时和关闭状态维持正确性。
深化切片 1:最小实验
围绕 nonblocking 和 EAGAIN 设计一个小实验。把一个复杂网络现象拆成一个客户端、一个服务端和一条可重复命令。先确认基础行为,再加并发、超时和异常输入。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 2:抓包观察
围绕 EAGAIN 和 epoll 设计一个小实验。用 tcpdump 或 Wireshark 保存原始报文,确认看到的是 SYN、ACK、FIN、RST、payload 还是应用层解析错误。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 3:错误路径
围绕 epoll 和 LT/ET 设计一个小实验。每个系统调用都要处理 EINTR、EAGAIN、ECONNRESET、ETIMEDOUT、EPIPE 等错误,并保留上下文。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 4:资源上限
围绕 LT/ET 和 Reactor 设计一个小实验。连接数、fd 数、缓冲区、请求大小、队列长度和超时都是协议设计的一部分。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 5:关闭顺序
围绕 Reactor 和 buffer 设计一个小实验。网络程序要区分停止接收新连接、排空已有连接、发送剩余响应、关闭写方向、释放 fd。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 6:跨平台
围绕 buffer 和 backpressure 设计一个小实验。Linux 的 epoll、BSD/macOS 的 kqueue、Windows 的 IOCP 模型不同,C++ 抽象应隐藏平台差异但暴露语义差异。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 7:生产诊断
围绕 backpressure 和 timeout 设计一个小实验。日志要包含 connection id、peer address、状态、读写字节数、错误码和关闭原因。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 8:安全边界
围绕 timeout 和 nonblocking 设计一个小实验。任何来自网络的长度、头字段、路径、编码和压缩数据都不可信,必须先限制再分配。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 9:最小实验
围绕 nonblocking 和 EAGAIN 设计一个小实验。把一个复杂网络现象拆成一个客户端、一个服务端和一条可重复命令。先确认基础行为,再加并发、超时和异常输入。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 10:抓包观察
围绕 EAGAIN 和 epoll 设计一个小实验。用 tcpdump 或 Wireshark 保存原始报文,确认看到的是 SYN、ACK、FIN、RST、payload 还是应用层解析错误。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 11:错误路径
围绕 epoll 和 LT/ET 设计一个小实验。每个系统调用都要处理 EINTR、EAGAIN、ECONNRESET、ETIMEDOUT、EPIPE 等错误,并保留上下文。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 12:资源上限
围绕 LT/ET 和 Reactor 设计一个小实验。连接数、fd 数、缓冲区、请求大小、队列长度和超时都是协议设计的一部分。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 13:关闭顺序
围绕 Reactor 和 buffer 设计一个小实验。网络程序要区分停止接收新连接、排空已有连接、发送剩余响应、关闭写方向、释放 fd。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 14:跨平台
围绕 buffer 和 backpressure 设计一个小实验。Linux 的 epoll、BSD/macOS 的 kqueue、Windows 的 IOCP 模型不同,C++ 抽象应隐藏平台差异但暴露语义差异。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 15:生产诊断
围绕 backpressure 和 timeout 设计一个小实验。日志要包含 connection id、peer address、状态、读写字节数、错误码和关闭原因。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 16:安全边界
围绕 timeout 和 nonblocking 设计一个小实验。任何来自网络的长度、头字段、路径、编码和压缩数据都不可信,必须先限制再分配。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 17:最小实验
围绕 nonblocking 和 EAGAIN 设计一个小实验。把一个复杂网络现象拆成一个客户端、一个服务端和一条可重复命令。先确认基础行为,再加并发、超时和异常输入。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 18:抓包观察
围绕 EAGAIN 和 epoll 设计一个小实验。用 tcpdump 或 Wireshark 保存原始报文,确认看到的是 SYN、ACK、FIN、RST、payload 还是应用层解析错误。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 19:错误路径
围绕 epoll 和 LT/ET 设计一个小实验。每个系统调用都要处理 EINTR、EAGAIN、ECONNRESET、ETIMEDOUT、EPIPE 等错误,并保留上下文。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 20:资源上限
围绕 LT/ET 和 Reactor 设计一个小实验。连接数、fd 数、缓冲区、请求大小、队列长度和超时都是协议设计的一部分。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 21:关闭顺序
围绕 Reactor 和 buffer 设计一个小实验。网络程序要区分停止接收新连接、排空已有连接、发送剩余响应、关闭写方向、释放 fd。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 22:跨平台
围绕 buffer 和 backpressure 设计一个小实验。Linux 的 epoll、BSD/macOS 的 kqueue、Windows 的 IOCP 模型不同,C++ 抽象应隐藏平台差异但暴露语义差异。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 23:生产诊断
围绕 backpressure 和 timeout 设计一个小实验。日志要包含 connection id、peer address、状态、读写字节数、错误码和关闭原因。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 24:安全边界
围绕 timeout 和 nonblocking 设计一个小实验。任何来自网络的长度、头字段、路径、编码和压缩数据都不可信,必须先限制再分配。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 25:最小实验
围绕 nonblocking 和 EAGAIN 设计一个小实验。把一个复杂网络现象拆成一个客户端、一个服务端和一条可重复命令。先确认基础行为,再加并发、超时和异常输入。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 26:抓包观察
围绕 EAGAIN 和 epoll 设计一个小实验。用 tcpdump 或 Wireshark 保存原始报文,确认看到的是 SYN、ACK、FIN、RST、payload 还是应用层解析错误。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 27:错误路径
围绕 epoll 和 LT/ET 设计一个小实验。每个系统调用都要处理 EINTR、EAGAIN、ECONNRESET、ETIMEDOUT、EPIPE 等错误,并保留上下文。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 28:资源上限
围绕 LT/ET 和 Reactor 设计一个小实验。连接数、fd 数、缓冲区、请求大小、队列长度和超时都是协议设计的一部分。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 29:关闭顺序
围绕 Reactor 和 buffer 设计一个小实验。网络程序要区分停止接收新连接、排空已有连接、发送剩余响应、关闭写方向、释放 fd。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 30:跨平台
围绕 buffer 和 backpressure 设计一个小实验。Linux 的 epoll、BSD/macOS 的 kqueue、Windows 的 IOCP 模型不同,C++ 抽象应隐藏平台差异但暴露语义差异。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。
深化切片 31:生产诊断
围绕 backpressure 和 timeout 设计一个小实验。日志要包含 connection id、peer address、状态、读写字节数、错误码和关闭原因。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,非阻塞 I/O、epoll 与 Reactor 就还没有真正掌握。