零拷贝

Posted by DeepBlue on 12-05,2020

零拷贝

什么是零拷贝?

零拷贝技术,就是避免将数据从一块存储拷贝到另外一块存储的技术,从而节省拷贝带来的CPU开销,零拷贝并不是将拷贝操作完全消除掉。

  • 传统拷贝:这个没什么说的,就是平时我们一般都会用到的拷贝方式;
  • MMAP方式的拷贝:当应用程序不需要对数据进行访问时,则可以避免将数据从内核空间拷贝到用户空间;
  • SendFile方式的拷贝:写时拷贝技术,数据不需要提前拷贝,而是当需要修改的时候再进行部分拷贝。

传统拷贝

传统的Linux系统中,标准的I/O接口(例如read,write)都是基于数据拷贝操作的,即是I/O操作会导致数据在内核地址空间的缓冲区和用户地址空间的缓冲区之间进行拷贝,所以标准I/O也被称作缓存I/O。这样做的好处是,如果所请求的数据已经存放在内核的高速缓冲存储器中,那么就可以减少实际的I/O操作,但坏处就是数据拷贝的过程,会导致CPU开销。

DMA(Direct Memory Access):直接存储器访问。DMA是一种无需CPU的参与,让外设和系统内存之间进行双向数据传输的硬件机制。当DMA完成数据的传送之后,使用系统中断提醒CPU,然后CPU就可以高效的处理数据。使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率。

1

我们可以看到需要经历四个阶段,2次DMA,2次CPU中断,总共四次拷贝,有四次上下文切换,并且会占用两次CPU。

  1. CPU发指令给I/O设备的DMA,由DMA将我们磁盘中的数据传输到内核空间的内核buffer。
  2. 第二阶段触发我们的CPU中断,CPU开始将将数据从kernel buffer拷贝至我们的应用缓存
  3. CPU将数据从应用缓存拷贝到内核中的socket buffer.
  4. DMA将数据从socket buffer中的数据拷贝到网卡缓存。

优点:开发成本低,适合一些对性能要求不高的,比如一些什么管理系统这种我觉得就应该够了

缺点:多次上下文切换,占用多次CPU,性能比较低。

MMAP方式的拷贝

我们提到用户态的进程是不能随意操作内核地址空间的,而且mmap也没有提供用户进程直接操作内核地址空间的能力,而是通过内存映射的机制,把内核中的部分内存空间映射到用户空间的内存,用户空间和内核空间共享一块相同的物理内存,从而提供用户进程对内存直接访问的能力。

有了mmap的支持,数据从文件中读取到内核空间之后,就不会再拷贝到用户空间,当调用socket的write时,数据会直接从内核缓存中直接拷贝到Socket的缓冲区中,避免了在用户空间中多中转一次。

3

我们可以看到,MMP与传统的拷贝方式相比,mmp减少了数据在用户空间进行一次拷贝mmap虽然能减少一次数据拷贝,但是还是需要4次上下文切换,拷贝的话需要执行三次,即两次DMA拷贝,一次CPU的拷贝:

  • 用户进程通过 mmap() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。

  • 将用户进程的内核空间的读缓冲区(read buffer)与用户空间的缓存区(user buffer)进行内存地址映射。

  • CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。

  • 上下文从内核态(kernel space)切换回用户态(user space),mmap 系统调用执行返回。

  • 用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。

  • CPU将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。

  • CPU利用DMA控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。

  • 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回。

SendFile方式的拷贝

sendfile内核调用是在Linux 2.1版本开始引入的,主要功能是在内核态中,在两个文件描述符之间传递数据,避免了用户空间和内核空间之间的数据拷贝操作。

使用sendfile时,数据中转与mmap类似,不经过用户空间,但是由于sendfile全程在内核态执行,因此只需要2次上下文切换:

  • 调用sendfile将文件内容通过socket发送出去时候,从用户态切换到内核态;
  • 任务完成之后,切换回来。

在Linux 2.4版本中,对sendfile进一步做了优化,之前从“文件数据缓存”到“socket缓存”时候,也需要一次拷贝,优化之后,“socket缓存”中只存储要发送的数据在“文件数据缓存”中的位置和偏移量,在实际发送时,根据位置和偏移量直接将“文件数据缓存”中的数据拷贝到网卡设备中,又省掉了一次拷贝操作。

基于 sendfile和DMA系统调用的零拷贝方式,整个拷贝过程会发生2 次上下文切换、0 次 CPU 拷贝以及 2 次 DMA 拷贝用户程序读写数据的流程如下:

  1. 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  3. CPU 把读缓冲区(read buffer)的文件描述符(file descriptor)和数据长度拷贝到网络缓冲区(socket buffer)。
  4. 基于已拷贝的文件描述符(file descriptor)和数据长度,CPU 利用 DMA 控制器的 gather/scatter 操作直接批量地将数据从内核的读缓冲区(read buffer)拷贝到网卡进行数据传输。
  5. 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。

但是使用这种方式也是有缺点的,就是我们再拷贝的过程中不能对数据进行任何的修改,如果我们对数据进行修改后再进行拷贝的话这种方式就行不通了。如果我们要对文件进行修改的话就必须使用传统的拷贝方式了。

因为基于SendFile的这种特性,零拷贝技术被用在Nginx等软件上,提升了很大的效率,比如我们使用nginx的时候对静态资源的读写就会用到零拷贝,大大增加了我们服务器的性能。