NIO网络编程(十)—— 零拷贝技术

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

传统的IO分析

如果要将一个文件从本地磁盘通过网络传输到另一台主机上,传统的IO会通过如下的代码,这段代码的步骤就是读取文件、将文件内容存到字节数组中、通过socket发送字节数组。

File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");

byte[] buf = new byte[(int)f.length()];
file.read(buf);

Socket socket = ...;
socket.getOutputStream().write(buf);
复制代码

这段代码的实际工作流程可以通过下面这张图看到:

在这里插入图片描述

  • 第一步:Java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 Java 程序的用户态切换至内核态,去调用操作系统(Kernel)的方法的读能力,将数据先读入内核缓冲区(磁盘数据不能直接就读入用户缓冲区)。

  • 第二步:从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf)。

  • 第三步:调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区

  • 第四步:接下来要向网卡写数据,这项能力 Java 也不具备,因此又需要从用户态切换至内核态,调用操作系统的写能力,将 socket 缓冲区的数据写入网卡。

可以看到虽然代码不长,但是中间环节较多,同时可以看到 JAVA 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的,分析一下这一系列步骤:

  • 用户态与内核态的切换发生了 3 次
  • 数据拷贝了共 4 次

NIO优化

可以使用nio的buffer,需要注意的是必须使用DirectByteBuf去分配buffer,因为使用ByteBuffer.allocate()分配buffer,底层对应 HeapByteBuffer,使用的还是 Java 内存,而使用ByteBuffer.allocateDirect() 底层对应DirectByteBuffer,直接使用的就是操作系统内存,这个内存有一个特点:操作系统可以访问,Java也可以访问

通过这个改进后,工作流程变成了下图:

在这里插入图片描述

大部分步骤与上一版相同,唯有一点不同的是:刚刚说到使用 DirectByteBuffer可以将堆外内存映射到 JVM 内存中来直接访问使用,因此可以将内核缓冲区和用户缓冲区当作同一块内存,变相地减少了一次数据的拷贝

  • 用户态与内核态的切换次数没有变化,还是发生了 3 次
  • 数据拷贝减少了一次,共 3 次

零拷贝技术

可以使用零拷贝技术对这一过程进一步优化,此外需要注意的是:零拷贝指的是数据无需拷贝到 JVM 内存中,而不是不进行拷贝。

零拷贝技术1

第一种零拷贝技术底层采用了 linux 2.1后提供的sendFile方法,在Java中对应的是两个 channel 调用 transferTo/transferFrom方法拷贝数据,需要注意的是:这两个方法这fileChannel里有,在SocketChannel没有

img

  • 这个方法改进的地方是,它不需要向directBuffer中传输数据了,可以直接从内核缓冲区发送到socket缓冲区,中间不经过Java了,减少了两次用户态与内核态的切换
  • Java首先调用transferTo方法,从 Java 程序的用户态切换至内核态,将数据读入内核缓冲区
  • 之后数据从内核缓冲区传输到 socket 缓冲区
  • 最后将 socket 缓冲区的数据写入网卡

分析一下这种方法:

  • 只发生了1次用户态与内核态的切换
  • 数据拷贝了 3 次

零拷贝技术2

linux 2.4 对上述方法再次进行了优化

img

  • 可以直接将数据内容从内核缓冲区发送到网卡,只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗
  • Java 调用transferTo方法后,要从 Java 程序的用户态切换至内核态,将数据读入内核缓冲区
  • 可以将 内核缓冲区的数据直接写入网卡

零拷贝技术的特点

  • 更少的用户态与内核态的切换
  • 不利用 cpu 计算,减少 cpu 缓存伪共享
  • 需要注意的是零拷贝适合小文件传输

猜你喜欢

转载自juejin.im/post/7032185836917489700