RDMAプログラミング入門

執筆者 : 小田 逸郎


1. はじめに

筆者が最初にInfiniBandやRDMAに触れたのは、もう20年近く昔の話になります。 それから、ブレークすることもなく、さりとて死に絶えることもなく、ひっそりと 生き続けてきました。最近また、ちょくちょく耳にするようになった気がします。 InfiniBand大手のMellanoxをNVIDIAが買収したというような話題もありました。

この20年程の間に、RDMAを使用する環境も手軽に用意できるようになりました。 なんと、普通のLinuxディストリビューションで普通に使えてしまいます。 とは言え、実際にRDMAで通信するプログラムを書こうとすると、まだ あまり情報がない気がします。本稿では、RDMAで通信するプログラムを書く ための第一歩を説明します。

2. 準備

ホスト間で通信を行うので、2台のホストを用意します。VMでも構いません。 2台のホスト間は、通常のEthernetで接続されていればOKです。そう、本稿では RDMA対応ハードウェアを使用せず、RDMAをした気になる、なんちゃって RDMAプログラミングを楽しみます。

インストール

本稿では、Linuxディストリビューションは、ubuntu 20.04 (focal) を使用します。

必要なパッケージをインストールします。以下のとおりです。

$ sudo apt update
$ sudo apt install linux-modules-extra-$(uname -r)
$ sudo apt install rdma-core libibverbs1 libibverbs-dev \
  librdmacm1 librdmacm-dev rdmacm-utils ibverbs-utils

RDMA関連のカーネルモジュールのほとんどが linux-modules-extra に入っています。 rdma-core 以下は、RDMAプログラミングやRDMAの動作確認に必要なパッケージです (依存して、他のRDMA関連パッケージも入ります)。

動作確認

RDMA対応ハードウェアを使用する代わりにソフトウェア実装のドライバを使用します。 rxe(RoCEv2のソフトウェア実装)と siw(iWARPのソフトウェア実装)の2種類が 使えますが、どちらでも結構です。ソフトウェア実装なので、まず、デバイスを 作成します。rdmaコマンドを使用します。形式は以下のとおりです。

usage: rdma link add NAME type TYPE netdev NETDEV
  NAME - 適当な名前
  TYPE - rxe または、siw
  NETDEV - 使用するNIC。通常に使用しているものでよい(別のNICを用意する必要はない)。

例を示します。

$ sudo rdma link add siw0 type siw netdev ens3
$ ibv_devices
    device                 node GUID
    ------              ----------------
    siw0                fa163ee17ef30000

ibv_devicesコマンドで、確認できれば、OKです。ibv_devinfoコマンドを使えば、もっと詳細な情報がでます。

メモ
rdmaコマンドは、iproute2パッケージに含まれます。iproute2パッケージは、通常インストールされているでしょう。rdma link addコマンドの裏側で、必要なカーネルモジュールのロードが行われています。

2台のホストで、デバイスの作成が済んだら、RDMA通信確認をしてみます。rpingコマンドを使用します。 一方のホストで待ち受けします。

host1 $ rping -s -v

もう一方から接続して通信を開始します。

host2 $ rping -c -a 192.168.0.11 -v -C 2

-aで指定したIPアドレスは、待ち受け側ホストのIPアドレスです。その他オプションの意味は、man pageを参照してください。

それぞれ、以下のような出力が出ればOKです。

host1 $ rping -s -v
server ping data: rdma-ping-0: ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqr
server ping data: rdma-ping-1: BCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrs
server DISCONNECT EVENT...
wait for RDMA_READ_ADV state 10
$
host2 $ rping -c -a 192.168.0.11 -v -C 2
ping data: rdma-ping-0: ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqr
ping data: rdma-ping-1: BCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrs
client DISCONNECT EVENT...
$

お手軽ですね。これでも、send、recieve、RDMA read、RDMA write と一通り実施されています。

メモ
rdmaコマンドの使用のためと、実のところあまりソフトウェア実装の品質が良くないので、 カーネルはなるべく新しい方がよいです。ディストリビューションは、ubuntu 20.04 である 必要はありませんが、少なくともカーネル5.4以降のものを使用してください。

メモ
NICが複数付いている場合は、このドキュメント に従って、以下のコマンドを実行しておいてください。

$ sudo sysctl -w net.ipv4.conf.all.arp_ignore=2

3. ライブラリ

RDMAプログラミングには、libibverbs と librdmacm の2つのライブラリを使用します。

いずれも大元のソースコードは、https://github.com/linux-rdma/rdma-core にあります。 先ほどインストールした、rdma-coreパッケージおよび、一緒にインストールしたパッケージのソースコードはすべてここにあります。

このソースコードを読めば、すべてが分かります。と、これで終わってしまっては身も蓋もないので、本稿では、知っておくと、ソースコードを読み易くなるような、基本的な事項を説明します。

メモ
ライブラリ間およびカーネルモジュール間の整合性を取る必要があるので、一部のパッケージのみ置き換えるのは危険です。ソースコードからのビルドについては、面倒なので、本稿では触れません。なお、本稿では、ubuntu標準のパッケージを使用しましたが、ハードウェアによっては、ベンダーがパッケージを配布している場合があります。その場合は、ベンダーのものを使用するのが無難でしょう(ただし、使用できるディストリビューションは制限されるかもしれません)。

libibverbs

InfiniBand verbs を実行するためのライブラリです。最もハードウェアに近い部分です。

メモ
InfiniBand以外(RoCE、iWARP)もこれを使用します。なお、本稿では、InfiniBand verbs には深入りしません。

関係するコンポーネントは以下のとおりです。

                    ユーザ                        | カーネル              
                                  uverbsN に ioctl
libibverbs API ---> libibverbs.so ----------------> ib_uverbs.ko
                        |                               |
                        + librxe-rdmav22.so   ------>   + rdma_rxe.ko
                        + libsiw-rdmav25.so   ------>   + siw.ko
                        + libi40iw-rdmav25.so ------>   + i40iw.ko
                        + libmlx5-rdmav25.so  ------>   + mlx5_ib.ko
                        + ...                           + ...

libibverbsライブラリは、/dev/infiniband/uverbsN ファイルにioctl(2)を発行することにより、デバイスを操作します。Nは、デバイスの認識順につけられる数字です。前述の動作確認でデバイスを作成した時に、/dev/infiniband/uverbs0 ファイルができていたはずです。

ライブラリは、本体(libibverbs.so)とデバイス毎のライブラリ(librxe-rdmav25.so等)から成り、使用するデバイスにより、対応するデバイス用ライブラリが動的にロードされます。デバイス毎のライブラリは、対応するカーネルモジュールを通して、デバイスを操作します。

デバイスによっては、初期化が済めば、あとは、ユーザライブラリ側から直接ハードウェアを操作するものもあります(カーネルバイパス)。本稿で使用しているのはソフトウェア実装なので、残念ながらすべて、システムコール経由となります(その上、Linuxカーネルの通常のネットワークスタックも通ります)。

メモ
デバイス毎のライブラリのv25の部分は、ビルドによって異なります。ディストリビューションで提供されているものは、揃っているはずです。

典型的なプログラムのAPI実行の流れを以下に示します。

sender                   reciever
-------------------      ------------------- 
ibv_get_device_list      ibv_get_device_list  ibデバイスの取得
ibv_open_device          ibv_open_device      ibデバイスのオープン
ibv_alloc_pd             ibv_alloc_pd         プロテクションドメイン(PD)作成
ibv_reg_mr               ibv_reg_mr           メモリリージョン(MR)の登録
ibv_create_cq            ibv_create_cq        コンプリーションキュー(CQ)作成
ibv_create_qp            ibv_create_qp        キューペア(QP)作成
ibv_modify_qp            ibv_modify_qp       QPの状態遷移: RESET->INIT
                         ibv_post_recv        受信ワークリクエスト(RR)投入
         <----------------->                  なんらかの手段で相手先のQP情報を交換
ibv_modify_qp            ibv_modify_qp        QPの状態遷移: INIT->RTR
ibv_modify_qp                                 QPの状態遷移: RTR->RTS (送信側のみ)
ibv_post_send                                 送信ワークリクエスト(WR)投入
ibv_poll_cq              ibv_poll_cq          ワークリクエストの完了をポーリング

(資源解放処理は略)

APIの詳細は、man page を参照してください。一般には、libibverbs ライブラリを直接使用するのではなく、後述の librdmacmライブラリを使用します。

librdmacm

コミュニケーションマネジメント(CM)を行ってくれるライブラリです。libibverbsによるプログラミングでは、途中に「なんらかの手段で相手先のQP情報交換」というミッシングリングがありましたが、そこを埋めてくれるものです。

librdmacmは、CMを提供するとともに、libibverbsをラッピングして、接続から通信まで一貫してプログラミングできるAPIを提供しています。librdmacmは、内部でlibibverbsを使用しています。

CMに関するコンポーネントを以下に示します。

                     ユーザ                     |        カーネル
                                rdma_cm に write           
librdmacm API ---> librdmacm.so ----------------> rdma_ucm.ko --> rdma_cm.ko  
                                                                   + iw_cm.ko (iWARP用CM)
                                                                   |   + siw.ko, i40iw.ko, ...
                                                                   |
                                                                   + ib_cm.ko (InfiniBand用CM)
                                                                       + rdma_rxe.ko, mlx5_ib.ko, ...

CMの本体は、rdma_cm カーネルモジュールです。ユーザアプリケーションから、CMの機能が使えるように、 rdma_ucmカーネルモジュールが用意されています。librdmacm では、 /dev/infiniband/rdma_cmファイルにwrite(2)システムコールを発行することにより、 rdma_ucmに処理を依頼しています。rdma_ucmは、ユーザコンテキストの管理と rdma_cm への橋渡しを行います。 rdma_cm から先は、iw_cm(iWARP用CM)とib_cm(InfiniBand、RoCE用CM)に分かれ、 最終的には、libibverbsと同様、デバイス毎のカーネルモジュールへ行き着きます。

メモ
なぜか、ioctl(2)ではなく、write(2)を使用していますが、実質的には、ioctl(2)と考えて差し支えありません。

典型的なプログラムのAPI実行の流れを以下に示します。内部的に使用している libibverbs APIも合わせて示します。各APIの概要は、プログラミング例で説明します。また、APIの詳細は、man pageを参照して下さい。

active側                      passive側
--------                      ---------
rdma_create_id                rdma_create_id
  ibv_get_device_list           ibv_get_device_list

rdma_resolve_addr             rdma_bind_addr
  ibv_device_open               ibv_device_open
  ibv_alloc_pd                  ibv_alloc_pd

rdma_resolve_route
                     
rdma_create_qp
  ibv_create_comp_channel
  ibv_create_cq
  ibv_create_qp
  ibv_modify_qp(RESET->INIT)

rdma_reg_msgs
  ibv_reg_mr
rdma_post_recv
  ibv_post_recv
                              rdma_listen
rdma_connect  --------------> rdma_get_request
     .
     .                        rdma_create_qp
     .                          ibv_create_comp_channel
     .                          ibv_create_cq
     .                          ibv_create_qp
     .                          ibv_modify_qp(RESET->INIT)
     .
     .                        rdma_reg_msgs
     .                          ibv_reg_mr
     .                        rdma_post_recv
     .                          ibv_post_recv
     .
  rdma_get_cm_event <-------  rdma_accept
    ibv_modify_qp(INIT->RTR)    ibv_modify_qp(INIT->RTR)
    ibv_modify_qp(RTR->RTS)     ibv_modify_qp(RTR->RTS)
  rdma_ack_cm_event

rdma_post_send
  ibv_post_send
rdma_get_send_comp            rdma_get_recv_comp
  ibv_poll_cq                   ibv_poll_cq

(資源解放処理は略)

メモ
librdmacmは、libibverbsをラッピングしていると言いましたが、完全にはラッピング仕切れておらず、libibverbsが染み出してきています。したがって、libibverbsの知識は必要です。また、お好みに応じて、librdmacm APIとlibibverbs APIを混在して使用することも可能で、よく見かけます。

4. プログラミング例

rpingライクなプログラムを自分で作って見ましょう。と言っても、本稿では、既に作成済のプログラムを参照しながら、ポイントを解説していきます。

プログラムは、https://github.com/oda-g/RDMAにあるので、参照してください。

基本編

コードは、src/rpp/rpp.cです。

前述した、librdmacm APIのプログラムの流れに忠実にしたがっています。active側から見ていきましょう。

   363        ret = rdma_create_id(NULL, &id, NULL, RDMA_PS_TCP);

まずは、操作のためのハンドル(rdma_cm_id構造体)を得ます(以降、ハンドルのことをcm_idと呼びます)。 これ以降に実行するライブラリAPIは、基本的にこのcm_idを指定することになります。

第一引数には、イベントチャネル(rdma_event_channel構造体)を与えますが、これには少し長い説明が 必要になります。

イベントチャネルの実体は、/dev/infiniband/rdma_cmをオープンしたファイルディスクリプタです。 これにwriteすることにより、CMに処理を依頼するのでした。CM処理の多くは、非同期です。 つまりwriteシステムコールは、処理の完了を待つことなく復帰します。処理の完了を知るためには、また、 そのための処理が用意されています。

本プログラムでは、イベントチャネルにNULLを指定しています。この場合、イベントチャネルは、 ライブラリ内部で作成され、その上、(当該cm_idに対して実行した)ライブラリAPIを同期型にしてくれます。 すなわち、ライブラリ内部でCM処理の完了を待ち合わせ、ライブラリAPI復帰時には、処理が完了している ことを保証してくれます。

自分でイベントチャネルを指定した場合、(当該cm_idに対して実行した)ライブラリAPIは、非同期型となります。 処理完了の取得を別に行う必要があります。(上級編で少し触れます。)

ほとんどのプログラムにおいて、同期型で十分のはずです。少なくとも、rppくらいのプログラムでは、 同期型で十分です。

   370       ret = rdma_resolve_addr(id, NULL, addr, 2000);

相手先のIPアドレスから、使用するデバイスの特定を行います。ライブラリ内部で、ibデバイスのオープンが 行われています。

   377       ret = rdma_resolve_route(id, 2000);

相手先までのルートを解決します。CM内部の処理になります。

    60  static int
    61  rpp_create_qp(struct rdma_cm_id *id)
    62  {
    63          struct ibv_qp_init_attr init_attr;
    64          int ret;
    65  
    66          memset(&init_attr, 0, sizeof(init_attr));
    67          init_attr.cap.max_send_wr = 2;
    68          init_attr.cap.max_recv_wr = 2;
    69          init_attr.cap.max_recv_sge = 1;
    70          init_attr.cap.max_send_sge = 1;
    71          init_attr.qp_type = IBV_QPT_RC;
    75          init_attr.sq_sig_all = 1;
    76  
    78          ret = rdma_create_qp(id, NULL, &init_attr);
    83          return ret;
    84  }

QPを作成します。rdma_create_qpでは、ibv_qp_init_attr構造体を渡しています。ここでは、 libibverbsが染み出してきています。また、ここはハードウェア依存もある部分なので、 他のデバイスを使用する場合は注意が必要です。(本稿では、ibv_qp_init_attr構造体の詳細には深入りしません。)

    86  static int
    87  rpp_setup_buffers(struct rdma_cm_id *id)
    88  {
    90          recv_mr = rdma_reg_msgs(id, &recv_buf, sizeof(recv_buf));
    97          send_mr = rdma_reg_msgs(id, &send_buf, sizeof(send_buf));
   104          read_mr = rdma_reg_read(id, read_data, sizeof(read_data));
   111          write_mr = rdma_reg_write(id, write_data, sizeof(write_data));
   117          return 0;
   118  }

通信に使用するメモリリージョンを登録します。

   395       ret = rdma_post_recv(id, NULL, &recv_buf, sizeof(recv_buf), recv_mr);

受信ワークリクエストを投入しておきます。大体、受信ワークリクエストは、QP作成後、接続確立前に 投入しておくというのが作法になっています。

   402       ret = rdma_connect(id, NULL);

相手先との接続を行います。今回は同期型にしているので、相手先からの応答を待って復帰します。復帰時には、 相手先との接続が確立しています。ここで、双方のQP情報の交換が行われます。

以降は、相手先と通信を行います。

   410          send_buf.buf = (uint64_t)read_data;
   411          send_buf.rkey = read_mr->rkey;
   412          send_buf.size = sizeof(read_data);
   415          ret = rpp_rdma_send(id);
   205  static int
   206  rpp_rdma_send(struct rdma_cm_id *id)
   207  {
   211          ret = rdma_post_send(id, NULL, &send_buf, sizeof(send_buf), send_mr, 0);
   217          return rpp_wait_send_comp(id);
   218  }
   186  static int
   187  rpp_wait_send_comp(struct rdma_cm_id *id)
   188  {
   190          struct ibv_wc wc;
   193          ret = rdma_get_send_comp(id, &wc);
   203  }

相手がRDMA READする領域の情報(rpp_rdma_info構造体)をsendします。具体的には、 rdma_post_sendで送信ワークリクエストを投入し、rdma_get_send_compで完了を待ち合せます。

相手は、受信した情報を元に RDMA READを行い、処理が完了したことを、send で通知 します。

   149  static int
   150  rpp_rdma_recv(struct rdma_cm_id *id)
   151  {
   156          ret = rdma_get_recv_comp(id, &wc);
   177          ret = rdma_post_recv(id, NULL, &recv_buf, sizeof(recv_buf), recv_mr);
   184  }

相手からの送信を受信します。受信ワークリクエストは既に投入済だったので、rdma_get_recv_compで 受信完了を待ち合せます。そして、次の受信ワークリクエストを投入しておく、という流れになって います。相手からもrpp_rdma_info構造体が送られてきますが、RDMA READが完了したという通知のためにsendして来ているだけなので、中身は見ません。

次に相手がRDMA WRITEする領域の情報をsendし、完了通知をrecieveします。 処理は同様ですので、説明は割愛します。

それでは、passive側の処理を見ていきましょう。

   228       ret = rdma_create_id(NULL, &listen_id, NULL, RDMA_PS_TCP);

まずは、cm_idの取得です。listen_idという変数名になっていますが、この理由は後で分かります。

   235       ret = rdma_bind_addr(listen_id, addr);

自ホストのIPアドレスから、使用するデバイスを特定します。ここで、ibデバイスのオープンが行われます。 active側は、rdma_resolve_addrrdma_resolve_routeを使用していましたが、passive側は、 rdma_bind_addrを使用します。

   242       ret = rdma_listen(listen_id, 1);

相手からの接続要求を受け付けられるようにします。rdma_listen自体では、待ち受けは しません。

   249       ret = rdma_get_request(listen_id, &id);

rdma_get_requestで相手からの接続要求を待ち合せます。相手からの接続要求が来ると、 接続処理が開始され、以降の通信で使用する新しい cm_id が返されます。

以降は、rdma_get_requestで返されたcm_idに対して、処理を行います。

   255          ret = rpp_create_qp(id);
   260          ret = rpp_setup_buffers(id);
   267          ret = rdma_post_recv(id, NULL, &recv_buf, sizeof(recv_buf), recv_mr);

接続を確立する前に、QPの作成、メモリリージョンの登録、受信ワークリクエストの投入を 行います。

   274       ret = rdma_accept(id, NULL);

rdma_acceptで接続の確立を行います。この延長で、active側のrdma_connectが復帰すること になります。

後は、acitve側とのやりとりです。

   281          ret = rpp_rdma_recv(id);
   288          ret = rdma_post_read(id, NULL, read_data, rlen, read_mr, 0, raddr, rkey);
   294          ret = rpp_wait_send_comp(id);
   302          ret = rpp_rdma_send(id);

RDMA READする領域の情報をrecieveし、その情報を元にRDMA READリクエストを投入します。 RDMA READの完了は、rdma_get_send_compで待ち合わせします。RDMA READ完了後、相手に完了 通知をsendします。

   308          ret = rpp_rdma_recv(id);
   318          ret = rdma_post_write(id, NULL, write_data, rlen, write_mr, 0, raddr, rkey);
   324          ret = rpp_wait_send_comp(id);
   330          ret = rpp_rdma_send(id);

同様にRDMA WRITEします。RDMA WRITEの完了待ちもrdma_get_send_compで行います。

これで、基本編は終わりです。基本的な処理の流れは掴めたのではないでしょうか。

簡略編

コードは、src/rpp/rpp_e.cです。

rdma_create_epというAPIがあるので、それを使ってみた例です。機能は、rpp.c と変わりありません。

rdma_create_epは、active側のrdma_create_idrdma_resolve_addrrdma_resolve_routerdma_create_qpという 一連の流れと、passive側のrdma_create_idrdma_bind_addrrdma_create_qpという一連の流れを、これひとつ で賄うという便利関数となっています。

    50  static int
    51  rpp_create_ep(const char *server_ip, struct rdma_cm_id **id, int server)
    52  {
    53  
    54          int ret;
    55          struct rdma_addrinfo hints, *res;
    56          struct ibv_qp_init_attr init_attr;
    57          struct ibv_wc wc;
    58  
    59          memset(&hints, 0, sizeof hints);
    60          hints.ai_port_space = RDMA_PS_TCP;
    61          if (server) {
    62                  hints.ai_flags = RAI_PASSIVE;
    63          }
    65          ret = rdma_getaddrinfo(server_ip, "7999", &hints, &res);
    70  
    71          memset(&init_attr, 0, sizeof(init_attr));
    72          init_attr.cap.max_send_wr = 2;
    73          init_attr.cap.max_recv_wr = 2;
    74          init_attr.cap.max_recv_sge = 1;
    75          init_attr.cap.max_send_sge = 1;
    76          init_attr.qp_type = IBV_QPT_RC;
    77          init_attr.sq_sig_all = 1;
    78  
    80          ret = rdma_create_ep(id, res, NULL, &init_attr);
    87  }

rdma_create_epは、active側、passive側共通で、違いは、62行目のフラグの区別だけです。 アドレス情報として、rdma_addrinfo構造体を渡すようになっており、それに伴い、 rdma_getaddrinfoを使用しています。上記はその使用例としても参照できます。 中でQPの作成も行われるので、ibv_qp_init_attr構造体も渡しています。

   351          ret = rpp_create_ep(server_ip, &id, 0);
   356          ret = rpp_setup_buffers(id);

active側の処理の流れです。rdma_create_idからrdma_create_qpまでが、rdma_create_epに 置き換えられています。

   230          ret = rpp_create_ep(server_ip, &listen_id, 1);
   236          ret = rdma_listen(listen_id, 1);
   243          ret = rdma_get_request(listen_id, &id);
   249          ret = rpp_setup_buffers(id);

passive側の処理の流れです。QPは、rdma_get_requestで取得した新しいcm_idに対して 作成しないといけなかったはずですが、これでいいのでしょうか。実は、rdma_create_epを 使用したときは、rdma_get_requestの延長で、新しいcm_idに対して、QPを作成するような 仕掛けになっています。分かりにくいですね。

後の処理は、rpp.cと同じです。

考察
rdma_create_epを使用すれば、プログラムが簡略になるかと思って使ってみましたが、意外に コード行数は減りませんでした。まあ、接続後の処理がプログラムのメインですからね。折角プログラムを書いたので、本稿で取り上げてみましたが、 上記のように、passive側で分かりにくいところがありますし、使うまでもないな、というのが正直な感想です。

上級編

コードは、src/rpp/rpp_h.cです。

rppでは、active側とpassive側が一対一で通信するだけでしたが、ここでは、もう少し、クライアント・サーバ風にしてみましょう。passive側(サーバ)を複数のactive側(クライアント)と同時に接続 できるようにして、また、クライアントとの通信が終わっても、サーバプロセスを終了しないようにしてみます。

active側の処理は、基本的に違いはありません。

   500          struct rpp_context *ct;
   501  
   502          ct = rpp_init_context();
   507          ret = rdma_create_id(NULL, &id, ct, RDMA_PS_TCP);

passive側が複数接続を扱うようになったので、プログラムの最初の方で定義していたstatic 変数を rpp_context構造体にまとめ、接続ごとに確保するようになっています。折角ですので、rdma_create_idの 第3引数に与えるようにしました。これは、contextというメンバ名でアクセス可能な、ユーザが自由に 設定できる値です。(後で、少し触れます。)

active側の処理の違いは、static変数へのアクセスがrpp_context構造体メンバへのアクセスになった だけです。

passive側の処理を見ていきましょう。

   388          ch = rdma_create_event_channel();
   395          ret = rdma_create_id(ch, &listen_id, NULL, RDMA_PS_TCP);
   405          ret = rdma_bind_addr(listen_id, addr);
   412          ret = rdma_listen(listen_id, 3);

rdma_create_event_channelでイベントチャンネルを作成し、rdma_create_idの第一引数に渡すように しています。後、rdma_bind_addrrdma_listenまでは変わりありません。(なお、rdma_bind_addrでは、 非同期のCM要求を出していないので、イベント処理の必要がありません。)

同期型の場合は、相手からの接続要求をrdma_get_requestで待ち合わせましたが、非同期型の 場合は、自力で、(rdma_listenに由来する)イベントを処理する必要があります。以降の処理は、 複数の要求を処理するため、whileループの中で行います。

   428          while (terminate == 0) {
   430                  ret = rdma_get_cm_event(ch, &event);
   435                  if (event->status != 0) {
   436                          fprintf(stderr, "event status == %d\n", event->status);
   437                          goto out;
   438                  }
   439                  if (event->event != RDMA_CM_EVENT_CONNECT_REQUEST) {
   440                          fprintf(stderr, "unexpected event %d != %d(expected)\n",
   441                                  event->event, RDMA_CM_EVENT_CONNECT_REQUEST);
   442                          goto out;
   443                  }
   444                  id = event->id;
   446                  ret = rdma_ack_cm_event(event);

rdma_listenに限らず、何か非同期のCM要求を実行した場合、CMからイベントという形で、 応答を貰う必要があります。それがrdma_get_cm_eventになります。これは、指定した イベントチャネルに対して、CMからイベントが返されるのを待ち受けます。

rdma_listenは、相手からの接続要求を受け付けられる状態にしますが、実際に接続要求が あると、RDMA_CM_EVENT_CONNECT_REQUEST というイベントが上がります。上記は それを待っています。

rdma_get_cm_eventの引数には、イベントチャネルを渡しています。イベントチャネルは、 複数のcm_idで共有可能です。eventのidメンバにcm_idが格納されており、どのcm_idに 対する処理か識別できるようになっています。(このとき、cm_idのcontextをその後の処理に 利用することがよく行われています。)

RDMA_CM_EVENT_CONNECT_REQUESTイベントの場合は、ちょっと特殊で、evnetのidには、 その後の通信で使用する新しいcm_idが格納されています。(listenしたcm_idは、eventの listen_idに格納されています。)

取得したイベントは、rdma_ack_cm_eventで、CMに返却する必要があります。

それでは、whileループの続きを見てみましょう。

   454                  ret = rdma_migrate_id(id, NULL);
   460                  ret = pthread_create(&th, NULL, exec_rpp, (void *)id);  
   467                  ret = pthread_detach(th);
   472          }

新しいcm_idは、listen時のcm_idとイベントチャネルを共有しています。rdma_migrate_idは、 イベントチャネルを別のものに変えるAPIです。ここでは、イベントチャネル(第2引数)として NULLを与えていますが、これは、同期型に変えることを意味します。この場合、イベントチャネル は、ライブラリ内部で作成されます。(rdma_create_idのときと同じ要領です。)

rdma_get_cm_eventからrdma_ack_cm_eventまでの処理は、実は、rdma_get_requestの中でも行われて いる処理です。なお、rdma_get_requestは、同期型のcm_idにしか使えません。

実際の通信部分は、別スレッド(exec_rpp関数)で行います。exec_rpp関数の中身は、rppと変わりありません ので説明は割愛します。コードをご参照ください。各クライアントとのやりとりは、スレッドごとに行うため、非同期型にする意味はありません。そのため、同期型にしたわけです。

スレッドを起動した後は、whileループの先頭に戻り、再び接続要求が来るのを待ちます。(rdma_listenを 出し直す必要はありません。)

考察
サーバサイドのプログラムでは、非同期型が必要になるのではないか、という仮説を立てて、rpp_h.cを 作ってみたのですが、意外に非同期型の出番はありませんでした。 rdma_migrate_idは、同期型から同期型への変更ができないという仕様だったので、かろうじて、 listen用cm_idを非同期型にする理由がありましたが、rdma_migrate_idが同期型から同期型への変更を 許していれば、あえて非同期型にする必要がなかったところです。まあ、そのおかげで、イベント処理の 例を示すことができましたが。

メモ
複数のcm_idでイベントチャネルを共有するのは、イベント処理が複雑になります。rping はそのような 実例ですので、興味があればコードを参照してください。ただし、プログラムの作りとしては、必要 以上に複雑にしていると、筆者は思うので、お勧めではありません。APIとそれに対応するイベントの 仕様などを把握するためには、大変参考にさせていただきました。
ライブラリの仕様や対カーネル(rdma_ucm)APIの仕様の把握は、rdma-coreのlibrdmacm の下のコードを見るのが一番です。また、examplesの下には、rpingも含め、いろいろとサンプルプログラムがあるので、 参考にしてください。
libibverbsには触れませんでしたが、やはり、rdma-coreのlibibverbsの下のコードを参照したり、librdmacmの下のコードからの 使われ方を見たりすると、仕様が把握できると思います。また、RDMA関連のカーネルの実装を調べるには、 これらライブラリから発行されるioctl/writeの延長から見るのがひとつの取っ掛かりになるかと思います。

5. おわりに

RDMAプログラミングがどんなものか、基本は掴めたのではないでしょうか。libibverbsやlibrdmacmは、ハードウェア依存 の部分もあるので、本稿で紹介したプログラムが実際のハードウェアで動くかどうかは、ちょっと自信がありません。 それでもベースにするには十分かと思います。ハードウェアも昔に比べたら随分お求め易くなっていますので、是非とも実際の ハードウェアで試してみてください。