高速网络的未来:零拷贝Zero-Copy架构

开发 前端
Java 中主要有两种方式实现零拷贝:使用 FileChannel 的 transferTo 和 transferFrom 方法,以及使用 MappedByteBuffer。

在当今高速发展的信息技术领域,追求极致的性能和效率是永恒的主题。而当我们深入探索计算机系统的内部奥秘时,一个令人瞩目的概念 —— 零拷贝(Zero-Copy)架构,逐渐走入我们的视野。想象一下,在数据如洪流般在系统中穿梭的场景下,传统的数据传输方式往往伴随着频繁的数据复制操作,这不仅消耗了大量的时间和系统资源,还成为了性能提升的瓶颈。而零拷贝架构宛如一位神奇的魔术师,以其独特的方式打破了这些束缚,为我们展现出一个全新的数据处理境界。

那么,零拷贝架构究竟有着怎样的魔力?它是如何实现高效的数据传输,为系统性能带来质的飞跃?让我们一同踏上这场充满惊喜与挑战的探索之旅,揭开零拷贝架构的神秘面纱,领略其在现代计算机系统中所绽放的璀璨光芒。

一、零拷贝架构简介

零拷贝指的是在 I/O 过程中,用户空间和内核空间不需要进行 CPU 数据拷贝。传统 I/O 与零拷贝架构在数据拷贝次数和上下文切换次数上存在明显差异。

零拷贝(zero-copy)基本思想是:数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现CPU的零参与,彻底消除 CPU在这方面的负载。实现零拷贝用到的最主要技术是DMA数据传输技术和内存区域映射技术。如图下所示,传统的网络数据报处理,需要经过网络设备到操作系统内存空间,系统内存空间到用户应用程序空间这两次拷贝,同时还需要经历用户向系统发出的系统调用。

而零拷贝技术则首先利用DMA技术将网络数据报直接传递到系统内核预先分配的地址空间中,避免CPU的参与;同时,将系统内核中存储数据报的内存区域映射到检测程序的应用程序空间(还有一种方式是在用户空间建立一缓存,并将其映射到内核空间,类似于linux系统下的kiobuf技术),检测程序直接对这块内存进行访问,从而减少了系统内核向用户空间的内存拷贝,同时减少了系统调用的开销,实现了真正的“零拷贝”。

图片图片

在传统的 I/O 操作中,读取文件并通过 Socket 发送,需要经过多次数据拷贝和上下文切换。具体来说,包括 4 次上下文切换、2 次 CPU 数据拷贝和 2 次 DMA 控制器数据拷贝。而 Linux 零拷贝架构旨在减少这些数据拷贝和上下文切换的次数,从而提高系统性能。在 Linux 操作系统层面上,有多种实现零拷贝的方案,如内存映射(mmap)、sendfile、splice、tee 等。

内存映射(mmap)是指用户空间和内核空间的虚拟内存地址同时映射到同一块物理内存,用户态进程可以直接操作物理内存,避免用户空间和内核空间之间的数据拷贝。其执行流程如下:用户进程通过系统调用 mmap 函数进入内核态,发生第 1 次上下文切换,并建立内核缓冲区;发生缺页中断,CPU 通知 DMA 读取数据;

DMA 拷贝数据到物理内存,并建立内核缓冲区和物理内存的映射关系;建立用户空间的进程缓冲区和同一块物理内存的映射关系,由内核态转变为用户态,发生第 2 次上下文切换;用户进程进行逻辑处理后,通过系统调用 Socket send,用户态进入内核态,发生第 3 次上下文切换;系统调用 Send 创建网络缓冲区,并拷贝内核读缓冲区数据;DMA 控制器将网络缓冲区的数据发送网卡,并返回,由内核态进入用户态,发生第 4 次上下文切换。

总结来看,mmap 避免了内核空间和用户空间的 2 次 CPU 拷贝,但增加了 1 次内核空间的 CPU 拷贝,整体上相当于只减少了 1 次 CPU 拷贝。针对大文件比较适合 mmap,小文件则会造成较多的内存碎片,得不偿失。当 mmap 一个文件时,如果文件被另一个进程截获可能会因为非法访问导致进程被 SIGBUS 信号终止。

sendfile 是在 linux2.1 引入的,它只需要 2 次上下文切换和 1 次内核 CPU 拷贝、2 次 DMA 拷贝。函数原型为 ssize_t sendfile (int out_fd, int in_fd, off_t *offset, size_t count);out_fd 为文件描述符,in_fd 为网络缓冲区描述符,offset 偏移量(默认 NULL),count 文件大小。

其内部执行流程是:用户进程系统调用 senfile,由用户态进入内核态,发生第 1 次上下文切换;CPU 通知 DMA 控制器把文件数据拷贝到内核缓冲区;内核空间自动调用网络发送功能并拷贝数据到网络缓冲区;CPU 通知 DMA 控制器发送数据;sendfile 系统调用结束并返回,进程由内核态进入用户态,发生第 2 次上下文切换。总结来看,数据处理完全是由内核操作,减少了 2 次上下文切换,整个过程 2 次上下文切换、1 次 CPU 拷贝,2 次 DMA 拷贝。虽然可以设置偏移量,但不能对数据进行任何的修改。

Linux2.4 对 sendfile 进行了优化,为 DMA 控制器引入了 gather 功能,即 sendfile+DMA gather。在不拷贝数据到网络缓冲区的情况下,将待发送数据的内存地址和偏移量等描述信息存在网络缓冲区,DMA 根据描述信息从内核的读缓冲区截取数据并发送。

其流程是:用户进程系统调用 senfile,由用户态进入内核态,发生第 1 次上下文切换;CPU 通知 DMA 控制器把文件数据拷贝到内核缓冲区;把内核缓冲区地址和 sendfile 的相关参数作为数据描述信息存在网络缓冲区中;CPU 通知 DMA 控制器,DMA 根据网络缓冲区中的数据描述截取数据并发送;sendfile 系统调用结束并返回,进程由内核态进入用户态,发生第 2 次上下文切换。总结来看,需要硬件支持,如 DMA;整个过程 2 次上下文切换,0 次 CPU 拷贝,2 次 DMA 拷贝,实现真正意义上的零拷贝。依然不能修改数据。

splice 是鉴于 Sendfile 的缺点,在 Linux2.6.17 中引入的。它在读缓冲区和网络操作缓冲区之间建立管道避免 CPU 拷贝:先将文件读入到内核缓冲区,然后再与内核网络缓冲区建立管道。其函数原型为 ssize_t splice (int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags)。执行流程如下:用户进程系统调用 splice,由用户态进入内核态,发生第 1 次上下文切换;CPU 通知 DMA 控制器把文件数据拷贝到内核缓冲区;建立内核缓冲区和网络缓冲区的管道;CPU 通知 DMA 控制器,DMA 从管道读取数据并发送。总结来看,依然不能修改数据。tee与splice 类同,但 fd_in 和 fd_out 都必须是管道。

⑴什么是零拷贝?

简单一点来说,零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。针对操作系统中的设备驱动程序、文件系统以及网络协议堆栈而出现的各种零拷贝技术极大地提升了特定应用程序的性能,并且使得这些应用程序可以更加有效地利用系统资源。这种性能的提升就是通过在数据拷贝进行的同时,允许 CPU 执行其他的任务来实现的。

零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率。而且,零拷贝技术减少了用户应用程序地址空间和操作系统内核地址空间之间因为上下文切换而带来的开销。进行大量的数据拷贝操作其实是一件简单的任务,从操作系统的角度来说,如果 CPU 一直被占用着去执行这项简单的任务,那么这将会是很浪费资源的;如果有其他比较简单的系统部件可以代劳这件事情,从而使得 CPU 解脱出来可以做别的事情,那么系统资源的利用则会更加有效。

⑵避免数据拷贝

  • 避免操作系统内核缓冲区之间进行数据拷贝操作。
  • 避免操作系统内核和用户应用程序地址空间这两者之间进行数据拷贝操作。
  • 用户应用程序可以避开操作系统直接访问硬件存储。
  • 数据传输尽量让 DMA 来做。

⑶将多种操作结合在一起

  • 避免不必要的系统调用和上下文切换。
  • 需要拷贝的数据可以先被缓存起来。
  • 对数据进行处理尽量让硬件来做。

前文提到过,对于高速网络来说,零拷贝技术是非常重要的。这是因为高速网络的网络链接能力与 CPU 的处理能力接近,甚至会超过 CPU 的处理能力。

如果是这样的话,那么 CPU 就有可能需要花费几乎所有的时间去拷贝要传输的数据,而没有能力再去做别的事情,这就产生了性能瓶颈,限制了通讯速率,从而降低了网络连接的能力。一般来说,一个 CPU 时钟周期可以处理一位的数据。举例来说,一个 1 GHz 的处理器可以对 1Gbit/s 的网络链接进行传统的数据拷贝操作,但是如果是 10 Gbit/s 的网络,那么对于相同的处理器来说,零拷贝技术就变得非常重要了。

对于超过 1 Gbit/s 的网络链接来说,零拷贝技术在超级计算机集群以及大型的商业数据中心中都有所应用。然而,随着信息技术的发展,1 Gbit/s,10 Gbit/s 以及 100 Gbit/s 的网络会越来越普及,那么零拷贝技术也会变得越来越普及,这是因为网络链接的处理能力比 CPU 的处理能力的增长要快得多。传统的数据拷贝受限于传统的操作系统或者通信协议,这就限制了数据传输性能。零拷贝技术通过减少数据拷贝次数,简化协议处理的层次,在应用程序和网络之间提供更快的数据传输方法,从而可以有效地降低通信延迟,提高网络吞吐率。零拷贝技术是实现主机或者路由器等设备高速网络接口的主要技术之一。

现代的 CPU 和存储体系结构提供了很多相关的功能来减少或避免 I/O 操作过程中产生的不必要的 CPU 数据拷贝操作,但是,CPU 和存储体系结构的这种优势经常被过高估计。存储体系结构的复杂性以及网络协议中必需的数据传输可能会产生问题,有时甚至会导致零拷贝这种技术的优点完全丧失。在下一章中,我们会介绍几种 Linux 操作系统中出现的零拷贝技术,简单描述一下它们的实现方法,并对它们的弱点进行分析。

二、零拷贝分类与优势

2.1零拷贝技术分类

零拷贝技术的发展很多样化,现有的零拷贝技术种类也非常多,而当前并没有一个适合于所有场景的零拷贝技术的出现。对于 Linux 来说,现存的零拷贝技术也比较多,这些零拷贝技术大部分存在于不同的 Linux 内核版本,有些旧的技术在不同的 Linux 内核版本间得到了很大的发展或者已经渐渐被新的技术所代替。本文针对这些零拷贝技术所适用的不同场景对它们进行了划分。概括起来,Linux 中的零拷贝技术主要有下面这几种:

  • 直接 I/O:对于这种数据传输方式来说,应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输:这类零拷贝技术针对的是操作系统内核并不需要对数据进行直接处理的情况,数据可以在应用程序地址空间的缓冲区和磁盘之间直接进行传输,完全不需要 Linux 操作系统内核提供的页缓存的支持。
  • 在数据传输的过程中,避免数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间进行拷贝。有的时候,应用程序在数据进行传输的过程中不需要对数据进行访问,那么,将数据从 Linux 的页缓存拷贝到用户进程的缓冲区中就可以完全避免,传输的数据在页缓存中就可以得到处理。在某些特殊的情况下,这种零拷贝技术可以获得较好的性能。Linux 中提供类似的系统调用主要有 mmap(),sendfile() 以及 splice()。
  • 对数据在 Linux 的页缓存和用户进程的缓冲区之间的传输过程进行优化。该零拷贝技术侧重于灵活地处理数据在用户进程的缓冲区和操作系统的页缓存之间的拷贝操作。这种方法延续了传统的通信方式,但是更加灵活。在Linux中,该方法主要利用了写时复制技术。

前两类方法的目的主要是为了避免应用程序地址空间和操作系统内核地址空间这两者之间的缓冲区拷贝操作。这两类零拷贝技术通常适用在某些特殊的情况下,比如要传送的数据不需要经过操作系统内核的处理或者不需要经过应用程序的处理。第三类方法则继承了传统的应用程序地址空间和操作系统内核地址空间之间数据传输的概念,进而针对数据传输本身进行优化。

我们知道,硬件和软件之间的数据传输可以通过使用 DMA 来进行,DMA  进行数据传输的过程中几乎不需要CPU 参与,这样就可以把 CPU 解放出来去做更多其他的事情,但是当数据需要在用户地址空间的缓冲区和Linux  操作系统内核的页缓存之间进行传输的时候,并没有类似  DMA  这种工具可以使用,CPU 需要全程参与到这种数据拷贝操作中,所以这第三类方法的目的是可以有效地改善数据在用户地址空间和操作系统内核地址空间之间传递的效率。

2.2Linux 零拷贝架构的优势

⑴减少了 CPU 拷贝,提升了 I/O 性能。

Linux 零拷贝架构通过多种方式减少了 CPU 拷贝操作,从而显著提升了 I/O 性能。在传统的 I/O 操作中,数据需要在用户空间和内核空间之间进行多次拷贝,这不仅占用了大量的 CPU 资源,还增加了数据传输的时间。而零拷贝架构则避免了这些不必要的拷贝操作,直接将数据从源地址传输到目标地址,大大提高了数据传输的效率。

例如,内存映射(mmap)虽然增加了一次内核空间的 CPU 拷贝,但整体上相当于只减少了一次 CPU 拷贝。对于大文件来说,mmap 可以有效地减少数据拷贝的次数,提高 I/O 性能。而 sendfile 在 Linux 2.1 引入后,只需要 2 次上下文切换和 1 次内核 CPU 拷贝、2 次 DMA 拷贝,相比传统的 I/O 操作,大大减少了 CPU 拷贝的次数。在 Linux 2.4 对 sendfile 进行优化后,引入了 DMA gather 功能,实现了真正意义上的零拷贝,整个过程只需要 2 次上下文切换和 0 次 CPU 拷贝、2 次 DMA 拷贝。

这些零拷贝技术的实现,使得数据传输更加高效,减少了 CPU 的负担,从而提升了系统的整体性能。

⑵降低了用户态和内核态切换次数,提高系统效率。

除了减少 CPU 拷贝外,Linux 零拷贝架构还降低了用户态和内核态的切换次数,进一步提高了系统效率。在传统的 I/O 操作中,读取文件并通过 Socket 发送需要经过多次用户态和内核态的切换,这不仅增加了系统的开销,还降低了系统的响应速度。

而零拷贝架构通过优化数据传输的流程,减少了系统调用的次数,从而降低了用户态和内核态的切换次数。例如,sendfile 只需要 2 次上下文切换,相比传统的 I/O 操作减少了 2 次切换次数。这使得系统能够更加高效地处理数据传输任务,提高了系统的整体效率。

此外,零拷贝技术还可以减少内存带宽的占用,提高系统的并发处理能力。在实际应用中,可以根据具体的业务需求选择合适的零拷贝技术,以达到最佳的性能优化效果。

三、零拷贝的定义

Zero-copy, 就是在操作数据时, 不需要将数据 buffer 从一个内存区域拷贝到另一个内存区域. 因为少了一次内存的拷贝, 因此 CPU 的效率就得到的提升;在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。

Netty 中的 Zero-copy 与 OS 的 Zero-copy 不太一样, Netty的 Zero-coyp 完全是在用户态(Java 层面)的, 它的 Zero-copy 的更多的是偏向于优化数据操作。

3.1Netty的“零拷贝”

主要体现在如下三个方面:

  • Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
  • Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便得对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
  • Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

3.2传统 IO 方式

在 java 开发中,从某台机器将一份数据通过网络传输到另外一台机器,大致的代码如下:

Socket socket = new Socket(HOST, PORT);
InputStream inputStream = new FileInputStream(FILE_PATH);
OutputStream outputStream = new DataOutputStream(socket.getOutputStream());

byte[] buffer = new byte[4096];
while (inputStream.read(buffer) >= 0) {
    outputStream.write(buffer);
}

outputStream.close();
socket.close();
inputStream.close();

看起来代码很简单,但如果我们深入到操作系统层面,就会发现实际的微观操作更复杂。具体操作如下图:

图片图片

  1. 用户进程向OS发出read()系统调用,触发上下文切换,从用户态转换到内核态。
  2. CPU发起IO请求,通过直接内存访问(DMA)从磁盘读取文件内容,复制到内核缓冲区PageCache中
  3. 将内核缓冲区数据,拷贝到用户空间缓冲区,触发上下文切换,从内核态转换到用户态。
  4. 用户进程向OS发起write系统调用,触发上下文切换,从用户态切换到内核态。
  5. 将数据从用户缓冲区拷贝到内核中与目的地Socket关联的缓冲区。
  6. 数据最终经由Socket通过DMA传送到硬件(网卡)缓冲区,write()系统调用返回,并从内核态切换回用户态。

图片图片

四、零拷贝(Zero-copy)

4.1数据拷贝基础过程

在Linux系统内部缓存和内存容量都是有限的,更多的数据都是存储在磁盘中。对于Web服务器来说,经常需要从磁盘中读取数据到内存,然后再通过网卡传输给用户:

图片图片

上述数据流转只是大框,接下来看看几种模式。

⑴仅CPU方式

  • 当应用程序需要读取磁盘数据时,调用read()从用户态陷入内核态,read()这个系统调用最终由CPU来完成;
  • CPU向磁盘发起I/O请求,磁盘收到之后开始准备数据;
  • 磁盘将数据放到磁盘缓冲区之后,向CPU发起I/O中断,报告CPU数据已经Ready了;
  • CPU收到磁盘控制器的I/O中断之后,开始拷贝数据,完成之后read()返回,再从内核态切换到用户态;

图片图片

⑵CPU&DMA方式

CPU的时间宝贵,让它做杂活就是浪费资源。

直接内存访问(Direct Memory Access),是一种硬件设备绕开CPU独立直接访问内存的机制。所以DMA在一定程度上解放了CPU,把之前CPU的杂活让硬件直接自己做了,提高了CPU效率。

目前支持DMA的硬件包括:网卡、声卡、显卡、磁盘控制器等。

图片图片

有了DMA的参与之后的流程发生了一些变化:

图片图片

主要的变化是,CPU不再和磁盘直接交互,而是DMA和磁盘交互并且将数据从磁盘缓冲区拷贝到内核缓冲区,之后的过程类似。

敲黑板】无论从仅CPU方式和DMA&CPU方式,都存在多次冗余数据拷贝和内核态&用户态的切换。”

我们继续思考Web服务器读取本地磁盘文件数据再通过网络传输给用户的详细过程。

4.2普通模式数据交互

一次完成的数据交互包括几个部分:系统调用syscall、CPU、DMA、网卡、磁盘等。

图片图片

系统调用syscall是应用程序和内核交互的桥梁,每次进行调用/返回就会产生两次切换:

  • 调用syscall 从用户态切换到内核态
  • syscall返回 从内核态切换到用户态

图片图片

来看下完整的数据拷贝过程简图:

图片图片

读数据过程:

  • 应用程序要读取磁盘数据,调用read()函数从而实现用户态切换内核态,这是第1次状态切换;
  • DMA控制器将数据从磁盘拷贝到内核缓冲区,这是第1次DMA拷贝;
  • CPU将数据从内核缓冲区复制到用户缓冲区,这是第1次CPU拷贝;
  • CPU完成拷贝之后,read()函数返回实现用户态切换用户态,这是第2次状态切换;

写数据过程:

  • 应用程序要向网卡写数据,调用write()函数实现用户态切换内核态,这是第1次切换;
  • CPU将用户缓冲区数据拷贝到内核缓冲区,这是第1次CPU拷贝;
  • DMA控制器将数据从内核缓冲区复制到socket缓冲区,这是第1次DMA拷贝;
  • 完成拷贝之后,write()函数返回实现内核态切换用户态,这是第2次切换;

综上所述:

  • 读过程涉及2次空间切换、1次DMA拷贝、1次CPU拷贝;
  • 写过程涉及2次空间切换、1次DMA拷贝、1次CPU拷贝;

可见传统模式下,涉及多次空间切换和数据冗余拷贝,效率并不高,接下来就该零拷贝技术出场了。

4.3零拷贝技术

(1)出现原因

我们可以看到,如果应用程序不对数据做修改,从内核缓冲区到用户缓冲区,再从用户缓冲区到内核缓冲区。两次数据拷贝都需要CPU的参与,并且涉及用户态与内核态的多次切换,加重了CPU负担。

我们需要降低冗余数据拷贝、解放CPU,这也就是零拷贝Zero-Copy技术。

(2)解决思路

目前来看,零拷贝技术的几个实现手段包括:mmap+write、sendfile、sendfile+DMA收集、splice等。

图片图片

(3)mmap方式

mmap函数的原型为void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);。其中,addr是开始映射的地址,属于进程的逻辑地址;length从开始映射地址,映射的长度,一般是一个页大小,4KB;prot是期望的内存保护标志,不能与文件打开的标志冲突,比如文件只可读,这里就不能可写,有PROT_EXEC(页内容可以被执行)、PROT_READ(页内容可以被读取)、PROT_WRITE(页可以被写入)、PROT_NONE(页不可访问)几种选项;flags指定映射对象的类型,映射选项和映射页是否可以共享,如MAX_FIXED(如果参数start所指的地址无法成功建立映射时,则放弃映射)、MAP_SHARED(与其他映射这个文件的进程共享映射内存,可能存在并发修改)、MAP_PRIVATE(对映射区域的写入操作会产生一个映射文件的复制,类似于写时复制,对此区域作的任何修改都不会写回原来的文件内容);fd是文件描述符;offset是文件映射的偏移量,已经映射了多少。

mmap是Linux提供的一种内存映射文件的机制,它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射,从而实现内核缓冲区与用户缓冲区的共享。这样就减少了一次用户态和内核态的CPU拷贝,但是在内核空间内仍然有一次CPU拷贝。

mmap+write 工作流程:

  • 第一,调用mmap函数将文件和进程虚拟地址空间映射。
  • 第二,将磁盘数据读取到页高速缓存。
  • 第三,调用write函数将页高速缓存数据直接写入套接字缓冲区。
  • 第四,将套接字缓冲区的数据写入网卡。

mmap+write 数据传输流程:

  • 用户进程调用mmap函数,向内核发起调用,CPU 从用户态切换到内核态。
  • 建立文件物理地址和虚拟内存映射区域的映射,或者说是内核缓冲区 (页高速缓存) 和虚拟内存映射区域的映射。
  • CPU 向磁盘 DMA 控制器发送读取指定位置和大小的指令,DMA 控制器将数据从磁盘拷贝到内核缓冲区。
  • mmap系统调用结束返回,CPU 从内核态切换到用户态。
  • 用户进程调用write函数,向内核发起调用,CPU 从用户态切换到内核态。
  • CPU 将页高速缓存中的数据拷贝到套接字缓冲区。
  • CPU 向磁盘 DMA 控制器发送 DMA 写指令,DMA 控制器从套接字缓冲区调用协议栈处理,最后把数据拷贝到网卡。
  • write系统调用结束返回,CPU 从内核态切换到用户态。

图片图片

mmap对大文件传输有一定优势,但是小文件可能出现碎片,并且在多个进程同时操作文件时可能产生引发coredump的signal。

(4)sendfile方式

sendfile函数原型为ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);。其中,out_fd是写入的文件描述符;in_fd是写入文件描述符;offset是从哪个位置开始读取;count是读取多少数据。

  • mmap+write方式有一定改进,但是由系统调用引起的状态切换并没有减少。
  • sendfile系统调用是在 Linux 内核2.1版本中被引入,它建立了两个文件之间的传输通道。

sendfile 工作原理

  • 第一,调用sendfile函数。
  • 第二,从磁盘读取数据,拷贝到内核缓冲区。
  • 第三,CPU 将内核缓冲区数据拷贝到套接字缓冲区。
  • 第四,套接字缓冲区数据拷贝到网卡。

sendfile 数据传输流程

  • 用户进程调用sendfile函数,向内核发起调用,CPU 从用户态切换内核态。
  • CPU 向磁盘 DMA 控制器发送读取数据的指令,DMA 控制器读取磁盘数据,拷贝到页高速缓存。
  • 然后 CPU 将页高速缓存的数据拷贝到套接字缓冲区。
  • CPU 向网卡 DMA 引擎发送读取指令,从套接字缓冲区调用协议栈处理,然后数据拷贝到网卡。
  • sendfile调用结束,CPU 从内核态切换到用户态。

sendfile + DMA scatter / gather copy

什么是 block DMA:

block DMA 就是要求传输数据块的源物理地址和目标物理地址都是连续的,每次只能传输一个数据块,传输完成后,中断机构触发中断。

什么是 SG-DMA:

  • 第一,SG-DMA 是 scatter /gather 的缩写,scatter 可以将一个源位置连续的数据块传输到目的地离散的存储;gather 可以将源位置分散的数据块传输到目的地连续的存储。
  • 第二,SG-DMA 会预先维护一个物理上不连续的块描述符的链表,描述符中包含有数据的起始地址和长度。传输时只需要遍历链表,按序传输数据,全部完成后发起一次中断即可,效率比 Block DMA 要高。

sendfile + DMA scatter /gather copy 的核心思想

sendfile 设计的时候并不是针对处理大文件的,如果需要处理大文件的话,需要调用另外一个接口sendfile64()。它的核心思想就是基于 DMA scatter /gather 来实现的。在 Linux 2.4 内核版本中,对sendfile系统方法做了优化升级,引入 SG-DMA 技术,需要 DMA 控制器支持。

其实就是对 DMA 拷贝加入了 scatter/gather 操作,它可以直接从内核空间缓冲区中将数据读取到网卡,多省去一次 CPU 拷贝。实现了真正意义上的零拷贝,整个过程 2 次上下文切换,0 次 CPU 拷贝,2 次 DMA 拷贝。

sendfile方式只使用一个函数就可以完成之前的read+write 和 mmap+write的功能,这样就少了2次状态切换,由于数据不经过用户缓冲区,因此该数据无法被修改。

splice 系统调用可以在内核缓冲区和socket缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。

图片

splice也有一些局限,它的两个文件描述符参数中有一个必须是管道设备。

以下使用 FileChannel.transferTo 方法,实现 zero-copy:

SocketAddress socketAddress = new InetSocketAddress(HOST, PORT);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(socketAddress);

File file = new File(FILE_PATH);
FileChannel fileChannel = new FileInputStream(file).getChannel();
fileChannel.transferTo(0, file.length(), socketChannel);

fileChannel.close();
socketChannel.close();

相比传统方式,零拷贝的执行流程如下图:

图片图片

可以看到,相比传统方式,零拷贝不走数据缓冲区减少了一些不必要的操作。

4.4零拷贝的应用

零拷贝在很多框架中得到了广泛使用,常见的比如 Netty、Kafka 等等。

在 kafka 中使用了很多设计思想,比如分区并行、顺序写入、页缓存、高效序列化、零拷贝等等。

上边博客分析了 Kafka 的大概架构,知道了 kafka 中的文件都是以.log 文件存储,每个日志文件对应两个索引文件.index 与.timeindex。kafka 在传输数据时利用索引,使用 fileChannel.transferTo (position, count, socketChannel) 指定数据位置与大小实现零拷贝。

kafka 底层传输源码:(TransportLayer)

/**
     * Transfers bytes from `fileChannel` to this `TransportLayer`.
     *
     * This method will delegate to {@link FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)},
     * but it will unwrap the destination channel, if possible, in order to benefit from zero copy. This is required
     * because the fast path of `transferTo` is only executed if the destination buffer inherits from an internal JDK
     * class.
     *
     * @param fileChannel The source channel
     * @param position The position within the file at which the transfer is to begin; must be non-negative
     * @param count The maximum number of bytes to be transferred; must be non-negative
     * @return The number of bytes, possibly zero, that were actually transferred
     * @see FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)
     */
    long transferFrom(FileChannel fileChannel, long position, long count) throws IOException;

实现类(PlaintextTransportLayer):

@OverRide 
 public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
      return fileChannel.transferTo(position, count, socketChannel);
 }

该方法的功能是将 FileChannel 中的数据传输到 TransportLayer,也就是 SocketChannel。在实现类 PlaintextTransportLayer 的对应方法中,就是直接调用了 FileChannel.transferTo () 方法。

五、零拷贝架构应用场景

5.1文件下载服务中的应用

在文件下载服务中,传统的 I/O 操作会导致多次数据拷贝和上下文切换,降低了系统性能。而 Linux 零拷贝架构可以有效地解决这个问题。例如,在文件下载服务中,可以使用内存映射(mmap)、sendfile 等零拷贝技术,直接将文件从磁盘读取到内核缓冲区,然后再通过网络发送到客户端,避免了用户空间和内核空间之间的数据拷贝,提高了文件下载的速度。

此外,零拷贝技术还可以减少内存带宽的占用,提高系统的并发处理能力。在文件下载服务中,可以同时处理多个客户端的请求,提高系统的吞吐量。

5.2Kafka 中的应用

Apache Kafka 是一款开源的、分布式的、高吞吐量的流平台,广泛用于实时数据流的处理。Kafka 的高性能得益于其服务端和客户端的架构设计,以及关键的设计和优化技术,如服务端的顺序写磁盘、零拷贝,客户端的批量发送等。

在 Kafka 中,使用了 Linux 的零拷贝技术 ——sendfile 系统调用来将消息从页面缓存发送到网络套接字。这样,数据可以在内核空间内直接传输,避免了在用户空间和内核空间之间来回拷贝数据,大大提高了数据传输的效率。

此外,Kafka 的客户端也充分考虑了性能优化。一个重要的优化技术是批量发送,即客户端将多条消息打包成一个批次,然后一次性发送到服务器。这种方式减少了网络交互的开销,提高了整体的吞吐量。

五、零拷贝架构的实例

6.1数据采集设备中的应用

在数据采集设备中,传统的 I/O 操作会导致数据在设备驱动、应用层和网卡之间进行多次拷贝,消耗大量的 CPU 资源,降低系统效率。而采用 Linux 零拷贝技术可以有效地解决这个问题。

例如,实际应用中有一数据采集设备 A,产生大量数据流。应用层读取通过该设备驱动获得数据,处理后再通过网卡分发出去。在传统操作中,数据要由 A 驱动 copy 到应用层,再由应用层处理后再次走网卡驱动,cpu 除了要进行大量的应用处理(计算)还要 copy,效率很低,接收端会出现数据卡的现象。

采用 zero-copy 技术后,具体实现如下:内核端定义共享内存大小和相关变量,通过一系列操作获取内核缓冲区的虚拟地址和物理地址,并将物理地址传给应用层。应用层通过获得的物理地址打开内存映射,获取共享内存指针,直接读取其中的内容。这样就避免了数据在设备驱动和应用层之间的拷贝,提高了系统效率。

6.2Java 中的零拷贝方式

Java 中主要有两种方式实现零拷贝:使用 FileChannel 的 transferTo 和 transferFrom 方法,以及使用 MappedByteBuffer。

使用 FileChannel 的 transferTo 和 transferFrom 方法:FileChannel 类提供了 transferTo 和 transferFrom 方法,可以在两个通道之间直接传输数据,而无需将数据拷贝到用户空间。例如,一个使用 FileChannel 的 transferTo 方法实现文件复制的示例中,通过将数据从源通道直接传输到目标通道,避免了数据在内核空间和用户空间之间的拷贝。

使用 MappedByteBuffer:MappedByteBuffer 允许我们将文件的某个区域直接映射到内存中,从而可以像操作内存一样来操作文件。这种方式同样减少了数据拷贝的次数。例如,一个使用 MappedByteBuffer 实现文件读取和写入的示例中。

责任编辑:武晓燕 来源: 深度Linux
相关推荐

2024-11-28 10:40:26

零拷贝技术系统

2020-10-12 06:33:18

Zero-Copy零拷贝CPU

2016-11-23 19:09:39

javanetty

2014-02-12 09:42:21

400G高速网络

2019-01-21 08:28:51

2022-05-05 13:57:43

Buffer设备MYSQL

2020-12-01 11:33:57

Python拷贝copy

2013-05-21 10:26:47

存储网络以太网虚拟化

2010-11-09 12:10:20

瞻博网络网络构架Juniper

2011-08-10 10:43:08

Fabric云计算融合网络层

2023-07-29 13:45:30

了不起 Java极

2022-01-10 09:26:08

零信任网络安全网络攻击

2011-12-26 10:59:02

数据中心网络TRILLSPB

2021-06-08 07:45:44

Go语言优化

2020-07-06 15:10:05

Linux拷贝代码

2020-07-23 15:40:54

Linux零拷贝文件

2013-03-20 10:08:05

华为SDN网络架构

2023-08-22 08:52:14

Go零值标识符

2021-09-08 05:31:44

医疗行业网络安全网络攻击

2011-11-23 13:29:05

点赞
收藏

51CTO技术栈公众号