翻译 Zero-copy

564 查看

翻译 Zero-copy文章
原文地址 http://www.ibm.com/developerw...
1.文章中粗斜字体表示翻译的不是太好,还有待修改
2.翻译的有不准确的地方,请留言,我会马上改正

Efficient data transfer through zero copy

很多Web应用都服务于大量的静态资源,这些静态资源大都是从磁盘中读取,然后将同样的数据写回给要返回的socket。这个过程似乎要求cpu进行相对的运转,但是这样有点低效:kernel从磁盘中读取的数据跨过kernel-user的边界写入应用,然后应用跨过kernel-user边界把数据写入到socket. 事实上,应用作为一个从磁盘获取的数据然后传输到socket中去的低效中介物。

数据每次穿过user-kernel边界,数据必须被复制一份,这样会消耗CPU周期与内存带宽。然而幸运的是,能通过一个叫“zero copy”的技术来消除这些复制。使用zero copy的应用要求kernel直接把数据从disk复制到socket,不需要通过应用。zero copy极大的提升了应用的性能 ,也减少了在kernel-mode 与 user-mode 之间上下文切换的次数。

java的类包在linux和unix操作系统上可以通过调用java.nio.channels.FileChannel 里的transferTo()支持zero copy的。你能通过transferTo() 直接把一个channel的数据传输到另一个可写的channel 里,而不要求通过应用来完成数据传输。 这篇文章首先演示了用传统复制方法传输简单文件所带来的过高的开销,然后再展示使用zero copy技术中的transferTo()方法而获得更好的表现。

Date transfer: The traditional approach

考虑到在网络上从文件中读取到数据然后传输到另一个程序的场景。(这个情况描述了很多服务应用的行为,包括web静态文件应用, FTP服务,邮件服务,等等)这些操作的核心是列表1中的两点。

Listing 1. Copying bytes from a file to a socket

File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

尽管Listing1 中只是一个概念性地简单地说明,内部的,复制操作需要在user-mode 和 kernel-mode 之间做4次上下文切换,并且操作完成前数据要被复制四次。图标1展示了这些数据怎么从文件中转移到socket中的:

Figure 1. Traditional data copying approach

图表2展示了上下文切换

Figure 2. Traditional context switches

过程包括一下:

  1. read()函数的调用导致了一次user-mode 到 kernel-mode的上下文切换(见图2)。内部的asys_read()调用(或者同等)被用于把数据从文件中读取出来。第一次复制(见图1)是以DMA引擎方式呈现的,DMA是从磁盘中读取数据后把这些数据存储到kernel address space buffer(内核地址空间缓冲区)。

  2. 大量被请求地数据从read buffer 复制到 user buffer,并且调用read()后返回结果。结果来自于函数调用引起从kernel-mode到user-mode的上下文切换。现在数据被存到了 user address space buffer( 用户地址空闲缓冲区)。

  3. send() socket 调用引起了从user-mode 到 kernel-mode 的上下文切换。第三次复制表现为又一次把数据存到了kernel address space buffer(内核地址空闲缓冲区)。这次,虽然,数据被放到了不同的缓冲区,这个缓冲区与目标socket相关。

  4. send() 系统调用返回,创造了第四次上下文切换。独立地和异步地,第四次复制发生是通过DMA引擎把数据从kernel buffer地数据传送到protocol引擎

中间kernel buffer地使用(不如直接把数据传输到user buffer)是低效地。但是中间kernel buffer被引用到程序中是为了提高性能的。当应用需要的数据没有达到kernel buffer的承载量,读的这边的中间buffer 允许kernel buffer 扮演“预读缓存”的角色。当请求的数据比kernel buffer承载量小的时候,性能就有一个大幅度地提高。在写的一边的中间buffer允许异步完成操作。

不幸地是,如果请求地数据比kernel的承载量大很多的话这种方法就会有一个瓶颈。数据在传递给appliction之前,在磁盘 ,kerenel buffer 和 user buffer 之间复制了多次。

zero copy 通过消除冗余的数据复制来提高性能

Data transfer: The zero-copy approach

如果再次检查传统的方法,你会发现第二次与第三次数据复制可以去掉的。应用除了缓存数据外没有做其他事情,然后将数据传回给socket buffer。相反,数据能被直接从read buffer 传输到 socket buffer 。transferTo() 方法就可以实现这个过程。Listing2 展示了transferTo()方法的特点:

Listing 2. The transferTo() method

public void transferTo(long position, long count, WritableByteChannel target);

transferTo()方法将数据从文件通道传输到可写数据的通道。内部,这个过程依潜在的操作系统必须支持zero copy;在unix和linux的各个版本中,这种调用被指定到了sendfile()系统调用,Listing3展示了数据从文件描述符传输到另一个描述符:

Listing 3. The sendfile() system call

#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

在Listing1 中的 file.read()与socket.send()的函数调用能被单独的transferTo()方法代替,Listing4中展示:

Listing 4. Using transferTo() to copy data from a disk file to a socket

transferTo(position, count, writableChannel);

图表3 展示了当transferTo()方法被调用时,数据的流向:

Figure 3. Data copy with transferTo()

图表4 展示了当 transferTo()方法被调用时,上下文切换的情况:

Figure 4. Context switching with transferTo()

调用 transferTo()时发生的步奏如下Listing4:

  1. transferTo()的调用使文件内容通过DMA的方式被复制到read buffer。然后将数据复制到与输出的套接字相关联的内核缓冲区中。

  2. 第三次复制发生是通过DMA引擎把数据从kernel socket buffers传输到协议引擎。

这是一个提高:我们把上下文切换从4次减少到了2次,把数据复制的次数从4次减少到3次(3次中只有一次涉及CPU),但是还没有达到我们zero copy的目标。如果底层网络接口卡支持收集操作的话,那么我们就可以进一步减少内核的数据复制。在linux 内核2.4和之后的版本中,socket buffer 描述符为要适应这个要求而被修改。这个方法不仅仅能减少上下文切,也可以消除涉需要Cpu参与重复数据的复制。user-side 层的使用仍然保持不变,但是内部调用已经改变:

  1. transferTo()方法调用使文件内容通过DMA引擎被复制到了kernel buffer

  2. 无数据被复制到socket buffer 。 相反,只有描述符的地址的信息和数据长度的信息被附加到了socket buffer。DMA引擎直接把数据从kernel buffer 传送到protocol engine,因此消除了剩余在Cpu里的副本。

图5 展示了调用transferTo() 数据复制的过程:

Figure 5. Data copies when transferTo() and gather operations are used

Building a file server

现在我们将zero copy 投入练习,用同一个例子在客服端与服务器端之间传输一个文件。TraditionalClient.java和 TraditionalServer.java是建立在传统复制场景的基础上,使用的是File.read()和Socket.send()方法。TraditionalServer.java是监听一个连接客户端的特定端口的服务端程序,然后一次从socket中读取4k大小的数据。TraditionalClient.java 用来连接服务端,从文件中读取(用 File.read()方法)4k数据,然后发送到服务端的socket通道上(用socket.send()方法)。

相似的是,TransferToServer.java和TransferToClient.java 起了同样的作用,但是用transferTo()方法代替上面的方法(File.read()和Socket.send()方法)去把数据从服务端传输到客户端。

Performance comparison

我们在运行着 2.6 kernel 的linux系统上执行了样本程序,并且测量一下传统方法和transferTo() 方法之间在传输不同文件大小的过程中运行时间。表1 展示了结果:

Table 1. Performance comparison: Traditional approach vs. zero copy

正如你看见的,相比传统方式传输方式,transferTo() API 降低大约65%的时间消耗,这对于处理大规模从I/O通道复制到另一个通道的应用有了一个潜在的大幅度的性能提升,例如web服务器。

Summary

我们演示了 用transferTo()方法 相比 从一个通道读取数据并写同一样的数据到另一个通道的方法 有一个极大优势。中间的buffer复制-即使他们隐藏在kernel中-能有一个可测量的成本。在通道之间处理大量的数据复制的应用中,zore-copy技术提高了很大的性能。