3 详细设计方案
以下分别从三个层次详细说明RDS软件的具体实现方法
3.1 sock接口层
RDS的sock是RDS向Linux的socket管理层注册的一组传输层管理结构,其目的是方便用户从用户态空间对内核态的RDMA接口进行使用,以socket的形式向用户态提供数据传输服务。针对我们目前所使用的情况,所有业务全部都在内核态,因此sock管理层向Linux的socket管理层注册是不需要的,目前暂时保留了socket创建和释放接口,其他的接口已经取消,修改为使用自己定义的独立接口。
3.1.1 逻辑连接管理
上层用户希望得到的是一种类似TCP的面向连接服务,rds通过一种异步事件的模式向用户提供了面向连接的可靠数据报文传输服务。为了向用户提供面向连接的服务,RDS在sock和逻辑连接rds_connection之间建立了一对一的绑定关系,也就是每个建立一个rds_scoket,都会有一个唯一的rds_connection与之对应。
在数据传输层面,每个逻辑连接rds_connection下,实现了多条传输链路ib_connection,因此rds_connection与ib_connection之间是一对多的关系,具体每个rds_connection下需要支持几个ib_connection则可以通过模块参数来指定。
ib_connection与硬件之间通过QP来建立对应关系,每个ib_connection与一个QP一一对应,RDS为实现面向连接的可靠数据传输服务,采用了RC QP。
逻辑连接rds_connection的寻址方式采用源IP+Port和目的IP+Port的方式,在源地址和目的地址之间通过使用不同的port号,来实现多种业务同时使用RDS提供的服务。逻辑连接之下的物理传输链路则通过QP队列号和RDMA Core中维护的路由信息来建立绑定和寻址。
3.1.2 sock层相关接口实现
1. rds_sock创建接口
在RDS初始化时完成协议的和协议句柄的注册后,当有用户调用socket的创建接口时候,最终会调用注册rds_create接口。rds_create接口定义如下所示:
static int rds_create(struct net *net, struct socket *sock, int protocol, int kern)
net指向网络名命名空间,sock是socket创建的前一个阶段内核创建的socket结构,protocol是用户创建socket时指定的协议编号。
在创建过程中需要完成的工作主要有以下几点。
1.收到上层调用后,检查相关参数。然后Linux接口sk_alloc分配rds_sock管理结构。在rds_sock中内嵌有通用的sock管理结构。内嵌sock结构是rds_sock与Linux内核socket管理层连接的纽带。
2.在成功分配rds_sock结构以后,调用__rds_create完成下面进一步的初始化任务。首先,需要完成时sock_init_data,对socket结构进行初始化,并将socket与sock之间建立起绑定关系。然后,将通用的协议操作接口句柄绑定到新创建的sock管理结构中,初始化协议编号和销毁函数。
3.在完成以上的一些通用工作后,需要初始化rds_sock中的发送队列,接收队列,通知队列,mr管理的红黑树等结构等,将rds_sock加入到全局链表,增加rds_sock计数,这个用于统计信息。
RDS中通用的操作句柄定义如下所示,它定义了用户在协议AF_RDS协议栈上所进行的通用操作,其中最核心的就是数据的收发接口、绑定接口和释放接口。
static struct proto_ops rds_proto_ops = {
.family = AF_RDS,
.owner = THIS_MODULE,
.release = rds_release,
.bind = rds_bind,
.connect = rds_connect,
.socketpair = sock_no_socketpair,
.accept = sock_no_accept,
.getname = rds_getname,
.poll = rds_poll,
.ioctl = rds_ioctl,
.listen = sock_no_listen,
.shutdown = sock_no_shutdown,
.setsockopt = rds_setsockopt,
.getsockopt = rds_getsockopt,
.sendmsg = rds_sendmsg,
.recvmsg = rds_recvmsg,
.mmap = sock_no_mmap,
.sendpage = sock_no_sendpage,
};
2. rds_sock释放接口rds_release()
当用户在一个socket上完成数据的收发操作任务以后,就需要将socket释放掉。当用户执行release动作后,最终会调用rds_release接口,完成一些必要的清理工作,标准的release结构如下所示:
static int rds_release(struct socket *sock)
sock是用户所使用socket句柄。
下面对释放操作的主要工作经概括性说明。
1.首先需要调用sock_orphan接口,将sock的状态设置为SOCK_DEAD状态,防止后边再对此sock进行操作。
2.清空接收队列。将rds_sock接收队列上的所有数据报文全部清空。在接收队列清空过程,需要特别注意的是,底层不能够在向结构队列挂载新的消息报文。为此采取了两个措施避免此种情况发送,首先对接收队列使用了读写锁进行保护,另外在接收数据的时候需要检查sock的状态,如果我SOCK_DEAD状态,是不允许挂载数据到上面的。
3.清空拥塞sock上对应端口的拥塞控制标志位。如果对应端口的拥塞控制状态发生变化,发送拥塞更新的通知到对端。
4.将rds_sock从rds的全局绑定管理哈希表中移除,并释放sock的引用计数。
5.清空发送队列。清空rds_sock发送队列上所有的消息,
6.清空rds_sock的MR的红黑树。将MR红黑树上所有的申请的mr全部释放。
7.清空rds_sock的通知队列。将sock通知队列上的所有通知消息全部清空。
8.从全局rds_sock链表移除,减少rds_sock的计数。这个结构用户统计信息目的。
9.减少传输层引用计数,解除socket与sock之间的关联,减少sock的引用计数。
3. rds_sock绑定接口rds_bind()
绑定所要做的工作是,检查要绑定的socket地址是否已绑定,检查绑定IP地址对应的传输层是否存在。绑定接口的定义如下所示:
int rds_bind(struct socket *sock, struct sockaddr_in *laddr, struct sockaddr_in *paddr)
sock是用户所使用的socket句柄,laddr和paddr分别指定了本地IP地址和端口号跟对端IP地址和端口号
下面对绑定操作进行展开说明。
1.检查用户输入参数无误,调用rds_add_bound将要绑定的socket地址加入到全局的哈希链表中。
2.调用rds_trans_get_preferred接口,检查用户绑定的IP传输层是否支持。
3.2 connection管理层
3.2.1 链路管理状态机
链路主要有以下几种状态,UP态、ERROR态、DISCONNECTING态、DOWN态、CONNECTING态,共五个状态。这五个状态的转换关系可以简单概述为以下的状态转换图。
在链路相关资源分配以后,或者链路的复位操作已经完成,链路处于DOWN状态。然后开始发起连接操作,发起连接操作需要分配相关内存和进行IP地址解析,如果失败并且链路状态没有错误,链路返回DOWN状态,如果在此期间链路状态发生转变,链路需要进入错误处理流程,进入ERROR状态。
连接建立过程中链路状态为CONNECTING状态,期间需要进行内存分配、地址解析、路由解析、发起连接请求或对连接请求处理,此过程较为复杂。可能会因为底层的物理链路建立失败,导致链路建立的失败,也可能因为rds的协议不匹配或者server端的拒绝造成连接建立失败。对内存分配失败和地址解析失败,处理前面已经说明。对于其他的连接错误情况,需要将链路设置为ERROR状态,进行链路的错误处理。
如果链路建立成功,链路状态切换到UP状态。在此状态下用户可以进行数据的发送和接收操作,在数据交互过程如果因为物理原因或者数据传输导致的软件错误,都会导致链路进入错误处理流程中,对链路进行复位操作。有一种特例的情况就是,在复位操作由ERROR转到DISCONNECTING时候发现链路状态错误出现在UP状态,就需要再次进行复位操作,这种情况出现,可能是因为底层的连接动作是异步的造成。
在任何状态下链路都可以转换为DRROR状态,比如链路状态错误,在链路进行状态切换时候,出现意外的状态插入,就需要进行错误处理,用户的强制复位操作也会使链路进入错误状态,连接建立过程的失败和数据传输过程导致的错误都会使链路进入错误状态。
在ERROR状态向DISCONNECTING状态转换过程中,如果出现UP状态的插入,就直接转换为DISCONNTING状态,其他状态插入前部进行错误处理。
DISCONNECTING状态在向DOWN状态转换过程中,需要进行链路的复位操作,清理链路上已分配的资源,在相关的清理操作处理完成后,检查中间是否有其他状态切入,如果有就需要进入错误处理流程,否则进入DOWN状态,准备进行下一次的连接。
链路状态机
3.2.2 链路复位流程
链路的复位流程是在rds_conn_shutdown接口中完成的,shutdown是在一个单线程中执行的。但是,在shutdown操作执行过程可能会与底层的时间产生并发,链路的状态是不可确定的。
1.检查链路状态是否为down状态,为down状态,取消其他挂起的down动作。后面开始进行重连操作。
2.如果不是down状态,只有UP和ERROR两种状态允许,进行下面的清理操作,并进入到disconnection状态。否则必须就必须进入错误处理状态。在进行清理前,必须要确保connection不是发送和refill状态,否则等待状态同步。然后进行底层链路的清理工作,等底层链路清理工作完成后,对上层链路进行复位操作,主要是消息的重传处理和传输失败的通知用户。
3.在做完以上的清理工作后,检查链路状态没有被中间打断,链路状态转为down状态。后面是取消其他挂起复位,开始进行重新连接。
3.2.3 链路建立请求发起
链路的创建一般是在client端主动发起,或者是链路因某种原因断开,链路在复位后,重新发起连接建立过程。链路建立是放在一个线程中,是异步完成的一个动作。连接建立发起是在rds_connect_worker接口中进行的。
1.发起连接建立前rds_connection的状态必须是初始化状态或者复位状态,即DOWN状态。链接建立前,先检查链路状态是否为DOWN状态,如果为DOWN状态,转为CONNECTING状态,开始进行链路建立流程。如果链路不是DOWN状态,那么接口直接返回。
2.发起链路建立过程,调用底层链路创建接口。底层链路创建接口的主要工作是分配cm_id资源,绑定事件处理接口,开始路由解析操作。如果操作无误,接口正常返回。如果出现错误,需要分两种情况区分,如果在链路建立过程中,链路状态没有变化,那么链路状态回退到DOWN状态,并在随机延时后,重新开启连接。如果链路状态发生改变,链路状态必须设置为ERROR状态,然后进行链路的复位操作。
3.2.4 接收链路建立请求处理
对接收到的链路连接请求处理主要是在rds_ib_cm_handle_connect接口中完成的。连接请求处理中涉及到的状态相关的步骤如下:
1.首先分配逻辑链路管理结构rds_connection,此时链路状态为复位DOWN状态。
2.然后,检查链路状态如果为DOWN状态,直接进入CONNECTING状态,进入下面处理流程。
3.如果不是,DOWN状态,分别对UP状态和CONNECTING两种状态进行分别的处理。如果是UP状态,需要对链路进行强制的复位操作操作,然后重新发起链路建立。如果是CONNECTING状态就需要考虑,连接已持续的时间,如果链路的建立持续时间超过了门限(15s),就必须复位链路重新发起连接。
4.在重连过程中使用了随机回退机制,防止出现两端竞争的情况。
3.3 数据传输层
基于Infiniband协议实现的传输层示例,称为IB传输层。IB层根据网卡的实际支持情况实现标准接口的功能,向逻辑连接管理层提供服务。
3.3.1 发送完成回调控制策略
当发送的工作请求超过rds_ib_sysctl_max_unsig_wrs门限时候,就需要设置发送完成回调标志位,请求NIC在发送完成后产生中断,以便尽快释放发送环上资源。同时rds给这个门限设置了范围,rds_ib_sysctl_max_unsig_wr_min到rds_ib_sysctl_max_unsig_wr_max。rds的推荐值为16,范围为1~64。此值设置过小的情况下,会导致中断的数量增加,CPU会频繁调度,增加不必要开销,如果此值设置过大,可能会出现换上的资源得不到及时的释放,可能造成对环的竞争加剧。
对接收端的通知策略是,在消息发送完成后通知对端。此时接收端NIC会产生中断,通知rds接收完成。如果设置slient发送标志的情况下,就不会通知对端数据达到。
3.3.2 普通消息发送处理流程
普通数据的发送策略是要求一次将所有数据发送完毕。普通数据的数据报文有严格规定,工作请求中只包含两个sge,第一个为消息报文头,第二个为消息报文负载,负载的最大长度为4K。这样就要求必须将大于4K的数据负载进行拆分,分段发送,到接收端重新组消息帧结构。由于数据的发送是严格保序的,所以无需对消息分段进行编号控制。
1、首先在根据请求发送的数据长度,计算需要工作请求个数,然后从环上分配。如果分配不到所需的环资源,则直接返ENOMEM错误,退出。
2、流控开启情况,从对端的授信中获取信用,去发送环和信用两者中最小值,作为发送工作请求个数。如果没有获取到信用,直接返ENOMEM错误,退出。
3、检查是否有数据负载需要发送,如果没有负载,直接进入发送流程5。如果有负载进入步骤4.
4、检查数据是否映射,没映射情况下,进行DMA映射并设置映射标志。映射直接进入下面步骤。
5、发送流程主要工作,是将用户的数据封装到发送工作请求链表中。并根据完成回调策略设置完成回调标志位,根据流控就只负载接收信用到对端。
3.3.3 RDMA消息发送处理流程
rdma数据发送的接口为rds_ib_xmit_rdma,rdma消息的发送方式为整个消息报文一次性发送,如果发送的环上没有足够的工作请求内存使用的话,直接返回错误NOMEM,等待下次发送。这样的问题就是如果RDMA的消息帧比较大的话,会增加发送重试的负担,发送环上的小片资源不能得到充分的使用,降低发送效率,增大了发送延时。
RDMA的发送策略是在消息发送完成以后生成完成通知,通知用户RDMA操作的完成状态。
检查RDMA消息的sge链表是否映射,没有映射会进行映射操作,并设置rdma操作的映射标志位op_mapped。
根据IB设备允许的单个工作请求允许发送的sge最大个数,获得RDMA消息帧,需要的工作请求个数,从环上分配,如果无法满足,释放已分配的环上资源,返回错误代码为ENOMEM。
提交发送工作请求链表到发送QP。
3.3.4 接收完成处理流程
在收到对端进行的RDMA操作后,NIC会产生中断,回调接收完成队列的完成函数,然后会调度软中断,轮询完成队列上的完成事件。根据WR的ID最高位判断接收或发送完成,进入接收完成处理流程。
从rds_ib_connection的接收环结构上取下最早的接收WR。接收WR的sge链表由2个sge组成,第一个为头结构,存放控制信息,长度是固定的。第二个为数据部分是接收到的数据,长度为PAGE_SIZE。
接收完成处理流程
3.3.5 普通消息接收处理流程:
在普通消息接收完成,挂载到rds_sock的接收队列后,回调用户。用户收到回调,调用接收接口,接收数据,将incoming中数据拷贝到用户内存后,释放incoming。
普通消息解析,普通消息发送时候,会将所有内容拆分为,控制头结构后面跟一个数据段。这样这个数据部分被拆分成,头加固定长度数据的结构。接收端在收到对端的普通消息后,会根据控制头结构中接收地址等信息,构建出收到的incoming消息。
消息的发送流程中,数据存放位置在rds_message的操作部分。数据接收,RDMA数据接收不需要消耗接收环上的frag和hdr资源,只需要重新提交。对于普通消息,需要根据收到的数据frag构建incoming,最终挂到接收队列。
普通消息接收处理流程
3.3.6 发送完成处理流程
发送完成NIC会产生中断,回调完成队列绑定的完成回调接口,在完成回调中调度软中断,轮询处理完成队列的完成条目。然后根据WR的ID最高位判断是接收或发送完成,分别进行处理。
发送完成主要处理流程如下:
1、工作请求ID的低32位为ring结构的下标编号。通过完成队列返回的工作请求ID,与ring上完成的最后一个工作请求下标进行比较,可以得到完成的工作请求个数。为了降低中断数量,每一个消息只在最后产生一次完成事件,因此,需要依次将完成的WR全部处理。
2、发送的rds_message,会在其最后一个发送工作请求上,记载消息的data或rdma操作指针。这样就可以通过工作请求ID找到发送操作指针,由发送操作指针找消息。通过在最后一个工作请求中做记录,从而实现消息与底层发送工作请求串之间的同步性。
3、在一个完整的消息发送完成后,会从消息data或rdma操作中取消通知,回调用户,然后将发送消息从connection和sock的发送队列上移除。
发送完成处理流程图
4 数据结构
4.1 rds_message
消息报文rds_msessage是RDS组织和管理用户数据的基本单位,所有用户提交的发送命令,都会在RDS发送接口中转换成rds_message结构来进行管理。
struct rds_message {
atomic_t m_refcount; /*<消息引用计数*/
struct list_head m_sock_item; /*<挂在rds_sock上*/
struct list_head m_conn_item; /*<挂在rds_connection上*/
struct rds_incoming m_inc; /*<包括rds msg头、rdma cookie等信息 */
u64 m_ack_seq;
__be32 m_daddr; /*<目的地址 */
unsigned long m_flags; /*<设置消息传输过程中的状态 */
/*<RDS_MSG_MAPPED表示消息处于传输层上不能进行unmap */
spinlock_t m_rs_lock; /*<保护m_rs*/
wait_queue_head_t m_flush_wait;
struct rds_sock *m_rs; /*<所属rds_sock*/
unsigned int m_used_sgs; /*<仅用于拥塞更新等带外控制信息 */
unsigned int m_total_sgs; /*<同上 */
void *m_final_op; /*<指示当前消息使用的发送控制结构 */
struct
{
struct rm_rdma_op rdma;
struct rm_data_op data;
};
};
m_refcount标记message引用次数,m_sock_item和m_conn_item分别为message挂载到rds_sock与rds_connection发送队列上的挂载点。m_daddr是消息目的地址,m_flags标志消息所处的状态,m_rs_lock保护刷新队列m_flush_wait和m_rs标明消息所属的rds_sock。m_final_op是消息最后一个对应操作,一个消息可以有多个操作同时进行。消息通过判断最后一个消息完成后,认为整个消息完整发送完成。rdma为rdma操作对用的控制结构,data是普通消息的控制结构。
在rds_message中与数据相关最重要的两个字段就是rdma和data,rdma结构中记录了需要执行write操作的所有必要信息,data结构中记录了需要执行send操作的所有必要信息。
struct rm_rdma_op
{
int op_rcookie_num; /*<RDMA操作vec个数*/
unsigned int op_write:1; /*< RDMA读写标志*/
unsigned int op_notify:1; /*< 通知请求标志*/
unsigned int op_active:1; /*<为1表示当前rds消息中有rdma操作 */
unsigned int op_bytes; /*<当前RDMA发送或接收的总字节,用于统计 */
unsigned int op_count;
odsp_sge_elem_t *op_sg; /*<记录rdma args命令传递的server端mr信息 */
odsp_sge_elem_t *op_remote_sg; /*<远端sg地址*/
uint64_t n_user_token;
void *arg;
callback_complete rdma_complete; /*<用户通知回调*/
int index; /*<首地址序号*/
struct rm_loop_s op_loop; /*<回环测试挂载点*/
struct loop_op_arg *lp_arg;
};
op_rcookie_num表明在这个消息报文中,用户希望rds执行的RDMA操作的次数。op_write表示执行RDMA操作为读或者写操作,目前只保留了RDMA写,RDMA读操作没法携带Index。op_notify表示用户调用RDMA发送后,RDMA发送完成后是否需要完成通知,op_active标志消息中是否包含RDMA操作。
rdma_complete表示在远端完成RDMA后在通知本端RDMA操作完成,op_sg为本端RDMA操作的sge信息,op_remote_sg为远端的RDMA操作的cookie地址信息。index是RDMA操作中用户附带的立即数,实质为第一个RDMA控制信息的编号,用户用于对地址解析。op_loop是回环测试标志,RDMA回环测试时,执行到此处发送流程返回。
struct rm_data_op
{
unsigned int op_active:1;
unsigned int op_nents; /* payload折合页数,拥塞更新及recv调用rds_send_internal 会使用该值,其他消息该值为0 */
unsigned int op_count; /* sg的个数,除拥塞更新消息及rds_send_internal外其他消息该值为0 */
struct scatterlist *op_sg; /* payload所在sg数组,仅用于拥塞更新及rds_send_internal */
odsp_sge_elem_t *sg; /*<用户发送普通数据*/
int vec_num; /*<用户发送vec个数*/
int data_len; /*<每个vec发送实际长度之和*/
odsp_notifier_t notifier; /*<用户普通消息通知*/
};
op_active表示消息中包含了普通消息发送。op_notify表示普通消息执行是异步发送,包含了通知。op_notify是异步发送通知,op_nents表示发送的sge个数,用于rds自己的普通控制信息。op_count表示是用户调用发送普通消息sge的个数。notifier为用户发送普通消息的回调通知,data_len发送消息的实际长度。vec_num表示用户发送的普通数据的vec个数。sg为用户发送的普通数据sge链表。op_sg是rds发送的控制消息的sg链表。
4.2 rds_incoming
struct rds_incoming {
atomic_t i_refcount; /*本结构体引用计数*/
struct list_head i_item; /*位于rds_sock上的挂载点*/
struct rds_connection *i_conn; /*报文所属的rds_connection逻辑连接*/
struct rds_header i_hdr; /*内嵌消息报文头*/
unsigned long i_rx_jiffies; /*接收消息报文的时间戳*/
__be32 i_saddr; /*消息报文源IP*/
/* extension fields for dealing with netfilter */
struct rds_connection *i_oconn; /*未使用*/
struct sk_buff *i_skb; /*未使用*/
unsigned long skb_start; /*未使用*/
uint32_t i_msg_idx; /*消息索引,每个socket上全局分配*/
};
这里需要重点关注i_msg_idx字段,该字段在接收普通消息时,会作为一个参数提交给上层向RDS注册的接收完成回调接口,上层在调用普通消息接收接口时,会重新把该字段的值带下来,RDS根据该索引值去对应的rds socket接收队列上取消息。
由于一个rds socket可能会在底层实现多路径传输,因此为了考虑并发性能问题,在每个传输路径上都设置了一个接收队列,一个rds socket可能对应多个接收队列,底层多路径传输时,消息到达的顺序不能保证跟发送时完全一致,因此需要通过该索引来避免消息乱序。
i_msg_idx由两部分组成,高4位用来表示所到达消息的底层传输路径索引,低28位表示整个rds socket上所有接收消息的全局索引。
4.3 rds_header
消息头结构,包括基本消息头和扩展消息头,扩展消息头部长度为16字节固定长度。
RDS消息头只用在普通消息的收发操作中,目前我们只使用到了基本消息头中的长度域,源端口和目的端口域,其他字段实际上并未使用到。
#define RDS_HEADER_EXT_SPACE 16
struct rds_header {
__be64 h_sequence;
__be64 h_ack; /* 送出的ACK编号 */
__be32 h_len;
__be16 h_sport;
__be16 h_dport;
u8 h_flags;
u8 h_credit; /* 需要广播出去的credit个数 */
u8 h_padding[4];
__sum16 h_csum;
u8 h_exthdr[RDS_HEADER_EXT_SPACE];
};
序号域h_sequence指明了消息的序号,每个发送rds_connection链路,会对其所发送的消息进行顺序的计数,构成了消息的序号。对端在收到消息后会通过消息的序号等信息,区分数据是否属于此消息。
确认序号域h_ack指明了本端已累积无错收到的信息。RDS所使用的确认机制是累积确认机制,ack序号之前的消息会一并确认。
数据长度域h_len指明了整个消息的长度。普通消息的发送是由两个部分组成,第一个WR用于传输消息的头结构,紧接着的第二个WR用于传输有效的载荷。上层用户有可能,一次传输很大的一块数据,这样就需要将消息拆分发送。同一个消息的,负载分段都会包含一个相同的消息头。由于IB的数据包的强制顺序性,因此无需上层重新排序,只需要按照收到的先后顺序组装即可。
源端口h_sport和目的端口h_dport分别用于指明,数据包的发送端socket端口和数据包接收端socket目的端口。这样做的目的主要用于数据的分发。
消息类型表示位h_flags,用户指明当前所发送的消息的类型。消息的类型主要有以下几类,拥塞消息、接收确认消息、重传消息、ping-pong消息和信用消息。
信用与h_credit,用于收发两端的信用更新。信用是接收端授信给发送端,可以发送的数据的长度的控制。由于普通消息的接收有效载荷为4k,信用指明了接收端可以接收多少个4k的缓存区。
校验和h_csum,用于指明整个消息头的校验和信息。接收端在收到消息后,首先需要对收到消息的报文头进行校验,如果校验不等,代表数据遭到破坏。
4.4 rds_ring
struct rds_ib_work_ring {
u32 w_nr; /* 环上work的总数 */
u32 w_alloc_ptr; /* 下次分配新work的起始位置 */
u32 w_alloc_ctr; /* 环上已分配work的总数 */
u32 w_free_ptr; /* 下次准备回收的work索引 */
atomic_t w_free_ctr; /* 环上待回收work的总数 */
};
RDS使用上述环结构来实现无锁资源管理,提高整体性能。
- 请求分配entry:
ring->w_alloc_ptr = (ring->w_alloc_ptr + ret) % ring->w_nr;
ring->w_alloc_ctr += ret;
w_alloc_ptr位置更新通过与总个数求余实现回绕。
- 释放entry:
ring->w_free_ptr = (ring->w_free_ptr + val) % ring->w_nr;
atomic_add(val, &ring->w_free_ctr);
w_free_ptr经过求余回绕。释放个数直接增加即可。
- 已分配entry的回退
ring->w_alloc_ptr = (ring->w_alloc_ptr - val) % ring->w_nr;
ring->w_alloc_ctr -= val;
- 可用的entry请求个数
diff = ring->w_alloc_ctr - (u32) atomic_read(&ring->w_free_ctr);
无符号数会回绕,差值反映是环形上可用的entry的个数。通过可以的entry个数与换上总的entry计数w_nr的差值,就可以用作环的满或者空的状态判断标志。
4.5 rds_mr
rds_mr是sock层对用户申请的mr的管理结构,通过此数据结构sock可以实现对mr的整个生命周期的监控和管理,方式用户使用中mr释放的遗漏。通过构建mr缓存池可以提高fmr的使用效率,实现mr的重用。
struct rds_mr {
struct rb_node r_rb_node;
atomic_t r_refcount;
u32 r_key; /*在mpt表中的index*/
/* A copy of the creation flags */
unsigned int r_use_once:1;
unsigned int r_invalidate:1;
unsigned int r_write:1;
/* This is for RDS_MR_DEAD.
* It would be nice & consistent to make this part of the above
* bit field here, but we need to use test_and_set_bit.
*/
unsigned long r_state;
void *r_ib_device; /*MR与注册该MR的device绑定*/
struct rds_transport *r_trans;
void *r_trans_private; /* struct rds_ib_mr* */
};
mr申请后会通过r_rb_node域挂载所属rds_sock的全局红黑树上进行管理。r_refcount是表示mr被引用次数,rds_mr的最终释放需要检查引用次数。r_key是NIC底层返回的注册内存后的访问标示,通过r_key可以实现对内存访问权限的管理和访问内存地址的转换功能,r_key和内存偏移地址构成cookie,给远端执行RDMA操作使用。
r_invalidate表示mr已经无效,底层可以释放。r_write表示要映射内存的读写权限,根据用户注册mr时对本端内存的访问授权,可以允许对端进行读或者写操作。r_state表示mr结构的状态,mr只有一种状态RDS_MR_DEAD,当mr释放时需要检查mr的状态。
r_sock表示此mr所属的rds_sock结构,r_trans表示mr所对应的传输层,r_trans_private表示传输层所对应的rds_ib_mr管理结构。
此处需要重点关注r_ib_device字段,用来将MR与Device之间建立绑定关系,可以实现同一个物理端口上申请的MR在该物理端口上的不同rds socket之间共享,降低上层业务在使用底层多路径传输时内存管理的复杂性。
6 数据收发路径
6.1 单Socket多路径线程模型
RDS中同一个socket上使用三个接收队列,分别与每个ib connection对应,三个ib connection之间不存在锁的竞争问题,同时测试程序启用三个接收线程处理接收,小块性能提升明显。
6.2 多Socket单路径线程模型
完全一对一发送,应用层启用多个RDS Socket,RDS中每个RDS socket只启用一个ib connection收发。