零拷贝
# DMA技术
# 引入DMA之前的IO方式
在没有DMA技术之前,I/O过程是这样的:
CPU发出对应的指令给磁盘控制器,然后返回。
磁盘控制器收到指令后,开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断。
CPU收到中断信号后,停下手头的工作,接着把磁盘控制器的数据一个字节一个字节地读进自己的寄存器中,然后再把寄存器里的数据写入到用户内存。在数据传输期间,CPU无法执行其他任务。
整个数据传输过程,都需要CPU亲自参与搬运数据的过程,而在这个过程中,CPU不能做其他事儿。
# DMA(直接内存访问)引入之后的IO方式
DMA技术,实际上就是在进行I/O设备和内存的数据传输的时候,数据搬运的工作全部交由DMA控制器,而CPU不参与任何与数据搬运相关的工作,这样CPU可以去处理别的事务。
具体过程如下:
- 用户进程调用read方法,向操作系统发出I/O请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态。
- 操作系统受到请求后,进一步将I/O请求发送给DMA控制器,然后让CPU执行其他事务。
- DMA进一步将I/O请求发送到磁盘控制器。
- 磁盘控制器受到DMA的I/O请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向DMA发起中断信号,告知自己的缓冲区已满。
- DMA收到磁盘信号后,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不需要占用CPU,CPU可以执行其他任务。
- 当DMA读取了足够多的数据,就会发送中断信号给CPU。
- CPU收到中断,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回。
# 传统的文件传输方式
这种方式共发生了四次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是read(),一次是write()。
其次,还发生了四次数据拷贝,其中两次是DMA拷贝,另外两次是CPU拷贝。
# 如何减少用户态与内核态的上下文切换次数
读取磁盘数据的时候,之所以要进行上下文切换,是因为用户空间没有权限操作磁盘或者网卡,内核的权限最高,这些操作设备的过程都需要交给操作系统内核来完成。
一次系统调用必然会发生两次上下文切换。
要想减少上下文切换的次数,就要减少系统调用的次数。
# 如何减少数据拷贝的次数
传统的文件传输方式要经过四次数据拷贝,而在这里面,从内核的读缓冲区拷贝到用户的缓冲区中,再从用户的缓冲区拷贝到socket的缓冲区里,这个过程是没有必要的。
因为在文件传输的过程中,在用户空间我们并不会对数据再加工,所以数据实际上不需要搬运到用户空间,因此用户的缓冲区是没有必要存在的。
# 零拷贝的实现
# mmap+write
mmap()系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样操作系统内核与用户空间就不要再进行任何的数据拷贝操作。
可以使用mmap()替换read()系统调用函数。
具体过程如下:
- 应用进程调用了mmap()后,DMA会把磁盘的数据拷贝到内核的缓冲区中,接着,应用进程跟操作系统内核共享这个缓冲区。
- 应用进程再调用write(),操作系统直接将内核缓冲区的数据拷贝到socket缓冲区中,这一切都在内核,需要CPU来搬运数据。
- 最后,把内核的socket缓冲区里的数据,拷贝到网卡的缓冲区中,这个过程由DMA完成。
这不是最佳的零拷贝实现,因为仍然需要通过CPU把内核缓冲区的数据拷贝到socket缓冲区中,仍然需要四次上下文切换。
# sendfile
sendfile()可以替代前面的read()和write()两个系统调用,这样可以减少一次系统调用,也就减少了两次上下文切换。
其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到socket缓冲区里,不再拷贝到用户态,这样就只有两次上下文切换和三次数据拷贝。
# SG-DMA
以上的技术还不是真正的零拷贝技术,因为还是涉及到一次CPU拷贝,如果网卡支持SG-DMA这种与普通DMA不同的技术,可以进一步减少通过CPU把内核缓冲区里的数据拷贝到socket缓冲区的过程。
具体过程如下:
- 通过DMA将磁盘上的数据拷贝到内核缓冲区里。
- 缓冲区描述符和数据长度传送到socket缓冲区,这样网卡的SG-DMA控制器就可以直接将内核缓冲区中的数据拷贝到网卡的缓冲区里,这避免了将数据从操作系统内核缓冲区拷贝到socket缓冲区的过程,也因此减少了一次数据拷贝的过程。
这才是所谓的零拷贝技术,没有在内存层面进行数据的拷贝,也就是全称不需要CPU参与数据的搬运,所有数据都是通过DMA来进行传输的。
# 大文件传输的方式
先说结论:
- 传输大文件的时候,使用异步I/O+直接I/O的方式。
- 传输小文件的时候,使用零拷贝技术。
我们首先看最初的例子,当用户调用read()读取文件时,进程会阻塞在read方法调用上,因为要等待磁盘数据的返回。
这里的PageCache就是我们之前说的内核缓冲区,它实际上是磁盘高速缓存。它的作用主要是缓存最近被访问的磁盘的数据,此外,它还有预读的功能,可以提高磁盘的读写性能。
但是,在传输大文件时,PageCache会不起作用,因为如果把这些数据加载如PageCache中,它的空间很快会被这些大文件占满,其他热点小文件将无法使用PageCache,这样的话磁盘读写的性能就下降了。
如果使用零拷贝技术传输大文件,由于没有享受到缓存带来的好处,反而浪费了一次DMA拷贝数据到PageCache的过程。
对于大文件的传输,可以使用异步I/O来实现:
对于磁盘来说,异步I/O只支持直接I/O(直接I/O没有用到PageCache)。