零拷贝,零开销,更高效!
零拷贝本身是一种思想,不与任何编程语言绑定,不懂Java的读者可以跳过零拷贝技术在Java中实现的具体细节。
许多Web应用提供大量的静态内容,主要就是从磁盘读取数据然后将数据写回套接字,中间不涉及数据的变换。这种操作对CPU的使用相对较少,但是效率很低:首先,内核从文件读取数据,然后将数据从内核空间拷贝到用户进程空间,最后应用程序将数据拷贝回内核空间并通过套接字发送。实际上,在整个流程中应用程序仅充当一个将数据从磁盘拷贝到套接字的低效中间层。
每次数据跨越用户态和内核态的边界,数据都需要拷贝,拷贝操作消耗CPU和内存带宽。幸运的是通过一种称为“零拷贝”的技术可消除这些不必要的拷贝。使用零拷贝的应用要求内核将磁盘数据直接拷贝到套接字而不再经过应用。零拷贝可以极大的提高应用的性能并减少上下文在内核态和用户态之间的切换次数。
在 Linux 和 Unix 系统中 Java 类库通过java.nio.channels.FileChannel
的transgerTo
方法支持零拷贝。可以使用transgerTo
方法在两个通道之间直接传递数据,而不要求数据经过应用程序。为了更好的理解零拷贝技术对性能的提升,首先通过传统复制语义实现一个简单文件传输功能,然后通过零拷贝技术实现同样功能,并比较两种实现在性能上的差异。
数据传输: 传统语义
考虑这样的场景:从文件读取数据并通过网络将数据传递给其他程序(这是很多应用的行为,包括提供静态内容的Web应用,FTP 服务器,邮件服务器等)。两个核心的操作如代码1所示:
代码 1. 从文件拷贝数据到套接字
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
虽然代码1非常的简单,但是在代码内部实现,拷贝操作需要上下文在用户态和内核态切换四次,在操作完成前数据需要拷贝四次。图1展示了数据如何从文件转移到套接字:
数据拷贝路径
图 1. 传统数据拷贝方法
图 2 展示了上下文切换:
上下文切换
图 2. 传统方法下的上下文切换
涉及的步骤包括:
-
read()
调用导致上下文从用户态切换到内核态。内核通过sys_read()
(或等价的方法)从文件读取数据。DMA引擎执行第一次拷贝:从文件读取数据并存储到内核空间的缓冲区。 -
请求的数据从内核的读缓冲区拷贝到用户缓冲区,然后
read()
方法返回。