Linux I/O演进史:从管道到io_uring,一文读懂服务端高性能I/O

发布时间:2026/9/14 3:24:28
Linux I/O演进史:从管道到io_uring,一文读懂服务端高性能I/O 作为常年泡在服务端开发和Linux内核边上的老码农我经常被刚转行做后端的朋友问一个问题Nginx为什么能扛住十万并发Netty里那些NIO、零拷贝到底是什么神仙操作每次我都觉得三言两语讲不透因为这些问题的根子全部扎在Linux I/O这条主线上。最近正好赶上梳理自己的知识体系借着“Linux I/O演进史”这条线从管道讲到零拷贝把服务端开发绕不开的I/O原语串起来写一篇长文。这篇东西不装高深也不念PPT就是按我的实际理解把阻塞、非阻塞、多路复用、mmap、sendfile、io_uring这些概念掰开揉碎讲清楚它们解决什么问题、为什么会出现、实际工程里怎么选。适合刚接触网络编程的开发者也适合那些写了很久业务代码但没系统捋过I/O模型的同学。1. 一切从阻塞开始管道与“卡着等”的I/O模型1.1 管道的诞生进程间传输数据的第一次抽象管道可能是Linux里最古老、也最容易被忽视的I/O原语。你在shell里写的cat access.log | grep 404背后就是一个匿名管道。Shell创建管道时会调用pipe()系统调用内核返回两个文件描述符一个管读端一个管写端。左命令往写端塞数据右命令从读端取数据数据不落盘直接在内核的管道缓冲区里流转。管道最有意思的地方在于它的阻塞语义。读端在读一个空管道时进程会睡眠直到写端写入数据然后唤醒它写端在管道缓冲区被写满时也会睡眠直到读端取走数据腾出空间。这套“读写双方互相等待”的机制天然实现了生产者与消费者的同步不需要额外的锁。很多新手写代码时会奇怪为什么用管道传数据那么“自动”因为阻塞语义本身就是流控。我刚开始做服务端开发时觉得管道只是个shell小把戏后来研究线程池、生产消费模型才发现pipe()背后的这套阻塞同步思想几乎贯穿了整个Linux I/O体系。理解了“阻塞是内核帮进程调度等待”这个本质后面看epoll、io_uring都会顺畅很多。1.2 阻塞式socket一连接一线程的野蛮时代管道解决了进程间通信但服务端要面对的是网络I/O。早年写C/S程序最朴素的写法就是阻塞socket配多线程主线程accept()阻塞等连接来一个连接就pthread_create开一个线程去recv()阻塞读数据。这段代码在并发低的时候没毛病连接数到几百上千就开始痛苦了。一个线程默认栈空间8MB一万个线程就是80GB虚拟内存直接把你打爆线程上下文切换的开销也在飙升CPU大量时间花在保存恢复寄存器、更新调度队列上面。这个阶段的本质痛点在于线程是阻塞在I/O上的而线程本身是昂贵的资源。服务端想要支撑更多连接指望“一连接一线程”是不现实的必须让少数线程服务大量连接——也就是说I/O模型要从“主动等待数据”变成“被动通知数据到了”。这就是后面非阻塞和多路复用登场的直接原因。1.3 同步I/O的本质应用进程是“被卡住”的一方讲阻塞和非阻塞最绕的是“同步/异步”这两对词的排列组合。我用一个快递的比方来记你网购一个包裹快递柜就在楼下。阻塞同步你搬把椅子坐楼下眼睛盯着快递柜包裹没到就干等着到了自己取。非阻塞同步你每隔五分钟下楼看一眼没到就回屋干别的到了自己取。异步你给快递员留了手机号包裹一放进柜子快递员打电话通知你你去取。Linux上早期的read/write都是“下楼等着取包裹”进程要么睡眠阻塞要么反复自查非阻塞轮询。这种让应用进程亲自参与数据搬运的模式统称同步I/O。一直到io_uring出现之前Linux上主流的高性能网络方案epoll 非阻塞socket依然是同步I/O——epoll只是帮你高效“看快递柜”数据搬运还是你自己来。2. 多路复用时代从select到epoll的跳跃2.1 select和poll思路正确实现粗糙既然不能一连接一线程那就让一个线程盯着所有连接。select的设想很好你把所有关心的socket文件描述符丢给内核内核逐个检查这些fd是否有数据可读、可写然后告诉你有几个fd就绪了你再遍历找出是哪个。select的问题一是在于fd数量上限通常1024个大并发直接不够用二是每次调用select都要把整个fd集合从用户态拷贝到内核态数据量一大拷贝开销就上去了三是内核不知道哪个fd就绪了只告诉你有多少个就绪应用还得O(n)遍历整个集合去查。poll解决了fd数量上限问题用链表了但每次依然要全量拷贝、全部扫描。这些接口在高并发面前很快触到天花板。100万个连接里只有10个真正有数据select/poll依然要全量扫描一遍才能把这10个捞出来。复杂度是O(n)n是监视的fd总量而不是实际就绪数。这种低效是结构性的光调参救不回来。2.2 epoll的核心内核帮你就绪了才通知epoll的出现可以说是Linux网络编程的转折点它的思想用一句话概括内核帮你维护关注列表并且只在有fd真正就绪时才通知你。使用上就三个函数epoll_create()在内核里创建一个eventpoll对象或者用epoll_create1(0)顺手设个额外标志位。epoll_ctl()往这个对象里增删改关心的fd可以注册EPOLLIN可读、EPOLLOUT可写等事件。epoll_wait()阻塞等待内核返回就绪fd的列表。epoll的厉害之处在于就绪列表是内核主动维护的。当某个被监视的fd收到数据时内核协议栈会回调ep_poll_callback把这个fd挂进eventpoll的就绪链表里。应用调用epoll_wait时直接把这个就绪链表里的fd拿走就行。复杂度是O(k)k是实际就绪的fd数不再是全量扫描。epoll_ctl还有个EPOLL_CTL_ADD时的细节如果你要用边缘触发模式注册时要加EPOLLET标志要避免“惊群”可以在事件里加上EPOLLEXCLUSIVE让内核只唤醒等待队列里的一个进程而不是全部。2.3 水平触发与边缘触发新手最容易踩的坑epoll提供两种触发模式面试必问实际开发也必踩坑。水平触发LTLevel Triggered只要fd还有数据没读完epoll_wait就反复通知你。比如缓冲区有5KB数据你只读了2KB下一次epoll_wait还会返回这个fd。逻辑简单不容易漏读。边缘触发ETEdge Triggered只在fd状态发生变化的那一刻通知一次。比如缓冲区从0变成5KB时通知一次你读了2KB还剩3KB如果不再有新数据进来epoll_wait不会再次通知你。ET模式下你必须把缓冲区里的数据一口气读完读到EAGAIN为止不然就会丢数据。很多新手第一次写ET逻辑不小心漏读莫名其妙丢包查半天不知道原因。我自己在工程上比较保守如果是做代理、网关这类需要高性能的场景我会用ET加循环读配合用户态环形缓冲区减少系统调用如果只是常规业务服务LT就够用了省心不少。要注意的是ET必须搭配非阻塞socket否则读不到数据时线程阻塞在recv上整个事件循环就卡死了。2.4 epoll的正确食用方式事件循环用epoll写服务端核心是一个事件循环while (1) { int n epoll_wait(epfd, events, maxevents, timeout); for (int i 0; i n; i) { if (events[i].events EPOLLIN) { // 可读调用 read 或 recv 读取数据 } if (events[i].events EPOLLOUT) { // 可写说明 socket 发送缓冲区有空位可以发了 } } }这个循环配合非阻塞socket就是Nginx、Redis单线程模型的雏形。Redis能够用单线程扛住高性能正是因为它的事件循环里几乎没有阻塞操作所有I/O都是非阻塞的CPU密集计算又极短单线程不会成为瓶颈。但要泼盆冷水epoll解决的是“怎么知道谁就绪了”解决不了“数据搬运还是应用进程来做”的问题。一个连接收到100MB数据应用进程还是要调用read把数据从内核拷贝到用户态再调用write把处理结果拷贝回内核态。这就引出了零拷贝的用武之地。3. 零拷贝让数据绕开用户态3.1 传统I/O路径到底拷贝了几次先看一个最常见的场景用户请求下载一个文件服务端程序把磁盘文件内容通过网络发给客户端。最朴素的写法是read(file_fd, buf, len); write(socket_fd, buf, len);这段代码背后发生了四次拷贝磁盘数据通过DMA拷贝到内核页缓存page cache。CPU把页缓存数据从内核态拷贝到用户态bufread系统调用的核心工作。CPU把buf数据从用户态再拷回内核态进入socket发送缓冲区。网卡DMA从socket缓冲区把数据读走发送到网络。第2步和第3步是纯CPU拷贝数据在内核态和用户态之间反复横跳。一次简单的文件发送要经过两趟“内核→用户→内核”既浪费时间又浪费内存带宽。大型文件传输时CPU大量消耗在memcpy上磁盘和网络反而不是瓶颈。零拷贝的目标通俗讲就是让数据尽量在内核里面自己转不去用户态“旅游”一圈。3.2 mmap write减少一次CPU拷贝零拷贝的第一步是用mmap替换readchar *addr mmap(file_fd, size, PROT_READ, MAP_PRIVATE, 0, 0); write(socket_fd, addr, size);mmap把文件页缓存直接映射到用户进程的地址空间里。当进程访问这块内存时内核通过缺页异常把数据页映射到进程页表并不需要显式地“读到用户态buf”。这么一来read那一步的显式CPU拷贝省掉了。但这套方案还没完全零拷贝write时数据依然要从“mmap映射的内核缓存区域”拷贝到socket发送缓冲区。同时mmap在高并发场景有隐患——同一个文件被多次线程mmap页表管理变复杂文件被截断或篡改时还可能收到SIGBUS进程直接崩了。真实工程项目中我不太建议在文件传输里裸用mmap除非你能确保文件不会被并发修改。3.3 sendfile真正的一键零拷贝Linux 2.1时代引入了sendfile()系统调用专门为“文件→socket”这个高频场景优化sendfile(out_fd, in_fd, offset, count);sendfile的好处是全程在内核态完成数据搬运不经过用户态。数据从磁盘DMA拷贝到页缓存再直接拷贝到socket缓冲区最后DMA发送。拿传统方案一对比四次拷贝变成两次还都是DMA拷贝上下文切换也从四次read/write变成两次sendfile调用。不过sendfile有两个“坑”需要记住如果数据要从文件发到文件不是socketsendfile不一定高效有些内核版本对“文件到文件”的支持并不走优化路径。sendfile在数据需要经过用户态加工的场景毫无用处。你想对下载的文件做加密、压缩、加header内核不知道你的业务规则必须把数据拿回用户态处理。我在做静态文件服务时就是用sendfile实现的静态资源下发CPU占用率肉眼可见地掉下来了。如果是Nginx它会根据请求类型自动选择sendfile或普通读写这也是它服务静态文件性能极佳的原因之一。3.4 splice管道思想的延续与局限有时候数据源不在文件里而在另一个socket里典型的代理服务器场景。两个socket之间转发数据传统做法是read(socket_a, buf, len); write(socket_b, buf, len);又是一次完整的内核↔用户拷贝。Linux 2.6.17引入了splice它可以在两个文件描述符之间移动数据操作的核心是一个管道int pipefd[2]; pipe(pipefd); // 把 socket_a 的数据导入管道 splice(socket_a, NULL, pipefd[1], NULL, len, SPLICE_F_MOVE); // 从管道导入 socket_b splice(pipefd[0], NULL, socket_b, NULL, len, SPLICE_F_MOVE);splice走的是“管道缓冲区”机制数据不需要经过用户态而是通过页引用计数来控制共享和转移。它的思路和管道一脉相承——都是让内核缓冲区作为中转站只是管道原本是为进程间通信设计的splice则把这种能力扩展到任意文件描述符组合。splice的限制也很明显不是所有文件系统都支持socket与某些设备之间的splice可能静默失败或退回普通拷贝管道缓冲区大小也有限传递大文件需要循环调用。Nginx虽然编译了splice支持但实际很多场景下默认不启用就是因为要考虑兼容性。3.5 零拷贝不是“完全没有拷贝”说个容易误会的点零拷贝中的“零”指的是“零次CPU参与的用户态拷贝”而不是零次数据复制。硬盘到内存、网卡到内存之间DMA是要“搬数据”的页缓存到socket缓冲区之间如果硬件不支持某些特性可能也还要一次CPU拷贝。真正的零拷贝是尽量把CPU从数据搬运工角色中解放出来让CPU专心做协议解析、业务逻辑、流程控制。判断你用的方案是不是零拷贝可以看一个指标top里%sy内核态CPU占比。我做过一个文件网关服务普通read/write时%sy经常冲到40%换成sendfile后直接降到个位数。那多出来的CPU就是从memcpy里省出来的。4. 现代异步原语io_uring带来的I/O革命4.1 epoll还缺什么同步与非阻塞的天花板epoll让服务端可以轻松管理百万连接但它有一个绕不开的边界它只是一个通知机制数据读写仍然需要应用进程主动发起系统调用。一次数据读取一定是epoll_wait等通知→ recv收数据两次系统调用中间还有CPU拷贝。在高IOPS每秒几十万次读写场景下系统调用本身的开销、用户态/内核态切换的开销开始成为瓶颈。你可能听过io_uring这是Linux 5.1引入的异步I/O框架被誉为近年来Linux存储/网络I/O最重大的变化。它的核心思想是把系统调用从“请内核做一件事并等结果”变成“把任务放进队列内核自己取做完放回结果队列”。4.2 io_uring的工作方式队列替代系统调用io_uring在初始化时会创建两个内核与应用共享的环形队列SQSubmission Queue提交队列应用把要做的I/O操作read、write、accept、openat等打包成sqeSubmission Queue Entry写进这个队列。CQCompletion Queue完成队列内核处理完一个操作后把结果填写成cqeCompletion Queue Entry放进这个队列应用从这里取结果。亮点在哪里整个过程大部分场景下不需要显式的系统调用。应用写SQ、读CQ本质是在操作一块与内核共享的内存区域完全绕过了传统read/write每次都要陷入内核的开销。内核线程在后台批量消费SQ里的请求批量产生CQ里的结果这是典型的“批处理”优化思路。io_uring还支持IOSQE_ASYNC等标志位把文件I/O放到内核的异步上下文中执行不需要调用方阻塞。配合io_uring_wait_cqe来等待完成事件应用模型有点像“把请求扔给内核线程池内核做完通知你”。4.3 服务端开发要不要立刻拥抱io_uringio_uring很惊艳但我不建议所有人在生产环境一股脑上。理由如下内核版本要求高Linux 5.1才开始有io_uring生产环境很多是5.4、5.10、5.15的系统功能差异不小踩坑资料相对少。很多业务场景的瓶颈根本不在系统调用开销上数据库查询、业务逻辑、网络延迟才是大头换了io_uring也没质变。生态不够成熟像Nginx、Redis等主流软件对io_uring的支持还在演进中配套的调试工具、监控体系还不如epoll完善。我的建议是如果是做超高性能自研网关、存储引擎这类“I/O就是命脉”的系统io_uring值得认真研究比如RocksDB社区、ScyllaDB都在用如果只是常规业务服务先把epoll 非阻塞模型吃透收益更大。4.4 共享内存被忽视但也算“零拷贝”的原语聊完io_uring我想绕回一个和零拷贝相关的古老原语共享内存Shared Memory。管道、socket的零拷贝路径再高效也绕不开“数据要从进程A的地址空间流经内核再到进程B”。共享内存的思路更暴力直接把一块物理内存同时映射到两个进程的地址空间里进程A往这块内存写数据进程B直接就能看到完全零拷贝、零系统调用。service端组件之间做高频数据交换时比如消息缓存、统计聚合、状态同步共享内存是性能杀手锏。Linux里最常用的接口是shmget(key, size, IPC_CREAT | 0666); shmat(shmid, NULL, 0);共享内存的范式是配合信号量或自旋锁来做同步否则双写或读写竞争会直接污染数据。我印象最深的是做日志采集器时用共享内存加无锁环形队列agent进程和采集进程之间交换日志吞吐量比走管道高了两个数量级CPU几乎不动。这算是“零拷贝”思想在进程间通信上的另一种极端体现只是共享内存的管理成本和安全边界比管道要高不少。5. 服务端I/O原语全景一张表串起所有关键模型5.1 各模型对比速查学了这么多原语建议用一个表格把它们的核心差异拎出来模型/原语解决的问题内核版本是否同步用户态拷贝次数核心场景阻塞read/write同步数据读写极早期同步1次内核→用户简单场景、管道、常规文件读写管道pipe有血缘关系进程之间的字节流传输极早期同步0次内核缓冲转手shell管道、父子进程通信select/poll多fd监听古老同步每次全量拷贝fd集合小规模连接、兼容旧代码epoll大规模fd就绪通知Linux 2.6同步但事件驱动就绪fd列表拷贝高并发网络服务Nginx、Redismmap write减少read的用户态拷贝很早就支持同步1次映射页到socket缓冲大文件随机读、共享内存模型sendfile文件到socket的直接传输Linux 2.1同步0次静态文件服务、下载服务splice任意fd之间内核态搬数据Linux 2.6.17同步0次socket代理、数据中转共享内存进程间共享数据块很早就支持同步需自配锁0次高频数据交换、无锁队列io_uring通用异步IO 批量提交Linux 5.1异步可控支持O_DIRECT绕过页缓存超高IOPS、自研存储/网关这个表里最直观的一条规律是I/O演进的方向始终围绕两个目标——减少用户态参与省CPU拷贝缩短路径和减少线程等待从阻塞到事件驱动再到异步。5.2 一条服务端请求的完整I/O旅程把上面的原语放进一个实际请求里就能看到它们如何协作。假设你用Nginx架了一个文件下载服务一个客户端来请求/repo/linux.tar.gzNginx的master进程提前创建好监听socket注册到epoll实例中。worker进程阻塞在epoll_wait上等待新连接事件。这一步是事件驱动模型几十个worker就能扛住几十万并发连接。客户端TCP握手完成后监听fd就绪worker调用accept4可设置非阻塞和close-on-exec拿到新连接fd再次epoll_ctl注册。客户端发送HTTP请求连接fd可读worker通过非阻塞recv读取请求头解析出要的文件路径。打开目标文件拿到fd如果文件不大且无需加工就调用sendfile把文件内容直接发给客户端。整个文件内容不进用户态由内核页缓存转发到socket缓冲区网卡DMA发出。如果文件超大Nginx可能走aio线程或io_uring路径避免worker线程在磁盘I/O上长时间阻塞确保事件循环继续处理其他连接。发送完成关闭或keep-alive复用连接fd从epoll里增删改等待下一个事件。整个过程里epoll管“什么时候有活”非阻塞管“干不了活不傻等”sendfile管“少搬数据”aio/io_uring管“别阻塞流程”。这几个原语各管一段正好串起了服务端的I/O主干。5.3 选型建议什么场景用什么方案不少读者会纠结“我用什么I/O模型最合适”我的经验如下就是写个内部小工具、批量脚本阻塞I/O 管道就够了别整花活越简单越稳。常规Web服务、API网关epoll 非阻塞socket是底线Java NIO、Netty、Go的netpoll、Nginx的event loop全是这个路子。海量小文件下载、视频点播sendfile是你的本命再配合页缓存预读性能非常可观。代理服务器、流量中转splice能在某些场景省掉CPU拷贝但要先量化确认你的瓶颈真在拷贝上。自研存储引擎、消息中间件、超高频交易系统io_uring或共享内存这类“极限方案”才值得上而且要做好充足的压测和内核版本兼容预案。多进程/多实例之间做高频小消息通信共享内存 无锁环形队列是不错的思路但要注意崩溃恢复和权限隔离。我见过太多团队把简单服务性能问题全归咎于I/O模型张口就是“我们要上io_uring”结果压测一看瓶颈是业务代码里一堆JSON序列化。I/O模型选型的首要原则是先量化再优化先解决确定性的问题再去追求理论上的极限。6. 常见问题与排查技巧实录6.1 管道写满导致死锁现象子进程往管道写日志写了一阵子卡住不动父进程也在等子进程退出整个程序僵死。原因管道缓冲区默认64KBLinux上可通过 fcntl 修改写端写满后会阻塞等待读端消费。如果读端一直没人读比如父进程在wait子进程而没读管道就会形成互相等待的死锁。排查思路用strace -p pid看进程卡在哪个系统调用上通常能看到write(pipe_fd, ...)一直处于阻塞状态。解决办法是保证读端及时消费或者把写端改成非阻塞并处理EAGAIN。我在写日志采集脚本时就特意给管道写端加了超时和丢弃策略防止子进程因为日志量过大被活活堵死。6.2 epoll边缘触发漏读数据现象服务偶发地收不到完整请求客户端一直等响应直到超时。原因ET模式下fd状态从无数据变成有数据时只通知一次如果recv没有把数据读完剩余数据就一直留在内核缓冲区后面不会有新通知。常见于一次只读固定大小的buf而数据量大于buf。排查思路检查事件循环里是否在读到EAGAIN后才退出读取循环。我还遇到过更隐蔽的坑缓冲区设置了SO_RCVLOWAT导致epoll认为可读但recv读不到数据。解决方法是把ET模式的读循环写成“无脑读到EAGAIN为止”并且确认没有设置奇怪的套接字低水位线。6.3 sendfile的offset与文件长度处理不当现象用sendfile发送大文件客户端下载的文件总是不完整或服务端报Invalid argument。原因sendfile的offset参数如果不传内核默认从当前文件偏移开始发但很多调用者忽略了对offset和count的管理。另外如果向一个不支持sendfile语义的描述符例如某些socket类型执行sendfile会返回EINVAL。排查思路先确认in_fd是普通文件、out_fd是TCP socket填充struct off_t offset时每次sendfile返回后记得更新offset并减去已发送长度。写一个循环发送逻辑直到count归零。我建议把sendfile封装成“带循环和偏移更新”的工具函数避免裸用。6.4 mmap文件被截断导致进程崩溃现象进程在访问mmap映射区域时突然收到SIGBUS直接退出日志里啥都没有。原因mmap映射了文件后如果另一个进程把文件截断truncate映射区域里超出新文件大小的部分就变成非法访问内核触发SIGBUS。这在共享配置文件、共享日志文件场景中很容易出现。排查思路从两个方面预防一是对可能被并发修改的文件慎用mmap优先考虑加锁或拷贝二是注册SIGBUS信号处理函数至少能在崩溃前打点日志。更稳妥的做法是用fcntl(F_SETLEASE)或文件锁来防止其他进程截断。我以前踩过一次后现在只要看到mmap第一反应就是问“谁来保证这个文件的完整性”。6.5 零拷贝在HTTPS加密场景失效现象线上服务从HTTP改成HTTPS后发现明明用了sendfileCPU开销却比之前高了。原因TLS加密必须在用户态完成。数据要从内核读出来用OpenSSL加密后再写回去sendfile根本用不上。看起来“零拷贝”失效了但这不是bug而是加密流程决定了必须触碰数据。排查思路如果流量必须加密零拷贝的意义就不在“绕过用户态”而在“减少用户态拷贝次数”——通常的做法是文件发送走sendfile到内核再出去加密后的数据走正常write。或者考虑内核TLSKTLS特性把加密下沉到内核态不过它对硬件和内核版本有要求。生产上我的建议是静态资源用CDN加HTTP/2配合客户端缓存优先减少数据重复传输比纠结加密路径上的零拷贝更有收益。6.6 多进程监听同一端口时的惊群问题现象Nginx多worker配置下突然的并发连接导致CPU突发飙高负载异常。原因所有worker进程都在epoll_wait同一个监听socket一个新的连接到达时内核会唤醒所有等待的worker但最终只有一个worker能accept成功其余进程白白被唤醒一轮这就是惊群效应。虽然消耗不至于致命但高并发下放大明显。排查思路Linux 4.5引入了SO_REUSEPORT可以让多个进程分别绑定同一端口的独立socket内核做负载均衡从源头避免多个worker争抢同一个fd或者使用EPOLLEXCLUSIVE标志让epoll只唤醒等待队列中的一个进程。我自己配置Nginx或自研网关时只要能确保worker数量与CPU核数匹配就会优先上SO_REUSEPORT实测在连接突发场景下更能扛。写在最后的一点经验我从读《Unix环境高级编程》开始接触这些I/O原语到后来亲手在项目里优化文件传输、处理epoll边缘触发丢包、围观性能测试从CPU打到“零拷贝”的种种现场最大的感悟是Linux I/O的演进并不花哨每一步都是被真实问题逼出来的——连接太多就用多路复用拷贝太重就搞零拷贝系统调用太频繁就上io_uring。对服务端开发者来说真正值钱的不是背下每个函数签名而是当你面对“十万并发”“大文件传输”“CPU打满”这些现象时能在脑子里第一时间浮现出这张演进地图问题出在哪一环哪个原语命中要害代价又是什么。这篇文章如果非要浓缩成一句话我会说先把阻塞I/O和管道的“同步等待”想明白把epoll的事件驱动玩顺再根据业务瓶颈决定要不要上零拷贝和异步I/O。工具永远在更新但数据从产生到消费这条路径上“减少等待、减少拷贝、减少无谓上下文切换”这三个基本原则什么时候都不过时。