sendfile 比 read+write 快,但仅限源为普通文件、目标为支持零拷贝的 socket 时成立;关键在于绕过页缓存到 socket 缓冲区的中间搬运,省去两次用户态拷贝。

sendfile 真的比 read + write 快吗?
快,但只在特定路径下成立:源 fd 是普通文件(open() 打开的),目标 fd 是 socket 且支持零拷贝(如 AF_INET TCP 套接字)。如果源是管道、socket 或 /dev/zero,sendfile() 会直接失败并返回 EINVAL;如果目标是普通文件,它不工作——Linux 内核明确禁止这种用法。
真正提速的关键不是“少一次用户态拷贝”,而是绕过内核页缓存到 socket 缓冲区的中间搬运。数据从磁盘页缓存直送协议栈发送队列,省掉一次 copy_to_user() + copy_from_user() 的 CPU 和 cache 带宽消耗。
实操建议:
- 务必检查
sendfile()返回值:-1 表示失败,需查errno(常见EINVAL、EBADF) - 不要对
stdin、stdout或其他非文件/非 socket fd 调用sendfile() - 传输前用
lseek()定位起始偏移,sendfile()第三个参数传NULL表示从当前 offset 开始,传指针则按引用更新 offset
怎么写一个健壮的 sendfile 循环?
sendfile() 不保证一次性发完全部字节,尤其在网络拥塞或 socket 发送缓冲区满时,它可能只写入部分数据(返回值小于请求长度),甚至返回 0(对端关闭连接)或 -1(错误)。必须循环处理,不能当成 write() 那样一调了事。
实操建议:
- 用
off_t *offset参数控制进度,每次传入上次返回后更新的 offset 指针 - 捕获
EAGAIN/EWOULDBLOCK:说明 socket 暂不可写,需等待epoll或select()通知可写后再试 - 忽略
EINTR(被信号中断),直接重试 - 示例片段:
ssize_t sent = 0; while (sent
为什么 sendfile 在某些 NFS 或加密文件系统上变慢甚至失败?
因为 sendfile() 依赖底层文件系统的 ->splice_read 接口。NFSv3 默认不实现该接口,NFSv4.2 才开始支持;Btrfs 加密子卷、F2FS 的某些配置、overlayfs 底层为加密 fs 时,也常缺失 splice 支持。此时内核会 fallback 到传统 read()/write() 路径,性能不升反降(多一次内核态判断开销),甚至直接返回 EINVAL。
实操建议:
- 用
strace -e trace=sendfile观察是否真走零拷贝路径(成功时无read/write系统调用跟随) - 检查文件系统类型和挂载选项:
findmnt -t nfs,btrfs,f2fs,确认内核版本是否 ≥ 4.12(NFSv4.2 splice 支持较稳) - 生产环境若无法控制存储后端,应准备 fallback:先试
sendfile(),失败或返回 0 后自动切到read()/write()分块逻辑
sendfile 和 splice、copy_file_range 怎么选?
三者都是零拷贝传输原语,但适用场景不同:sendfile() 专用于「文件 → socket」;splice() 更通用(fd 之间任意拼接,但要求至少一端是 pipe);copy_file_range() 是「文件 → 文件」专用,且从 4.5+ 才稳定支持跨文件系统。
实操建议:
- 纯 HTTP 文件服务?坚持用
sendfile(),最成熟、兼容性最好(2.6.33+ 全支持) - 需要把文件内容注入 pipe 再由另一个进程处理?用
splice(),但得自己创建 pipe - 做后台文件复制(比如备份)?优先试
copy_file_range(),fallback 到sendfile()+socketpair()(hack 方式)或传统读写 - 注意:
splice()对 socket 的支持有限制:仅 Linux 3.9+ 支持 socket 作为 target,且必须是 TCP;UDP 不行
真正卡住性能的往往不是 sendfile() 本身,而是 socket 缓冲区大小、TCP_NODELAY 设置、磁盘 I/O 调度策略,以及你有没有在循环里反复 stat() 文件大小——这些细节比选哪个系统调用更容易拖垮吞吐量。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











