RDMA编程-2 client端流程

本文介绍了RDMA客户端编程流程,包括获取地址信息、创建事件通道、队列对和内存区域等步骤,并提供了一个完整的客户端代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在上一篇建立RDMA侦听 部分讲了RDMA 通信时server端需要进行的操作,这一篇我们来说一下客户端的流程。

##client端流程
RDMA编程时的client端和socket编程也是大体类似的,只是有一些跟设备相关的细节,包括地址解析,queue pair, memory region 等要自己显式处理。

  1. rdma_getaddrinfo
    获取服务器的地址信息
  2. rdma_create_event_channel
    create channel to receive events
  3. rdma_create_id
    allocate an rdma_cm_id, this is conceptually similar to a socket
  4. rdma_resolve_addr
    obtain a local RDMA device to reach the remote address
  5. rdma_get_cm_event
    wait for RDMA_CM_EVENT_ADDR_RESOLVED event
  6. rdma_ack_cm_event
    ack event
  7. rdma_create_qp
    allocate a QP for the communication
  8. rdma_resolve_route
    determine the route to the remote address
  9. rdma_get_cm_event
    wait for RDMA_CM_EVENT_ROUTE_RESOLVED event
  10. rdma_ack_cm_event
    ack event
  11. rdma_connect
    connect to the remote server
  12. rdma_get_cm_event
    wait for RDMA_CM_EVENT_ESTABLISHED event
  13. rdma_ack_cm_event
    ack event
  14. ibv_post_send
    Perform data transfers over connection
  15. rdma_disconnect
    tear-down connection
  16. rdma_get_cm_event
    wait for RDMA_CM_EVENT_DISCONNECTED event
  17. rdma_ack_cm_event
    ack event
  18. rdma_destroy_qp
    destroy the QP
  19. rdma_destroy_id
    release the rdma_cm_id
  20. rdma_destroy_event_channel
    release the event channel

下面是一个完整的client代码,这个代码可以配合建立RDMA侦听 里的server端代码进行测试。

测试时需要注意,不要使用使用localhost/127.0.0.1这样的回环地址去连接server端,回环地址会绕过RDMA网卡,因此无法进行RDMA通信。

//compile :  gcc rdma-client.cpp -libverbs -lrdmacm -o rdma-client

#define _ISOC11_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <infiniband/verbs.h>
#include <rdma/rdma_cma.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>


struct rdma_event_channel* ec;
struct rdma_cm_id* cm_id;
struct ibv_pd* pd;
struct ibv_context* rdma_cm_context;
void* recv_buf;
#define RECV_BUF_SIZE 4096
struct ibv_mr* recv_mr;



int on_route_resolved(struct rdma_cm_id* id)
{
	int rc = 0;
	struct rdma_conn_param cm_params;
	struct ibv_qp_init_attr qp_attr={0};
	struct PfRdmaConnection* conn = (struct PfRdmaConnection*)id->context;
	
	struct ibv_context* rdma_context = id->verbs;
	pd = ibv_alloc_pd(rdma_context);
	recv_buf = aligned_alloc(4096, RECV_BUF_SIZE);
	recv_mr = ibv_reg_mr(pd, recv_buf, RECV_BUF_SIZE, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);	
	
	struct ibv_comp_channel* comp_channel = ibv_create_comp_channel(rdma_context);
	struct ibv_cq* cq = ibv_create_cq(rdma_context, 512, NULL, comp_channel, 0);
	ibv_req_notify_cq(cq, 0);


	
    qp_attr.send_cq = cq;
    qp_attr.recv_cq = cq;
    qp_attr.qp_type = IBV_QPT_RC;
    qp_attr.cap.max_send_wr = 512;
    qp_attr.cap.max_recv_wr = 512;
    qp_attr.cap.max_send_sge = 1;
    qp_attr.cap.max_recv_sge = 1;
	rc = rdma_create_qp(id, pd, &qp_attr);
	if (rc)
	{
		perror("create_qp failed, errno:%d");
        return rc;
	}
	

	memset(&cm_params, 0, sizeof(cm_params));

	cm_params.private_data = NULL;
	cm_params.private_data_len = 0;
	cm_params.responder_resources = (uint8_t)8;
	cm_params.initiator_depth = (uint8_t)8;
	cm_params.retry_count = 7;
	cm_params.rnr_retry_count = 7;
	rc = rdma_connect(id, &cm_params);
	if(rc)
	{
		perror("rdma_connect failed, errno");
		return rc;
	}
	return 0;
}




int main(int argc, char** argv)
{
	
	int port=10121;
	
	
	int rc = 0;
	if(argc != 2){
		fprintf(stderr, "Usage: rdma-client <server_ip>\n");
		return 1;
	}
	char* ip_str=argv[1];
	struct addrinfo* addr;
	rc = getaddrinfo(ip_str, NULL, NULL, &addr);
	if(rc)
	{
		perror("getaddrinfo failed, ");
		return rc;
	}
	((struct sockaddr_in*)addr->ai_addr)->sin_port = htons((uint16_t)port);
	printf("connecting to server ip:%s...\n", ip_str);
	
	ec = rdma_create_event_channel();
	if(ec == NULL)
	{
		perror("rdma_create_event_channel failed, errno:");
		return errno;
	}
	rc = rdma_create_id(ec, &cm_id, NULL, RDMA_PS_TCP);
	if(rc)
	{
		perror("rdma_create_id failed, errno");
		return rc;
	}
	
	rc = rdma_resolve_addr(cm_id, NULL, addr->ai_addr, 500);
	if(rc)
	{
		perror("rdma_resolve_addr failed, errno:");
		return rc;
	}
	
	printf("Begin process cm event...\n");
	struct rdma_cm_event *event = NULL;
	while(rdma_get_cm_event(ec, &event) == 0) {
		struct rdma_cm_event event_copy;
		memcpy(&event_copy, event, sizeof(*event));
		rdma_ack_cm_event(event);
		switch(event_copy.event)
		{
			case RDMA_CM_EVENT_ADDR_RESOLVED:
				printf("get event RDMA_CM_EVENT_ADDR_RESOLVED\n");
				rc = rdma_resolve_route(event_copy.id, 2000);
				if(rc)
				{
					perror("rdma_resolve_route failed, errno:");
					return rc;
				}
				break;
			case RDMA_CM_EVENT_ROUTE_RESOLVED:
				printf("get event RDMA_CM_EVENT_ROUTE_RESOLVED\n");
				on_route_resolved(event_copy.id);
				break;
			case RDMA_CM_EVENT_ESTABLISHED:
				printf("get event RDMA_CM_EVENT_ESTABLISHED\n");
				{
					struct ibv_send_wr wr, *bad_wr = NULL;
					struct ibv_sge sge;


					memset(&wr, 0, sizeof(wr));
					wr.wr_id = (uint64_t)1;
					wr.next = NULL;
					wr.sg_list = &sge;
					wr.num_sge = 1;
					wr.opcode = IBV_WR_SEND;
					wr.send_flags = IBV_SEND_SIGNALED;
					strcpy((char*)recv_buf, "HelloWorld"); 
					sge.addr = (uint64_t)recv_buf;
					sge.length = strlen((char*)recv_buf);
					sge.lkey = recv_mr->lkey;
					printf("send data to server\n");
					rc = ibv_post_send(cm_id->qp, &wr, &bad_wr);
					if (rc)
					{
						fprintf(stderr, "ibv_post_send failed, rc:%d\n", rc);
						return rc;
					}
					return 0;
				}
				break;
			case RDMA_CM_EVENT_DISCONNECTED:
				printf("get event RDMA_CM_EVENT_DISCONNECTED\n");
				
				break;
			default:
				break;
		}
	}
	freeaddrinfo(addr);
	return 0;
}


通过这两篇文章,读者应该可以快速的建立一个自己的RDMA通信程序了。在上面的例子里面,server端使用了IBV_RECEIVE操作,client端使用了IBV_SEND操作。这个意义上还是和socket编程很像的。虽然这样操作也避免了类似TCP协议栈在CPU上的开销,但这远不是RDMA的精髓。RDMA总共提供了4个操作,分成两类:

  1. 双边API (two-sided), 这类API的操作需要server, client双方配合,需要一边先post_recv, 然后另外一边post_send,两个操作完成一次传输。这类操作就是我们例子里用到的
    • IBV_SEND
    • IBV_RECEIVE
  2. 单边API (one-sided), 这类API只需要server或者client一边调用API,就可以从另一边的内存里面读写数据。包括
    • IBV_READ
    • IBV_WRITE

要想用好RDMA, 就要将这两类API配合使用好。往往需要结合业务设计出最合适的协议模型。也欢迎大家到我的开源存储项目PureFlash ,这是一个RDMA在实际项目应用的具体例子。

<think>嗯,用户让我分析另一个内核函数`enable_device_and_get`。之前他问过`rdmacg_register_device`,现在这个看起来也是设备注册相关的,不过属于IB子系统。用户可能是想深入理解Linux内核中设备注册启用的流程,或者是在开发或调试相关驱动时遇到了问题。 首先,我需要确定这个函数的功能。根据注释,这个函数的作用是启用设备,使其对所有客户端返回设备指针的公共API可见。即使失败,也会返回一个新的get。这里的“get”可能是指引用计数,所以需要关注refcount的操作。 接下来看函数结构。函数开始设置refcount为2,注释说一个是给xa,另一个是当前线程,防止并行注销。这可能涉及到并发控制,使用读写信号量devices_rwsem。然后设置设备标记为已注册,降级写锁,执行enable_driver回调,之后遍历已注册的客户端,添加上下文,最后处理兼容设备。 用户可能想知道每个步骤的作用,比如为什么refcount设置为2,读写锁的作用,以及各个步骤如何保证线程安全。另外,用户可能对错误处理路径感兴趣,比如enable_driver失败后的处理,或者add_client_context出错时的流程。 需要注意的是,用户之前的问题关于rdmacg,现在转向IB设备的启用,可能是在研究设备资源管理启用的不同阶段。需要将两者联系起来,说明不同子系统之间的协作,比如rdmacg注册设备后,如何通过enable_device_and_get使其对客户端可见。 可能用户是内核开发者,或者系统程序员,需要了解设备初始化的细节,以便调试或扩展功能。因此,回答要详细解释每一步,特别是并发控制引用计数管理,这两个是内核编程中的关键点。 另外,注释中提到即使失败也会返回新的get,所以在错误路径中是否调整了refcount?查看代码,发现如果enable_driver失败,会跳到out标签,释放读锁,但refcount可能没有被释放?需要检查是否有对应的put操作,可能在调用此函数后处理。 最后,总结函数的作用流程,强调其如何确保设备安全地对客户端可见,同时处理并发注册注销的情况,以及错误处理机制。需要确保解释清晰,结构合理,帮助用户逐步理解。</think>以下是对`enable_device_and_get`函数的逐步解析: --- ### **功能概述** `enable_device_and_get`是Linux内核中用于**启用IB(InfiniBand)设备**并使其对客户端可见的关键函数。它的核心作用包括: 1. 通过引用计数锁机制保证设备注册的**线程安全性**。 2. 调用设备驱动实现的`enable_driver`回调完成硬件初始化。 3. 将设备与已注册的客户端(如用户态API、内核模块)绑定。 --- ### **参数说明** - `struct ib_device *device` 指向需要启用的IB设备,包含设备操作函数指针(如`enable_driver`)资源信息。 --- ### **代码逐层分析** #### **1. 初始化引用计数** ```c refcount_set(&device->refcount, 2); ``` - **目的**:设置设备的引用计数为2- 1个引用属于`xa`(设备索引数组,用于全局管理设备)。 - 1个引用属于当前线程,防止在启用过程中设备被意外注销。 - **线程安全**:通过引用计数确保设备不会被并发释放。 --- #### **2. 加写锁并标记设备为已注册** ```c down_write(&devices_rwsem); xa_set_mark(&devices, device->index, DEVICE_REGISTERED); ``` - **锁机制**:使用**读写信号量`devices_rwsem`**的写锁,确保此时其他线程无法修改设备状态。 - **标记设备**:在全局设备数组`devices`中,通过`xa_set_mark`设置设备的`DEVICE_REGISTERED`标志,表示设备已注册完成。 --- #### **3. 降级为读锁** ```c downgrade_write(&devices_rwsem); ``` - **优化锁粒度**:将写锁降级为读锁,允许其他线程并发读取设备列表,但阻止它们修改设备状态。 - **目的**:在后续操作(如调用驱动回调)期间保持设备可见性,同时避免长时间持有写锁导致性能问题。 --- #### **4. 调用设备驱动的启用回调** ```c if (device->ops.enable_driver) { ret = device->ops.enable_driver(device); if (ret) goto out; } ``` - **驱动初始化**:调用设备特定的`enable_driver`函数(由驱动开发者实现),通常用于硬件初始化或资源配置。 - **错误处理**:若回调失败,直接跳转到`out`标签释放锁并返回错误码。 --- #### **5. 绑定已注册的客户端** ```c down_read(&clients_rwsem); xa_for_each_marked (&clients, index, client, CLIENT_REGISTERED) { ret = add_client_context(device, client); if (ret) break; } up_read(&clients_rwsem); ``` - **遍历客户端**:使用`xa_for_each_marked`遍历所有标记为`CLIENT_REGISTERED`的客户端(如用户态API、内核模块)。 - **绑定设备与客户端**:对每个客户端调用`add_client_context`,将设备上下文添加到客户端结构中,使客户端能访问该设备。 --- #### **6. 处理兼容性设备** ```c if (!ret) ret = add_compat_devs(device); ``` - **兼容性支持**:若前述步骤成功(`ret == 0`),调用`add_compat_devs`为旧版本接口或兼容性需求创虚拟设备。 --- #### **7. 释放锁并返回结果** ```c out: up_read(&devices_rwsem); return ret; ``` - **释放读锁**:无论成功或失败,最终释放`devices_rwsem`的读锁。 - **返回值**:返回错误码`ret`(0表示成功,负数表示失败)。 --- ### **关键机制与设计** 1. **引用计数(refcount)** - 初始值为2,确保设备在启用过程中不会被释放。 - 后续需通过`ib_device_put()`减少引用计数(未在本函数中显式调用)。 2. **锁的降级(downgrade_write)** - 减少锁竞争:在完成关键写操作后降级为读锁,允许其他线程并发读取设备列表。 3. **客户端动态绑定** - 支持热插拔:新设备启用时自动绑定到已存在的客户端,无需重启服务。 --- ### **实际应用场景** 假设一个IB网卡驱动被加载时: 1. 内核调用`enable_device_and_get`启用设备。 2. 设备被标记为`DEVICE_REGISTERED`,对用户态API(如`ibv_get_device_list`)可见。 3. 用户态应用(如MPI库)可通过该设备创队列对(QP)进行RDMA通信。 --- ### **错误处理路径** - **`enable_driver`失败**:直接跳转到`out`释放锁,返回错误码(如硬件初始化失败)。 - **`add_client_context`失败**:中断客户端绑定循环,返回错误码(如内存分配失败)。 --- ### **总结** 该函数是IB设备启用的核心逻辑,通过**锁机制****引用计数**保证线程安全,同时动态绑定客户端与兼容性设备。其设计平衡了性能(锁降级)与可靠性(引用计数),是Linux内核中设备管理模型的典型实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值