I/Oの多重化(epoll API)を使いやすくしたい

はじめに epoll APIは有能だなあと思う今日この頃ですが、epoll_wait()してイベントを取り出す処理をべた書きするとコードが汚くなりがちです。少し前の記事ですが、epoll APIを使ったチャットサーバでは、epoll_wait()後のfor(){}が長くなり、読みにくいものになっていました。そこでイベント監視を少し一般化して裏でepollを呼ぶような仕組みを作りたくなりました。話がややこしいので結論だけ書いておくと、 epoll_data_t dataのユーザ定義のポインタに関数ポインタが混じった構造体を代入すると簡潔にアプリケーション側が書けそう インタレストリストへ追加/削除するような基本的な関数を作ってアプリケーション側から呼ぶと良さそう だねっていう話です。 後述するこれらの良くないであろう点でも書いていますが、あまりいいものではないです。 この記事内で使うコードの一式をGitHubに置いておきます。ヘッダファイルはこの記事内ですべてを載せてるわけではなので使ってみたい場合は以下のリポジトリからどうぞ。 いい感じにするための方針 方向性 いい感じにするには、struct epoll_eventのメンバであるepoll_data_t dataカギになりそうです。epoll_data_tは共用体なので実際に(同時に)使えるメンバは1つだけです。前回のチャットサーバと同様に、void *ptrを使うことを考えていきます。void *ですので任意の型で代入することができます。 typedef union epoll_data { void *ptr; // ユーザ定義のデータへのポインタ int fd; // ファイルディスクリプタ uint32_t u32; // 32ビット整数 uint64_t u64; // 64ビット整数 } epoll_data_t; 前回のチャットサーバで、for(){}が長くなっている原因はIO可能になって処理する内容を関数として作られていないからというのがありますが、何よりもそれらの処理が連結リストのポインタの操作が混じっていることが原因だと思います。そのため、ポインタ操作など面倒くさいものはできるだけ裏でやるようにすると良さそうです。 void*ptrに入れる構造体 void *ptrに代入するものを以下のstruct io_eventのようにしました。 /** * epollのstruct epoll_eventのユーザ定義のフィールドで使いたい構造体 */ struct io_event { //! 監視対象のファイルディスクリプタ int fd; //! そのイベントがIO可能になった時に呼び出される関数(ポインタ) void (*handler)(struct io_event*); //! 呼び出される関数内で使いたい変数など(任意) void* arg; //! 前のio_event(連結リスト) struct io_event* prev; //! 後のio_event(連結リスト) struct io_event* next; //! 最後に発生したイベント日時 struct tm* timestamp; //! 発生したイベントの種類 observe_type type; }; int fd struct epoll_eventのメンバであるepoll_data_t dataは共用体なので、void *ptrを使うとファイルディスクリプタを代入する変数int fdは使えなくなり、そのままだと何かと不便なので以下のstruct io_eventにファイルディスクリプタの変数を作りました。 void (*handler)(struct io_event*) IO可能となった時に呼び出される関数を関数ポインタvoid (*handler)(struct io_event*);としました。その引数をstruct io_event*とし、そのIO可能となったファイルディスクリプタのイベント情報を渡せるようにしました。 void* arg void (*handler)(struct io_event*);内で使いたい変数などを入れておくための変数。例えば、クライアント-サーバモデルを考えたときにファイルディスクリプタにユーザ名結び付けた場合は、この変数にユーザ名を入れておくことでユーザ名の登録後の発言で、発言内容と発言元のユーザ名が取れるようになります。 struct io_event* prev、struct io_event* next; 連結(双方向)リストのためのポインタ、説明不要 struct tm* timestamp IOが可能となった時点のタイムスタンプ observe_type type 監視するイベントの種類(以下の列挙体) /** * @enum observe_type * 監視するイベントの種類が定義された列挙体 */ typedef enum { //! EPOLLINに相当 OBS_IN = 1u, //! EPOLLOUTに相当 OBS_OUT = 4u, //! EPOLLONESHOTに相当 OBS_ONESHOT = 1u << 30, } observe_type; /** * event_io全体で使われる変数一式が定義された構造体 */ struct event_config { //! epollのファイルディスクリプタ int epfd; //! struct io_eventの先頭ポインタ struct io_event* head; //! struct io_eventの末尾ポインタ struct io_event* tail; }; これらを使って基本的なイベント監視の登録/解除の操作を作っていきます。 ...

July 3, 2021 · Eisuke Okazaki

I/O多重化でチャットサーバを作る

チャットサーバの簡単な説明 1プロセス1スレッドで動作するチャットサーバを考えていきます。 echoサーバとほぼ同じです。 複数のクライアントが接続できるサーバを考えます。 クライアントからの1回目のwrite()でユーザ名の登録をします。 ユーザ名の登録以外(2回目以降)のwrite()はサーバはそれを受け取った後に、メッセージを他の全クライアントに対して送信します。 サーバはメッセージを受け取った後に以下のフォーマットに従って編集します。 [メッセージの通し番号 発言元のユーザ名 日時] メッセージ プログラムの実行例(というか最終的に出来上がったものの実行例) ※ 今回はサーバがメインです。クライアント側はtelnetをいくつか開いて、それぞれをクライアントと想定しています。 発言元はログイン時、ログアウト時は"server"とし、それ以外の発言はユーザ名を表示しています。 epoll APIについて I/Oの多重化を扱うにはいくつかの方法があるようですが、select()やpoll()よりも扱いやすそうだったため、今回はepoll()を使用しました。 epoll APIはLinux固有の機能であるためLinux上での実行を想定しています。 int epoll_create(int size); epollインスタンスの作成 インタレストリストを空にする 引数 説明 備考 int size 監視対象のファイルディスクリプタ数 sizeは初期化処理のヒントのためであって、sizeを超えた数のファイルディスクリプタの処理も可能(だから適当な値でいいっぽい?) 戻り値の型 説明 int 成功時にファイルディスクリプタ、エラー時に-1 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev); インタレストリストの変更(追加、編集、削除) 引数 説明 int epfd epoll_create();で作成したfd int op EPOLL_CTL_ADD: epfd上のインタレストリストにfdを追加する EPOLL_CTL_MOD: epfd上のインタレストリストに追加済みのfdを指定されたイベントに変更する EPOLL_CTL_DEL : epfd上のインタレストリストからfdを削除する(監視を外す) int fd 監視対象のファイルディスクリプタ struct epoll_event* ev epollイベント.eventsとユーザデータ.dataが格納されたstruct epoll_event 戻り値の型 説明 int 成功時に0、エラー時に-1 int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout); イベントを待つ ...

June 4, 2021 · Eisuke Okazaki