什么是零拷贝?
我们知道操作系统有用户空间和内核空间,在实现 read/write
操作时会涉及到多次系统调用,系统调用就意味着上下文切换,而上下文切换是很耗时的操作。零拷贝的目的就是减少上下文切换次数从而避免多次不必要的数据拷贝。
读写数据的底层
DMA
DMA(Direct Memory Access)直接内存访问,DMA 使得外设和内存之间进行 IO数据传输时不再经过 CPU,可以直接传输。
缓冲区IO
- 进程发起
read
请求,首先会检查内核空间缓冲区是否存在进程需要的数据,如果存在就直接复制到进程所在的内存区。如果没有,系统向磁盘请求数据并通过 DMA将数据写入到内核的read
缓冲区,最后再将内核缓冲区数据拷贝到进程的内存区。 - 进程发起
write
请求,首先会把进程内存中数据拷贝到内核的write
缓冲区,然后通过 DMA将内核缓冲区的数据刷回到磁盘中。
虚拟内存
虚拟内存实现了:(1)多个虚拟地址映射到同一个物理地址;(2)虚拟内存可用空间大于物理内存。通过将用户空间和内核空间的虚拟地址映射到同一个物理地址,就可以实现 DMA对同一块内存操作而不需要多余的拷贝。
传统IO
传统 read
- 进程发起
read
系统调用,由用户态切换为内核态,然后系统通过 DMA将数据从磁盘加载到内核空间缓冲区; - 将内核空间缓冲区数据拷贝到用户空间进程的内存中,
read
系统调用返回。系统调用返回又是一次内核空间到用户空间的切换。
传统 write
- 进程发起
write
系统调用,由用户态切换为内核态,然后系统将用户态下进程内存数据拷贝到内核空间的 socket缓冲区(同样为内核缓冲区,但是供socket使用),接着write
系统调用返回,再触发上下文切换。 - socket缓冲区数据通过 DMA发送到网卡,这个过程可以异步进程,不一定保证数据能成功刷盘。
总结
总的来说一次 read,一次 write操作,传统IO 共涉及到了 4次上下文切换,4次数据拷贝,分别是2次CPU数据拷贝,2次DMA数据拷贝。
Mmap+Write实现零拷贝
Mmap
#include<sys/mman.h>
void* mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
mmap
实现了将一个文件或其它对象映射进内存,主要用途如下:
- 将一个普通文件映射进内存,通常用在频繁文件读写上,这样用内存读写代替 I/O读写,获取更高的性能;
- 匿名映射,为关联进程(如父子进程)提供共享内存空间;在父进程中先调用
mmap
,然后调用fork
,创建子进程的同时子进程会继承父进程的地址。这样父子进程共同维护内存映射地址,实现进程间共享内存。 - 为无关联的进程提供共享内存空间,也是将普通文件映射到内存中;
更详细解释参考:https://zhuanlan.zhihu.com/p/477641987
IO过程
- 进程发起
mmap
系统调用,导致用户空间到内核空间的切换,然后通过 DMA将磁盘文件的数据复制到内核空间缓冲区; mmap
系统调用返回,再次导致上下文切换;- 不需要将数据从内核空间拷贝到用户空间,因为内存映射二者能同时操作同一块内存空间;
- 进程发起
write
系统调用,导致用户空间切换到内核空间。将数据从内核空间缓冲区复制到内核空间 socket缓冲区; write
系统调用返回,内核空间切换到用户空间;- 异步地将 socket缓冲区数据刷盘。
总结
整个过程涉及到 4次上下文切换,3次数据拷贝,分别是 2次 DMA拷贝和 1次 CPU拷贝。
sendfile 实现零拷贝
ssize_t sendfile(int out_fd,int in_fd,off_t *offset,size_t count)
- out_fd:输出文件描述符;
- in_fd:输入文件描述符,必须为真实文件,不能为管道或 socket;
- offset:从输入文件那个位置开始读,NULL 表示文件默认起始位置;
- count:out_fd 与 in_fd 之间传输的字节数,由于是在内核态完成的数据传输,避免了内核态和用户态间的切换。
- 用户进程发起 sendfile 系统调用,上下文从用户态切换到内核态;
- DMA控制器把数据从磁盘拷贝到内核缓冲区;
- CPU将数据从内核缓冲区拷贝到 socket 缓冲区;
- DMA控制器,异步地将数据从 socket 缓冲区拷贝到网卡;
- 上下文从内核态切换到用户态。
整个 sendfile
实现零拷贝过程可以发现,发生了2次用户空间和内核空间的上下文切换,以及3次数据拷贝(2次DMA数据拷贝,1次CPU拷贝)。
sendfile+DMA scatter/gather 实现零拷贝
实际上内核空间将数据从磁盘通过DMA拷贝到内核缓冲区,再将数据通过CPU拷贝到 socket 缓冲区,再将数据从 socket 缓冲区拷贝到网卡的过程稍显复杂,如果能够直接将数据从内核缓冲区拷贝到网卡,就更加快捷了。
- 用户进程发起 sendfile 系统调用,切换到内核态;
- DMA控制器将数据从磁盘拷贝到内核缓冲区;
- CPU将内核缓冲区文件描述符信息发送到 socket缓冲区;
- DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡;
sendfile+DMA+scatter+gather
的方式发生了2次用户空间和内核空间切换,以及2次DMA数据拷贝(唯一的一次通过CPU从内核缓冲区拷贝数据到socket缓冲区被优化了)。