零拷贝(Zero Copy)

      以前刚接触Dubbo框架的时候,也随带粗浅的了解了一下Netty,但是现在时间久了,有必要重新熟悉下Netty,并且记录下学习笔记。

      首先需要了解一个概念零拷贝(Zero Copy),零拷贝在Netty中的位置还是举足轻重,如下图(镇楼)

      来源Netty官网:https://netty.io/index.html

      零拷贝(Zero Copy),涉及到操作系统中的一些知识,深感自己的这方面的知识浅薄,如果有谬误,也属于正常。

      传统IO

     

  1. JVM调用read()
  2. OS会从用户态陷入内核态,向disk请求调用读取文件数据,之后DMA会把文件数据读入到内存的内核地址空间的buffer中。
  3. OS内核再把上面buffer的文件数据通过上下文切换copy到用户态的buffer,我们的程序就可以读到了数据。
  4. 我们程序读到文件内容做了某些修改后调用write()。
  5. OS再次上下文切换到用户态把文件数据copy到内核态socket描述符的buffer,最后数据刷出到socket。
  6. 刷到socket后,OS上下文切换回用户态,返回JVM的程序成功。

        传统IO:4次OS上下文切换,2次昂贵的文件数据拷贝。

 

 改进版本一: sendfile()/transferTo() 实现zero-copy

   

 

      

  1. 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard driver ——> kernel buffer)。
  2. 然后再将数据从内核空间缓冲区拷贝到内核中与socket相关的缓冲区中(第二次拷贝: kernel buffer ——> socket buffer)。
  3. sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)。

    版本一实现的I/O:只使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝(1次CPU执行的,2次DMA执行的)。 你可能会说操作系统仍然需要在内核内存空间中复制数据(kernel buffer —>socket buffer)。 是的,但从操作系统的角度来看,这已经是零拷贝,因为没有数据从内核空间复制到用户空间。 内核需要复制的原因是因为通用硬件DMA访问需要连续的内存空间(因此需要缓冲区)。

 

改进版本二: sendfile()+DMA scatter-and-gather实现zero-copy

 

        socket buffer 并非copy  kernel buffer中的数据,拷贝fd(kernel buffer内存地址和偏移量), 协议引擎(protocol engine)  从 kernel buffer和socket buffer 中gather(收集)信息,利用的就是scatter/gather的思想。

  1. 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
  2. 没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket缓冲区当中。该描述符包含了两方面的信息:kernel buffer的内存地址,kernel buffer的偏移量。
  3. sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(第二次拷贝: kernel buffer ——> protocol engine),这样就避免了最后一次CPU数据拷贝。

      版本二实现的I/O:只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。


改进版本三,通过mmap实现zero-copy

 
  
    传统I/O用户空间缓冲区中存有数据,因此应用程序能够对此数据进行修改等操作;而sendfile零拷贝消除了所有内核空间缓冲区与用户空间缓冲区之间的数据拷贝过程,因此sendfile零拷贝I/O的实现是完成在内核空间中完成的,这对于应用程序来说就无法对数据进行操作了。为了解决这个问题,Linux提供了mmap零拷贝来实现我们的需求。mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。
我们的程序发起一次系统调用,将一个文件(或者文件的一部分)映射到虚拟地址空间的一部分,注意这时候没有分配和映射到具体的物理内存空间,而是到第一次加载这个文件的时候,通过MMU把之前虚拟地址换算成物理地址,把文件加载进物理内存。

具体分析消耗步骤如下:

  1. 发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
  2. mmap系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间。因为用户空间和内核空间共享了这个缓冲区数据,所以用户空间就可以像在操作自己缓冲区中数据一般操作这个由内核空间共享的缓冲区数据。
  3. 发出write系统调用,导致用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket相关联的缓冲区(第二次拷贝: kernel buffer ——> socket buffer)。
  4. write系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)

      版本三实现的I/O: 4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。明显,它与传统I/O相比仅仅少了1次内核空间缓冲区和用户空间缓冲区之间的CPU拷贝。这样的好处是,我们可以将整个文件或者整个文件的一部分映射到内存当中,用户直接对内存中对文件进行操作,然后是由操作系统来进行相关的页面请求并将内存的修改写入到文件当中。我们的应用程序只需要处理内存的数据,这样可以实现非常迅速的I/O操作。

 

 

 

参考文献:https://blog.csdn.net/cringkong/article/details/80274148
                  https://www.jianshu.com/p/03852a291c56
posted @ 2019-06-19 11:37  Don'tYouSee  阅读(791)  评论(0)    收藏  举报