1. 从一次线上抖动说起为什么 select 撑不住高并发很多做 Linux 服务端的朋友都经历过这样的场景单机连接数刚过千CPU 的 sys 占用就飙到 30% 以上strace一看全是select在反复扫描 fd 集合。这不是代码写得差而是 select 的模型天生如此——每次调用都要把整个 fd 集合从用户态拷进内核态内核再线性遍历一遍返回时再拷回来。连接数越多这份无效功越大。epoll 就是为解决这个问题而生的。它是 Linux 下 IO 多路复用的第三代接口核心能力是让一个线程同时盯住几十万个 socket只在真正有数据可读可写时才被唤醒。适合谁写网关、IM 长连接服务、Redis/Nginx 这类中间件、以及任何需要单机扛住万级以上并发连接的场景。这篇文章我会先把 select/poll/epoll 的机制差异讲透重点拆开 epoll 的红黑树 就绪链表设计然后给出一份能直接跑的 epoll 服务端骨架最后结合 TaoToken 的统一 Key/API 通道演示怎么把 AI 编码工具接进来让写这类底层代码时少踩坑。全程可复制、可验证。2. select、poll、epoll 的机制差异到底在哪2.1 select 和 poll 的共同瓶颈select 用fd_set位图表示关注的 fd上限被FD_SETSIZE卡死在 1024默认。poll 换成了pollfd数组去掉了数量上限但两者有个致命共性无状态。所谓无状态是指内核不记得你上次关注了哪些 fd。每次调用select/poll你都得把完整列表重新传一遍内核重新遍历一遍。复杂度是 O(n)n 是监听的 fd 总数而不是活跃 fd 数。一个 10 万连接、同一时刻只有 100 个活跃的服务select 每次都要扫 10 万个99.9% 是白干。2.2 epoll 的三个系统调用epoll 把这套流程拆成了三步关键是把注册和等待分离int epoll_create1(int flags); // 创建 epoll 实例返回一个 fd int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 增删改关注的事件 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待就绪事件epoll_ctl的 op 有三个宏EPOLL_CTL_ADD注册新 fd、EPOLL_CTL_MOD修改监听事件、EPOLL_CTL_DEL移除 fd。epoll_event结构里events是关注的事件掩码data是用户数据常用data.fd存 fd或data.ptr存自定义结构指针。常用事件掩码对照掩码含义EPOLLIN可读含对端正常关闭EPOLLOUT可写EPOLLERR发生错误EPOLLHUP对端挂断EPOLLET边缘触发模式EPOLLONESHOT只通知一次需重新注册2.3 红黑树 就绪链表epoll 高效的本质调用epoll_create时内核创建一个eventpoll结构体里面有两个关键成员一棵红黑树rbr和一条就绪链表rdllist。红黑树存的是所有被监控的 fd封装成epitem。用红黑树而不是哈希或数组是因为它增删查都是 O(log n)且能高效去重——同一个 fd 重复 ADD 会被识别出来。就绪链表存的是已经发生事件、等着被取走的 fd。真正的魔法在回调机制当你epoll_ctl(ADD)一个 socket 时内核会在这个 socket 的等待队列上注册一个回调。当网卡收到数据、中断处理把数据拷进内核缓冲区后回调被触发把这个 fd 对应的epitem挂到就绪链表上。于是epoll_wait被调用时只需要看就绪链表空不空非空就把链表里的节点拷到用户态数组返回空就 sleep 到超时。整个过程和监听的 fd 总数无关只和活跃 fd 数相关。这就是为什么 epoll 在大量连接、少量活跃的 WAN 场景下碾压 select/poll而在所有连接都活跃的高速 LAN 场景优势并不明显甚至因为epoll_ctl的额外开销略慢。3. LT 与 ET两种触发模式的行为差异LT水平触发是默认模式。只要缓冲区里还有数据没读完下次epoll_wait还会通知你。编程简单不容易漏事件相当于更快的 poll。ET边缘触发只在状态变化的那一刻通知一次。如果一次没把数据读干净剩下的数据不会再有新通知除非对端又发了新数据。所以 ET 必须配合非阻塞 socket并且用循环读到read返回EAGAIN为止。// ET 模式下的标准读法 while (1) { ssize_t count read(fd, buf, sizeof(buf)); if (count -1) { if (errno ! EAGAIN) { perror(read); /* 真错误 */ } break; // EAGAIN 表示读干净了 } else if (count 0) { break; // 对端关闭 } // 处理 buf 中的 count 字节 }Nginx 默认用 ET追求极致性能业务代码如果对正确性要求高、团队经验一般LT 更稳妥。我试过在压测里把同一份逻辑从 LT 改成 ETQPS 提升有限但漏读导致的 bug 排查成本高得多选型要权衡。4. TaoToken 前置统一 Key 与 API 通道写底层网络代码时我经常让 AI 工具帮忙补全 epoll 骨架、审 ET 循环有没有漏EAGAIN。但多个工具各配各的 Key、各记各的 endpoint管理起来很乱。TaoToken 的作用就是提供一个统一的 Key 和 API 通道把模型对话、编码补全这些能力收敛到一个入口。官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基址不带 UTMhttps://taotoken.net/api你需要先拿到一个 Key再去配置工具。Key 在控制台的 API Keys 页面生成控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite拿到 Key 后下面给两份可直接复制的配置骨架。5. 可复制配置settings.json 与 config.toml 骨架5.1 Claude Code 风格 settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: sk-你的TaoToken密钥, ANTHROPIC_MODEL: claude-sonnet-4-20250514 }, permissions: { allow: [Read, Edit, Bash(gcc:*), Bash(make:*)] } }把ANTHROPIC_BASE_URL指向 TaoToken 的 API 基址ANTHROPIC_AUTH_TOKEN填你的 Key。这样工具的所有请求都走统一通道换模型只改ANTHROPIC_MODEL一行。5.2 通用 config.toml 骨架[provider] name taotoken base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 [model] default claude-sonnet-4-20250514 max_tokens 8192 temperature 0.2 [request] timeout_seconds 60 retry 2temperature调低到 0.2是因为写 epoll 这类底层代码时我更希望模型给确定性的、符合 man 手册的答案而不是发挥创意。6. 验证请求确认通道与 epoll 行为都正确6.1 验证 TaoToken 通道配置好后先用一条最小请求确认通道通curl -s https://taotoken.net/api/v1/messages \ -H x-api-key: sk-你的TaoToken密钥 \ -H anthropic-version: 2023-06-01 \ -H content-type: application/json \ -d { model: claude-sonnet-4-20250514, max_tokens: 64, messages: [{role:user,content:用一句话说明 epoll 的 ET 模式为什么要非阻塞}] }返回里有正常的content文本说明 Key 和通道都没问题。想直接在网页里对话验证模型可以走模型对话入口https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite6.2 验证 epoll 的 ET 行为写一个最小服务端监听端口用 ET 模式然后开两个终端一个跑服务一个用nc连上去发两段数据观察是否只收到一次通知。#include sys/epoll.h #include sys/socket.h #include netinet/in.h #include fcntl.h #include unistd.h #include stdio.h #include string.h #include errno.h #define MAXEVENTS 64 static int set_nonblocking(int fd) { int flags fcntl(fd, F_GETFL, 0); return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } int main(void) { int lfd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr {0}; addr.sin_family AF_INET; addr.sin_addr.s_addr INADDR_ANY; addr.sin_port htons(9000); bind(lfd, (struct sockaddr *)addr, sizeof(addr)); set_nonblocking(lfd); listen(lfd, SOMAXCONN); int efd epoll_create1(0); struct epoll_event ev, events[MAXEVENTS]; ev.events EPOLLIN | EPOLLET; ev.data.fd lfd; epoll_ctl(efd, EPOLL_CTL_ADD, lfd, ev); for (;;) { int n epoll_wait(efd, events, MAXEVENTS, -1); for (int i 0; i n; i) { if (events[i].data.fd lfd) { int cfd accept(lfd, NULL, NULL); set_nonblocking(cfd); ev.events EPOLLIN | EPOLLET; ev.data.fd cfd; epoll_ctl(efd, EPOLL_CTL_ADD, cfd, ev); printf(accepted fd%d\n, cfd); } else { char buf[512]; while (1) { ssize_t c read(events[i].data.fd, buf, sizeof(buf)); if (c -1) { if (errno ! EAGAIN) perror(read); break; } else if (c 0) { close(events[i].data.fd); break; } printf(read %zd bytes\n, c); } } } } }编译运行gcc -O2 -o epoll_demo epoll_demo.c ./epoll_demo另一个终端printf hello | nc 127.0.0.1 9000你会看到accepted fd...和read 5 bytes。如果一次nc发两段比如printf aa; sleep 1; printf bbET 模式下会看到两次 read 通知因为对端触发了两次状态变化。这就是 ET 的行为验证。7. 本篇常见错排查报错一epoll_wait返回后 read 一直 EAGAIN但数据明明在。多半是 fd 没设非阻塞ET 模式下阻塞读会卡死整个循环。检查fcntl(fd, F_SETFL, flags | O_NONBLOCK)有没有漏。报错二连接数一多就Too many open files。epoll 实例本身占一个 fd每个连接占一个。检查ulimit -n必要时在服务启动前ulimit -n 65535或改/etc/security/limits.conf。报错三ET 模式下偶发丢数据。典型是 read 循环没读到EAGAIN就 break 了。ET 必须读到返回EAGAIN或0才算处理完中途 break 会漏掉缓冲区剩余数据。报错四TaoToken 请求返回 401。检查ANTHROPIC_AUTH_TOKEN或x-api-key是否填了完整 Key有没有多余空格。Key 在 API Keys 页面重新生成即可。报错五epoll_ctl返回EEXIST。同一个 fd 重复 ADD。要么先 DEL 再 ADD要么改用EPOLL_CTL_MOD。报错六epoll_wait的 maxevents 传得比实际数组大。会导致内核往数组外写栈溢出。maxevents 必须 ≤ 数组实际长度。8. 工具侧落地把 AI 编码接进 epoll 开发流写这类底层代码我习惯让 AI 帮忙做三件事审 ET 循环、生成 epoll_ctl 的边界处理、解释 man 手册里含糊的返回值。要让这些能力稳定可用工具侧的配置得先落地。如果你主要做长期编码和 Agent 类任务建议走 Coding Plan把额度集中管理https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite接入细节和参数说明看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteClaude Code 用户可以直接参考 Anthropic 兼容配置https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite配置好之后回到 epoll 本身把上面那份骨架跑通用nc验证 ET 行为再逐步加上写事件处理、EPOLLONESHOT、超时管理。红黑树和就绪链表的设计理解透了后面调 Nginx、调 Redis 的网络层很多为什么这么写的疑问会自然消解。
