Netty -04- NIO的拷贝和零拷贝

零拷贝的基本介绍

  • 零拷贝是网络编程的关键,很多性能优化都离不开。

  • Java中的零拷贝说的是只是用户态的零拷贝,不是操作系统层面的零拷贝 (CPU拷贝)

  • 在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。那么,他们在 OS 里,到底是怎么样的一个

    的设计?我们分析 mmap 和 sendFile 这两个零拷贝

  • 另外我们看下 NIO 中如何使用零拷贝


传统 IO 数据读写

 Java 传统 IO 和 网络编程的一段代码

Netty -04- NIO的拷贝和零拷贝_第1张图片

 传统 IO 模型

Netty -04- NIO的拷贝和零拷贝_第2张图片

这是一个从磁盘文件中读取并且通过Socket写出的过程,对应的系统调用如下。

  • 程序使用read()系统调用,系统由用户态转换为内核态,磁盘中的数据由DMA(Direct memory access)的方式读取到内核读缓冲区(kernel buffer)。DMA过程中CPU不需要参与数据的读写,而是DMA处理器直接将硬盘数据通过总线传输到内存中。
  • 系统由内核态转为用户态,当程序要读的数据已经完全存入内核读缓冲区以后,**程序会将数据由内核读缓冲区,写入到用户缓冲区,**这个过程需要CPU参与数据的读写。
  • 程序使用write()系统调用,系统由用户态切换到内核态,数据从用户缓冲区写入到网络缓冲区(Socket Buffer),这个过程需要CPU参与数据的读写。
  • 系统由内核态切换到用户态,网络缓冲区的数据通过DMA的方式传输到协议栈(存储缓冲区)中(protocol engine)

可以看到,**普通的拷贝过程经历了四次内核态和用户态的切换(上下文切换),两次CPU从内存中进行数据的读写过程,**这种拷贝过程相对来说比较消耗系统资源。


mmap

 mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网 络传输时,就可以减少内核空间到用户空间的拷贝次数。

Netty -04- NIO的拷贝和零拷贝_第3张图片

  1. mmap()系统调用首先会使用DMA的方式将磁盘数据读取到内核缓冲区,然后通过内存映射的方式,使用户缓冲区和内核读缓冲区的内存地址为同一内存地址,也就是说不需要CPU再讲数据从内核读缓冲区复制到用户缓冲区。
  2. 当使用write()系统调用的时候,cpu将内核缓冲区(等同于用户缓冲区)的数据直接写入到网络发送缓冲区(socket buffer),然后通过DMA的方式将数据传入到协议栈中准备发送。

**可以看到这种内存映射的方式减少了CPU的读写次数,但是用户态到内核态的切换(上下文切换)依旧有四次,**同时需要注意在进行这种内存映射的时候,有可能会出现并发线程操作同一块内存区域而导致的严重的数据不一致问题,所以需要进行合理的并发编程来解决这些问题。


内核空间内部传输I/O

 Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换

 通过sendfile()系统调用,可以做到内核空间内部直接进行I/O传输。

Netty -04- NIO的拷贝和零拷贝_第4张图片

  1. sendfile()系统调用也会引起用户态到内核态的切换,与内存映射方式不同的是,用户空间此时是无法看到或修改数据内容,也就是说这是一次完全意义上的数据传输过程。
  2. 从磁盘读取到内存是DMA的方式,从内核读缓冲区读取到网络发送缓冲区,依旧需要CPU参与拷贝,而从网络发送缓冲区到协议栈中依旧是DMA方式。

依旧有一次CPU进行数据拷贝,两次用户态和内核态的切换操作,相比较于内存映射的方式有了很大的进步,但问题是程序不能对数据进行修改,而只是单纯地进行了一次数据的传输过程。


理想状态下的零拷贝I/O

Netty -04- NIO的拷贝和零拷贝_第5张图片

 依旧是系统调用sendfile()

sendfile(socket, file, len);

 可以看到,这是真正意义上的零拷贝,因为其间CPU已经不参与数据的拷贝过程,也就是说完全通过其他硬件和中断的方式来实现数据的读写过程吗,但是这样的过程需要硬件的支持才能实现。

借助于硬件上的帮助,我们是可以办到的。之前我们是把页缓存的数据拷贝到socket缓存中,实际上,我们仅仅需要把缓冲区描述符传到socket缓冲区,再把数据长度传过去,这样DMA控制器直接将页缓存中的数据打包发送到网络中就可以了。

系统调用sendfile()发起后,磁盘数据通过DMA方式读取到内核缓冲区,内核缓冲区中的数据通过DMA聚合网络缓冲区,然后一齐发送到协议栈中。

可以看到在这种模式下,是没有一次CPU进行数据拷贝的,所以就做到了真正意义上的零拷贝,虽然和前一种是同一个系统调用,但是这种模式实现起来需要硬件的支持,但对于基于操作系统的用户来讲,操作系统已经屏蔽了这种差异,它会根据不同的硬件平台来实现这个系统调用

需要注意的一点是,这里其实有 一次 cpu 拷贝:kernel buffer -> socket buffer,但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以忽略


零拷贝的再次理解

  • 我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有

    一份数据)

  • 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪

    共享以及无 CPU 校验和计算。


mmap 和 sendFile 的区别

mmap sendFile
适合小数据量读写 适合大文件传输
需要 4 次上下文切换,3 次数据拷贝 需要 3 次上下文切换,最少 2 次数据拷贝
不能(必须从内核拷贝到 Socket 缓冲区) 可以利用 DMA 方式,减少 CPU 拷贝

案列

客户端

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 7001));
FileChannel fileChannel = FileChannel.open(Paths.get("F:\\Animation\\2.txt"), StandardOpenOption.CREATE_NEW, StandardOpenOption.READ);


//传统方式下的读写
//        ByteBuffer buffer=ByteBuffer.allocate(1024);
//        while(fileChannel.read(buffer)!=-1){
//            buffer.flip();
//            socketChannel.write(buffer);
//            buffer.clear();
//        }

//transferTo底层使用的就是零拷贝
//在linux下,一个transferTo方法就可以全部传输
//在win下,一个transferTo只能发送2G,就需要分段传输文件,而且要指明传输的位置
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总的字节数:"+transferCount);

fileChannel.close();
socketChannel.close();

transferTo底层使用的就是零拷贝
在linux下,一个transferTo方法就可以全部传输
在win下,一个transferTo只能发送2G,就需要分段传输文件,而且要指明传输的位置。transferFrom只能发送8M

看源码:2147483647L/(1024*1024*1024)≈2G, 8388608L/(1024*1024)=8M


服务端

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(7001));
ServerSocket socket = serverSocketChannel.socket();

//创建buffer
ByteBuffer buffer=ByteBuffer.allocate(1024*4);

while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();

    int readcount=0;
    while(readcount!=-1){
        readcount = socketChannel.read(buffer);
        buffer.rewind();
    }
}
}

运行

发送的总的字节数:672

你可能感兴趣的:(网络编程,java)