ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

epoll 的 LT、ET-block socket、ET-no-block socket:三种模式配置骨架与验证动作

epoll 的 LT、ET-block socket、ET-no-block socket:三种模式配置骨架与验证动作 1. 从一次回显服务的“卡死”说起如果你在 Linux 上写过高并发网络服务大概率绕不开 epoll。它是什么一句话epoll 是 Linux 内核提供的一种 IO 多路复用机制让一个线程就能同时盯住成千上万个 socket 的读写状态。它能做什么把“哪个连接有数据可读、哪个连接可写”这件事委托给内核去检测你的程序只需要处理真正就绪的那批 fd。适合谁写 TCP 服务器、网关、即时通讯后端、自定义协议服务的同学尤其是被 C10K 甚至 C100K 连接数折磨过的人。我见过太多人在回显服务里踩同一个坑用 LT 模式跑得好好的一改成 ET 就发现客户端发来的长数据只回了一半或者服务端直接卡在read上不动了。根因往往不是 epoll 本身而是没搞清楚 LT、ET-block、ET-no-block 这三种组合在事件注册和 socket 阻塞属性上的差异。这篇就聚焦这个场景用回显服务把三种模式的配置骨架、验证动作、排障要点一次讲透正文会给出可复制的参数清单和压测验证步骤帮你定位 ET 模式下的读写循环与 EAGAIN 处理。2. 三种模式到底差在哪一张表先建立直觉在动手写代码前先把行为差异钉死。epoll 的事件注册靠struct epoll_event的events字段默认是水平触发LT加上EPOLLET就变成边沿触发ET。而 socket 是否阻塞由fcntl的O_NONBLOCK决定跟 epoll 是两套独立开关。模式events 标志socket 属性通知行为典型风险LTEPOLLIN阻塞或非阻塞均可缓冲区有数据就反复通知效率略低但不易丢数据ET-blockEPOLLIN | EPOLLET阻塞状态变化只通知一次循环读时阻塞在 read饿死其他 fdET-no-blockEPOLLIN | EPOLLET非阻塞状态变化只通知一次必须处理 EAGAIN否则误判断开注意ET 模式的核心契约是“内核只在你上次没读完、这次又有新数据时才再通知”。所以你必须一次把缓冲区读干净而“读干净”的标志就是read返回 -1 且errno EAGAIN。LT 的语义是“只要读缓冲区还有数据epoll_wait就会一直返回这个 fd”。这解释了为什么 LT 模式下即使你每次只读 5 字节剩下的数据下一轮还会被通知不会丢。而 ET 只在你“从无到有”或“从读完到又有新数据”时通知一次读不完的部分如果没在本次循环里处理掉就可能永远等不到下一次通知。3. TaoToken 前置把模型对话和接入文档放到手边写这类底层代码时我习惯把调试辅助和文档查询的入口准备好。TaoToken 提供模型对话、Coding Plan、API Keys 和接入文档几个入口适合在写 epoll 服务时顺手查参数、验证协议行为。想直接和模型对话确认 epoll 语义或让它帮你 review 读写循环走模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewrite长期写网络编程、Agent 类项目需要稳定的编码额度看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewrite要生成和管理调用凭证进控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewrite创建 API Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewrite接入文档含接口说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewrite如果你用 Claude Code 这类工具做辅助开发Anthropic 兼容入口在这里https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewriteAPI 基址统一是 https://taotoken.net/api 注意这个地址不带 UTM 参数配置时直接填即可。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。4. 可复制配置三种模式的 epoll 注册骨架下面用回显服务echo server作为统一场景监听 8888 端口收到数据原样回发。三种模式共用epoll_create、epoll_ctl、epoll_wait三件套差异集中在事件注册和通信循环。4.1 公共骨架epoll 三件套#include sys/epoll.h #include fcntl.h #include errno.h int epfd epoll_create(1); // size 已废弃填 0 即可 struct epoll_event ev, evs[128]; ev.events EPOLLIN; ev.data.fd lfd; // 监听 fd 先挂上 epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, ev); while (1) { int n epoll_wait(epfd, evs, 128, -1); // -1 表示一直阻塞 for (int i 0; i n; i) { int curfd evs[i].data.fd; if (curfd lfd) { int cfd accept(lfd, NULL, NULL); // 三种模式的差异从这里开始 } else { // 通信处理三种模式差异最大 } } }4.2 LT 模式默认行为最省心LT 不需要额外标志socket 保持默认阻塞即可。通信循环里读一次、回一次就行剩下的数据下一轮epoll_wait会继续通知。// accept 后注册 ev.events EPOLLIN; // 不加 EPOLLET 就是 LT ev.data.fd cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, ev); // 通信循环 char buf[1024] {0}; int num read(curfd, buf, sizeof(buf)); if (num 0) { write(curfd, buf, num); } else if (num 0) { epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL); close(curfd); }4.3 ET-block能跑但埋着雷ET-block 只加EPOLLETsocket 仍是阻塞。问题在于ET 要求你循环读到 EAGAIN但阻塞 socket 在数据读完后read会直接挂起而不是返回 -1。结果就是当前 fd 把整个线程卡死其他连接全部饿死。ev.events EPOLLIN | EPOLLET; // 边沿触发 ev.data.fd cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, ev); // 通信循环这段在 ET-block 下会阻塞 char buf[512]; int len; while ((len read(curfd, buf, sizeof(buf))) 0) { write(curfd, buf, len); } // 数据读完后 read 不返回卡在这里4.4 ET-no-block生产环境该用的组合正确做法是把通信 fd 设成非阻塞再配合 ET。这样read读完后返回 -1 且errno EAGAIN循环自然退出线程继续处理其他 fd。// accept 后立刻设置非阻塞 int flag fcntl(cfd, F_GETFL); flag | O_NONBLOCK; fcntl(cfd, F_SETFL, flag); ev.events EPOLLIN | EPOLLET; ev.data.fd cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, ev); // 通信循环读到 EAGAIN 为止 char buf[512]; int len; while ((len read(curfd, buf, sizeof(buf))) 0) { write(STDOUT_FILENO, buf, len); write(curfd, buf, len); } if (len 0) { printf(client disconnect\n); epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL); close(curfd); } else if (errno EAGAIN || errno EWOULDBLOCK) { printf(data drained\n); // 正常数据读完了 } else { perror(read); // 真正的错误 }注意EAGAIN和EWOULDBLOCK在 Linux 上值相同但写上两个更可移植。判断顺序必须是先判len 0对端关闭再判 EAGAIN最后才是真错误。5. 验证请求用压测动作确认三种模式行为光看代码不够得用实际动作验证。我试过用nc和自写客户端配合观察服务端打印的epoll_wait返回次数。5.1 验证 LT 的“反复通知”在 LT 服务端里每次epoll_wait返回都打印一次计数。客户端一次性发 20 字节服务端每次只读 5 字节# 客户端 printf 1234567890abcdefghij | nc 127.0.0.1 8888你会看到服务端打印多次“epoll 返回第 N 次”每次读走 5 字节直到读完。这证明 LT 在缓冲区有数据时会持续通知。5.2 验证 ET 的“只通知一次”同样客户端发 20 字节ET 服务端如果只读一次 5 字节你会发现剩下的 15 字节再也没被处理epoll_wait不再返回该 fd。这就是 ET 丢数据的经典现象。5.3 验证 ET-no-block 的“读干净”把通信循环改成while读到 EAGAIN再发 20 字节服务端会一次性把 20 字节全部回显并打印“data drained”。用strace -e traceread,write跟踪能看到read返回 -1 EAGAIN 后循环退出。5.4 压测脚本骨架# 并发 100 连接每连接发 1KB for i in $(seq 1 100); do (head -c 1024 /dev/zero | nc 127.0.0.1 8888 /dev/null) done wait观察 ET-no-block 服务端 CPU 占用平稳、无连接饿死ET-block 则可能在某个连接上卡住后续连接无响应。6. 本篇常见错排查6.1 read 返回 -1 就 exit把 EAGAIN 当致命错误这是 ET-no-block 最常见的坑。数据读完后read返回 -1errno是 EAGAIN这是正常状态不是错误。如果你直接perror加exit服务就崩了。正确做法是单独判断 EAGAIN 分支。6.2 ET 模式下忘了设非阻塞线程被 read 挂起表现是服务端处理完第一个连接后第二个连接完全没反应。用gdbattach 上去看堆栈会发现卡在read系统调用。解决就是 accept 后立刻fcntl设O_NONBLOCK。6.3 只注册 EPOLLIN却想写大块数据ET 模式下写事件也要注意。如果你注册了EPOLLOUT写缓冲区不满时 LT 会一直通知ET 只在“从满到不满”时通知一次。大块数据要自己维护发送偏移量配合EPOLLOUT重试。6.4 忘记处理对端 RST压测时可能看到read: Connection reset by peer。这表示对端发了 TCP RST通常是客户端异常退出。服务端应捕获这个错误清理 fd而不是让整个进程退出。6.5 epoll_ctl DEL 后没 closeEPOLL_CTL_DEL只是把 fd 从 epoll 树摘掉不会关闭 fd。漏掉close会导致 fd 泄漏长跑服务最终耗尽描述符。两件事要成对出现。7. 继续验证与接入三种模式的骨架到这里就完整了。LT 适合快速原型和对丢数据零容忍的场景ET-no-block 是高性能服务的标配但必须把 EAGAIN 处理写对ET-block 基本只用于理解原理生产别用。如果你想用模型帮你 review 这段读写循环或者让它生成对应的压测客户端可以直接走模型对话入口https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewrite 。长期写网络编程和 Agent 项目需要稳定编码额度看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewrite 。接入前先创建 API Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewrite 参数细节对照接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentepoll_lt_et_socketutm_campaignrewrite 。API 基址是 https://taotoken.net/api 配置时直接填。
返回列表