*零拷贝
1 用户态和内核态
- 用户态:处于用户态执行时,进程所能访问的内存空间和对象收到限制,其所处于占有的处理器是可被抢占的。
- 内核态:处于内核态执行时,能访问所有内存空间和对象,且所占有的处理器是不允许被抢占的。
2 什么是零拷贝:
零拷贝通过尽量避免拷贝操作来缓解 CPU 的压力。。Linux 下常见的零拷贝技术可以分为两大类:一是针对特定场景,去掉不必要的拷贝;二是去优化整个拷贝的过程。由此看来,零拷贝并没有真正做到“0”拷贝,它更多是一种思想。
拷贝流程(图 借用网上的)
读:进程发起read请求之后,内核接收到read请求之后,会先检查内核空间中是否已经存在进程所需要的数据,如果已经存在,则直接把数据copy给进程的缓冲区;如果没有内核随即向磁盘控制器发出命令,要求从磁盘读取数据,磁盘控制器把数据直接写入内核read缓冲区,这一步通过DMA完成;接下来就是内核将数据copy到进程的缓冲区;
写:如果进程发起write请求,同样需要把用户缓冲区里面的数据copy到内核的socket缓冲区里面,然后再通过DMA把数据copy到网卡中,发送出去;
这个过程经历了四次拷贝,但是真正消耗资源和浪费时间的是用户态和内核态之间的拷贝。
3 优化方案 transferTo
数据直接在内核态 有read buffer 拷贝到 socket Buffer。还是存在一次内核态中由Read Buffer到 socket buffer的拷贝。
4 零拷贝优化方案
只包含关于数据的位置和长度的信息的描述符被追加到了socket buffer 缓冲区中。DMA引擎直接把数据从内核缓冲区传输到协议引擎(protocol engine),从而消除了最后一次CPU copy。经过上述过程,数据只经过了2次copy就从磁盘传送出去了。这个才是真正的Zero-Copy。
这里的零拷贝其实是根据内核状态划分的,在这里没有经过CPU的拷贝,数据在用户态的状态下,经历了零次拷贝,所以才叫做零拷贝,但不是说不拷贝。
5 哪里会用到零拷贝
5.1 Java的NIO
在 Java NIO 中的Channel就相当于操作系统的内核空间(kernel space)的缓冲区,而 Buffer 对应的相当于操作系统的用户空间中的用户缓冲区(user buffer)。
即:Channel ---- Kernel Buffer; Buffer --- User Buffer
堆外内存(DirectBuffer)在使用后需要应用程序手动回收,而堆内存(HeapBuffer)的数据在 GC 时可能会被自动回收。因此,在使用 HeapBuffer 读写数据时,为了避免缓冲区数据因为 GC 而丢失,NIO 会先把 HeapBuffer 内部的数据拷贝到一个临时的 DirectBuffer 中的本地内存(native memory),这个拷贝涉及到 sun.misc.Unsafe.copyMemory() 的调用,背后的实现原理与 memcpy() 类似。 最后,将临时生成的 DirectBuffer 内部的数据的内存地址传给 I/O 调用函数,这样就避免了再去访问 Java 对象处理 I/O 读写。
(1)MappedByteBuffer
MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷贝方式的提供的一种实现,意思是把一个文件从 position 位置开始的 size 大小的区域映射为内存映像文件。这样之添加地址映射,而不进行拷贝。
(2)DirectByteBuffer
DirectByteBuffer 的对象引用位于 Java 内存模型的堆里面,JVM 可以对 DirectByteBuffer 的对象进行内存分配和回收管理,是 MappedByteBuffer 的具体实现类。因此同样具有零拷贝技术。
(3)FileChannel
FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。
我们直接看Linux2.4的版本,socket缓冲区做了调整,DMA带收集功能。
(1)DMA从拷贝至内核缓冲区
(2)将数据的位置和长度的信息的描述符增加至内核空间(socket缓冲区)
(3)DMA将数据从内核拷贝至协议引擎
这个复制过程是零拷贝过程。
5.2 Netty
Netty 中的零拷贝和上面提到的操作系统层面上的零拷贝不太一样, 我们所说的 Netty 零拷贝完全是基于(Java 层面)用户态的。
- Netty 通过 DefaultFileRegion 类对FileChannel 的 tranferTo() 方法进行包装,相当于是间接的通过java进行零拷贝。
- 我们的数据传输一般都是通过TCP/IP协议实现的,在实际应用中,很有可能一条完整的消息被分割为多个数据包进行网络传输,而单个的数据包对你而言是没有意义的,只有当这些数据包组成一条完整的消息时你才能做出正确的处理,而Netty可以通过零拷贝的方式将这些数据包组合成一条完整的消息供你来使用。
Netty的零拷贝
- CompositeChannelBuffer:这个类的主要作用是将多个ChannelBuffer组成一个虚拟的ChannelBuffer来进行操作,避免多个ChannelBuffer之间的拷贝。CompositeChannelBuffer并没有将多个ChannelBuffer真正的组合起来,而只是保存了他们的引用,这样就避免了数据的拷贝,实现了Zero Copy。
- wrap操作:可以把bypte[]数组、ByteBuf、ByteBuffer 包装成一个 ByteBuf 对象, 进而避免了拷贝操作。
- ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf,避免了内存的拷贝。
- 通过FileRegion 包装的FileChannel.transferTo 实现文件传输,可以直接将文件缓冲区的数据发送到目标Channel,避免传统通过循环write方式导致的拷贝问题。
5.3 kafka
- 索引文件使用的是 mmap + write 方式(mmap 基于内存映射),
- 数据文件使用的是 sendfile 方式。适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。
如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。