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

原文链接为: 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(); } } } ```

相关推荐
dleei3 分钟前
MySql安装及SQL语句
数据库·后端·mysql
CryptoPP18 分钟前
springboot 对接马来西亚数据源API等多个国家的数据源
spring boot·后端·python·金融·区块链
Source.Liu32 分钟前
【学Rust写CAD】27 双线性插值函数(bilinear_interpolation.rs)
后端·rust·cad
yinhezhanshen36 分钟前
理解rust里面的copy和clone
开发语言·后端·rust
uhakadotcom43 分钟前
Helm 简介与实践指南
后端·面试·github
栗筝i1 小时前
Spring 核心技术解析【纯干货版】- XIX:Spring 日志模块 Spring-Jcl 模块精讲
java·后端·spring
企鹅不耐热.1 小时前
Scala基础知识6
开发语言·后端·scala
程序员一诺2 小时前
【Django开发】前后端分离django美多商城项目第15篇:商品搜索,1. Haystack介绍和安装配置【附代码文档】
后端·python·django·框架
冷琅辞2 小时前
Go语言的嵌入式网络
开发语言·后端·golang
跟着珅聪学java4 小时前
spring boot +Elment UI 上传文件教程
java·spring boot·后端·ui·elementui·vue