零拷贝可以直观的理解为不需要将数据从一个存储区域拷贝到另外一个存储区域,从而提高数据的效率。这里的零是指CPU参与整个拷贝过程的次数。下面我们来聊聊传统的数据传输(write+read)和零拷贝的几种实现方式的数据传输原理。
1、write+read数据传输的原理
在我们的Java代码中传输的数据时候会用到write方法,write方法写数据发送到网卡的过程如下图所示:
图片
然后接收方使用read方法从网卡接收数据的流程如下图所示:
图片
通过write+read的方式我们就可以实现数据的传输,但是在这两个方法背后是需要做很多的工作,如下所示的原理图:
图片
当我们调用read方法的时候,首先需要从用户态切换到内核态,然后通过DMA拷贝(Direct Memory Access,即直接内存访问。DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与)将磁盘里面的文件拷贝到内核缓冲区上,数据拷贝到内存缓冲区之后又需要进行内核态转化为用户态,将内核缓冲区中的数据拷贝到用户缓冲区。这里涉及到2次的状态切换、1次DMA拷贝、1次CPU拷贝。
文件数据拷贝到用户缓存区之后,首先需要从用户态切换到内核态,然后通过调用write方法,此时CPU就会将用户缓存区的文件数据拷贝到内核的socket缓存区上,最后通过DMA拷贝将数据拷贝到网卡上,当数据拷贝到网卡成功后再从内核态切换到用户态。
传统的文件传输的整个过程中,涉及到了4次用户态与内核态的上下文切换,执行了4次数据的拷贝(2次DMA拷贝、2次CPU拷贝)。
在文件数据传输的过程中,我们的目的就是将磁盘的文件发送到网卡上
图片
但是文件数据需要经过多个过程才能到达网卡,于是就研究人员提出了通过减少用户态和内核态的转换或者减少内存拷贝的次数的方式来提高文件拷贝的效率,这就是零拷贝技术产生的背景。
2、mmap + write
使用mmap+write方式替换原来的传统IO方式,实质就是利用了虚拟内存。虚拟内存在现代操作系统使用很广泛,其特性如下所示:
(a)多个虚拟内存可以指向同一个物理地址。
(b)虚拟内存空间可以远远大于物理内存空间。
mmap正是利用第一条特性,将内核空间和用户空间的虚拟地址映射到同一个物理地址,这样在IO操作时就不需要来回复制了,如下所示的虚拟内存示意图:
图片
mmap + write实现的零拷贝的流程图如下所示:
图片
当调用mmap方法的时候,首先从用户态切换到内核态,然后将磁盘文件使用DMA拷贝到内核缓存区,由于内核缓冲区与用户缓冲区已经完成了映射(虚拟内存),所以这个时候就不需要将数据从内核缓冲区拷贝到用户缓存区,当数据拷贝到内核缓冲区之后,又要从内核态切换到用户态。
当调用write方法的时候,进程要从用户态切换到内核态,然后进程直接操作内核缓冲区里面的数据拷贝到socket缓冲区中,拷贝到socket缓冲区完成后,再通过DMA拷贝到网卡,网上数据拷贝完成之后又要从内核态切换到用户态。
整个过程还是存在了4次用户态和内核态的切换,发生了3次数据的拷贝(2次DMA拷贝、1次CPU拷贝),虽然相比于传统的文件传输过程少了一次CPU拷贝,数据的传输的效率有一定的提升。
3、sendfile
sendfile是Linux2.1版本提供的一个系统调用函数,主要是负责发送文件的。只需要调用sendfile函数就可以完成整个文件拷贝的过程,如下图所示的流程图:
图片
当调用sendfile的时候,首先需要从用户态切换到内核态,使用DMA拷贝将文件拷贝到内核缓存区,然后将内核缓存区的数据通过CPU拷贝到socket的缓存区,最后再通过DMA拷贝数据到网卡上,完成网卡数据拷贝后再从内核态切换到用户态。
sendfile实现数据传输的过程存在了2次用户态和内核态的切换,发生了3次数据的拷贝(2次DMA拷贝、1次CPU拷贝),相比于mmap + write的方式又提升了一些效率。
4、sendfile + SG-DMA
在Linux2.4版本中网卡支持SG-DMA技术,那么使用SG-DMA可以进一步的优化零拷贝的过程,如下所示的流程图:
图片
当调用sendfile的时候,首先用户态切换到内核态,然后使用DMA拷贝将文件从磁盘拷贝到内核缓存区,接下来它就会将描述符和数据长度发送到socket缓存区,这样就可以直接将数据从内核缓存区通过SG-DMA拷贝到网卡,数据拷贝到网卡上结束后再从内核态切换到用户态。
通过SG-DMA拷贝就不需要将数据拷贝到socket缓存区再通过DMA的方式拷贝到网卡了,而是直接从内核缓存区拷贝到网卡。整个过程存在了2次用户态和内核态的切换,发生了2次数据的拷贝(2次DMA拷贝、0次CPU拷贝)。
send file + SG-DMA算是真正意义上实现了零拷贝技术,它在整个过程都是通过DMA在系统内核完成的,数据拷贝不需要CPU参与。
5、splice
在Linux2.6.17内核版本中引入了splice系统调用方法,splice和sendfle方法不同点在于它是不需要硬件支持。如下所示的splice原理图:
图片
splice是在内核空间的缓存区和socket缓存区之间建立管道,从而避免了两者之间的CPU拷贝操作。splice的整个拷贝过程发生了2次用户态和内核态的切换,2次数据的拷贝(2次DMA拷贝、0次CPU拷贝)。
总结:
(1)无论是传统的IO方式还是零拷贝技术,2次DMA拷贝是必备的(DMA都是依赖硬件完成的),零拷贝只是减少CPU拷贝与上下文的切换(用户态和内核态的切换)。
(2)零拷贝的实现有mmap+write、sendfile、sendfile + SG-DMA、splice等方式。
(3)不是所有的操作系统都支持零拷贝技术,目前只有在使用NIO和 Epoll数据传输时才可使用。
(4)RocketMQ和Kafka都使用到了零拷贝的技术。其中,RocketMQ中生产者发送数据、消费者读取数据都是使用mmap+write方式;而Kafka的生产者持久化数据使用mmap+write方法,消费者读取数据使用sendfile方式。
(5)Java的NIO中MappedByteBuffer底层使用的是mmap;FileChannel的transferTo()/transferFrom(),底层使用sendfile。