资讯动态

IO多路复用深度解析:从select、poll到epoll的LT与ET实战

发布时间:2026/9/13 4:51:19 来源:尧图企业网站定制
IO多路复用这东西搞网络编程的人迟早都得正面硬刚它。只要是做高并发服务端、中间件或者底层网络框架的几乎天天跟它打交道。你可能听过一句话正常开发不用自己写epoll但如果你不理解多路复用的原理和行为差异等到线上出现CPU飙高、连接堆积、延迟抖动的时候你连排查方向都找不到。这篇文章我就把select、poll、epoll这些老伙计的脾性彻底讲透包括它们的内核行为差异、LT和ET两种触发模式的坑、以及真实场景里怎么配合组件做调优都是这些年实打实踩坑换来的经验。先给一个全局认知IO多路复用本质上就是让一个线程同时盯住成千上万个连接哪个连接上有数据可读、可写就处理哪个没有事件就阻塞等待。它解决的核心问题不是“读写更快”而是“让有限的线程服务更多的连接”。Nginx能单线程扛住百万并发Redis单线程还能跑十万级QPS底层靠的都是这套机制。这篇文章适合正在学网络编程的开发者、准备系统设计面试的工程师以及线上服务出过性能问题的运维和后台同学。我会用大量代码片段、实时对比和踩坑记录来讲不是停留在概念层面。1. 为什么必须用IO多路复用从阻塞IO到事件驱动的演进逻辑1.1 阻塞IO的致命痛点咱们先看最朴素的阻塞IO模型。你写一个socket服务端调用accept()等待客户端连接没有连接时线程就卡死在那。有连接进来后调用recv()读数据客户端半天不发数据线程又卡死在recv()上。如果这时候第二个客户端连上来谁来处理没人处理。解决办法也很直觉一个连接一个线程主线程只负责accept()每来一个连接就创建新线程去处理。这套模型在连接少的情况下完全没问题代码也好写业务逻辑可以随意阻塞。但连接数一旦上来线程数量跟着暴涨问题就来了。每个线程默认栈大小8MB1000个连接就是8GB虚拟内存光内存就顶不住。更不用说线程切换带来的上下文切换开销CPU时间全耗在切换上了真正干活的反而没多少。我见过一个团队用线程池优化但连接数超过线程池上限后请求还是排队延迟照样惨不忍睹。线程不是不能开而是开线程的成本远比你想象得高换来的却是大量线程在99%的时间里都在空等。1.2 非阻塞IO和忙轮询的性价比陷阱有人想既然阻塞会卡线程那我用非阻塞模式不就行了把socket设置成O_NONBLOCKrecv()调用不会等数据没数据就立即返回EAGAIN。然后主线程用一个循环不停遍历所有连接挨个recv()一遍有数据就处理没数据就跳过。这就是忙轮询。这个方案避免了多线程但代价更大。每次recv()都是一次系统调用要从用户态切到内核态。你有10万个连接每轮都要调用10万次recv()哪怕99%的连接什么都没干系统调用开销也把CPU跑满了。更关键的是你怎么知道哪几个连接有数据你只能挨个去问这是纯盲试。而且每次recv()调用还涉及把数据从内核缓冲区拷贝到用户缓冲区即使数据没到这一趟也要走完。忙轮询的本质是用CPU时间换线程资源在高并发场景下这种交换极其不划算。1.3 多路复用凭什么成为主流方案IO多路复用的思路和上面这些完全不一样。它把“监听”这项工作下沉到内核去做。你告诉内核“帮我盯着这批fd有任何一个可读或可写就告诉我。”然后你用一个线程阻塞在等待函数上内核发现有事件就返回对应的fd列表。这个过程里用户态和内核态之间传递的是“哪些fd就绪了”这个信息而不是盲目的反复系统调用。这就是它和忙轮询的本质差异你把遍历和等待的工作交给更擅长做这件事的内核。内核可以通过自己的内部数据结构维护fd集合当IO事件发生时由驱动的回调机制触发唤醒而不是用户态一个个去问。所以连接数越多多路复用的优势越明显。10万个连接可能只有两三个有数据多路复用一次调用就能拿到这两三个fd其他没事件的连接完全不需要被扫描。1.4 一个能说明问题的最小模型我经常用这个模型给人讲多路复用。假设你是一个只有一个服务员的餐厅每桌客人点了菜之后就不再呼叫服务员而是把“需要服务”的状态放在桌上。服务员每隔几秒在店里走一圈只服务那些桌上有提示牌的客人。这比之前的“每桌配一个服务员”高效得多也比“服务员不停问每一桌客人要不要加水”高效得多。这个模型里“店内走一圈”就是一次epoll_wait“桌上的提示牌”就是内核里的事件就绪状态“只有提示才服务”就是事件驱动。你不需要知道每一桌客人此刻的具体状态你只需要知道谁发出了请求。这也是为什么叫“多路复用”——多个连接复用同一个等待线程而不是每个连接独占一个线程。2. select、poll、epoll深度对比三个时代的迭代逻辑2.1 select的实现机制与文件描述符上限之谜select是最早进入POSIX标准的多路复用接口至今快五十年了。它的用法很直白把fd集合放进fd_set结构体调用select()时把可读集合、可写集合、异常集合传进去内核帮你检查哪些fd就绪了返回后你把每个fd挨个检查一遍。fd_set底层其实是个位图每一位代表一个fd是否在监听集合里。位图的大小由FD_SETSIZE决定通常是1024。这意味着你最多只能监听1024个fd超了就不让用了。很多人以为这个限制可以靠改宏重新编译内核绕过实际上这是个两难把FD_SETSIZE调大会导致每次调用select()时拷贝的位图变大而且返回后线性扫描的开销也变大得不偿失。还有两个隐藏问题经常被忽略。第一select()每次调用前你要重新把fd集合从用户态拷贝到内核态调用结束后内核返回的结果也放在这个集合中下次调用前你还得再重置一遍这来回拷贝在连接数上来后是巨大的性能损耗。第二内核本身也是线性遍历所有fd来检查状态的所以即使没有事件发生你也要付出O(n)的时间来“巡视”一遍。select在fd数量少、活跃连接占比高的场景其实还行一旦fd数量上千性能就悬崖式下跌。2.2 poll出现后解决了什么又留下了什么poll的诞生直接解决了一个痛点取消FD_SETSIZE上限。它改用pollfd数组来管理fd数组长度理论上可以随意扩展不再受位图大小限制。每个pollfd结构体里有fd、events要监听的事件、revents返回的就绪事件通过revents和events分离实现了“输入输出参数分离”比select里集合被覆写的设计干净不少。但poll的核心缺陷没有变它依然要线性扫描所有fd依然要每次把整个fd数组从用户态拷贝到内核态内核返回后用户态还要再遍历一遍数组来找哪些fd就绪。连接数一多时间复杂度还是O(n)只是n的上限变大了而已。说句难听的poll只是把select的短板从“数量受限”改成了“数量不受限但效率不行”内部机制并没有本质飞跃。实际推进过程中还有一个折磨人的细节每次调用poll你都得从业务维护的连接列表里重建一次pollfd数组因为内核会修改revents字段。这意味着所有fd的注册信息要不断重复拷贝到内核连接数越大这个拷贝开销越让人肉疼。所以poll适合几千连接以内的场景再往上就不行了。2.3 epoll的三板斧注册、等待、获取事件epoll是Linux专属方案也是目前Linux平台上事实标准的IO多路复用实现。它引入了三个系统调用epoll_create创建实例epoll_ctl注册/修改/删除fdepoll_wait等待事件。这个设计把“注册”和“等待”拆开了注册是一次性的后续等待时不需要再重复拷贝整个fd集合这是和select/poll最大的区别之一。内核里epoll用红黑树维护所有注册的fd增删改的时间复杂度都是O(log n)。当某个fd上有IO事件发生时内核通过设备驱动的回调函数把对应fd节点加入就绪链表epoll_wait检测到链表非空就把就绪事件拷贝到用户态数组。这个过程中它只返回有事件发生的fd不返回全部fd所以用户态不需要遍历所有连接。就绪检测的时间复杂度做到O(1)处理能力不再随总连接数线性下降。还有一个细节很重要epoll_wait返回时把内核里就绪链表的事件copy_to_user到用户态数组。如果用户态提供的事件数组长度不够内核会把多余的就绪事件留在链表里下次调用继续返回。理解这个机制对后面的ET模式排查有帮助。2.4 三张表说清楚select、poll、epoll的本质差异我整理了一份对比表项目里做技术选型时可以直接参考维度selectpollepollfd数量上限受FD_SETSIZE限制约1024无硬性上限无硬性上限受系统内存影响时间复杂度O(n)线性扫描O(n)线性扫描注册O(log n)就绪O(1)用户态拷贝每次调用全量拷贝fd集合每次调用全量拷贝fd数组注册时拷贝一次等待时不重复拷贝就绪事件返回方式修改fd_set需遍历检查修改revents需遍历数组直接返回就绪fd列表跨平台几乎所有平台Windows不支持Linux独有触发模式仅水平触发仅水平触发支持水平触发和边沿触发线程安全性较差较差相对较好多线程可配合这张表直接回答了“为什么高并发服务器都选epoll”数量不受限、效率不受fd总数影响、不需要反复拷贝。但是请注意epoll是Linux平台的方案如果你做跨平台服务端macOS上要用kqueueWindows上用IOCP或select这是由操作系统IO模型决定的不是你想用哪个就用哪个。2.5 kqueue和IOCP简说epoll不是唯一答案既然提到跨平台顺便说说kqueue。macOS和BSD系统上的kqueue和epoll逻辑很像也是事件驱动、内核维护注册状态、只返回就绪事件。区别在于kqueue的事件类型更丰富不仅能监听socket连文件、信号、定时器都可以挂进去API抽象更统一。但kqueue是BSD系专属Linux上不存在所以Nginx在Linux跑epoll、在macOS跑kqueue源码里做了大量平台抽象。Windows上的IOCP思路又不一样。它是真正的异步IO模型你发起一个异步读操作系统读完数据后通知你取结果连“等待事件”这一步都省了。这比epoll更彻底但编程模型也复杂得多需要处理重叠结构和完成端口。Netty在Windows上用的就是IOCP。所以说epoll是Linux世界的高性能答案但不是所有平台的高性能答案选型前一定要先确认部署环境。3. LT和ET触发模式一张图理解的机制十次线上事故才记住的坑3.1 水平触发和边沿触发的本质区别epoll支持两种触发模式这是select和poll都不具备的。LTLevel Triggered水平触发是默认模式只要内核缓冲区里还有数据可读epoll_wait就会一直返回这个fd。比如缓冲区里来了1KB数据你只读了500字节下次调用epoll_wait还会把这个fd返回给你直到数据被读完。ETEdge Triggered边沿触发则是在fd状态发生变化的那一刻通知你一次从无数据变为有数据或者从不可写变为可写。你收到通知后如果没把数据读完后续不再触发除非又有新数据到来。我用一个生活化类比来解释LT就像一个快递柜只要柜子里有包裹每当你看一眼柜子系统就会提醒你有包裹ET就像一个门铃快递员按下门铃响一次你没听到那面包裹就躺着门铃不会再响第二次除非下一个快递员再来。3.2 ET模式为什么要求非阻塞和循环读ET模式的高效之处在于它减少了事件通知次数内核不需要反复把同一个fd放进就绪链表减少了用户态和内核态的交互频次。但代价是你必须在一次通知中尽可能把所有数据读完否则剩下的数据要等下一个新事件到来时才能处理。要实现“读完为止”ET模式有两个硬性要求。第一socket必须是非阻塞的否则最后一次循环读没有数据时会阻塞在recv()上整个线程就卡死了。第二读取必须循环到返回EAGAIN为止只有这个返回值才代表这次真的读干净了。我见过太多第一次写ET模式的同事只读一次就完事结果数据卡在缓冲区里服务端迟迟不处理客户端还在等响应最后只能靠超时重试兜底。这里还有一个很多人忽略的点ET模式下epoll_wait只在状态变化时触发如果你用了epoll_ctl的EPOLL_CTL_MOD重新注册事件在某些内核版本上也可能会产生一次事件注入导致不合预期的唤醒。所以ET模式下事件处理逻辑要严格按照“状态机”的思路写不要依赖临时事件。3.3 LT和ET在实际项目中的选型建议既然ET更高效为什么很多项目依然用LT因为LT的编程模型更简单不容易掉数据。你在LT模式下一次没读完没关系下次epoll_wait还会告诉你。这种容错性对业务逻辑复杂、处理时间不确定的场景非常友好比如SSL解密、HTTP解析等需要分片处理的场景。LT是安全性优先的默认选择ET是性能优先的进阶选择。我的建议是如果你在写一个框架或中间件对性能和响应时延有极致追求比如网关、代理、IM服务端可以用ET非阻塞循环读配合内存池管理接收缓冲区性能天花板更高。如果你在写普通的业务服务连接数和数据量都适中用LT就够了开发效率高线上行为好预测。不过要提一句Nginx用的是ET模式这是它能用单线程处理海量连接的关键之一。而Redis多路复用部分默认用的是LT模式但业务逻辑简单单次读取就能吞下大部分请求性能瓶颈主要在命令执行本身不在IO模型上。3.4 ET模式下导致吞吐量骤降的典型案例我可以说一个真实踩过的坑。之前做即时通讯网关连接数3万左右用的ET模式。某次上线后发现推送量一大CPU使用率反而下降了但消息延迟从原来的几十毫秒涨到几秒。一开始完全没想到是IO模型的问题以为是下游服务变慢。后来抓包才发现服务端在ET模式下只来得及读取一次数据请求被拆成了多个TCP分片到达第一次读取只读到了部分分片后续分片虽然已经到达但因为“没有新事件触发”epoll_wait一直不返回这个fd剩下数据就在内核缓冲区里躺着。客户端等不到响应超时重试又引发更多流量恶化了情况。解决办法也很简单把所有socket句柄全部设置非阻塞读取逻辑改成循环读直到EAGAIN。还有一个辅助手段是开启TCP的SO_RCVBUF调整把接收缓冲区调大减少分片被截断的概率。这件事给我最深的教训是ET模式不是拿过来就能用它要求你对底层socket行为有完整的掌控否则就是拿生产环境交学费。4. 实战中的核心场景与组件联动Redis、Nginx和Netty的IO模型解剖4.1 Redis为什么能单线程扛高并发Redis的数据结构操作都是内存级别的速度快到微秒级真正的瓶颈在于网络IO。如果IO效率不高再快的内存操作也得等网络。Redis主线程采用IO多路复用用ae事件处理库封装了Linux的epoll、macOS的kqueue以及兼容的select事件驱动地处理客户端请求和定时任务。主线程在aeMain循环里调用aeProcessEvents先查最近定时任务算好超时时间然后调用多路复用的等待函数。有事件来了就分发到对应处理函数比如读事件对应readQueryFromClient写事件对应sendReplyToClient。因为命令处理本身就是纯内存操作单线程不需要考虑锁竞争配合多路复用自然能轻松支撑几万甚至十万级别的QPS。说句题外话Redis虽然有单线程但持久化fork子进程做RDBAOF的fsync也可能放后台线程执行这不算破坏单线程事件循环模型不要搞混。4.2 Nginx的惊群问题与多进程协作Nginx用的是master-worker多进程模型每个worker进程都有一份自己的epoll实例。早期版本有个著名的“惊群”问题当新连接到达时所有worker进程的epoll都被唤醒竞争同一个accept()最后只有一个成功其他都白忙活。Linux 2.6内核支持SO_REUSEPORT后Nginx引入了按进程绑定独立监听端口的模式内核将请求负载均衡到不同端口队列从根源上避免了惊群。Nginx的高并发能力不仅来自epoll还来自它的事件驱动架构。它在事件处理阶段采用状态机式的HTTP解析每个连接维护当前解析状态读数据就推进状态不阻塞在任何等待操作上。配合ET模式和非阻塞IO一个worker进程能轻松管理几万个活跃连接。这给我们的启示是多路复用只是一个起点要发挥它的全部威力整个业务处理流程都不能有任何阻塞。4.3 Kafka和RocketMQ对多路复用的应用差异Kafka不像Redis那样所有连接都托管在主线程而是用Selector组件在客户端连接和网络请求之间做分发。它在Acceptor线程accept新连接后将连接注册进processor线程的selector中processor线程再通过多路复用监听这些连接上的请求。每个processor线程有自己的selector这样既能水平扩展线程数又能保持每个线程IO多路复用。RocketMQ的Remoting模块则直接用Netty这也说明Netty在Java生态里几乎成了网络层标准组件。Netty内部对多路复用的封装已经非常成熟它屏蔽了Linux epoll和NIO selector的差异还提供了EpollEventLoopGroup这种原生epoll实现比Java NIO在性能和堆外内存使用上更有优势。如果你的服务是Java技术栈我建议直接站在Netty肩膀上而不是自己封装多路复用后者要踩的坑实在太多。4.4 自研多路复用服务端的程序骨架不管组件怎么封装多路复用的核心程序结构其实就那几步。我写一个最简的epoll服务端骨架它不完整但能清楚展示关键节点// 创建epoll实例 int epfd epoll_create(1024); // 监听socket int listen_fd socket(AF_INET, SOCK_STREAM, 0); bind(listen_fd, ...); listen(listen_fd, 1024); // 把监听fd加入epoll 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[1024]; while (1) { // 等待事件 int n epoll_wait(epfd, events, 1024, -1); for (int i 0; i n; i) { if (events[i].data.fd listen_fd) { // 有新的连接到来 int conn_fd accept(listen_fd, NULL, NULL); setNonBlocking(conn_fd); // 配合ET模式必须非阻塞 struct epoll_event ev_conn; ev_conn.events EPOLLIN | EPOLLET; ev_conn.data.fd conn_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, ev_conn); } else { // 处理已有连接的数据 handle_read(events[i].data.fd); } } }这个骨架虽然精简但包含几个关键设计监听fd和连接fd在同一张epoll表里通过data.fd区分事件类型epoll_wait的第二个参数是事件数组每次最多返回1024个就绪事件如果是ET模式handle_read里必须非阻塞循环读直到EAGAIN。实际工程里还要考虑EPOLLOUT写事件的管理、过期连接踢除、读半包处理、内存池复用等复杂度会成倍上升。所以如果你不是在校验自己的底层基本功我依然建议直接使用成熟框架。4.5 多路复用下的定时器实现技巧多路复用通常配合超时管理来做连接空闲检测。你不可能为每个连接单独起一个定时线程成本太高。常见做法是在epoll_wait传入一个timeout参数每轮事件循环醒来时检查所有连接的最近活跃时间把超时的连接关掉。另一种更高性能的做法是用最小堆或时间轮管理定时器把最近要超时的连接时间作为epoll_wait的timeout参数这样epoll_wait既能等事件也能在超时时刻准点醒来不会误伤其他连接的等待时间。Redis对空闲连接的处理就是这个思路阻塞等待事件前算好最近一个定时任务的执行时间到了时间点哪怕没有事件也会醒来做定时任务。这个设计很值得借鉴尤其是连接数多、空闲比例高的场景能显著降低无意义的唤醒次数。5. 常见问题排查与性能调优多路复用实战避坑指南5.1 文件描述符耗尽怎么快速定位和解决多路复用下连接数一旦超过进程的fd数量上限accept()根本拿不到新连接表现为客户端连接超时或者直接被拒。最常见的排查命令是ulimit -n查看当前进程的fd限制以及用lsof -p 进程号 | wc -l统计进程实际打开的fd数。如果两者很接近基本可以判断是fd耗尽了。解决思路分两层一是调大ulimit -n和/etc/security/limits.conf里的nofile限制这部分其实是运维侧的基本操作二是优化代码里的fd使用习惯比如连接的socket没关闭、日志文件句柄反复开关、数据库连接池没复用都可能悄悄吃满fd。我遇到过一个案例客户端连接断开后服务端没有清掉对应fd导致fd数量稳步上涨最终触发fd耗尽。这个问题的根因是没监听EPOLLRDHUP和EPOLLHUP事件正常情况下连接关闭后epoll会返回对应事件你不处理就等于漏了。5.2 回调风暴和边缘触发饥饿为什么你的线程卡死ET模式下有个典型的“饿死”问题某个连接数据量特别大你循环读到EAGAIN但这个过程中其他连接的读写一直得不到调度就像一个大嗓门的人一直霸占话筒。如果处理线程数量少其他请求延迟会飙升表现为“有些连接很快有些连接超时”。这是ET的高性能和公平性之间的天然冲突。解决办法通常是限制单次循环读的上限比如最多读64KB或者最多循环100次即使没读到EAGAIN也先让出CPU下次事件再继续。另一种方案是把这个连接的剩余读工作丢到线程池让主事件循环保持高效调度。这个思路在Netty的DefaultEventExecutor设计里也能看到它允许慢业务从IO线程池剥离避免拖慢事件循环。5.3 EPOLLOUT事件的正确使用姿势很多人一开始只关注EPOLLIN觉得能读数据就够了但写出高性能服务必须把EPOLLOUT也管好。发送数据时如果你直接send()一个很大的包内核发送缓冲区满了send()会阻塞或者返回EAGAIN。这时候你如果傻等其他连接就得不到服务。正确姿势是当send()返回EAGAIN时把待发送的数据挂在连接的应用层缓冲区里再通过epoll_ctl给这个fd注册EPOLLOUT事件。等内核发送缓冲区有空余空间时epoll_wait会返回这个fd的写事件你从应用层缓冲区里取出剩余数据继续发送。发送完毕后记得用EPOLL_CTL_MOD把EPOLLOUT事件去掉否则fd一直可写会反复触发无意义的事件白白消耗CPU。这个机制在实现限流和慢消费场景时尤其重要。比如一个消费端处理不过来服务端发送缓冲区填满你用EPOLLOUT挂起发送既不会丢数据也不会让线程阻塞。5.4 多线程下epoll惊群的二次困扰epoll本身还有一个惊群变种多个线程同时阻塞在同一个epoll实例的epoll_wait上当有事件发生时所有线程都会被唤醒但只有一个能拿到事件。Linux 4.5引入了EPOLLEXCLUSIVE标志可以避免这种多线程同时唤醒但需要所有线程在epoll_ctl注册事件时都带上这个标志。如果你的模型是需要水平扩展的Java服务避免惊群最简单的方式是使用多个epoll实例每个实例由一个线程处理连接按负载均衡算法分配到不同的实例。Netty中的EventLoopGroup就是这样做的每个EventLoop有自己的selector连接绑定到一个EventLoop上与该连接相关的所有读写都在同一个线程内串行处理既避免数据竞争又避免惊群。这是理解Netty线程模型的核心也是为什么Netty在大并发场景下能做到如此干净高效的底层原因。5.5 性能测试时容易被忽略的调优参数最后分享一组我在压测多路复用服务时经常调整的系统参数。net.core.somaxconn控制监听队列长度如果accept()处理不过来客户端连接会堆积在队列里甚至被拒绝。net.ipv4.tcp_max_syn_backlog影响SYN半连接队列大小高并发首次连接时这个值很重要。net.ipv4.tcp_tw_reuse配合tcp_timestamps可以加快TIME_WAIT状态socket的回收减少端口占用。还有一个很多人不注意的rmem_max和wmem_max控制socket收发缓冲区大小。如果你做的是大包传输适当调大这两个值能减少内核缓冲区与用户态之间的拷贝次数因为数据可以攒够一批再读出来。如果你做的是小包高频交互缓冲区调太大会增加内存占用和触发延迟反而未必好。所以不要盲目照搬网上的“调大缓冲区”经验一定要结合自己的包大小和延迟需求来定。拿我自己举例做IM网关时我明确区分了控制消息和业务消息控制消息走默认缓冲区保证低延迟业务消息的socket单独调大缓冲区保证大文件传输效率。这种区分在同一个进程里完全可行属于用空间换性能的典型实操。5.6 跨平台兼容的两个坑错误的类型假设和边缘回调如果你不是只在Linux上跑写多路复用代码时还要小心平台差异。第一个坑是时间结构体类型select的timeval在某些平台用秒和微秒在Windows上还涉及fd_set的第一字段是fd_count而不是位图如果你照着Linux的代码直接抄到Windows那基本一跑就崩。第二个坑是边缘触发在不同平台上的语义不完全一致。Linux的ET对“状态变化”的定义是数据从无到有、缓冲区从满到不满而FreeBSD的kqueue在EV_CLEAR语义上的细节也有差异。做跨平台库时建议用条件编译把IO模型隔离掉Nginx的实现就是最好的参考样板不要试图写一份代码同时完美适配所有平台。写在最后分享一点个人经验做网络编程这些年我最大的体会是多路复用不是一种高深的魔法而是一种资源调度哲学。它把宝贵的线程资源从无意义的等待中解放出来让每次CPU调度都尽可能做有用的事。真正的高手不是背几个API而是能理解系统在什么条件下会有怎样的行为然后顺着系统的脾性设计自己的代码。如果你刚接触这个概念我建议你亲自动手写一个epoll版本的Echo Server压测一下再对比着写一个select版本感受一下性能差距。踩过这些底层细节的坑之后再去看Netty、Nginx、Redis的源码你会发现自己看的不再是堆积的函数调用而是一整套环环相扣的IO哲学。这种理解一旦建立处理线上网络问题时的底气会完全不一样。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价