sendfile是Linux内核系统调用,需在代码中显式调用以实现零拷贝;适用前提包括源文件为磁盘文件、目标为已连接socket、文件系统支持mmap、内核≥2.2;小文件分发时可减少上下文切换与CPU拷贝,配合epoll和TCP_CORK等优化效果更佳。

sendfile 本身不是一条“指令”,而是一个 Linux 内核提供的系统调用(syscall),不能通过命令行直接键入 sendfile 就开启零拷贝。它需要在应用程序代码中显式调用,由内核在运行时启用优化路径。所谓“开启内核级零拷贝”,本质是让数据流绕过用户空间,全程在内核态完成搬运——而 sendfile() 正是为此设计的原生接口。
下面从实际落地角度说明如何真正用好它来提升海量小文件分发效率:
sendfile 的适用前提必须满足
-
in_fd(源文件描述符)必须指向真实磁盘文件(如open("a.jpg", O_RDONLY)),不支持管道、socket 或/proc类伪文件; -
out_fd(目标描述符)必须是已连接的 socket(TCP/UDP 均可,但 UDP 需注意数据包大小限制); - 文件需支持
mmap语义(绝大多数 ext4/xfs 等本地文件系统都支持); - Linux 内核 ≥ 2.2(现代发行版均满足),推荐 ≥ 2.4 以启用 DMA gather copy(彻底消除 CPU 拷贝)。
小文件场景下的关键优化点
海量小文件(如图标、JS/CSS、切片资源)分发时,sendfile 的优势不是单次吞吐,而是降低单位请求的上下文切换与内存拷贝开销:
- 传统
read() + write():每次请求触发 4 次上下文切换 + 4 次数据拷贝(含 2 次 CPU 拷贝); -
sendfile():压至 2 次上下文切换 + 最多 2 次 DMA 拷贝(Linux 2.4+ 下仅 2 次拷贝,0 次 CPU 拷贝); - 对千级/万级并发小文件请求,CPU 时间节省可达 30%–50%,尤其缓解 softirq 和上下文切换瓶颈。
实际编码中需注意的细节
- 不要循环调用
sendfile处理极小文件(:系统调用本身有固定开销,对 1KB 文件反复调用反而不如合并后writev; -
配合
TCP_CORK或TCP_NODELAY控制 Nagle 算法:小文件连续发送时,启用TCP_CORK可减少小包数量,避免网络层碎片; -
务必检查返回值并处理
EAGAIN/EWOULDBLOCK:非阻塞 socket 下需轮询或结合 epoll 使用,不可假设一次调用发完全部; -
偏移量
offset参数建议显式传入指针:避免多个线程共享同一文件描述符时因lseek竞态导致错位读取; -
文件打开时加
O_SEQUENTIAL(ext4)或O_DIRECT(大文件)无益反损:小文件随机访问常见,O_DIRECT会绕过页缓存,反而破坏sendfile依赖的内核缓冲区机制。
典型高效用法示意(C 语言片段)
int fd = open("icon.png", O_RDONLY | O_CLOEXEC);
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &addr_len);
// 启用 TCP_CORK 减少小包
int cork = 1;
setsockopt(connfd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
off_t offset = 0;
ssize_t sent = sendfile(connfd, fd, &offset, 4096); // 发 4KB
if (sent > 0) {
// 成功发送,可继续 sendfile 或关闭
}
// 关闭前取消 cork
cork = 0;
setsockopt(connfd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
close(fd);
close(connfd);
真正提升海量小文件分发效率,靠的不是单点 sendfile 调用,而是把它嵌入到事件驱动架构(如 epoll + 非阻塞 socket) 中,复用连接、批量调度、合理分片,并辅以静态资源缓存(如内核 page cache 预热)。它不复杂,但容易忽略前提和边界。










