1. 传统数据传送
传统数据从Socket网络中传送,需要4次数据拷贝和4次上下文切换:
- 将磁盘文件,读取到操作系统内核缓冲区;
- 将内核缓冲区的数据,拷贝到用户空间的缓冲区;
- 数据从用户空间缓冲区拷贝到内核的socket网络发送缓冲区;
- 数据从内核的socket网络发送缓冲区拷贝到网卡接口(硬件)的缓冲区,由网卡进行网络传输。
2. 零拷贝实现原理
零拷贝的目的是为了减少IO流程中不必要的拷贝,以及减少用户进程地址空间和内核地址空间之间因为上下文切换而带来的开销。由于虚拟机不能直接操作内核,因此它的实现需要操作系统OS的支持,也就是需要kernel内核暴漏API。
2.1 Netty中的零拷贝
-
Direct Buffers
:Netty的接收和发送ByteBuffer采用直接缓冲区(Direct Buffer
)实现零拷贝,直接在内存区域分配空间,避免了读写数据的二次内存拷贝,这就实现了读写Socket的零拷贝。
-
CompositeByteBuf
:它可以将多个ByteBuf封装成ByteBuf,对外提供统一封装后的ByteBuf接口。CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
- 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进行数据的拷贝。