我们先回顾下CQ的作用。CQ意为完成队列,它的作用和WQ(SQ和RQ)相反,硬件通过CQ中的CQE/WC来告诉软件某个WQE/WR的完成情况。再次提醒读者,对于上层用户来说一般用WC,对于驱动程序来说,一般称为CQE,本文不对两者进行区分。
CQE可以看作一份“报告”,其中写明了某个任务的执行情况,其中包括:
每当硬件处理完一个WQE之后,都会产生一个CQE放在CQ队列中。如果一个WQE对应的CQE没有产生,那么这个WQE就会一直被认为还未处理完,这意味着什么呢?
在产生CQE之前,硬件可能还未发送消息,可能正在发送消息,可能对端有接收到正确的消息。由于内存区域是在发送前申请好的,所以上层软件收到对应的CQE之前,其必须认为这片内存区域仍在使用中,不能将所有相关的内存资源进行释放。
在产生CQE之前,有可能硬件还没有开始写入数据,有可能数据才写了一半,也有可能数据校验出错。所以上层软件在获得CQE之前,这段用于存放接收数据的内存区域中的内容是不可信的。
总之,用户必须获取到CQE并确认其内容之后才能认为消息收发任务已经完成。
前面的文章说过,可靠意味着本端关心发出的消息能够被对端准确的接收,这是通过ACK、校验和重传等机制保证的。
因为不可靠的服务类型没有重传和确认机制,所以产生CQE表示硬件已经将对应WQE指定的数据发送出去了。以前说过UD只支持SEND-RECV操作,不支持RDMA操作。所以对于UD服务的两端,CQE产生时机如下图所示:
每个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。
同一个WQ中的WQE,其对应的CQE间是保序的
硬件是按照“先进先出”的FIFO顺序从某一个WQ(SQ或者RQ)中取出WQE并进行处理的,而向WR关联的CQ中存放CQE时,也是遵从这些WQE被放到WQ中的顺序的。简单来说,就是谁先被放到队列里,谁就先被完成。该过程如下图所示:
需要注意的是,使用SRQ的情况以及RD服务类型的RQ这两种情况是不保序的,本文中不展开讨论。
不同WQ中的WQE,其对应的CQE间是不保序的
前文中我们说过,一个CQ可能会被多个WQ共享。这种情况下,是不能保证这些WQE对应的CQE的产生顺序的。如下图所示(WQE编号表示下发的次序,即1最先被下发,6最后被下发):
上面的描述其实还包含了“同一个QP的SQ和RQ中的WQE,其对应的CQE间是不保序的”的情况,这一点其实比较容易理解,SQ和RQ,一个负责主动发起的任务,一个负责被动接收的任务,它们本来就可以是认为是两条不同方向的通道,自然不应该相互影响。假设用户对同一个QP先下发了一个Receive WQE,又下发一个Send WQE,总不能对端不给本端发送消息,本端就不能发送消息给对端了吧?
既然这种情况下CQE产生的顺序和获取WQE的顺序是不相关的,那么上层应用和驱动是如何知道收到的CQE关联的是哪个WQE呢?其实很简单,CQE中指明它所对应的WQE的编号就可以了。
另外需要注意的是,即使在多个WQ共用一个CQ的情况下,“同一个WR中的WQE,其对应的CQE间是保序的”这一点也是一定能够保证的,即上图中的属于WQ1的WQE 1、3、4对应的CQE一定是按照顺序产生的,对于属于WQ2的WQE 2、5、6也是如此。
同QP一样,CQ只是一段存放CQE的队列内存空间。硬件除了知道首地址以外,对于这片区域可以说是一无所知。所以需要提前跟软件约定好格式,然后驱动将申请内存,并按照格式把CQ的基本信息填写到这片内存中供硬件读取,这片内存就是CQC。CQC中包含了CQ的容量大小,当前处理的CQE的序号等等信息。所以把QPC的图稍微修改一下,就能表示出CQC和CQ的关系:
CQ Number,就是CQ的编号,用来区别不同的CQ。CQ没有像QP0和QP1一样的特殊保留编号,本文中不再赘述了。
IB协议中有三种错误类型,立即错误(immediate error)、完成错误(Completion Error)以及异步错误(Asynchronous Errors)。
立即错误的是“立即停止当前操作,并返回错误给上层用户”;完成错误指的是“通过CQE将错误信息返回给上层用户”;而异步错误指的是“通过中断事件的方式上报给上层用户”。可能还是有点抽象,我们来举个例子说明这两种错误都会在什么情况下产生:
结果:产生立即错误(有的厂商在这种情况会产生完成错误)
一般这种情况下,驱动程序会直接退出post send流程,并返回错误码给上层用户。注意此时WQE还没有下发到硬件就返回了。
结果:产生完成错误
因为WQE已经到达了硬件,所以硬件会产生对应的CQE,CQE中包含超时未响应的错误详情。
结果:产生异步错误
因为软件一直没取CQE,所以自然不会从CQE中得到信息。此时IB框架会调用软件注册的事件处理函数,来通知用户处理当前的错误。
由此可见,它们都是底层向上层用户报告错误的方式,只是产生的时机不一样而已。IB协议中对不同情况的错误应该以哪种方式上报做了规定,比如下图中,对于Modify QP过程中修改非法的参数,应该返回立即错误。
本文的重点在于CQ,所以介绍完错误类型之后,我们着重来看一下完成错误。完成错误是硬件通过在CQE中填写错误码来实现上报的,一次通信过程需要发起端(Requester)和响应端(Responder)参与,具体的错误原因也分为本端和对端。我们先来看一下错误检测是在什么阶段进行的(下图对IB协议中Figure 118进行了重画):
Requester的错误检测点有两个:
即对SQ中的WQE进行检查,如果检测到错误,就从本地错误检查模块直接产生CQE到CQ,不会发送数据到响应端了;如果没有错误,则发送数据到对端。
2. 远端错误检测
即检测响应端的ACK是否异常,ACK/NAK是由对端的本地错误检测模块检测后产生的,里面包含了响应端是否有错误,以及具体的错误类型。无论远端错误检测的结果是否有问题,都会产生CQE到CQ中。
Responder的错误检测点只有一个:
实际上检测的是对端报文是否有问题,IB协议也将其称为“本地”错误检测。如果检测到错误,则会体现在ACK/NAK报文中回复给对端,以及在本地产生一个CQE。
需要注意的是,上述的产生ACK和远端错误检测只对面向连接的服务类型有效,无连接的服务类型。比如UD类型并不关心对端是否收到,接收端也不会产生ACK,所以在Requester的本地错误检测之后就一定会产生CQE,无论是否有远端错误。
然后我们简单介绍下几种常见的完成错误:
完整的完成错误类型列表请参考IB协议的10.10.3节。
同QP一样,我们依然从通信准备阶段(控制面)和通信进行阶段(数据面)来介绍IB协议对上层提供的关于CQ的接口。
同QP一样,还是“增删改查”四种,但是可能因为对于CQ来说,上层用户是资源使用者而不是管理者,只能从CQ中读数据而不能写数据,所以对用户开放的可配的参数就只有“CQ规格”一种。
创建的时候用户必须指定CQ的规格,即能够储存多少个CQE,另外用户还可以填写一个CQE产生后的回调函数指针(下文会涉及)。内核态驱动会将其他相关的参数配置好,填写到跟硬件约定好的CQC中告知硬件。
释放一个CQ软硬件资源,包含CQ本身及CQC,另外CQN自然也将失效。
这里名字稍微有点区别,因为CQ只允许用户修改规格大小,所以就用的Resize而不是Modify。
查询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就是轮询的意思。用户调用这个接口之后,CPU就会定期去检查CQ里面是否有新鲜的CQE,如果有的话,就取出这个CQE(注意取出之后CQE就被“消耗”掉了),解析其中的信息并返回给上层用户。
直译过来是请求完成通知,用户调用这个接口之后,相当于向系统注册了一个中断。这样当硬件将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