- [1\. はじめに](https://valinux.hatenablog.com/entry/#1-%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB) - [2\. 準備](https://valinux.hatenablog.com/entry/#2-%E6%BA%96%E5%82%99) - [インストール](https://valinux.hatenablog.com/entry/#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB) - [動作確認](https://valinux.hatenablog.com/entry/#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D) - [3\. ライブラリ](https://valinux.hatenablog.com/entry/#3-%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA) - [libibverbs](https://valinux.hatenablog.com/entry/#libibverbs) - [librdmacm](https://valinux.hatenablog.com/entry/#librdmacm) - [4\. プログラミング例](https://valinux.hatenablog.com/entry/#4-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E4%BE%8B) - [基本編](https://valinux.hatenablog.com/entry/#%E5%9F%BA%E6%9C%AC%E7%B7%A8) - [簡略編](https://valinux.hatenablog.com/entry/#%E7%B0%A1%E7%95%A5%E7%B7%A8) - [上級編](https://valinux.hatenablog.com/entry/#%E4%B8%8A%E7%B4%9A%E7%B7%A8) - [5\. おわりに](https://valinux.hatenablog.com/entry/#5-%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB) 執筆者 : 小田 逸郎 --- ## 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が複数付いている場合は、[このドキュメント](https://github.com/linux-rdma/rdma-core/blob/master/Documentation/librdmacm.md) に従って、以下のコマンドを実行しておいてください。 ``` $ sudo sysctl -w net.ipv4.conf.all.arp_ignore=2 ``` ## 3\. ライブラリ RDMAプログラミングには、libibverbs と librdmacm の2つのライブラリを使用します。 いずれも大元のソースコードは、[https://github.com/linux-rdma/rdma-core](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](https://github.com/oda-g/RDMA)にあるので、参照してください。 ## 基本編 コードは、[src/rpp/rpp.c](https://github.com/oda-g/RDMA/blob/master/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_addr`と`rdma_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](https://github.com/oda-g/RDMA/blob/master/src/rpp_e/rpp_e.c)です。 `rdma_create_ep`というAPIがあるので、それを使ってみた例です。機能は、rpp.c と変わりありません。 `rdma_create_ep`は、active側の`rdma_create_id`、`rdma_resolve_addr`、`rdma_resolve_route`、`rdma_create_qp`という 一連の流れと、passive側の`rdma_create_id`、`rdma_bind_addr`、`rdma_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](https://github.com/oda-g/RDMA/blob/master/src/rpp_h/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_addr`と`rdma_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](https://github.com/linux-rdma/rdma-core/tree/master/librdmacm) の下のコードを見るのが一番です。また、examplesの下には、rpingも含め、いろいろとサンプルプログラムがあるので、 参考にしてください。 libibverbsには触れませんでしたが、やはり、rdma-coreの[libibverbs](https://github.com/linux-rdma/rdma-core/tree/master/libibverbs)の下のコードを参照したり、librdmacmの下のコードからの 使われ方を見たりすると、仕様が把握できると思います。また、RDMA関連のカーネルの実装を調べるには、 これらライブラリから発行されるioctl/writeの延長から見るのがひとつの取っ掛かりになるかと思います。 ## 5\. おわりに RDMAプログラミングがどんなものか、基本は掴めたのではないでしょうか。libibverbsやlibrdmacmは、ハードウェア依存 の部分もあるので、本稿で紹介したプログラムが実際のハードウェアで動くかどうかは、ちょっと自信がありません。 それでもベースにするには十分かと思います。ハードウェアも昔に比べたら随分お求め易くなっていますので、是非とも実際の ハードウェアで試してみてください。