头图
原文链接为: https://developer.ibm.com/articles/j-zerocopy/

一些Web应用提供大量的静态资源,这相当于从磁盘读取大量数据,然后通过Socket将这些数据回写。

这种活动似乎看起来只需要一点CPU,但是非常低效: 内核从磁盘读取数据,并将其穿过内核-用户边界给应用,然后应用再将其穿过内核-用户边界推送过来,写入Socket。事实上,应用程序充当了从磁盘到Socket的低效媒介。

每次数据穿越用户空间和内核空间的边界时,都必须进行复制,这会消耗CPU周期和内存带宽。

幸运的是,你可以通过一种被称为“零拷贝”的技术来消除这些拷贝。

应用发起零拷贝请求,内核将直接将数据从磁盘复制到sockt, 不在需要通过应用程序。

零拷贝极大的改善了应用性能,减少了内核态到用户态之间的上下文切换次数。

Java 标准库在Linux和UNIX系统上通过Java.nio.channels.FileChannel的transferTo方法来支持零拷贝。

你可以通过transferTo方法直接将字节从调用该方法的通道传输到另一个可写字节的通道,而不需要数据流经应用程序。

本文首先演示通过传统方式拷贝进行简单文件传输所产生的开销,然后展示使用transferTo带来更好的性能实现。

数据传输的传统方法

考虑从文件中读取数据并通过网络传输给另外一个程序的状况(这个场景是许多应用服务器都会有的行为,包括Web 应用提供静态字段,FTP 服务器,邮件服务器等等)。操作核心是下面的两个调用,或者下载完整的代码[1]

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

尽管上面的代码在概念上很简单,但是内部的实现上要求复制操作在内核态和用户态上下文切换4次,数据要复制四次才能完成。

Figure 1. 传统数据拷贝

Figure 2. 传统上下文切换

  1. read()调用将会导致用户态到内核态上下文切换。内部会发出sys_read()(或类似命令)来读取文件中的数据。第一次复制(见Figure 1)由直接内存访问引擎执行,它从磁盘读 取文件内筒并将其存储到内核地址空间的缓冲区。
  2. 被请求的数据会从读缓冲区进去到用户缓冲区,然后read()调用被返回。 调用返回将会从内核态切换到用户态, 现在数据被存储到用户地址区空间缓冲区。
  3. send() socket 调用将会导致用户态从内核态切换。第三次复制将再次把数据放入到内核地址空间。不过,这一次数据被放入了不同缓冲区,一个目标相关Socket的缓冲区。
  4. send调用返回,将会产生第四次上下文切换。与此同时,独立且异步地, 当DMA引擎将数据从内核缓冲区复制到协议引擎,发生了第四次数据复制。

使用内核缓冲区当做媒介,而不是直接将数据传输到用户缓冲区,看起来比较低效。

但是为了改善性能,在程序中引入了中间内核缓冲区。

在读取端使用中间缓冲区,可以让内核缓冲区充当"超前读取缓存",当应用程序要求的数据还没有达到内核缓冲区的容量时。

当请求数据的数量小于内核缓冲区的大小时,将会大大的改善性能。写入侧的缓冲区允许异步导入。不幸的是,如果请求的数据量大大超过了内核缓冲区,这种方式将会成为性能瓶颈。数据在磁盘、内核缓冲区、用户缓冲区之间多次复制,最后才来到应用程序。

零拷贝消除了这些重复的拷贝来提升性能。

数据传输,零拷贝

如果你再次检查传统的拷贝方式,你就会发现第二次到第三次的复制事实上不是必须的。应用程序除了缓存数据将其传送给Socket的缓冲区之外,什么都不做。相反数据可以直接从读缓冲区传输到Socket缓冲区。transferTo方法允许你执行这个操作。下面展示了transferTo的签名。

Listing 2. The transferTo() method

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

trtransferTo() 方法将数据从文件通道传输到给定的可写字节通道。

在内部,它依赖于操作系统对零拷贝的支持,在UNIX和Linux的各种版本中,这个方法会被路由到 sendFile调用,如下面的调用所示,该调用将一个文件描述符传输到另外一个文件传输符。

The sendfile() system call

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

file.read()调用和socket.send调用被transferTo调用所取代。

使用 transferTo() 从磁盘复制数据到Socket。

transferTo(position, count, writableChannel);

transferTo 复制数据到Socket。当使用上面的代码进行所采取的步骤如下:

  1. transferTo方法导致文件内容被DMA引擎复制到读缓冲区。然后数据由内核直接复制到与socket输出关联的内核缓冲区中。
  2. 第三次复制发生在DMA引擎将数据从Socket 内核缓冲区传递到协议引擎中。

这是一个改进: 我们将上下文切换次数从四次减少到了两次,复制数据从四次减少到了三次(只有一次涉及到CPU)

但这还没有到达零拷贝。如果网卡支持收集操作,我们可以进一步减少内核执行的复制请求。在Linux 的2.4版本或者更新的版本,Socket缓冲区描述符已经满足此要求。

这种方法不仅减少了上下文切换次数,还消除了需要CPU参与的重复数据复制。开发者的用法保持不变,但内部机制已经发生了改变。

  1. transferTo方法通过DMA引擎将文件内容直接复制到内核缓冲区。
  2. 没有数据被复制进入Socket缓冲区,相反只有数据长度位置的描述符的信息被附加到Socket缓冲区。

    DMA引擎将数据直接从内核缓冲区传递到协议引擎,从而消除了剩余的最终的CPU复制。

构建一个文件服务器

现在让我们完成的实操一把,完整的代码见译者注。 TraditionalClient和TraditionalServer基于传统复制方式,用File.read()和Socket.send()方法。

TraditionalServer是一个在特定端口监听等待客户端连接的应用程序。每次从socket中读取4kb的数据。TraditionalClient连接到服务端,使用Filer.read方法每次从文件中读取4kb数据,使用Socket.send()将文件从socket发送到服务端。

类似地,TransferToServer.javaTransferToClient.java 执行相同的功能,但使用 transferTo() 方法(进而使用 sendfile() 系统调用)将文件从服务器传输到客户端。

性能比较

我们在Linux 2.6版本执行了相同程序,测量了传统方法和transferTo方法在在不同文件大小上下的运行时间:

正如你所看的transferTo API相对于传统方式快了百分之六十五。

这极大了增强了一些应用程序将大量的数据从一个I/O通道复制到另外一个I/O通道的性能,比如web服务器。

Summary

我们演示了与从一个通道读取并写入数据到另外一个通道相比,transferTo的性能优势。

中间缓冲区复制,即使隐藏在内核里面,也可能会产生大量的成本。在大量在通道之间复制数据的应用程序中,零拷贝技术可以提供显著的性能提升。性能提升。

译者注

[1] 提到的代码

public class TraditionalClient {
    
    
    
    public static void main(String[] args) {

    int port = 2000;
    String server = "localhost";
    Socket socket = null;
    String lineToBeSent;
    
    DataOutputStream output = null;
    FileInputStream inputStream = null;
    int ERROR = 1;
    
    
    // connect to server
    try {
        socket = new Socket(server, port);
        System.out.println("Connected with server " +
                   socket.getInetAddress() +
                   ":" + socket.getPort());
    }
    catch (UnknownHostException e) {
        System.out.println(e);
        System.exit(ERROR);
    }
    catch (IOException e) {
        System.out.println(e);
        System.exit(ERROR);
    }
    
    try {
        String fname = "sendfile/NetworkInterfaces.c";
        inputStream = new FileInputStream(fname);
        
        output = new DataOutputStream(socket.getOutputStream());
        long start = System.currentTimeMillis();        
        byte[] b = new byte[4096];
        long read = 0, total = 0;
        while((read = inputStream.read(b))>=0) {
        total = total + read;
            output.write(b);
        }
        System.out.println("bytes send--"+total+" and totaltime--"+(System.currentTimeMillis() - start));
    }
    catch (IOException e) {
        System.out.println(e);
    }

    try {
        output.close();
        socket.close();
        inputStream.close();
    }
    catch (IOException e) {
        System.out.println(e);
    }
    }    
}
import java.net.*;
import java.io.*;

public class TraditionalServer {
    
    public static void main(String args[]) {
    
    int port = 2000;
    ServerSocket server_socket;
    DataInputStream input;
    
    try {
        
        server_socket = new ServerSocket(port);
        System.out.println("Server waiting for client on port " + 
                   server_socket.getLocalPort());
        
        // server infinite loop
        while(true) {
        Socket socket = server_socket.accept();
        System.out.println("New connection accepted " +
                   socket.getInetAddress() +
                   ":" + socket.getPort());
        input = new DataInputStream(socket.getInputStream()); 
        // print received data 
        try {
            byte[] byteArray = new byte[4096];
            while(true) {
                int nread = input.read(byteArray , 0, 4096);
                if (0==nread) 
                    break;
            }
        }
        catch (IOException e) {
            System.out.println(e);
        }
        
        // connection closed by client
        try {
            socket.close();
            System.out.println("Connection closed by client");
        }
        catch (IOException e) {
            System.out.println(e);
        }
        
        }          
    }    
    catch (IOException e) {
        System.out.println(e);
    }
  }
}
public class TransferToClient {
    
    public static void main(String[] args) throws IOException{
        TransferToClient sfc = new TransferToClient();
        sfc.testSendfile();
    }
    public void testSendfile() 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 = "sendfile/NetworkInterfaces.c";
        long fsize = 183678375L, sendzise = 4094;
        
        // FileProposerExample.stuffFile(fname, fsize);
        FileChannel fc = new FileInputStream(fname).getChannel();
            long start = System.currentTimeMillis();
        long nsent = 0, curnset = 0;
        curnset =  fc.transferTo(0, fsize, sc);
        System.out.println("total bytes transferred--"+curnset+" and time taken in MS--"+(System.currentTimeMillis() - start));
        //fc.close();
      }
}
public class TransferToServer  {
  ServerSocketChannel listener = null;
  protected void mySetup()
  {
    InetSocketAddress listenAddr =  new InetSocketAddress(9026);

    try {
      listener = ServerSocketChannel.open();
      ServerSocket ss = listener.socket();
      ss.setReuseAddress(true);
      ss.bind(listenAddr);
      System.out.println("Listening on port : "+ listenAddr.toString());
    } catch (IOException e) {
      System.out.println("Failed to bind, is port : "+ listenAddr.toString()
          + " already in use ? Error Msg : "+e.getMessage());
      e.printStackTrace();
    }

  }
  public static void main(String[] args)
  {
    TransferToServer dns = new TransferToServer();
    dns.mySetup();
    dns.readData();
  }
  private void readData()  {
      ByteBuffer dst = ByteBuffer.allocate(4096);
      try {
          while(true) {
              SocketChannel conn = listener.accept();
              System.out.println("Accepted : "+conn);
              conn.configureBlocking(true);
              int nread = 0;
              while (nread != -1)  {
                  try {
                      nread = conn.read(dst);
                  } catch (IOException e) {
                      e.printStackTrace();
                      nread = -1;
                  }
                  dst.rewind();
              }
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
  }
}

北冥有只鱼
147 声望35 粉丝