Zero-Copy详细的技术细节

许多Web应用程序服务的静态内容显著量,这相当于读数据从盘的和写入完全相同的数据回响套接字这种活动可能出现需要相对少的CPU的活性,但它是有些低效:内核读取数据从磁盘的并推动它穿过内核用户边界的应用程序,然后该应用程序推回跨内核用户界写出到套接字。实际上,应用程序充当低效的中介从磁盘文件到套接字获取数据。

每次数据遍历用户内核边界,它必须被复制,这会消耗CPU周期和存储器带宽。幸运的是,你可以通过消除一种被称为这些副本-恰如其分地- 零拷贝使用零复制请求,内核将数据直接从磁盘文件到套接字复制,而不需要通过应用去的应用程序。零拷贝大大提高应用程序的性能,并减少内核模式和用户模式之间的上下文切换的数量。

Java类库支持在Linux和UNIX系统零拷贝通过transferTo()的方法 java.nio.channels.FileChannel。你可以使用 transferTo()方法直接从它被调用到另一个可写字节通道的通道传输的字节,而无需数据通过应用程序流。本文首先展示了通过传统拷贝语义进行简单文件传输的开销,然后展示了如何使用零拷贝技术, transferTo()实现了更好的性能。

日期传输:传统方法

考虑从文件中读取,并通过网络将数据传输到另一个节目的场景。(此方案描述的许多服务器应用程序的行为,包括提供静态内容的Web应用程序,FTP服务器,邮件服务器,等等。)操作的核心是在这两个通话清单1中(见下载的链接完整的示例代码):

清单1.复制字节从文件到套接字
1
2
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

虽然清单1的概念很简单,在内部,复制操作需要用户模式和内核模式之间的四个上下文切换和数据复制四次之前操作完成。图1显示了数据是如何在内部从文件到套接字流程:

图1.传统的数据拷贝方法
传统的数据拷贝方法

图2显示了上下文切换:

图2.传统上下文切换
传统上下文切换

涉及的步骤如下:

  1. read()调用导致上下文切换(参见图2)从用户模式到内核模式。内部一个sys_read()(或等效物)发出从文件读出的数据。第一个副本(参见图1)由直接存储器存取(DMA)引擎,它从磁盘读出的文件的内容,并将它们存入一个内核地址空间缓冲液进行。
  2. 请求的数据量从读缓冲器到用户的缓冲区,以及复制的read()呼叫返回。从调用的返回导致从内核另一个上下文切换回用户模式。现在该数据被存储在用户地址空间的缓冲区。
  3. send()套接字调用导致从用户模式到内核模式的上下文切换。进行第三副本将数据放到一个内核地址空间缓冲区一次。此时,虽然,数据被放入不同的缓冲区,即与目的插槽相关联的一个。
  4. send()系统调用返回,创造了第四个上下文切换。自主,异步,第四拷贝发生,因为DMA引擎从内核缓冲区协议引擎传送数据。

中间内核缓冲区(而不是直接传送数据到用户缓冲器)的使用似乎没有效率。但中间内核缓冲区被引入处理以提高性能。在读取方面使用中间缓冲区允许内核缓冲区充当“预读缓存”的时候,应用程序并没有要求尽可能多的数据内核缓冲区成立。当所请求的数据量小于内核缓冲区大小这显著提高性能。在写入侧中间缓冲器允许写入异步完成。

不幸的是,这种方法本身可成为性能瓶颈如果被请求的数据的大小大于内核缓冲区大小大得多。得到的数据的磁盘,内核缓冲区,和用户缓冲器之间复制多次之前,最后交付给应用程序。

零拷贝通过消除这些冗余的数据拷贝提高性能。

数据传输:零拷贝方法

如果你重新审视传统的情况下,您会发现,第二和第三个数据副本实际上并不是必需的。该应用程序,不外乎缓存数据并传送回套接字缓冲区。相反,数据可以直接从读缓冲器向套接字缓冲区传送。transferTo() 方法可以让你这样做正是这一点。清单2所示的方法签名 transferTo()

清单2. transferTo() 方法
1
public void transferTo(long position, long count, WritableByteChannel target);

transferTo()从文件通道给定的可写字节信道方式传输数据。在内部,它依赖于底层操作系统对零拷贝的支持; 在UNIX和Linux的各种口味,这个呼叫路由到sendfile() 系统调用,如清单3中所示,从一个文件描述符传输到另一个数据:

清单3. sendfile()系统调用
1
2
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

该的作用file.read()以及socket.send() 在通话清单1可以通过一个单一的替代 transferTo()调用,如清单4所示:

清单4.使用 transferTo()将数据从一个磁盘文件复制到套接字
1
transferTo(position, count, writableChannel);

图3示出了当数据路径transferTo()使用方法:

图3.数据与复制 transferTo()
与数据复制的transferTo()

图4示出了当在上下文切换transferTo() 时使用的方法:

图4.上下文切换与 transferTo()
上下文中使用的transferTo切换时()

当您使用采取的步骤transferTo()清单4分别是:

  1. transferTo()方法会导致文件内容被复制到由DMA引擎读取缓冲器。然后该数据由内核与输出套接字相关联的内核缓冲区复制。
  2. 第三个拷贝发生,因为DMA引擎通过从内核套接字缓冲区的协议引擎的数据。

这是一种进步:我们减少上下文切换的数目从四个两个,降低了数据的份数从四个三(其中只有一个涉及CPU)。但是,这还没有让我们对我们的零拷贝的目标。我们可以进一步减少内核做,如果底层网络接口卡支持重复数据收集操作在Linux内核2.4及更高版本,套接字缓冲区描述符被修改,以适应这一要求。这种方法不仅可以减少多个上下文切换,还消除了需要CPU参与复制的数据副本。用户端使用仍保持不变,但内部函数发生了变化:

  1. transferTo()方法会导致文件内容被复制到由DMA引擎内核缓冲区。
  2. 没有数据被复制到套接字缓冲区。而是,只提供有关数据的位置和长度的信息被附加在套接字缓冲区描述符。DMA引擎直接从内核缓冲区协议引擎通过数据,从而消除了剩下的最后一个CPU副本。

图5示出了使用数据的副本transferTo()与收集操作:

图5.数据拷贝时 transferTo(),收集使用操作
当使用的transferTo()和收集操作的数据拷贝

构建一个文件服务器

现在,让我们把零拷贝到实践中,使用传输客户端和服务器(请参阅之间文件的同一个例子下载的示例代码)TraditionalClient.java和 TraditionalServer.java基于传统的复制语义,使用File.read()Socket.send()TraditionalServer.java是监听服务器程序为客户端的特定端口进行连接,然后在从套接字一次读取4K字节的数据。TraditionalClient.java连接到服务器,读取(使用File.read()从文件)4K字节的数据,并发送(使用socket.send())中的内容到服务器经由套接字。

同样地,TransferToServer.java和 TransferToClient.java执行相同的功能,而是使用transferTo()方法(以及反过来的sendfile()系统调用)的文件从服务器传送到客户端。

性能比较

我们执行运行2.6内核的Linux系统上的示例程序和测量以毫秒为单位既有传统方法和运行时间transferTo()为各种尺寸的方法。表1示出其结果:

表1.性能对比:传统方法与零拷贝
文件大小 普通文件传输(毫秒) 的transferTo(毫秒)
7MB 156 45
21MB 337 128
63MB 843 387
98MB 1320 617
200MB 2124 1150
350MB 3631 1762
700MB 13498 4422
1GB 18399 8537

正如你所看到的,transferTo()API带来的下跌比例为传统方法的时间大约为65%。这具有提高性能显著为做数据的拷贝的一个很大的从一个I / O通道到另一个应用程序,如Web服务器的潜力。

概要

我们已经证明使用的性能优势 transferTo()相比从一个信道读出和写入相同的数据到另一个。中间缓冲区拷贝-即使是那些隐藏在内核-能有一个可衡量的成本。在于做通道间数据的拷贝的一个很大的应用中,零拷贝技术可以提供一个显著性能改进。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章