为什么要零拷贝
零拷贝,网络上的解释是:
“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.
翻译成中文就是:零拷贝描述的是CPU不参与数据从一个内存区域拷贝到另一个区域的计算机操作过程。
在实际应用中,为了通过减少甚至完全避免不必要的CPU对数据的拷贝操作以及内核与用户态的切换消耗及其带来的内存带宽的占用,来提升计算机操作效率,就有了所谓的零拷贝技术。
现代的 CPU 和存储体系结构提供了很多特征可以有效地实现零拷贝技术,但是因为存储体系结构非常复杂,而且网络协议栈有时需要对数据进行必要的处理,所以零拷贝技术有可能会产生很多负面的影响,甚至会导致零拷贝技术自身的优点完全丧失
为什么要用零拷贝,我觉得用官网的阐述更准确:
如今,很多网络服务器都是基于客户端 - 服务器这一模型的。在这种模型中,客户端向服务器端请求数据或者服务;服务器端则需要响应客户端发出的请求,并为客户端提供它所需要的数据。随着网络服务的逐渐普及,video 这类应用程序发展迅速。当今的计算机系统已经具备足够的能力去处理 video 这类应用程序对客户端所造成的重负荷,但是对于服务器端来说,它应付由 video 这类应用程序引起的网络通信量就显得捉襟见肘了。而且,客户端的数量增长迅速,那么服务器端就更容易成为性能瓶颈。而对于负荷很重的服务器来说,操作系统通常都是引起性能瓶颈的罪魁祸首。举个例子来说,当数据“写”操作或者数据“发送”操作的系统调用发出时,操作系统通常都会将数据从应用程序地址空间的缓冲区拷贝到操作系统内核的缓冲区中去。操作系统这样做的好处是接口简单,但是却在很大程度上损失了系统性能,因为这种数据拷贝操作不单需要占用 CPU 时间片,同时也需要占用额外的内存带宽。
零拷贝的原理探析
传统IO工作过程
以上过程对应的操作系统调用如下:
read(file,tmp_buf,len)
write(socket,tmp_buf,len)
整个过程是:硬盘—>内核缓冲区—>用户缓冲区—>内核socket缓冲区—>协议引擎
- 程序调用操作系统的read函数,系统由用户态转为内核态,发生第一次上下文切换;控制器将芯片内部一个内存储区块(例如硬盘)移动到内核缓冲区块(kernel buffer)
- CPU将内核缓冲区块(kernel buffer)拷贝到用户缓冲区块(user buffer),系统由内核态转为用户态,发生第二次上下文切换
- 程序调用操作系统的write函数,CPU将用户缓冲区块的数据拷贝到内核的套接字缓冲区块(socket buffer),系统由用户态转换为内核态,发生第三次上下文切换
- DMA控制器将内核缓冲区块的socket buffer拷贝到协议引擎(例如网卡/缓存等),此时系统由内核态转换为用户态,发生第四次上下文切换
总结:以上过程一共产生两次CPU数据拷贝和四次上下文切换。
如何实现零拷贝技术
零拷贝没有具体的实现标准,依赖于操作系统的支持,操作系统支持零拷贝实现方式,就可以实现零拷贝,反之则不支持,跟具体的程序设计语言没有关系。故零拷贝的工作流程没有固定的标准,依赖于操作系统的支持度。一般来讲,常见的有四种实现方式,这里只介绍直接内存映射和sendfile。
基于直接内存映射的零拷贝
以上过程对应的操作系统调用如下:
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
这种实现的核心是将用户缓冲区的内存地址与内核缓冲区的内存地址做了映射,这样用户上下文中可以直接操作内核缓冲区的内存数据。省略了将内核态的数据拷贝到用户态的缓冲区中,节省了一次cpu拷贝,但是内核态与用户态的上下文切换次数还是没变。
采用该方法实现零拷贝,需要注意内存映射过程中会出现用户缓冲区与内核缓冲区的数据不一致的情况,需要通过合理的并发编程来避免此问题。
基于sendfile实现的零拷贝
以上过程对应的操作系统调用:
sendfile(socket, file, len);
sendfile
系统调用在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了数据在内核缓冲区和用户缓冲区之间的拷贝,操作效率很高,被称之为零拷贝。
整个过程是:硬盘—>内核缓冲区—>内核socket缓冲区—>协议引擎
这个过程仍然有一次cpu copy的过程,实际上这个过程也是可以避免的,具体可以了解下带有DMA收集拷贝功能的sendfile。
关于零拷贝,这里讲到了基本的原理,具体的可以详细阅读
零拷贝的应用
NIO中的内存映射
File file = new File("xxx");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer buffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
其中fc.map
就是采用了操作系统中的内存映射方法,此方法适合对大文件的读取操作。
NIO中的零拷贝
File file = new File("xxx");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fc = raf.getChannel();
SocketChannel sc = SocketChannel.open(new InetSocketAddress("", xxx));
fileChannel.transferTo(0, fc.size(), socketChannel);
此方法是NIO中的零拷贝,transferTo
方法就是通过调用sendfile()
(linux,windows调用肯定不太一样)。Netty中就用到了FileChannel.transferTo方法。