DMA全称为Direct Memory Access,即直接内存访问。意思是外设对内存的读写过程可以不用CPU参与而直接进行。我们先来看一下没有DMA的时候:

假设I/O设备为一个普通网卡,为了从内存拿到需要发送的数据,然后组装数据包发送到物理链路上,网卡需要通过总线告知CPU自己的数据请求。然后CPU将会把内存缓冲区中的数据复制到自己内部的寄存器中,再复制到I/O设备的存储空间中。如果数据量比较大,那么很长一段时间内CPU都会忙于搬移数据,而无法投入到其他工作中去。
CPU的最主要工作是计算,而不是进行数据复制,这种工作属于白白浪费了它的计算能力。为了给CPU“减负”,让它投入到更有意义的工作中去,后来人们设计了DMA机制:

可以看到总线上又挂了一个DMA控制器,它是专门用来读写内存的设备。有了它以后,当我们的网卡想要从内存中拷贝数据时,除了一些必要的控制命令外,整个数据复制过程都是由DMA控制器完成的。过程跟CPU复制是一样的,只不过这次是把内存中的数据通过总线复制到DMA控制器内部的寄存器中,再复制到I/O设备的存储空间中。CPU除了关注一下这个过程的开始和结束以外,其他时间可以去做其他事情。
DMA控制器一般是和I/O设备在一起的,也就是说一块网卡中既有负责数据收发的模块,也有DMA模块。
RDMA( Remote Direct Memory Access )意为远程直接地址访问,通过RDMA,本端节点可以“直接”访问远端节点的内存。所谓直接,指的是可以像访问本地内存一样,绕过传统以太网复杂的TCP/IP网络协议栈读写远端内存,而这个过程对端是不感知的,而且这个读写过程的大部分工作是由硬件而不是软件完成的。
为了能够直观的理解这一过程,请看下面两个图(图中箭头仅做示意,不表示实际逻辑或物理关系):

传统网络中,“节点A给节点B发消息”实际上做的是“把节点A内存中的一段数据,通过网络链路搬移到节点B的内存中”,而这一过程无论是发端还是收段,都需要CPU的指挥和控制,包括网卡的控制,中断的处理,报文的封装和解析等等。
上图中左边的节点在内存用户空间中的数据,需要经过CPU拷贝到内核空间的缓冲区中,然后才可以被网卡访问,这期间数据会经过软件实现的TCP/IP协议栈,加上各层头部和校验码,比如TCP头,IP头等。网卡通过DMA拷贝内核中的数据到网卡内部的缓冲区中,进行处理后通过物理链路发送给对端。
对端收到数据后,会进行相反的过程:从网卡内部存储空间,将数据通过DMA拷贝到内存内核空间的缓冲区中,然后CPU会通过TCP/IP协议栈对其进行解析,将数据取出来拷贝到用户空间中。
可以看到,即使有了DMA技术,上述过程还是对CPU有较强的依赖。
而使用了RDMA技术之后,这一过程可以简单的表示成下面的示意图:

RDMA主要应用在高性能计算(HPC)领域和大型数据中心当中,并且设备相对普通以太网卡要昂贵不少(比如Mellanox公司的Connext-X 5 100Gb PCIe网卡市价在4000元以上)。由于使用场景和价格的原因,RDMA与普通开发者和消费者的距离较远,目前主要是一些大型互联网企业在部署和使用。
RDMA技术为什么可以应用在上述场景中呢?这就涉及到它的以下几个特点:

由于Linux等操作系统将内存划分为用户空间和内核空间,在传统的Socket通信流程中CPU需要多次把数据在内存中来回拷贝。而通过RDMA技术,我们可以直接访问远端已经注册的内存区域。
从Linux系统上看,除了引导系统的BIN区,整个内存空间主要被分成两个部分:内核空间(Kernel space)、用户空间(User space)。“用户空间”和“内核空间”的空间、操作权限以及作用都是不一样的。内核空间是Linux自身使用的内存空间,主要提供给程序调度、内存分配、连接硬件资源等程序逻辑使用;用户空间则是提供给各个进程的主要空间。用户空间不具有访问内核空间资源的权限,因此如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成:从用户空间切换到内核空间,然后在完成相关操作后再从内核空间切换回用户空间。
Linux 中零拷贝技术的实现方向
① 直接 I/O:对于这种数据传输方式来说,应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,但是硬件上的数据不会拷贝一份到内核空间,而是直接拷贝至了用户空间,因此直接I/O不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。
② 在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝
③ copy-on-write(写时复制技术):在某些情况下,Linux操作系统的内核空间缓冲区可能被多个应用程序所共享,操作系统有可能会将用户空间缓冲区地址映射到内核空间缓存区中。当应用程序需要对共享的数据进行修改的时候,才需要真正地拷贝数据到应用程序的用户空间缓冲区中,并且对自己用户空间的缓冲区的数据进行修改不会影响到其他共享数据的应用程序。所以,如果应用程序不需要对数据进行任何修改的话,就不会存在数据从系统内核空间缓冲区拷贝到用户空间缓冲区的操作。
零拷贝机制的原理
下面我们通过一个Java非常常见的应用场景:将系统中的文件发送到远端(该流程涉及:磁盘上文件 ——> 内存(字节数组) ——> 传输给用户/网络)来详细展开传统I/O操作和通过零拷贝来实现的I/O操作。
传统I/O
① 发出read系统调用:导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将文件中的数据从磁盘上读取到内核空间缓冲区(第一次拷贝: hard drive ——>kernel buffer)。
② 将内核空间缓冲区的数据拷贝到用户空间缓冲区(第二次拷贝: kernel buffer ——>user buffer),然后read系统调用返回。而系统调用的返回又会导致一次内核空间到用户空间的上下文切换(第二次上下文切换)。
③ 发出write系统调用:导致用户空间到内核空间的上下文切换(第三次上下文切换)。将用户空间缓冲区中的数据拷贝到内核空间中与socket相关联的缓冲区中(即,第②步中从内核空间缓冲区拷贝而来的数据原封不动的再次拷贝到内核空间的socket缓冲区中。)(第三次拷贝: user buffer ——>socket buffer)。
④ write系统调用返回,导致内核空间到用户空间的再次上下文切换(第四次上下文切换)。通过DMA引擎将内核缓冲区中的数据传递到协议引擎(第四次拷贝: socket buffer ——>protocol engine),这次拷贝是一个独立且异步的过程。
Q:你可能会问独立和异步这是什么意思?难道是调用会在数据被传输前返回?
A:事实上调用的返回并不保证数据被传输;它甚至不保证传输的开始。它只是意味着将我们要发送的数据放入到了一个待发送的队列中,在我们之前可能有许多数据包在排队。除非驱动器或硬件实现优先级环或队列,否则数据是以先进先出的方式传输的。总的来说,传统的I/O操作进行了4次用户空间与内核空间的上下文切换,以及4次数据拷贝。其中4次数据拷贝中包括了2次DMA拷贝和2次CPU拷贝。
Q: 传统I/O模式为什么将数据从磁盘读取到内核空间缓冲区,然后再将数据从内核空间缓冲区拷贝到用户空间缓冲区了?为什么不直接将数据从磁盘读取到用户空间缓冲区就好?
A: 传统I/O模式之所以将数据从磁盘读取到内核空间缓冲区而不是直接读取到用户空间缓冲区,是为了减少磁盘I/O操作以此来提高性能。因为OS会根据局部性原理在一次read()系统调用的时候预读取更多的文件数据到内核空间缓冲区中,这样当下一次read()系统调用的时候发现要读取的数据已经存在于内核空间缓冲区中的时候只要直接拷贝数据到用户空间缓冲区中即可,无需再进行一次低效的磁盘I/O操作(注意:磁盘I/O操作的速度比直接访问内存慢了好几个数量级)。
Q: 既然系统内核缓冲区能够减少磁盘I/O操作,那么我们经常使用的BufferedInputStream缓冲区又是用来干啥的?
A: BufferedInputStream的作用是会根据情况自动为我们预取更多的数据到它自己维护的一个内部字节数据缓冲区中,这样做能够减少系统调用的次数以此来提供性能。总的来说内核空间缓冲区的一大用处是为了减少磁盘I/O操作,因为它会从磁盘中预读更多的数据到缓冲区中。而BufferedInputStream的用处是减少“系统调用”。
DMA
DMA(Direct Memory Access) ———— 直接内存访问 :DMA是允许外设组件将I/O数据直接传送到主存储器中并且传输不需要CPU的参与,以此将CPU解放出来去完成其他的事情。
而用户空间与内核空间之间的数据传输并没有类似DMA这种可以不需要CPU参与的传输工具,因此用户空间与内核空间之间的数据传输是需要CPU全程参与的。所有也就有了通过零拷贝技术来减少和避免不必要的CPU数据拷贝过程。
通过sendfile实现的零拷贝I/O
① 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。然后再将数据从内核空间缓冲区拷贝到内核中与socket相关的缓冲区中(第二次拷贝: kernel buffer ——> socket buffer)。
② sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)总的来说,通过sendfile实现的零拷贝I/O只使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。
Q:但通过是这里还是存在着一次CPU拷贝操作,即,kernel buffer ——> socket buffer。是否有办法将该拷贝操作也取消掉了?
A:有的。但这需要底层操作系统的支持。从Linux 2.4版本开始,操作系统底层提供了scatter/gather这种DMA的方式来从内核空间缓冲区中将数据直接读取到协议引擎中,而无需将内核空间缓冲区中的数据再拷贝一份到内核空间socket相关联的缓冲区中。带有DMA收集拷贝功能的sendfile实现的I/O
从Linux 2.4版本开始,操作系统底层提供了带有scatter/gather的DMA来从内核空间缓冲区中将数据读取到协议引擎中。这样一来待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。那么从文件中读出的数据就根本不需要被拷贝到socket缓冲区中去,只是需要将缓冲区描述符添加到socket缓冲区中去,DMA收集操作会根据缓冲区描述符中的信息将内核空间中的数据直接拷贝到协议引擎中。
① 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
② 没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket缓冲区当中。该描述符包含了两方面的信息:a)kernel buffer的内存地址;b)kernel buffer的偏移量。
③ sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(第二次拷贝: kernel buffer ——> protocol engine),这样就避免了最后一次CPU数据拷贝。总的来说,带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。
关于sendfile:
#include <sys/sendfile.h> ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);在linux2.6.33版本之前 sendfile指支持文件到套接字之间传输数据,即in_fd相当于一个支持mmap的文件,out_fd必须是一个socket。但从linux2.6.33版本开始,out_fd可以是任意类型文件描述符。所以从linux2.6.33版本开始sendfile可以支持“文件到文件”和“文件到套接字”之间的数据传输。
“传统I/O” VS “sendfile零拷贝I/O”
- 传统I/O通过两条系统指令read、write来完成数据的读取和传输操作,以至于产生了4次用户空间与内核空间的上下文切换的开销;而sendfile只使用了一条指令就完成了数据的读写操作,所以只产生了2次用户空间与内核空间的上下文切换。
- 传统I/O产生了2次无用的CPU拷贝,即内核空间缓存中数据与用户空间缓冲区间数据的拷贝;而sendfile最多只产出了一次CPU拷贝,即内核空间内之间的数据拷贝,甚至在底层操作体系支持的情况下,sendfile可以实现零CPU拷贝的I/O。
- 因传统I/O用户空间缓冲区中存有数据,因此应用程序能够对此数据进行修改等操作;而sendfile零拷贝消除了所有内核空间缓冲区与用户空间缓冲区之间的数据拷贝过程,因此sendfile零拷贝I/O的实现是完成在内核空间中完成的,这对于应用程序来说就无法对数据进行操作了。
Q:对于上面的第三点,如果我们需要对数据进行操作该怎么办了?
A:Linux提供了mmap零拷贝来实现我们的需求。通过mmap实现的零拷贝I/O
mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。
① 发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
② mmap系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间。因为用户空间和内核空间共享了这个缓冲区数据,所以用户空间就可以像在操作自己缓冲区中数据一般操作这个由内核空间共享的缓冲区数据。
③ 发出write系统调用,导致用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket相关联的缓冲区(第二次拷贝: kernel buffer ——> socket buffer)。
④ write系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)总的来说,通过mmap实现的零拷贝I/O进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。
FileChannel与零拷贝
FileChannel中大量使用了我们上面所提及的零拷贝技术。
FileChannel的map方法会返回一个MappedByteBuffer。MappedByteBuffer是一个直接字节缓冲器,该缓冲器的内存是一个文件的内存映射区域。map方法底层是通过mmap实现的,因此将文件内存从磁盘读取到内核缓冲区后,用户空间和内核空间共享该缓冲区。
MappedByteBuffer内存映射文件是一种允许Java程序直接从内存访问的一种特殊的文件。我们可以将整个文件或者整个文件的一部分映射到内存当中,那么接下来是由操作系统来进行相关的页面请求并将内存的修改写入到文件当中。我们的应用程序只需要处理内存的数据,这样可以实现非常迅速的I/O操作。FileChannel map的三种模式
- 只读模式
/** * Mode for a read-only mapping. */ public static final MapMode READ_ONLY = new MapMode("READ_ONLY");只读模式来说,如果程序试图进行写操作,则会抛出ReadOnlyBufferException异常
- 读写模式
/** * Mode for a read/write mapping. */ public static final MapMode READ_WRITE = new MapMode("READ_WRITE");读写模式表明,对结果对缓冲区所做的修改将最终广播到文件。但这个修改可能会也可能不会被其他映射了相同文件程序可见。
- 专用模式
/** * Mode for a private (copy-on-write) mapping. */ public static final MapMode PRIVATE = new MapMode("PRIVATE");私有模式来说,对结果缓冲区的修改将不会被广播到文件并且也不会对其他映射了相同文件的程序可见。取而代之的是,它将导致被修改部分缓冲区独自拷贝一份到用户空间。这便是OS的“copy on write”原则。
FileChannel的transferTo、transferFrom
如果操作系统底层支持的话transferTo、transferFrom也会使用相关的零拷贝技术来实现数据的传输。所以,这里是否使用零拷贝必须依赖于底层的系统实现。

上图(原图[1])可以很好的解释“0拷贝”和“内核Bypass”的含义。上下两部分分别是基于Socket的和基于RDMA的一次收-发流程,左右分别为两个节点。可以明显的看到Socket流程中在软件中多了一次拷贝动作。而RDMA绕过了内核同时也减少了内存拷贝,数据可以直接在用户层和硬件间传递。

通信领域两大出场率最高的性能指标就是“带宽”和“时延”。简单的说,所谓带宽指的是指单位时间内能够传输的数据量,而时延指的是数据从本端发出到被对端接收所耗费的时间。因为上述几个特点,相比于传统以太网,RDMA技术同时做到了更高带宽和更低时延,所以其在带宽敏感的场景——比如海量数据的交互,时延敏感——比如多个计算节点间的数据同步的场景下得以发挥其作用。
RDMA本身指的是一种技术,具体协议层面,包含Infiniband(IB),RDMA over Converged Ethernet(RoCE)和internet Wide Area RDMA Protocol(iWARP)。三种协议都符合RDMA标准,使用相同的上层接口,在不同层次上有一些差别。

上图[2]对于几种常见的RDMA技术的协议层次做了非常清晰的对比,
2000年由IBTA(InfiniBand Trade Association)提出的IB协议是当之无愧的核心,其规定了一整套完整的链路层到传输层(非传统OSI七层模型的传输层,而是位于其之上)规范,但是其无法兼容现有以太网,除了需要支持IB的网卡之外,企业如果想部署的话还要重新购买配套的交换设备。
RoCE从英文全称就可以看出它是基于以太网链路层的协议,v1版本网络层仍然使用了IB规范,而v2使用了UDP+IP作为网络层,使得数据包也可以被路由。RoCE可以被认为是IB的“低成本解决方案”,将IB的报文封装成以太网包进行收发。由于RoCE v2可以使用以太网的交换设备,所以现在在企业中应用也比较多,但是相同场景下相比IB性能要有一些损失。
iWARP协议是IETF基于TCP提出的,因为TCP是面向连接的可靠协议,这使得iWARP在面对有损网络场景(可以理解为网络环境中可能经常出现丢包)时相比于RoCE v2和IB具有更好的可靠性,在大规模组网时也有明显的优势。但是大量的TCP连接会耗费很多的内存资源,另外TCP复杂的流控等机制会导致性能问题,所以从性能上看iWARP要比UDP的RoCE v2和IB差。
需要注意的是,虽然有软件实现的RoCE和iWARP协议,但是真正商用时上述几种协议都需要专门的硬件(网卡)支持。
iWARP本身不是由Infiniband直接发展而来的,但是它继承了一些Infiniband技术的设计思想。这三种协议的关系如下图所示:

提到IB协议,就不得不提到两大组织——IBTA和OFA。
成立于1999年,负责制定和维护Infiniband协议标准。IBTA独立于各个厂商,通过赞助技术活动和推动资源共享来将整个行业整合在一起,并且通过线上交流、营销和线下活动等方式积极推广IB和RoCE。
IBTA会对商用的IB和RoCE设备进行协议标准符合性和互操作性测试及认证,由很多大型的IT厂商组成的委员会领导,其主要成员包括博通,HPE,IBM,英特尔,Mellanox和微软等,华为也是IBTA的会员。
成立于2004年的非盈利组织,负责开发、测试、认证、支持和分发独立于厂商的开源跨平台infiniband协议栈,2010年开始支持RoCE。其对用于支撑RDMA/Kernel bypass应用的OFED(OpenFabrics Enterprise Distribution)软件栈负责,保证其与主流软硬件的兼容性和易用性。OFED软件栈包括驱动、内核、中间件和API。
上述两个组织是配合关系,IBTA主要负责开发、维护和增强Infiniband协议标准;OFA负责开发和维护Infiniband协议和上层应用API。
Linux内核的RDMA子系统还算比较活跃,经常会讨论一些协议细节,对框架的修改比较频繁,另外包括华为和Mellanox在内的一些厂商也会经常对驱动代码进行修改。
邮件订阅:http://vger.kernel.org/vger-lists.html#linux-rdma
代码位于内核drivers/infiniband/目录下,包括框架核心代码和各厂商的驱动代码。
代码仓:https://git.kernel.org/pub/scm/linux/kernel/git/rdma/rdma.git/
对于上层用户,IB提供了一套与Socket套接字类似的接口——libibverbs,前文所述三种协议都可以使用。参考着协议、API文档和示例程序很容易就可以写一个Demo出来。本专栏中的RDMA社区专指其用户态社区,在github上其仓库的名字为linux-rdma。
主要包含两个子仓库:
用户态核心代码,API,文档以及各个厂商的用户态驱动。
一个功能强大的用于测试RDMA性能的工具。
代码仓:https://github.com/linux-rdma/
UCX是一个建立在RDMA等技术之上的用于数据处理和高性能计算的通信框架,RDMA是其底层核心之一。我们可以将其理解为是位于应用和RDMA API之间的中间件,向上层用户又封装了一层更易开发的接口。

设计和生产IB相关硬件的厂商有不少,包括Mellanox、华为、收购了Qlogic的IB技术的Intel,博通、Marvell,富士通等等,这里就不逐个展开了,仅简单提一下Mellanox和华为。
IB领域的领头羊,协议标准制定、软硬件开发和生态建设都能看到Mellanox的身影,其在社区和标准制定上上拥有最大的话语权。目前最新一代的网卡是支持200Gb/s的ConnextX-6系列。
去年初推出的鲲鹏920芯片已经支持100Gb/s的RoCE协议,技术上在国内处于领先地位。但是软硬件和影响力方面距离Mellanox还有比较长的路要走,相信华为能够早日赶上老大哥的步伐。
微软、IBM和国内的阿里、京东都正在使用RDMA,另外还有很多大型IT公司在做初步的开发和测试。在数据中心和高性能计算场景下,RDMA代替传统网络是大势所趋。笔者对于市场接触不多,所以并不能提供更详细的应用情况。