Infiniband 内核态API

Linux RDMA 内核态Verbs

RDMA Verbs API和Verbs消费者分类

RDMA Verbs不是所有用户都可以任意使用的。RDMA Verbs分为两种,第一种是User-level,另外一种是Privileged。即Verbs API分为用户态和内核态两种。用户态的Verbs API在Linux-RDMA-Core这个项目中被实现。而内核态的Verbs API是集中于Linux内核,形成了Linux Kernel RDMA subsystem。
在InfiniBand的规范定义中,将使用Verbs API的用户分为了两类,第一种是特权级消费者,特权级消费者可以使用所有的Verbs。另外一种是用户级消费者,普通用户只允许使用User-level的Verbs API。
按照InfiniBand规范中的说明,二者的区别如下所示。

  • 特权级消费者:需要操作一个特权级别的指令去访问操作系统内部的数据结构,并且实现对Channel接口的控制。
  • 用户级消费者:必须要通过其他代理的方式去操作操作系统的数据结构,只能使用特定的Verbs API。
    对于Verbs API的级别,在InfiniBand的规范中也有相应的说明。
    对于这张表格,第二个标签是实现必要性,如果值为Mandatory,那么就强制要求实现,否则可以选择性实现。
    第三个标签Consumer Accessibility则是说明了这条Verbs API需要的权限。
    VerbClass1
    VerbClass2
    VerbClass3
    可以从表中看到,User-Level等级的Verbs API不多,一共就以下几个。
    用户级别的消费者可以使用的Verbs API为Post Send和Post Recv、对AH的增删查改、绑定Memory Window这几个。
  • AH :Address Handle,用于保存地址信息。以指针或者句柄的形式标记一个远程的地址。
    我们可以发现,这几个Verbs API的特点都是数据交互相关的,而涉及到对内核数据结构修改,例如QP的增删查改、内存相关的Verbs API,都需要Privileged权限才可以使用。

    为什么需要内核态Verbs API

    参考了一部分的网上资料,总结起来就一句话。为了实现数据的绕开内核功能,需要控制平面在内核做出相应的调整。
    控制路径上的调用一般会陷入到内核态,而数据路径上则直接从用户态到硬件。因此Verbs API从功能上来说可以分为数据平面Verbs和控制平面Verbs。总体上,数据平面的Verbs API是所有消费者都可以使用的,而控制平面的Verbs API则只有特权级消费者才能调用。
  • 控制平面
    • QP的增删查改
    • Memory Region的增删查改
  • 数据平面
    • Post Send
    • Post Recv
    • Poll CQ
    • Bind Memory Window
      这部分推测是出于安全的考虑,例如DMA设备注册内存区域这个操作,就需要陷入到内核,不允许普通用户直接使用相关的API注册。

      不需要经过内核的Verbs API

      这部分,我们以Post Send为例,从rdma-core的代码不断向下挖掘分析。
      分析rdma-core/libibverbs/examples/rc_pingpong.c文件。这是一个使用rdma-core提供的用户态API实现rdma通信的案例程序。
      首先在main函数里,调用了pp_post_send函数,而参数ctx是struct pingpong_context类型的结构体,它的定义如下所示。
      1
      2
      3
      4
      5
      6
      7
              if (pp_post_send(ctx)) {

                  fprintf(stderr, "Couldn't post send\n");

                  return 1;

              }
      struct pingpong_context的定义如下所示。它更加仔细的封装了ib在通信时所需要的一些东西。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      struct pingpong_context {

          struct ibv_context  *context;

          struct ibv_comp_channel *channel;

          struct ibv_pd       *pd;

          struct ibv_mr       *mr;

          struct ibv_dm       *dm;

          union {

              struct ibv_cq       *cq;

              struct ibv_cq_ex    *cq_ex;

          } cq_s;

          struct ibv_qp       *qp;

          struct ibv_qp_ex    *qpx;

          char            *buf;

          int          size;

          int          send_flags;

          int          rx_depth;

          int          pending;

          struct ibv_port_attr     portinfo;

          uint64_t         completion_timestamp_mask;

      };
      从pp_post_send继续分析。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      static int pp_post_send(struct pingpong_context *ctx)

      {

          struct ibv_sge list = {

              .addr   = use_dm ? 0 : (uintptr_t) ctx->buf,

              .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_flags = ctx->send_flags,

          };

          struct ibv_send_wr *bad_wr;



          if (use_new_send) {

              ibv_wr_start(ctx->qpx);



              ctx->qpx->wr_id = PINGPONG_SEND_WRID;

              ctx->qpx->wr_flags = ctx->send_flags;



              ibv_wr_send(ctx->qpx);

              ibv_wr_set_sge(ctx->qpx, list.lkey, list.addr, list.length);



              return ibv_wr_complete(ctx->qpx);

          } else {

              return ibv_post_send(ctx->qp, &wr, &bad_wr);

          }

      }
      在这个位置出现了分支结构,其中涉及到两个send函数,一个是ibv_wr_send,另外一个是ibv_post_send。
      区别该分支的变量为use_new_send,可以从变量名推断,这是用于判断是否是第一次发送数据。

      ibv_wr_send

      调用该函数的情况为use_new_send == 1的情况,根据变量的真实含义推断,该情况应为第一次发送数据的情况。
      1
      2
      3
      4
      5
      6
      7
      static inline void ibv_wr_send(struct ibv_qp_ex *qp)

      {

          qp->wr_send(qp);

      }
      而这里调用了qp结构体的wr_send函数。找到wr_send函数继续向下分析。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      static void wr_send(struct ibv_qp_ex *ibqp)

      {

          struct rxe_qp *qp = container_of(ibqp, struct rxe_qp, vqp.qp_ex);

          struct rxe_send_wqe *wqe = addr_from_index(qp->sq.queue, qp->cur_index);



          if (check_qp_queue_full(qp))

              return;



          memset(wqe, 0, sizeof(*wqe));



          wqe->wr.wr_id = qp->vqp.qp_ex.wr_id;

          wqe->wr.opcode = IBV_WR_SEND;

          wqe->wr.send_flags = qp->vqp.qp_ex.wr_flags;

          wqe->ssn = qp->ssn++;



          advance_qp_cur_index(qp);

      }
      那么到这里,就无法继续深入了。可以看到,代码声明了一个wqe,而wqe是作为wq的一个元素。那么接下去,就应该是硬件负责的部分了。
      并且在rdma-core/providers目录下,出现了mlx5的内容。可以推测,该目录是负责与硬件驱动相关的部分。
      翻阅rdma-core/providers/mlx5/qp.c文件,出现了下列相关的代码。我认为也许是与发送wqe相关的操作。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      int mlx5_post_send(struct ibv_qp *ibqp, struct ibv_send_wr *wr,

                 struct ibv_send_wr **bad_wr)

      {

      #ifdef MW_DEBUG

          if (wr->opcode == IBV_WR_BIND_MW) {

              if (wr->bind_mw.mw->type == IBV_MW_TYPE_1)

                  return EINVAL;



              if (!wr->bind_mw.bind_info.mr ||

                  !wr->bind_mw.bind_info.addr ||

                  !wr->bind_mw.bind_info.length)

                  return EINVAL;



              if (wr->bind_mw.bind_info.mr->pd != wr->bind_mw.mw->pd)

                  return EINVAL;

          }

      #endif



          return _mlx5_post_send(ibqp, wr, bad_wr);

      }
      这就是说,在rdma-core这个用户态的RMDA Verbs API项目中,也存在与硬件驱动的部分。
      这部分可以说明,在用户态下调用post_send函数,可以直接与相关的硬件进行交互。

      ibv_post_send

      而另外一条路,ibv_post_send函数则与内核态下的内容高度相似,通过调用设备绑定的post_send函数实现该功能。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      static inline int ibv_post_send(struct ibv_qp *qp, struct ibv_send_wr *wr,

                      struct ibv_send_wr **bad_wr)

      {

          return qp->context->ops.post_send(qp, wr, bad_wr);

      }

      struct ibv_context_ops {

          int (*_compat_query_device)(struct ibv_context *context,

                          struct ibv_device_attr *device_attr);

          int (*_compat_query_port)(struct ibv_context *context,

                        uint8_t port_num,

                        struct _compat_ibv_port_attr *port_attr);

          void *(*_compat_alloc_pd)(void);

          void *(*_compat_dealloc_pd)(void);

          void *(*_compat_reg_mr)(void);

          void *(*_compat_rereg_mr)(void);

          void *(*_compat_dereg_mr)(void);

          struct ibv_mw *     (*alloc_mw)(struct ibv_pd *pd, enum ibv_mw_type type);

          int         (*bind_mw)(struct ibv_qp *qp, struct ibv_mw *mw,

                             struct ibv_mw_bind *mw_bind);

          int         (*dealloc_mw)(struct ibv_mw *mw);

          void *(*_compat_create_cq)(void);

          int         (*poll_cq)(struct ibv_cq *cq, int num_entries, struct ibv_wc *wc);

          int         (*req_notify_cq)(struct ibv_cq *cq, int solicited_only);

          void *(*_compat_cq_event)(void);

          void *(*_compat_resize_cq)(void);

          void *(*_compat_destroy_cq)(void);

          void *(*_compat_create_srq)(void);

          void *(*_compat_modify_srq)(void);

          void *(*_compat_query_srq)(void);

          void *(*_compat_destroy_srq)(void);

          int         (*post_srq_recv)(struct ibv_srq *srq,

                               struct ibv_recv_wr *recv_wr,

                               struct ibv_recv_wr **bad_recv_wr);

          void *(*_compat_create_qp)(void);

          void *(*_compat_query_qp)(void);

          void *(*_compat_modify_qp)(void);

          void *(*_compat_destroy_qp)(void);

          int         (*post_send)(struct ibv_qp *qp, struct ibv_send_wr *wr,

                               struct ibv_send_wr **bad_wr);

          int         (*post_recv)(struct ibv_qp *qp, struct ibv_recv_wr *wr,

                               struct ibv_recv_wr **bad_wr);

          void *(*_compat_create_ah)(void);

          void *(*_compat_destroy_ah)(void);

          void *(*_compat_attach_mcast)(void);

          void *(*_compat_detach_mcast)(void);

          void *(*_compat_async_event)(void);

      };
      软硬件交汇处,并不是本周的探索重点。但是相信继续往下探索,会出现ibv_context_ops结构体与mlx5设备驱动绑定的地方。
      以上两条路径,均未发现有以ib_开头的函数,即没有出现调用内核态Verbs API的地方。因此,可以认为,post_send函数是没有经过内核态的。
      这里一路看下来,发现post_send完直接到硬件去了,根本没发现数据包封装的细节,这里推测数据包这部分可能是在网卡上完成的。

      需要经过内核的Verbs API

      该章节,以ibv_reg_mr为例,从ibv_reg_mr的定义开始。这里其实非常复杂,参考了一部分网上的资料。
      如果直接参考ibv_reg_mr宏的入口,是找不到相关的调用路径的。最终我查阅了资料,发现ibv_cmd_reg_mr函数才是真正进入到内核态的入口。
      但是目前还不了解ibv_cmd_reg_mr与ibv_reg_mr之间的关系。
      ibv_cmd_reg_mr的定义如下。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      int ibv_cmd_reg_mr(struct ibv_pd *pd, void *addr, size_t length,

                 uint64_t hca_va, int access,

                 struct verbs_mr *vmr, struct ibv_reg_mr *cmd,

                 size_t cmd_size,

                 struct ib_uverbs_reg_mr_resp *resp, size_t resp_size)

      {

          int ret;



          cmd->start    = (uintptr_t) addr;

          cmd->length       = length;

          /* On demand access and entire address space means implicit.

           * In that case set the value in the command to what kernel expects.

           */

          if (access & IBV_ACCESS_ON_DEMAND) {

              if (length == SIZE_MAX && addr) {

                  errno = EINVAL;

                  return EINVAL;

              }

              if (length == SIZE_MAX)

                  cmd->length = UINT64_MAX;

          }



          cmd->hca_va       = hca_va;

          cmd->pd_handle    = pd->handle;

          cmd->access_flags = access;



          ret = execute_cmd_write(pd->context, IB_USER_VERBS_CMD_REG_MR, cmd,

                      cmd_size, resp, resp_size);

          if (ret)

              return ret;



          vmr->ibv_mr.handle  = resp->mr_handle;

          vmr->ibv_mr.lkey    = resp->lkey;

          vmr->ibv_mr.rkey    = resp->rkey;

          vmr->ibv_mr.context = pd->context;

          vmr->mr_type        = IBV_MR_TYPE_MR;

          vmr->access = access;



          return 0;

      }
      这里通过write命令写一条命令到操作系统内核。命令的类型就是IB_USER_VERBS_CMD_REG_MR。
      1
      2
      3
          ret = execute_cmd_write(pd->context, IB_USER_VERBS_CMD_REG_MR, cmd,

                      cmd_size, resp, resp_size);
      然后我们查看Linux内核中的InfiniBand的相关代码。
      可以在drivers/infiniband/core/uverbs_cmd.c这个目录下找到相关的代码。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
              DECLARE_UVERBS_WRITE(

                  IB_USER_VERBS_CMD_REG_MR,

                  ib_uverbs_reg_mr,

                  UAPI_DEF_WRITE_UDATA_IO(struct ib_uverbs_reg_mr,

                              struct ib_uverbs_reg_mr_resp),

                  UAPI_DEF_METHOD_NEEDS_FN(reg_user_mr)),
      这里可以找到一个叫做ib_uverbs_reg_mr的函数。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      static int ib_uverbs_rereg_mr(struct uverbs_attr_bundle *attrs)

      {

      // 省略前文
      // 注册过程
          new_mr = ib_dev->ops.rereg_user_mr(mr, cmd.flags, cmd.start, cmd.length,

                             cmd.hca_va, cmd.access_flags, new_pd,

                             &attrs->driver_udata);

          if (IS_ERR(new_mr)) {

              ret = PTR_ERR(new_mr);

              goto put_new_uobj;

          }

          if (new_mr) {

              new_mr->device = new_pd->device;

              new_mr->pd = new_pd;

              new_mr->type = IB_MR_TYPE_USER;

              new_mr->uobject = uobj;

              atomic_inc(&new_pd->usecnt);

              new_uobj->object = new_mr;



              rdma_restrack_new(&new_mr->res, RDMA_RESTRACK_MR);

              rdma_restrack_set_name(&new_mr->res, NULL);

              rdma_restrack_add(&new_mr->res);



              /*

               * The new uobj for the new HW object is put into the same spot

               * in the IDR and the old uobj & HW object is deleted.

               */

              rdma_assign_uobject(uobj, new_uobj, attrs);

              rdma_alloc_commit_uobject(new_uobj, attrs);

              uobj_put_destroy(uobj);

              new_uobj = NULL;

              uobj = NULL;

              mr = new_mr;

          } else {

              if (cmd.flags & IB_MR_REREG_PD) {

                  atomic_dec(&orig_pd->usecnt);

                  mr->pd = new_pd;

                  atomic_inc(&new_pd->usecnt);

              }

              if (cmd.flags & IB_MR_REREG_TRANS)

                  mr->iova = cmd.hca_va;

          }



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

          resp.lkey      = mr->lkey;

          resp.rkey      = mr->rkey;



          ret = uverbs_response(attrs, &resp, sizeof(resp));



      put_new_uobj:

          if (new_uobj)

              uobj_alloc_abort(new_uobj, attrs);

      put_uobj_pd:

          if (cmd.flags & IB_MR_REREG_PD)

              uobj_put_obj_read(new_pd);



      put_uobjs:

          if (uobj)

              uobj_put_write(uobj);



          return ret;

      }
      到这里,就可以看到从用户态到内核态的全貌。而用户态和内核态的交互是通过一个叫做ABI的东西实现的。

      ABI

      这部分参考了RDMA之用户态和内核态的交互的相关内容。
      ABI就是应用程序二进制接口(Application Binary Interface,ABI)。
      ABI定义了运行时的程序之间交流的格式,比如参数以什么形式传递(分别写到指定的寄存器/使用栈)、以什么格式传递以及返回值放到哪里等等。
      uverbs API规定了用户态和内核态之间的命令消息cmd的格式和返回消息resp的格式。
      RDMA软件栈通过设计uverbs ABI接口来保证不同版本的用户态和内核态之间的兼容性,即某个版本的用户态库,可以直接运行在各种版本的内核上
      ABI
      拿Create QP这个函数距离。IB规范上这样定义用户态传入内核态的cmd格式。
      CQP-CMD
      resp的定义如下。
      CQP-RESP
      在rdma-core/libibverbs/kern-abi.h文件中有如下定义。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      DECLARE_CMD(IB_USER_VERBS_CMD_ALLOC_MW, ibv_alloc_mw, ib_uverbs_alloc_mw);

      DECLARE_CMD(IB_USER_VERBS_CMD_ALLOC_PD, ibv_alloc_pd, ib_uverbs_alloc_pd);

      DECLARE_CMDX(IB_USER_VERBS_CMD_ATTACH_MCAST, ibv_attach_mcast, ib_uverbs_attach_mcast, empty);

      DECLARE_CMDX(IB_USER_VERBS_CMD_CLOSE_XRCD, ibv_close_xrcd, ib_uverbs_close_xrcd, empty);

      DECLARE_CMD(IB_USER_VERBS_CMD_CREATE_AH, ibv_create_ah, ib_uverbs_create_ah);

      DECLARE_CMD(IB_USER_VERBS_CMD_CREATE_COMP_CHANNEL, ibv_create_comp_channel, ib_uverbs_create_comp_channel);

      DECLARE_CMD(IB_USER_VERBS_CMD_CREATE_CQ, ibv_create_cq, ib_uverbs_create_cq);

      DECLARE_CMD(IB_USER_VERBS_CMD_CREATE_QP, ibv_create_qp, ib_uverbs_create_qp);

      // 省略。。。
      看这部分代码,可以看到规定了内核态API和用户态API的映射。例如下列代码。
      1
      DECLARE_CMD(IB_USER_VERBS_CMD_ALLOC_MW, ibv_alloc_mw, ib_uverbs_alloc_mw);
      IB_USER_VERBS_CMD_ALLOC_MW是cmd的类型,ibv_开头的函数是用户态的API,而ib_开头的函数是内核态的API。
      那么可以推测,在该头文件中出现定义的函数,只要调用,那么就需要陷入到内核态里去。

      总结

      本周总结

  • 对用户态和内核态的Verbs API做了一定的了解。
  • 了解了用户态API与内核态API交互的大体框架。

    未实现的

  • 了解IB网络数据包封装的细节。

    可选的深入的方向

  • RDMA通信细节
  • 了解RDMA Verbs ABI的内容
  • 了解RDMA与IB网卡交互的内容

    下周工作

    按照张老师意见,IB数据包应该是在驱动里面完成封装的,可以具体看看驱动的代码。