传统IO
:::tips 传统IO: 4次上下文切换, 2次DMA拷贝,4次CPU拷贝 :::
在I0传输过程中,如何进行零拷贝优化?
- 方式一:减少用户空间和内核空间的拷贝
- 方式二:减少内核空间的内存复制
-
方式一:减少用户空间和内核空间的拷贝
mmap + write 零拷贝
使用mmap的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射。
- 省去了将数据从内核读缓冲区(read buffer) 拷贝到用户缓冲区(user buffer) 的过程。
Java中的MappedByteBuffer
简而言之,将文件的内核缓冲区,直接映射到用户空间的内存地址,这样对文件的操作不再是write/ read,而是直接对内存地址的操作。
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 拷贝方式存在用户程序不能对数据进行修改的问题。
点击查看【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拷贝操作。
方式三:减少JVM堆内存的内存复制
1. Java堆内存send操作需要3次复制,而一次网络IO复制,需要6次复制(参见上面的传统方式)。
Java的堆内存地址值,与JNI 堆外内存的地址值不一致。
由于JVM的有自己的内存模型,JVM缓冲区起始地址和长度,与JNI 堆外内存的值不一致,所以,不能直接传递给JNI函数去调用底层的c语言内存操作函数。
2.直接内存的一次网络send操作,需要2次内存复制。