基于 RDMA-CM(librdmacm)的聊天程序

📅 2026/7/19 18:24:20 👁️ 阅读次数 📝 编程学习
基于 RDMA-CM(librdmacm)的聊天程序

一个基于 RDMA-CM(librdmacm)的聊天程序——RDMA SEND/RECV 走 RoCEv2,流量真实经过光纤,而不是走内核 TCP。

下面是完整程序——数据面走 RDMA SEND/RECV(RoCEv2 over 光纤),控制面用 RDMA-CM 建连,API 风格接近 socket,适合作为 verbs 编程的入门模板。

rdma_chat.c

/* * rdma_chat.c — 基于 RDMA-CM 的双终端聊天程序 * * 数据面走 RDMA SEND/RECV(RoCEv2),控制面由 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 */#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<fcntl.h>#include<sys/select.h>#include<arpa/inet.h>#include<rdma/rdma_cma.h>#include<infiniband/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,.length=BUF_SIZE,.lkey=recv_mr->lkey,};structibv_recv_wrwr={.wr_id=1,.sg_list=&sge,.num_sge=1,};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,.length=len,.lkey=send_mr->lkey,};structibv_send_wrwr={.wr_id=2,.sg_list=&sge,.num_sge=1,.opcode=IBV_WR_SEND,.send_flags=IBV_SEND_SIGNALED,};structibv_send_wr*bad;returnibv_post_send(id->qp,&wr,&bad);}/* 创建 PD/CQ/QP 并注册收发内存,然后预投递第一个 RECV */staticintsetup_qp(structrdma_cm_id*id){pd=ibv_alloc_pd(id->verbs);if(!pd){perror("ibv_alloc_pd");return-1;}cq=ibv_create_cq(id->verbs,64,NULL,NULL,0);if(!cq){perror("ibv_create_cq");return-1;}structibv_qp_init_attrattr={.send_cq=cq,.recv_cq=cq,.cap={.max_send_wr=32,.max_recv_wr=32,.max_send_sge=1,.max_recv_sge=1,},.qp_type=IBV_QPT_RC,};if(rdma_create_qp(id,pd,&attr)){perror("rdma_create_qp");return-1;}send_buf=malloc(BUF_SIZE);recv_buf=malloc(BUF_SIZE);if(!send_buf||!recv_buf){perror("malloc");return-1;}send_mr=ibv_reg_mr(pd,send_buf,BUF_SIZE,IBV_ACCESS_LOCAL_WRITE);recv_mr=ibv_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*id=ev->id;enumrdma_cm_event_typegot=ev->event;rdma_ack_cm_event(ev);if(got==want)returnid;if(got==RDMA_CM_EVENT_REJECTED||got==RDMA_CM_EVENT_CONNECT_ERROR||got==RDMA_CM_EVENT_ADDR_ERROR||got==RDMA_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_family=AF_INET;local.sin_port=htons(CHAT_PORT);if(inet_pton(AF_INET,argv[1],&local.sin_addr)!=1){fprintf(stderr,"本端 IP 格式错误\n");return1;}structrdma_event_channel*ec=rdma_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);conn=wait_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{peer=local;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);conn=id;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];intrunning=1;while(running){/* 1) 键盘输入 → RDMA SEND */fd_set rfds;FD_ZERO(&rfds);FD_SET(STDIN_FILENO,&rfds);structtimevaltv={0,50000};/* 50ms 轮询节拍 */if(select(STDIN_FILENO+1,&rfds,NULL,NULL,&tv)>0&&FD_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];intn=ibv_poll_cq(cq,8,wc);for(inti=0;i<n;i++){if(wc[i].status!=IBV_WC_SUCCESS){fprintf(stderr,"\nCQ 错误: %s\n",ibv_wc_status_str(wc[i].status));running=0;break;}if(wc[i].opcode==IBV_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->event==RDMA_CM_EVENT_DISCONNECTED){printf("\n对端已断开。\n");running=0;}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.2

rdma_resolve_addr会按本端 IP 选中对应的网卡/GID(自动选 RoCEv2 的 GID),连接建立后双向输入文字即聊,quit退出。

统计信息:

验证流量真的过了光纤

聊天时在第三个终端观察计数器增长:

watch-n1"sudo ethtool -S enp23s0f1np1 | grep -E '^ *(tx|rx)_packets' | head -4"# 或看 RDMA 端口计数watch-n1'sudo 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 轮询完成事件

两个值得注意的点:

  1. RECV 必须预投递——RDMA 没有 socket 那样的内核缓冲,收到消息前必须先把接收缓冲区挂到 QP 上,所以每收到一条就要post_recv补一个;
  2. 同机双卡自环时,绑定不同卡的 IP 后 rdma_cm 会为两端 QP 解析出不同端口的 GID,流量必须出光纤绕一圈才能到达对端——这是它区别于普通 socket 聊天(会被内核回环短路)的关键。

如果跑的时候rdma_bind_addr报错,先检查ip -br addr | grep f1确认两个口的 IP 还在;rdma_connect失败则用show_gids确认两端都有 RoCEv2 的 GID。