
在现代计算中,中央处理器(CPU)的速度常常超过其与外部世界交互的能力。这就产生了一个根本性的瓶颈:如何在不让 CPU 承担缓慢、重复的拷贝任务的情况下,在内存和 I/O 设备(如网卡和硬盘)之间移动大量数据。虽然基本的直接内存访问(DMA)通过委托传输提供了一个部分解决方案,但它难以应对现代操作系统中物理内存的碎片化特性。本文通过深入探讨分散-聚集 DMA 来应对这一挑战,这是一种与虚拟内存系统协调工作的复杂硬件机制。读者将首先探索其核心原理,追溯从简单 I/O 到分散-聚集列表的复杂机制及其性能权衡的演进过程。随后,本文将展示该技术在不同应用中的深远影响,揭示分散-聚集 DMA 如何成为高性能计算的基石。
要领会像分散-聚集 DMA 这样一种机制的精妙之处,我们必须首先理解它所巧妙解决的问题。让我们开启一段旅程,从计算机移动数据的最简单方式开始,看看新的挑战如何一步步迫使我们发明更复杂的解决方案,最终达到分散-聚集 I/O 这种软硬件优美协作的境界。
想象一下,您计算机的中央处理器(CPU)是一位才华横溢的大学教授。它能以闪电般的速度处理抽象思维,每秒执行数十亿次复杂的计算。现在,想象一下让这位教授花一天时间把箱子从图书馆搬到送货卡车上。这就是简单 I/O(输入/输出)操作的本质。将数据从网卡或硬盘移动到内存是一项简单、重复且对 CPU 来说极其缓慢的任务。
最基本的方法,称为编程 I/O (PIO),正是迫使 CPU 这样做。对于每一个字节或字的数据,CPU 都必须从设备中读取它,然后写入内存。这是巨大的才能浪费。当 CPU 忙于扮演搬运工的角色时,它并没有在做它被设计来从事的高级计算工作。系统的整体性能急剧下降,不是因为教授慢,而是因为他正忙于一项低技能的工作。
显而易见的解决方案是委托。教授不必亲自搬运,而是可以雇佣一个专门的助手,其唯一的工作就是搬运箱子。在计算机中,这个助手被称为直接内存访问(DMA)控制器。
使用基本 DMA,CPU 的角色从搬运工变成了管理者。它给 DMA 控制器一个简单的指令:“请将这么多字节的数据从这个设备地址移动到这个内存地址。”然后 CPU 就回去做它重要的工作,而 DMA 控制器则处理整个传输过程。任务完成后,DMA 控制器可以通过中断的方式通知 CPU。这是效率上的一次巨大飞跃。教授可以自由地思考,而助手则处理后勤工作。
然而,即使是委托也有其成本。在 DMA 传输开始之前,CPU 必须花费少量时间进行设置——将源地址、目标地址和传输大小写入 DMA 控制器的寄存器。这个设置会产生一个固定的延迟,一种启动传输的“文书工作”成本。我们称这个设置时间为 。
这就带来了一个有趣而实际的权衡。如果你只需要移动非常少量的数据怎么办?花设置时间去委托值得吗?DMA 传输的总时间是设置时间加上实际的数据传输时间:,其中 是数据大小, 是 DMA 带宽。CPU 自己完成所需的时间仅仅是 ,其中 是 CPU 自身的内存拷贝带宽。
为了让 DMA 物有所值,我们需要 。对于非常小的 值,固定的设置成本 可能占主导地位,使得 CPU 更快。存在一个盈亏平衡负载大小 ,在该大小下两种方法耗时完全相同。只有当传输量大于 时,DMA 的真正好处——其更高的潜在带宽和解放 CPU——才开始显现。这就像雇佣一家专业的搬家公司;搬一把椅子到房间另一头是小题大做,但搬整个家却是必不可少的。
我们简单的 DMA 模型有一个隐藏且相当苛刻的假设:内存目标是一个单一、巨大的、物理上连续的块。但在现代计算机中,找到一大块未使用的、连续的物理内存,就像在繁华的市中心找到一个完全空置的停车场一样困难。
现代操作系统使用一种称为分页的技术来管理内存。当一个程序请求一个大缓冲区时,操作系统会给它一个在所谓虚拟地址空间中连续块的错觉。实际上,操作系统会在任何可用的地方找到小的、标准大小的物理内存空闲块(称为帧),并将程序的虚拟页面映射到这些物理上分散的帧上。这种映射关系存储在页表中,它就像一个目录,告诉 CPU 如何为任何给定的虚拟地址找到物理数据。
这对我们简单的 DMA 控制器来说是一个危机。它不知道页表;它只理解物理地址。要传输一个被操作系统分割成三个不相邻的 4 KiB 物理帧的 12 KiB 缓冲区,CPU 能做什么?唯一的选择是退回到昂贵的拷贝操作:CPU 必须首先分配一个新的、12 KiB 的物理上连续的缓冲区(一个“中转缓冲区”),将数据从三个分散的用户帧中拷贝进去,然后才能请求简单的 DMA 控制器执行传输。这让我们又回到了最初的问题:CPU 再次背负了大量的拷贝工作,完全违背了使用 DMA 的初衷。
这就是分散-聚集 DMA 作为我们故事中英雄登场的地方。它代表了更深层次的委托。CPU不再告诉 DMA 控制器“移动一个大块”,而是可以给它一个指令列表。这个列表,被称为分散-聚集列表或描述符链,可能看起来像这样:
DMA 控制器现在足够智能,可以处理这个列表。对于出向传输,它从这些分散的内存位置读取(聚集)数据,并将其作为一个单一、无缝的流发送到设备。对于入向传输,它从设备获取一个连续的流,并将其写入(分散)到指定的物理内存片段中。
这种机制是分页式虚拟内存系统的完美搭档。操作系统不再需要创建物理上连续的拷贝。它只需查询其页表,找出缓冲区的虚拟页面在物理上的位置,并根据这些信息构建一个分散-聚集列表。CPU 的工作从完整的数据拷贝简化为仅仅准备一个包含地址和长度的短列表。操作系统内存管理与硬件 I/O 能力之间的这种协同作用是现代系统性能的基石。
分散-聚集 DMA 的威力是不可否认的,但其真正的效率取决于一系列微妙而有趣的权衡。分散-聚集列表本身不是没有成本的;每个条目或描述符都会增加开销。一次传输的总时间是所有描述符处理延迟的总和加上实际数据移动的时间。最小化总时间是一门艺术。
让我们考虑一个需要传输一组用户缓冲区的场景。在理想情况下,我们会为每个缓冲区创建一个描述符。CPU 成本只是设置这些描述符的时间。但如果缓冲区不能完美地符合硬件的规则,例如,要求所有传输都必须在 64 字节边界上开始,该怎么办?
如果一个缓冲区未对齐,操作系统可能被迫分别处理其未对齐的头部和尾部。一种常见的技术是使用中转缓冲区:CPU 将小的、未对齊的两端拷贝到临时的、对齐的缓冲区中,并为它们创建单独的 DMA 描述符。一个逻辑上的缓冲区可能变成三个物理上的 DMA 段:拷贝的头部、原始的对齐的中间部分,以及拷贝的尾部。这使得描述符的数量增加了两倍!
在这里我们面临一个关键的权衡。一方面,我们做了一点 CPU 拷贝工作。另一方面,我们极大地增加了描述符的数量。如果每个描述符的设置成本很高,这种描述符数量的激增可能使得“优化后”的分散-聚集传输的总 CPU 开销甚至高于将整个数据集拷贝到单个大缓冲区中的成本。分散-聚集 DMA 的性能是设置成本和拷贝成本之间的微妙平衡。
当数据段在物理上彼此接近但并非完全连续时,就出现了另一个优化机会。想象两个段被一个小的、未使用的间隙隔开。DMA 控制器应该使用两个独立的描述符,产生两次设置开销吗?还是应该使用一个覆盖两个段以及无用间隙的单一描述符,传输一些垃圾数据但节省一次设置成本?
答案在于对时间的简单比较。设描述符开销为 ,总线带宽为 。省掉一个描述符所节省的时间是 。传输大小为 的间隙所浪费的时间是 。仅当浪费的时间小于节省的时间时,将传输合并为单个描述符才是有利的:
这给了我们一个清晰的合并阈值,。如果间隙小于这个阈值,传输垃圾数据会更快。这表明,可以为 DMA 引擎编程,使用简单而强大的启发式方法来即时优化其自身的操作。
最后,让我们考虑分散-聚集列表本身。操作系统应该如何在内存中安排这个描述符列表?是作为一个简单的、连续的数组(通常称为环形缓冲区),还是作为一个链表,其中每个描述符都包含一个指向下一个的指针?这似乎是一个微不足道的软件细节,但它对硬件有深远的影响。
关键在于预取。为了隐藏从主内存获取描述符的延迟(),智能的 DMA 引擎希望在实际需要下一个描述符之前很久就请求它。
使用环形缓冲区,所有描述符的地址都是可预测的。如果引擎正在处理描述符 ,它知道下一个在 address_of_i + size_of_descriptor。因此,它可以并行发出多个获取请求,创建一个流水线。如果硬件可以处理 个并行获取,它就可以有效地将内存延迟分摊,将等待一个描述符的平均时间减少到 。
使用链表,描述符 的地址被锁在描述符 内部。引擎直到当前描述符到达并被处理后,才能开始获取下一个描述符。这种串行依赖完全破坏了预取流水线。等待每一个描述符的时间都是完整的内存延迟 。
开销的差异是惊人的 。这是一个深刻的计算机系统原理的优美例证:硬件的性能并非独立于与之交互的软件数据结构。一个简单的选择既可以释放硬件的潜力,也可以扼杀它。
到目前为止,我们的模型大都是确定性的,假设时间和速率是固定的。但真实世界是一个混乱的、随机的地方。内存总线是共享资源,我们的 DMA 传输必须与其他设备竞争。这种争用会引入随机延迟,导致相同传输的完成时间各不相同。这种变化被称为抖动。
我们可以使用排队论的工具来分析这种抖动。通过将总线建模为一个简单的队列(例如,一个 M/M/1 队列),我们不仅可以推导出平均完成时间,还可以推导出其方差 。对于一个到达率为 、服务率为 的系统,这个方差结果是 。
我们为什么要关心方差?因为系统的稳定性通常取决于可预测性。考虑中断节制,这是一个网卡为了避免让 CPU 不堪重负而采用的功能,它只在一批 个数据包被接收后才产生单个中断。操作系统依赖于这个中断以某种规律的间隔到达。但是接收 个数据包的时间是 个独立的、随机的完成时间之和。每个完成时间的高方差会导致中断间隔的高方差,使系统的行为变得不稳定。
通过理解单次传输的统计数据,我们可以控制批次的统计数据。 次传输的中断间隔的变异系数(标准差除以平均值)是 。如果我们需要确保这个变化低于某个目标,比如说 ,我们可以计算所需的最小批次大小 ()。这是一个绝佳的例子,说明了理解硬件机制的基本、底层的统计行为,如何使我们能够在操作系统中设计出健壮的、高层的策略。归根结底,分散-聚集 DMA 不仅仅是移动字节的机制;它是现代计算机复杂、概率性交响乐中的一个基本组成部分。
掌握了分散-聚集 DMA 的精妙原理——即告知硬件移动什么数据,而无需逐块指定如何移动它的艺术——我们现在可以踏上一段旅程,看看这个简单而强大的思想将我们带向何方。它不仅仅是隐藏在设备驱动程序中的一个技术优化;它是一个基本概念,回响在计算机科学与工程的许多领域。它的美在于能够为原本复杂的数据处理问题带来效率和简洁性,将中央处理器(CPU)解放出来,专注于更有趣的工作。
分散-聚集 DMA 最直接、最深远的应用是在现代操作系统的核心:输入/输出(I/O)处理。想象一下你的应用程序想要向网卡或硬盘写入一大块数据。在传统系统中,这是一个令人惊讶的繁琐过程。出于安全考虑,操作系统不能简单地让硬件设备访问你应用程序的私有内存。因此,CPU 必须首先介入,将你的数据从应用程序的缓冲区拷贝到操作系统内核内存的一个指定区域。但故事可能还没有结束。如果硬件设备要求数据位于一个单一的、物理上连续的内存块中,而内核的缓冲区恰好被分割成多个物理页面,CPU 可能需要执行另一次拷贝,这次是从碎片化的内核缓冲区拷贝到一个特殊的、物理上连续的“中转缓冲区”。只有这样,到设备的 DMA 传输才能最终开始。
这是一条充满低效的路径。我们最宝贵的计算资源——CPU,将其时间花费在执行单调、重复的拷贝操作上。分散-聚集 DMA 为摆脱这种苦差事提供了一种惊人优雅的方法。通过分散-聚集,操作系统可以简单地识别出持有应用程序数据的物理内存页面列表——无论它们如何分散——并将这个列表直接交给 DMA 引擎。然后,硬件会智能地从每个位置获取数据,并即时组装它们,就好像它们是一个单一的、连续的块一样。这种“零拷贝”方法消除了中间由 CPU 驱动的拷贝,从而显著提高了吞吐量并降低了 CPU 负载。数据直接从其源头流向目的地,这证明了将工作委托给最擅长此项工作的专用硬件的力量。
这种“零拷贝”理念在高性能网络和存储中找到了它的天然归宿。考虑一个向客户端发送响应的 Web 服务器。最终的数据包是一个复合对象:由内核生成的 TCP/IP 头部、来自应用程序的 HTTP 头部,以及实际内容(可能是一大块从磁盘读取的文件)。天真的方法是分配一个缓冲区,并在发送前费力地将这些部分一一拷贝进去。分散-聚集 I/O 允许一种远为动态和高效的方法。系统可以创建一个描述符列表,指向这些分散的内存片段——一个用于 TCP 头部,一个用于 HTTP 头部,以及一个或多个用于驻留在系统页面缓存中的文件数据。网络接口控制器(NIC)然后直接从内存中收集这些片段,并将它们作为单个、连贯的数据包传输。这在数字世界里,就如同厨师直接从各个容器中取用食材来摆盘,而完全不需要一个中央搅拌碗 [@problemİD:3663017]。
这一原则对于现代分布式系统的支柱——远程过程调用(RPC)也至关重要。在 RPC 中发送大型数据负载时,我们希望避免拷贝。使用分散-聚集,系统可以将包含负载的用户空间页面钉在内存中,让 NIC 直接从中读取。然而,现实世界引入了有趣的限制。一个 NIC 可能只支持每次传输有限数量的描述符。如果一个大的、未对齐的缓冲区跨越了太多的页面,零拷贝传输可能无法实现,迫使系统退回到旧的拷贝方法。此外,为了确保数据在传输过程中不被应用程序修改(一种“检查时-使用时”风险),操作系统必须暂时将内存页面标记为只读。这揭示了硬件能力、操作系统内存管理和软件协议语义要求之间美妙的相互作用。
在存储领域,尤其随着高速固态硬盘(SSD)的出现,分散-聚集 DMA 与设备智能强大地协同工作。像 NVMe 这样的现代存储协议允许操作系统同时提交大量独立的 I/O 请求。分散-聚集 DMA 正是让操作系统能够高效地构建这些请求批次的机制。设备接收到一长串命令队列后,可以智能地重新排序它们以优化其内部操作——例如,将对邻近闪存块的写入操作组合在一起。这种并行性有效地隐藏了设备固有的随机访问延迟。我们能保持在途中的命令越多(最多可达设备的队列深度 ),我们为每个独立操作所承受的延迟比例就越小。在理想化模型中,被隐藏的延迟比例接近 。这表明,主机端的内存访问策略(分散-聚集)如何释放了复杂的设备端调度策略的全部潜力。
到目前为止,我们已经将分散-聚集 DMA 视为一种从非连续源移动批量数据的工具。但它的能力可以远比这更微妙和结构化,特别是与更先进的 DMA 引擎结合时。想象一个不仅理解地址和长度,还理解“步幅”(数据块之间的固定距离)的 DMA 引擎。突然之间,DMA 引擎不再仅仅是数据的搬运工,更是数据的雕刻家。
这种能力对科学计算和图形学具有变革性意义。考虑矩阵转置这个基本操作。一个以行主序存储的矩阵,其列元素在内存中相距甚远。要读取一列,必须以等于一整行宽度的步幅跨越内存。一个支持跨步的 DMA 引擎可以被编程来精确地完成这个任务。通过将分散-聚集描述符链接在一起,每个描述符都被编程为以正确的步幅读取一列的一部分,整个矩阵可以在最少的 CPU 干预下完成转置。这将一个复杂、对缓存不友好的 CPU 任务转变为一个高效、被卸载的硬件操作。
这种选择性、稀疏传输的思想在操作系统检查点和虚拟化中找到了另一个强大的应用。为了创建容错备份或对虚拟机进行实时迁移,我们必须对其内存进行快照。拷贝整个内存区域——通常是数 GB 大小——是缓慢且浪费的,因为其中大部分可能自上次快照以来并未改变。一个远为优雅的解决方案是使用 CPU 内存管理单元维护的“脏页位图”。这个位图精确地告诉我们哪些页面被修改过。操作系统可以扫描这个位图,并构建一个只引用脏页的分散-聚集描述符列表。然后,DMA 引擎只拷贝必要的数据,跳过大片未改变的内存区域。这将密集的拷贝操作转变为稀疏操作,使得像实时迁移这样的过程变得可行。
我们现在正进入一个新的前沿,在这里,分散-聚集 DMA 不仅仅是一种优化,而是一种促成新型计算机架构的使能技术。“智能网卡”(Smart NICs)和数据处理单元(DPUs)的兴起就是这一点的证明。这些设备拥有可编程的 DMA 引擎,它们不仅移动数据,还能对数据执行计算。例如,一个智能网卡可以被编程来检查传入的网络数据包,计算其密钥的哈希值,并使用分散-聚集 DMA 将数据包的有效负载直接写入主机内存中哈希表的正确桶中。为了防止 CPU 读取到一个被部分写入的桶,设备可以实现一个同步协议,在其 DMA 突发传输前后写入一个“版本号”。这不仅卸载了数据移动,还卸载了数据处理流水线的重要部分,这是朝着构建下一代数据中心迈出的关键一步。
但与所有强大的工具一样,也存在微妙的成本和权衡。虽然分散-聚集 DMA 使 CPU 免于拷贝,但它可能增加内存访问模式的复杂性。从许多分散位置访问数据可能会给 CPU 的转译后备缓冲器(TLB)带来压力,TLB是存储最近虚拟到物理地址转换的缓存。处理一个由 个小的、随机对齐的缓冲区组成的有效负载,可能比处理一个大的、连续的缓冲区导致明显更多的 TLB 未命中。这是一个系统级权衡的绝佳例子:我们减少了一个瓶颈(CPU 拷贝开销),但可能略微增加了另一个瓶颈(内存翻译开销)。
也许最深刻的联系,是在我们考虑具有多个独立 DMA 引擎并发操作的系统时揭示出来的。想象两个引擎, 和 ,负责写入共享数据结构的不同部分,之后必须根据组合结果写入最终的头部。在一个没有严格设备间内存排序保证的世界里,可能会出现混乱。 可能在 之前很久就完成了它的写入,而第三方可能会根据不完整的数据过早地写入头部。我们如何才能在没有持续 CPU 干预的情况下协调这场舞蹈?解决方案就在 DMA 描述符本身。我们可以设计一个协议,让引擎通过共享内存标志直接通信。例如, 在完成其写入后,执行一个特殊的“栅栏”描述符以确保其数据全局可见,然后在内存中设置一个标志。 在其自身写入后,执行一个“等待”描述符,轮询该标志。只有当它看到 设置的标志时,它才继续写入最终的头部。这是一场由并发硬件演奏的宏伟交响乐,其指挥不是 CPU,而是描述符列表本身。它表明,分散-聚集 DMA 在其最先进的形式中,是用于编程并发硬件的一种语言,是从头开始构建复杂、可靠和高性能系统的工具。
从一个简单的优化到现代系统设计的基石,分散-聚集 DMA 揭示了计算机科学固有的美和统一性——一个关于内存寻址的优雅思想,其影响可以波及到从操作系统、网络到科学计算,乃至并发编程本质的一切事物。