首页 / 资讯中心 / 文章详情

epoll与Reactor:网络编程高并发的核心事件驱动模型

epoll与Reactor:网络编程高并发的核心事件驱动模型 ★ FEATURED ARTICLE
很多人在看网络编程相关源码Redis、Nginx、Netty的时候都会遇到同一个名字IO多路复用、Reactor反应堆模式以及Linux内核里极有代表性的epoll。先用一句话点透这篇内容的核心Reactor模式就是一个“事件循环事件分发”的中心化调度模型单线程通过epoll这种多路复用器盯住所有连接事件一到就按预先注册的回调函数分发给对应处理器去执行。这不是源码里某一行注释能讲明白的事而是你理解高并发服务端架构的关键。如果你正在啃Linux内核相关的并发模型或者想自己写一个网络框架这篇能帮你把“epoll和Reactor到底怎么咬合在一起”这件事彻底盘清楚。我第一次接触epoll不是看man手册而是参读Redis的ae事件循环源码。当时的震撼感一直到今天都很清晰原来支撑几万并发连接的服务器核心不是疯狂开线程而是一个小循环加一堆回调把事件挨个分发出去。这个套路就是Reactor模式。这篇内容不想写成长篇大论的理论综述我尽量用干活的视角把Linux内核里IO多路复用的演进、Reactor的四个角色、基于epoll的最小可运行实现以及我在线上踩过的坑一整套讲清楚。适合三类人看一是刚入行网络编程、对select/ poll/epoll只停留在概念阶段的同学二是想读Redis、Nginx、Netty源码但被事件循环绕晕的开发者三是准备动手写自己的网络库或网关层需要设计事件模型的高级开发。1. 为什么需要IO多路复用与Reactor从阻塞到非阻塞的演进1.1 传统阻塞模型到底卡在哪里先回到最朴实的情况。你写一个服务端程序监听一个端口需要接收客户端连接。最容易想到的思路是这样accept到一个连接就pthread_create一个线程专门去read和write这个fd。看起来没什么问题业务代码也好写毕竟每个连接相互隔离——一个线程卡住了不影响其他线程。但当一个服务要支撑上万甚至十万并发连接的时候问题就变得不可接受了。一是线程资源扛不住。Linux下一个线程默认栈大小是8MB虽然真正分配内存是按需分页但创建线程有明确的内核开销线程控制块TCB、栈空间、调度实体每一项都要占资源。你开5000个线程光是调度起来CPU时间就大量浪费在上下文切换上。有人测过Pthread创建线程的延迟大概在几十微秒加上调度切换在高并发下这个成本会被急剧放大。二是线程切换打满CPU业务真正干活的时间比例反而下降。这就是经典的C10K问题的根源——不是CPU不够快而是模型把资源都耗在了管理线程上。还有一个被很多人忽略的点阻塞式read的判断逻辑很别扭。你不知道客户端什么时候会发数据只能让线程一直阻塞在read上。为了处理一个连接偶尔一次的数据交互你要白白用一个线程空转等在那里。服务端的吞吐量自然上不去。所以我们要的是一种机制让一个线程可以同时等着成千上万个连接上的数据谁有数据谁就主动通知我而不是我轮流去问每一个连接。这个机制在Linux内核里就叫IO多路复用。1.2 从select/poll到epoll事件驱动的转折点选select还是poll很多教科书上的说法是select有FD_SETSIZE限制默认1024poll用链表突破了连接数上限。这些都对但select/poll还有一个更致命的性能问题它的工作方式是“轮询拷贝”。内核每次都要遍历一遍你传上来的所有fd检查每个fd是否有事件然后把整个fd数组从用户态拷贝到内核态等有事件后再从内核态拷回用户态。连接数一涨这个O(n)的遍历就成了瓶颈——即使只有1个连接有数据内核也要把10万个fd全扫一遍。epoll的出现在思路上是一次根本性转变。它不问你“这几千个fd现在有没有事件”而是先让你把感兴趣的fd注册到一个事件表里然后由内核主动维护一份就绪链表机制。当某个fd上真正发生事件时内核通过回调机制把这个fd挂到就绪队列你调用epoll_wait拿到的是已经发生事件的fd列表而不是全量扫描结果。这样即使连接数很大每次epoll_wait处理的事件数量也只是就绪的那一小部分复杂度从O(n)降到了接近O(就绪事件数)这是量级的差距。更深一层的原因是内核数据结构的变化。epoll在内核里维护了两套核心结构一棵红黑树用来存你注册的所有fd一个就绪链表用来存已经触发事件的fd。注册、删除fd方便查询也稳定。配合事件回调机制数据到达网卡触发软中断中断处理函数把fd塞进就绪链表epoll_wait从链表中取出来返回给用户空间。这套“内核帮你盯连接、事件到了主动通知你”的模型正好是Reactor模式的底层地基。不同多路复用机制的核心差异我用一个表整理出来维度selectpollepoll连接数限制FD_SETSIZE通常1024无固定上限理论上限为系统最大fd数内核检测方式每次全量线性扫描fd数组每次全量线性扫描fd链表回调机制只处理真正就绪的fd用户态与内核态拷贝每次调用都整体拷贝fd集合每次调用都整体拷贝fd数组注册时拷贝一次事件返回仅拷贝就绪项水平触发LT支持支持支持边缘触发ET不支持不支持支持大并发场景性能差O(n)差O(n)好接近O(就绪事件数)我最早用select写一个聊天室demo连接数刚过100就开始丢包CPU一路飙到60%。换成epoll之后同样一台机器几千连接CPU占用反而降下来了。这就是模型正确带来的直接收益。2. Reactor反应堆模式把事件循环变成核心引擎2.1 一句话总结反应堆现在可以正面回答标题里的问题了。一句话总结反应堆Reactor用一个由多路复用器驱动的事件循环统一等待所有IO事件事件一旦发生事件循环把它分发到预先注册好的对应回调处理函数上整个等待和处理过程是异步的、非阻塞的核心线程不会被任何单个连接的IO卡住。为了加深印象我给一个生活化类比。Reactor很像一个电话客服中心的总机接线员。来电事件打进总机接线员根据拨号fd上的事件类型转给对应分机回调函数接线员的职责是快速接、快速转而不是亲自处理每一通电话的业务。如果你把接线员换成每一位客户配一个专属客服一连接一线程成本瞬间爆炸。Reactor就是那个高效的接线员epoll就是他那台能同时监听上千条线路的电话交换设备。网上关于Reactor的定义很多有人强调“事件驱动”有人强调“非阻塞”这些都对但最容易忽略的是“分发器”这个动作。Reactor模式最核心的思想不在于事件循环本身——任何循环都能转起来——而在于把“发生了什么事”和“这件事由谁来处理”解耦。连接、读写、关闭每一种状态变化都是一种事件每种事件都对应一个回调回调提前被注册好。事件循环拿到一个就绪事件只需要查一下回调表然后调用即可。2.2 Reactor的四个核心角色与协作关系这个模式的参与者拆到底只有四个东西Event事件描述某个fd上发生的具体事情比如EPOLLIN可读、EPOLLOUT可写、EPOLLERR出错、EPOLLHUP挂断。在Linux上事件本质上就是文件描述符上的一组状态位。Demultiplexer多路分发器也就是epoll本身。它负责阻塞等待事件把“众多连接中的哪些连接有事”这件事捞出来。Dispatcher事件循环这是Reactor的核心逻辑通常就是一个while循环不断调用epoll_wait拿到就绪事件之后按事件的类型、fd的编号去回调表里找到对应的处理函数。Handler事件处理器真正的业务代码所在。客户端发来一个请求解析协议、分发逻辑、构造响应全部在Handler里完成。Handler通过回调的形式注册到事件循环上。这四个角色的关系在代码层面是层层嵌套的。Handler是用户自己写的Dispatcher和Demultiplexer构成框架层Event是两者之间的信息载体。我在写自己的事件库时最痛苦的部分不是循环怎么写而是把Handler和Event绑定起来的数据结构设计——用什么保存handler指针回调的签名怎么定义通用性和效率怎么平衡这些细节才是Reactor工程化的真正门槛。2.3 为什么叫“反应堆”从比喻到本质“反应堆”这个词来源于核反应堆。核反应堆里核燃料不断裂变释放能量控制棒吸收中子调节反应速度冷却剂带走热量。Reactor模式借用了这个意象连接就是核燃料不断产生事件中子epoll是控制棒决定哪些事件可以被“看见”事件循环和回调是冷却循环把事件源源不断送到处理器去消耗。这个比喻其实很精准。反应堆的关键特征是“持续反应、不断发生、需要统一调度”Reactor处理的事件也是持续不断、随时可能发生的它必须在一个循环里连续工作而不是像调用普通函数那样有清晰的开始和结束。另一个特征是“被动响应”——事件不来回调不执行事件一来立刻响应。这就是名字里“reaction反应”的含义。理解了这一点你就明白为什么Reactor几乎成了高并发服务器的事实标准。Linux内核提供的epoll是能力底座但光有epoll还不能称为Reactor——你得有事件循环、有回调注册、有分发逻辑才算是真正把Reactor模式落地了。epoll负责“看到事件”Reactor负责“组织对事件的响应”两者配合才是完整形态。3. 基于epoll实现Reactor的最小可运行代码3.1 先约定核心数据结构理论说了不少还是得来点实操。我从零实现一个极简的Reactor核心然后用它写一个echo服务端完整跑通。不用框架、不用第三方库只用epoll和socket系统调用目标是让你能照着敲一遍理解每个字段和函数的意义。第一步是定义事件结构体。我习惯这样设计typedef struct event_loop event_loop; typedef void (*event_cb)(event_loop *loop, int fd, void *arg); struct event { int fd; uint32_t events; // 注册的事件类型EPOLLIN、EPOLLOUT等 event_cb read_cb; // 可读事件回调 event_cb write_cb; // 可写事件回调 void *arg; // 回调透传参数 struct event *next; // 用于链表管理简单实现够用 };这里的关键设计是fd与回调函数的绑定。epoll_event结构体里有一个union成员data可以存放一个指针我们把这个指针指向自己的event结构。这样epoll_wait返回时每个就绪事件都能直接找到它对应的回调不需要再来回查找。事件循环结构体#define MAX_EVENTS 1024 struct event_loop { int epfd; // epoll实例的fd struct epoll_event events[MAX_EVENTS]; // epoll_wait的返回缓冲区 };3.2 事件循环的初始化和注册函数初始化很简单创建epoll实例即可。这里用epoll_create1(0)而不是老式的epoll_create(1024)原因在于epoll_create1支持传入标志位代码更现代、语义更清晰int event_loop_init(event_loop *loop) { loop-epfd epoll_create1(0); if (loop-epfd 0) { return -1; } return 0; }注册事件的时候核心操作是填充struct epoll_event然后调用epoll_ctl。这里有个极其重要的细节epoll_event.data优先级很高因为它保存的是你注册时传给内核的指针内核在事件就绪时会原样返回。我建议不要直接存裸fd而是存指向event结构体的指针这样回调里能拿到完整的上下文int event_add(event_loop *loop, struct event *ev) { struct epoll_event epev; memset(epev, 0, sizeof(epev)); epev.events ev-events; epev.data.ptr ev; // 关键点指针直接指向event结构体 if (epoll_ctl(loop-epfd, EPOLL_CTL_ADD, ev-fd, epev) 0) { return -1; } return 0; }3.3 事件循环主流程epoll_wait 分发回调这是整个Reactor的心脏一个死循环反复等待就绪事件然后逐个分发到回调函数。这里体现的就是“事件到达后按预先注册的handler分发给对应处理器”的核心逻辑void event_loop_run(event_loop *loop) { while (1) { int n epoll_wait(loop-epfd, loop-events, MAX_EVENTS, -1); if (n 0) { if (errno EINTR) { continue; // 被信号打断继续 } break; } for (int i 0; i n; i) { struct event *ev (struct event *)loop-events[i].data.ptr; if (!ev) continue; uint32_t revents loop-events[i].events; if (revents (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) { // 出错或对方关闭如果注册了可读回调把关闭逻辑交给它 if (ev-read_cb) { ev-read_cb(loop, ev-fd, ev-arg); } } if (revents EPOLLIN) { if (ev-read_cb) { ev-read_cb(loop, ev-fd, ev-arg); } } if (revents EPOLLOUT) { if (ev-write_cb) { ev-write_cb(loop, ev-fd, ev-arg); } } } } }细心的人会问按这个逻辑如果同一事件同时触发了EPOLLIN和EPOLLOUT同一个fd是否会执行两次回调是的会。这正是事件循环的本质——事件是什么类型就调用什么处理函数而不是串行地“先处理读再处理写”。这个设计在写真实业务时尤其要注意不要在回调里假设同一批事件有严格的先后顺序。3.4 用echo server验证整个Reactor有了核心的事件循环剩下就是用socket函数把网络部分接起来。echo server的意思是客户端发什么服务端原样返回什么适合用来验证读写事件的分发是否正确。服务端逻辑有三个回调accept回调接收新连接设置非阻塞把连接fd注册到事件循环上。read回调读取客户端数据原样写回。error回调连接异常或关闭回收资源。我把完整代码拆开解释。先是监听socket的accept回调void on_accept(event_loop *loop, int listen_fd, void *arg) { int conn_fd accept(listen_fd, NULL, NULL); if (conn_fd 0) { return; } // 设置为非阻塞 int flags fcntl(conn_fd, F_GETFL, 0); fcntl(conn_fd, F_SETFL, flags | O_NONBLOCK); // 分配event结构体注册可读回调 struct event *ev malloc(sizeof(struct event)); memset(ev, 0, sizeof(*ev)); ev-fd conn_fd; ev-events EPOLLIN; ev-read_cb on_read; ev-arg loop; event_add(loop, ev); }然后是业务回调void on_read(event_loop *loop, int fd, void *arg) { char buf[4096]; while (1) { ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { write(fd, buf, n); // echo原样写回 } else if (n 0) { // 对端关闭释放资源 epoll_ctl(loop-epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); free(arg); return; } else { if (errno EAGAIN || errno EWOULDBLOCK) { return; // 数据读完了下次事件再处理 } epoll_ctl(loop-epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); free(arg); return; } } }这里用了一个在真实项目中非常常见的技巧connected socket都设置成非阻塞然后在read时循环读取直到返回EAGAIN。这样做的原因是后续如果要切换到边缘触发EPOLLET循环读直到EAGAIN是必须的写法。我现在就保持着这个习惯不管你用什么触发模式循环读到EAGAIN都不会错。main函数把一切组装起来int main() { int listen_fd socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); addr.sin_port htons(9000); bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)); listen(listen_fd, 128); event_loop loop; event_loop_init(loop); struct event listen_ev; memset(listen_ev, 0, sizeof(listen_ev)); listen_ev.fd listen_fd; listen_ev.events EPOLLIN; listen_ev.read_cb on_accept; listen_ev.arg (void *)loop; event_add(loop, listen_ev); event_loop_run(loop); close(listen_fd); return 0; }整个代码结构是清晰的事件循环只负责“看到事件并调用对应回调”连接管理、数据读写、资源释放全部散落在各个回调里。这就是Reactor模式在工程上最基本的形态。虽然这个版本没有线程池、没有多进程、没有定时器但核心模型已经完整了。注意这里listen socket也用了非阻塞标志。不加SOCK_NONBLOCK的话accept在有新连接时没问题但在极端情况下比如连接被客户端中途丢弃accept可能阻塞导致事件循环卡死。这是我踩过的一个真实坑提前在这里标出来。4. Reactor实战中的关键细节与常见坑4.1 边缘触发还是水平触发怎么选Linux的epoll支持两种触发模式水平触发LT和边缘触发ET。水平触发是默认模式只要fd上还有未读的数据epoll_wait就会反复通知你边缘触发则只在状态变化的那一刻通知一次如果你没有一次性把数据读完后续即使有剩余数据内核也不会再告诉你。很多网文章吹ET性能高、代表了epoll的进阶用法。这句话对了一半。ET确实减少了内核就绪列表的重复挂载但代价是处理逻辑要严谨——你必须在一次通知里尽量把数据读完读到EAGAIN为止。如果在EAGAIN之前没有读完数据就会一直滞留在内核缓冲区里客户端等不到响应服务端等不到新事件连接变成了僵尸。我的实际建议是新手和大部分业务场景先用LT等在线上确实出现了“大量同一fd反复触发”的瓶颈证据再考虑ET。Redis的ae事件循环默认就坚持用LTNginx在ET上踩过无数坑之后现在的accept和read处理逻辑也极度谨慎。不要为了“看起来高级”给自己制造调试地狱。如果你坚持用ET必须做到两件事accept时用循环接受直到返回EAGAINread时用循环读取直到返回EAGAIN。4.2 EPOLLONESHOT和事件竞争还有一个经常被忽略的事件修饰符EPOLLONESHOT。一个注册了EPOLLONESHOT的事件在被触发一次之后内核会自动把它从就绪状态中移除直到你重新用EPOLL_CTL_MOD重新注册。这个修饰符在多线程场景下尤其有用当你把回调丢给线程池处理而这个线程还没处理完当前连接的数据另一个线程又在处理同一个fd上的事件就造成了事件竞争。我遇到过一个真实线上问题一个TCP连接连续触发两次可读事件第一次的回调把整条消息发到线程池去处理第二次的回调又开始读取同一个缓冲区的数据两个线程同时操作同一个连接数据全部错乱。加EPOLLONESHOT之后每个连接在同一时刻只会被一个线程处理处理完再重新注册事件问题迎刃而解。4.3 惊群效应多个线程同时epoll_wait的坑所谓惊群是指多个线程或多个进程同时阻塞在同一个epoll实例的epoll_wait上当一个事件发生时内核会唤醒所有等待者但最终只有一个线程能真正处理事件其他线程被白白唤醒一次浪费CPU。在Linux 4.5之前内核确实会唤醒全部等待者。解决方式要么是epoll_wait之前加锁要么就是用较新内核提供的EPOLLEXCLUSIVE标志它就是专门为“多线程共用一个epoll fd、但事件只唤醒一个线程”设计的。如果你的服务器是单Reactor多线程模型在注册监听socket时对EPOLLIN事件加上EPOLLEXCLUSIVE这样当有新连接到达只有一个worker线程被唤醒不会惊动全部。不过话说回来如果你还在入门阶段我建议先用“单线程Reactor线程池处理器”的模型。它比多线程Reactor好理解得多也更容易排查问题。等事件模型稳定了再往多线程方向演进。4.4 回调里千万不能做阻塞操作单线程事件循环有个铁律回调函数里不能有任何阻塞操作。如果你在网络回调里直接去查询数据库、调用远程HTTP接口、sleep、甚至做一次磁盘同步IO那么整个事件循环就会被卡住。这时候所有连接上面的登录、心跳、业务请求全部排队等待延迟从毫秒级飙升到秒级表现就是服务“假死”。正确的做法是回调里只做非阻塞的协议解析、内存操作把耗时任务拆出来扔给线程池异步处理处理完成后再通过eventfd或者其他机制把结果塞回事件循环让主循环继续调度后续逻辑。Netty在处理IO线程和业务线程划分时就是这么干的IO线程只负责网络读写真正的业务逻辑在业务线程池里执行用Future或者回调串联。别以为自己写框架时没这个问题我见过很多人第一次用自研Reactor时在read回调里直接写日志文件磁盘IO一慢整个服务就瘫痪了。日志要异步远程调用要异步数据库操作要异步。这是Reactor模式下最高的纪律。4.5 连接关闭与内存释放的顺序C环境下最容易陷入的低级错误是连接关闭后没有正确释放event结构体或者释放之后又被事件循环引用到。epoll有一个现象当对端关闭连接时即使你没有注册EPOLLINepoll也会返回EPOLLHUP或EPOLLRDHUP事件。如果你的回调解构不够严谨就可能出现fd已经close内存已经free但事件循环还在继续调用这个fd上的回调。这时候往往是随机崩溃或者内存越界非常难排查。我的经验是三点第一在回调里正确判断EPOLLRDHUP事件读到EOF或者收到RST时立刻把fd从epoll树里摘除第二fd close和event结构体的free必须同时进行不要分开第三如果你用EPOLLONESHOT必须确认当前没有其他线程在操作同一个event结构体再释放资源。更稳妥的做法是设计一个延迟释放队列把要释放的连接先加入队列等事件循环下一次迭代结束时再统一回收这样能彻底避免use-after-free问题。5. 用Reactor改造一个真实服务的思路5.1 从零设计事件循环的步骤清单如果你决定从零写一个基于epoll的Reactor我建议按这个顺序来每一步都验证过再走下一步实现event和event_loop结构体先只有注册、删除、分发三个功能。用监听socket接入accept回调验证新连接可以进来。实现read回调并做echo验证确认数据能正常收发。增加write回调的注册与触发实现半关闭状态的优雅处理。加入EPOLLONESHOT和线程池让耗时任务可以异步执行。引入定时器机制处理心跳超时、连接超时、任务超时。最后才是ET优化和其他边缘功能。这里每一步都有讲究。比如第5步线程池的引入会带来事件竞争问题这时候你必须考虑加锁还是EPOLLONESHOT第6步的定时器设计最简单的就是小顶堆Redis就是这么做的性能稳定、实现也不复杂。不要一开始就想着把所有功能一次性写完先跑通主线再逐步加强否则调试起来你会被一堆问题同时淹没。5.2 三种经典线程模型用一张表看清落点Reactor模式的落地形态经过多年演进基本定型为三种主流模型模型典型代表事件循环数量优势劣势单Reactor单线程Redis1个简单、无锁、性能极高无法利用多核单个回调阻塞全盘卡死单Reactor多线程多数自研网关1个主循环 线程池主循环简单业务并行主循环仍是瓶颈事件竞争需要解决多Reactor多线程Netty1个主Reactor N个SubReactor多核利用充分扩展性好实现复杂连接模型和线程模型需要对齐这三个模型不是互相替代的关系而是不同规模、不同团队能力下的合理选择。Redis用单线程却能做到10万QPS原因是它的业务操作全是内存级的没有阻塞单线程反而省了锁竞争。Netty是多Reactor的标杆它把连接accept分给主Reactor把每个连接上的IO读写分发给不同的SubReactor每个SubReactor各自持有一个独立的事件循环互不干扰。你给自己的项目选型时评估一下自己的业务是不是IO密集型、要不要跨核延展再决定用哪层复杂度。5.3 现成框架这么多该不该自己造轮子很多人在学了Reactor之后第一反应是“我要自己写一个网络框架”。我不反对因为动手写是理解原理的最佳方式我自己也写过。但如果你做的是业务系统而不是中间件基础设施请三思。Redis的ae事件循环是单文件几百行代码你可以直接读通libevent和libev的代码则复杂得多bug修复也一直在演进Netty对多线程Reactor的封装已经非常成熟直接用比重新造一个省心太多。真正建议自己实现Reactor的场景有这几个一是你在学习阶段为了彻底搞懂原理二是你在做定制化的中间件比如网关、代理、数据库中间件需要完全掌控事件模型和内存分配三是业务对延迟极度敏感你不想在第三方框架的线程模型上妥协。否则用成熟的框架是更理性的工程选择。但即使你决定用Netty也建议把这篇文章里的event_loop_run代码和它的EventLoop.java源码放在一起对比着看你会发现核心模型的继承关系极其明显——换的只是语言和封装不换的是那个“事件循环回调分发”的骨架。6. 最后的实际操作感慨我踩过的几个深坑写到这里我额外分享两个真实踩坑细节它们都不是理论知识而是线上环境逼出来的经验。第一个是关于epoll_wait返回值突然变成-1的情况。程序运行一段时间后某个时刻开始疯狂打印错误日志查了半天才发现是SIGCHLD信号打断了epoll_waiterrno被设置成EINTR。解决办法就是我在event_loop_run里写的那两行检测到EINTR就continue。千万别把这情况当作致命错误退出循环否则你的服务会在半夜莫名挂掉。第二个是关于文件描述符耗尽的排查。用lsof -p | wc -l数了一下发现进程打开的fd数量逼近ulimit上限但业务连接数并没有那么高。后来发现是连接关闭时close被放在了业务线程里执行而事件循环仍然持有这个fd导致fd资源迟迟没有真正回收。这个问题的根治方案就是把close也统一收口到事件循环里执行或者干脆用第4.5节提到的延迟释放队列。我的排查工具很朴素strace跟一下系统调用看看哪个fd没有close再用/proc/ /fd目录数一遍所有打开的fd基本能定位问题。最后说句真心话学会Reactor不算什么高深功夫但真正把它用好需要你在线上环境摸爬滚打很多年。每当我看到新代码里有人把耗时的磁盘IO直接写进read回调或者在ET模式下不会循环读数据导致连接卡死我都会想起自己当年踩过的坑。读源码是一个很好的入口但如果你愿意从这篇内容出发自己动手写一个echo server再逐步加线程池、定时器最后把它用到真实项目里你的理解会远远超过任何文章给你带来的。动手敲一遍代码比读一百遍原理都管用。
阅读完成 · 觉得有帮助?
咨询建站