零拷贝机制(Zero-Copy)是在操作数据时不需要将数据从一块内存区域复制到另一块内存区域的技术,这样就避免了内存的拷贝,使得可以提高CPU的。零拷贝机制是一种操作数据的优化方案,通过避免数据在内存中拷贝达到的提高CPU性能的方案。
1.操作系统的零拷贝机制
操作系统的存储空间包含硬盘和内存,而内存又分成用户空间和内核空间。以从文件服务器下载文件为例,服务器需要将硬盘中的数据通过网络通信发送给客户端,大致流程如下:
第一步:操作系统通过DMA传输将硬盘中的数据复制到内核缓冲区
第二步:操作系统执行read方法将内核缓冲区的数据复制到用户空间
第三步:操作系统执行write方法将用户空间的数据复制到内核socket缓冲区
第四步:操作系统通过DMA传输将内核socket缓冲区数据复制给网卡发送数据
流程如下图示:
整个流程中:DMA拷贝2次、CPU拷贝2次、用户空间和内核空间切换4次
整个流程从内核空间和硬件之间数据拷贝是DMA复制传输,内核空间和用户空间之间数据拷贝是通过CPU复制.另外CPU除了需要参与拷贝任务,还需要多次从内核空间和用户空间之间来回切换,无疑都额外增加了很多的CPU工作负担。
所以操作系统为了减少CPU拷贝数据带来的性能消耗,提供了几种解决方案来减少CPU拷贝次数
1.1.使用mmap函数
mmap函数的作用相当于是内存共享,将内核空间的内存区域和用户空间共享,这样就避免了将内核空间的数据拷贝到用户空间的步骤,通过mmap函数发送数据时上述的步骤如下:
第一步:操作系统通过DMA传输将硬盘中的数据复制到内核缓冲区,执行了mmap函数之后,拷贝到内核缓冲区的数据会和用户空间进行共享,所以不需要进行拷贝
第二步:CPU将内核缓冲区的数据拷贝到内核空间socket缓冲区
第三步:操作系统通过DMA传输将内核socket缓冲区数据拷贝给网卡发送数据
流程如下图示:
整个流程中:DMA拷贝2次、CPU拷贝1次、用户空间和内核空间切换4次
可以发现此种方案避免了内核空间和用户空间之间数据的拷贝工作,但是在内核空间内部还是会有一次数据拷贝过程,而且CPU还是会有从内核空间和用户空间的切换过程
1.2.使用sendfile函数
senfile函数的作用是将一个文件描述符的内容发送给另一个文件描述符。而用户空间是不需要关心文件描述符的,所以整个的拷贝过程只会在内核空间操作,相当于减少了内核空间和用户空间之间数据的拷贝过程,而且还避免了CPU在内核空间和用户空间之间的来回切换过程。整体流程如下:
第一步:通过DMA传输将硬盘中的数据复制到内核页缓冲区
第二步:通过sendfile函数将页缓冲区的数据通过CPU拷贝给socket缓冲区
第三步:网卡通过DMA传输将socket缓冲区的数据拷贝走并发送数据
流程如下图示:
整个过程中:DMA拷贝2次、CPU拷贝1次、内核空间和用户空间切换0次
可以看出通过sendfile函数时只会有一次CPU拷贝过程,而且全程都是在内核空间实现的,所以整个过程都不会使得CPU在内核空间和用户空间进行来回切换的操作,性能相比于mmap而言要更好
另外如果硬件支持的话,sendfile函数还可以直接将文件描述符和数据长度发送给socket缓冲区,然后直接通过DMA传输将页缓冲区的数据拷贝给网卡进行发送即可,这样就避免了CPU在内核空间内的拷贝过程,流程如下:
第一步:通过DMA传输将硬盘中的数据复制到内核页缓冲区
第二步:通过sendfile函数将页缓冲区数据的文件描述符和数据长度发送给socket缓冲区
第三步:网卡通过DMA传输根据文件描述符和文件长度直接从页缓冲区拷贝数据
如下图示:
整个过程中:DMA拷贝2次、CPU拷贝0次、内核空间和用户空间切换0次
所以整个过程都是没有CPU拷贝的过程的,实现了真正的CPU零拷贝机制
1.3.使用slice函数
splice函数的作用是将两个文件描述符之间建立一个管道,然后将文件描述符的引用传递过去,这样在使用到数据的时候就可以直接通过引用指针访问到具体数据。过程如下:
第一步:通过DMA传输将文件复制到内核页缓冲区
第二步:通过splice函数在页缓冲区和socket缓冲区之间建立管道,并将文件描述符的引用指针发送给socket缓冲区
第三步:网卡通过DMA传输根据文件描述符的指针直接访问数据
如下图示:
整个过程中:DMA拷贝2次、CPU拷贝0次、内核空间和用户空间切换0次
可以看出通过slice函数传输数据时同样可以实现CPU的零拷贝,且不需要CPU在内核空间和用户空间之间来回切换
总结:实际上操作系统的零拷贝机制只是针对于CPU的零拷贝,而内核空间和硬件之间还是会存在数据拷贝的过程,只不过通过DMA传输,而不需要CPU来参与数据的拷贝过程可以看出通过mmap函数可以减少一次CPU拷贝,但是还会有一个CPU拷贝。而使用sendfile和splice函数都已经实现了CPU零拷贝而实现了数据传输过程。
2.Java中的零拷贝机制
Java的应用程序经常会遇到数据传输的场景,在Java NIO包中就提供了零拷贝机制的实现,主要是通过NIO包中的FileChannel实现FileChannel提供了transferTo和transferFrom方法,都是采用了调用底层操作系统的sendfile函数来实现的CPU零拷贝机制。
kafka服务器就是采用了FileChannel的transfer方法实现了高性能的IO传输操作。
Netty中的零拷贝机制Netty作为NIO的高性能网络通信框架,同样也实现了零拷贝机制,不过和操作系统的零拷贝机制则不是一个概念。
Netty中的零拷贝机制体现在多个场景:
-
使用直接内存,在进行IO数据传输时避免了ByteBuf从堆外内存拷贝到堆内内存的步骤,而如果使用堆内内存分配ByteBuf的话,那么发送数据时需要将IO数据从堆内内存拷贝到堆外内存才能通过Socket发送
-
Netty的文件传输使用了FileChannel的transferTo方法,底层使用到sendfile函数来实现了CPU零拷贝
-
Netty中提供CompositeByteBuf类,用于将多个ByteBuf合并成逻辑上的ByteBuf,避免了将多个ByteBuf拷贝成一个ByteBuf的过程
-
ByteBuf支持slice方法可以将ByteBuf分解成多个共享内存区域的ByteBuf,避免了内存拷贝
零拷贝总结
等一下,不是说零拷贝吗?为什么还是要 2 次拷贝?
首先我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据,sendFile 2.1 版本实际上有 2 份数据,算不上零拷贝)。例如我们刚开始的例子,内核缓存区和 Socket 缓冲区的数据就是重复的。
而零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
再稍微讲讲 mmap 和 sendFile 的区别。
mmap 适合小数据量读写,sendFile 适合大文件传输。
mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。