本文转载自:深度Linux,以下是账号名片
在当今数据洪流奔涌的时代,系统性能的优化可谓至关重要。你是否好奇,为何有的系统能在海量数据传输中,依旧保持高效流畅,而有的却陷入卡顿?答案或许就藏在一项神秘的技术 —— 零拷贝(Zero-Copy)中。它宛如一位隐匿在幕后的高手,默默地提升着系统性能,让数据传输实现飞跃式的加速。
想象一下,在传统的数据传输模式里,数据如同一位历经波折的旅人,在磁盘、内核缓冲区、用户缓冲区以及网络缓冲区之间来回辗转,每一次的 “落脚” 与 “启程”,都伴随着 CPU 的忙碌搬运。这不仅耗费了大量的 CPU 资源,还让数据传输的速度大打折扣。而零拷贝技术的出现,就像是为数据搭建了一条高速公路,减少甚至避免了这些不必要的 “旅途”,让数据能够快速、高效地抵达目的地。零拷贝技术究竟有着怎样的魔力?它与 DMA、PageCache 又有着怎样千丝万缕的联系?接下来,就让我们一同揭开零拷贝技术的神秘面纱,深入探索其背后的奥秘,相信你定会为这项神奇的技术所折服。
01
零拷贝(zero-copy)技术
1.1 传统数据拷贝的困境
在深入了解零拷贝之前,让我们先来剖析一下传统数据拷贝的工作方式。想象一下,你正在从服务器上下载一个文件,这个看似简单的操作背后,其实涉及到了一系列复杂的数据传输过程。
当你发起下载请求时,操作系统会先从磁盘中读取文件数据。这个过程中,数据首先会被读取到内核缓冲区,然后再被拷贝到用户空间的应用程序缓冲区。接着,应用程序将数据发送到网络,数据又会从用户空间缓冲区拷贝到内核空间的 socket 缓冲区,最后通过网卡发送出去。
具体来说,传统 I/O 的数据传输过程如下:
用户态到内核态切换:应用程序调用 read 系统调用,请求读取文件数据。此时,CPU 从用户态切换到内核态,开始执行内核中的代码。
磁盘数据读取到内核缓冲区:内核通过 DMA(直接内存访问)技术,将磁盘数据直接拷贝到内核缓冲区。DMA 技术可以让硬件设备(如磁盘控制器)直接访问内存,而不需要 CPU 的干预,从而减轻 CPU 的负担。
内核缓冲区数据拷贝到用户缓冲区:数据从内核缓冲区拷贝到用户空间的应用程序缓冲区。这一步需要 CPU 的参与,因为用户空间和内核空间是相互隔离的,数据不能直接在两者之间传递。
内核态到用户态切换:read 系统调用返回,CPU 从内核态切换回用户态,应用程序可以处理用户缓冲区中的数据。
用户态到内核态切换:应用程序调用 write 系统调用,请求将数据发送到网络。CPU 再次从用户态切换到内核态。
用户缓冲区数据拷贝到 socket 缓冲区:数据从用户缓冲区拷贝到内核空间的 socket 缓冲区,准备通过网络发送出去。这一步同样需要 CPU 的参与。
socket 缓冲区数据发送到网卡:内核通过 DMA 技术,将 socket 缓冲区中的数据拷贝到网卡缓冲区,然后通过网络发送出去。
内核态到用户态切换:write 系统调用返回,CPU 从内核态切换回用户态,数据传输完成。
从上述过程可以看出,传统 I/O 在一次简单的文件传输中,就涉及了 4 次用户态与内核态的上下文切换,以及 4 次数据拷贝(其中 2 次是 DMA 拷贝,2 次是 CPU 拷贝)。上下文切换和数据拷贝都会消耗 CPU 资源和时间,在高并发的场景下,这些开销会严重影响系统的性能。例如,在一个高并发的文件服务器中,如果每一次文件传输都要经历如此繁琐的过程,那么服务器的吞吐量将会受到极大的限制,响应速度也会变慢,用户体验也会大打折扣。
1.2 零拷贝的定义与核心思想
为了突破传统数据拷贝的性能瓶颈,零拷贝技术应运而生。零拷贝(zero-copy)并不是指完全不进行数据拷贝,而是一种通过操作系统内核优化,减少数据在用户空间(User Space)与内核空间(Kernel Space)之间冗余拷贝的技术,甚至完全避免不必要的 CPU 数据搬运,从而显著提升数据传输效率、降低 CPU 占用率。其核心目标可以总结为以下几点:
减少数据拷贝次数:传统的数据传输方式通常需要进行多次数据拷贝,而零拷贝技术通过巧妙的设计,尽可能地减少了这种不必要的复制。例如,在某些实现方式中,数据可以直接在内核空间中进行传输,避免了在用户空间和内核空间之间的来回拷贝,将传统的 4 次拷贝减少到最少 0 次 CPU 拷贝。
减少上下文切换次数:上下文切换是指 CPU 从一个任务切换到另一个任务时,需要保存和恢复任务的状态信息,这个过程会消耗一定的时间和资源。零拷贝技术通过减少系统调用的次数,从而减少了用户态和内核态之间的上下文切换次数。例如,传统的 I/O 操作需要 2 次系统调用(read/write),会导致 4 次用户态→内核态切换,而零拷贝技术可以将系统调用次数减少到 1 次,大大降低了上下文切换的开销。
避免内存冗余占用:在传统的数据传输中,数据往往需要在用户空间缓冲区中重复存储,这不仅浪费了内存资源,还增加了数据管理的复杂性。零拷贝技术通过让数据直接在内核空间中流转,避免了数据在用户空间的重复存储,提高了内存的利用率。
以 Linux 系统中的sendfile系统调用为例,这是一种常见的零拷贝实现方式。在使用sendfile时,数据可以直接从内核缓冲区传输到 socket 缓冲区,而不需要经过用户空间。具体过程如下:
用户态到内核态切换:应用程序调用sendfile系统调用,请求将文件数据发送到网络。CPU 从用户态切换到内核态。
磁盘数据读取到内核缓冲区:内核通过 DMA 技术,将磁盘数据直接拷贝到内核缓冲区。
内核缓冲区数据直接传输到 socket 缓冲区:内核直接将内核缓冲区中的数据传输到 socket 缓冲区,而不需要经过用户空间。这一步利用了内核的特殊机制,直接在内核空间中完成数据的传输,避免了数据在用户空间和内核空间之间的拷贝。
socket 缓冲区数据发送到网卡:内核通过 DMA 技术,将 socket 缓冲区中的数据拷贝到网卡缓冲区,然后通过网络发送出去。
内核态到用户态切换:sendfile系统调用返回,CPU 从内核态切换回用户态,数据传输完成。
对比传统 I/O 和零拷贝技术的数据传输路径,可以明显看出零拷贝技术的优势。在传统 I/O 中,数据需要在用户空间和内核空间之间多次拷贝,而在零拷贝技术中,数据可以直接在内核空间中传输,减少了数据拷贝的次数和上下文切换的开销。这就好比在物流运输中,传统 I/O 就像是货物需要多次装卸、转运,而零拷贝技术则像是货物可以直接从起点运输到终点,中间不需要多次中转,大大提高了运输的效率。
避免数据拷贝:
避免操作系统内核缓冲区之间进行数据拷贝操作。
避免操作系统内核和用户应用程序地址空间这两者之间进行数据拷贝操作。
用户应用程序可以避开操作系统直接访问硬件存储。
数据传输尽量让 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 操作系统中出现的零拷贝技术,简单描述一下它们的实现方法,并对它们的弱点进行分析。
02
DMA技术:零拷贝的硬件基石
2.1 RDMA 是何方神圣?
RDMA,全称远程直接数据存取(Remote Direct Memory Access),是一种创新性的网络通信技术。在传统网络通信模式下,数据传输往往需要经过操作系统及多层软件协议栈的处理,这会导致大量的 CPU 资源被占用,数据传输延迟较高。而 RDMA 技术的出现,旨在解决这些问题,它能够让计算机直接访问远程计算机的内存,而无需在本地和远程计算机之间进行繁琐的数据复制,从而显著降低数据传输的延迟,提高数据处理效率,这使得它在现代网络通信中占据着至关重要的地位,尤其在对网络性能要求极高的领域,如高性能计算(HPC)、数据中心、云计算等,发挥着不可或缺的作用。
2.2 DMA的工作原理
DMA(Direct Memory Access,直接内存访问)是一种能够让硬件设备(如磁盘控制器、网卡等)直接与内存进行数据传输的技术,而不需要 CPU 全程参与数据搬运过程。在传统的数据传输方式中,CPU 需要亲自将数据从一个存储区域搬运到另一个存储区域,这就像一个人需要亲自搬着货物从一个仓库运到另一个仓库,不仅耗费体力(CPU 资源),而且效率低下。而 DMA 技术就像是引入了一辆自动搬运车,它可以自己按照设定的路线(传输参数),将货物(数据)从一个仓库(源地址)搬运到另一个仓库(目标地址),而人(CPU)则可以去做其他更重要的事情。
DMA 的工作流程可以分为以下几个关键步骤:
初始化阶段:在数据传输开始之前,CPU 需要对 DMA 控制器进行初始化配置。这就好比给自动搬运车设定好出发地、目的地和搬运货物的数量。CPU 会向 DMA 控制器写入源地址(数据的来源位置,例如磁盘的某个扇区)、目标地址(数据要传输到的位置,例如内存的某个区域)以及传输数据的长度等参数 。
DMA 请求阶段:当硬件设备准备好要传输的数据时,它会向 DMA 控制器发送一个 DMA 请求信号,就像货物已经准备好了,通知自动搬运车来取货。
总线控制权获取阶段:DMA 控制器接收到请求后,会向 CPU 发送总线请求信号,请求接管系统总线的控制权。因为在同一时刻,系统总线只能被一个设备使用,所以 DMA 控制器需要先获取总线控制权,才能进行数据传输。CPU 在完成当前的总线操作后,会响应 DMA 请求,将总线控制权交给 DMA 控制器,就像人暂时放下手中与总线相关的工作,让自动搬运车使用运输通道(总线)。
数据传输阶段:在获得总线控制权后,DMA 控制器开始按照预先设定的参数,直接在硬件设备和内存之间进行数据传输。它会从源地址读取数据,然后写入到目标地址,这个过程不需要 CPU 的干预,自动搬运车按照设定路线,将货物从源仓库搬运到目标仓库。
传输完成通知阶段:当数据传输完成后,DMA 控制器会向 CPU 发送一个中断信号,通知 CPU 数据传输已经结束,就像自动搬运车完成搬运任务后,通知人可以继续其他工作了。CPU 收到中断信号后,会进行相应的处理,例如检查数据传输是否正确,或者启动下一个任务。
在这个过程中,涉及到的硬件组件主要有 DMA 控制器、硬件设备(如磁盘、网卡)和内存。DMA 控制器是整个数据传输过程的核心控制单元,它负责协调硬件设备和内存之间的数据传输;硬件设备是数据的来源或目的地;内存则是数据存储和中转的地方。它们之间通过系统总线进行数据和信号的传输,共同完成高效的数据传输任务。
下面是 RDMA 整体框架架构图,从图中可以看出,RDMA 提供了一系列 Verbs 接口,可在应用程序用户空间,操作RDMA硬件。RDMA绕过内核直接从用户空间访问RDMA 网卡。RNIC(RDMA 网卡,RNIC(NIC=Network Interface Card ,网络接口卡、网卡,RNIC即 RDMA Network Interface Card)中包括 Cached Page Table Entry,用来将虚拟页面映射到相应的物理页面。
(1)直接内存访问机制
RDMA 的核心在于其直接内存访问机制。在传统的网络通信中,数据传输需要 CPU 的深度参与,例如数据从应用程序缓冲区拷贝到内核缓冲区,再通过网络协议栈进行封装和传输,接收端则需要逆向操作,将数据从内核缓冲区拷贝到应用程序缓冲区,整个过程涉及多次数据拷贝和 CPU 上下文切换,效率较低。
而 RDMA 允许计算机直接存取其他计算机的内存,绕过了处理器的繁琐处理过程,数据传输的大部分工作由硬件来执行,直接在远程系统的内存之间进行读写操作,极大地提高了数据传输的效率和速度,减少了 CPU 的负担,使得系统能够将更多的资源用于实际的数据处理任务,从而提升整体性能。
(2)零拷贝与内核旁路
零拷贝技术是 RDMA 的另一大关键特性。在传统通信模式下,数据在传输过程中需要在不同的内存区域之间进行多次拷贝,例如从用户空间拷贝到内核空间,再从内核空间拷贝到网络设备缓冲区等,这些拷贝操作不仅消耗 CPU 资源,还会增加数据传输的延迟。而 RDMA 实现了零拷贝,使得数据能够直接在应用程序的缓冲区与网络之间进行传输,无需中间的拷贝环节,大大减少了数据传输的开销和延迟。
内核旁路也是 RDMA 提升性能的重要手段。在传统网络通信中,应用程序与网络设备之间的通信需要经过操作系统内核的干预,这会导致上下文切换和系统调用的开销。而 RDMA 允许应用程序在用户态直接与网卡进行交互,避免了内核态与用户态之间的上下文切换,进一步降低了 CPU 的负担,提高了数据传输的效率和响应速度。例如,在一些对实时性要求极高的金融交易系统中,RDMA 的零拷贝和内核旁路技术能够确保交易数据的快速传输和处理,减少交易延迟,提高交易效率。
(3)RDMA通信协议
目前,有三种支持RDMA的通信技术:
InfiniBand(IB): 基于 InfiniBand 架构的 RDMA 技术,需要专用的 IB 网卡和 IB 交换机。从性能上,很明显Infiniband网络最好,但网卡和交换机是价格也很高。
RoCE:即RDMA over Ethernet(RoCE), 基于以太网的 RDMA 技术,也是由 IBTA 提出。RoCE支持在标准以太网基础设施上使用RDMA技术,但是需要交换机支持无损以太网传输,只不过网卡必须是支持RoCE的特殊的NIC。
iWARP:Internet Wide Area RDMA Protocal,基于 TCP/IP 协议的 RDMA 技术(在现有TCP/IP协议栈基础上实现RDMA技术,在TCP协议上增加一层DDP),由 IETF 标 准定义。iWARP 支持在标准以太网基础设施上使用 RDMA 技术,而不需要交换机支持无损以太网传输,但服务器需要使用支持iWARP 的网卡。与此同时,受 TCP 影响,性能稍差。
这三种技术都可以使用同一套API来使用,但它们有着不同的物理层和链路层;需要注意的是,上述几种协议都需要专门的硬件(网卡)支持。
2.3 DMA 在零拷贝中的角色
在零拷贝技术中,DMA 扮演着至关重要的角色,它是实现数据高效传输的关键硬件支撑。零拷贝的核心目标是减少数据在用户空间和内核空间之间的拷贝次数,以及降低 CPU 在数据传输过程中的参与度,而 DMA 技术正好能够满足这些需求,实现数据在内核空间或不同硬件设备间的直接传输。
以网络数据接收为例,当网卡接收到网络数据包时,传统的方式是先将数据传输到内核缓冲区,然后 CPU 再将数据从内核缓冲区拷贝到用户空间的应用程序缓冲区。这个过程不仅涉及多次数据拷贝,而且 CPU 需要花费大量时间在数据搬运上。而借助 DMA 技术,网卡可以直接通过 DMA 控制器将接收到的数据包传输到内存中的内核缓冲区,无需 CPU 参与这一数据传输过程。
CPU 可以在 DMA 传输数据的同时,去处理其他任务,如运行应用程序的业务逻辑等。这样一来,大大减少了 CPU 的负载,提高了系统的整体性能和响应速度,就像在物流配送中,货物可以直接从发货地通过高效的运输工具(DMA)送到仓库(内核缓冲区),而不需要人工(CPU)一次次地搬运,节省了人力和时间成本。
再看磁盘数据读取的场景,当应用程序需要从磁盘读取数据时,如果没有 DMA 技术,CPU 需要不断地从磁盘读取数据块,并将其拷贝到内存中。而有了 DMA,磁盘控制器可以通过 DMA 控制器将磁盘数据直接传输到内存中的指定位置。在 Linux 系统中,当应用程序调用 read 系统调用来读取磁盘文件时,内核会初始化 DMA 操作,将磁盘数据直接读取到内核的 PageCache(页缓存)中。这个过程中,CPU 只需要初始化 DMA 传输参数,然后就可以去执行其他任务,数据的实际传输由 DMA 控制器负责。
数据读取完成后,DMA 控制器会通知 CPU,CPU 再根据需要将数据从 PageCache 拷贝到用户空间(如果需要的话)。相比传统方式,这种借助 DMA 的零拷贝方式减少了 CPU 的干预,提高了数据读取的效率,使得系统能够更快速地响应用户的请求,就像在图书馆借书,以前需要读者自己在书架间查找并搬运书籍,现在有了自动借书系统(DMA),读者只需要在系统上登记(CPU 初始化参数),系统就会自动将书籍送到指定位置,读者可以利用等待的时间去做其他事情。
03
PageCache:零拷贝的软件加速器
3.1 PageCache 的工作机制
PageCache 是 Linux 内核中一种至关重要的磁盘数据缓存机制,它犹如一个智能的高速数据中转站,极大地提升了文件系统的读写性能。PageCache 主要由物理内存中的页面组成,这些页面的内容对应于磁盘上的物理块,其大小通常为 4KB。PageCache 就像一个精心管理的仓库,将磁盘上的数据缓存到内存中,使得后续对这些数据的访问能够直接从内存中快速获取,而无需频繁地访问速度较慢的磁盘。
当用户进程对文件发起读取请求时,内核会首先如同一个敏锐的侦察兵,迅速检查该文件的内容是否已被加载到内存中的 PageCache 中。如果数据在缓存中,即发生了缓存命中,这就好比在仓库中轻松找到了所需的货物,内核可以直接从内存中读取数据并返回给用户进程,这个过程非常迅速,就像从身边的架子上直接取物一样,避免了耗时的磁盘 I/O 操作。例如,当我们多次读取同一个配置文件时,第一次读取后数据被缓存到 PageCache 中,后续读取就可以直接从缓存中获取,大大提高了读取速度。
然而,如果数据不在缓存中,即发生了缓存未命中,情况就会稍微复杂一些。此时,内核会如同接到紧急任务的调度员,发起磁盘 I/O 操作。内核会通过 DMA 技术,利用 DMA 控制器这个高效的搬运工,将磁盘上的数据直接传输到内存中的 PageCache 中。这个过程就像是从远方的仓库紧急调配货物到本地仓库。数据传输完成后,内核会将数据填充到 PageCache 中,并建立起文件与缓存页面之间的映射关系,就像给货物贴上标签并分类存放,以便后续访问。同时,为了进一步提高性能,内核还可能会采用预读技术,根据局部性原理,预测用户接下来可能需要访问的数据,并提前将其读取到 PageCache 中,就像提前准备好可能会用到的货物,随时待命。
在写数据时,PageCache 采用了一种延迟写(write - back)策略,这是一种高效的数据写入优化方式。当用户进程请求写入数据到文件时,内核并不会立即将数据写入磁盘,而是先如同将货物暂存在仓库的临时区域一样,将数据写入 PageCache 中,并将对应的页面标记为 “脏页”,表示该页面中的数据与磁盘上的数据不一致。这样做的好处是显而易见的,它允许内核将多次写操作合并为一次,就像将多个小订单合并成一个大订单一起处理,减少了磁盘 I/O 的次数。例如,当我们编辑一个文档并多次保存时,数据并不会每次都立即写入磁盘,而是先缓存在 PageCache 中,等到合适的时机再统一写入磁盘。
那么,什么时候会将脏页中的数据真正写入磁盘呢?这主要有以下几种触发条件:一是时间间隔,系统会定期如每隔一段时间(例如 5 秒、30 秒等)唤醒回写线程,这个回写线程就像一个定时巡检员,扫描脏页并将其写回磁盘;二是内存压力,当系统可用内存不足时,内核需要回收 PageCache 页来分配新内存,此时会先将脏页写回磁盘,就像仓库空间不足时,需要将暂存的货物整理归位;三是脏页比例,当系统中脏页数量超过一定阈值时,也会触发回写,以保证系统的稳定性;四是显式同步,应用程序调用 fsync ()、fdatasync () 或 sync () 等系统调用时,会强制刷新文件和文件系统的脏页,将数据立即写入磁盘,这就像是紧急调用,要求立即将特定的货物发送出去;五是文件关闭或卸载时,通常也会触发相关脏页回写,确保数据的完整性。在回写过程中,回写线程会找到脏页,发起磁盘 I/O 将数据写回到磁盘文件对应的位置,就像将货物准确无误地送回远方的仓库,写成功后,该页标记为 “干净”,表示数据已同步。
3.2 PageCache 与零拷贝的协同效应
PageCache 与零拷贝技术之间存在着紧密的协同关系,它们相互配合,如同默契的搭档,共同为高效的数据传输提供了强大的支持。在传统的数据传输过程中,数据往往需要在用户空间和内核空间之间多次拷贝,这不仅耗费时间,还占用大量的 CPU 资源。而 PageCache 和零拷贝技术的结合,巧妙地解决了这一问题。
以 sendfile 系统调用为例,这是实现零拷贝的一种重要方式。当应用程序调用 sendfile 系统调用来传输文件时,数据首先会从磁盘读取到 PageCache 中。如果 PageCache 中已经缓存了所需的数据,那么就可以直接从 PageCache 中获取,避免了再次从磁盘读取数据的开销。这一步就像是在本地仓库中直接找到了要发送的货物,无需再从远方仓库调配。然后,数据可以直接从 PageCache 拷贝到 socket 缓冲区,准备通过网络发送出去。
在这个过程中,数据不需要经过用户空间,减少了一次 CPU 拷贝,实现了数据在内核空间的直接传输。这就好比货物从本地仓库直接被搬运到发货区,无需经过其他中间环节,大大提高了传输效率。最后,通过 DMA 技术将 socket 缓冲区中的数据传输到网卡,发送到网络中,这个过程同样减少了 CPU 的参与,提高了传输速度。
在实际应用中,像 Nginx 这样的高性能 Web 服务器就充分利用了 PageCache 和零拷贝技术。当 Nginx 处理静态文件的传输时,通过 sendfile 系统调用,借助 PageCache 的缓存功能,将文件数据直接从磁盘缓存传输到网络,减少了数据拷贝和上下文切换的开销,从而能够快速地响应大量的客户端请求,提高了服务器的吞吐量和性能。在一个高并发的 Web 服务场景中,大量用户同时请求下载静态资源,Nginx 利用 PageCache 和零拷贝技术,能够迅速地将文件数据传输给用户,使得用户能够快速地获取到所需的资源,提升了用户体验。
04
零拷贝技术的实现方式
4.1 mmap+write
mmap+write 是零拷贝技术的一种实现方式,它通过将文件映射到内存,实现了数据的高效传输。在传统的数据传输方式中,当应用程序需要读取文件数据并发送时,数据需要从磁盘读取到内核缓冲区,再从内核缓冲区拷贝到用户空间缓冲区,最后从用户空间缓冲区拷贝到 socket 缓冲区进行发送,这个过程涉及多次数据拷贝和上下文切换,效率较低。
而 mmap+write 方式则巧妙地利用了虚拟内存的特性,减少了数据拷贝的次数。具体来说,mmap 系统调用会将文件的内容映射到进程的虚拟地址空间中,使得应用程序可以直接访问文件数据,就像访问内存中的数据一样。这个映射过程实际上是在内核空间中完成的,文件数据并没有真正拷贝到用户空间,而是通过页表机制建立了文件与内存之间的映射关系。此时,应用程序对映射内存区域的任何读写操作,实际上都是对文件的读写操作,数据的修改会直接反映到文件中。
当应用程序需要将文件数据发送到网络时,只需要调用 write 系统调用,将映射内存区域的数据写入 socket 缓冲区。由于映射内存区域与文件数据是共享的,所以这个过程中不需要再次将数据从用户空间拷贝到内核空间,只需要进行一次从内核空间的文件映射区域到 socket 缓冲区的拷贝,然后通过 DMA 技术将 socket 缓冲区的数据发送到网卡,实现数据的网络传输。
mmap+write 方式的优势在于减少了一次 CPU 拷贝,即将数据从内核缓冲区拷贝到用户缓冲区的过程。这使得数据传输的效率得到了显著提升,特别是在处理大文件或者高并发的数据传输场景中,能够有效降低 CPU 的使用率,提高系统的整体性能。例如,在一个文件服务器中,当大量用户同时请求下载文件时,使用 mmap+write 方式可以快速地将文件数据发送给用户,减少了服务器的负载,提升了用户体验。
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.2 sendfile
sendfile 是 Linux 系统中实现零拷贝的重要系统调用,它为文件到网络的数据传输提供了一种高效的方式。sendfile 最早在 Linux 2.1 版本中被引入,随着内核版本的不断更新,其功能和性能也在不断优化和提升。
在早期的 Linux 内核版本(如 2.1 - 2.3 版本)中,sendfile 系统调用已经能够实现将文件数据直接从内核缓冲区传输到 socket 缓冲区,避免了数据在用户空间的拷贝。当应用程序调用 sendfile 时,数据首先通过 DMA 技术从磁盘读取到内核的 PageCache 中,然后内核直接将 PageCache 中的数据拷贝到 socket 缓冲区,最后通过 DMA 将 socket 缓冲区的数据发送到网卡。这个过程减少了传统方式中数据从内核缓冲区到用户缓冲区,再从用户缓冲区到 socket 缓冲区的两次拷贝,提高了数据传输效率。
到了 Linux 2.4 版本,sendfile 系统调用引入了 SG - DMA(Scatter - Gather Direct Memory Access,分散 / 聚集直接内存访问)技术,实现了真正意义上的零拷贝。在这种模式下,当调用 sendfile 时,数据从磁盘读取到内核缓冲区后,不再需要将数据从内核缓冲区拷贝到 socket 缓冲区,而是通过 SG - DMA 技术,直接根据数据描述符(包含数据的位置和长度等信息)从内核缓冲区读取数据并发送到网卡。这使得数据传输过程中完全没有 CPU 参与的数据拷贝操作,进一步降低了 CPU 的使用率,提高了数据传输的速度和系统的吞吐量。
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.3 splice 和 tee
splice 和 tee 是 Linux 内核中另外两种实现零拷贝的方式,它们为数据在不同文件描述符之间的传输提供了更加灵活和高效的解决方案。
splice 系统调用允许在两个文件描述符之间直接传输数据,而无需经过用户空间。它通过在内核空间中建立一个管道缓冲区,实现了数据的高效流动。当使用 splice 进行数据传输时,数据可以从一个文件描述符(如文件、管道、socket 等)直接 “拼接” 到另一个文件描述符中。
具体来说,当调用 splice 时,内核会在两个文件描述符对应的内核缓冲区之间建立一个数据传输通道,数据通过 DMA 技术直接在这两个缓冲区之间传输,避免了数据在用户空间的拷贝和 CPU 的参与。这种方式非常适用于需要在不同设备或数据源之间进行数据传输的场景,比如将文件数据直接传输到 socket 进行网络发送,或者在两个文件之间进行数据复制等。例如,在一个网络代理服务器中,splice 可以用于将客户端发送的数据直接转发到目标服务器,提高数据转发的效率。
splice 函数在网络编程和文件处理等领域有着广泛的应用。在网络编程中,它可以用于实现高效的网络数据转发、文件上传下载等功能。在文件处理中,splice 可用于在不同文件描述符之间快速地移动数据,例如将一个大文件的内容快速地复制到另一个文件中。
以下是一个使用 splice 函数实现简单回显服务的代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>
int main(int argc, char **argv) {
if (argc <= 2) {
printf("usage: %s ip portn", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
// 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
// 设置端口复用
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
// 绑定套接字
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret!= -1);
// 监听连接
ret = listen(sock, 5);
assert(ret!= -1);
// 接受连接
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if (connfd < 0) {
printf("errno is: %sn", strerror(errno));
} else {
// 创建管道
int pipefd[2];
ret = pipe(pipefd);
assert(ret!= -1);
// 将客户端数据定向到管道
ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret!= -1);
// 将管道数据定向回客户端
ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret!= -1);
// 关闭连接
close(connfd);
}
// 关闭套接字
close(sock);
return 0;
}
在这段代码中,首先创建了一个 TCP 套接字,并将其绑定到指定的 IP 地址和端口进行监听。当有客户端连接时,接受客户端的连接请求。接着,创建一个管道,用于在客户端和服务器之间传输数据。通过两次调用 splice 函数,将客户端发送的数据通过管道回显给客户端。在这个过程中,数据直接在内核空间中通过管道和 socket 进行传输,避免了数据在用户空间的拷贝,提高了数据传输的效率。
通过使用 splice 函数实现回显服务,可以看到其在网络编程中的优势。与传统的数据传输方式相比,splice 减少了数据拷贝和上下文切换的开销,尤其在处理大量数据传输或高并发的网络请求时,能够显著提升网络应用的性能。它为开发者提供了一种高效、简洁的方式来实现数据的快速传输和处理,使得网络编程在数据传输方面更加高效和可靠。
tee 系统调用则主要用于在两个管道文件描述符之间复制数据,并且复制过程不会消耗数据,即源管道中的数据不会因为复制而被删除。这意味着数据可以同时被发送到多个目标,且不影响原来的数据流。tee 的工作原理是在内核空间中创建一个临时缓冲区,将源管道中的数据复制到这个缓冲区,然后再将缓冲区中的数据分别写入到目标管道中。这种特性使得 tee 非常适合用于日志记录和实时数据分析等场景。在一个分布式系统中,需要将某个关键数据的副本发送到多个不同的处理节点进行分析和处理,同时又要保证原始数据的完整性,此时就可以使用 tee 系统调用将数据复制到多个管道,分别发送到不同的节点,实现数据的多向传输和处理 。
05
零拷贝的应用场景
5.网络传输
在网络传输领域,零拷贝技术发挥着至关重要的作用,它为提升数据传输效率和网络性能带来了显著的优势。
在网络服务器中,零拷贝技术被广泛应用于加速文件传输和提高网络吞吐量。以高性能 Web 服务器 Nginx 为例,当用户请求下载一个静态文件时,Nginx 通过 sendfile 系统调用,借助零拷贝技术,将文件数据直接从磁盘的 PageCache 传输到网络 socket 缓冲区,然后发送给客户端。这个过程避免了数据在用户空间和内核空间之间的多次拷贝,减少了 CPU 的使用率和上下文切换次数,从而能够快速地响应大量的客户端请求,提高了服务器的并发处理能力和文件传输速度。在一个高并发的电商网站中,大量用户同时请求下载商品图片、文档等静态资源,Nginx 利用零拷贝技术,可以轻松应对这些请求,确保用户能够快速获取所需资源,提升了用户体验。
CDN(Content Delivery Network,内容分发网络)内容分发网络也是零拷贝技术的重要应用场景。CDN 的主要任务是将内容(如视频、音频、网页等)快速分发给分布在不同地理位置的用户。在 CDN 节点中,当需要将缓存的内容发送给用户时,零拷贝技术可以减少数据传输的时间和资源消耗。通过零拷贝,数据可以直接从磁盘或内存缓存中传输到网络,提高了内容分发的速度和效率,确保用户能够流畅地观看视频、浏览网页等,减少了卡顿和加载时间。在视频流媒体服务中,CDN 利用零拷贝技术,将视频内容快速传输给用户,保证了视频播放的流畅性,让用户能够享受高质量的观看体验。
5.2文件处理
在文件处理方面,零拷贝技术同样展现出了强大的性能优势,它能够显著减少 I/O 操作,提升文件处理的效率。
在文件读写场景中,传统的文件读写方式通常需要多次数据拷贝,导致 I/O 性能较低。而零拷贝技术通过 mmap 或 sendfile 等方式,可以实现数据的高效读写。使用 mmap 将文件映射到内存后,应用程序可以直接通过内存操作来读写文件,避免了传统的 read 和 write 系统调用带来的数据拷贝开销。在处理大文件时,这种方式可以大大提高文件的读写速度,减少 I/O 等待时间。在日志处理系统中,大量的日志数据需要频繁地写入磁盘,使用零拷贝技术可以快速地将日志数据写入文件,提高了日志处理的效率。
在数据库备份场景中,零拷贝技术也发挥着重要作用。数据库备份通常涉及大量数据的读取和传输,如果采用传统方式,会消耗大量的时间和资源。利用零拷贝技术,数据库可以直接将数据从磁盘传输到备份存储设备或网络,减少了数据在内存中的拷贝次数,提高了备份的速度和效率。在一些大型数据库系统中,每天都需要进行全量或增量备份,使用零拷贝技术可以大大缩短备份时间,减少对数据库正常运行的影响。
06
java提供的零拷贝方式
6.1 Java NIO对mmap的支持
Java NIO有一个MappedByteBuffer的类,可以用来实现内存映射。它的底层是调用了Linux内核的mmap的API。
public class MmapTest { public static void main(String[ ] args) { try { FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ); MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40); FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); //数据传输 writeChannel.write(data); readChannel.close(); writeChannel.close(); }catch (Exception e){ System.out.println(e.getMessage()); } }}
6.2Java NIO对sendfile的支持
FileChannel的transferTo()/transferFrom(),底层就是sendfile() 系统调用函数。Kafka 这个开源项目就用到它,平时面试的时候,回答面试官为什么这么快,就可以提到零拷贝sendfile这个点。
public class SendFileTest { public static void main(String[ ] args) { try { FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ); long len = readChannel.size(); long position = readChannel.position(); FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); //数据传输 readChannel.transferTo(position, len, writeChannel); readChannel.close(); writeChannel.close(); } catch (Exception e) { System.out.println(e.getMessage()); } }}
6.3案例分析
使用 Java NIO 中的零拷贝技术的案例分析及代码实现示例,用于将一个文件从磁盘读取并通过网络发送给客户端。
传统方式的问题
在传统的文件传输方式中,当从磁盘读取文件并通过网络发送时,通常需要进行多次数据拷贝。首先,数据从磁盘读取到内核缓冲区,然后从内核缓冲区拷贝到用户空间缓冲区,接着当要通过网络发送时,又要从用户空间缓冲区拷贝到内核的 socket 缓冲区,最后从内核 socket 缓冲区发送到网络。这不仅消耗了大量的 CPU 时间和内存带宽,还增加了数据传输的延迟。
例如,在一个简单的文件服务器应用中,如果使用传统的方式处理大量的文件传输请求,服务器的 CPU 可能会因为频繁的数据拷贝而负载过高,导致响应其他请求变慢,同时也会影响文件传输的速度和效率。
零拷贝的优势
零拷贝技术可以避免这些不必要的数据拷贝操作。在 Java 中,可以使用FileChannel的transferTo方法来实现零拷贝。通过这种方式,数据可以直接从磁盘文件读取到网络通道,减少了数据在用户空间和内核空间之间的来回拷贝,从而提高了传输效率,降低了 CPU 使用率和延迟。
对于大规模文件传输场景,如视频文件分发、大文件下载服务等,零拷贝技术可以显著提高系统的性能和吞吐量,能够更快地响应客户端请求,同时减少服务器资源的消耗。
代码实现:
import java.io.FileInputStream;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.channels.FileChannel;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;public class ZeroCopyFileServer { public static void main(String[] args) throws IOException { // 监听端口 int port = 8888; ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(port)); while (true) { // 等待客户端连接 SocketChannel socketChannel = serverSocketChannel.accept(); // 打开文件并获取FileChannel String filePath = "your_file_path_here"; // 替换为实际文件路径 try (FileInputStream fileInputStream = new FileInputStream(filePath); FileChannel fileChannel = fileInputStream.getChannel()) { // 使用零拷贝将文件数据传输到网络通道 long bytesTransferred = fileChannel.transferTo(0, fileChannel.size(), socketChannel); System.out.println("Transferred " + bytesTransferred + " bytes."); } // 关闭连接 socketChannel.close(); } }}
首先,创建了一个ServerSocketChannel并绑定到指定端口8888,用于监听客户端连接。
在循环中,接受客户端连接后,打开要传输的文件,获取FileChannel。
然后,使用FileChannel的transferTo方法将文件数据直接传输到SocketChannel(代表与客户端的连接通道),实现了零拷贝的数据传输。
最后,关闭与客户端的连接,等待下一个客户端连接。
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...