Linux sendfile 和 splice 怎么选:文件到网络的零拷贝边界与 strace 验收
来源:17golang原创
时间:2026-08-26 03:58:59 467浏览 收藏
静态文件接口突然变慢时,很多排查会先把线程数和缓存调大,却没看数据到底经过了几次用户态拷贝。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 观察路径,用响应大小和累计字节验收结果,优化才算真正落地。
-
215 收藏
-
426 收藏
-
387 收藏
-
242 收藏
-
436 收藏
-
415 收藏
-
文章 · linux | 8小时前 | 性能优化 · Linux · 系统调用 · io_uring · 异步IO · Linux 异步IO io_uring SQPOLL IORING_SETUP_SQPOLL484 收藏
-
222 收藏
-
450 收藏
-
222 收藏
-
252 收藏
-
446 收藏
-
文章 · linux | 16小时前 | Linux · 故障排查 · auditd · 系统审计 · 日志运维 · Linux 审计日志 auditd auditctl backlog limit exceeded 队列溢出391 收藏
-
345 收藏
-
295 收藏
-
458 收藏
-
428 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习