Linux 零拷贝技术
一、传统做法,经历“四次拷贝”
数据 1.读取到内核缓冲区 2.拷贝到用户缓冲区 3.写入到内核缓冲区 4.拷贝到网卡
使用 DMA,减少2次拷贝,还剩2次拷贝
DMA 负责硬盘到内核缓冲区和内核到网卡的传输。
CPU 仍需处理内核和用户缓冲区之间的数据传输。
二、Linux 四种零拷贝技术
需硬件支持 DMA (Direct Memory Access,直接内存访问) 一种让数据在硬盘和内存之间直接传输的技术,不需要 CPU 逐字节参与
方法 | 描述 | CPU 参与度 | 适用场景 |
---|---|---|---|
sendfile | 直接发送文件数据到套接字,无需拷贝到用户空间 | 极少,数据直接传输 | 文件服务器、视频流传输等大文件场景 |
splice | 在内核空间内高效地在文件描述符之间传输数据。 | 极少,完全在内核内 | 文件、管道与 socket 之间的复杂传输场景 |
mmap + write | 映射文件到内存,用 write 发送数据,灵活处理数据 | 中等,需要映射和写入 | 数据需要处理或修改的场景,如压缩加密 |
tee | 将管道中的数据复制到另一个管道,无需消耗原始数据。 | 极少,内核中数据复制 | 日志处理、实时数据监控等多目标场景 |
2.1 sendfile 文件式零拷贝
sendfile 最早引入,专为高效传输大文件的情况。例如文件服务器、流媒体传输、备份系统等。
int input_fd = open("input.txt", O_RDONLY);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
int client_fd = accept(server_fd, NULL, NULL);
//
sendfile(client_fd, input_fd, NULL, 1024);
//
close(input_fd);
close(client_fd);
close(server_fd);
2.2 splice 管道式零拷贝
在不同类型文件描述符之间高效直接移动数据。实现复杂数据流向控制。
例如从文件到网络 socket 的传输,或在文件、管道和 socket 之间传递数据
int input_fd = open("input.txt", O_RDONLY);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
int client_fd = accept(server_fd, NULL, NULL);
//
splice(input_fd, NULL, client_fd, NULL, 1024, SPLICE_F_MORE);
//
close(input_fd);
close(client_fd);
close(server_fd);
2.3 mmap + write:映射式零拷贝
提供了更大的灵活性,允许在用户态访问数据内容。
适用于发送数据前对文件进行预处理的场景(如压缩、加密或者数据转换等)
使用 mmap 将文件数据映射到进程的虚拟地址空间,避免显式的数据拷贝。
通过 write 直接将映射的内存区域数据发送到目标文件描述符(如网络 socket)
int input_fd = open("input.txt", O_RDONLY);
struct stat file_stat;
fstat(input_fd, &file_stat);
// 映射
char *mapped = mmap(NULL, file_stat.st_size, PROT_READ, MAP_PRIVATE, input_fd, 0);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
int client_fd = accept(server_fd, NULL, NULL);
write(client_fd, mapped, file_stat.st_size);
//
munmap(mapped, file_stat.st_size);
close(input_fd);
close(client_fd);
close(server_fd);
2.4 tee: 复制式零拷贝
把一个管道中数据复制到另一管道,同时保留原管道中数据。这意味着数据可以同时被发送到多个目标,且不影响原来的数据流,
非常适合日志记录和实时数据分析等需要把同样的数据送往不同地方的场景
int pipe_fd[2];
pipe(pipe_fd);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
int client_fd = accept(server_fd, NULL, NULL);
// 使用 tee 复制数据
tee(pipe_fd[0], pipe_fd[1], 1024, 0); //复制管道中的数据
splice(pipe_fd[0], NULL, client_fd, NULL, 1024, SPLICE_F_MORE); //发送到socket
close(pipe_fd[0]);
close(pipe_fd[1]);
close(client_fd);
close(server_fd);