2023-10-07 09:33:47 179阅读
rxe是完全使用软件实现的rdma协议。基本架构如下图所示,特性如下:
代码路径:
用户态代码路径:rdma-core/providers/rxe/
内核代码路径:kernel/driver/infiniband/sw/rxe/
本文的分析基于内核4.19版本的代码。
(1)Pool机制-rxe用于管理qpc,cqc,mrc等context使用的方法,两个结构体如下所示:
struct rxe_pool_entry {
struct rxe_pool *pool;
struct kref ref_cnt;
struct list_head list;
/* only used if indexed or keyed */
struct rb_node node;
u32 index;
};
struct rxe_pool {
struct rxe_dev *rxe;
spinlock_t pool_lock; /* pool spinlock */
size_t elem_size; /*qp、cq、mr等结构体的大小*/
struct kref ref_cnt;
void (*cleanup)(struct rxe_pool_entry *obj);
enum rxe_pool_state state;
enum rxe_pool_flags flags;
enum rxe_elem_type type; /*pool 的类型*/
unsigned int max_elem; /*支持的pool的个数*/
atomic_t num_elem; /*当前pool中的个数*/
/* only used if indexed or keyed */
struct rb_root tree; /*用于管理pool*/
unsigned long *table; /*index bitmap*/
size_t table_size; /*根据max_index和min_index计算的table size*/
u32 max_index; /*和范围*/
u32 min_index;
u32 last; /*记录上次分配的bit,便于查找index*/
size_t key_offset;
size_t key_size;
};
(2)Queue机制-rxe模块用于管理qp、cq队列使用的方法,如下所示:
struct rxe_queue结构体说明
struct rxe_queue {
struct rxe_dev *rxe;
struct rxe_queue_buf *buf;
struct rxe_mmap_info *ip;
size_t buf_size;
size_t elem_size;
unsigned int log2_elem_size;
unsigned int index_mask;
};
buf_size = sizeof(rxe_queue_buf) + queue depth * queue entry size
elem_size = queue entry size
index_mask = queue depth – 1
buf = vmalloc_user(buf_size),用于给用户态mmap使用。
struct rxe_queue_buf结构体说明:
struct rxe_queue_buf {
__u32 log2_elem_size;
__u32 index_mask; /*队列深度的掩码*/
__u32 pad_1[30];
__u32 producer_index; /*生产者index*/
__u32 pad_2[31];
__u32 consumer_index; /*消费者index*/
__u32 pad_3[31];
__u8 data[0]; /*队列的起始地址*/
};
内核和用户态对queue的操作主要使用到index_mask、producer_index;和consumer_index。对于queue中的pi和ci的修改陈述:(1)mmap的内存,所以内核和用户态是操作的一个数据(2)CQ的PI内核修改,加锁;CI用户态修改,加锁;(3)SQ的PI用户态修改,加锁;CI内核修改,不加锁;(4)RQ的PI用户态修改,加锁;CI内核修改,不加锁
通过insmod rdma_rxe.ko触发rxe模块的加载,主要完成下面两件事情
通过sysfs或者netlink发起rxe设备add请求,与内核rxe模块通信完成rxe设备的加载(具体使用方法见后文)。
rxe模块响应add请求,通过rxe_add()函数完成结构体struct rxe_dev{}的初始化
上图是MR注册的基本过程。最终完成struct rxe_mem{}的初始化。
struct rxe_mem {
struct rxe_pool_entry pelem;
union {
struct ib_mr ibmr;
struct ib_mw ibmw;
};
struct rxe_pd *pd;
struct ib_umem *umem;
u32 lkey;
u32 rkey;
enum rxe_mem_state state;
enum rxe_mem_type type;
u64 va; /*用户传入的地址*/
u64 iova; /*用户传入的地址*/
size_t length; /*用户传入的长度*/
u32 offset; /*第一个entry的偏移*/
int access; /*访问权限*/
int page_shift;
int page_mask;
int map_shift;
int map_mask;
u32 num_buf; /* entry的个数*/
u32 nbuf;
u32 max_buf;
u32 num_map; /*一级表的个数*/
struct rxe_map **map; /*二级页表*/
};
说明:
(1)4K页表存放256个地址entry(struct rxe_phys_buf)。enrty包含了地址和长度地址和长度都是通过ib_umem_get()整理后得来的。size不一定全部是4K。可以使连续地址的大小
(2)MR的组织形式以二级页表的方式组织,
(3)MR中记录页表存放的地址,地址存放的是内核的虚拟地址
(4)rxe_mem->offset是第一个entry中的可使用的偏移。
(1)根据sge的key获取index,从rb tree中找到对应的mr context。 需要循环遍历。
(2)根据sge中的addr也mr context中保存的地址进行比较,将数据memcpy到payload对应的地址中。这块是sge对应的buf和skb 对应的buf互相copy(TX和RX)
上图是创建CQ的过程。(1)CQ有创建个数的最大限制。(2)CQ不同于MR、QP不需要用rb_tree管理,通过QP找到CQ即可。结构体如下:
struct rxe_cq {
struct rxe_pool_entry pelem;
struct ib_cq ibcq;
struct rxe_queue *queue;
spinlock_t cq_lock;
u8 notify;
bool is_dying;
int is_user;
struct tasklet_struct comp_task; /*中断函数*/
};
(1)内核在RX流程中完成CQE结构体的赋值,根据CQ的PI,填充CQE,PI++,填充CQE过程需要加锁
(2)用户态根据CQ addr、CI和PI,获取CQE, CI++, 返回给APP.Poll cq 需要加锁的
cq->complete task用于中断模式。调用创建CQ时复制的ib_uverbs_comp_handle()处理函数。ib_uverbs_comp_handler()函数调用wake_up_interruptible()唤醒对应阻塞的线程,通知用户态处理。
上图是创建QP的基本过程,完成struct rxe_qp{}的初始化。
struct rxe_qp {
struct rxe_pool_entry pelem;
struct ib_qp ibqp;
struct ib_qp_attr attr;
unsigned int valid;
unsigned int mtu;
int is_user;
struct rxe_pd *pd;
struct rxe_srq *srq;
struct rxe_cq *scq;
struct rxe_cq *rcq;
enum ib_sig_type sq_sig_type;
struct rxe_sq sq;
struct rxe_rq rq;
struct socket *sk;
u32 dst_cookie;
struct rxe_av pri_av;
struct rxe_av alt_av;
struct list_head grp_list;
spinlock_t grp_lock; /* guard grp_list */
struct sk_buff_head req_pkts; /*内核协议栈的req pkt*/
struct sk_buff_head resp_pkts;
struct sk_buff_head send_pkts;
struct rxe_req_info req; /*tx处理*/
struct rxe_comp_info comp; /*ACK报文*/
struct rxe_resp_info resp; /*respond报文*/
atomic_t ssn;
atomic_t skb_out;
int need_req_skb;
struct timer_list retrans_timer; /*超时重传*/
u64 qp_timeout_jiffies;
/* Timer for handling RNR NAKS. */
struct timer_list rnr_nak_timer; /*rnr nak 重传*/
spinlock_t state_lock; /* guard requester and completer */
struct execute_work cleanup_work;
};
说明:
(1)不同于其他的硬件RDMA厂商,没有db_record
(2)RXE是内核申请queue buf,用户态映射。硬件厂商是用户态申请
(3)收发包的回调函数参数是QP。不需要lookup QO
(4)wqe size是固定的,内核和用户态用一个结构体。SQ和RQ的组织如下:
上图是Post send的基本过程。Send wqe的结构体如下:
struct rxe_send_wqe {
struct rxe_send_wr wr; /*wqe hdr信息*/
struct rxe_av av;
__u32 status;
__u32 state; /*wqe的状态 pending、process*/
__aligned_u64 iova;
__u32 mask;
__u32 first_psn; /*wqe对应报文的第一个psn*/
__u32 last_psn; /*wqe对应报文的最后一个psn*/
__u32 ack_length;
__u32 ssn;
__u32 has_rd_atomic;
struct rxe_dma_info dma; /*sge信息*/
};
说明:
(1)post send敲doorbel是向内核发送了POST_SEND的命令,代码如下;
cmd.hdr.command = IB_USER_VERBS_CMD_POST_SEND;
cmd.hdr.in_words = sizeof(cmd) / 4;
cmd.hdr.out_words = sizeof(resp) / 4;
cmd.response = (uintptr_t)&resp;
cmd.qp_handle = ibqp->handle;
cmd.wr_count = 0;
cmd.sge_count = 0;
cmd.wqe_size = sizeof(struct ibv_send_wr);
write(ibqp->context->cmd_fd, &cmd, sizeof(cmd));
(2)报文发送是在当前post send的上下文完成
(3)分片的报文要记录buf信息,一次只获取mtu大小的报文(lookup mr),填充获取addr和payload len
(4)对于需要ACK的wqe,需要pending,还需要放在SQ中
(5)QP的ci在ack后,更新
(6)post send过程加锁了(分为用户态PI加锁和内核态QP处理报文过程加锁)所以同时只有一个线程能操作队列。多个线程操作同一个qp时,第一个获取到QP资源的线程会继续处理到没有wqe,后面的线程直接返回。
(7)最终发包是调用rxe_send()函数完成。
上图是post recv的过程。(1)post recv过程需要加锁,所以同时只有一个线程下发rqe(2)操作不会下限到内核。结构体如下:
struct rxe_recv_wqe {
__aligned_u64 wr_id;
__u32 num_sge; /*sge个数*/
__u32 padding;
struct rxe_dma_info dma; /*sge*/
};
上图是RX的流程,rxe收到的包只有IB头+payload两部分,UDP之前的包头已经被剥离。收到的包分为两种包:(1)处理requester;(2)处理ACK报文;代码如下:
static inline void rxe_rcv_pkt(struct rxe_dev *rxe, struct rxe_pkt_info *pkt,struct sk_buff *skb)
{
if (pkt->mask & RXE_REQ_MASK)
rxe_resp_queue_pkt(rxe, pkt->qp, skb);
else
rxe_comp_queue_pkt(rxe, pkt->qp, skb);
}
上图是处理request报文的过程。简单说明:
(1)RDMA read request在softirq上下文处理,获取buf,通过rxe_send发送
(2)Requester 队列中的报文大于1个,在softirq中处理
(3)只有一个报文,在UDP tunnel收包上下文处理
上图是处理ACK报文的过程。简单说明:
(1)当respond 队列中的报文数量大于1,在softirp上下文处理
(2)一个报文,在UDP tunnel收包上下文处理
(3)没有处理cq full的情况,应该是认为报文是成功发送的,用户不用关心。
(4)SQ ci ++
解析ACK报文,判断opcode是rdma read resp,处理过程如下:
(1)首先从SQ中获取根据CI和SQ_addr获取wqe
(2)解析wqe,获取要存储报文的sge
(3)将rdma read resp的报文写到对应的buf中
(1)首先从SQ中获取根据CI和SQ_addr获取wqe。
(2)解析wqe,发现wqe->last_psn(这个wqe对应的最后一个分片报文的psn) < ack 报文中的psn时,认为是累计ack
(1)确定设备上是否有rxe的用户态驱动,不存在需要更新/安装rdma-core
(2)确定设备上是否有IB、rxe的内核驱动,不存在就找对应的内核源码,单独编译driver/infiniband/生成驱动
(1)两种方法:
使用perftest测试,使用标卡和cx6对打测试,中间一跳交换机
分析
2023-10-07 09:33:47 179阅读
rxe是完全使用软件实现的rdma协议。基本架构如下图所示,特性如下:
代码路径:
用户态代码路径:rdma-core/providers/rxe/
内核代码路径:kernel/driver/infiniband/sw/rxe/
本文的分析基于内核4.19版本的代码。
(1)Pool机制-rxe用于管理qpc,cqc,mrc等context使用的方法,两个结构体如下所示:
struct rxe_pool_entry {
struct rxe_pool *pool;
struct kref ref_cnt;
struct list_head list;
/* only used if indexed or keyed */
struct rb_node node;
u32 index;
};
struct rxe_pool {
struct rxe_dev *rxe;
spinlock_t pool_lock; /* pool spinlock */
size_t elem_size; /*qp、cq、mr等结构体的大小*/
struct kref ref_cnt;
void (*cleanup)(struct rxe_pool_entry *obj);
enum rxe_pool_state state;
enum rxe_pool_flags flags;
enum rxe_elem_type type; /*pool 的类型*/
unsigned int max_elem; /*支持的pool的个数*/
atomic_t num_elem; /*当前pool中的个数*/
/* only used if indexed or keyed */
struct rb_root tree; /*用于管理pool*/
unsigned long *table; /*index bitmap*/
size_t table_size; /*根据max_index和min_index计算的table size*/
u32 max_index; /*和范围*/
u32 min_index;
u32 last; /*记录上次分配的bit,便于查找index*/
size_t key_offset;
size_t key_size;
};
(2)Queue机制-rxe模块用于管理qp、cq队列使用的方法,如下所示:
struct rxe_queue结构体说明
struct rxe_queue {
struct rxe_dev *rxe;
struct rxe_queue_buf *buf;
struct rxe_mmap_info *ip;
size_t buf_size;
size_t elem_size;
unsigned int log2_elem_size;
unsigned int index_mask;
};
buf_size = sizeof(rxe_queue_buf) + queue depth * queue entry size
elem_size = queue entry size
index_mask = queue depth – 1
buf = vmalloc_user(buf_size),用于给用户态mmap使用。
struct rxe_queue_buf结构体说明:
struct rxe_queue_buf {
__u32 log2_elem_size;
__u32 index_mask; /*队列深度的掩码*/
__u32 pad_1[30];
__u32 producer_index; /*生产者index*/
__u32 pad_2[31];
__u32 consumer_index; /*消费者index*/
__u32 pad_3[31];
__u8 data[0]; /*队列的起始地址*/
};
内核和用户态对queue的操作主要使用到index_mask、producer_index;和consumer_index。对于queue中的pi和ci的修改陈述:(1)mmap的内存,所以内核和用户态是操作的一个数据(2)CQ的PI内核修改,加锁;CI用户态修改,加锁;(3)SQ的PI用户态修改,加锁;CI内核修改,不加锁;(4)RQ的PI用户态修改,加锁;CI内核修改,不加锁
通过insmod rdma_rxe.ko触发rxe模块的加载,主要完成下面两件事情
通过sysfs或者netlink发起rxe设备add请求,与内核rxe模块通信完成rxe设备的加载(具体使用方法见后文)。
rxe模块响应add请求,通过rxe_add()函数完成结构体struct rxe_dev{}的初始化
上图是MR注册的基本过程。最终完成struct rxe_mem{}的初始化。
struct rxe_mem {
struct rxe_pool_entry pelem;
union {
struct ib_mr ibmr;
struct ib_mw ibmw;
};
struct rxe_pd *pd;
struct ib_umem *umem;
u32 lkey;
u32 rkey;
enum rxe_mem_state state;
enum rxe_mem_type type;
u64 va; /*用户传入的地址*/
u64 iova; /*用户传入的地址*/
size_t length; /*用户传入的长度*/
u32 offset; /*第一个entry的偏移*/
int access; /*访问权限*/
int page_shift;
int page_mask;
int map_shift;
int map_mask;
u32 num_buf; /* entry的个数*/
u32 nbuf;
u32 max_buf;
u32 num_map; /*一级表的个数*/
struct rxe_map **map; /*二级页表*/
};
说明:
(1)4K页表存放256个地址entry(struct rxe_phys_buf)。enrty包含了地址和长度地址和长度都是通过ib_umem_get()整理后得来的。size不一定全部是4K。可以使连续地址的大小
(2)MR的组织形式以二级页表的方式组织,
(3)MR中记录页表存放的地址,地址存放的是内核的虚拟地址
(4)rxe_mem->offset是第一个entry中的可使用的偏移。
(1)根据sge的key获取index,从rb tree中找到对应的mr context。 需要循环遍历。
(2)根据sge中的addr也mr context中保存的地址进行比较,将数据memcpy到payload对应的地址中。这块是sge对应的buf和skb 对应的buf互相copy(TX和RX)
上图是创建CQ的过程。(1)CQ有创建个数的最大限制。(2)CQ不同于MR、QP不需要用rb_tree管理,通过QP找到CQ即可。结构体如下:
struct rxe_cq {
struct rxe_pool_entry pelem;
struct ib_cq ibcq;
struct rxe_queue *queue;
spinlock_t cq_lock;
u8 notify;
bool is_dying;
int is_user;
struct tasklet_struct comp_task; /*中断函数*/
};
(1)内核在RX流程中完成CQE结构体的赋值,根据CQ的PI,填充CQE,PI++,填充CQE过程需要加锁
(2)用户态根据CQ addr、CI和PI,获取CQE, CI++, 返回给APP.Poll cq 需要加锁的
cq->complete task用于中断模式。调用创建CQ时复制的ib_uverbs_comp_handle()处理函数。ib_uverbs_comp_handler()函数调用wake_up_interruptible()唤醒对应阻塞的线程,通知用户态处理。
上图是创建QP的基本过程,完成struct rxe_qp{}的初始化。
struct rxe_qp {
struct rxe_pool_entry pelem;
struct ib_qp ibqp;
struct ib_qp_attr attr;
unsigned int valid;
unsigned int mtu;
int is_user;
struct rxe_pd *pd;
struct rxe_srq *srq;
struct rxe_cq *scq;
struct rxe_cq *rcq;
enum ib_sig_type sq_sig_type;
struct rxe_sq sq;
struct rxe_rq rq;
struct socket *sk;
u32 dst_cookie;
struct rxe_av pri_av;
struct rxe_av alt_av;
struct list_head grp_list;
spinlock_t grp_lock; /* guard grp_list */
struct sk_buff_head req_pkts; /*内核协议栈的req pkt*/
struct sk_buff_head resp_pkts;
struct sk_buff_head send_pkts;
struct rxe_req_info req; /*tx处理*/
struct rxe_comp_info comp; /*ACK报文*/
struct rxe_resp_info resp; /*respond报文*/
atomic_t ssn;
atomic_t skb_out;
int need_req_skb;
struct timer_list retrans_timer; /*超时重传*/
u64 qp_timeout_jiffies;
/* Timer for handling RNR NAKS. */
struct timer_list rnr_nak_timer; /*rnr nak 重传*/
spinlock_t state_lock; /* guard requester and completer */
struct execute_work cleanup_work;
};
说明:
(1)不同于其他的硬件RDMA厂商,没有db_record
(2)RXE是内核申请queue buf,用户态映射。硬件厂商是用户态申请
(3)收发包的回调函数参数是QP。不需要lookup QO
(4)wqe size是固定的,内核和用户态用一个结构体。SQ和RQ的组织如下:
上图是Post send的基本过程。Send wqe的结构体如下:
struct rxe_send_wqe {
struct rxe_send_wr wr; /*wqe hdr信息*/
struct rxe_av av;
__u32 status;
__u32 state; /*wqe的状态 pending、process*/
__aligned_u64 iova;
__u32 mask;
__u32 first_psn; /*wqe对应报文的第一个psn*/
__u32 last_psn; /*wqe对应报文的最后一个psn*/
__u32 ack_length;
__u32 ssn;
__u32 has_rd_atomic;
struct rxe_dma_info dma; /*sge信息*/
};
说明:
(1)post send敲doorbel是向内核发送了POST_SEND的命令,代码如下;
cmd.hdr.command = IB_USER_VERBS_CMD_POST_SEND;
cmd.hdr.in_words = sizeof(cmd) / 4;
cmd.hdr.out_words = sizeof(resp) / 4;
cmd.response = (uintptr_t)&resp;
cmd.qp_handle = ibqp->handle;
cmd.wr_count = 0;
cmd.sge_count = 0;
cmd.wqe_size = sizeof(struct ibv_send_wr);
write(ibqp->context->cmd_fd, &cmd, sizeof(cmd));
(2)报文发送是在当前post send的上下文完成
(3)分片的报文要记录buf信息,一次只获取mtu大小的报文(lookup mr),填充获取addr和payload len
(4)对于需要ACK的wqe,需要pending,还需要放在SQ中
(5)QP的ci在ack后,更新
(6)post send过程加锁了(分为用户态PI加锁和内核态QP处理报文过程加锁)所以同时只有一个线程能操作队列。多个线程操作同一个qp时,第一个获取到QP资源的线程会继续处理到没有wqe,后面的线程直接返回。
(7)最终发包是调用rxe_send()函数完成。
上图是post recv的过程。(1)post recv过程需要加锁,所以同时只有一个线程下发rqe(2)操作不会下限到内核。结构体如下:
struct rxe_recv_wqe {
__aligned_u64 wr_id;
__u32 num_sge; /*sge个数*/
__u32 padding;
struct rxe_dma_info dma; /*sge*/
};
上图是RX的流程,rxe收到的包只有IB头+payload两部分,UDP之前的包头已经被剥离。收到的包分为两种包:(1)处理requester;(2)处理ACK报文;代码如下:
static inline void rxe_rcv_pkt(struct rxe_dev *rxe, struct rxe_pkt_info *pkt,struct sk_buff *skb)
{
if (pkt->mask & RXE_REQ_MASK)
rxe_resp_queue_pkt(rxe, pkt->qp, skb);
else
rxe_comp_queue_pkt(rxe, pkt->qp, skb);
}
上图是处理request报文的过程。简单说明:
(1)RDMA read request在softirq上下文处理,获取buf,通过rxe_send发送
(2)Requester 队列中的报文大于1个,在softirq中处理
(3)只有一个报文,在UDP tunnel收包上下文处理
上图是处理ACK报文的过程。简单说明:
(1)当respond 队列中的报文数量大于1,在softirp上下文处理
(2)一个报文,在UDP tunnel收包上下文处理
(3)没有处理cq full的情况,应该是认为报文是成功发送的,用户不用关心。
(4)SQ ci ++
解析ACK报文,判断opcode是rdma read resp,处理过程如下:
(1)首先从SQ中获取根据CI和SQ_addr获取wqe
(2)解析wqe,获取要存储报文的sge
(3)将rdma read resp的报文写到对应的buf中
(1)首先从SQ中获取根据CI和SQ_addr获取wqe。
(2)解析wqe,发现wqe->last_psn(这个wqe对应的最后一个分片报文的psn) < ack 报文中的psn时,认为是累计ack
(1)确定设备上是否有rxe的用户态驱动,不存在需要更新/安装rdma-core
(2)确定设备上是否有IB、rxe的内核驱动,不存在就找对应的内核源码,单独编译driver/infiniband/生成驱动
(1)两种方法:
使用perftest测试,使用标卡和cx6对打测试,中间一跳交换机
分析