译: 通过零拷贝实现高效数据传输

原文链接为: developer.ibm.com/articles/j-...

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

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

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

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

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

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

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

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

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

数据传输的传统方法

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

lua 复制代码
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

arduino 复制代码
public void transferTo(long position, long count, WritableByteChannel target);

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

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

The sendfile() system call

arduino 复制代码
#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。

scss 复制代码
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] 提到的代码

java 复制代码
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);
	}
    }    
}
java 复制代码
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);
	}
  }
}
java 复制代码
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();
	  }
}
java 复制代码
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();
	  }
  }
}
相关推荐
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠3 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries3 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_4 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平5 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码6 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞7 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb