前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux源码分析-RDMA的通信连接管理CM模块

Linux源码分析-RDMA的通信连接管理CM模块

原创
作者头像
ssbandjl
修改2024-04-30 07:59:13
870
修改2024-04-30 07:59:13
举报
文章被收录于专栏:Linux内核DPULinux内核

术语

主动端(active)

被动端(passive)

简介

RDMA CM 是一种通信管理器,用于设置可靠、连接和不可靠的数据报数据传输。 它提供用于建立连接的 RDMA 传输中立接口。 API 概念基于套接字,但适用于基于队列对 (QP) 的语义:通信必须通过特定的 RDMA 设备进行,并且数据传输基于消息。 RDMA CM 可以控制 RDMA API 的 QP 和通信管理(连接建立/拆除)部分,或者仅控制通信管理部分。 它与 libibverbs 库定义的 verbs API 结合使用。 libibverbs 库提供了发送和接收数据所需的底层接口。 RDMA CM 可以异步或同步操作。 用户通过在特定调用中使用 rdma_cm 事件通道参数来控制操作模式。 如果提供了事件通道,rdma_cm 标识符将报告该通道上的事件数据(例如连接结果)。 如果未提供通道,则所选 rdma_cm 标识符的所有 rdma_cm 操作将被阻止,直到完成。 RDMA CM 为不同的 libibverbs 提供商提供了一个选项来宣传和使用特定于该提供商的各种 QP 配置选项。 此功能称为 ECE(增强连接建立)

RDMA CM编程参考模型

rdma_cm 支持通过 libibverbs 库和接口提供的所有verbs。 但是,它还为一些更常用的verbs功能提供了包装函数。 完整的抽象verbs调用集是:

代码语言:javascript
复制
rdma_reg_msgs - 注册用于发送和的缓冲区数组接收
rdma_reg_read - 为 RDMA 读取操作注册缓冲区
rdma_reg_write - 为 RDMA 写操作注册缓冲区
rdma_dereg_mr - 取消注册内存区域
rdma_post_recv - 发布缓冲区以接收消息
rdma_post_send - 发布缓冲区以发送消息
rdma_post_read - 发布 RDMA 以将数据读入缓冲区
rdma_post_write - 发布 RDMA 以从缓冲区发送数据
rdma_post_recvv - 发布缓冲区向量以接收消息
rdma_post_sendv - 发布缓冲区向量来发送消息
rdma_post_readv - 发布缓冲区向量以接收 RDMA读
rdma_post_writev - 发布缓冲区向量以发送 RDMA 写入
rdma_post_ud_send - 发布缓冲区以在 UD QP 上发送消息
rdma_get_send_comp - 获取发送或 RDMA 的完成状态操作
rdma_get_recv_comp - 获取有关已完成接收的信息

客户端操作

本节提供通信主动方(active)或客户端的基本操作的总体概述。 此流程假定异步操作,并显示低级调用详细信息。 对于同步操作,将消除对 rdma_create_event_channel、rdma_get_cm_event、rdma_ack_cm_event 和 rdma_destroy_event_channel 的调用。 抽象调用(例如 rdma_create_ep)将其中多个调用封装在单个 API 下。 用户还可以参考示例应用程序来获取代码示例。 一般的连接流程是:

代码语言:javascript
复制
rdma_getaddrinfo 检索目的地的地址信息
rdma_create_event_channel 创建接收事件的通道
rdma_create_id 分配一个 rdma_cm_id,这在概念上类似于套接字
rdma_resolve_addr 获取本地RDMA设备以到达远程地址
rdma_get_cm_event 等待 RDMA_CM_EVENT_ADDR_RESOLVED 事件
rdma_ack_cm_event 确认事件
rdma_create_qp 为通信分配一个QP
rdma_resolve_route 确定到远程地址的路由
rdma_get_cm_event 等待 RDMA_CM_EVENT_ROUTE_RESOLVED 事件
rdma_ack_cm_event 确认事件
rdma_connect 连接远程服务器
rdma_get_cm_event 等待 RDMA_CM_EVENT_ESTABLISHED 事件
rdma_ack_cm_event 确认事件
?
通过连接执行数据传输
rdma_disconnect 断开连接
rdma_get_cm_event 等待 RDMA_CM_EVENT_DISCONNECTED 事件
rdma_ack_cm_event 确认事件
rdma_destroy_qp 销毁 QP
rdma_destroy_id 释放rdma_cm_id
rdma_destroy_event_channel 释放事件通道
rdma_set_local_ece 设置所需的 ECE 选项

在服务端/被动方也是以上类似的操作, 用于在节点之间设置不可靠的数据报(UD)通信。 然而,QP 之间没有形成实际连接,因此不需要断开连接。 尽管此示例显示客户端发起/断开连接,但连接的任何一方都可以发起断开连接

服务端操作

本节概述了通信的被动端或服务器端的基本操作。 一般的连接流程是

代码语言:javascript
复制
rdma_create_event_channel 创建接收事件的通道
rdma_create_id 分配一个rdma_cm_id,这在概念上类似于套接字
rdma_bind_addr 设置本地监听的端口号
rdma_listen 开始监听连接请求
rdma_get_cm_event 等待具有新 rdma_cm_id 的 RDMA_CM_EVENT_CONNECT_REQUEST 事件
rdma_create_qp 为新 rdma_cm_id 上的通信分配 QP
rdma_accept 接受连接请求
rdma_ack_cm_event 确认事件
rdma_get_cm_event 等待 RDMA_CM_EVENT_ESTABLISHED 事件
rdma_ack_cm_event 确认事件
?
通过连接执行数据传输
rdma_get_cm_event 等待 RDMA_CM_EVENT_DISCONNECTED 事件
rdma_ack_cm_event 确认事件
rdma_disconnect 断开连接
rdma_destroy_qp 销毁 QP
rdma_destroy_id 释放连接的rdma_cm_id
rdma_destroy_id 释放监听的rdma_cm_id
rdma_destroy_event_channel 释放事件通道
rdma_get_remote_ece 获取客户端发送的ECe选项
rdma_set_local_ece 设置所需的 ECE 选项

其他调用参考

代码语言:javascript
复制
rdma_accept(3), rdma_ack_cm_event(3), rdma_bind_addr(3),
rdma_connect(3), rdma_create_ep(3), rdma_create_event_channel(3),
rdma_create_id(3), rdma_create_qp(3), rdma_dereg_mr(3),
rdma_destroy_ep(3), rdma_destroy_event_channel(3),
rdma_destroy_id(3), rdma_destroy_qp(3), rdma_disconnect(3),
rdma_event_str(3), rdma_free_devices(3), rdma_getaddrinfo(3),
rdma_get_cm_event(3), rdma_get_devices(3), rdma_get_dst_port(3),
rdma_get_local_addr(3), rdma_get_peer_addr(3),
rdma_get_recv_comp(3), rdma_get_remote_ece(3),
rdma_get_request(3), rdma_get_send_comp(3), rdma_get_src_port(3),
rdma_join_multicast(3), rdma_leave_multicast(3), rdma_listen(3),
rdma_migrate_id(3), rdma_notify(3), rdma_post_read(3)
rdma_post_readv(3), rdma_post_recv(3), rdma_post_recvv(3),
rdma_post_send(3), rdma_post_sendv(3), rdma_post_ud_send(3),
rdma_post_write(3), rdma_post_writev(3), rdma_reg_msgs(3),
rdma_reg_read(3), rdma_reg_write(3), rdma_reject(3),
rdma_resolve_addr(3), rdma_resolve_route(3),
rdma_get_remote_ece(3), rdma_set_option(3), mckey(1),
rdma_client(1), rdma_server(1), rping(1), ucmatose(1), udaddy(1)

源码调用栈

以RDMA上层通信框架libfabric为例

代码语言:javascript
复制
server: 服务端创建事件通道,创建通信标识ID, 启动RDMA监听
rdma_create_event_channel <- vrb_eq_open <- fi_eq_open
rdma_create_id <- vrb_create_ep <- vrb_open_ep <- fi_endpoint, librdmacm/cma.c -> rdma_create_event_channel HG创建端点 na_ofi_basic_ep_open
rdma_listen -> fi_listen -> .listen = vrb_pep_listen -> vrb_pep_listen -> rdma_listen, na_ofi_basic_ep_open -> fi_enable -> rxm_ep_ctrl -> rxm_start_listen -> fi_listen
?
if (ofi_epoll_add(_eq->epollfd, _eq->channel->fd, OFI_EPOLL_IN, NULL)) -> 将rdma事件通道的fd关联到eq的epollfd
?
client:客户端创建事件通道,创建通信标识ID, 解析服务端地址, 发送数据时, 获取连接, 解析路由
rdma_create_event_channel
rdma_create_id
rdma_resolve_addr -> RDMA_CM_EVENT_ADDRESS_RESOLVED ->  rxm_open_conn -> fi_endpoint (vrb_open_ep) -> rdma_resolve_addr HG -> HG_Trigger -> hg_op_id->callback(&hg_cb_info) 查询地址设置的回调 lookup_callback ->  HG_Forward -> NA_Msg_send_unexpected -> fi_senddata -> rxm_get_conn -> fi_endpoint -> vrb_open_ep -> vrb_create_ep -> rdma_resolve_addr
rdma_resolve_route -> RDMA_CM_EVENT_ROUTE_RESOLVED -> rxm_send_connect -> fi_connect -> rdma_resolve_route 也是HG发送的时候建立连接
------------------ 
分配RDMA结构(服务端和客户端对等, 均要执行), 查询网卡, 分配保护域, 创建完成通道,完成队列, 通知完成队列准备好接收完成事件, 创建队列对, 注册内存
ibv_query_device <- fi_getinfo -> vrb_getinfo -> ibv_query_device
ibv_alloc_pd <- fi_domain -> rxm_domain_open -> ibv_alloc_pd
ibv_create_comp_channel -> na_ofi_eq_open -> fi_cq_open -> vrb_cq_open -> ibv_create_comp_channel
ibv_create_cq -> na_ofi_eq_open -> fi_cq_open -> vrb_cq_open -> ibv_create_cq
ibv_req_notify_cq -> na_ofi_poll_try_wait -> fi_trywait -> vrb_trywait -> vrb_cq_trywait
rdma_create_qp -> na_ofi_context_create -> fi_enable -> rdma_create_qp
ibv_reg_mr -> NA_Mem_register -> na_ofi_mem_register -> fi_mr_regv -> ibv_reg_mr
------------------
轮询完成队列
ibv_poll_cq -> na_ofi_msg_send_unexpected -> fi_senddata -> fi_send -> vrb_flush_cq -> ibv_poll_cq
?
接收端提前往接收队列放置工作请求WR
ibv_post_recv -> rxm_open_conn -> ibv_post_recv | na_ofi_tag_recv, na_ofi_msg_multi_recv -> fi_trecv -> ibv_post_recv
?
客户端与服务端建立连接
rdma_connect -> server -> RDMA_CM_EVENT_CONNECT_REQUEST, -> fi_senddata -> rxm_get_conn -> rdma_connect
?
server:
case RDMA_CM_EVENT_CONNECT_REQUEST
------------------ 
分配RDMA结构
ibv_query_device
ibv_alloc_pd
ibv_create_comp_channel
ibv_create_cq
ibv_req_notify_cq
rdma_create_qp
ibv_reg_mr
------------------
ibv_post_recv
rdma_accept
RDMA_CM_EVENT_ESTABLISHED
ibv_post_send
?
clinet: 客户端发送非预期消息
RDMA_CM_EVENT_ESTABLISHED
ibv_post_send -> na_ofi_msg_send_unexpected -> ibv_post_send
?
销毁资源
server:
rdma_disconnect
?
ibv_dereg_mr
ibv_destroy_cq
ibv_destroy_comp_channel
rdma_destroy_qp
?
rdma_destroy_id
rdma_destroy_event_channel
?
client:
rdma_disconnect
?
ibv_dereg_mr
ibv_destroy_cq
ibv_destroy_comp_channel
rdma_destroy_qp
?
rdma_destroy_id
rdma_destroy_event_channel

以RDMA用户态驱动中CM服务端为例

代码语言:javascript
复制
librdmacm/examples/rdma_server.c -> main
examples/rdma_server.c -> main -> run
  static const char *server = "0.0.0.0";
  static const char *port = "7471";
    hints.ai_flags = RAI_PASSIVE;
    hints.ai_port_space = RDMA_PS_TCP
    rdma_getaddrinfo
        ucma_init
      sync_devices_list
        ibv_get_device_list
        insert_cma_dev
          cma_dev->guid = ibv_get_device_guid(dev)
          list_add_after(&cma_dev_list, &p->entry, &cma_dev->entry)
      ucma_set_af_ib_support
    ucma_getaddrinfo
      ucma_convert_to_rai
  rdma_create_ep
    ucma_passive_ep -> librdmacm:在 rdma_create_ep 中添加对被动端的支持,允许调用 rdma_create_ep 来监听 rdma_cm_id
      rdma_bind_addr2
  rdma_listen -> CMA_INIT_CMD(&cmd, sizeof cmd, LISTEN) -> ucma_listen
    ucma_query_addr
  rdma_get_request
    rdma_get_cm_event
      retry:
      CMA_INIT_CMD_RESP(&cmd, sizeof cmd, GET_EVENT, &resp, sizeof resp) -> ucma_get_event
  ibv_query_qp
  rdma_reg_msgs
  rdma_post_recv
  rdma_accept(id, NULL) -> librdmacm/rdma_server:添加新的示例服务器应用程序,提供一个简单的服务器应用程序来演示接受来自客户端的连接请求并交换消息所需的最少编码
        ucma_valid_param(id_priv, conn_param)
        ucma_modify_qp_rtr -> librdmacm:允许用户指定最大 RDMA 资源,允许用户指示库应选择建立连接时应使用的最大可用 RDMA 读取值。 库根据本地硬件限制和连接请求数据选择最大值
            qp_attr.qp_state = IBV_QPS_INIT
            rdma_init_qp_attr(id, &qp_attr, &qp_attr_mask)
                CMA_INIT_CMD_RESP(&cmd, sizeof cmd, INIT_QP_ATTR, &resp, sizeof resp) -> ucma_init_qp_attr
            ibv_modify_qp(id->qp, &qp_attr, qp_attr_mask
            qp_attr.qp_state = IBV_QPS_RTR
            rdma_init_qp_attr(id, &qp_attr, &qp_attr_mask)
            rdma_seterrno(ibv_modify_qp(id->qp, &qp_attr, qp_attr_mask))
        ucma_modify_qp_rts
        CMA_INIT_CMD(&cmd, sizeof cmd, ACCEPT) -> ucma_accept
        ucma_complete(id)
            rdma_get_cm_event(id_priv->id.channel, &id_priv->id.event)
  rdma_get_recv_comp
  rdma_post_send
  rdma_get_send_comp

创建事件通道rdma_create_event_channel

代码语言:javascript
复制
以下是部分接口详解:
创建事件通道:
rdma_create_event_channel - 打开用于报告通信事件的通道。 描述:异步事件通过事件通道上报给用户。 每个事件通道映射到一个文件描述符。 注意:所有创建的事件通道必须通过调用 rdma_destroy_event_channel 销毁。 用户应调用 rdma_get_cm_event 来检索事件通道上的事件。 另请参见:rdma_get_cm_event、rdma_destroy_event_channel, 流程: 查询获取所有IB设备,存放在cma_dev_array全局数组中;检测是否支持AF_IB协议, 打开CM的fd, 返回事件
struct rdma_event_channel *rdma_create_event_channel(void)
  ucma_init()
  channel->fd = open_cdev(dev_name, dev_cdev) -> 打开fd /dev/infiniband/rdma_cm
  返回通道
  
用户态完整调用栈:
struct rdma_event_channel *rdma_create_event_channel(void)
    ucma_init
        if (cma_dev_cnt)
        check_abi_version
        ibv_get_device_list
        cma_dev_array = calloc(dev_cnt, sizeof(*cma_dev_array))
        cma_dev_array[i].guid = ibv_get_device_guid(dev_list[i])
        ucma_set_af_ib_support()
            rdma_create_id(NULL, &id, NULL, RDMA_PS_IB)
                rdma_create_id2
                    ucma_init
                    ucma_alloc_id
            rdma_create_event_channel -> to kernel -> ucma_open -> bpftrace -e 'kprobe:ucma_open{ printf("bt:%s\n", kstack); }'
                    CMA_INIT_CMD_RESP(&cmd, sizeof cmd, CREATE_ID, &resp, sizeof resp) -> UCMA_CMD_CREATE_ID -> ucma_create_id
                    ret = write(id_priv->id.channel->fd, &cmd, sizeof cmd)
          VALGRIND_MAKE_MEM_DEFINED(&resp, sizeof resp)
                    ucma_insert_id(id_priv)
            idm_set(&ucma_idm, id_priv->handle, id_priv)
            rdma_bind_addr(id, (struct sockaddr *) &sib)
        rdma_bind_addr2
          CMA_INIT_CMD(&cmd, sizeof cmd, BIND)
          ucma_query_addr
          ucma_query_gid
    channel->fd = open("/dev/infiniband/rdma_cm", O_RDWR | O_CLOEXEC)
    return channel

分配通信标识rdma_create_id -> ucma_create_id

代码语言:javascript
复制
分配通信标识
int rdma_create_id(struct rdma_event_channel *channel, struct rdma_cm_id **id, void *context, enum rdma_port_space ps)
cmd = UCMA_CMD_CREATE_ID
ret = write(id_priv->id.channel->fd, &cmd, sizeof cmd) -> 通知内核
ucma_insert_id(id_priv)
  idm_set -> librdmacm:定义通过 RDMA 接口 (rsockets) 的流式传输,引入了一组新的 API,支持 RDMA 设备上的字节流接口。 新接口与套接字匹配,只是所有函数调用都以“r”为前缀。 定义了以下函数: rsocket rbind、rlisten、raccept、rconnect rshutdown、rclose rrecv、rrecvfrom、rrecvmsg、rread、rreadv rsend、rsendto、rsendmsg、rwrite、rwritev rpoll、rselect rgetpeername、rgetsockname rsetsockopt、rgetsockopt、rfcntl 函数采用相同的方法 参数与用于套接字的参数相同。 目前支持以下功能和标志: PF_INET、PF_INET6、SOCK_STREAM、IPPROTO_TCP MSG_DONTWAIT、MSG_PEEK SO_REUSEADDR、TCP_NODELAY、SO_ERROR、SO_SNDBUF、SO_RCVBUF O_NONBLOCK rpoll 调用支持轮询 rsockets 和普通 fd, 
  index_map(二级指针): 索引映射 - 将结构与索引关联起来。 同步必须由调用者提供。 调用者必须通过将索引映射设置为 0 来初始化它
  提供一组索引操作接口, 设置,插入(idx_insert),增长(idx_grow),替换,移除,清理等
  rsocket是附在rdma_cm库中的一个子模块,提供了完全类似于socket接口的rdma调用
  对于rdma编程,目前主流实现是利用rdma_cm来建立连接,然后利用verbs来传输数据。  rdma_cm和ibverbs分别会创建一个fd,这两个fd的分工不同。rdma_cm fd主要用于通知建连相关的事件,verbs fd则主要通知有新的cqe发生。当直接对rdma_cm fd进行poll/epoll监听时,此时只能监听到POLLIN事件,这意味着有rdma_cm事件发生。当直接对verbs fd进行poll/epoll监听时,同样只能监听到POLLIN事件,这意味着有新的cqe  作者:异客z 链接:https://www.jianshu.com/p/4d71f1c8e77c
?
内核态调用栈:
?
rdma_create_id
static ssize_t ucma_write
    ret = ucma_cmd_table[hdr.cmd](file, buf + sizeof(hdr), hdr.in, hdr.out) -> ucma_create_id
        ctx = ucma_alloc_ctx(file)
        cm_id = rdma_create_user_id(ucma_event_handler, ctx, cmd.ps, qp_type)
            __rdma_create_id
                id_priv->state = RDMA_CM_IDLE
                id_priv->id.event_handler = event_handler <- cma_listen_handler
                id_priv->gid_type = IB_GID_TYPE_IB
                id_priv->seq_num &= 0x00ffffff
                rdma_restrack_new(&id_priv->res, RDMA_RESTRACK_CM_ID)
                if (parent)
                    rdma_restrack_parent_name
                        rdma_restrack_attach_task
            rdma_restrack_set_name
                rdma_restrack_attach_task
        ucma_set_ctx_cm_id(ctx, cm_id)
        ucma_finish_ctx(ctx)
            list_add_tail(&ctx->list, &ctx->file->ctx_list)
            xa_store(&ctx_table, ctx->id, ctx, GFP_KERNEL)

绑定地址rdma_bind_addr -> ucma_bind

代码语言:javascript
复制
内核态:
ucma_bind
    ucma_get_ctx
    rdma_bind_addr
        state = RDMA_CM_ADDR_BOUND,
        cma_check_linklocal
        memcpy(cma_src_addr(id_priv), addr, rdma_addr_size(addr))
        id_priv->afonly = 1
        daddr = cma_dst_addr(id_priv) -> return (struct sockaddr *) &id_priv->id.route.addr.dst_addr
        ret = cma_get_port(id_priv)
            cma_select_ib_ps
            cma_use_port
                bind_list = cma_ps_find(id_priv->id.route.addr.dev_addr.net, ps, snum)
                cma_alloc_port
                or cma_check_port
                cma_bind_port

监听rdma_listen -> ucma_listen

代码语言:javascript
复制
用户态:
rdma_listen -> ucma_listen
    atomic_set(&ctx->backlog, cmd.backlog)
    rdma_listen
        if (!cma_comp_exch(id_priv, RDMA_CM_ADDR_BOUND, RDMA_CM_LISTEN))
            rdma_bind_addr
                rdma_bind_addr_dst cma_dst_addr
        cma_check_port
        rdma_cap_ib_cm
        cma_ib_listen
            ib_cm_insert_listen(id_priv->id.device, cma_ib_req_handler, svc_id)        
        cma_listen_on_all
?
内核调用栈:
#0  cma_listen_on_dev (id_priv=0xffff888108b66800, cma_dev=0xffff88810980e480, to_destroy=0xffffc9000382fd08) at drivers/infiniband/core/cma.c:2540
#1  0xffffffffc05427a8 in cma_listen_on_all (id_priv=0xffff888108b66800) at drivers/infiniband/core/cma.c:2592
#2  rdma_listen (id=0xffff888108b66800, backlog=1024) at drivers/infiniband/core/cma.c:3867
#3  0xffffffffc0483d70 in ucma_listen (file=<optimized out>, inbuf=<optimized out>, in_len=<optimized out>, out_len=<optimized out>) at drivers/infiniband/core/ucma.c:1105
?
cma_listen_on_dev
    __rdma_create_id(net, cma_listen_handler, id_priv,
    dev_id_priv->state = RDMA_CM_ADDR_BOUND
    _cma_attach_to_dev
    rdma_restrack_add
    cma_id_get
    rdma_listen
        if (!cma_comp_exch(id_priv, RDMA_CM_ADDR_BOUND, RDMA_CM_LISTEN))
        cma_ib_listen
            addr = cma_src_addr(id_priv)
            svc_id = rdma_get_service_id(&id_priv->id, addr)
            id = ib_cm_insert_listen(id_priv->id.device, cma_ib_req_handler, svc_id)
                cm_id_priv = cm_alloc_id_priv
                cm_init_listen
                listen_id_priv = cm_insert_listen(cm_id_priv, cm_handler)
                    cur_cm_id_priv = rb_entry(parent, struct cm_id_private,
                    rb_link_node(&cm_id_priv->service_node, parent, link)
                    rb_insert_color(&cm_id_priv->service_node, &cm.listen_service_table) -> 该函数是红黑树相对于普通二叉排序树最大的差别。这个函数中平衡二叉树的方式主要有3种:a)改变颜色,0表示red,1表示black。对于对齐的节点,低4位总是0,所以默认的情况下是红色的节点。那么对于父节点是black的情况,直接插入即可,无需改变颜色。如果父节点是红色则需要改变颜色。b)左旋转,将一个节点的右孩子变为父节点,节点本身变为左孩子。c)右旋转,将一个节点的做孩子变为父节点,节点本身变为右孩子
                cm_id_priv->id.state = IB_CM_LISTEN
    list_add_tail(&dev_id_priv->listen_list, &id_priv->listen_list)

初始化QP属性-INIT_QP_ATTR -> ucma_init_qp_attr

代码语言:javascript
复制
内核态:
ucma_init_qp_attr
    rdma_init_qp_attr(ctx->cm_id, &qp_attr, &resp.qp_attr_mask)
        cma_ib_init_qp_attr
            ib_addr_get_pkey
            ib_find_cached_pkey
        or ib_cm_init_qp_attr > 每一个QP状态所设置的QP属性不一样
            cm_init_qp_init_attr
                qp_attr->qp_access_flags = IB_ACCESS_REMOTE_WRITE
                qp_attr->pkey_index = cm_id_priv->av.pkey_index
                qp_attr->port_num = cm_id_priv->av.port->port_num
            cm_init_qp_rtr_attr
                qp_attr->ah_attr = cm_id_priv->av.ah_attr
                qp_attr->ah_attr.ib.dlid = cm_id_priv->av.dlid_datapath
                qp_attr->path_mtu = cm_id_priv->path_mtu
                qp_attr->dest_qp_num = be32_to_cpu(cm_id_priv->remote_qpn)
                qp_attr->rq_psn = be32_to_cpu(cm_id_priv->rq_psn)
                qp_attr->max_dest_rd_atomic
                qp_attr->min_rnr_timer = 0
                rdma_ah_get_dlid(&cm_id_priv->alt_av.ah_attr)
                qp_attr->alt_port_num = cm_id_priv->alt_av.port->port_num
                ...
            cm_init_qp_rts_attr
                qp_attr->sq_psn = be32_to_cpu(cm_id_priv->sq_psn)
                qp_attr->retry_cnt = cm_id_priv->retry_count
                qp_attr->rnr_retry = cm_id_priv->rnr_retry_count
                qp_attr->max_rd_atomic = cm_id_priv->initiator_depth
                qp_attr->timeout = cm_id_priv->av.timeout
                qp_attr->path_mig_state = IB_MIG_REARM
                ...
        or iw_cm_init_qp_attr
            iwcm_init_qp_init_attr
            iwcm_init_qp_rts_attr
        qp_attr->timeout = id_priv->timeout
        qp_attr->min_rnr_timer = id_priv->min_rnr_timer

建立连接rdma_connect -> ucma_connect

代码语言:javascript
复制
内核态:
UCMA_CMD_CONNECT -> static ssize_t (*ucma_cmd_table[]) -> static ssize_t ucma_connect
    copy_from_user
    ucma_get_ctx_dev
    ucma_copy_conn_param -> RDMA/cma:为AF_IB设置qkey,允许用户在使用AF_IB时指定qkey。 qkey 被添加到 struct rdma_ucm_conn_param 中代替保留字段,但为了向后兼容,仅当关联的 rdma_cm_id 使用 AF_IB 时才可访问
        ...
        dst->qkey = (id->route.addr.src_addr.ss_family == AF_IB) ? src->qkey : 0;
    rdma_connect_ece -> RDMA/ucma:扩展ucma_connect以接收ECE参数,CMID的主动方通过librdmacm的rdma_connect()和内核的ucma_connect()发起连接。 扩展 UCMA 接口来处理这些新参数
        rdma_connect(id, conn_param) -> rdma_connect_locked
            cma_comp_exch(id_priv, RDMA_CM_ROUTE_RESOLVED, RDMA_CM_CONNECT)
            rdma_cap_ib_cm(id->device, id->port_num) -> rdma_cap_ib_cm - 检查设备端口是否具有 Infiniband Communication Manager 功能。 @device:要检查的设备 @port_num:要检查的端口号 InfiniBand 通信管理器是通过通用服务接口 (GSI) 访问的许多预定义通用服务代理 (GSA) 之一。 它的作用是促进节点之间连接的建立以及已建立的连接的其他管理相关任务。 返回:如果端口支持 IB CM,则返回 true(但这并不能保证 CM 实际正在运行)
                RDMA_CORE_CAP_IB_CM
            if (id->qp_type == IB_QPT_UD) -> cma_resolve_ib_udp -> RDMA/cma:添加对 RDMA_PS_UDP 的支持,允许通过 rdma_cm 使用 UD QP,以便为使用 SIDR 解析数据报消息的 IB 地址提供地址转换服务
            or cma_connect_ib
                check_add_overflow(offset, conn_param->private_data_len, &req.private_data_len) -> 为了简单性和代码卫生,下面的后备代码坚持 a、b 和 *d 具有相同的类型(类似于 min() 和 max() 宏),而 gcc 的类型通用溢出检查器接受不同的类型。 因此,我们不只是将 check_add_overflow 设置为 __builtin_add_overflow 的别名,而是添加类似于下面的类型检查
                ib_create_cm_id(id_priv->id.device, cma_ib_handler, id_priv)
                    cm_alloc_id_priv -> RDMA/cm:简化建立监听cm_id
                        cm_id_priv->id.cm_handler = cm_handler
                        RB_CLEAR_NODE(&cm_id_priv->service_node) -> rb_tree
                        init_completion(&cm_id_priv->comp)
                        xa_alloc_cyclic -> 在 XArray 中找到存储此条目的位置 -> 在 xa 中查找 limit.min 和 limit.max 之间的空条目,将索引存储到 id 指针中,然后将条目存储在该索引处。 并发查找不会看到未初始化的 id。 对空条目的搜索将从下一个开始,并在必要时回绕, https://docs.kernel.org/core-api/xarray.html#c.xa_alloc_cyclic
                        cm_id_priv->id.local_id = (__force __be32)id ^ cm.random_id_operand
                    cm_finalize_id(cm_id_priv)
                        xa_store(&cm.local_id_table,
                trace_cm_send_req
                ib_send_cm_req(id_priv->cm_id.ib, &req)
                    ib_mad_send_buf -> ib_mad_send_buf - MAD 数据缓冲区和发送的工作请求。@next:用于将 MAD 链接在一起以进行发布的指针。 @mad:为没有激活 RMPP 的 MAD 引用分配的 MAD 数据缓冲区。 对于使用 RMPP 的 MAD,引用公共和管理类特定标头。 @mad_agent:分配缓冲区的 MAD 代理。 @ah:发送 MAD 时使用的地址句柄。 @context:用户控制的上下文字段。 @hdr_len:表示MAD的数据头的大小。 此长度包括常见的 MAD、RMPP 和特定于类的标头。 @data_len:表示用户传输的数据的总大小。 @seg_count:为此发送分配的 RMPP 段数。 @seg_size:每个 RMPP 段中数据的大小。 这不包括特定于类的标头。 @seg_rmpp_size:每个 RMPP 段的大小,包括类特定标头。 @timeout_ms:等待响应的时间。 @retries:重试响应请求的次数。 对于使用 RMPP 的 MAD,这适用于每个窗口。 完成后,返回完成传输所需的重试次数。 用户负责初始化 MAD 缓冲区本身,任何 RMPP 标头除外。 超出 data_len 分配的额外段缓冲区空间是填充
                    cm_validate_req_param(param)
                    cm_id_priv->timewait_info = cm_create_timewait_info(cm_id_priv->
                        INIT_DELAYED_WORK(&timewait_info->work.work, cm_work_handler)
                    cm_init_av_by_path(param->primary_path, param->ppath_sgid_attr, &av) -> IB/cm:将 sa_path_rec 的成员替换为“struct sgid_attr *”,在处理 CM 消息中的路径记录条目时,现在还提供关联的 GID 属性。 目前,对于 RoCE,网络设备的网络命名空间指针和 ifindex 存储在路径记录条目中。 在处理 CM 消息时,netdev 的这两个字段都可以随时更改。 另外,存储网络命名空间而不保留引用将导致释放后使用崩溃。 因此将其删除。 RoCE 的网络设备信息是通过 ib_cm 请求中引用的 gid 属性提供的。 这样的设计会导致当网络指针无效时内核可能崩溃的情况。 然而今天它总是被初始化为init_net,它不会变得无效。 为了支持处理接收到的数据包的任意命名空间中的数据包,有必要避免这种情况。 该补丁消除了对网络指针和 ifindex 的依赖; 相反,它将依赖于包含指向 netdev 的指针的 SGID 属性
                        port = get_cm_port_from_path(path, sgid_attr)
                            rdma_find_gid
                                index = find_gid(table, gid, &gid_attr_val, false, mask, NULL)
                        ib_find_cached_pkey(cm_dev->ib_device, port->port_num,
                            cache = device->port_data[port_num].cache.pkey
                            if ((cache->table[i] & 0x7fff) == (pkey & 0x7fff))
                                if (cache->table[i] & 0x8000)
                        cm_set_av_port(av, port)
                        ib_init_ah_attr_from_path(cm_dev->ib_device, port->port_num, path, &new_ah_attr, sgid_attr) -> av->ah_attr 可能会根据 wc 或请求处理时间进行初始化,这可能会引用 sgid_attr。 因此在堆栈上初始化一个新的ah_attr。 如果初始化失败,则使用旧的 ah_attr 来发送任何响应。 如果初始化成功,则使用新的 ah_attr 覆盖旧的。 这样就可以使用 ah_attr 来返回错误响应 -> ib_init_ah_attr_from_path - 根据 SA 路径记录初始化地址句柄属性。 @device:设备关联啊属性初始化。 @port_num:指定设备上的端口。 @rec:用于 ah 属性初始化的路径记录条目。 @ah_attr:从路径记录初始化地址句柄属性。 @gid_attr:初始化期间要考虑的SGID属性。 当 ib_init_ah_attr_from_path() 返回成功时,(a) 对于 IB 链路层,当 IB 链路层存在 GRH 时,它可选地包含对 SGID 属性的引用。 (b) 对于 RoCE 链路层,它包含对 SGID 属性的引用。 用户必须调用 rdma_destroy_ah_attr() 来释放对使用 ib_init_ah_attr_from_path() 初始化的 SGID 属性的引用
                            rdma_ah_find_type
                            rdma_ah_set_sl
                            rdma_ah_set_port_num(ah_attr, port_num)
                            rdma_ah_set_static_rate(ah_attr, rec->rate)
                            roce_resolve_route_from_path(rec, gid_attr)
                                might_sleep -> RDMA/addr:将 addr_resolve 标记为 might_sleep(),在通过 ib_nl_fetch_ha() 的一条路径下,这会调用 nlmsg_new(GFP_KERNEL),这是一个睡眠调用。 这是一条非常罕见的路径,因此将 fetch_ha() 和有条件调用 fetch_ha() 的模块外部入口点标记为 might_sleep()
                                rdma_gid2ip((struct sockaddr *)&sgid, &rec->sgid)
                                    if (ipv6_addr_v4mapped((struct in6_addr *)gid))
                                        memcpy(&out_in->sin_addr.s_addr, gid->raw + 12, 4)
                                    else
                                        memcpy(&out_in->sin6_addr.s6_addr, gid->raw, 16)
                                rdma_gid2ip((struct sockaddr *)&dgid, &rec->dgid)
                                addr_resolve((struct sockaddr *)&sgid, (struct sockaddr *)&dgid, &dev_addr, false, true, 0)
                                    if (resolve_by_gid_attr) -> RDMA/core:RoCE考虑gid属性的net ns,解析目的地址或路由时,当netnamespace不可用时,参考SGID属性的netdevice的netnamespace。 这通常是从网络到达 RoCE 端口的请求的情况
                                        set_addr_netns_by_gid_rcu(addr)
                                    ret = addr4_resolve(src_in, dst_in, addr, &rt) -> https://hustcat.github.io/queue-pair-in-rdma/
                                        ip_route_output_key(addr->net, &fl4) -> ip_route_output_flow(net, flp, NULL) -> 查找路由表:为套接字缓冲区设置路由出口信息。如果已有TCP连接,则套接字缓冲区保存了路由信息
                                        addr->hoplimit = ip4_dst_hoplimit(&rt->dst) -> 访问到dst metric的RTAX_HOPLIMIT字段,此字段并未做初始化,其值为零,ip4_dst_hoplimit会采用系统默认的hoplimit
                                    or ret = addr6_resolve(src_in, dst_in, addr, &dst)
                                        ipv6_dst_lookup_flow
                                        ip6_dst_hoplimit
                                    rdma_set_src_addr_rcu
                                        rdma_ah_set_grh(attr, dgid, flow_label, sgid_attr->index, hop_limit, traffic_class)
                                            grh->flow_label = flow_label
                                            grh->sgid_index = sgid_index
                                            grh->hop_limit = hop_limit
                                            grh->traffic_class = traffic_class
                                    addr_resolve_neigh -> arp
                                        memcpy(addr->dst_dev_addr, addr->src_dev_addr, MAX_ADDR_LEN)
                                        or ret = fetch_ha(dst, addr, dst_in, seq)
                                    rdma_addr_set_net_defaults
                                rec->roce.route_resolved = true
                            else rdma_ah_set_dlid(ah_attr, be32_to_cpu(sa_path_get_dlid(rec))) -> attr->ib.dlid = (u16)dlid
                            init_ah_attr_grh_fields(device, port_num,
                                sa_conv_pathrec_to_gid_type(rec) -> IB_GID_TYPE_ROCE
                                gid_attr = rdma_find_gid_by_port(device, &rec->sgid, type,
                                    local_index = find_gid(table, gid, &val, false, mask, NULL)
                                or rdma_hold_gid_attr
                                rdma_move_grh_sgid_attr
                        rdma_move_ah_attr(&av->ah_attr, &new_ah_attr)
                    cm_move_av_from_path(&cm_id_priv->av, &av)
                    msg = cm_alloc_priv_msg(cm_id_priv) -> cm_alloc_msg
                        mad_agent = cm_id_priv->av.port->mad_agent
                        ah = rdma_create_ah(mad_agent->qp->pd, &cm_id_priv->av.ah_attr, 0)
                            rdma_fill_sgid_attr
                            rdma_lag_get_ah_roce_slave
                            _rdma_create_ah -> IB/核心:引入和使用 rdma_create_user_ah,引入 rdma_create_user_ah API,该 API 允许将 udata 传递给提供程序驱动程序,并另外解析 RoCE 的 DMAC。 ib_resolve_eth_dmac() 解析单播、多播、链接本地 ipv4 映射的 ipv6 和 ipv6 目标 gid 条目的目标 mac 地址。 这允许所有 RoCE 提供程序驱动程序避免重复此类代码。 这种更改带来了一致性,其中 IB 核心始终解析 dmac 并将其传递给用户和内核使用者的 RoCE 提供程序驱动程序,并且 ah_attr->roce.dmac 始终是提供程序驱动程序的输入字段。 这种一致性避免了将 ib_resolve_eth_dmac 符号导出到提供程序或其他模块。 因此,它会在补丁系列的后面部分作为导出符号被删除。 现在 uverbs 和 umad 都使用 rdma_create_user_ah API,它修复了 umad 地址的 DMAC 无效的问题
                                rdma_zalloc_drv_obj_gfp -> kzalloc_node(size, gfp, dev->ops.get_numa_node(dev)
                                device->ops.create_user_ah(ah, &init_attr, udata) -> irdma_create_user_ah
                                    irdma_setup_ah
                                    irdma_create_hw_ah -> IRDMA_OP_AH_CREATE
                                    hash_add(iwdev->ah_hash_tbl, &parent_ah->list, key)
                                or device->ops.create_ah(ah, &init_attr, NULL)
                            rdma_lag_put_ah_roce_slave
                            rdma_unfill_sgid_attr
                        m = ib_create_send_mad(mad_agent, cm_id_priv->id.remote_cm_qpn, cm_id_priv->av.pkey_index, 0, IB_MGMT_MAD_HDR, IB_MGMT_MAD_DATA, GFP_ATOMIC, IB_MGMT_BASE_VERSION)
                            opa = rdma_cap_opa_mad(mad_agent->device, mad_agent->port_num) -> IB/mad:添加部分英特尔 OPA MAD 支持,此补丁是 3 个补丁中的第一个,添加了 OPA MAD 处理 1) 添加英特尔 Omni-Path 架构定义 2) 增加最大管理版本以适应 OPA 3) 更新 ib_create_send_mad 如果设备支持 OPA MAD 和发送的 MAD 是 OPA 基本版本,根据需要更改 MAD 大小和 sg 长度
                            if (ib_mad_kernel_rmpp_agent(mad_agent))
                            INIT_LIST_HEAD(&mad_send_wr->rmpp_list)
                            mad_send_wr->mad_list.cqe.done = ib_mad_send_done;
                            mad_send_wr->send_wr.wr.num_sge = 2
                            mad_send_wr->send_wr.wr.opcode = IB_WR_SEND
                            ret = alloc_send_rmpp_list(mad_send_wr, mad_size, gfp_mask)
                        m->context[0] = cm_id_priv
                    msg->context[1] = (void *)(unsigned long)IB_CM_REQ_SENT
                    ib_post_send_mad -> [IB] 修复 MAD 层 DMA 映射,以避免在映射后触及数据缓冲区。MAD 层在 DMA 映射完成后触及用于发送的数据缓冲区,从而违反了 DMA API。 这会导致非缓存一致性架构出现问题,因为执行 DMA 的设备不会看到仅存在于 CPU 缓存中的有效负载缓冲区的更新。 通过让所有 MAD 使用者使用 ib_create_send_mad() 分配其发送缓冲区,并将 DMA 映射移动到 MAD 层,以便可以在调用 send 之前(以及 MAD 层对发送缓冲区进行任何修改之后)完成此操作,可以解决此问题。 在非缓存一致性 PowerPC 440SPe 系统上进行测试
                        ib_mad_enforce_security -> IB/核心:在管理数据报上强制执行安全性,在创建和销毁 MAD 代理时分配和释放安全上下文。 该上下文用于控制对 PKey 的访问以及发送和接收 SMP。 发送或接收 MAD 时,检查代理是否有权访问端口子网前缀的 PKey。 在 SMI QP 的 MAD 和监听代理注册期间,检查调用进程是否有权访问管理子网并向 LSM 注册回调以获取策略更改通知。 当发生策略更改通知时,重新检查权限并设置一个标志,指示允许发送和接收 SMP。 发送和接收 MAD 时,如果代理位于 SMI QP 上,请检查代理是否有权访问 SMI。 由于安全策略可以更改,因此在创建代理时可能允许许可,但不再允许
                            rdma_protocol_ib
                            ib_security_pkey_access
                        ib_is_mad_class_rmpp
                        handle_outgoing_dr_smp
                        ib_mad_kernel_rmpp_agent
                        ib_send_rmpp_mad
                        ib_send_mad
                            ib_dma_map_single
                                ib_uses_virt_dma
                                dma_map_single
                            ib_post_send
                                .post_send = mlx5_ib_post_send_nodrain,
            or cma_connect_iw
    ucma_put_ctx

服务端接收请求rdma_accept -> ucma_accept

代码语言:javascript
复制
static ssize_t ucma_accept
    else ret = rdma_accept_ece(ctx->cm_id, NULL, &ece)
        rdma_accept -> rdma_accept - 调用以接受连接请求或响应。@id:与请求关联的连接标识符。 @conn_param:建立连接所需的信息。 如果接受连接请求,则必须提供此信息。 如果接受连接响应,则该参数必须为 NULL。 通常,此例程仅由侦听器调用以接受连接请求。 如果用户正在执行自己的 QP 转换,则还必须在连接的活动端调用它。 如果出现错误,则会向远程端发送拒绝消息,并将与 id 关联的 qp 的状态修改为错误,以便刷新任何先前发布的接收缓冲区。 该函数供内核 ULP 使用,并且必须从处理程序回调下调用
            lockdep_assert_held(&id_priv->handler_mutex)
            ret = cma_rep_recv(id_priv)
                cma_modify_qp_rtr -> RDMA/cma:使用用户值覆盖默认的 responder_resources,默认情况下,responder_resources 参数设置为连接请求中收到的参数。 被动方在接受连接时可以覆盖该值。 将 QP 转换为 RTR 状态时使用被动方提供的值,而不是连接请求中给出的值。 如果没有此更改,如果被动方支持的responder_resources 少于请求中的资源,RTR 转换可能会失败。 为了代码一致性并防止 QP 破坏,请重构覆盖 initiator_depth 以匹配 responder_resources 的设置方式
                    qp_attr.qp_state = IB_QPS_INIT
                    rdma_init_qp_attr
                    ib_modify_qp
                    qp_attr.qp_state = IB_QPS_RTR
                    ib_modify_qp
                cma_modify_qp_rts
                trace_cm_send_rtu
                ib_send_cm_rtu -> IB:基于IP地址的RDMA连接管理器,基于InfiniBand的基于IP地址连接的内核连接管理代理。 该代理定义了通用 RDMA 连接抽象,以支持想要通过不同 RDMA 设备进行连接的客户端。 该代理还代表客户端处理 RDMA 设备热插拔事件
                    data = cm_copy_private_data(private_data, private_data_len)
                    msg = cm_alloc_msg(cm_id_priv)
                    cm_format_rtu
                    ib_post_send_mad(msg, NULL)
                    cm_id->state = IB_CM_ESTABLISHED
                    cm_set_private_data(cm_id_priv, data, private_data_len)

更多内核态调用接口

代码语言:javascript
复制
drivers/infiniband/core/ucma.c
static ssize_t (*ucma_cmd_table[])(struct ucma_file *file,
                   const char __user *inbuf,
                   int in_len, int out_len) = {
    [RDMA_USER_CM_CMD_CREATE_ID]   = ucma_create_id,
    [RDMA_USER_CM_CMD_DESTROY_ID]  = ucma_destroy_id,
    [RDMA_USER_CM_CMD_BIND_IP]   = ucma_bind_ip,
    [RDMA_USER_CM_CMD_RESOLVE_IP]  = ucma_resolve_ip,
    [RDMA_USER_CM_CMD_RESOLVE_ROUTE] = ucma_resolve_route,
    [RDMA_USER_CM_CMD_QUERY_ROUTE]   = ucma_query_route,
    [RDMA_USER_CM_CMD_CONNECT]   = ucma_connect,
    [RDMA_USER_CM_CMD_LISTEN]  = ucma_listen,
    [RDMA_USER_CM_CMD_ACCEPT]  = ucma_accept,
    [RDMA_USER_CM_CMD_REJECT]  = ucma_reject,
    [RDMA_USER_CM_CMD_DISCONNECT]  = ucma_disconnect,
    [RDMA_USER_CM_CMD_INIT_QP_ATTR]  = ucma_init_qp_attr,
    [RDMA_USER_CM_CMD_GET_EVENT]   = ucma_get_event,
    [RDMA_USER_CM_CMD_GET_OPTION]  = NULL,
    [RDMA_USER_CM_CMD_SET_OPTION]  = ucma_set_option,
    [RDMA_USER_CM_CMD_NOTIFY]  = ucma_notify,
    [RDMA_USER_CM_CMD_JOIN_IP_MCAST] = ucma_join_ip_multicast,
    [RDMA_USER_CM_CMD_LEAVE_MCAST]   = ucma_leave_multicast,
    [RDMA_USER_CM_CMD_MIGRATE_ID]  = ucma_migrate_id,
    [RDMA_USER_CM_CMD_QUERY]   = ucma_query,
    [RDMA_USER_CM_CMD_BIND]    = ucma_bind,
    [RDMA_USER_CM_CMD_RESOLVE_ADDR]  = ucma_resolve_addr,
    [RDMA_USER_CM_CMD_JOIN_MCAST]  = ucma_join_multicast
};

参考

RDMA CM用户手册: https://man7.org/linux/man-pages/man7/rdma_cm.7.html

RDMA CM用户态仓库(笔记): https://github.com/ssbandjl/rdma-core/blob/master/readme

晓兵(ssbandjl)

博客: /developer/user/5060293/articles | https://logread.cn | https://blog.csdn.net/ssbandjl | https://www.zhihu.com/people/ssbandjl/posts

DPU专栏

/developer/column/101987

技术会友: 欢迎对DPU/智能网卡/卸载/网络,存储加速/安全隔离等技术感兴趣的朋友加入DPU技术交流群

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 术语
  • 简介
    • RDMA CM编程参考模型
      • 客户端操作
        • 服务端操作
          • 其他调用参考
          • 源码调用栈
            • 以RDMA上层通信框架libfabric为例
              • 以RDMA用户态驱动中CM服务端为例
                • 创建事件通道rdma_create_event_channel
                  • 分配通信标识rdma_create_id -> ucma_create_id
                    • 绑定地址rdma_bind_addr -> ucma_bind
                      • 监听rdma_listen -> ucma_listen
                        • 初始化QP属性-INIT_QP_ATTR -> ucma_init_qp_attr
                          • 建立连接rdma_connect -> ucma_connect
                            • 服务端接收请求rdma_accept -> ucma_accept
                              • 更多内核态调用接口
                              • 参考
                              • 晓兵(ssbandjl)
                                • DPU专栏
                                相关产品与服务
                                访问管理
                                访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                                http://www.vxiaotou.com