最近做的业务涉及到的 I/O 操作比较多,对于Linux上的 I/O 操作的优化 Zero Copy 早有耳闻,今天打算由上而下(从应用层到底层,当然并不会涉及到内核的细节)的研究一下这个问题。
为了更好的描述 zero copy ,本文将以网络服务器的简单过程所涉及的内容展开,该过程通过网络将存储在服务端的文件中的数据提供给客户端。整个过程主要是网络的 I/O 操作,数据至少被复制了4次,并且几乎已经执行了许多用户/内核上下文切换。 如下图所示,经过了下面四个步骤:
步骤一:操作系统发生 read 系统调用读取磁盘中的文件内容并将其存储到内核地址空间缓冲区中。
第二步:将数据从内核缓冲区复制到用户缓冲区,read 系统调用返回。调用的返回导致了从内核返回到用户模式的上下文切换,现在,数据存储在用户地址空间缓冲区中,它可以再次开始向下移动。
第三步:write 系统调用导致从用户模式到内核模式的上下文切换,执行第三个复制,将数据再次放入内核地址空间缓冲区中。但是这一次,数据被放入一个不同的缓冲区,这个缓冲区是与套接字相关联的。
第四步:写系统调用返回,创建第四个上下文切换。并将数据写入网络 I/O 中,网络传输中的服务端的操作逻辑到此结束。
从上图中我们知道,整个网络传输过程中数据被复制了多达4次之多,也进行了多次从用户态到内核态的切换。那么有没有可能减少数据的复制次数,提高网络 I/O 的效率呢?答案是肯定的。
那么到底什么是零拷贝呢?就是将数据直接从内核态的缓冲区中直接拷贝到 Socket 的缓冲区中,没有经过用户态的缓冲区,之所以被叫做零拷贝是相对于用户态来说的。如下图所示:
总的来说,从操作系统的角度来看是零拷贝,因为数据不是在内核缓冲区之间复制的。当使用零拷贝时,除了复制避免之外,还用其他性能优势,例如更少的上下文切换、更少的CPU数据缓存污染和没有CPU 校验和计算。
下面代码主要使用 JDK NIO 实现了上面阐述的业务逻辑
public class ZeroCopy { public static void main(String[] args) throws IOException { ZeroCopy zeroCopy = new ZeroCopy(); zeroCopy.sendfile(); } public void sendfile() throws IOException { String host = "localhost"; int port = 9026; SocketAddress sad = new InetSocketAddress(host, port); SocketChannel sc = SocketChannel.open(); sc.connect(sad); sc.configureBlocking(true); String fname = "src/main/java/zerocopy/test.data"; FileChannel fc = new FileInputStream(fname).getChannel(); long start = System.nanoTime(); long counter = fc.transferTo(0, fc.size(), sc); System.out.println("发送的总字节数:" + counter + " 耗时(ns):" + (System.nanoTime() - start)); sc.close(); fc.close(); } }
对于 I/O 操作的优化也可以参考零拷贝的思路来对我们的系统进行优化,最近了解到 kafka 之所以可以能够承载高吞吐量跟它强依赖底层操作系统的 page cache 有很大关系,跟零拷贝的减少数据在内核态与用户态之间的拷贝,上下文切换有异曲同工的操作,对 kafka 还不甚了解不敢多说了……
如上图所示,从宏观上来看,操作系统的体系架构分为用户态和内核态。内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括 CPU 资源、存储资源、I/O 资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。