執筆者 : 小田 逸郎
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_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です。
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です。
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
の下のコードを見るのが一番です。また、examplesの下には、rpingも含め、いろいろとサンプルプログラムがあるので、
参考にしてください。
libibverbsには触れませんでしたが、やはり、rdma-coreのlibibverbsの下のコードを参照したり、librdmacmの下のコードからの
使われ方を見たりすると、仕様が把握できると思います。また、RDMA関連のカーネルの実装を調べるには、
これらライブラリから発行されるioctl/writeの延長から見るのがひとつの取っ掛かりになるかと思います。
5. おわりに
RDMAプログラミングがどんなものか、基本は掴めたのではないでしょうか。libibverbsやlibrdmacmは、ハードウェア依存 の部分もあるので、本稿で紹介したプログラムが実際のハードウェアで動くかどうかは、ちょっと自信がありません。 それでもベースにするには十分かと思います。ハードウェアも昔に比べたら随分お求め易くなっていますので、是非とも実際の ハードウェアで試してみてください。