传统IO

:::tips 传统IO: 4次上下文切换, 2次DMA拷贝,4次CPU拷贝 ::: image.png

:::info

在I0传输过程中,如何进行零拷贝优化?

  • 方式一:减少用户空间和内核空间的拷贝
  • 方式二:减少内核空间的内存复制
  • 方式三:减少JVM堆内存的内存复制 :::

    方式一:减少用户空间和内核空间的拷贝

    mmap + write 零拷贝

  • 使用mmap的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射

  • 省去了将数据从内核读缓冲区(read buffer) 拷贝到用户缓冲区(user buffer) 的过程。

image.png
点击查看【processon】

Java中的MappedByteBuffer

简而言之,将文件的内核缓冲区,直接映射到用户空间的内存地址,这样对文件的操作不再是write/ read,而是直接对内存地址的操作。

  1. MappedByteBuffer buffer = filechannel.map(MapMode.READ_WRITE, 0, 1000);

方式二:减少内核空间的内存复制

1. Sendfile+ DMA Gather Copy零拷贝

通过Sendfile系统调用,数据可以直接在内核空间内部进行I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝。

特点

基于Sendfile系统调用的零拷贝方式,整个拷贝过程会发生2次上下文切换,0次CPU拷贝 和 2次DMA拷贝。

缺点

Sendfi le+DMA gather copy 拷贝方式存在用户程序不能对数据进行修改的问题。
image.png
点击查看【processon】

Java对sendfile的封装

transferTo() 通过sendfile系统调用实现零拷贝。

:::tips

  • 对于小文件,mmap方式的性能比较好,大块文件使用sendfile方式效率更高
  • RocketMQ 使用的是mmap + write方式的零拷贝,RocketMQ单条消息大小为4M。
  • Kafka索引文件使用的是 mmap+write 方式,仅仅数据文件使用的是Sendfile方式。Sendfile适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。kafka最近的版本对消息的最大限制是2G :::

    2. Splice系统调用

    Splice系统调用可以在内核空间的读缓冲区(read buffer) 和网络缓冲区(socket buffer)之间建立管道(pipeline) ,从而避免了两者之间的CPU拷贝操作。
    image.png

方式三:减少JVM堆内存的内存复制

1. Java堆内存send操作需要3次复制,而一次网络IO复制,需要6次复制(参见上面的传统方式)。
image.png
Java的堆内存地址值,与JNI 堆外内存的地址值不一致。
由于JVM的有自己的内存模型,JVM缓冲区起始地址和长度,与JNI 堆外内存的值不一致,所以,不能直接传递给JNI函数去调用底层的c语言内存操作函数。
2.直接内存的一次网络send操作,需要2次内存复制。
image.png