7. NIO zero copy

One, zero-copy

1 Introduction

  Zero copy describes CPU does not perform copying data from one task to another storage area of the storage area, which is used typically to transfer a file over a network to reduce the CPU cycles and memory bandwidth .

advantage:

  • Reduce or even totally avoid unnecessary CPU copy , so that freed CPU to perform other tasks
  • Reducing memory bandwidth occupancy
  • Typically zero-copy technique is also possible to reduce the space between the user and the operating system kernel space context switch

2. Traditional copy mechanism

Java IO and traditional network programming a piece of code:

1 File file = new File("test.txt");
2 RandomAccessFile raf = new RandomAccessFile(file, "rw");
3 
4 byte[] arr = new byte[(int) file.length()];
5 raf.read(arr);
6 
7 Socket socket = new ServerSocket(8080).accept();
8 socket.getOutputStream().write(arr);

 

  A document transmitted through the network to another program, in the interior of the OS, the copy operation to be subjected to the context switching between the user mode and the four kernel mode, even four times the data is copied, the copy four, two CPU involvement copy times required between the user mode and kernel mode, the kernel mode between the two IO devices and DMA mode to copy without CPU participation . Zero copy between users to avoid the copy mode and kernel mode, switching between the two reduced user mode kernel mode. As shown below:

  ① read () call causes a context from user mode to kernel mode switching. Internally called sys_read () to read data from the file. The first copy is completed by a DMA (direct memory access), the contents of the file from the disk is read out, stored in the kernel buffer. ( Hardware -> Kernel Buffer )

  ② data is then requested copy to the user buffer, then read () returns successfully. Call return trigger a second context switch: from kernel to user. So far, buffer the data stored in the user's. ( Kernel Buffer -> the User Buffer )

  ③ send () Socket call brought a third context switch, this time from user mode to kernel mode. At the same time, it has also undergone a third copy: the data into the kernel adress space in. Of course, the first step of the kernel buffer and the buffer is a different buffer. ( The User Buffer -> Kernel Buffer )

  ④ eventually send () system call returns, but also resulted in fourth context switch. While fourth copy occurs, DMA egine data copied from the kernel buffer to the protocol engine. The fourth copy is independent and asynchronous. ( Kernel Buffer -> Hardware )

 

 

3. sendFile achieved by zero-copy I / O

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

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

 

 

 4. 带有DMA收集拷贝功能的sendfile实现的I/O

 

 

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

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

5. 通过mmap实现的零拷贝I/O

  传统I/O用户空间缓冲区中存有数据,因此应用程序能够对此数据进行修改等操作;而sendfile零拷贝消除了所有内核空间缓冲区与用户空间缓冲区之间的数据拷贝过程,因此sendfile零拷贝I/O的实现是完成在内核空间中完成的,这对于应用程序来说就无法对数据进行操作了。为了解决这个问题,Linux提供了mmap零拷贝来实现我们的需求。

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

  ③ 发出write系统调用,导致用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket相关联的缓冲区(第二次拷贝: kernel buffer —> socket buffer)。
  ④ write系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer —> protocol engine)

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

3. NIO零拷贝机制

   NIO引入了用于通道的缓冲区的ByteBuffer。 ByteBuffer有三个主要的实现:直接在内核中操作文件。

① HeapByteBuffer

  在调用ByteBuffer.allocate()时使用。 它被称为堆,因为它保存在JVM的堆空间中,因此您可以获得所有优势,如GC支持和缓存优化。 但是,它不是页面对齐的,这意味着如果您需要通过JNI与本地代码交谈,JVM将不得不复制到对齐的缓冲区空间。

② DirectByteBuffer

  在调用ByteBuffer.allocateDirect()时使用。 JVM将使用malloc()在堆空间之外分配内存空间。 因为它不是由JVM管理的,所以你的内存空间是页面对齐的,不受GC影响,这使得它成为处理本地代码的完美选择。 然而,你要C程序员一样,自己管理这个内存,必须自己分配和释放内存来防止内存泄漏。

③ MappedByteBuffer

  在调用FileChannel.map()时使用。 与DirectByteBuffer类似,这也是JVM堆外部的情况。 它基本上作为OS mmap()系统调用的包装函数,以便代码直接操作映射的物理内存数据。

 1 import java.io.RandomAccessFile;
 2 import java.nio.MappedByteBuffer;
 3 import java.nio.channels.FileChannel;
 4 
 5 /*
 6 说明
 7 1. MappedByteBuffer 可让文件直接在内存(堆外内存)修改, 操作系统不需要拷贝一次
 8  */
 9 public class MappedByteBufferTest {
10     public static void main(String[] args) throws Exception {
11         RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
12         //获取对应的通道
13         FileChannel channel = randomAccessFile.getChannel();
14 
15         /**
16          * 参数1: FileChannel.MapMode.READ_WRITE 使用的读写模式
17          * 参数2: 0 : 可以直接修改的起始位置
18          * 参数3:  5: 是映射到内存的大小(不是索引位置) ,即将 1.txt 的多少个字节映射到内存
19          * 可以直接修改的范围就是 0-5
20          * 实际类型 DirectByteBuffer
21          */
22         MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
23         mappedByteBuffer.put(0, (byte) 'H');
24         mappedByteBuffer.put(3, (byte) '9');
25        // mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException
26         randomAccessFile.close();
27         System.out.println("修改成功~~");
28     }
29 }

 

④ NIO中的FileChannel拥有transferTo和transferFrom两个方法,可直接把FileChannel中的数据拷贝到另外一个Channel,或直接把另外一个Channel中的数据拷贝到FileChannel。该接口常被用于高效的网络/文件的数据传输和大文件拷贝。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于Java IO中提供的方法。

 1 import java.io.FileInputStream;
 2 import java.io.FileOutputStream;
 3 import java.nio.channels.FileChannel;
 4 
 5 public class NIOFileChannel04 {
 6     public static void main(String[] args)  throws Exception {
 7         //创建相关流
 8         FileInputStream fileInputStream = new FileInputStream("C:\\Users\\QMillet\\Pictures\\Saved Pictures\\wallhaven-w8lo2p.png");
 9         FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg");
10         //获取各个流对应的filechannel
11         FileChannel sourceCh = fileInputStream.getChannel();
12         FileChannel destCh = fileOutputStream.getChannel();
13         //使用transferForm完成拷贝
14         destCh.transferFrom(sourceCh,0,sourceCh.size());
15         //关闭相关通道和流
16         sourceCh.close();
17         destCh.close();
18         fileInputStream.close();
19         fileOutputStream.close();
20     }
21 }

Guess you like

Origin www.cnblogs.com/qmillet/p/12147121.html