原文出自知乎:Java中的零拷贝
Java中的零拷贝
先提出两个问题:
- IO过程中,哪些步骤进行了拷贝?哪些地方零拷贝?
- Java支持哪些零拷贝?
带着这俩问题,我们一起来看下面的探究。
哪里听说过零拷贝?真的0次拷贝吗?
相信大家伙在以往的学习中,或多或少在下面这些组件、框架中有听说过零拷贝 (Zero-Copy)?
Kafka Netty rocketmq nginx apache
什么是零拷贝?
零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
- 零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
- 零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销
可以看出没有说不需要拷贝,只是说减少冗余[不必要]的拷贝。
LinuxI/O机制及零拷贝介绍
IO中断与DMA
IO中断,需要CPU响应,需要CPU参与,因此效率比较低。

用户进程需要读取磁盘数据,需要CPU中断,发起IO请求,每次的IO中断,都带来CPU的上下文切换。
因此出现了——DMA。
DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU 的大量中断负载。 DMA控制器,接管了数据读写请求,减少CPU的负担。这样一来,CPU能高效工作了。 现代硬盘基本都支持DMA。

Linux IO流程
实际因此IO读取,涉及两个过程: 1、DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区; 2、用户进程,将内核缓冲区的数据copy到用户空间。 这两个过程,都是阻塞的。

传统数据传送

比如:读取文件,再用socket发送出去 传统方式实现: 先读取、再发送,实际经过1~4四次copy。
buffer = File.read
Socket.send(buffer)
1、第一次:将磁盘文件,读取到操作系统内核缓冲区; 2、第二次:将内核缓冲区的数据,copy到application应用程序的buffer; 3、第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区); 4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。

传统方式,读取磁盘文件并进行网络发送,经过的四次数据copy是非常繁琐的。实际IO读写,需要进行IO中断,需要CPU响应中断(带来上下文切换),尽管后来引入DMA来接管CPU的中断请求,但四次copy是存在“不必要的拷贝”的。
重新思考传统IO方式,会注意到实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。
显然,第二次和第三次数据copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的背景和意义。
传统数据传送所消耗的成本:4次拷贝,4次上下文切换。 4次拷贝,其中两次是DMA copy,两次是CPU copy。如下图所示 拷贝是个IO过程,需要系统调用。

注意一点的是 内核从磁盘上面读取数据 是 不消耗CPU时间的,是通过磁盘控制器完成;称之为DMA Copy。 网卡发送也用DMA。
零拷贝的出现
目的:减少IO流程中不必要的拷贝 零拷贝需要OS支持,也就是需要kernel暴露api。虚拟机不能操作内核,

Linux支持的(常见)零拷贝
一、mmap内存映射
data loaded from disk is stored in a kernel buffer by DMA copy. Then the pages of the application buffer are mapped to the kernel buffer, so that the data copy between kernel buffers and application buffers are omitted.
DMA加载磁盘数据到kernel buffer后,应用程序缓冲区(application buffers)和内核缓冲区(kernel buffer)进行映射,数据再应用缓冲区和内核缓存区的改变就能省略。

mmap内存映射将会经历:3次拷贝: 1次cpu copy,2次DMA copy; 以及4次上下文切换
二、sendfile
linux 2.1支持的sendfile
when calling the sendfile() system call, data are fetched from disk and copied into a kernel buffer by DMA copy. Then data are copied directly from the kernel buffer to the socket buffer. Once all data are copied into the socket buffer, the sendfile() system call will return to indicate the completion of data transfer from the kernel buffer to socket buffer. Then, data will be copied to the buffer on the network card and transferred to the network.
当调用sendfile()时,DMA将磁盘数据复制到kernel buffer,然后将内核中的kernel buffer直接拷贝到socket buffer; 一旦数据全都拷贝到socket buffer,sendfile()系统调用将会return、代表数据转化的完成。 socket buffer里的数据就能在网络传输了。

sendfile会经历:3次拷贝,1次CPU copy 2次DMA copy; 以及2次上下文切换
三、Sendfile With DMA Scatter/Gather Copy
Then by using the DMA scatter/gather operation, the network interface card can gather all the data from different memory locations and store the assembled packet in the network card buffer.
Scatter/Gather可以看作是sendfile的增强版,批量sendfile。

Scatter/Gather会经历2次拷贝: 0次cpu copy,2次DMA copy
IO请求批量化
DMA scatter/gather:需要DMA控制器支持的。 DMA工作流程:cpu发送IO请求给DMA,DMA然后读取数据。 IO请求:相当于可以看作包含一个物理地址。 从一系列物理地址(10)读数据:普通的DMA (10请求) dma scatter/gather:一次给10个物理地址, 一个请求就可以(批量处理)。
4、splice
Linux 2.6.17 支持splice
it does not need to copy data between kernel space and user space. When using this approach, data are copied from disk to kernel buffer first. Then the splice() system call allows data to move between different buffers in kernel space without the copy to user space. Unlike the method sendfile() with DMA scatter/gather copy, splice() does not need support from hardware.
数据从磁盘读取到OS内核缓冲区后,在内核缓冲区直接可将其转成内核空间其他数据buffer,而不需要拷贝到用户空间。 如下图所示,从磁盘读取到内核buffer后,在内核空间直接与socket buffer建立pipe管道。 和sendfile()不同的是,splice()不需要硬件支持。

注意splice和sendfile的不同,sendfile是将磁盘数据加载到kernel buffer后,需要一次CPU copy,拷贝到socket buffer。 而splice是更进一步,连这个CPU copy也不需要了,直接将两个内核空间的buffer进行set up pipe。
splice会经历 2次拷贝: 0次cpu copy 2次DMA copy; 以及2次上下文切换
Linux零拷贝机制对比
无论是传统IO方式,还是引入零拷贝之后,2次DMA copy 是都少不了的。因为两次DMA都是依赖硬件完成的。
