はじめに:本記事の趣旨
以下、C言語、Linux環境を前提とする。
複数のファイルディスクリプタへのイベントを管理したいとき、例えばselect()を使用する方法があるが、噂では1これはかなり遅く(O(n))、性能を求めるならepollインターフェースを使うのが良い、とのことらしい。
しかしながら、 epollインターフェースは各関数が何を行なっているのかがよく分からない。故にどこで何を呼べば求めている処理を実現できるのかが明瞭でない。
以下では、 select()の使用法と対応させながら、epollインターフェースの使用法を簡明にまとめる。 よって、前者の挙動をある程度理解していることが前提となる。
また、例として、 listen用のソケットディスクリプタと、各クライアントと接続されたソケットディスクリプタ群とを管理するコードを使用 する。その為、ソケット通信に関する最低限の知識も前提とする。
なお(これは予防線だが)本記事は、厳密な理解を追求するものではなく、一先ずepollを大きな違和感なく使用できるようにすることを目的とするものであり、またあくまでも自分用のメモに過ぎない。
select(), epollとは
私の拙い日本語でこれを改めて説明することに意味はない。
誰もが何度も参照するであろうLinuxマニュアルへのリンクを貼ることで、説明を終えたことにしよう。
####1. select関数
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/select.2.html
####2. epollインターフェース
- インターフェース全体
- epoll_create()
- epoll_ctl()
- epoll_wait()
では以下より本題、即ちselectと対比する形でのepoll使用法の説明に入っていこう。
具体的には、 以下のフェーズに分けて、読み込みイベント(ソケットへの受信)を待つ場合の使用法(呼び出すべき関数とその使い方)をみていく。
よって、受信のあったソケットディスクリプタの管理を目的とすることを前提とした記述となっていることに留意して欲しい。
I. 初期化及びlisten用ソケットの追加
####1. select関数
select()を使う場合には先ず、fd_set構造体に対象のlisten用ソケットディスクリプタをセットする作業が必要となる。
なお、同様のファイルディスクリプタ集合に対して何度もループしてselect()を実行する場合には、以下のコードのように2つのfd_set構造体を用意しておくと簡便であろう。
/* 管理対象となるファイルディスクリプタの集合 */
fd_set target_fds;
/* selectの結果の格納先 */
fd_set readable_fds;
/* 読み込みを待つ、listen用のソケットディスクリプタ */
int sd_listen;
/* 管理対象保存用fd_setを初期化 */
FD_ZERO(&target_fds);
/* 管理対象保存用fd_setにlisten用ソケットを追加 */
FD_SET(sd_listen, &target_fds);
while ( 1 )
{
/* 結果格納用fd_setに、管理用fd_setの中身をコピーする */
/* 結果格納用fd_setはselectが書き換えてしまうので、毎回コピーする必要あり */
memcpy(&readable_fds, &target_fds, sizeof(fd_set));
/* 以下で、selectを使用(省略) */
}
####2. epollインターフェース
epollインターフェースを使用する場合には、少々複雑な作業が必要となる。
-
まず、インターフェース使用の為のepollインスタンスを作成する。当該インスタンスとはepoll_create()関数が返すファイルディスクリプタを通じてやり取りできる。
-
次に、listen用ソケットディスクリプタを追加する処理だが、これも少しばかり厄介で、以下の2つの処理が必要である。
- ディスクリプタに紐づけるイベント情報を設定する。(epoll_event構造体2を使用)
- epoll_ctl()関数(EPOLL_CTL_ADDオプションを渡す)を使って、当該ディスクリプタとイベント情報とをepollインスタンスに追加する。
/* 管理対象とするファイルディスクリプタの上限数 */
#define N_FDS (5)
/* epoll_waitの結果の格納先 */
struct epoll_event events[N_FDS];
/* epollインスタンスを参照するファイルディスクリプタ */
int epfd;
/* ファイルディスクリプタと紐付けるイベント情報 */
struct epoll_event ev;
/* 読み込みを待つ、listen用のソケットディスクリプタ */
int sd_listen;
/* epollインスタンスを作成 */
epfd = epoll_create(N_FDS);
if ( epfd < 0)
{
/* エラー処理 */
}
/* listen用ソケットに紐付けるイベント情報を設定する */
memset(&ev, 0, sizeof(struct epoll_event)); /* イベント情報の初期化 */
ev.events = EPOLLIN; /* 入力待ち(読み込み待ち) */
ev.data.fd = sd_listen;
/* epollインスタンスに、sd_listenと上記のイベント情報とを追加する */
epoll_ctl(epfd, EPOLL_CTL_ADD, sd_listen, &ev);
while ( 1 )
{
/* 以下で、epoll_waitを使用(省略) */
}
II. ディスクリプタへのアクセスを検知
listen用ソケットに受信があった場合には、接続要求をしてきたクライアントとの通信を確立し、当該クライアントと接続されたソケットディスクリプタを管理対象に追加する。
なお、select、epollのいずれも、タイムアウトは設定せずブロッキングさせる場合の実装を示す。
(よって、select()の第4引数はNULL、epoll_wait()の第4引数は-1とする。)
####1. select関数
select()を使うと、引数でポインタを渡したfd_set構造体(readable_fds)に読み込み可能ディスクリプタがセットされる。
この状態でFD_ISSET()を使用することで、特定のディスクリプタが読み込み可能になっているかを確認することができる。
新たなソケット(クライアントとのソケット)を管理対象に追加する方法は、I. 初期化及びlisten用ソケットの追加でlisten用ソケットを追加したときと同じである。
/* 初期化部分は省略 */
while ( 1 )
{
/* 結果格納用fd_setに、管理用fd_setの中身をコピーする(省略) */
/* 以下で、selectを使用 */
/* ディスクリプタの最大値を計算(省略) */
int max_fd = /*省略*/;
/* selectを実行 */
if ( select(max_fd + 1, &readable_fds, NULL, NULL) <= 0 )
{
/* 例外処理 */
}
/* listen用ソケットに通信が来ている場合 */
if ( FD_ISSET(sd_listen, &readable_fds) )
{
/* クライアントとの通信を確立(省略) */
int sd_client = accept(/*省略*/);
/* 管理対象保存用fd_setにクライアントとのソケットを追加 */
FD_SET(sd_client, &target_fds);
}
/* クライアントソケットに通信が来ている場合 */
if ( FD_ISSET(/*省略*/) )
{
/* クライアントとの処理(省略) */
}
}
####2. epollインターフェース
epoll_wait()を使うと、引数で渡したepoll_events構造体配列(events)に、読み込み可能状態のディスクリプタの情報が格納される。
この配列の要素にアクセスすることで、読み込み可能なディスクリプタを取得できる。
新たなソケット(クライアントとのソケット)を管理対象に追加する方法は、I. 初期化及びlisten用ソケットの追加でlisten用ソケットを追加したときと同じである。
/* 初期化部分は省略 */
while ( 1 )
{
/* 以下で、epoll_waitを使用 */
/* epoll_waitは、イベントの発生したディスクリプタの数を返す */
int n_events = epoll_wait(epfd, events, N_FDS, -1);
if ( n_events <= 0 )
{
/* 例外処理 */
}
/* 通信のあったディスクリプタを順にチェック */
int i;
for ( i = 0; i < n_events; i++)
{
/* ディスクリプタが不正の場合 */
if ( events[i].data.fd < 0 )
{
continue;
}
/* listen用ソケットに通信が来ている場合 */
if ( events[i].data.fd == sd_listen )
{
/* クライアントとの通信を確立(省略) */
int sd_client = accept(/*省略*/);
/* クライアントとのソケットに紐付けるイベント情報を設定する */
memset(&ev, 0, sizeof(struct epoll_event)); /* イベント情報の初期化 */
ev.events = EPOLLIN; /* 入力待ち(読み込み待ち) */
ev.data.fd = sd_client;
/* epollインスタンスに、sd_clientと上記のイベント情報とを追加する */
epoll_ctl(epfd, EPOLL_CTL_ADD, sd_client, &ev);
}
/* その他のソケット(クライアントとのソケット)に通信が来ている場合 */
else
{
/* 通信してきたクライアントとのソケット */
int sd_client = events[i].data.fd;
/* 当該クライアントとの処理(省略) */
}
}
}
III. ディスクリプタを管理対象から削除
最後に、特定のディスクリプタを(追加ではなく)削除するコードを提示する。
####1. select関数
FD_CLR()を使うだけである。引数はFD_SET()と同様。
/* 削除対象たるディスクリプタ */
int poor_socket = /*省略*/;
/* 管理対象保存用fd_setから削除 */
FD_CLR(poor_socket, &target_fds);
####2. epollインターフェース
追加のときと同様に、epoll_ctl()関数を使う。
但し今回は削除なので、EPOLL_CTL_DELオプションを指定する。3
/* 削除対象たるディスクリプタ */
int poor_socket = /*省略*/;
/* epollインスタンスからpoor_socketの情報を削除 */
epoll_ctl(epfd, EPOLL_CTL_ADD, poor_socket, NULL);
おわりに
select()、epollいずれについても、その本質、深いところには全く触れない記事となった4が、epollを修得する助けに、或いは少なくとも一種の備忘録に、なることを願う。
おまけに、ソースコード全体を貼っておく。各処理を説明する為のツギハギ状態のコードなので、あくまで参考程度に。
####1. select関数
/* 管理対象となるディスクリプタの集合 */
fd_set target_fds;
/* selectの結果の格納先 */
fd_set readable_fds;
/* 読み込みを待つ、listen用のソケットディスクリプタ */
int sd_listen;
/* 管理対象保存用fd_setを初期化 */
FD_ZERO(&target_fds);
/* 管理対象保存用fd_setにlisten用ソケットを追加 */
FD_SET(sd_listen, &target_fds);
while ( 1 )
{
/* 結果格納用fd_setに、管理用fd_setの中身をコピーする */
/* 結果格納用fd_setはselectが書き換えてしまうので、毎回コピーする必要あり */
memcpy(&readable_fds, &target_fds, sizeof(fd_set));
/* 以下で、selectを使用 */
/* ディスクリプタの最大値を計算(省略) */
int max_fd = /*省略*/;
/* selectを実行 */
if ( select(max_fd + 1, &readable_fds, NULL, NULL) <= 0 )
{
/* 例外処理 */
}
/* listen用ソケットに通信が来ている場合 */
if ( FD_ISSET(sd_listen, &readable_fds) )
{
/* クライアントとの通信を確立(省略) */
int sd_client = accept(/*省略*/);
/* 管理対象保存用fd_setにクライアントとのソケットを追加 */
FD_SET(sd_client, &target_fds);
}
/* クライアントソケットに通信が来ている場合 */
if ( FD_ISSET(/*省略*/) )
{
/* クライアントとの処理(省略) */
}
/* 削除対象たるディスクリプタ */
int poor_socket = /*省略*/;
/* 管理対象保存用fd_setから削除 */
FD_CLR(poor_socket, &target_fds);
}
####2. epollインターフェース
/* 管理対象とするファイルディスクリプタの上限数 */
#define N_FDS (5)
/* epoll_waitの結果の格納先 */
struct epoll_event events[N_FDS];
/* epollインスタンスを参照するファイルディスクリプタ */
int epfd;
/* ファイルディスクリプタと紐付けるイベント情報 */
struct epoll_event ev;
/* 読み込みを待つ、listen用のソケットディスクリプタ */
int sd_listen;
/* epollインスタンスを作成 */
epfd = epoll_create(N_FDS);
if ( epfd < 0)
{
/* エラー処理 */
}
/* listen用ソケットに紐付けるイベント情報を設定する */
memset(&ev, 0, sizeof(struct epoll_event)); /* イベント情報の初期化 */
ev.events = EPOLLIN; /* 入力待ち(読み込み待ち) */
ev.data.fd = sd_listen;
/* epollインスタンスに、sd_listenと上記のイベント情報とを追加する */
epoll_ctl(epfd, EPOLL_CTL_ADD, sd_listen, &ev);
while ( 1 )
{
/* 以下で、epoll_waitを使用 */
/* epoll_waitは、イベントの発生したディスクリプタの数を返す */
int n_events = epoll_wait(epfd, events, N_FDS, -1);
if ( n_events <= 0 )
{
/* 例外処理 */
}
/* 通信のあったディスクリプタを順にチェック */
int i;
for ( i = 0; i < n_events; i++)
{
/* ディスクリプタが不正の場合 */
if ( events[i].data.fd < 0 )
{
continue;
}
/* listen用ソケットに通信が来ている場合 */
if ( events[i].data.fd == sd_listen )
{
/* クライアントとの通信を確立(省略) */
int sd_client = accept(/*省略*/);
/* クライアントとのソケットに紐付けるイベント情報を設定する */
memset(&ev, 0, sizeof(struct epoll_event)); /* イベント情報の初期化 */
ev.events = EPOLLIN; /* 入力待ち(読み込み待ち) */
ev.data.fd = sd_client;
/* epollインスタンスに、sd_clientと上記のイベント情報とを追加する */
epoll_ctl(epfd, EPOLL_CTL_ADD, sd_client, &ev);
}
/* その他のソケット(クライアントとのソケット)に通信が来ている場合 */
else
{
/* 通信してきたクライアントとのソケット */
int sd_client = events[i].data.fd;
/* 当該クライアントとの処理(省略) */
}
}
/* 削除対象たるディスクリプタ */
int poor_socket = /*省略*/;
/* epollインスタンスからpoor_socketの情報を削除 */
epoll_ctl(epfd, EPOLL_CTL_ADD, poor_socket, NULL);
}
-
構造体epoll_eventの簡単な説明は、例えばepoll_ctlのマニュアルページ( https://linuxjm.osdn.jp/html/LDP_man-pages/man2/epoll_ctl.2.html )にある。 ↩
-
今回のコードでは削除時にepoll_ctl()の第4引数にNULLを渡しているが、Linux 2.6.9 以前ではNULLを渡してはいけないらしい。( https://linuxjm.osdn.jp/html/LDP_man-pages/man2/epoll_ctl.2.html 「バグ」参照) ↩
-
というのも執筆者自身が理解できていないので。 ↩