什么是零拷贝?
利用直接内存访问(Direct Memory Access,DMA),消除数据在内存中不必要的拷贝,即零拷贝技术(Zero-Copy)。
零拷贝不是没有任何复制,而是消除了消耗CPU和内存带宽的不必要的冗余复制。
为什么要有零拷贝?
传统IO过程:
用户进程–read()—>CPU(转化为内核态)–请求读取—>磁盘–多次缓冲区拷贝后转化为用户态—>read()返回—->用户进程
CPU被大量文件数据占用,多次切换上下文,期间干不了其他事情。并且,多次缓冲区的拷贝极大限制了文件传输性能。
于是Zero-Copy出现了。
优化思路
-
减少上下文切换的次数。传统文件IO(
read()+write())有4次上下文切换(read用户->内核->用户,write用户->内核->用户)。 -
减少各种缓冲区的拷贝次数。传统读取:磁盘控制器缓冲区->内核缓冲区->用户缓冲区,每次拷贝都在消耗时间。
mmap()+write()
read()会把内核缓冲区的数据拷贝到用户缓冲区,为了减少拷贝,换成mmap()。
mmap(Memory Map)核心思想是利用虚拟内存管理机制,在进程虚拟地址空间与物理资源(比如文件)之间建立直接的映射关系。
#include <sys/mman.h>
void *mmap(void addr[.length],
size_t length,
int prot,
int flags,
int fd,
off_t offset);
使用
mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
把内核缓冲区的数据直接映射到用户缓冲区,这样就避免了一次拷贝。
然后write(),把数据直接拷贝到socket缓冲区。由于数据还在内核缓冲区,这些都在内核态发生。
不过系统调用上下文切换的次数没变。
sendfile()
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
read from in_fd,然后write to out_fd。offset是in_fd内部的偏移量,count是最大处理长度。返回实际处理长度。
- 直接减少了一次系统调用,减少上下文切换开销
- 直接把数据从内核缓冲区拷贝到socket缓冲区,不再经过用户态
这时有磁盘->内核缓冲区->socket缓冲区->网卡,共3次拷贝(CPU一次,从内核读缓冲区到socket写缓冲区)。
如果网卡还支持SD-DMA技术,数据可以绕过sockert缓冲区,从内核缓冲区直接发送到网卡。内核空间缓冲区中对应的数据描述信息(内存地址、地址偏移量)记录到socket缓冲区中,由 DMA 根据内存地址、地址偏移量将数据批量地从内核缓冲区拷贝到网卡设备中(DMA gather copy),这样就又省去了内核空间中仅剩的 1 次 CPU 拷贝操作。
splice()
sendfile的局限性是,只能从本地文件的fd发送到socket fd,且在一定程度上依赖硬件支持。
ssize_t splice(int fd_in,
off_t *_Nullable off_in,
int fd_out,
off_t *_Nullable off_out,
size_t len,
unsigned int flags);
在内核空间的读缓冲区与socket缓冲区之间建立管道,但使用时数据并不真正经过管道,只是用来管理数据流,避免了CPU拷贝操作。
// 接收文件示例
int pipefd[2];
pipe(pipefd);
ssize_t bytes_read;
size_t total_bytes = 0;
...
while (1) {
// 从socket接收
bytes_read = splice(sock_fd, nullptr, pipefd[1], nullptr, 65536, SPLICE_F_MOVE);
// 输出到本地文件
auto bytes_written = splice(pipefd[0], nullptr, local_fd, nullptr, bytes_read, SPLICE_F_MOVE);
if (...) { ... }
total_bytes += bytes_written;
}
发送文件同理。
过程与sendfile类似,但是splice比较自由,可以用在任意的fd上,前提是有管道做媒介。
Linux零拷贝对比
| 操作 | CPU拷贝 | DMA拷贝 | 系统调用 | 上下文切换 |
|---|---|---|---|---|
read()+write() |
2 | 2 | read+write |
4 |
mmap()+write() |
1 | 2 | mmap+write |
4 |
sendfile() |
1 | 2 | sendfile |
2 |
sendfile()+DMA gather copy |
0 | 2 | sendfile |
2 |
splice() |
0 | 2 | splice |
2 |