当前位置:首页 > 文章列表 > 文章 > linux > Linux sendfile 和 splice 怎么选:文件到网络的零拷贝边界与 strace 验收

Linux sendfile 和 splice 怎么选:文件到网络的零拷贝边界与 strace 验收

来源:17golang原创 2026-08-26 03:58:59 0浏览 收藏

静态文件接口突然变慢时,很多排查会先把线程数和缓存调大,却没看数据到底经过了几次用户态拷贝。Linux 的 sendfile 和 splice 都能减少这类搬运,但它们解决的不是同一个问题:前者适合“文件直接发给 socket”,后者适合“让 pipe 参与数据流转”。

要点速览:
  • 文件到 TCP socket 的单一路径优先看 sendfile,接口更短,代码和验收成本也低。
  • splice 至少要求数据流的一端是 pipe,适合把多个文件描述符串成可组合的内核路径。
  • 两者都可能短写、遇到 EAGAIN 或因文件系统和目标描述符不兼容而回退,不能只看函数名判断成功。
  • 用 strace -e trace=sendfile,splice,read,write 观察实际系统调用,再用累计字节数核对结果。

Linux read write 用户态拷贝与 sendfile 文件到 socket 内核路径对比

先把“零拷贝”放回文件发送现场

假设一个下载接口要把 /srv/files/report.tar 发送到客户端。传统写法通常是应用调用 read 把文件读进用户缓冲区,再调用 write 或 send 交给 socket。数据至少在内核缓冲区和用户缓冲区之间往返一次,应用还要自己处理缓冲区大小、剩余字节和非阻塞写。

sendfile(out_fd, in_fd, offset, count) 把输出描述符放在前面,输入描述符放在后面。直接文件下载时可以把 socket 作为 out_fd、普通文件作为 in_fd,让内核完成主要的数据搬运。Linux man-pages 也明确提醒:一次成功调用不保证发送完 count,调用方必须准备好继续发送。

sendfile 适合一条直线,splice 适合可组合的管道

可以先用数据路径而不是“哪个更快”来做选择:

场景优先方案判断依据
普通文件直接发到 TCP socketsendfile输入是文件,输出是 socket,路径单一
文件、socket 与多个处理阶段要串接splice需要 pipe 作为内核中的中转站
两端都是普通文件先评估其他复制接口不要为了“零拷贝”强行塞入 pipe
非阻塞 socket任一方案都要循环检查短写、EAGAIN 和取消逻辑

splice 的关键限制是 pipe:它在两个文件描述符之间移动数据,但至少一端必须是 pipe。这个约束看起来麻烦,却换来了组合能力。例如应用可以先把 socket 数据 splice 到 pipe,再把 pipe 内容 splice 到文件;也可以在多个 pipe 之间配合 tee 做分流。若需求只是文件下载,这种额外结构反而会增加关闭顺序和异常路径。

最小实现先处理短写和偏移量

直接文件到 socket 时,sendfile 的循环大致是下面这样。示例刻意保留返回值检查,避免把“一次调用返回正数”误认为完整发送:

off_t offset = 0;
struct stat st;
fstat(file_fd, &st);

while (offset  1024 * 1024 ? 1024 * 1024 : (size_t)left;
    ssize_t n = sendfile(socket_fd, file_fd, &offset, chunk);
    if (n > 0) continue;
    if (n == -1 && (errno == EINTR)) continue;
    if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) break;
    perror("sendfile");
    break;
}

这里的 offset 由系统调用更新,文件描述符自身的当前位置不一定是你想要的业务状态。非阻塞模式下遇到 EAGAIN 不是失败结束,而是等待 socket 可写后继续;事件循环必须保留当前偏移量。

splice 的 pipe 边界决定了代码形状

如果中间确实需要 pipe,先创建一对描述符,再让数据在 pipe 两端移动。下面展示的是文件到 socket 的组合路径:文件先进入 pipe,pipe 再进入 socket。

int p[2];
pipe2(p, O_NONBLOCK);
off_t offset = 0;

ssize_t moved = splice(file_fd, &offset, p[1], NULL, 64 * 1024, 0);
if (moved > 0) {
    ssize_t sent = splice(p[0], NULL, socket_fd, NULL, moved, SPLICE_F_MOVE);
    /* sent 可能小于 moved,剩余数据必须留在 pipe 中继续处理 */
}

示例中的 moved 和 sent 是两段不同的进度。第二次调用短写时,不能简单丢掉 moved - sent 字节;它们仍然在 pipe 里,下一轮应该先继续消费 pipe,再决定是否向文件端补数据。pipe 的读写端也要在生命周期结束时分别关闭,否则对端可能永远等不到 EOF。

Linux splice 通过 pipe 连接文件和 socket 并用 strace 验收短写的工程示意

用 strace 证明实际走了哪条路径

性能改造验收时,先用小文件跑通,再观察系统调用序列。命令中的过滤项只保留本次比较有关的调用:

strace -f -tt -e trace=sendfile,splice,read,write \
  ./download-server 2>trace.log

grep -E 'sendfile|splice|read\(|write\(' trace.log

如果实现确实使用 sendfile,日志里应该能看到它的调用和返回字节数;如果是 pipe 方案,则应看到成对的 splice。不要只因为出现了 sendfile 就认为没有用户态读写:错误回退、协议头发送、日志输出仍可能出现 read 或 write。验收要同时对照响应文件大小、累计发送量和系统调用返回值。

哪些情况下不要急着改成零拷贝

  • 文件需要在发送前解压、加密、转码或修改内容时,应用本来就要触碰字节,直接 sendfile 的收益会收窄。
  • 目标描述符不是 socket,或者文件系统对当前路径支持不好时,要准备 EINVAL、ENOSYS 等回退路径。
  • TLS 终止在应用进程内时,明文文件通常还要进入加密库,系统调用减少不等于网络链路完全绕过用户态。
  • 非阻塞事件循环没有保存偏移量、pipe 剩余量和关闭状态时,改造后更容易出现截断文件或连接悬挂。

最稳妥的做法是把普通 read/write 作为可验证的后备实现,先记录基线吞吐、CPU 和 p95 延迟,再逐个替换路径。没有基线,就很难判断优化是否只是改变了调用形态。

常见问题

sendfile 和 splice 哪个一定更快?

没有“一定”。数据路径、文件系统、socket 阻塞方式、TLS 和内核版本都会影响结果。先按描述符关系选,再用同一文件、同一并发和同一网络条件做压测。

splice 为什么总要创建 pipe?

Linux 的 splice 接口要求两个描述符中至少有一个是 pipe。pipe 是它把不同数据源组合成内核数据流的边界,也是代码需要额外维护剩余量和关闭顺序的原因。

sendfile 返回小于文件大小就是失败吗?

不是。成功调用可以只发送一部分,非阻塞 socket 还可能因暂时不可写而停下。应该根据返回值推进偏移量,等待可写事件后继续。

怎么确认零拷贝改造没有偷偷回退?

用 strace 看实际系统调用,再核对累计字节、文件大小和错误分支。若出现 EINVAL 或 ENOSYS,记录回退原因,不要把普通 read/write 的结果伪装成 sendfile 成功。

把选择写成一条可验收的规则

文件直接发 socket,就先用 sendfile;需要 pipe 连接多个描述符或做内核内数据编排,再考虑 splice。两种接口都必须循环处理短写,并为非阻塞、信号中断和不兼容描述符准备清晰的回退。最后用 strace 观察路径,用响应大小和累计字节验收结果,优化才算真正落地。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python TypeIs 和 TypeGuard 怎么选:类型收窄、运行时判断与静态检查边界Python TypeIs 和 TypeGuard 怎么选:类型收窄、运行时判断与静态检查边界
上一篇
Python TypeIs 和 TypeGuard 怎么选:类型收窄、运行时判断与静态检查边界
JavaScript AbortController.abort(reason) 实战:取消请求后区分用户操作与超时
下一篇
JavaScript AbortController.abort(reason) 实战:取消请求后区分用户操作与超时
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    402次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    478次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    487次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    433次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    260次使用