高效IO之零拷贝技术
这种技术是出现在 IO
操作上的, IO
操作会大量消耗 CPU
的性能,为什么说 IO
操作很容易成为性能瓶颈呢,每一个的 IO
操作都会涉及到操作系统的内核空间和用户空间的转换,真正执行的 IO
操作实际上是在操作系统的内核空间进行。无论是 文件IO
,还是 网络IO
,最后都可以统一为用户空间和内核空间数据的交换。计算机中内存和 CPU
都是非常稀有的资源,应该尽可能提高这些资源的使用效率。 IO
操作经常需要与磁盘就行交互,所以 IO
操作相比于 CPU
的速度要慢好几个数量级。利用这两者之间的速度差异,就可以实现不同种类的 IO
方式,也就是俗称的 IO模型
。
常见IO模型
常见 IO模型
也就是同步和异步的
同步IO模型
- 阻塞 BIO
- 非阻塞 NIO
- IO多路复用:poll、epoll、select
- 信号驱动IO
异步IO模型
- Linux AIO
这里说明一下:IO模型
虽然有些号称是不阻塞的,那是指在等待数据就绪的过程中是不阻塞的,但是在接收数据的时候,依然还是阻塞的。 AIO
是这些 IO
模型中真正实现完全不阻塞, AIO
在被调用之后直接返回,连接收数据的阶段也是非阻塞的,等到数据接收完成之后,内核才会返回一个通知,也就是说当用户进程接收到通知时,数据已经接收完成。在 Linux
中提供了 AIO
的实现,但是实际上使用的并不多,更多还是使用独立的异步 IO
库,比如 libevent
、 libev
、 libuv
。
一般文件操作是咋样?
场景:将一个文件复制到另一个文件
public static void copyFileByStream(File source, File dest) throws
IOException {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(dest);){
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}
}
正常我们就可以编写出如上代码,但是这些代码在操作系统上执行了哪些动作呢?如果不知道原理是没法找出优化点的,所以我们要探索一下拷贝的实现机制
拷贝实现机制分析
首先,你需要理解用户态空间(User Space)
和内核态空间(Kernel Space)
,这是操作系统层面的基本概念,操作系统内核、硬件驱动等运行在内核态空间,具有相对高的特权;而用户态空间,则是给普通应用和服务使用。
当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。
写入操作也是类似,仅仅是步骤相反,你可以参考下面这张图。
流程:
- 将需要拷贝的数据拷贝到内核空间中,于是产生了一次用户空间到内核空间的上下文切换
- 数据读取后返回后导致上下文从内核空间切换到了用户空间(这两步完成了读操作)
- 将读取的数据要写入到IO设备中,执行写入操作把用户空间切换到内核空间上下文中,并将数据写入到内核空间
- 将内核数据写入到目标IO设备中,然后写入操作返回结果,从内核空间返回到用户空间上下文中(这两步完成了写操作)
这个过程当中一共出现了4次数据拷贝和4次用户态-内核态的上下文切换(每一次系统调用都是两次上下文切换:用户态->内核态->用户态)。
所以,这种方式会带来一定的额外开销,可能会降低 IO
效率。
而基于 NIO transferTo
的实现方式,在 Linux
和 Unix
上,则会使用到零拷贝技术
,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。注意, transferTo
不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行 Socket
发送,同样可以享受这种机制带来的性能和扩展性提高。
transferTo
的传输过程是:
仔细检查之前的流程,其实第二次和第三次的复制没有必要(就是将在内核空间中的数据返回到用户空间和用户空间数据写入到内核空间中)
流程改变为:
- 从用户态切换到内核态
transferTo()
调用使文件内容通过DMA
的方式被复制到内核空间中。DMA
引擎直接把数据从内核空间 复制到目标IO设备
中 - 任务完成之后,切换回来。
可以理解为内核态空间与磁盘之间的数据传输,不需要再经过用户态空间,只需要2次切换、2次拷贝。