说起零拷贝之前,先来了解下服务器中文件数据通过网络传输到客户端的流程。作为应用服务器,其中会有很多从磁盘中读取数据,然后应用程序对加载到内存中的数据进行处理,然后通过网卡发送给客户端,传统数据处理通过以下两个函数实现:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
在这个过程中,数据流转的大致过程如下:
- 应用程序通过read进行操作系统函数调用,此时cpu由用户态切换到内核态,此时DMA引擎直接将磁盘上数据读取到内核缓冲区
- 内核缓冲区中的数据进行拷贝,复制到用户空间缓冲区,在内存空间之间数据拷贝,是需要cpu来参与的,拷贝结束cpu状态由内核态转换到用户态
- 用户空间的数据想要发送到客户端,通过write进行操作系统函数调用,这个时候发生了cpu状态的切换,由用户态转换到内核态。然后用户缓冲区数据拷贝到内核socket发送缓冲区,这个时候的复制也需要cpu进行参与
-
最后socket缓冲区中的数据需要复制到网卡缓冲区中,由网卡发送到客户端,然后cpu状态切换到用户态
数据拷贝过程
可以见到,在这个过程中发生了2次cpu copy和2次DMA copy,以及发生了数次cpu状态切换。这个操作对于应用服务器来说很频繁,因此带来的开销也是非常大。
因此所谓的零拷贝就是,让其中的2次cpu拷贝省略掉,因为这两次cpu拷贝的数据其实已经在内存中,没有必要再让cpu参与进来进行数据的拷贝,浪费cpu。在大量文件读写的时候,这个优化带来的收益还是比较可观的。
零拷贝的实现方式有两种:
- mmap结合write
- sendfile
mmap通过虚拟内存映射,让多个虚拟地址指向同一个物理内存地址,用户空间的虚拟地址和内核空间的虚拟地址指向同一个物理内存地址,这样用户空间和内核空间共享同一个内存数据。这样DMA引擎从磁盘上加载的数据不需要在内核空间和用户空间进行复制,减少了一次cpu拷贝。
sendfile通过系统调用,并且规定了in_fd文件描述符必须是可以mmap的,sendfile只能将文件数据发送到socket中,sendfile减少了一次cpu状态的切换
sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
无论是mmap结合write方式还是sendfile方式都只是减少了一次cpu拷贝,而后DMA引擎还具有了收集功能,可以在内核缓存区发送到socket缓冲区的时候避免掉cpu复制,只是将缓冲区地址和数据长度发送给socket缓冲区,然后DMA引擎通过收集功能直接读取收集数据发送到网卡中。这里依赖DMA引擎的收集功能省略掉了最后一次cpu拷贝,到此才是真正的零拷贝。
所谓的零拷贝就是避免数据在内核空间缓存区和用户空间缓缓冲区之间的复制,避免掉2次cpu复制,释放cpu。
在RocketMq中采用的是mmap()结合write()方式来实现零拷贝。
FileChannel.map(MapMode.READ_WRITE, 0, fileSize);
在java中还可以通过FileChannel.transferTo()来实现数据从文件描述符传输到socket中,它的底层是通过sendfile系统调用来实现。
网友评论