零拷贝详解
背景
在程序开发中,将文件磁盘发送到另一个网络端是很常见的场景,通过代码实现也很简单,比如Java中,可以使用Inputsteam分块读取文件(通常我们将缓存区设置为8KB),然后将缓存区数据输出到Outputstream中。更好一点的方式,是通过PipedInputStream实例,让它自己去管理缓存区。
然而如果为了性能考虑,这种先读取文件在发送文件的方式,在操作系统层面对资源是有极大的损耗。为什么这么说呢,下面一张图说明了这个原因。
- JVM 执行read() 系统调用
- 操作系统从用户态,切换到内核态,读取磁盘数据到内核态缓存区buffer
- 内核态复制数据到应用buffer,从内核态切换到用户态,获取read()调用返回值
- JVM处理业务逻辑,然后执行系统调用write()
- 操作系统从用户态,切换到内核态,将应用buffer,复制到内核态buffer中
- 操作系统返回用户态,执行后续操作
如果应用程序对性能指标不关注,比如延迟、吞吐量,上面的流程是完全可以的。但是如果应用程序需要考虑性能问题,比如静态资源服务器,这样做并不会有很好的性能。上述中有4次内核态和用户态的转换,以及两次不必要的内存复制。
零拷贝
从上述问题可以看出,将数据从内核态复制到用户态,再从用户态复制到内核态,这两步操作是完全没有必要的,因为我们并没有去处理数据,仅仅是将数据从一个socket中复制到另一个。零拷贝技术就消除了这两步额外的内存复制。实现零拷贝的技术,并没有统一的标准,这依赖于操作系统的底层实现。一般来说,那些UNIX系统实现零拷贝功能,通常使用sendfile() 系统调用。
下图说明了sendfile系统调用实现零拷贝的方式
看到上图,消除了内核空间和用户空间直接的复制,但是同样存储从硬盘到内核空间的复制。的确如此,但是从操作系统的层面看,它已经实现了零拷贝,因为没有数据从内核空间到用户空间的拷贝。如果硬件支持了scatter-n-gather特性,这次的拷贝就可以避免。
设备支持scatter-n-gather特性如下图
众多的web服务器,都支持零拷贝,像Tomcat和Apache。默认情况下Apache是关闭零拷贝的。
注:Java中NIO中java.nio.channels.FileChannel#transferTo 提供了零拷贝的方法