IB程序设计流程
以ibv_ud_pingpong为例
文章目录
1. 声明控制结构体
1.1全局结构体
struct pingpong_context {
struct ibv_context *context; //ibv设备上下文
struct ibv_comp_channel *channel; //event事件通道
struct ibv_pd *pd; //保护域
struct ibv_mr *mr; //内存注册
struct ibv_cq *cq; //完成队列
struct ibv_qp *qp; //QP任务队列
struct ibv_ah *ah; //address handler
void *buf; //缓存地址
int size; // 数据大小
int rx_depth; //接收队列中未完成任务的最大数量
int pending; //?
struct ibv_port_attr portinfo; //端口信息
};
1.2目标地址结构体
struct pingpong_dest {
int lid; //目标lid
int qpn; //目标qp序号
int psn; //?
union ibv_gid gid; //?
};
2. 主函数执行流程
2.1准备工作
声明变量
获取参数
2.2 正式工作
2.2.1 获取设备
获取设备列表 dev_list = ibv_get_deivce_list(NULL)
获取指定设备 ib_dev = dev_list[i]
- 也可以通过
ibv_get_device_name(dev_list[i])
获取指定设备名称,与给定名称进行比较,获取指定名称设备
2.2.2 初始化全局结构体(核心)----pp_init_ctx
static struct pingpong_context *pp_init_ctx(struct ibv_device *ib_dev, int size,
int rx_depth, int port,
int use_event)
初始化结构体
struct pingpong_context *ctx;
ctx = malloc(sizeof *ctx); //为结构体开辟空间
初始化用户指定参数
ctx->size = size;
ctx->rx_depth = rx_depth;
初始化缓存
ctx->buf = memalign(page_size, size + 40); //分配指定大小内存作为缓冲区
if (!ctx->buf) {
fprintf(stderr, "Couldn't allocate work buf.\n");
goto clean_ctx;
}
/* FIXME memset(ctx->buf, 0, size + 40); */
memset(ctx->buf, 0x7b, size + 40); //将缓存区域size+40范围内都设置为0x7b
初始化设备
ctx->context = ibv_open_device(ib_dev);
if (!ctx->context) {
fprintf(stderr, "Couldn't get context for %s\n",
ibv_get_device_name(ib_dev));
goto clean_buffer;
}
初始化事件通道
if (use_event) {
ctx->channel = ibv_create_comp_channel(ctx->context);
if (!ctx->channel) {
fprintf(stderr, "Couldn't create completion channel\n");
goto clean_device;
}
} else
ctx->channel = NULL;
初始化保护域(protection domain)
/*
* QP,AH,MR和SRQ等组件通常只能和与自己同类的组件进行交互,而保护域就是负责进行不同类的组件之间的交互。
* PD可以将QP和MR以及MW进行绑定,以此使得网卡可以访问主机内存,并对这一过程进行控制
* PD还能再UD传输中将QP与AH进行绑定,以此控制对于UD传输目标的访问。
*/
ctx->pd = ibv_alloc_pd(ctx->context);
if (!ctx->pd) {
fprintf(stderr, "Couldn't allocate PD\n");
goto clean_comp_channel;
}
向保护域中注册缓存区
ctx->mr = ibv_reg_mr(ctx->pd, ctx->buf, size + 40, IBV_ACCESS_LOCAL_WRITE);
if (!ctx->mr) {
fprintf(stderr, "Couldn't register MR\n");
goto clean_pd;
}
创建完成队列
ctx->cq = ibv_create_cq(ctx->context, rx_depth + 1, NULL,ctx->channel, 0);
if (!ctx->cq) {
fprintf(stderr, "Couldn't create CQ\n");
goto clean_mr;
}
创建QP
{
struct ibv_qp_init_attr attr = {
.send_cq = ctx->cq, //指定发送任务的完成队列
.recv_cq = ctx->cq, //指定接收任务的完成队列
.cap = { //QP属性设置
.max_send_wr = 1, // 发送队列中未完成任务的最大数量
.max_recv_wr = rx_depth, //接收队列中未完成任务的最大数量
.max_send_sge = 1, //发送队列中,一个WR中包含的散列表SGE的最大数量
.max_recv_sge = 1 //接收队列中,一个WR中包含的散列表SGE的最大数量
},
.qp_type = IBV_QPT_RC,//QP类型
}; //创建QP所需的参数
ctx->qp = ibv_create_qp(ctx->pd, &attr);
if (!ctx->qp) {
fprintf(stderr, "Couldn't create QP\n");
goto clean_cq;
}
}
修改qp状态从REST到INIT(端口与QP绑定)
- 一旦QP转换为INIT状态,用户就可以开始将接收缓冲区发布到接收队列
{
struct ibv_qp_attr attr = {
.qp_state = IBV_QPS_INIT,
.pkey_index = 0, //pkey 索引
.port_num = port, //指定QP端口号
.qkey = 0x11111111 //QP的编号? //非连接传输状态专用
}; //qp属性修改参数
if (ibv_modify_qp(ctx->qp, &attr,
IBV_QP_STATE |
IBV_QP_PKEY_INDEX |
IBV_QP_PORT |
IBV_QP_QKEY)) {
fprintf(stderr, "Failed to modify QP to INIT\n");
goto clean_qp;
}
}
2.2.3 接收队列登记任务WR------pp_post_recv
static int pp_post_recv(struct pingpong_context *ctx, int n)//n = ctx->rx_depth接收队列深度
向接收队列登记WR,用于指定接收任务的缓存区域
//散列表
struct ibv_sge list = {
.addr = (uintptr_t) ctx->buf, //申请的缓存区域
.length = ctx->size + 40, //缓存大小
.lkey = ctx->mr->lkey //ibv_reg_mr中获取的mr
};
//任务请求wr
struct ibv_recv_wr wr = {
.wr_id = PINGPONG_RECV_WRID, //用户分配的work request ID
.sg_list = &list, //WR的散列表
.num_sge = 1, //sg_list的条目数
};
//ibv_post_recv()动词
struct ibv_recv_wr *bad_wr; //用于存储出错任务请求
int i;
for (i = 0; i < n; ++i)
if (ibv_post_recv(ctx->qp, &wr, &bad_wr))
break;
return i;
2.2.4 事件—ibv_req_notify_cq
//ibv_req_notify_cq 为指定完成队列(CQ)配置通知机制
if (use_event)
if (ibv_req_notify_cq(ctx->cq, 0)) {
fprintf(stderr, "Couldn't request CQ notification\n");
return 1;
}
//函数原型
int ibv_req_notify_cq(struct ibv_cq *cq, int solicited_only)
//cq ibv_create_cq 创建的完成队列
//solicited_only 只有当WR被标记为solicited时才进行通知
//用户使用ibv_get_cq_event来接收提醒
//通知机制将只对一个通知进行防护。一旦发出通知,该机制必须通过ibv_req_notify_cq对的新调用重新配置。
当完成队列条目(CQE)被放置在CQ上时,完成事件将被发送到与CQ相关联的完成信道(CC)。
2.2.5 获取端口信息----pp_get_port_info
//获取端口信息
//ib_port用户指定ib设备端口,默认为1
//&ctx->portinfo 存储端口信息
if (pp_get_port_info(ctx->context, ib_port, &ctx->portinfo)) {
fprintf(stderr, "Couldn't get port info\n");
return 1;
}
//函数原型 ibv_query_port检索与端口关联的各种属性
//context:struct ibv_context from ibv_open_device
int pp_get_port_info(struct ibv_context *context, int port,
struct ibv_port_attr *attr)
{
return ibv_query_port(context, port, attr);
}
struct ibv_port_attr
{
enum ibv_port_state state;//端口状态
enum ibv_mtu max_mtu; //端口支持的最大传输单元MTU
enum ibv_mtu active_mtu; //当前的MTU
int gid_tbl_len; //GID表格的长度(source global id)
uint32_t port_cap_flags; //端口支持的容量大小
uint32_t max_msg_sz; //最大信息长度
uint32_t bad_pkey_cntr; //Bad P_key counter
uint32_t qkey_viol_cntr; //Q_Key violation counter
uint16_t pkey_tbl_len; //分区表长度
uint16_t lid; //该端口被分配的第一个LID
uint16_t sm_lid; //subet manager子网管理的LID
uint8_t lmc; //LID mask control 当多个LID被分配给当前端口时使用
uint8_t max_vl_num; //最大虚拟线路
uint8_t sm_sl; //SM服务等级SL
uint8_t subnet_timeout; //子网传播延时
uint8_t init_type_reply; //SM的初始化方式
uint8_t active_width; //当前带宽
uint8_t active_speed; //当前速度
uint8_t phys_state; //物理端口状态
};
2.2.6 获取GID属性
GID:用于标识网络适配器上的端口、路由器上的端口或多播组的128位标识符。
GID是一个有效的128位IPv6地址(根据RFC 2373),具有IBA中定义的附加属性限制,以促进高效的发现、通信和路由。
if (gidx >= 0) {
if (ibv_query_gid(ctx->context, ib_port, gidx, &my_dest.gid)) {
fprintf(stderr, "Could not get local gid for gid index "
"%d\n", gidx);
return 1;
}
} else
memset(&my_dest.gid, 0, sizeof my_dest.gid);
//从设备的GID表格中获取指定序号的GID
//context 设备
//port_num 物理端口号(1是第一个端口)
//index 返回GID表格中的第几个条目(0为第一个)
int ibv_query_gid(struct ibv_context *context, uint8_t port_num, int index, union ibv_gid *gid)
//gid数据结构
//GID是由GUID(全局唯一标识符)和子网分配的前缀组成
union ibv_gid
{
uint8_t raw[16];
struct
{
uint64_t subnet_prefix;
uint64_t interface_id;
} global;
};
2.2.7 建立连接(核心)----pp_connetct_ctx
/*建立双方QP之间的连接
* ctx: 设备
* port: ib_port 设备端口号
* my_psn: packet sequence number 接收队列起始包序号
* sl: service level 服务等级
* dest:
* sgid_idx:
*/
static int pp_connect_ctx(struct pingpong_context *ctx, int port, int my_psn,
int sl, struct pingpong_dest *dest, int sgid_idx)
QP从INIT状态到RTR状态
当QP绑定了相应的缓存,它就能从INIT状态转换为准备接受(RTR-Ready to receive)状态。
struct ibv_qp_attr attr = {
.qp_state = IBV_QPS_RTR //状态转换标志
};
if (ibv_modify_qp(ctx->qp, &attr, IBV_QP_STATE)) {
fprintf(stderr, "Failed to modify QP to RTR\n");
return 1;
}
从这一阶段开始QP就能开始接收工作。
QP从RTR状态到RTS状态
当QP计入RTR状态后,就能够转换为RTS(Ready to send)状态
attr.qp_state = IBV_QPS_RTS;//状态转换标志
attr.sq_psn = my_psn; //send queue starting packet sequence number发送队列起始包序号,应该与远端QP的接收队列起始包序号rq_psn相同
if (ibv_modify_qp(ctx->qp, &attr,
IBV_QP_STATE |
IBV_QP_SQ_PSN)) {
fprintf(stderr, "Failed to modify QP to RTS\n");
return 1;
}
从这一段开始,QP就能够开始发送操作并且已经启用全部功能。用户可以通过ibv_post_send
向其发送传输请求了。
创建AH(Address handler)
AH包含了数据到达远端目标所需的全部信息。在面向连接的传输方式(RC,UC)中,AH与QP相绑定。在面向数据报传输(UD)方式中,AH与工作请求(WR)绑定
//设置AH属性
struct ibv_ah_attr ah_attr = {
.is_global = 0,
.dlid = dest->lid,
.sl = sl,
.src_path_bits = 0,
.port_num = port
};
if (dest->gid.global.interface_id) {
ah_attr.is_global = 1;
ah_attr.grh.hop_limit = 1;
ah_attr.grh.dgid = dest->gid;
ah_attr.grh.sgid_index = sgid_idx;
}
//创建AH
ctx->ah = ibv_create_ah(ctx->pd, &ah_attr);
if (!ctx->ah) {
fprintf(stderr, "Failed to create AH\n");
return 1;
}
return 0;
}
//函数原型
// pd: 保护域,ibv_pd,由ibv_alloc_pd生成
// attr: AH参数
struct ibv_ah *ibv_create_ah(struct ibv_pd *pd, struct ibv_ah_attr *attr)
//AH参数数据结构
struct ibv_ah_attr
{
struct ibv_global_route grh; //全局路由,详情见下
uint16_t dlid; //目标lid
uint8_t sl; //服务等级
uint8_t src_path_bits; //source path bits 源路径位?
uint8_t static_rate; //静态速度
uint8_t is_global; //这是一个全局地址,使用grh
uint8_t port_num; //为了到达当前地址所使用的物理端口号
};
struct ibv_global_route
{
union ibv_gid dgid; //目标gid
uint32_t flow_label;//流表格
uint8_t sgid_index; //源gid的序号
uint8_t hop_limit; //hop limit
uint8_t traffic_class; //traffic class
};
2.2.8 发送队列登记任务-pp_post_send
static int pp_post_send(struct pingpong_context *ctx, uint32_t qpn)
ibv_post_send
将WR列表登记到发送队列上。这一操作将初始化所有数据传输,包括RDMA操作。当出现第一个错误时,对于WR列表的处理将会停止,指向发生错误的WR的指针将会在参数bad_wr
中返回
struct ibv_sge list = {
.addr = (uintptr_t) ctx->buf + 40,
.length = ctx->size,
.lkey = ctx->mr->lkey
};
struct ibv_send_wr wr = {
.wr_id = PINGPONG_SEND_WRID,
.sg_list = &list, //散列表
.num_sge = 1, //散列表的条目数
.opcode = IBV_WR_SEND, //send操作
.send_flags = IBV_SEND_SIGNALED, //发送此WR的完成事件。仅对QP设置sq_sig_all为0的QP有用
.wr = {
.ud = {
.ah = ctx->ah,
.remote_qpn = qpn,
.remote_qkey = 0x11111111
}
}
};
struct ibv_send_wr *bad_wr;
return ibv_post_send(ctx->qp, &wr, &bad_wr);
//ibv_send_wr数据结构
struct ibv_send_wr
{
uint64_t wr_id; //用户分配的工作请求wr ID
struct ibv_send_wr *next; //指向下一个WR的指针,最后一个为NULL
struct ibv_sge *sg_list; //当前WR的散列表
int num_sge; //sg_list散列表的条目数
enum ibv_wr_opcode opcode;//操作编号 如IBV_WR_RDMA_WRITE,IBV_WR_SEND 等
enum ibv_send_flags send_flags;//(可选)-这是标志的按位或。如IBV_SEND_FENCE等
uint32_t imm_data;/* network byte order */ //以网络字节顺序发送的即时数据
union
{
struct
{
uint64_t remote_addr;//rdma/atomic操作时使用的远端虚拟地址
uint32_t rkey; //remote key(远端使用ibv_reg_mr获取),rdma/atomic操作使用
} rdma;
struct
{
uint64_t remote_addr;//同上
uint64_t compare_add;//用于比较和交换操作的比较数据
uint64_t swap;//交换数据
uint32_t rkey;//同上
} atomic;
struct
{
struct ibv_ah *ah;//数据包操作使用的ah
uint32_t remote_qpn;//远端qp编号
uint32_t remote_qkey;//qkey
} ud;
} wr;
uint32_t xrc_remote_srq_num; //shared receive queue (SRQ) number 仅用于XRC
};
2.2.9 获取完成事件
// channel: ibv_comp-channel 由ibv_create_comp_channel创建
// cq : 指向与事件关联的完成队列的指针
// cq_context :用户提供的上下文,由ibv_create_cq设置
if (ibv_get_cq_event(ctx->channel, &ev_cq, &ev_ctx)) {
fprintf(stderr, "Failed to get cq_event\n");
return 1;
}
ibv_get_cq_event
会等待特定完成通道(CC completion channel)的消息提醒。这是一个阻塞操作,用户应当分配一个ibv_cq
和一个void
给这个函数。他们会被返回适当的值,用户应当负责释放这些指针。
每一个消息提醒都必须通过ibv_ack_cq-event
进行确认。由于ibv_destory_cq
操作会等待所有事件被确认才执行。
当一个特定完成队列CQ的消息提醒被发送到了CC中,那么这个完成队列CQ就解除了和CC的绑定关系,知道再次通过ibv_req_notify_cq
绑定,CQ都不会再次发送消息提醒。
这个操作仅仅是通知一个CQ有一个完成队列条目(CQE)要处理,它并不会自己去处理这些CQE。用户需要通过ibv_poll_cq
来进行相关处理。
2.2.10 轮询完成队列
/*
* 从完成队列CQE中检索CQE
* @param cq: 结构ibv_cq,由ibv_create_cq创建
* @param num_entries: 返回的完成队列最大条目
* @return wc: 完成条目数组
* @return int:返回条目数量
*/
int ibv_poll_cq(struct ibv_cq *cq, int num_entries, struct ibv_wc *wc)
必须定期对CQ进行轮询,以防止超支。如果发生超支,则CQ将会被关闭并发送异步事件IBV_EVENT_CQ_ERR。