InfiniBand Kernel RDMA Subsystem

RDMA内核态Verbs学习

  • 本文参考了知乎博主Savir创建的知乎RDMA专栏。
  • 本文还参考了知乎博主围城的博客内容。
  • 本文阅读的Linux内核版本为6.0.1。

    内核态API与用户态API

    Verbs API是一组用于使用RDMA服务的接口。Verbs API向用户提供了有关RDMA的一切功能,包括注册MR, 创建QP等操作。
    在Linux中,Verbs API功能由用户态的rdma-core和内核中的Kernel RDMA Subsystem系统。其中分别以ibv_和ib_作为前缀区分。
    RDMA希望数据通路上的调用尽可能绕开内核,而控制面上则需要在内核做出一定量的工作,因此就存在部分的内核态API。

    RDMA发送基本过程

    基本概念

  • WQ : Work Queue。WQ是存储工作请求的队列。
  • WQE : Work Queue Element。WQE是WQ存放的元素,涉及到具体的意义。WQ是一份“任务书”。任务通常是由软件下达给硬件的。
  • QP : Queue Pair。一对WQ,通信过程需要两端,QP是发送队列和接收队列的组合。且在RDMA中,通信的基本单位是QP,对于每个节点来说,每个进程可以使用若干QP。每个节点的QP都有一个唯一的编号,称为QPN。
  • CQ :Complete Queue。完成队列,通常存放一个任务的完成报告。其中元素的名字是CQE,和WQE是一组相反的概念。如果说WQE是软件下达给硬件的,那么CQE则是硬件反馈给软件的。
    Node与QPN

    基本过程

    基本的工作流程如下图所示。下图说明了可靠服务类型的交互流程,因此第五步是由接收端返回ACK给发送端。
    RDMA工作过程
    而RQ是一个被动的过程,只有收到Send报文的时候,硬件才会消耗RQ中的WQE。
    因此在APP眼中,又将引入以下两个概念。
  • WC:Work Complete。就是CQE在用户APP中的映射。
  • WR:Work Request。就是WQE在用户APP中的映射。

    代码层面

    post_send的实现

    先看到内核中的发送函数ib_post_send(),最终也是调用与设备相关的函数post_send。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static inline int ib_post_send(struct ib_qp *qp,

                       const struct ib_send_wr *send_wr,

                       const struct ib_send_wr **bad_send_wr)

    {

        const struct ib_send_wr *dummy;



        return qp->device->ops.post_send(qp, send_wr, bad_send_wr ? : &dummy);

    }
    而ops.post_send是ops结构体中的一个函数指针,ops是ib_device_ops类型的。看结构应该是定义了从设备到具体硬件的过程。通过函数指针的形式,引用到真正的post_send实现之中去。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct ib_device_ops {
    ....
        int (*post_send)(struct ib_qp *qp, const struct ib_send_wr *send_wr,

                 const struct ib_send_wr **bad_send_wr);

        int (*post_recv)(struct ib_qp *qp, const struct ib_recv_wr *recv_wr,

                 const struct ib_recv_wr **bad_recv_wr);
       ....
    post_send函数是在指定的QP上进行操作的。而QP又与device绑定,而与设备相关的操作device_ops又与device绑定。
    那么post_send具体的实现,又是在哪步被挂上的呢?
    看到Linux内核源码中的drivers/infiniband/hw/mlx5/main.c文件中,存在以下代码。
    1
    2
    3
    4
    5
    6
    7
    static const struct ib_device_ops mlx5_ib_dev_ops = {
    ...
    .post_recv = mlx5_ib_post_recv_nodrain,

        .post_send = mlx5_ib_post_send_nodrain,
    ...
    }
    可以看到在此处绑定了mlx5_ib_dev_ops的具体函数。那么接下去就有两个探索方向,一个是向下到硬件实现去,另外一个则是向上探索mlx5_ib_dev_ops在何处被启用。
    向下探索
    继续跳转代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    static inline int mlx5_ib_post_send_nodrain(struct ib_qp *ibqp,

                            const struct ib_send_wr *wr,

                            const struct ib_send_wr **bad_wr)

    {

        return mlx5_ib_post_send(ibqp, wr, bad_wr, false);

    }
    进入到mlx5_ib_post_send的具体实现之中,按照章节RDMA发送基本过程中所说,硬件设备会提取RQ中的一个WR(WQE),根据WR的内容处理消息。在这里还没有仔细阅读。
    向上探索
    查找mlx5_ib_dev_ops的定义,可以看到在函数mlx5_ib_stage_caps_init中绑定了设备操作与设备。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static int mlx5_ib_stage_caps_init(struct mlx5_ib_dev *dev) {
    // .... 省略
        if (MLX5_CAP_DEV_MEM(mdev, memic) ||

            MLX5_CAP_GEN_64(dev->mdev, general_obj_types) &

            MLX5_GENERAL_OBJ_TYPES_CAP_SW_ICM)

            ib_set_device_ops(&dev->ib_dev, &mlx5_ib_dev_dm_ops);



        ib_set_device_ops(&dev->ib_dev, &mlx5_ib_dev_ops);
    // .... 省略
    }
    查找mlx5_ib_stage_caps_init在何时被调用。找到mlx5_ib_stage_caps_init在struct mlx5_ib_profile中被定义。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    static const struct mlx5_ib_profile pf_profile = {
    // .... 省略
        STAGE_CREATE(MLX5_IB_STAGE_CAPS,

                 mlx5_ib_stage_caps_init,

                 mlx5_ib_stage_caps_cleanup),
    // .... 省略
    }
    而pf_profile在mlx5r_probe函数被引用。而该函数形成了mlx5 driver的一部分。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    static struct auxiliary_driver mlx5r_driver = {

        .name = "rdma",

        .probe = mlx5r_probe,

        .remove = mlx5r_remove,

        .id_table = mlx5r_id_table,

    };
    该驱动在mlx5_ib_init函数中被使用。该函数通过module_init被加载。module_init是一个宏,用于在载入内核的模块。
    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
    module_init(mlx5_ib_init);

    static int __init mlx5_ib_init(void)

    {

        int ret;



        xlt_emergency_page = (void *)__get_free_page(GFP_KERNEL);

        if (!xlt_emergency_page)

            return -ENOMEM;



        mlx5_ib_event_wq = alloc_ordered_workqueue("mlx5_ib_event_wq", 0);

        if (!mlx5_ib_event_wq) {

            free_page((unsigned long)xlt_emergency_page);

            return -ENOMEM;

        }



        mlx5_ib_odp_init();

        ret = mlx5r_rep_init();

        if (ret)

            goto rep_err;

        ret = auxiliary_driver_register(&mlx5r_mp_driver);

        if (ret)

            goto mp_err;

        ret = auxiliary_driver_register(&mlx5r_driver);

        if (ret)

            goto drv_err;

        return 0;



    drv_err:

        auxiliary_driver_unregister(&mlx5r_mp_driver);

    mp_err:

        mlx5r_rep_cleanup();

    rep_err:

        destroy_workqueue(mlx5_ib_event_wq);

        free_page((unsigned long)xlt_emergency_page);

        return ret;

    }
    如果将前文所述内容反过来看,就是一个完整的Linux Kernel绑定Verbs API和具体的驱动的过程。

    WR的实现

    而send_wr的定义如下。ib_send_wr在内核Verbs API中是最基本的WR数据结构,随之衍生的WR数据结构还有
    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
    struct ib_send_wr {

        struct ib_send_wr      *next;

        union {

            u64     wr_id;

            struct ib_cqe   *wr_cqe;

        };

        struct ib_sge          *sg_list;

        int         num_sge;

        enum ib_wr_opcode   opcode;

        int         send_flags;

        union {

            __be32      imm_data;

            u32     invalidate_rkey;

        } ex;

    };
    SGE
    可以在ib_send_wr中看到以下的定义。
    1
    2
    3
        struct ib_sge          *sg_list;

        int         num_sge;
    sg_list用来存放SGE元素,每一个SG是一个数据段。SG(Scatter/Gather),从名字中可以看出,该数据段的用处就是聚合分散的地址空间。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct ib_sge {

        u64 addr;

        u32 length;

        u32 lkey;

    };
    字段说明如下所示。
  • addr : 数据段所在的虚拟内存的起始地址。
  • length : 数据段长度。
  • lkey : 数据段对应的lkey。
    WR-SGE
    RECV过程中的WR行为
    本来是想在post_send中寻找使用SGE聚合消息,然后组装成数据帧的过程。但是没有发现相关内容。
    但是在post_recv中却发现了相关内容。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int mlx5_ib_post_recv(struct ib_qp *ibqp, const struct ib_recv_wr *wr,

                  const struct ib_recv_wr **bad_wr, bool drain)

    {
    // 省略
            if (qp->flags_en & MLX5_QP_FLAG_SIGNATURE)

                scat++;


    // 遍历WR
    // 此处的WR是ib_recv_wr
            for (i = 0; i < wr->num_sge; i++)

                set_data_ptr_seg(scat + i, wr->sg_list + i);

    // 省略
        }
    scat是mlx5_wqe_data_seg类型的消息,根据RDMA接收消息的过程进行合理推测。
  • 当接收端设备接收到消息后,调用post_recv回调函数。
  • 此时post_recv回调函数消费一个WR,根据WR中的SGE配置内存区域。聚合成scat。
  • 根据scat的地址信息对数据进行复制。

    总结

  • 分析了Linux RDMA InfiniBand实现,从上到底层,驱动的初始化过程。