10. RDMA之Completion Queue

https://zhuanlan.zhihu.com/p/259650980

知乎对Markdown的支持不太完整,这篇文章又比较长,所以格式上看起来可能有一点乱,还请包涵。本文欢迎转载,转载请注明出处。

我们曾经在前面的文章中简单介绍过CQ,本文将更深入的讲解关于它的一些细节。阅读本文前,读者可以先温习一下这篇文章:

基本概念

我们先回顾下CQ的作用。CQ意为完成队列,它的作用和WQ(SQ和RQ)相反,硬件通过CQ中的CQE/WC来告诉软件某个WQE/WR的完成情况。再次提醒读者,对于上层用户来说一般用WC,对于驱动程序来说,一般称为CQE,本文不对两者进行区分。

CQE可以看作一份“报告”,其中写明了某个任务的执行情况,其中包括:

  • 本次完成了哪个WQE指定的任务
  • 本次任务执行了什么操作
  • 本次任务执行成功/失败,失败原因是XXX
  • ...

每当硬件处理完一个WQE之后,都会产生一个CQE放在CQ队列中。如果一个WQE对应的CQE没有产生,那么这个WQE就会一直被认为还未处理完,这意味着什么呢?

  • 涉及从内存中取数据的操作(SEND和WRITE)

在产生CQE之前,硬件可能还未发送消息,可能正在发送消息,可能对端有接收到正确的消息。由于内存区域是在发送前申请好的,所以上层软件收到对应的CQE之前,其必须认为这片内存区域仍在使用中,不能将所有相关的内存资源进行释放。

  • 涉及向内存中存放数据的操作(RECV和READ)

在产生CQE之前,有可能硬件还没有开始写入数据,有可能数据才写了一半,也有可能数据校验出错。所以上层软件在获得CQE之前,这段用于存放接收数据的内存区域中的内容是不可信的。

总之,用户必须获取到CQE并确认其内容之后才能认为消息收发任务已经完成。

何时产生

我们将按照服务类型(本篇只讲RC和UD)和操作类型来分别说明,因为不同的情况产生CQE的时机和含义都不同,建议读者回顾第4篇和第5篇。

  • 可靠服务类型(RC)

前面的文章说过,可靠意味着本端关心发出的消息能够被对端准确的接收,这是通过ACK、校验和重传等机制保证的。

  • SEND
    SEND操作需要硬件从内存中获取数据,然后组装成数据包通过物理链路发送到对端。对SEND来说,Client端产生CQE表示对端已准确无误的收到数据,对端硬件收到数据并校验之后,会回复ACK包给发送方。发送方收到这ACK之后才会产生CQE,从而告诉用户这个任务成功执行了。如图所示,左侧Client端在红点的位置产生了本次任务的CQE。
  • RECV
    RECV操作需要硬件将收到的数据放到用户WQE中指定的内存区域,完成校验和数据存放动作后,硬件就会产生CQE。如上图右侧Server端所示。
  • WRITE
    对于Client端来说,WRITE操作和SEND操作是一样的,硬件会从内存中取出数据,并等待对端回复ACK后,才会产生CQE。差别在于,因为WRITE是RDMA操作,对端CPU不感知,自然用户也不感知,所以上面的图变成了这样:
  • READ
    READ和RECV有点像,Client端发起READ操作后,对端会回复我们想读取的数据,然后本端校验没问题后,会把数据放到WQE中指定的位置。完成上述动作后,本端会产生CQE。READ同样是RDMA操作,对端用户不感知,自然也没有CQE产生。这种情况上图变成了这样:
  • 不可靠服务类型(UD)

因为不可靠的服务类型没有重传和确认机制,所以产生CQE表示硬件已经将对应WQE指定的数据发送出去了。以前说过UD只支持SEND-RECV操作,不支持RDMA操作。所以对于UD服务的两端,CQE产生时机如下图所示:

WQ和CQ的对应关系

  1. 每个WQ都必须关联一个CQ,而每个CQ可以关联多个SQ和RQ

这里的所谓“关联”,指的是一个WQ的所有WQE对应的CQE,都会被硬件放到绑定的CQ中,需要注意同属于一个QP的SQ和RQ可以各自关联不同的CQ。如下图所示,QP1的SQ和RQ都关联了CQ1,QP2的RQ关联到了CQ1、SQ关联到了CQ2。

因为每个WQ必须关联一个CQ,所以用户创建QP前需要提前创建好CQ,然后分别指定SQ和RQ将会使用的CQ。

  1. WQE和对应的CQE间是不保序的

虽然硬件是按照“先进先出”的FIFO顺序从WQ中取出WQE的,但是并不能保证哪个WQE先执行完。所以并不是说硬件先取WQE1,后取WQE2,就会先在CQ中放CQE1,后放CQE2。所以并不一定是下图这样的:

​硬件也不会等上一个WQE产生了CQE之后再处理下一个WQE,因为这样效率很低(比如有的WQE可能要发送多个数据包;有的对端可能要很久才能返回)。所以真是情况有可能是这样的:

既然CQE产生的顺序和获取WQE的顺序是不相关的,那么上层应用和驱动是如何知道收到的CQE关联的是哪个WQE呢?其实很简单,CQE中指明它所对应的WQE的编号就可以了。

CQC

同QP一样,CQ只是一段存放CQE的队列内存空间。硬件除了知道首地址以外,对于这片区域可以说是一无所知。所以需要提前跟软件约定好格式,然后驱动将申请内存,并按照格式把CQ的基本信息填写到这片内存中供硬件读取,这片内存就是CQC。CQC中包含了CQ的容量大小,当前处理的CQE的序号等等信息。所以把QPC的图稍微修改一下,就能表示出CQC和CQ的关系:

CQN

CQ Number,就是CQ的编号,用来区别不同的CQ。CQ没有像QP0和QP1一样的特殊保留编号,本文中不再赘述了。

完成错误

IB协议中有三种错误类型,立即错误(immediate error)、完成错误(Completion Error)以及异步错误(Asynchronous Errors)。

立即错误的是“立即停止当前操作,并返回错误给上层用户”;完成错误指的是“通过CQE将错误信息返回给上层用户”;而异步错误指的是“通过中断事件的方式上报给上层用户”。可能还是有点抽象,我们来举个例子说明这两种错误都会在什么情况下产生:

  • 用户在Post Send时传入了非法的操作码,比如想在UD的时候使用RDMA WRITE操作。

结果:产生立即错误(有的厂商在这种情况会产生完成错误)

一般这种情况下,驱动程序会直接退出post send流程,并返回错误码给上层用户。注意此时WQE还没有下发到硬件就返回了。

  • 用户下发了一个WQE,操作类型为SEND,但是长时间没有受到对方的ACK。

结果:产生完成错误

因为WQE已经到达了硬件,所以硬件会产生对应的CQE,CQE中包含超时未响应的错误详情。

  • 用户态下发了多个WQE,所以硬件会产生多个CQE,但是软件一直没有从CQ中取走CQE,导致CQ溢出。

结果:产生异步错误

因为软件一直没取CQE,所以自然不会从CQE中得到信息。此时IB框架会调用软件注册的事件处理函数,来通知用户处理当前的错误。

由此可见,它们都是底层向上层用户报告错误的方式,只是产生的时机不一样而已。IB协议中对不同情况的错误应该以哪种方式上报做了规定,比如下图中,对于Modify QP过程中修改非法的参数,应该返回立即错误。

本文的重点在于CQ,所以介绍完错误类型之后,我们着重来看一下完成错误。完成错误是硬件通过在CQE中填写错误码来实现上报的,一次通信过程需要发起端(Requester)和响应端(Responder)参与,具体的错误原因也分为本端和对端。我们先来看一下错误检测是在什么阶段进行的(下图对IB协议中Figure 118进行了重画):

Requester的错误检测点有两个:

  1. 本地错误检测

即对SQ中的WQE进行检查,如果检测到错误,就从本地错误检查模块直接产生CQE到CQ,不会发送数据到响应端了;如果没有错误,则发送数据到对端。

2. 远端错误检测

即检测响应端的ACK是否异常,ACK/NAK是由对端的本地错误检测模块检测后产生的,里面包含了响应端是否有错误,以及具体的错误类型。无论远端错误检测的结果是否有问题,都会产生CQE到CQ中。

Responder的错误检测点只有一个:

  1. 本地错误检测

实际上检测的是对端报文是否有问题,IB协议也将其称为“本地”错误检测。如果检测到错误,则会体现在ACK/NAK报文中回复给对端,以及在本地产生一个CQE。

需要注意的是,上述的产生ACK和远端错误检测只对面向连接的服务类型有效,无连接的服务类型。比如UD类型并不关心对端是否收到,接收端也不会产生ACK,所以在Requester的本地错误检测之后就一定会产生CQE,无论是否有本地错误。

然后我们简单介绍下几种常见的完成错误:

  • RC服务类型的SQ完成错误
  • Local Protection Error
    本地保护域错误。本地WQE中指定的数据内存地址的MR不合法,即用户试图使用一片未注册的内存中的数据。
  • Remote Access Error
    远端权限错误。本端没有权限读/写指定的对端内存地址。
  • Transport Retry Counter Exceeded Error
    重传超次错误。对端一直未回复正确的ACK,导致本端多次重传,超过了预设的次数。
  • RC服务类型的RQ完成错误
  • Local Access Error
    本地访问错误。说明对端试图写入其没有权限写入的内存区域。
  • Local Length Error
    本地长度错误。本地RQ没有足够的空间来接收对端发送的数据。

完整的完成错误类型列表请参考IB协议的10.10.3节。

用户接口

同QP一样,我们依然从通信准备阶段(控制面)和通信进行阶段(数据面)来介绍IB协议对上层提供的关于CQ的接口。

控制面

同QP一样,还是“增删改查”四种,但是可能因为对于CQ来说,上层用户是资源使用者而不是管理者,只能从CQ中读数据而不能写数据,所以对用户开放的可配的参数就只有“CQ规格”一种。

  • 创建——Create CQ

创建的时候用户必须指定CQ的规格,即能够储存多少个CQE,另外用户还可以填写一个CQE产生后的回调函数指针(下文会涉及)。内核态驱动会将其他相关的参数配置好,填写到跟硬件约定好的CQC中告知硬件。

  • 销毁——Destroy CQ

释放一个CQ软硬件资源,包含CQ本身及CQC,另外CQN自然也将失效。

  • 修改——Resize CQ

这里名字稍微有点区别,因为CQ只允许用户修改规格大小,所以就用的Resize而不是Modify。

  • 查询——Query CQ

查询CQ的当前规格,以及用于通知的回调函数指针。

通过对比RDMA规范和软件协议栈,可以发现很多verbs接口并不是按照规范实现的。所以读者如果发现软件API和协议有差异时也无须感到疑惑,RDMA技术本身一直还在演进,软件框架也处于活跃更新的状态。如果更关心编程实现,那么请以软件协议栈的API文档为准;如果更关心学术上的研究,那么请以RDMA规范为准。

数据面

CQE是硬件将信息传递给软件的媒介,虽然软件知道在什么情况下会产生CQE,但是软件并不知道具体什么时候硬件会把CQE放到CQ中。在通信和计算机领域,我们把这种接收方不知道发送方什么时候发送的模式称为“异步”。我们先来举一个网卡的例子,再来说明用户如何通过数据面接口获取CQE(WC)。

网卡收到数据包后如何让CPU知道这件事,并进行数据包处理,有两种常见的模式:

  • 中断模式

当数据量较少,或者说偶发的数据交换较多时,适合采用中断模式——即CPU平常在做其他事情,当网卡收到数据包时,会上报中断打断CPU当前的任务,CPU转而来处理数据包(比如TCP/IP协议栈的各层解析)。处理完数据之后,CPU跳回到中断前的任务继续执行。

每次中断都需要保护现场,也就是把当前各个寄存器的值、局部变量的值等等保存到栈中,回来之后再恢复现场(出栈),这本身是有开销的。如果业务负载较重,网卡一直都在接收数据包,那么CPU就会一直收到中断,CPU将一直忙于中断切换,导致其他任务得不到调度。

  • 轮询模式

所以除了中断模式之外,网卡还有一种轮询模式,即收到数据包后都先放到缓冲区里,CPU每隔一段时间会去检查网卡是否受到数据。如果有数据,就把缓冲区里的数据一波带走进行处理,没有的话就接着处理别的任务。

通过对比中断模式我们可以发现,轮询模式虽然每隔一段时间需要CPU检查一次,带来了一定的开销,但是当业务繁忙的时候采用轮询模式能够极大的减少中断上下文的切换次数,反而减轻了CPU的负担。

现在的网卡,一般都是中断+轮询的方式,也就是根据业务负载动态切换。

在RDMA协议中,CQE就相当于是网卡收到的数据包,RDMA硬件把它传递给CPU去处理。RDMA框架定义了两种对上层的接口,分别是poll和notify,对应着轮询和中断模式。

Poll completion queue

很直白,poll就是轮询的意思。用户调用这个接口之后,CPU就会定期去检查CQ里面是否有新鲜的CQE,如果有的话,就取出这个CQE(注意取出之后CQE就被“消耗”掉了),解析其中的信息并返回给上层用户。

Request completion notification

直译过来是请求完成通知,用户调用这个接口之后,相当于向系统注册了一个中断。这样当硬件将CQE放到CQ中后,会立即触发一个中断给CPU,CPU进而就会停止手上的工作取出CQE,处理后返回给用户。

同样的,这两种接口使用哪种,取决于用户对于实时性的要求,以及实际业务的繁忙程度。

感谢阅读,CQ就介绍到这里,下篇打算详细讲讲SRQ。

协议相关章节

9.9 CQ错误检测和恢复

10.2.6 CQ和WQ的关系

10.10 错误类型及其处理

11.2.8 CQ相关控制面接口

11.4.2 CQ相关数据面接口

其他参考资料

[1] Linux Kernel Networking - Implement and Theory. Chapter 13. Completion Queue

原文地址:https://www.cnblogs.com/ztguang/p/15188501.html