Netty零拷贝实现原理
1. 传统数据传送
传统数据从Socket网络中传送,需要4次数据拷贝和4次上下文切换:
- 将磁盘文件,读取到操作系统内核缓冲区;
- 将内核缓冲区的数据,拷贝到用户空间的缓冲区;
- 数据从用户空间缓冲区拷贝到内核的socket网络发送缓冲区;
- 数据从内核的socket网络发送缓冲区拷贝到网卡接口(硬件)的缓冲区,由网卡进行网络传输。
传统方式,读取磁盘文件并进行网络发送,经过的4次数据拷贝和4次上下文切换是非常繁琐的。实际IO读写,需要进行IO中断,需要CPU响应中断(带来上下文切换),尽管后来引入
DMA
来接管CPU的中断请求,但四次拷贝仍在存在不必要的环节。
2. 零拷贝实现原理
零拷贝的目的是为了减少IO流程中不必要的拷贝,以及减少用户进程地址空间和内核地址空间之间因为上下文切换而带来的开销。由于虚拟机不能直接操作内核,因此它的实现需要操作系统OS的支持,也就是需要kernel内核暴漏API。
2.1 Netty中的零拷贝
-
Direct Buffers
:Netty的接收和发送ByteBuffer采用直接缓冲区(Direct Buffer
)实现零拷贝,直接在内存区域分配空间,避免了读写数据的二次内存拷贝,这就实现了读写Socket的零拷贝。
如果使用传统的堆内存缓冲区(Heap Buffer)进行Socket读写,JVM会将堆内存Buffer拷贝到直接内存中,然后才写入Socket中。相比堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
-
CompositeByteBuf
:它可以将多个ByteBuf封装成ByteBuf,对外提供统一封装后的ByteBuf接口。CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作。
- Netty的文件传输类
DefaultFileRegion
通过调用FileChannel.transferTo()
方法实现零拷贝,文件缓冲区的数据会直接发送给目标Channel。底层调用Linux操作系统中的sendfile()
实现的,数据从文件由DMA引擎拷贝到内核read缓冲区,;DMA从内核read缓冲区将数据拷贝到网卡接口(硬件)的缓冲区,由网卡进行网络传输。
2.2 Java中零拷贝
-
通过Java的
FileChannel.transferTo()
方法实现零拷贝,底层是调用Linux操作系统中的sendfile()
实现的,数据从文件由DMA引擎拷贝到内核read缓冲区;DMA从内核read缓冲区将数据拷贝到网卡接口(硬件)的缓冲区,由网卡进行网络传输。 -
通过Java的
FileChannel.map()
方法实现零拷贝,底层是调用Linux操作系统中的mmap()
实现的,将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射,这种方式适合读取大文件,同时也能对文件内容进行更改,但是如果其后要通过SocketChannel
发送,还是需要CPU进行数据的拷贝。