零拷贝(NIO和Netty)

168 阅读3分钟

所谓的【零拷贝】,并不是真正无拷贝,而是不会拷贝数据到用户态(从Java角度,就是到jvm内存中)。

需求

将一个文件通过网络写出(将一个文件发送给网络中的其他电脑)。

传统IO

  1. Java本身并不具备从硬件读写(IO)能力,因此read方法调用后,要从Java程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用DMA(Direct Memory Access)来实现文件读,其间也不会使用CPU;

DMA可以理解为硬件单元,用来解放CPU完成文件IO

  1. 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(既byte[]),这期间CPU会参与拷贝,无法利用DMA;
  2. 调用write方法,这时将数据从用户缓冲区写入socket缓冲区,CPU会参与拷贝;
  3. 接下来要向网卡写数据,这项能力Java不具备(直接操作硬件),因此又得从用户态切换到内核态,调用操作系统的写能力,使用DMA将socket缓冲区的数据写入网卡,不会使用cpu;

小结:

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

NIO优化

NIO是JDK1.4引入的。

Linux2.1

提供了sendFile方法,Java中对应着两个channel调用transferTo/tansferFrom方法拷贝数据。

  1. Java调用transferTo方法后,要从Java程序的用户态切换至内核态,使用DMA将数据读入内核缓冲区,不会使用CPU
  2. 数据从内核缓冲区传输到socket缓冲区,CPU会参与拷贝
  3. 最后使用DMA将socket缓冲区的数据写入网卡,不会使用CPU

小结:

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

Linux2.4

  1. Java调用transferTo方法后,要从Java程序的用户态切换至内核态,使用DM将数据读入内核缓冲区,不会使用CPU
  2. 只会将一些offset和length信息拷入socket缓冲区,几乎无消耗
  3. 使用DMA将内核缓冲区的数据写入网卡,不会使用CPU

小结:

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

总结

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

Netty中的零拷贝

Netty中所谓的零拷贝,实际就是ByteBuf类中的slice()方法生成一个新的ByteBuf对象,与原对象共用底层的数据,但是有独立的readIndex,writeIndex等。这样就可以进行单独的读操作。(拷贝的对象,不要进行写操作,因为共用,所以写操作会影响原始对象对应的数据)

Netty中使用直接内存,内核缓冲区和用户缓冲区可以是同一个,减少了一次数据拷贝。

AIO(Netty废弃5.x)

AIO 用来解决数据复制阶段的阻塞问题

  • 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
  • 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果

异步模型需要底层操作系统(Kernel)提供支持

  • Windows 系统通过 IOCP 实现了真正的异步 IO
  • Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势

Netty5.x废弃原因(同Linux方式实现AIO):

  1. 性能没有优势;
  2. 引入不必要的复杂性
  3. 维护成本增加