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