了解零拷贝之前,你需要先了解什么是系统调用。以下以linux为例。
在linux中,系统分为内核空间和用户空间,我们所运行的JAVA等应用程序是运行在用户空间的,而用户空间是无法直接操作硬盘,网卡等硬件设备的。
因此需要通过系统调用来让内核帮我们读写硬件上的数据。
而所谓的系统调用,其实就是内核给我们提供的一些方法,例如read方法,你可以在linux中使用man 2 read
看到该方法的声明:
当我们用户空间的应用要读一个硬盘上的文件时,将发生以下步骤:
示意图如下:
以上过程中发生了:2次态切换(上下文切换),1次DMA拷贝,1次CPU拷贝
如果是一次网络请求,应用程序读取一个文件并返回给远在它方的浏览器,那么又发生了什么?
示意图如下:
以上过程中发生了:4次态切换(上下文切换),2次DMA拷贝,2次CPU拷贝
所谓零拷贝,就是为了减少上下文切换次数,减少拷贝次数,以达到提高性能的目的。
那么零拷贝是如何实现的呢?首先你要明白,零拷贝是内核实现的,而不是用户态应用程序实现的,用户态的进程只能通过系统调用来实现各种功能。
mmap也是一种系统调用,可以通过man 2 mmap
来查看说明。
mmap能够开辟一块用户空间,并且与内核缓冲区做一个映射,简单来说就是开辟了一块“用户”与“内核”共享的一块空间。
那么就减少了一次CPU从内核空间到用户空间的数据拷贝
以上过程中发生了:4次态切换(上下文切换),2次DMA拷贝,1次CPU拷贝
从上图可以看到,mmap方案并不够理想,其实还是有一次拷贝,4次上下文切换。于是有了sendfile
linux 内核2.1版本引入了sendfile,可以使用man 2 sendfile
查看说明。
sendfile的方式是直接将数据从内核空间的“读缓冲区”拷贝到“socket缓冲区”,与mmap方式相比:
看到这里,大家一定想喷人了,说好的零拷贝呢?怎么搞来搞去还有一次拷贝。不要急,linux内核版本2.4优化了sendfile,实现了真正的零拷贝。
2.4版本所做的修改,就是将原本的CPU拷贝数据操作,改为:将读缓冲区的文件描述符添加到socket缓冲区中。
那么DMA在读取socket缓冲区时,其实是通过文件描述符信息直接读取到了数据,而数据本身并没有被拷贝,被拷贝的只是个“文件描述符”而已。
示意图如下:
以上过程发生了:2次态切换(上下文切换),2次DMA拷贝,0次CPU拷贝
sendfile并不是零拷贝的唯一选择,splice是linux内核2.6.17引入,可以使用man 2 splice
查看说明
splice零拷贝的原理和sendfile差不多,它两的区别是:
以上过程发生了:2次态切换(上下文切换),2次DMA拷贝,0次CPU拷贝
零拷贝方案对比:
系统调用 | 上下文切换次数 | DMA拷贝次数 | CPU数据拷贝次数 |
---|---|---|---|
mmap & write | 4 | 2 | 1 |
sendfile 2.1 版本 | 2 | 2 | 1 |
sendfile 2.4 版本 | 2 | 2 | 0 |
splice | 2 | 2 | 0 |