一、基本架构:
urdma是使用DPDK实现的全软件用户态rdma功能。基本架构如上图所示:
1、urdmad:使用DPDK实现的primary进程,是RDMA网卡资源的管理进程。主要功能是:
- 初始化DPDK,创建kni网卡
- RDMA QP资源的管理
- QP的建链
- 下发匹配规则,将对应的报文送给对应的队列。
- 从kni网口处理非RDMA报文交给内核协议栈。
2、urdma_prov,实现用户态verbs接口,同时APP作为DPDK的secondary进程运行。主要功能是:
- 标准IB vebrs的实现
- 实现RDMA报文的收发包处理
- 和urdmad交互,完成RDMA的建链
3、urdma_kmod,运行在内核态用于支持RDMA CM建链操作。目前实现只有控制面,不支持数据面操作。这部分可以参考内核代码softiwarp: /infiniband/sw/siw。
二、urdma协议
urdma是基于UDP实现的RDMA协议。是一个非标准的iWARP或者Roce,和标准的iWARP协议相比较,如上图所示:
1、urdma实现iWARP DDP和RDMAP protocol
2、urdma实现TRP,用于可靠性传输字段
3、urdma选择UDP,避免粘包,还有TCP的一些其他特性。
对应的协议头部如下:
2.1协议头 rdmap
rdmap_head:
struct rdmap_packet {
uint8_t ddp_flags; /* 0=Tagged 1=Last 7-6=DDP_Version */
uint8_t rdmap_info; /* 1-0=RDMAP_Version 7-4=Opcode */
uint32_t sink_stag;
} __attribute__((__packed__));
ddp_flags:
用来标识Buffer模型。值为1时表示Tagged Buffer Model,值为0时表示Untagged Buffer Model。接收端依据此flag来决定如何解析报文。
rdmap_info:
rdma的opcode字段send/read/write/atomic
sink_stag:
从代码上看是一个扩展字段:目前用于rdma read,write,atomic作为rkey。其余情况为0
2.2 协议头:ddp
ddp tag hdr: 用于rdma write和rdma read resp
struct rdmap_tagged_packet {
struct rdmap_packet head;
uint64_t offset;
} __attribute__((__packed__));
offset 表示Payload在Tagged Buffer中从Buffer的起始地址到最终数据的存放地址的偏移
ddp untag hdr:用于rdma send、read req、atimoc
struct rdmap_untagged_packet {
struct rdmap_packet head;
uint32_t qn; /* Queue Number */
uint32_t msn; /* Message Sequence Number */
uint32_t mo; /* Message Offset */
} __attribute__((__packed__));
qn: 数据接收端的untagged队列的序号,个人感觉可以理解为opcode。 ddp_queue_send = 0, ddp_queue_read_request = 1, ddp_queue_terminate = 2(错误处理), ddp_queue_atomic_response = 3, ddp_queue_ack = 4,
msn: 消息序列号,必须从1开始,每次递增1。当到达0xFFFFFFFF之后,重新变为0。每个消息都只能对应一个Untagged packet,所以msn+qn能够唯一确定一个Untagged packet 。
mo:表示从QN和MSN确认的Untagged Buffer的起始位置到消息目的地址的偏移
2.3 协议头:trp
trp hdr
struct trp_hdr {
uint32_t psn;
uint32_t ack_psn;
uint16_t opcode;
} __attribute__((__packed__));
psn:(1)作为累计ack的时候是最小psn; (2)作为普通ack是当前报文的psn
ack_psn:(1)作为累计ack的时候是最大psn(psn + 1); (2)为普通ack是已经收到的ack psn
opcode:trp的opcode当前只支持0,累计ack, QP shutdown。
->累计ack:当接收端预期的recv_ack_psn小于接收的psn,认为丢失了ack时使用
->QP shutdown:在本端QP down的时候通知对端QP shutdown时使用
三、报文处理流程
1、urdmad为每个RDMA队列对分配一个硬件接收/发送以太网队列,便于APP直接通过IB verbs访问NIC,下发报文。
2、NIC硬件过滤器用于将数据包分离到RX队列中
- 目前从代码实现来看只支持Flow direct过滤
- urdmad将未过滤的包送给内核协议栈处理
- 命中过滤规则的包直接送给对应的QP接收队列,APP通过IB vebrs处理
四、设备初始化
第一步:insmod urdma.ko, 结果如下图所示
1、创建字符设备/dev/urdma,用于RDMA CM建链使用
2、注册urdma bus
3、完成/sys/device/urdma的创建
4、注册netdev的notify,关注网卡的注册,UP、DOWN事件
5、创建cm thread
第二步:以primary方式运行urdmad ,完成RDMA驱动加载, 结果如下图所示。
1、rte_eal_init()初始化DPDK运行环境
2、初始化core资源,用于后续APP使用RDMA功能时一个进程占用一个core。
3、do_init_driver()作为最主要的初始化函数,主要做以下事情:
- 读取/etc/rdma/urdma.json配置,配置文件会设置预设的队列深度、预设的QP个数等信息。(需要看实际网卡的能力)
- 创建控制通路:socket用于处理APP进程的QP申请、销毁消息。以及将主进程的数据同步到APP进程
- open /dev/urdma设备,用于RDMA CM建链,同时在建链的时候,设置过滤规则到硬件。
- kni网卡的初始化加载,这个动作会创建kni网卡(ifconfig可以看到),同时内核会响应通知notify。urdma.ko响应完成rdma设备的创建和注册。
五、APP--init ibv context
主要说明下ibv_get_device_list()接口在urdma中的实现。这个接口在ibvebs中,最终会调用到ops->alloc_device:urdma_device_alloc()接口。主要做以下事情:
- 确认存在urdma_0设备
- 创建线程(our_eal_master_thread()),和urdmad进程通信,确保两边的部分数据一致(max_qp等)。
- 在our_eal_master_thread线程上下文遍历本进程创建所有QP的收发包处理
- 初始化Ibv设备
六、MR
6.1 reg mr
由于DPDK的特性,APP作为second进程,申请的内存是DPDK已经pin主的内存,在注册MR的时候,不需要pin内存,更不需要陷入内核。所以MR的注册很简单。
usiw_reg_mr()
->usiw_hash_mr()根据address和len得到rkey
->urdma_reg_mr_with_rkey() 填充并返回ibv_mr,使用hash表(头插法)管理usiw_mr
6.2 lookup mr
usiw_mr_lookup() 传入usiw_mr_table 和 rkey
-> 根据rkey % capacity 找到table->entry
-> for() 遍历usiw_mr->next
-> rkey相同,则找到mr,否则返回NULL
七、QP
7.1 创建QP
usiw_create_qp()完成QP的创建,QP和SQ、RQ的结构体如上。过程如下:
1、port_get_next_qp() 与urdmad进程通信,urdmad将QP地址给到APP进程。QP的内存这块理解是DPDK做了特殊处理,主从进程拿到的都是物理地址(地址打印分析)才能两边同时操作一个QP。
2、ibv_cmd_create_qp() 陷入内核,内核主要是接收并存储传递下来的QP数据,用于RDMA CM建链,内核返回qpn。
3、send_cq、recv_cq字段的初始化
4、创建SQ和RQ,结构体如右所示。使用两个ring环完成post send和post recv。 初始状态ring为空,storage都存入free_ring
5、QP创建成功,将qp加入到ctx->qp_active链表上,用于our_eal_master_thread线程收发包遍历链表
6、置QP状态处于usiw_qp_unbound状态(未和远端建链)。
7.2 建链
建链分为两部分:
(1)APP进程
APP通过RDMA CM接口陷入内核。rdma_connect/accept
rdma_accept陷入内核
-> siw_accept()完成QP的建链
->siw_modify_qp()修改qp状态
-> notify_established()将QP加入到链表上。唤醒/dev/urdma的poll,通知urdma进程当前有事件到来,要处理。
(2)urdmad进程
do_poll()做epoll_wait,等待fd事件
->监听到fd事件执行fd->data_ready
->chardev_data_ready() read fd,陷入内核
-> urdma_chardev_read() 从链表上获取建链数据,返回用户态。数据格式如上图struct urdma_qp_connected_event{}所示
-> handle_qp_connected_event()处理qp建链事件
->do_setup_qp()本端QP记录对端的QP信息,下发过滤规则到硬件网卡,最后置QP为usiw_qp_connected(连接状态)
7.3 post send
usiw_post_send()当QP状态是connect和running才能post send,send wqe的结构体如上
->for() 遍历wr
->qp_get_next_send_wqe() 加锁从free_ring中取wqe,如果没有直接返回
->根据wr完成填充wqe
->rte_ring_enqueue(qp->sq.ring, wqe)将wqe放入ring中。
7.4 post recv
usiw_post_recv()当QP状态是error时直接返回,recv wqe的结构体如上
->for() 遍历wr
->qp_get_next_recv_wqe () 加锁从free_ring中取wqe,如果没有直接返回
->根据wr完成填充wqe
->rte_ring_enqueue(qp->rq0.ring, wqe)将wqe放入ring中。
八、CQ
8.1 创建CQ
usiw_create_cq完成CQ的创建,结构体如上。过程如下:
1、计算CQ的大小,申请CQ
2、ibv_cmd_create_cq(),陷入内核,内核管理CQ,返回CQN
3、初始化cq->cqe_ring和cq->free_ring
4、将storage填充到free_ring中
5、其他字段初始化,返回ibv_cq
8.2 poll cq
usiw_poll_cq() 完成cq的poll。cqe_ring的填充在收发包的过程中完成
->do_poll_cq() 入参:cq、num、出参:cqe[]
->RING_DEQUEUE_BURST() 从cqe_ring中一次批量获取num个cqe
->for() 根据取出来的cqe个数遍历cqe,memcpy到出参中
->RING_ENQUEUE_BURST() 将出来的cqe一次批量放到free_ring中
->convert_cqes() 将cqe 转换为ibv_wc
九、TX/RX流程分析
APP处理报文都在our_eal_master_thread() 上下文处理,过程如下:
kni_loop()函数循环处理报文
-> list_for_each_safe(&ctx->qp_active, qp, qp_next, ctx_entry) 遍历要处理的QP
-> if (qp->conn_state == connected) 则执行start_qp()
->start_qp() : qp->remote_ep字段的初始化,初始化tx发送队列,置QP为running状态, 继续执行process_qp()
-> if (qp->conn_state == running) 则执行progress_qp(),参数qp
->process_receive_queue() 处理收包,
->rte_eth_rx_burst() 函数将报文从硬件队列中获取到本地
->for() 循环遍历获取的报文, 过程中有prefetch,预取内存的数据包
->process_data_packet() 处理RX报文
->首先解析报文头部,如果非建链QP的报文,直接丢弃报文
->解析trp头部:
-> if (trp_opcode == trp_sack) 收到片段psn的确认ack, process_trp_sack()根据数据包中的min_psn和max_psn, while() 遍历pending wqe,置ACK标记
-> if (trp_opcode == trp_fin) 执行qp_shutdown(), 置QP error状态,消耗SQ、RQ的所有wqe,上送error cqe:IBV_WC_WR_FLUSH_ERR
-> 解析rdma->opcode
-> send:消耗rqe,上送cqe, IBV_WC_SUCCESS
-> read request:根据rkey找mr,判断是否在范围内,请求的addr和length,组装一个readresp的请求头,待后面respond_next_read_atomic()处理
-> write:代码中没有处理
-> atomic_request:根据rkey找mr,判断是否在范围内,组装一个atomic resp请求头,待后面respond_next_read_atomic()处理
-> read resp:每收到一个报文,根据包中的rkey找到mr,根据包中的地址和数据长度,将收到的报文memcoy到地址中,等到收到最后一个read resp报文后,从pending的wqe中找到rdma req wqe,再消耗cqe,将pending的wqe 放入到free_ring中
-> atomic resp:从pending的wqe中找到atomic req wqe,再消耗cqe,将pending的wqe 放入到free_ring中
->sweep_unacked_packets() 处理还没有被ack的报文
->for() 遍历被pending的wqe,if (pending-psn < send_last_ack_psn),执行do_process_ack()
->do_process_ack() 处理ack的wqe,此处只处理SEND和WRITE,上送cqe,将wqe放入free_ring中
->for() 遍历被pending的wqe,重传未ACK的报文,如果重传失败,则上送error cqe: IBV_WC_FATAL_ERR/ IBV_WC_RETRY_EXC_ERR,置QP 为ERROR状态
->rte_ring_dequeue(qp->sq.ring, (void **)&send_wqe) 一次处理一个wqe,调用progress_send_wqe()将报文发送去,同时将报文pending
->respond_next_read_atomic() 处理rdma read resp 和rdma atomic resp
->send_ack() 回复ack报文
->flush_tx_queue() 调用驱动接口,将在发送队列中的素有数据包发送出去
-> if (qp->conn_state == shutdown) 则执行qp_shutdown()发送trp_fin报文,然后将qp置为ERR,flush sq、rq. 上送cqe err(IBV_WC_WR_FLUSH_ERR)
-> if (qp->conn_state == error) 从ctx->qp_active上将qp删除,如果cq的引用计数为1后, 释放cq,然后发送消息destroy qp的消息到urdmad进程,本端释放qp.
十、urdma分析总结
- 基于UDP实现的rdma功能。
- 支持RC和UD
- 支持RDMA SEND/RECV、READ、atomic
- 支持可靠传输
- 需要占用大量的CPU资源
- 收发包上下文对于内存使用预取功能、使用无锁环,有一定的效率提升
- 支持累计ACK。
- 不同于rxe和siw,urdma是在用户态实现,数据路径不需要陷入内核
- QP资源有限制,取决于硬件网卡的收发包队列个数:min(tx_queue_num,rx_queue_num)
- 网卡需要支持Flow direct,否则需要修改代码使用软件进行过滤报文
- 一个进程只有只有一个主线程在进行收发包,遍历当前进程创建的QP,然后进行收发包。
- 收发包过程需要进行一次数据COPY,此操作会增加时延和降低带宽,是个优化点。
- 如果urdma进程出现问题,则无法继续使用rdma功能
- urdma是私有协议,只能两端都是urdma进行测试,否则无法使用
十一、使用方法
前提条件:
1、dpdk版本 17.05以上,20版本以下。因为urdma使用的dpdk接口,在最新版本都发生的了很大变化。尽可能选择17.05、17.11版本的DPDK,这样改的少。
2、内核版本选择4.xx的版本,urdma需要编译urdma.ko。最新内核的IB头文件发生了变化,需要进行大量才能编译通过。
3、选择的网卡尽可能支持Flow direct功能,使用硬件过滤,否则需要修改代码,在kni处理报文的代码处(do_xchg_packets())过滤RDMA报文交给对应的APP处理。
4、rdma core的版本使用17-18,建议选择17.11。
5、DPDK的编译(17.05为例,编译过程可能需要解决编译问题)
(1)vi config/common_base
#修改
CONFIG_RTE_BUILD_SHARED_LIB=y
CONFIG_RTE_EAL_IGB_UIO=y
CONFIG_RTE_KNI_KMOD=y
(2)编译
make config T=x86_64-native-linuxapp-gcc
export EXTRA_CFLAGS="-w -Wno-address-of-packed-member“
make
6、编译urdma,过程可能解决编译问题
export RTE_SDK=/root/dpdk-17.05
export RTE_TARGET=build
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$RTE_SDK/$RTE_TARGET/lib:/root/urdma-master/src/liburdma/.libs
autoreconf –I
./configure --sysconfdir /etc
make
make install
7、两端都必须是urdma,才能测试
运行和测试
1、运行urdmad,初始化RDMA设备,过程如下:
modprobe uio
modprobe ib_core
modprobe ib_uverbs
modprobe ib_cm
modprobe ib_umad
modprobe iw_cm
modprobe rdma_cm
modprobe rdma_ucm
rmmod urdma.ko
insmod /home/matrix/workspace/rdma/dpdk/dpdk-src/build/kmod/igb_uio.ko
insmod /home/matrix/workspace/rdma/dpdk/dpdk-src/build/kmod/rte_kni.ko
insmod /home/matrix/workspace/rdma/urdma/urdma-src/src/kmod/urdma.ko
#将liburdma-rdmav17.so拷贝到libibverbs下
cp /usr/local/lib/liburdma-rdmav17.so /usr/lib/x86_64-linux-gnu/libibverbs/liburdma-rdmav17.so
#dpdk绑定网卡
/home/matrix/workspace/rdma/dpdk/dpdk-src/usertools/dpdk-devbind.py -b igb_uio 03:00.0
#设置大页
mount -t hugetlbfs nodev /mnt/huge
sh -c 'echo 128 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages'
#设置库
export RTE_SDK=/home/matrix/workspace/rdma/dpdk/dpdk-src
export RTE_TARGET=build
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$RTE_SDK/$RTE_TARGET/lib:/home/matrix/workspace/rdma/urdma/urdma-src/src/liburdma/.libs
#运行urdma。
/home/matrix/workspace/rdma/urdma/urdma-src/src/urdmad/urdmad -l 0-1 -d librte_pmd_vmxnet3_uio.so -d librte_mempool_ring.so --proc-type=primary --file-prefix=ubuntu
参数
-l:表示运行的核;
-d: 表示要加载的动态库(mempool是dpdk的特性需要加载,vmxnet3是选择的网卡(根据网卡选择));
--proc-type: 运行的优先级;
--file-prefix: 表示hostname
2、看到kni网卡,ibv_device能看到IB设备,此时可以进行RDMA测试了
root@ubuntu:/sys/devices/urdma# ifconfig
ens32: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.92.160 netmask 255.255.255.0 broadcast 192.168.92.255
inet6 fe80::250:56ff:fe25:e76c prefixlen 64 scopeid 0x20<link>
ether 00:50:56:25:e7:6c txqueuelen 1000 (Ethernet)
RX packets 62854 bytes 4448101 (4.4 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 167011 bytes 131558018 (131.5 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
kni0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.186.130 netmask 255.255.255.0 broadcast 0.0.0.0
inet6 fe80::20c:29ff:fe92:d2e6 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:92:d2:e6 txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 12 bytes 1338 (1.3 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
root@ubuntu:/sys/devices/urdma# ibv_devices
device node GUID
------ ----------------
rxe0 025056fffe25e76c
urdma_0 000c2992d2e60000
root@ubuntu:/sys/devices/urdma#