的聊天程序)
一个基于 RDMA-CMlibrdmacm的聊天程序——RDMA SEND/RECV 走 RoCEv2流量真实经过光纤而不是走内核 TCP。下面是完整程序——数据面走 RDMA SEND/RECVRoCEv2 over 光纤控制面用 RDMA-CM 建连API 风格接近 socket适合作为 verbs 编程的入门模板。rdma_chat.c/* * rdma_chat.c — 基于 RDMA-CM 的双终端聊天程序 * * 数据面走 RDMA SEND/RECVRoCEv2控制面由 rdma_cm 完成连接建立。 * 同机双卡自环两端各自绑定一张卡的 IP流量真实经过光纤。 * * 用法 * 服务端卡2所在终端: ./rdma_chat 10.10.1.2 listen * 客户端卡1所在终端: ./rdma_chat 10.10.1.1 10.10.1.2 * * 编译 * gcc -O2 -Wall -o rdma_chat rdma_chat.c -lrdmacm -libverbs */#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includefcntl.h#includesys/select.h#includearpa/inet.h#includerdma/rdma_cma.h#includeinfiniband/verbs.h#defineCHAT_PORT7471#defineBUF_SIZE1024staticstructibv_pd*pd;staticstructibv_cq*cq;staticstructibv_mr*send_mr,*recv_mr;staticchar*send_buf,*recv_buf;/* 投递一个接收请求RDMA 的 RECV 必须预投递 */staticintpost_recv(structrdma_cm_id*id){structibv_sgesge{.addr(uintptr_t)recv_buf,.lengthBUF_SIZE,.lkeyrecv_mr-lkey,};structibv_recv_wrwr{.wr_id1,.sg_listsge,.num_sge1,};structibv_recv_wr*bad;returnibv_post_recv(id-qp,wr,bad);}/* 用 RDMA SEND 发送一条消息 */staticintpost_send(structrdma_cm_id*id,intlen){structibv_sgesge{.addr(uintptr_t)send_buf,.lengthlen,.lkeysend_mr-lkey,};structibv_send_wrwr{.wr_id2,.sg_listsge,.num_sge1,.opcodeIBV_WR_SEND,.send_flagsIBV_SEND_SIGNALED,};structibv_send_wr*bad;returnibv_post_send(id-qp,wr,bad);}/* 创建 PD/CQ/QP 并注册收发内存然后预投递第一个 RECV */staticintsetup_qp(structrdma_cm_id*id){pdibv_alloc_pd(id-verbs);if(!pd){perror(ibv_alloc_pd);return-1;}cqibv_create_cq(id-verbs,64,NULL,NULL,0);if(!cq){perror(ibv_create_cq);return-1;}structibv_qp_init_attrattr{.send_cqcq,.recv_cqcq,.cap{.max_send_wr32,.max_recv_wr32,.max_send_sge1,.max_recv_sge1,},.qp_typeIBV_QPT_RC,};if(rdma_create_qp(id,pd,attr)){perror(rdma_create_qp);return-1;}send_bufmalloc(BUF_SIZE);recv_bufmalloc(BUF_SIZE);if(!send_buf||!recv_buf){perror(malloc);return-1;}send_mribv_reg_mr(pd,send_buf,BUF_SIZE,IBV_ACCESS_LOCAL_WRITE);recv_mribv_reg_mr(pd,recv_buf,BUF_SIZE,IBV_ACCESS_LOCAL_WRITE);if(!send_mr||!recv_mr){perror(ibv_reg_mr);return-1;}returnpost_recv(id);}/* 阻塞等待指定的 CM 事件 */staticstructrdma_cm_id*wait_event(structrdma_event_channel*ec,enumrdma_cm_event_typewant){structrdma_cm_event*ev;while(rdma_get_cm_event(ec,ev)0){structrdma_cm_id*idev-id;enumrdma_cm_event_typegotev-event;rdma_ack_cm_event(ev);if(gotwant)returnid;if(gotRDMA_CM_EVENT_REJECTED||gotRDMA_CM_EVENT_CONNECT_ERROR||gotRDMA_CM_EVENT_ADDR_ERROR||gotRDMA_CM_EVENT_ROUTE_ERROR){fprintf(stderr,连接失败CM 事件 %d\n,got);exit(1);}}returnNULL;}intmain(intargc,char**argv){if(argc!3){fprintf(stderr,用法:\n 服务端: %s 本端IP listen\n 客户端: %s 本端IP 对端IP\n,argv[0],argv[0]);return1;}intis_server(strcmp(argv[2],listen)0);structsockaddr_inlocal{0},peer{0};local.sin_familyAF_INET;local.sin_porthtons(CHAT_PORT);if(inet_pton(AF_INET,argv[1],local.sin_addr)!1){fprintf(stderr,本端 IP 格式错误\n);return1;}structrdma_event_channel*ecrdma_create_event_channel();if(!ec){perror(rdma_create_event_channel);return1;}structrdma_cm_id*id,*conn;if(rdma_create_id(ec,id,NULL,RDMA_PS_TCP)){perror(rdma_create_id);return1;}if(is_server){if(rdma_bind_addr(id,(structsockaddr*)local)){perror(rdma_bind_addr本端 IP 是否已配置在卡上);return1;}if(rdma_listen(id,1)){perror(rdma_listen);return1;}printf(等待对端连接%s:%d...\n,argv[1],CHAT_PORT);connwait_event(ec,RDMA_CM_EVENT_CONNECT_REQUEST);if(setup_qp(conn))return1;if(rdma_accept(conn,NULL)){perror(rdma_accept);return1;}wait_event(ec,RDMA_CM_EVENT_ESTABLISHED);}else{peerlocal;if(inet_pton(AF_INET,argv[2],peer.sin_addr)!1){fprintf(stderr,对端 IP 格式错误\n);return1;}if(rdma_resolve_addr(id,(structsockaddr*)local,(structsockaddr*)peer,2000)){perror(rdma_resolve_addr);return1;}wait_event(ec,RDMA_CM_EVENT_ADDR_RESOLVED);if(rdma_resolve_route(id,2000)){perror(rdma_resolve_route);return1;}wait_event(ec,RDMA_CM_EVENT_ROUTE_RESOLVED);connid;if(setup_qp(conn))return1;if(rdma_connect(conn,NULL)){perror(rdma_connect);return1;}wait_event(ec,RDMA_CM_EVENT_ESTABLISHED);}printf(已连接输入文字回车发送输入 quit 退出。\n );fflush(stdout);/* 握手完成后把事件通道设为非阻塞主循环轮询stdin CQ CM 事件 */fcntl(ec-fd,F_SETFL,fcntl(ec-fd,F_GETFL)|O_NONBLOCK);charline[BUF_SIZE];intrunning1;while(running){/* 1) 键盘输入 → RDMA SEND */fd_set rfds;FD_ZERO(rfds);FD_SET(STDIN_FILENO,rfds);structtimevaltv{0,50000};/* 50ms 轮询节拍 */if(select(STDIN_FILENO1,rfds,NULL,NULL,tv)0FD_ISSET(STDIN_FILENO,rfds)){if(!fgets(line,sizeofline,stdin))break;line[strcspn(line,\n)]0;if(strcmp(line,quit)0)break;if(line[0]!\0){strcpy(send_buf,line);if(post_send(conn,(int)strlen(send_buf)1))perror(post_send);printf( );fflush(stdout);}}/* 2) 完成队列 → 打印收到的消息 */structibv_wcwc[8];intnibv_poll_cq(cq,8,wc);for(inti0;in;i){if(wc[i].status!IBV_WC_SUCCESS){fprintf(stderr,\nCQ 错误: %s\n,ibv_wc_status_str(wc[i].status));running0;break;}if(wc[i].opcodeIBV_WC_RECV){printf(\r[对端] %s\n ,recv_buf);fflush(stdout);post_recv(conn);/* 重新投递接收 */}}/* 3) 对端断开检测 */structrdma_cm_event*ev;if(rdma_get_cm_event(ec,ev)0){if(ev-eventRDMA_CM_EVENT_DISCONNECTED){printf(\n对端已断开。\n);running0;}rdma_ack_cm_event(ev);}}rdma_disconnect(conn);rdma_destroy_qp(conn);ibv_dereg_mr(send_mr);ibv_dereg_mr(recv_mr);free(send_buf);free(recv_buf);ibv_destroy_cq(cq);ibv_dealloc_pd(pd);rdma_destroy_id(conn);rdma_destroy_event_channel(ec);return0;}编译# 如果还没装开发库sudoaptinstalllibibverbs-dev librdmacm-dev gcc-O2-Wall-ordma_chat rdma_chat.c-lrdmacm-libverbs运行两个终端各绑一张卡# 终端1 —— 服务端绑 18卡口2 的 IP./rdma_chat10.10.1.2 listen# 终端2 —— 客户端绑 17卡口2 的 IP连服务端./rdma_chat10.10.1.110.10.1.2rdma_resolve_addr会按本端 IP 选中对应的网卡/GID自动选 RoCEv2 的 GID连接建立后双向输入文字即聊quit退出。统计信息验证流量真的过了光纤聊天时在第三个终端观察计数器增长watch-n1sudo ethtool -S enp23s0f1np1 | grep -E ^ *(tx|rx)_packets | head -4# 或看 RDMA 端口计数watch-n1sudo cat /sys/class/infiniband/mlx5_1/ports/1/counters/port_rcv_packets 2/dev/null程序结构说明verbs 学习要点rdma_cm 控制面类似 socket verbs 数据面RDMA 本体 ───────────────────────────── ──────────────────────────── create_id / bind_addr / listen alloc_pd 保护域 resolve_addr / resolve_route create_cq 完成队列 accept / connect create_qp 队列对RC 模式 │ reg_mr 注册内存拿到 lkey ▼ ibv_post_recv 预投递接收 ESTABLISHED 之后 ─────────────────► ibv_post_send RDMA SEND 发送 ibv_poll_cq 轮询完成事件两个值得注意的点RECV 必须预投递——RDMA 没有 socket 那样的内核缓冲收到消息前必须先把接收缓冲区挂到 QP 上所以每收到一条就要post_recv补一个同机双卡自环时绑定不同卡的 IP 后 rdma_cm 会为两端 QP 解析出不同端口的 GID流量必须出光纤绕一圈才能到达对端——这是它区别于普通 socket 聊天会被内核回环短路的关键。如果跑的时候rdma_bind_addr报错先检查ip -br addr | grep f1确认两个口的 IP 还在rdma_connect失败则用show_gids确认两端都有 RoCEv2 的 GID。