百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

一文搞懂!通过零拷贝实现高效数据传输

ahcoder 2025-03-24 13:27 87 浏览

背景

网络上不缺 零拷贝 这个技术话题的讲解;但能讲透这里面的一个知识点的,怕是很少。有些大而不全,有些专而不精。一篇国外2008年 讲零拷贝的文章;虽历经十多年 但作者对零拷贝里面transferTo 这个细致技术点的讲解和论证,还不错。本文开头稍显啰嗦,但我认为作者把本文的摘要都写到了开头这几段里。

许多 Web 应用程序对大量静态内容提供对外服务,这相当于从磁盘读取数据;然后不做任何的改动并把完全相同的数据,写回到响应套接字。这个一读和一写的过程,可能看起来需要相对较少的 CPU 活动,但效率有些低下。

操作系统内核从磁盘读取数据,然后将这些数据从内核态拷贝到用户态,以此推送到应用程序;然后应用程序把用户态的数据拷贝到内核态;然后再将其推回要写出到套接字。实际上,应用程序充当了将数据从磁盘文件获取到套接字的低效中介。

每次数据穿越用户-内核边界时,都必须进行数据的拷贝,这会消耗 CPU 周期和内存带宽。幸运的是,您可以通过一种称为“零拷贝” 的技术来消除这些副本。使用零拷贝这种技术的应用程序,请求内核将数据直接从磁盘文件复制到套接字,而不通过应用程序。零拷贝极大地提高了应用程序性能,并减少了内核和用户模式之间的上下文切换次数。

Java 类库里 通过
java.nio.channels.FileChannel类的transferTo()的方法在 Linux 和 UNIX 系统上支持零拷贝。您可以使用该transferTo()方法将字节直接从调用它的通道传输到另一个可写字节通道,而不需要数据流经应用程序(即要发送的数据,不需要在拷贝到用户态内存空间里)。本文首先演示了通过传统复制语义完成简单文件传输所产生的开销,然后展示了使用零拷贝技术transferTo() 如何实现更好的性能。

数据传输:传统方法

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

清单 1. 将字节从文件复制到套接字

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

虽然清单 1 在概念上很简单,但在内部,复制操作需要在用户模式和内核模式之间进行四次上下文切换,并且在操作完成之前数据会被复制四次。图 1 显示了数据如何从文件内部移动到套接字:

图 1. 传统数据复制方法

图 2 显示了上下文切换:

图 2. 传统的上下文切换

这里面的步骤:

  1. 该调用会导致从用户模式到内核模式的read()上下文切换(参见图 2 )。在内部发出 一次sys_read()(或等效的)来从文件中读取数据。第一次复制 参见图 1)由直接内存访问 (DMA) 引擎执行,该引擎从磁盘读取文件内容并将其存储到内核地址空间缓冲区中。(通过DMA把硬件上的数据,复制到内核磁盘缓冲区)
  2. 请求的数据从读取(内核磁盘)缓冲区复制到用户缓冲区,然后调用read()返回。调用的返回导致另一次上下文从内核切换回用户模式。现在数据存储在用户地址空间缓冲区中。
  3. 套接字send()调用导致上下文从用户模式切换到内核模式。执行第三次复制以将数据再次放入内核地址空间缓冲区(内核socket缓存区)中。不过,这一次,数据被放入不同的缓冲区中,该缓冲区与目标套接字相关联。
  4. 系统send()调用返回,创建第四个上下文切换。当 DMA 引擎将数据从内核缓冲区传递到协议引擎时,会独立且异步地进行第四次复制。

使用了中间的内核缓冲区(而不是直接将数据传输到用户缓冲区)可能看起来效率低下。但内核缓冲区实际是用来提高性能的。当应用程序没有请求内核缓冲区所容纳的数据量时,在读取端使用中间缓冲区允许内核缓冲区充当“预读缓存”。当请求的数据量小于内核缓冲区大小时,这会显着提高性能。因为写入端的中间缓冲区允许写入异步完成。(猜测作者想表达的是,如果要读的磁盘的数据,正好内核缓存里有,那么直接读内核缓冲区里的数据,不需要再去读磁盘上的数据,以此来提高读的性能;而对于写,同样的,应用程序在往磁盘上写数据时,先写到内核的磁盘缓冲区,而不直接写到硬件上,后续异步在写入硬件上,以此来提高写的性能)

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

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

数据传输:零拷贝方法

如果您重新检查传统场景,您会发现实际上并不需要第二个和第三个数据拷贝(即内核磁盘缓冲区拷贝到用户态,再从用户态拷贝到内核socket缓冲区)。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么也不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。该transferTo()方法可以让您准确地实现这一点。清单 2 显示了 transferTo() 方法的签名:

清单 2. TransferTo() 方法

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

该transferTo()方法将数据从文件通道传输到给定的可写字节通道。在内部,取决于底层操作系统对零拷贝的支持;在 UNIX 和各种版本的 Linux 中,此调用被路由到sendfile()系统调用,如清单 3 所示,该系统调用将数据从一个文件描述符传输到另一个文件描述符:

清单 3. sendfile() 系统调用

arduino

复制代码

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

清单 1中的file.read()和socket.send(),这两个调用 可以替换为单个调用,如清单 4 所示:transferTo()

清单 4. 使用transferTo() 将数据从磁盘文件拷贝到套接字

transferTo(position, count, writableChannel);

图3展示了使用 transferTo()方法时的数据路径:

图 3. 使用 TransferTo() 进行数据拷贝

transferTo()图 4 显示了使用该方法时的上下文切换:

图 4. 使用 TransferTo() 进行上下文切换

在清单 4 里使用 transferTo() 方法的步骤是:

  1. 该transferTo()方法使 DMA 引擎将文件内容复制到读缓冲区中。然后数据被内核复制到与输出套接字关联的内核缓冲区中。(即通过DMA把硬盘上的数据复制到内核磁盘缓冲区;然后在把内核磁盘缓存区数据复制到内核sockert缓冲区)
  2. 第三次复制发生在 DMA 引擎将数据从内核套接字缓冲区传递到协议引擎时。

这是一项改进:我们将上下文切换的次数,从 4 次减少到了 2 次,并将数据副本的数量从 4 个减少到了 3 个(其中只有一个涉及 CPU)。但这还没有让我们达到零拷贝的目标。如果底层网络接口卡支持收集操作,我们可以进一步减少内核所做的数据重复。在 Linux 内核 2.4 及更高版本中,修改了套接字缓冲区描述符以适应此要求。这种方法不仅减少了多次上下文切换,还消除了需要 CPU 参与的重复数据副本。用户端用法仍然保持不变,但内在原理发生了变化:

  1. 该transferTo()方法使 DMA 引擎将文件内容复制到内核缓冲区中。
  2. 没有数据拷贝到套接字缓冲区中。相反,只有包含数据位置和长度信息的描述符才会附加到套接字缓冲区。DMA 引擎将数据直接从内核缓冲区传递到协议引擎,从而消除了剩余的最终 CPU 参与的数据拷贝。

图 5 显示了在进行数据拷贝时 transferTo()方法 使用的收集操作:

图 5. 使用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()系统调用)将文件从服务器传输到客户端。

性能对比

我们在linux 2.6 上执行相同的示例程序,并测量传统方法和各种文件大小的场景的 运行时间(以毫秒为单位) 。表 1 显示结果:

表 1. 性能比较:传统方法与零拷贝

正如您所看到的,transferTo()与传统方法相比,减少了大约 65% 的时间。对于需要将大量数据从一个 I/O 通道复制到另一个 I/O 通道的应用程序(例如 Web 服务器),这有可能显着提高性能。

概括

我们已经展示使用 transferTo() 方法 ,从一个通道读取相同数据并将相同数据写入另一个通道相比的性能优势。中间缓冲区副本(即使是隐藏在内核中的拷贝)可能会产生可衡量的成本。在一个应用程序里,通过在两个通道之间进行大量数据复制时,使用零拷贝技术可以显着提高性能。

相关推荐

Java程序员必备的Linux命令速查表

Java程序员必备的Linux命令速查表在Java开发的世界里,Linux就像一位默默支持的幕后英雄。作为一名Java开发者,掌握一些基本的Linux命令,不仅能提高工作效率,还能让你在团队中显得格外...

Linux 命令速查手册:这 30 个高频指令,拯救 90% 的运维小白!

在Linux系统的世界里,命令行是强大的武器。对于运维小白而言,掌握一些高频使用的Linux命令,能极大提升工作效率,轻松应对各种系统管理任务。今天,就为大家奉上精心整理的30个Linu...

linux磁盘管理相关命令(linux磁盘管理常用命令)

磁盘的使用情况会直接影响系统的性能,因此我们经常会用到以下命令,主要围绕:fdisk:磁盘分区df:文件系统的磁盘空间占用情况du:文件目录的磁盘空间占用情况查看磁盘关系lsblk查看磁盘分区情况fd...

第四章 Linux常用shell命令-4.5.磁盘管理

主要介绍一下跟磁盘管理相关命令,有比较多的内容摘抄自网络,如有侵权,请及时联系我删除:显示目前在Linux系统上的文件系统磁盘使用情况统计:df创建和维护分区表的程序:fdisk将磁盘分区或镜像挂...

Linux新手必备:20个高效命令轻松掌握!

Linux基本命令使用指南在现代计算机操作系统中,Linux因其开放性、灵活性和强大的功能,广泛应用于服务器和开发环境中。作为技术人员,掌握Linux的基本命令是非常重要的。在本文中,我们将重点介绍2...

每日必学Linux命令:ls命令(linux命令详解之ls命令)

ls命令是linux下最常用的命令。ls命令就是list的缩写缺省下ls用来打印出当前目录的清单如果ls指定其他目录那么就会显示指定目录里的文件及文件夹清单。通过ls命令不仅可以查看linux文件...

Linux系统dev和proc目录详解(linux dev/sr0)

简介:Linux系统里的/dev和/proc目录那可是相当重要的系统文件。在Linux系统中,/dev目录专门用来存放设备文件。不光有设备文件,系统里还有好多特殊功能也是通过设备的形式...

Linux切换目录之cd命令(linux切换指定目录)

1.基本概念1.1命令作用当我们在Linux系统上工作时,做得相当多的一项任务就是在不同的目录之间进行切换,这时就需要用到cd命令了。cd是"changedirectory"的首...

Linux切换目录(cd命令)(linux如何切换到目录)

cd命令,是ChangeDirectory的缩写,用来切换工作目录。Linux命令按照来源方式,可分为两种,分别是Shell内置命令和外部命令。所谓Shell内置命令,就是Shel...

MongoDB数据库的快速部署和启动(mongodb的使用教程)

一、Mongodb介绍常见数据库介绍关系数据库RDBMS设计表结构,通过SQL语句进行操作。连表关系常见的关系型数据库:mysqloracle(商业)DB2(IBM)sqlserver(微软...

5分钟学会网络服务搭建,飞凌i.MX9352 + Linux 6.1实战示例

在“万物互联”的技术浪潮下,网络服务已成为连接物理世界与数字世界的核心纽带,它不仅赋予了终端设备“开口说话”的能力,更构建了智能设备的开发范式。本文就将以飞凌嵌入式OK-MX9352-C开发板(搭载了...

centos安装geoserver并配置开机启动

前提条件:服务器已经安装了java环境一、下载下载地址:http://geoserver.org/release/maintain/下载后文件名为:geoserver-2.19.3-bin.zip二、...

开机启动流程(开机流程图)

grubandbootCentos5,6的开机启动流程grubCentos7的开机启动流程Centos5,6的开机启动流程initrd/initramfs一般存储在/boot目录下,以.img...

Linux cron服务概述(crontab服务)

cron是Linux/Unix系统中一个非常重要的后台服务(守护进程),用于在预定的时间间隔自动执行命令或脚本。它使得自动化重复性任务成为可能,例如日志清理、数据备份、系统维护等。1.cron...

CentOS 8利用rc.local进行开机自启动的配置

CentOS8利用rc.local进行开机自启动的配置CentOS8linux系统是不建议使用rc.local进行开机自启动的,建议创建systemdservice。我们为了方便以后多一个配置...