Linux sendfile 和 splice 怎么选:文件到网络的零拷贝边界与 strace 验收
静态文件接口突然变慢时,很多排查会先把线程数和缓存调大,却没看数据到底经过了几次用户态拷贝。Linux 的 sendfile 和 splice 都能减少这类搬运,但它们解决的不是同一个问题:前者适合“文件直接发给 socket”,后者适合“让 pipe 参与数据流转”。
- 文件到 TCP socket 的单一路径优先看
sendfile,接口更短,代码和验收成本也低。 splice至少要求数据流的一端是 pipe,适合把多个文件描述符串成可组合的内核路径。- 两者都可能短写、遇到
EAGAIN或因文件系统和目标描述符不兼容而回退,不能只看函数名判断成功。 - 用
strace -e trace=sendfile,splice,read,write观察实际系统调用,再用累计字节数核对结果。

先把“零拷贝”放回文件发送现场
假设一个下载接口要把 /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 socket | sendfile | 输入是文件,输出是 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。

用 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 观察路径,用响应大小和累计字节验收结果,优化才算真正落地。
Python TypeIs 和 TypeGuard 怎么选:类型收窄、运行时判断与静态检查边界
- 上一篇
- Python TypeIs 和 TypeGuard 怎么选:类型收窄、运行时判断与静态检查边界
- 下一篇
- JavaScript AbortController.abort(reason) 实战:取消请求后区分用户操作与超时
-
- 文章 · linux | 13小时前 | Linux · 运维排查 · 文件归档 · Linux tar 解包 keep-old-files skip-old-files
- Linux tar 解包如何避免覆盖同名文件:--keep-old-files、--skip-old-files 与回滚检查
- 252浏览 收藏
-
- 文章 · linux | 21小时前 | linux运维 · 日志分析 · 故障排查 · 服务管理器 · 启动限流 · Linux 自动重启 journalctl StartLimit
- Linux 服务反复重启怎么定位:自动重启、启动限流与 journal 时间窗
- 428浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 5276次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 4790次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 4737次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 4998次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 4943次使用
-
- 深入了解Golang网络编程Net包的使用
- 2023-01-23 215浏览
-
- 详解golang执行Linuxshell命令完整场景下的使用方法
- 2023-01-07 426浏览
-
- go程序部署到linux上运行的实现方法
- 2023-01-02 387浏览
-
- Linux系统下Go语言开发环境搭建
- 2023-01-07 242浏览
-
- Golang标准库syscall详解(什么是系统调用)
- 2023-01-07 436浏览

