POSIX/Linux下可读与可写文件描述符高效连接方法
零拷贝数据传输方案
在UNIX/Linux系统下,确实存在无需经过用户空间的零拷贝数据传输方法,完全满足你的需求,以下是几种关键实现方式:
1. sendfile() 系统调用
这是最常用的零拷贝方案,直接在内核空间完成数据从源文件描述符到目标描述符的传输,完全跳过用户态缓冲区。
适用场景
- 源是常规文件(或支持mmap的对象),目标是socket(比如网络传输场景);Linux 2.6.33及以上版本也支持源和目标都是常规文件。
代码示例
#include <sys/sendfile.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> ssize_t copy_with_sendfile(int src_fd, int dst_fd) { struct stat stat_buf; fstat(src_fd, &stat_buf); off_t offset = 0; ssize_t total_bytes = 0; ssize_t bytes_sent; while ((bytes_sent = sendfile(dst_fd, src_fd, &offset, stat_buf.st_size - offset)) > 0) { total_bytes += bytes_sent; } return total_bytes; }
sendfile()会自动处理循环传输直到EOF,无需手动管理缓冲区。
2. splice() 系统调用
这是更通用的零拷贝方案,支持任意两个管道类描述符之间的数据传输(包括socket、管道、常规文件等,只要它们支持seek或属于管道类型)。
优势
相比sendfile(),splice()不要求源必须是常规文件,能处理更多场景(比如socket到socket、管道到文件等)。
代码示例
#include <fcntl.h> #include <unistd.h> #include <errno.h> ssize_t copy_with_splice(int src_fd, int dst_fd) { ssize_t total_bytes = 0; ssize_t bytes_moved; const size_t chunk_size = 1024 * 1024; // 可根据系统性能调整 while (1) { bytes_moved = splice(src_fd, NULL, dst_fd, NULL, chunk_size, SPLICE_F_MOVE | SPLICE_F_NONBLOCK); if (bytes_moved > 0) { total_bytes += bytes_moved; } else if (bytes_moved == 0) { break; // 遇到EOF } else { if (errno == EAGAIN || errno == EINTR) { continue; // 非阻塞重试或中断后继续 } else { return -1; // 其他错误 } } } return total_bytes; }
SPLICE_F_MOVE提示内核直接移动数据页而非拷贝,SPLICE_F_NONBLOCK避免阻塞(可根据场景选择是否启用)。
3. tee() 系统调用(多目标场景)
如果需要将一份数据同时传输到多个目标描述符,tee()可以在两个管道描述符之间复制数据,同样不经过用户空间。它通常和splice()配合使用,先通过tee()复制数据到多个管道,再用splice()将管道数据分发到目标描述符。
关于shell管道的原理
UNIX shell的管道本质上利用了内核管道机制,当执行类似cat file | grep xxx的命令时,内核会直接在两个进程的文件描述符之间传递数据,无需用户态拷贝,底层逻辑和上述系统调用同源。
总结
- 文件到socket的传输,优先用
sendfile(),实现最简单; - 通用描述符组合(如socket到socket、管道到文件),用
splice(); - 这些方法都能完全避免用户空间的缓冲区拷贝,性能远高于手动read/write循环。
内容的提问来源于stack exchange,提问作者Logan R. Kearsley
相关产品推荐
相关产品推荐

