【Java】Java中的零拷贝( 三 )


Java NIO中提供了MappedByteBuffer来处理文件映射,下面是一个读取文件的例子:
public class MappedByteBufferTest {public static void main(String[] args) {try (RandomAccessFile file = new RandomAccessFile(new File("/Users/sml/test.txt"), "r")) {// 获取FileChannelFileChannel fileChannel = file.getChannel();long size = fileChannel.size();// 调用map方法进行文件映射,返回MappedByteBufferMappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);byte[] bytes = new byte[(int)size];for (int i = 0; i < size; i++) {// 读取数据bytes[i] = mappedByteBuffer.get();}} catch (Exception e) {e.printStackTrace();}}}零拷贝零拷贝一般指的是从磁盘读取文件发送到网络或者从网络接收数据写入到磁盘文件的过程中,减少数据的拷贝次数 。
网络I/O
网络I/O与网络数据发送/接收有关,与文件I/O的底层原理一致,同样以读取数据为例,文件I/O是从磁盘读取文件,网络I/O是从网卡中读取数据 。比如客户端与服务端建立了一个连接,客户端向服务端发送数据,服务端从网卡中读取客户端发送的数据到内核中的socket缓冲区,再将socket缓冲区的数据复制到用户空间的缓冲区:

【Java】Java中的零拷贝

文章插图
使用缓存I/O发送数据到网络
首先看一下使用缓存I/O从磁盘文件读取数据并发送到网络上的过程:
【Java】Java中的零拷贝

文章插图
  1. 用户发起系统调用,进入到内核态,DMA从磁盘上读取数据到内核缓冲区(DMA复制);
  2. CPU将内核缓冲区的数据拷贝到用户缓冲区(CPU复制),切换回到用户空间;
  3. 再次从用户空间切换到内核空间,CPU将用户缓冲区的数据拷贝到socket缓冲区(CPU复制);
  4. DMA将socket缓冲区的数据拷贝到网卡(DMA复制),之后从内核空间切换回用户空间;
使用缓存I/O数据经过了四次拷贝,需要多次在内核空间和用户空间来回切换,影响系统性能 。从数据拷贝的过程可以看到有些步骤其实是多余的,比如第二步,如果可以直接将内核缓存区的数据拷贝到socket缓冲区,或者直接将内核缓冲区的数据拷贝到网卡,岂不是减少了数据拷贝的次数?零拷贝就是这样一种致力于减少数据拷贝的技术 。
Linux中的零拷贝sendfileLinux在2.1版本中引入了sendfile函数,可以实现将数据从一个文件描述符传输到另外一个文件描述符:
  1. 发起sendfile系统调用,进入到内核空间;
  2. DMA从磁盘读取文件到内核缓冲区(DMA复制);
  3. 将内核缓冲区数据拷贝到socket缓冲区(CPU复制);
  4. 将socket缓冲区数据拷贝到网卡(DMA复制),之后切换回用户空间;
    【Java】Java中的零拷贝

    文章插图
sendfile减少了一次数据从内核缓冲区拷贝到用户缓冲区的过程,可以直接将内核缓冲区的数据拷贝到socket缓冲区 。
sendfile + DMA GATHERLinux在2.4版本中引入了gather技术,我们知道内核缓冲区在内存中有对应的地址,gather操作可以将内核缓冲区的内存地址、地址偏移量信息记录到socket缓冲区中,之后DMA根据地址信息从内存中读取数据到网卡中,减少了数据从内核缓冲区到socket缓冲区的拷贝过程:
【Java】Java中的零拷贝

文章插图
可以看到零拷贝并不是指的数据一次拷贝都没有发生,而是指减少CPU进行数据拷贝的次数 。
Java中的零拷贝MappedByteBuffer在内存映射中说过,可以通过文件映射的方式将磁盘的文件内容映射到虚拟地址空间,用户空间就可以通过虚拟地址直接访问物理内存中的映射的文件数据,而Java NIO中也提供了

经验总结扩展阅读