Netty 中零copy 的实现
首先,我想介绍一下传统上零拷贝的含义,事实上,Netty中的零拷贝又有所区别,最后再介绍Netty中零拷贝的四种实现方式。
数据传输的相关概念
计算机早期的数据传输方式:
整个流程:
- CPU主动启动I/O设备;
- CPU不断询问I/O设备是否准备好;
- 若I/O设备准备好了,CPU开始从I/O设备中读数据;
- CPU将数据传输给主存
从整个数据传输流程可以看出:CPU是一直被占用的,无法处理其他任务,性能比较差。
这种模式下,数据传输采用了一种程序中断的方式,相当于将早期CPU不断询问I/O设备的过程去掉,流程变为:
- CPU主动启动I/O设备;
- CPU启动I/O设备后,不再询问,做其他事;
- 当I/O设备准备好了,则CPU开始从I/O设备读取数据;
- CPU将数据传输给主存。
DMA(Direct Memory Access)直接内存存取
上述方式在一定程度上提高了CPU的利用率,但在程序中断时,CPU仍然被占用,为此,引入了DMA原理,主存和I/O设备之间有一条数据通路,如此一来,主存和I/O设备之间传输数据就不用中断CPU了。
传统的发送数据要经过四个阶段,两次DMA。两次CPU中断,,共有四次拷贝,四次上下文切换,占用了两次CPU:
CPU发送指令给I/O设备的DMA,由DMA将磁盘中的数据传输到内核空间的kernal biffer上;
之后触发CPU中断,CPU开始将内核空间的数据拷贝到用户空间的应用缓存上;
CPU将数据从应用缓存拷贝到内核空间的socket buffer上;
DMA将socket buffer上的数据拷贝到网卡缓存上。
从整个流程可以看出,传统传输数据的优缺点:
优点:开发成本低;
缺点:多次上下文切换,多次占用CPU资源,性能不好
零拷贝
传统意义上的零拷贝相当于去掉上述流程的第2和3个阶段,计算机在网络上发送文件时,不需要将数据从内核空间拷贝到用户空间,而是由内核空间直接传输到网络上。Java NIO中文件通道的transferTo()方法实现了OS的sendfile,利用channel完成了零拷贝。
调用sendfile(),CPU发指令让DMA 将磁盘数据拷贝到内核缓存(kernal buffer)中;
DMA拷贝完成发出中断请求,进行CPU拷贝,拷贝到socket buffer中;
sendfile()调用完成返回,DMA将socket buffer中的数据拷贝到网卡缓存中。
、
由于上述过程不需要CPU参与拷贝过程,效率是最好的,而Netty就是采用这种方式。
Netty的零拷贝
Netty的零拷贝与上述意义上的零拷贝不太一样,更多的是用户态的(Java层面),其零拷贝更多的是偏向优化数据操作的层面。
Netty的零拷贝主要体现在:
Netty提供了CompositeByteBuf(复合缓冲区),可以将多个ByteBuf合并成一个逻辑上的ByteBuf,避免各个ByteBuf间的拷贝;
通过wrap操作,可以将byte[]数组,ByteBuf,ByteBuffer等包装成一个Netty ByteBuf对象,进而避免拷贝操作;
ByteBuf支持slice(分区)操作,可以将ByteBuf分解成多个共享同一个存储区域的ByteBuf,避免内存的拷贝;
整个流程并未将数据从内核空间拷贝到用户空间,即为零拷贝。虽然内存拷贝减少到3次,但仍要中断CPU来复制数据,原因在于DMA需要知道内存地址才能发送数据。在Linux2.4内核中进行了改进,将内核缓存(kernel buffer)中对应的数据描述信息(偏移量等)记录到相应的socket buffer中,形成了以下传输过程:
Netty提供了CompositeByteBuf(复合缓冲区),可以将多个ByteBuf合并成一个逻辑上的ByteBuf,避免各个ByteBuf间的拷贝;
通过wrap操作,可以将byte[]数组,ByteBuf,ByteBuffer等包装成一个Netty ByteBuf对象,进而避免拷贝操作
ByteBuf支持slice(分区)操作,可以将ByteBuf分解成多个共享同一个存储区域的ByteBuf,避免内存的拷贝;
通过FileRegion包装的FileChannel的transferTo实现文件传输,直接将文件缓冲区的数据发送到目标channel,避免传统通过循环write方式导致的内存拷贝问题。
1、CompositeByteBuf,将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝。
使用方式:
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, ByteBuf1, ByteBuf1);
注意: addComponents第一个参数必须为true,那么writeIndex才不为0,才能从compositeByteBuf中读到数据。
2、wrapedBuffer()方法,将byte[]数组包装成ByteBuf对象。
byte[] bytes = data.getBytes();ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
Unpooled.wrappedBuffer(bytes)就是进行了byte[]数组的包装工作,过程中不存在内存拷贝。
3、ByteBuf的分割,slice()方法。将一个ByteBuf对象切分成多个ByteBuf对象。
ByteBuf directByteBuf = ByteBufAllocator.DEFAULT.directBuffer(1024);ByteBuf header = directBy
header和body两个ByteBuf对象实际上还是指向directByteBuf的存储空间。
具体的四种实现Netty的零拷贝的实现请参照:https://segmentfault.com/a/1190000007560884