恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

零拷贝实战:从read+write到sendfile/splice,高性能I/O路径优化

  • 首页
  • 资讯中心
  • /
  • 零拷贝实战:从read+write到sendfile/splice,高性能I/O路径优化

相关资讯

VM15.0.x版本的运行centOS7出现电脑的蓝屏的问题 2026/10/5 10:45:56
Archimatix实战:Unity程序化建模与参数化街区生成指南 2026/10/5 10:45:56
KeyarchOS上DPDK硬件级网络加速:dpdk-tools实战与调优 2026/10/5 10:45:56

最新资讯

互补格雷码与相移码结合的结构光相位解包裹实现
深度学习心电异常检测:从信号预处理到部署的完整实践指南
从自动生成到源码解析:OpenZeppelin 5.x ERC20合约实战
基于SpringBoot与微信小程序的大学生餐厅点餐系统实战
Spring循环依赖源码解析:三级缓存能解决什么,解决不了什么
插件机制深度解析与加载失败排查:从 IAR 到前端工具链的实战指南

今日推荐

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单
YOLOv5 OBB旋转框训练实战:从DOTA数据准备到调参避坑全流程
Zeron 终端、Worktree 与 Diff 面板:像 IDE 一样查看并驱动你的代码变更

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

零拷贝实战:从read+write到sendfile/splice,高性能I/O路径优化

发布时间:2026/10/5 10:45:56
零拷贝实战:从read+write到sendfile/splice,高性能I/O路径优化 开篇先聊个很多人在背面试题时都会背到、但真正落到项目里却容易用错的概念——零拷贝Zero-Copy。我之前在一套C流式文件服务里做I/O路径优化把单机吞吐从大概1.2GB/s拉到2.7GB/s靠的并不是堆线程池而是把内核和用户态之间那几趟无意义的数据搬运砍掉了。这篇文章不会只讲sendfile和splice怎么调用而是从数据到底走了几条路、每一趟拷贝为什么会发生、什么时候可以安全绕过这几个角度把整个高性能I/O的核心逻辑拆开给你看。不管你是写网关、写文件服务、写日志采集器还是单纯想弄明白“为什么我的readwrite比不过别人的sendfile”这篇文章都值得你花15分钟读一遍。看完你至少能回答三个问题零拷贝到底零了什么、什么场景下它能真正起作用、什么场景下它反而帮倒忙。1. 零拷贝到底在解决什么问题1.1 一次最简单的readwrite背后发生了什么假设我们写一个最朴素的代理逻辑从磁盘读文件通过网络发出去。代码可能就三五行但数据在内核和用户态之间来回倒腾的次数远比你以为的多。第一次应用调用read数据从磁盘经过DMA拷贝到内核的页缓存page cache。这是第一次拷贝硬件参与的速度快但并不是免费的。接着内核把这份数据从页缓存复制到用户态缓冲区这是第二次拷贝走的是CPU纯浪费。应用程序拿到数据后调用write这份数据又从用户态缓冲区拷回内核的socket发送缓冲区这是第三次拷贝。最后内核协议栈把socket缓冲区的数据交给网卡又是一次DMA拷贝第四次。所以一个极其普通的readwrite实际发生了四次拷贝其中两次是CPU参与的用户态与内核态之间往返拷贝。CPU不仅浪费时间搬数据还要在用户态和内核态之间切换四次每次都带TLB刷新的代价。你机器看着cpu占用不高是因为大部分时间卡在内存带宽上光这点就足以把吞吐锁死在某一个值上。很多人以为零拷贝是“不用拷贝”的技术。准确说它真正绕过的只是“CPU参与的、在内核和用户态之间冗余往返的拷贝”硬件层面的DMA拷贝最终还是有的。1.2 用户态缓冲区这个中转发货站大部分时候根本没必要那问题就来了用户态缓冲区到底在什么情况下是必需的在你需要修改数据内容的时候在你需要把数据从非连续的内存区域拼装成连续报文的时候在你需要解析协议头再做分发的时候用户态缓冲区都不可或缺。但如果你只是想把文件内容完整地、原封不动地从磁盘挪到网络或者说从一端挪到另一端这个中转发货站就是纯成本。就好比你在A仓库取了一箱货运到B中转站卸下来再搬上另一辆车运到C仓库。中间那个中转站没有对货物做任何加工纯粹多花一次搬运工时。零拷贝做的事情用大白话讲就是告诉内核货不用进城了直接在A仓库和C仓库之间开一条直通链路。2. 内核里那些“少搬一趟”的机制到底怎么选2.1 mmap write好上手但坑也最多很多人第一反应是mmap把文件映射到用户地址空间然后直接write出去。这样确实省掉了read时从内核页缓存往用户缓冲区搬的那次拷贝文件数据在页缓存里映射之后用户态可以直接读到write的时候又会从这块映射区域把数据再拷到socket缓冲区。所以总共是一次DMA进页缓存、一次CPU拷到socket缓冲区、一次DMA出网卡。三次拷贝比朴素readwrite少搬一次。但mmap方案有一个很容易被忽视的坑——页错误page fault。mmap区域首次访问时数据并不在内存里访问时会触发缺页中断把磁盘数据读进页缓存。如果你的文件很大或者访问模式是随机性的缺页开销会吃掉你省下来的那点拷贝收益。另一个坑是mmap区域和写回之间有延迟如果你改完映射区立刻断电数据不保证落地更要命的是如果有人截断文件你正在访问的映射区域会直接触发SIGBUS进程崩掉。所以我的建议是mmap适合在一台机器上做高频小数据量的读写共享场景不太适合做大规模文件转发的核心路径。2.2 sendfile转发场景的主力但不是万能药sendfile系统调用做的事非常直接在内核态直接把页缓存里的文件数据送到socket缓冲区。如果网卡支持SG-DMAscatter-gather DMA数据甚至不需要真正拼到socket缓冲区里而是把页缓存数据的地址和长度直接塞给网卡描述符由网卡自己把分散的内存页取走发送。这种情况下一个sendfile转发路径只剩两次DMA拷贝CPU全程一次数据搬运都不参与。这就是它性能爆炸的原因。但是sendfile有两个天生的限制第一它只能从文件描述符往socket描述符方向走不能用于socket到socket的转发。你要做一个纯内存代理想把客户端A的数据直接转给客户端Bsendfile帮不上忙。第二它是“发送”视角的转发文件大小信息在调用时就要确定。传入的文件fd可以是任意支持mmap的文件但不能是socket、管道这类动态流。调用者也没办法在数据流中间做任何修改。2.3 splice把管道当“中转滑梯”真正的万能选手splice做的事情是让内核直接搬运数据但允许数据源和目的地任意组合只要有一端是管道。它的工作模式是从一个fd读数据进管道缓冲区再从这个管道缓冲区写到另一个fd。整个过程中数据不经过用户态管道缓冲区里的page指针可以直接被目标fd借用所以同样不需要真正的数据拷贝。这个方法最大的价值在于解开了sendfile的束缚——你可以从一个socket splicing到另一个socket可以实现服务端直接转发客户端流量而不占用用户态内存你也可以用splice把数据从磁盘送进管道再让另一个线程从管道splice出去实现更灵活的生产消费流水线。需要注意splice和sendfile有个性能上的差异splice每走一步都需要两次系统调用一次进管道、一次出管道而sendfile只需要一次系统调用完成全链路。所以海量小包、高并发连接场景下系统调用开销会成为瓶颈。sendfile适合文件到socket的整块搬移splice适合组合灵活的流式场景。机制CPU参与拷贝次数必须经过管道数据可修改系统调用次数适用核心场景readwrite2次否是2次小文件、需要业务处理mmapwrite1次否是1N次频繁读写同一文件sendfile0次支持SG-DMA时否否1次文件-socketsplice0次支持SG-DMA时是至少一端否每次拼接2次任意fd组合转发3. 内核里那台隐藏的“送货小推车”DMA重映射与网卡协同3.1 为什么网卡的SG-DMA能力这么重要前面的表格里我都附带了一个条件支持SG-DMA。这是什么概念传统DMA只能搬运一段连续内存所以内核必须先把文件数据从多个不连续的页缓存页集中拷贝到一个连续的socket缓冲区里这样网卡才能通过一次DMA把它取走。这一步就多了一次CPU参与的真实拷贝。支持SG-DMA的网卡则允许内核直接给出一张“清单”上面写清楚每个内存页的地址和长度。网卡芯片按清单自行去各个内存页抓数据组装成数据包发出去。整个过程CPU只需要写描述符不参与数据搬运。所以在选型或者看性能压测报告的时候先确认网卡是不是支持SG-DMA。大部分现代服务器网卡比如Intel XL710、Mellanox ConnectX系列都支持。但虚拟化环境里virtio-net这类半虚拟化设备对SG的支持要看具体的虚拟化实现经常成为性能瓶颈。3.2 更深一层的优化从内核协议栈手里继续抢时间如果你做的是高性能网关可能还会遇到一种情况你已经用了sendfile或者splice但压测发现CPU还是偏高。这时候再看一眼数据路径大概率是TCP/IP协议栈在处理校验和、分片、TSO/GRO卸载时消耗了CPU。这就是为什么更进一步的做法是把这些工作卸载到网卡硬件上。比如开启TCP Segmentation Offload让内核把一大块数据直接交给网卡由网卡硬件自己按MSS切分成多个TCP段发送。又比如Receive Side Scaling让多队列网卡把不同连接的中断分发到不同CPU核心上避免单核处理所有软中断。这些机制和零拷贝并不冲突叠加使用之后CPU才能真正从数据搬运和协议处理中解脱出来专注干业务逻辑。4. 实操把传统文件服务改成零拷贝的完整路径4.1 从readwrite改成sendfile最少改动方案假设你有一个C写的文件服务原来的核心发送逻辑大概是这样的std::ifstream file(path, std::ios::binary); std::string buffer(64 * 1024, \0); while (file.read(buffer[0], buffer.size()) || file.gcount()) { ssize_t sent 0; auto n file.gcount(); while (sent n) { ssize_t ret ::send(client_fd, buffer.data() sent, n - sent, 0); if (ret -1) { if (errno EINTR) continue; if (errno EAGAIN) { /* 需要处理背压 */ } } sent ret; } }这段代码的问题非常典型每次循环都要触发一次read的上下文切换、一次write的上下文切换或者更多次如果数据没一次发完。既拷了四次数据又频繁切换用户态与内核态。改成sendfile的版本核心逻辑会被压缩到极小int in_fd ::open(path, O_RDONLY); off_t offset 0; off_t remaining file_size; while (remaining 0) { ssize_t n ::sendfile(client_fd, in_fd, offset, remaining); if (n -1) { if (errno EINTR) continue; if (errno EAGAIN) { // socket发送缓冲区满等待可写事件后继续 poll(nullptr, 0, 10); continue; } // 处理其他错误 break; } remaining - n; }注意sendfile的第三个参数是一个指针每次调用后会被更新为新的偏移量所以循环内不需要自己维护偏移但必须确保sendfile不会一次发送超过remaining的字节。有人会想一次传个超大长度不就完了吗实际上socket缓冲区是有限的背压时sendfile照样会返回EAGAIN所以这个循环不能省。4.2 用splice实现socket到socket的零拷贝转发如果你的场景是纯流量转发比如写一个TCP relay用splice做起来效率比用户态readwrite高得多。关键点在于splice要求至少一端是管道所以你要准备一个管道把输入流先拼接进管道再拼接出去。int pipe_fds[2]; ::pipe(pipe_fds); auto relay [](int in_fd, int out_fd) { while (true) { // in_fd - pipe ssize_t m ::splice(in_fd, nullptr, pipe_fds[1], nullptr, 65536, SPLICE_F_MOVE); if (m -1) { if (errno EAGAIN) { // 没有可读数据等待下一次轮询 } else { break; } } // pipe - out_fd ssize_t n ::splice(pipe_fds[0], nullptr, out_fd, nullptr, m, SPLICE_F_MOVE); if (n -1) { if (errno EAGAIN) { // 目标端背压暂时等一等 } } } };这个结构看起来简单但实际项目里要注意两个点一是管道缓冲区默认只有64KB如果输入输出速率不匹配管道会堵住需要配合epoll去调度二是SPLICE_F_MOVE这个标志只是提示内核尽量转移page所有权并不是强制的别指望它产生本质差异。真正的性能来源依然是“不走用户态”。4.3 参数选择和优化的计算逻辑做这类优化经常需要性能预算概念。比如你要支撑10Gbps的网络吞吐也就是约1.25GB/s的真数据流量。如果CPU参与一次64KB数据的复制大约耗时1到2微秒取决于内存频率和通道数。在2.5GHz的现代CPU上这就相当于吃掉了几千个时钟周期。每秒钟要搬运1.25GB大约需要2万次64KB复制所以光拷贝就要消耗2万乘以几千周期直接吃掉数个核心的全部性能。从算这笔账出发你就能理解为什么数据转发类服务核心指标要看“每比特CPU成本”。sendfile和splice不参与用户态拷贝才可能在单核上支撑3到5Gbps的转发性能而readwrite模式下单核能跑到1Gbps已经很吃力了。这也是我在项目里做架构决策时最依赖的数据判断方式。5. 什么场景该用、什么场景不该用5.1 适合零拷贝的典型场景静态文件下载服务尤其是大文件视频点播、软件包分发文件内容完全不需要修改直接sendfile是理论最优。TCP反向代理或端口转发不关心协议内容只需原封不动转发流量splice能省下大量用户态开销。日志采集与传输日志文件从磁盘读出来发到采集端不修改数据同样适合sendfile。共享内存版本的大数据传递如果进程间需要传大量数据且不改动内容用pipe配合splice比消息队列省太多。5.2 用零拷贝反而更糟的场景第一个是超小文件。比如平均几百字节的短消息sendfile和splice的系统调用成本占比极高可能比readwrite的缓存命中路径还慢。这种场景优先优化的是减少系统调用次数、批量合并发送而不是追求一次拷贝都不发生。第二个是数据必须做协议解析、压缩、加密、编码转换的场景。你想在用户态做TLS加密就不得不把明文搬到用户态加密后再把密文发出去。零拷贝只能优化“不需要碰数据”的那部分。第三个是共享缓存命中率极高的场景。比如数据已经被热数据池缓存readwrite时内核页缓存命中后一次CPU拷贝的成本可能只有几百纳秒跟sendfile的额外系统调用开销相比差别不大。小额I/O密集时反而是业务逻辑本身占大头。场景特征推荐方案原因大文件、内容只读sendfile一次系统调用完成全部socket到socket转发splice绕过用户态参与需要解析报文再分发readwrite/handler拿到用户态才能改内容高频小消息批量聚合readwrite减少系统调用次数优先进程内共享内存读多写少mmap减少read拷贝随机访问也要小心缺页6. 常见问题与排查以及一些没人写进文档的经验6.1 为什么sendfile在虚拟化环境里表现平平碰到过不少朋友在云主机上测sendfile发现和readwrite差距没有那么夸张。排查思路是先确认网卡队列和SG支持情况。在虚拟机里网卡一般是模拟出来的SG-DMA卸载能力往往被虚拟化层削弱。可以查看ethtool -k eth0的输出看scatter-gather和tcp-segmentation-offload是否开启。很多时候打开TSO、GRO之后sendfile吞吐能再上一个台阶。还有一点容易被忽略hypervisor会对DMA操作做额外的内存映射开销。拿容器场景来说只要网卡还是走宿主机协议栈零拷贝能省掉的也只是容器内的用户态拷贝宿主机那边依然有自己的数据路径。所以容器里的零拷贝优化预期要比裸金属保守一些。6.2 sendfile出现EINVAL的经典原因代码明明照着文档写的结果一调用就返回EINVAL。最典型的原因是input fd指向的文件不支持mmap语义比如某些特殊文件系统或者O_DIRECT打开的文件。还有个常见情况就是output fd不是socket。sendfile的第二个参数必须是支持mmap的文件描述符第四个参数必须是socket。如果拿一个管道当成output那就要换成splice。排查建议先用strace确认是不是EINVAL然后检查fd的类型与打开标志。O_DIRECT模式下的文件内核页缓存路径会绕过sendfile不一定按预期工作。6.3 零拷贝并没有让磁盘IO变成异步魔法零拷贝解决的是数据不在用户态多待一圈的问题但它并没有改变整个读写流程对磁盘I/O的同步依赖。文件发到一半磁盘读直线下降时发送线程照样会卡在等待磁盘DMA完成上。换句话说零拷贝优化的是数据搬运效率不是磁盘随机读的延迟。所以如果业务热点是随机小文件读比如大量小图片请求性能瓶颈往往在预读、缓存替换策略、磁盘队列深度这些因素上。把sendfile换成splice并不会带来质变应该优化的是预读窗口或者用io_uring把I/O做成真正的异步。这一点我在实际项目里吃过亏——当时以为改成零拷贝就能解决小文件服务的低吞吐结果压测数据变化很小追查下去才发现瓶颈在块设备层排队。6.4 压测时最容易犯的错误只测吞吐不测并发零拷贝的收益在大并发下特别明显但很多人做对比压测时只开一个连接顺序发文件。这种场景下readwrite的内核页缓存命中率极高CPU拷贝有一定开销但网络本身是瓶颈差值不明显。建议至少模拟多连接同时下载、包大小混合的情况这时候才能看出CPU毛利率的差别。工具方面我常用wrk做HTTP场景压测再用perf top看CPU占比。如果perf top里看到的已经是copy_page_to_iter或者skb_copy_datagram_iter这类函数占大头说明数据拷贝路径还有优化空间如果看到的全是网络驱动和协议栈符号说明零拷贝路径已经走通了剩下的瓶颈很可能在别的环节。最后再分享一个实操里的判断口诀数据不被业务修改、形态是大块文件、路径明确是磁盘到网络就优先上sendfile数据要跨socket转发且不关心内容就上splice数据要到用户态做处理那就老老实实accept这次拷贝别为了“零拷贝”三个字硬套。零拷贝不是一个性能银弹它是把系统里可以省掉的重复工作省掉的那把手术刀。选对路径它能让你的服务吞吐上一个台阶选错路径它只是一个让你在压测报告里自我安慰的名词。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号