零拷贝是一个耳熟能详的词语,在linux,kafak,Rocket MQ等产品中都有使用,通常用户提升IO性能。

传统Linux中的零拷贝

        就是在数据操作时候,不需要从一个内存位置拷贝到另一个内存位置(比如从用户态拷贝到内核(Kernel)),这样减少一次内存拷贝的损耗,从而节省了CPU时钟周期和内存带宽。

        我们模拟一个场景,从文件中读取数据,然后传输到网络。那么传统的数据拷贝分为哪几部呢?

        1.从用户进程发起read()调用后,上下文从用户态切换到内核态。DMA引擎从文件中读取内容,并存储到内核态缓冲区,这是第一次拷贝。

        2.读取的数据从内核缓冲区拷贝到用户缓冲区,然后返回给用户进程。会导致上下文从内核态再次切换到用户态。

        3.用户调用send()方法期望将数据发送到网络中。此时会触发第三次线程切换,用户态再次切换到内核态。请求数据从用户缓冲区拷贝到Socket缓冲区。

        4.最终send()系统调用结束后返回用户进程,发生第四次上下文切换。第四次拷贝会异步执行,从Socket缓冲区拷贝到执行引擎中。

        回顾一下上述的数据拷贝过程,发现2,3步的拷贝是可以去除的,DMA引擎从文件读取数据后,放入内核缓冲区,然后可以直接从内核缓冲区传输到Socket缓冲区,从而减少内存拷贝的次数。

        Linux中系统调用sendfile()可以实现从一个文件描述符传输到另外一个文件描述符,从而实现零拷贝技术。在Java中也实现了零拷贝技术,就是NIO中 FileChannel中的transferTo()方法,底层依赖了操作系统的零拷贝机制。可以直接将数据中FileChannle拷贝到另外一个channel.

        比较大的变化就是,DMA引擎从文件中直接数据到内核态缓冲区,然后直接拷贝到Socket缓冲区。不再拷贝到用户态缓冲区。上述的优化距离零拷贝的要求还是有差距的。Linux2.4版本之后,开发者对Socker Buffer追加一些dispatcher信息来进一步减少内核数据的复制。DMA引擎读取文件到内核缓冲区,然后并没有再拷贝到SocketBuffer缓冲区,只是将数据的长度以及位置信息被追加到 Socket 缓冲区,然后 DMA 引擎根据这些描述信息,直接从内核缓冲区读取数据并传输到协议引擎中,从而消除最后一次 CPU 拷贝。

         从Linux系统的角度来说,零拷贝是为了避免用户态和内核态直接的CPU拷贝。2次的DMA数据是必不可少的,只是这两次的DMA拷贝依赖硬件完成,不需要CPU参与。减少不要的CPU拷贝,就可以称为为零拷贝。

零拷贝技术-LMLPHP

Netty中的零拷贝

        Netty中的零拷贝和传统Linux中的零拷贝不太一样。Netty中的零拷贝除了操作系统级别的功能封装,更多的是面向用户态的数据操作优化。主要体现在如下五个方面:

1. 对外内存,避免JVM对内存到对外内存的拷贝。

2.CompositeByteBuffer  可以组合多个Buffer对象合并成一个逻辑对象,避免传统的内存拷贝方式将多个Buffer合并成一个大的Buffer

3.通过Unpooled.wrapper将一个byte数组包装成一个ByteBuffer对象,包装过程不产生内存拷贝。

4.ByteBuffer.slice操作与Unpoller.wrapper作用相反,slice操作将一个ByteBuffer对象切分成多个ByteBuf对象,切分过程不产生内存拷贝。底层共享一个byte数组的存储空间。

5.Netty使用FileRegion实现文件传输。FileRegion底层封装FileChannle.transferTo()方法,可以将文件缓冲区的数据直接拷贝到目标Channel,避免内核缓冲区到用户缓冲区直接的数据拷贝。这属于操作系统的零拷贝。

03-28 11:41