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; }; これらを使って基本的なイベント監視の登録/解除の操作を作っていきます。 ...