必须用epoll_cloexec避免子进程继承无效epoll fd导致静默失败和资源泄漏;et模式需循环读写至eagain/ewouldblock;epoll_wait事件数组须动态扩容防截断和栈溢出;accept须非阻塞循环直至eagain,并立即设client fd为非阻塞。

epoll_create1() 为什么要用 EPOLL_CLOEXEC
不加 EPOLL_CLOEXEC,fork 子进程后,子进程会继承 epoll fd,但通常不打算在子进程里用它——结果是父进程 close 掉 epoll fd 后,子进程还拿着一个无效句柄,后续 epoll_ctl 或 epoll_wait 可能静默失败或触发 EBADF。更隐蔽的问题是:子进程 exit 时没显式 close,内核资源泄漏(尤其长周期守护进程反复 fork)。
- 始终用
epoll_create1(EPOLL_CLOEXEC),别用过时的epoll_create - 如果必须用
epoll_create(老内核),手动调fcntl(epoll_fd, F_SETFD, FD_CLOEXEC) -
EPOLL_CLOEXEC不影响当前进程行为,只控制 fork 行为,零成本
ET 模式下必须循环读写直到 EAGAIN 或 EWOULDBLOCK
ET(Edge Triggered)不是“通知一次就完事”,而是“状态变化才通知”。比如 socket 缓冲区从空变非空,epoll 才唤醒;但如果你只 read 一次就停,剩下数据还在内核缓冲区里,而状态没再变,下次 epoll_wait 就不会再次提醒你——连接就卡死在半读状态。
- read 场景:循环
recv(fd, buf, len, MSG_DONTWAIT),直到返回-1且errno == EAGAIN或EWOULDBLOCK - write 场景同理:循环
send(fd, buf, len, MSG_DONTWAIT),直到全发完或遇到EAGAIN - 千万别在 ET 模式下用阻塞 socket,否则一次 read/send 就卡住整个 event loop
- 注意
MSG_DONTWAIT是 per-call 标志,比全局设O_NONBLOCK更安全(避免其他库误用)
fd 数量暴涨时,epoll_wait 返回数组大小不能硬写 1024
很多人直接开个 struct epoll_event events[1024],觉得够用。但单机百万连接下,一次 epoll_wait 可能就活跃几千事件——栈上分配 1024 个 epoll_event(每个 12 字节)才 12KB,看似安全,但实际容易踩两个坑:
- 内核返回事件数超过数组长度,
epoll_wait截断,丢事件(不报错!) - 高并发下频繁栈分配大数组,可能触发栈溢出(尤其协程栈小或嵌套深时)
- 更糟的是:有人把 events 数组 malloc 在堆上但复用不清理,导致旧事件残留干扰逻辑
正确做法:用 std::vector<epoll_event></epoll_event> 动态扩容,初始 reserve 1024,每次调用前 clear,size() 传给 epoll_wait;或者用 mmap 分配固定大页(如 64KB),自己管理 offset。
立即学习“C++免费学习笔记(深入)”;
accept() 必须非阻塞 + 循环收完所有新连接
监听 socket 设了 O_NONBLOCK 后,accept() 可能返回 -1 且 errno == EAGAIN,表示“这次没有新连接可取”。但很多人只 accept 一次就回去等下一轮 epoll_wait,结果漏掉同一时刻到达的多个 SYN —— Linux 的 listen backlog 队列可能积压多个已完成三次握手的连接,一次 accept 只取一个。
- 只要
accept()成功,就继续 while 循环,直到返回-1且errno == EAGAIN - 新 accept 出来的 client fd 必须立刻设
O_NONBLOCK,否则后续 ET 模式下 read/write 会阻塞 - 记得检查
accept()返回的 fd 是否超出RLIMIT_NOFILE,超限要关连接并打日志,否则 fd 耗尽后整个服务假死
大规模连接真正难的不是 epoll 本身,而是每个细节都得和内核行为对齐:ET 模式、非阻塞、边缘触发语义、fd 生命周期、错误码分支覆盖——少一个,流量高峰时就变成随机丢连接或 CPU 空转。








