ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

select、poll、epoll:从阻塞到事件驱动的IO多路复用全解析

select、poll、epoll:从阻塞到事件驱动的IO多路复用全解析 1. 为什么需要IO多路转接1.1 阻塞IO与多线程模型的瓶颈先回忆一下最原始的网络编程模型。服务端创建一个socket调用accept等待客户端连接一旦有连接进来就read数据处理完再write回去。这个流程在只有一个客户端的时候没有任何问题代码写起来也很简单逻辑清晰直白。但现实中的服务端不可能只服务一个客户端。多个客户端同时连接、同时发数据那你只靠单线程阻塞模型就完全扛不住。read卡在那里等数据其他客户端全被晾着。有人说这还不简单来一个连接就开一个线程去处理多线程总行了吧这个方案在小规模场景下确实能跑连接数几十上百个时问题不大。但连接数一旦上来线程的创建销毁、上下文切换、内核态用户态切换的成本就开始吞噬性能四核八核的机器也会被线程调度拖垮。更麻烦的是每个线程通常要为连接维护独立的栈空间默认8MB一万个线程就意味着80GB虚拟内存这还不算线程切换带来的CPU浪费。所以核心矛盾很明确进程/线程数量过多导致资源消耗大而连接本身的活跃度往往很低很多连接建立后大部分时间在空闲等待为每个空闲连接分配一个线程是巨大的浪费。这时候就需要一种机制让单个线程能同时监视多个文件描述符一旦某个描述符就绪可读、可写、有异常就立刻通知应用程序去处理。这就是IO多路转接也叫IO多路复用英文是IO multiplexing对应的系统调用就是select、poll、epoll这三个。1.2 多路转接的核心思想IO多路转接的核心思想其实特别朴实用一个线程同时监听多个fd交给内核去帮我们盯着这些描述符的状态变化哪个fd有数据到了哪个fd可以写了内核统一告诉我们。应用程序只需要在就绪的fd上做实际读写不浪费任何一次系统调用来探测空闲连接。用生活化的类比来说就是以前你开一家餐厅每来一桌客人就安排一个专职服务员一对一伺候客人不点菜时服务员也得在旁边干站着。而多路转接相当于你只安排一个领班站在大厅里扫一圈所有桌子的状态哪桌客人举手了就去处理哪桌。领班一个人就能盯几百桌这就是多路复用的价值。理解了这个模型再去看select/poll/epoll的实现思路就非常清晰了做的事其实都一样注册你关心的fd等待内核告诉你哪些fd就绪然后遍历就绪列表挨个处理。差别只在于这个过程中内核和用户态之间的数据拷贝方式、事件通知粒度、以及就绪fd的查找效率。1.3 三个函数在Linux网络编程中的位置需要先说明的是这三者都是操作系统提供的系统调用用户态程序需要直接或间接使用它们来构建事件循环。对Linux后端开发来说epoll是绝对主流Nginx、Redis、Netty的Linux版本底层都是epoll。但select和poll并非没有价值它们在某些小众但特殊的场景下仍然有用比如需要跨平台兼容的代码Windows没有epoll但有select比如监听的fd数量很少时又比如telnet这类老协议工具实现里。理解它们的差异不是为了让你写代码时纠结选哪个而是让你脑子里装着一张完整的“网络并发方案地图”遇到具体场景时能条件反射般选出合适的方案。2. select第一代多路复用方案2.1 select的工作原理与使用流程select的函数签名是#include sys/select.h #include sys/time.h int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);使用流程分四步先把要监听的fd放进fd_set集合调用select后内核会阻塞等待这些集合中任一fd就绪然后内核会修改fd_set集合把没有就绪的fd从集合中清掉最后用户态遍历fd_set找到仍然在集合里的fd就是真正就绪的fd可以读写。这里有几个关键点值得展开说一下。第一个是nfds参数。它不是某个fd的值而是所有要监听的fd中的最大值加1。为什么这样做因为内核遍历fd_set时是用位图结构实现的fd_set本质上是一个bitmap内核需要知道从0号fd开始最多检查到哪个fd。传nfds的值内核就能把fd_set从0到nfds-1的位逐一检查不用把整个1024位都扫一遍。但这也意味着如果你只监听fd 500和fd 900内核会把0到900之间所有位都检查一遍中间那些你没监听的fd也白白检查了。所以select在fd数量多但值分散时效率是明显下降的。第二个是fd_set有三个集合读集合、写集合、异常集合。你可以在同一个select调用里同时监听读和写事件这在实际编码中非常有用。比如你想向某个连接发送大文件但发送缓冲区满了你可以把该fd同时加入写集合等待缓冲区可写后再继续发送。第三个是timeout参数它支持三种情况传NULL表示永久阻塞直到有fd就绪传一个全0的timeval表示完全非阻塞立即检查并返回传一个具体的timeval表示最多阻塞这么长时间。这个参数精度是微秒级比sleep的秒级粒度精细得多。实际开发中很多事件循环就是靠一个固定timeout值来控制主循环节拍的。下面给一个最小可用的select多客户端服务器示例。为了把注意力放在select本身我把错误处理简写了#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include sys/select.h #define MAX_CLIENTS 10 int main() { int listen_fd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(9999); addr.sin_addr.s_addr htonl(INADDR_ANY); bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)); listen(listen_fd, 5); fd_set read_fds; int all_fds[MAX_CLIENTS]; for (int i 0; i MAX_CLIENTS; i) { all_fds[i] -1; } all_fds[0] listen_fd; while (1) { FD_ZERO(read_fds); int max_fd -1; for (int i 0; i MAX_CLIENTS; i) { if (all_fds[i] ! -1) { FD_SET(all_fds[i], read_fds); if (all_fds[i] max_fd) { max_fd all_fds[i]; } } } int ret select(max_fd 1, read_fds, NULL, NULL, NULL); if (ret 0) { continue; } if (FD_ISSET(listen_fd, read_fds)) { int client_fd accept(listen_fd, NULL, NULL); for (int i 1; i MAX_CLIENTS; i) { if (all_fds[i] -1) { all_fds[i] client_fd; break; } } if (--ret 0) { continue; } } for (int i 1; i MAX_CLIENTS; i) { if (all_fds[i] ! -1 FD_ISSET(all_fds[i], read_fds)) { char buf[1024]; ssize_t n read(all_fds[i], buf, sizeof(buf)); if (n 0) { close(all_fds[i]); all_fds[i] -1; } else { write(all_fds[i], buf, n); } if (--ret 0) { break; } } } } return 0; }这段代码的逻辑非常直观每次循环重建fd_setselect返回后就逐个检查每个fd是否就绪。这个框架是所有select程序的通用范式理解了它你就理解了select的全部用法。2.2 select的三大痛点用select做过几年服务端的人对下面这三个问题估计都有切肤之痛。第一个痛点是fd_set大小被内核硬编码限制。在Linux上FD_SETSIZE默认是1024也就是说一个进程的select最多只能监听1024个fd。这个限制对于现代高并发服务器来说几乎不可接受一个稍微像样点的服务端程序连接数分分钟就超过1024了。有人会去改FD_SETSIZE然后重新编译内核但这既不优雅也不可移植一旦跨平台代码就废了。第二个痛点是每次调用select用户态都需要把整个fd_set拷贝到内核态select返回后内核又把修改后的fd_set拷回用户态。这个双向拷贝的开销随着fd数量线性增长。两个位图拷贝每次拷贝8KB看起来不多但如果你每秒调用select 10000次那就是80MB的拷贝量这部分完全是无意义的开销。第三个痛点是返回后你不知道是哪个fd就绪了只能遍历整个fd_set去逐个检查。假设你监听了1000个连接每次select返回时只有一个连接有数据你也得把1000个位都检查一遍才知道是哪个就绪。更麻烦的是select返回后会修改fd_set把未就绪的fd清除掉所以你必须在下一次调用前重新把所有fd重新FD_SET一遍。这就是为什么上面的示例代码每次循环开头都要重新FD_ZERO和FD_SET一番这个重建过程本身也是浪费。这三个痛点叠加在一起就注定了select只能处理中小规模的并发场景。但它也不是完全没有优点。select的可移植性极好Windows、Linux、macOS、Android都有select实现timeval精度是微秒级在多平台下行为一致另外select的内部实现比poll和epoll简单逻辑出问题时更容易调试。对某些只要求兼容性的CLI工具来说select到今天仍有一席之地。3. poll把select的坑填了一半3.1 poll的接口改进解决了什么问题poll函数在接口设计上比select先进了一代它彻底抛弃了fd_set位图改用struct pollfd数组。函数声明如下#include poll.h int poll(struct pollfd *fds, nfds_t nfds, int timeout);每个pollfd结构体包含三个字段struct pollfd { int fd; // 要监听的fd short events; // 关心的事件POLLIN/POLLOUT等 short revents; // 实际发生的事件由内核填写 };这个设计解决了一个很关键的痛点events和revents分离。在select里内核会修改传入的fd_set所以你每次都要重建fd_set但在poll里内核只修改revents字段你的events字段不受影响所以同一组pollfd数组可以反复复用无需每次重建。这个改进在处理海量连接时非常实在省掉了select里每次循环都有的重建开销。poll没有FD_SETSIZE限制因为pollfd是一个动态数组你传多少就监听多少内存开销只跟你的数组长度相关。这意味着在64位系统上单个进程能监听的fd数量上限只受系统最大fd数量通常由ulimit -n控制和内存大小限制。默认的1024限制在poll这里不存在。poll的timeout参数单位是毫秒精度比select低但作为阻塞等待的节拍控制也够用了。timeout为-1表示永久阻塞为0表示非阻塞立即返回。poll支持的events事件类型也更丰富。除了基础的POLLIN可读、POLLOUT可写和POLLERR错误还有POLLHUP对端挂断、POLLPRI带外数据、POLLRDHUPLinux特有对端关闭写端等。尤其POLLRDHUP这个事件很有用你需要用起来。一个基础的poll服务器代码示例#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include poll.h #define MAX_CLIENTS 1024 int main() { int listen_fd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(9999); addr.sin_addr.s_addr htonl(INADDR_ANY); bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)); listen(listen_fd, 5); struct pollfd fds[MAX_CLIENTS]; memset(fds, 0, sizeof(fds)); for (int i 0; i MAX_CLIENTS; i) { fds[i].fd -1; } fds[0].fd listen_fd; fds[0].events POLLIN; int max_count 1; while (1) { int ret poll(fds, max_count, -1); if (ret 0) { continue; } if (fds[0].revents POLLIN) { int client_fd accept(listen_fd, NULL, NULL); int i; for (i 1; i MAX_CLIENTS; i) { if (fds[i].fd -1) { fds[i].fd client_fd; fds[i].events POLLIN; break; } } if (i max_count) { max_count i 1; } if (--ret 0) { continue; } } for (int i 1; i max_count; i) { if (fds[i].fd ! -1 (fds[i].revents POLLIN)) { char buf[1024]; ssize_t n read(fds[i].fd, buf, sizeof(buf)); if (n 0) { close(fds[i].fd); fds[i].fd -1; } else { write(fds[i].fd, buf, n); } if (--ret 0) { break; } } } } return 0; }对比select版本的代码你会发现poll版本省掉了每次循环都执行FD_ZERO/FD_SET的步骤整个主循环更加清爽。因为events字段不会在poll返回后被改动所以重新定义监听范围和事件类型的逻辑被大幅简化。3.2 poll仍然存在的性能问题poll解决了select的连接数上限和集合重建问题但有两个核心性能问题它并没有解决。第一个问题是内核仍然需要遍历整个pollfd数组。每次调用poll内核都要把所有pollfd从用户态拷贝到内核态然后遍历数组检查每个fd对应的socket是否有事件最后再把这个数组从内核态拷贝回用户态。如果数组里有一万个fd每次poll调用就是一万次无差别扫描即使其中九千九百九十九个fd都处于空闲状态。这种O(n)的遍历成本在连接数量越来越大时会成为明显的性能瓶颈。第二个问题是返回后仍然需要用户态遍历数组找就绪fd。poll原样继承了select的“大海捞针”式处理方式返回值只是告诉你有多少个fd就绪了但没告诉你具体是哪些。在高并发场景下这个遍历的CPU开销是实实在在的。比如同时有一万个连接但只有两个连接有数据你还是得把一万个pollfd全部过一遍找出那两个revents非零的。如果说select的核心限制是连接数上限1024那poll的核心限制就是无差别遍历带来的O(n)时间复杂度。它的无状态遍历机制跟select本质上是一样的都属于内核每次调用全量扫描的“老派”方案。所以在实际项目中poll往往作为跨平台代码的备选方案或者连接数在几千级别时的简化实现。它在接口友好度上确实比select好但在高性能场景下Linux平台的正解还是epoll。4. epollLinux下的最终形态4.1 epoll的三个API与事件通知机制epoll是Linux内核针对select/poll的O(n)遍历问题专门设计的一套事件驱动模型。它不是单个函数而是一组三个接口#include sys/epoll.h int epoll_create(int size); 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_create创建一个epoll实例内核会为这个实例维护两个关键数据结构一棵红黑树和一个就绪链表。红黑树用于存储所有注册到这个epoll实例上的fd方便快速增删改查就绪链表用于存储已经发生事件的fdepoll_wait返回时直接从这个链表取数据。epoll_ctl负责往红黑树里添加、修改、删除fd。你只需要在fd第一次注册时调用一次epoll_ctl(ADD)之后每次wait只是等待事件通知不需要像select/poll那样每次把全部fd重新拷贝给内核。这个“注册一次反复使用”的模型是epoll高性能的重要前提。epoll_wait返回时events数组里装的都是就绪的fd用户态只需要遍历events数组处理实际就绪的连接不需要像select/poll那样扫描全部监听fd。假设有十万个连接只有五个活跃epoll遍历的只有这五个而select/poll要扫十万个。我刚接触epoll时最好奇的一点是为什么它做到了只有活跃fd才会被返回这背后其实用一个很巧妙的内核机制实现。当一个fd注册到epoll后内核会为该fd挂上一个回调函数这个回调与socket的等待队列挂钩。当socket收到数据、变成可读时内核唤醒等待队列上的进程同时触发epoll回调把该fd加入epoll实例的就绪链表。简单说epoll不是靠“定期轮询”来判断状态而是靠“事件发生时才通知”用中断式的回调替代了轮询式的扫描。4.2 LT模式与ET模式的核心差异epoll对fd有两种事件触发模式水平触发Level-TriggeredLT和边沿触发Edge-TriggeredET。LT模式是默认模式也是select/poll所用的模式。它把fd反复报告直到你的程序真正处理完这个事件。假设socket的接收缓冲区有2KB数据你用read读取了1KB缓冲区还剩1KBLT模式会再次通知你该fd可读直到缓冲区清空为止。这种模式容错性高因为事件会一直提醒你漏一次还有下一次代码写起来不容易踩坑。ET模式则只在fd状态发生变化的那一刻通知你一次。比如缓冲区从空变为有数据内核会在这个边沿跳变时报告事件但如果这次你没把数据读完之后即使缓冲区里还躺着数据内核也不再通知你了除非有新的数据再次到达。ET模式要求程序员必须一次性把fd上的数据全部读取干净否则就会丢数据。这两种模式在实际开发中的取舍非常关键。LT模式代码易写适合绝大多数普通业务场景。ET模式虽然高效但处理起来相当棘手因为你需要使用非阻塞IO循环read直到返回EAGAIN才能确保把数据读干净。一个不小心漏读了数据这个连接可能就永远卡住了。我对ET模式的建议是新手别碰ET等你对IO模型有足够深的理解后再考虑生产环境如果追求极致性能再用ET而且要配合专门的测试用例验证。4.3 epoll的代码模板与ET模式读写细节一个蛮经典的epoll LT模式服务器代码模板大概是这样的#include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h #include sys/socket.h #include netinet/in.h #include sys/epoll.h #define MAX_EVENTS 1024 static int set_nonblock(int fd) { int flags fcntl(fd, F_GETFL, 0); return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } int main() { int listen_fd socket(AF_INET, SOCK_STREAM, 0); set_nonblock(listen_fd); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(9999); addr.sin_addr.s_addr htonl(INADDR_ANY); bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)); listen(listen_fd, 128); int epfd epoll_create(1); struct epoll_event ev; ev.events EPOLLIN; ev.data.fd listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, ev); struct epoll_event events[MAX_EVENTS]; while (1) { int n epoll_wait(epfd, events, MAX_EVENTS, -1); for (int i 0; i n; i) { if (events[i].data.fd listen_fd) { int client_fd; while ((client_fd accept(listen_fd, NULL, NULL)) 0) { set_nonblock(client_fd); ev.events EPOLLIN | EPOLLET; ev.data.fd client_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, ev); // 对端直接关闭时accept还能accept出新连接吗 // 这里用while循环accept循环直到accept返回-1且errnoEAGAIN } } else { int fd events[i].data.fd; char buf[4096]; while (1) { ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { write(fd, buf, n); // 简化回显逻辑 } else if (n 0) { close(fd); break; } else { if (errno EAGAIN || errno EWOULDBLOCK) { break; } close(fd); break; } } } } } return 0; }注意这个模板结合了LT模式的accept逻辑和ET模式的read逻辑。实际开发中accept也应该配合while循环把所有挂起连接一次收完否则在高并发瞬间可能有连接滞留。ET模式下读取数据的细节值得说透。ET模式触发一次后read要一直循环到返回EAGAIN才表示数据读完了。这里的关键是fd必须是非阻塞的否则在没有数据时read会阻塞住线程整个事件循环就卡死了。read返回0表示对端关闭需要处理连接清理read返回-1且errno是EAGAIN或EWOULDBLOCK表示当前没有更多数据可以安全退出循环read返回-1且errno是其他值比如ECONNRESET表示连接异常要关闭fd。很多线上事故就发生在这里。有的同学用ET模式read循环里没有处理EAGAIN就退出了结果fd上还剩一半数据内核又因为边沿已过而不再通知这条连接直接废掉。客户端那边则表现为等不到响应服务端日志却没有任何错误。这种问题极难排查所以再次强调ET模式的read循环一定要写对。5. 三者对比与选型建议5.1 核心维度横向对比把三个函数放在一张表里各自的特点就一目了然了。对比项selectpollepoll底层数据结构fd_set位图pollfd数组红黑树就绪链表最大连接数受FD_SETSIZE限制Linux默认1024无上限受系统fd数量限制无上限受系统fd数量限制每次调用都需要拷贝全部fd到内核是是否注册一次即可用户态查找就绪fd的方式遍历全部fd遍历全部fd直接拿就绪链表时间复杂度大量空闲连接O(n)O(n)O(活跃数)事件触发模式仅水平触发仅水平触发水平触发边沿触发可移植性Windows/Linux/macOS等大多数Unix/Linux系统仅Linux接口的易用性fd_set位操作繁琐pollfd数组结构清晰三个接口配合稍复杂从这张表能看出一个清晰的演进脉络select限制数量poll放开数量epoll解决效率。5.2 实际场景怎么选先说结论如果你是做Linux服务端且目标并发连接数超过一千那基本没有悬念直接用epoll。Nginx、Redis为什么在Linux上性能那么强其中一个重要原因就是底层用的epoll。Netty在Linux上运行时会自动检测并切换到epoll模式。这些都是被大规模生产环境验证过的方案。如果你的代码需要跨平台运行比如同一个程序要跑在Windows和Linux上那选择就变成了select或者封装了这些系统调用的跨平台库。注意poll在Windows的socket环境下并不原生支持Windows上只有select以及WSAPoll如果你写的是跨平台socket代码用select最稳妥。很多跨平台网络库内部就是条件编译Windows用select或IOCPLinux用epoll。如果你的连接数很少比如就监听十几个客户端而且大多数时间连接空闲那select和poll完全够用根本没必要上epoll。原因很简单epoll的优势在于大量连接场景下避免无差别遍历连接少的时候这个优势根本体现不出来反而平添复杂度。能用简单方案解决的事没必要上复杂方案。telnet、ssh这类工具内部为什么还用select因为它们通常只监听一个socket加一个标准输入最多再加一个socket总共两三个fd。这点量epoll完全是大炮打蚊子select的1024上限绰绰有余。但这种低fd数量的场景塞进epoll的红黑树里反而要额外维护回调、链表逻辑没有任何收益。5.3 性能数据与踩坑经验从实测数据来看当连接数在一百以内select/poll/epoll三者的效率差距非常小因为每次调用的遍历成本本身不大。连接数到一千时select开始出现明显瓶颈fd_set的64字节实际是16字节的fd_set结构中有1024个bit占128字节拷贝和遍历开始拖慢主循环。到一万连接时poll每次全量遍历已经非常可观而epoll只处理活跃连接CPU消耗几乎不随总连接数增长。我自己的一个实际测试场景是模拟12000个长连接客户端每隔几秒发一条心跳。select版本的程序在连接数到3000左右时CPU已经打满poll稍好一点但也在8000左右触顶epoll版本全程CPU占用稳定在百分之十几。这就是边沿触发和事件回调模型带来的量级差异。绕过几个常见的坑从代码写法上来讲epoll开发里最容易出问题的点主要有这样几个第一个坑是忘了给accept出来的client_fd设置非阻塞。如果不设置在ET模式下read到EAGAIN之前不会返回你的主线程会卡在某个慢速客户端的read上整个服务端瞬间假死。设置非阻塞这个操作要养成条件反射。第二个坑是EPOLLIN处理完之后忘了处理EPOLLOUT。如果某个客户端对端接收窗口满了你往这个fd写数据会阻塞此时你应该监听EPOLLOUT事件等缓冲区可写时再继续写。很多初学者写回显服务时只监听读事件结果TCP发送缓冲区一满就丢数据。第三个坑是连接关闭事件其实是没有EPOLLCLOSE的。判断对端关闭的唯一可靠方式是read返回0。如果对方正常close你的读事件会触发read返回0如果对方直接RST你的读事件也可能会触发并读到ECONNRESET错误。所以统一在read的返回值里处理连接关闭逻辑就好。第四个坑是epoll_wait返回的events数组中可能同一个fd同时出现EPOLLIN和EPOLLOUT也可能同一个fd在连续两次epoll_wait中重复出现。LT模式下这种重复更常见处理逻辑要保持幂等比如用状态机来管理每个连接当前处于什么阶段而不是简单依赖事件名称。5.4 更进一步epoll与Reactor模式如果你已经能熟练使用epoll了那下一步一定要去了解一下Reactor模式。epoll解决的是事件通知问题而Reactor解决的是事件分发和业务处理的问题。两者结合才是高并发网络服务器的完整形态。简单说Reactor模式把网络服务端拆成几个组件事件源epoll事件分发器把就绪事件分发到对应处理器事件处理器具体业务逻辑。这种拆分的好处是业务代码跟网络IO代码解耦新增一种协议时只需要新增一个事件处理器不用改epoll核心逻辑。Netty、Redis、Nginx在架构上都属于Reactor模型只是实现细节各有差异。理解了epoll之后再去读这些开源项目的事件循环代码阅读难度会骤降。我个人在实际项目里最常用的封装方式是维护一个连接对象池每个连接包含fd、读缓冲区、写缓冲区、当前状态等字段再准备一张全局哈希表用fd做key映射到连接对象。epoll_wait返回后从哈希表查fd对应的连接然后调用连接对象上的事件处理方法。这个模式用起来非常顺手。5.5 最后的实操建议回想一下这几个函数的定位我最后分享几条实操建议。不要在服务器代码里混用多个多路复用机制。我在一个项目里见过有人主线程用epoll监听所有连接某个子线程里又用select等另一个事件结果两个事件循环互相阻塞调试了整整两天。一个进程里同时存在两套多路复用模型几乎必然会出现协作问题。用epoll时记得定期调整文件描述符上限。ulimit -n默认往往是1024你代码里用epoll创建了一万个连接但系统层根本不允许你打开那么多fd连接全都在accept之前就被拒绝了。生产环境通常把ulimit -n调到65535或更高这只是起步操作。每次epoll_wait的超时时间不要设置太小。如果你在循环里无限设成0CPU会空转浪费设置成-1永久阻塞又可能让一些定时任务无法按节拍执行。我通常的做法是设置成100毫秒或1000毫秒然后配合定时器来处理非网络类任务这样网络请求和定时任务可以共用同一个主循环。最后关于这些多路复用机制的学习曲线我的建议是先把select调通搞懂fd_set和阻塞唤醒机制再去看poll的数组模型最后再深入epoll的红黑树和就绪链表结构。一步一个台阶每个函数背后的设计思路都吃透再看Netty、Redis这类框架的网络模型时就能一眼看穿它们底层的秘密。
返回列表