Event Poll (epoll) で複数のファイルディスクリプタを監視する
目次
epoll とは (大規模言語モデルによる説明を編集したものです)
epoll
は、Linuxカーネルのシステムコールで、スケーラブルなI/Oイベント通知メカニズムを提供します¹。これは、Linuxカーネルのバージョン2.5.45で初めて導入されました¹。その主な機能は、複数のファイルディスクリプタを監視し、それらのいずれかでI/Oが可能かどうかを確認することです¹。
epoll
は、より要求の厳しいアプリケーションで、監視するファイルディスクリプタの数が多い場合に、より良いパフォーマンスを達成するために、古いPOSIXのselect(2)およびpoll(2)システムコールを置き換えることを目指しています¹。古いシステムコールである select や poll は、監視対象のファイルディスクリプタが増えると、処理時間も線形的に増加します(O(n)で動作する)。対照的に、epoll は監視するファイルディスクリプタの数が増えても、パフォーマンスが大きく劣化しません(O(1)で動作する)¹。
epoll
のAPIは以下のようになっています¹:
int epoll_create(int flags);
: epollオブジェクトを作成し、そのファイルディスクリプタを返します。flagsパラメータはepollの動作を変更するために使用します。有効な値は 0 または EPOLL_CLOEXECです。EPOLL_CLOEXEC が指定された場合、新しいファイルディスクリプタに対してclose-on-execフラグ(プロセスがファイルを生成する前に、そのファイルディスクリプタを自動的にクローズするフラグ)がセットされます。int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
: このオブジェクトによって監視されるファイルディスクリプタとそのイベントを制御(設定)します。opはADD、MODIFY、DELETEのいずれかになります。int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
: epoll_ctlで登録されたイベントのいずれかが発生するか、タイムアウトが経過するまで待ちます。発生したイベントはeventsに返され、一度に最大maxeventsまで返されます。
epoll
はエッジトリガモードとレベルトリガモードの両方を提供します¹。エッジトリガモードでは、新しいイベントがepollオブジェクトにエンキューされたときにのみ、epoll_waitの呼び出しが返されます。一方、レベルトリガモードでは、条件が保持されている限り、epoll_waitの呼び出しが返されます¹。
(1) epoll - Wikipedia. https://en.wikipedia.org/wiki/Epoll.
(2) linux - How do I use epoll? - Stack Overflow. https://stackoverflow.com/questions/31230708/how-do-i-use-epoll.
(3) Epoll Library | SpringerLink. https://link.springer.com/chapter/10.1007/978-1-4842-8731-6_12.
サンプルコード
以下は、epoll_create
、epoll_ctl
、およびepoll_wait
を組み合わせたサンプルコードです。この例では、簡単なネットワークサーバーを作成して、クライアントからの接続を待ち受けます。
1#include <stdio.h>
2#include <stdlib.h>
3#include <unistd.h>
4#include <string.h>
5#include <sys/epoll.h>
6#include <arpa/inet.h>
7
8#define MAX_EVENTS 10 // 最大同時接続数
9#define PORT 8080 // サーバーが待ち受けるポート番号
10#define BUFFER_SIZE 1024 // バッファのサイズ(受信データを格納するため)
11
12int main() {
13 int listen_fd, conn_fd, soval;
14 struct sockaddr_in serv_addr, client_addr;
15 socklen_t client_len = sizeof(client_addr);
16
17 // epoll_create: epollインスタンスの作成
18 // 第1引数:サイズを指定する。通常、epollインスタンスの内部で使用されるデータ構造の初期サイズを示す。
19 // しかし、ほとんどの実装では、この引数は実際には無視され、1を指定することで
20 // 必要な内部データ構造が適切に初期化される。
21 // 戻り値 :イベント待機のためのデータ構造(epollインスタンス)を生成し、
22 // その識別子(ファイルディスクリプタ)を返す。
23 int epfd = epoll_create(1);
24 if (epfd == -1) {
25 perror("epoll_create"); // エラーメッセージを出力
26 return 1; // エラー終了
27 }
28
29 // socket: ソケットの作成
30 // 第1引数:AF_INET(IPv4アドレスファミリー)を指定。
31 // これによりIPv4アドレスで通信するソケットが作成される。
32 // 第2引数:SOCK_STREAM(ストリーム型ソケット)を指定。
33 // これはTCP接続を使用するソケットで、信頼性のあるデータストリームを提供する。
34 // 第3引数:プロトコルを指定。0を指定することで、指定されたソケットタイプ(SOCK_STREAM)に最適な
35 // プロトコルが自動的に選択される。通常はTCPプロトコルが選ばれる。
36 // 戻り値 :新しく作成されたソケットのファイルディスクリプタ
37 listen_fd = socket(AF_INET, SOCK_STREAM, 0);
38 if (listen_fd == -1) {
39 perror("socket");
40 return 1;
41 }
42
43 // setsockopt: ソケットのオプション設定。ここではアドレスの再利用を有効にする。
44 // 第1引数: socket関数で作成したソケットのファイルディスクリプタ。
45 // 第2引数: SOL_SOCKET(ソケットレベルでのオプションを指定するための定数)を指定。
46 // これにより、ソケットの基本的な設定(例: 再利用可能なアドレス)を変更することができる。
47 // 第3引数: SO_REUSEADDR(ソケットのアドレス再利用を許可するオプション)を指定。
48 // これにより、ソケットが閉じられた後すぐに同じポート番号を再利用することが可能になる。
49 // 再起動時のソケットバインディングの問題を回避するために使用される。
50 // 第4引数: 設定するオプションの値を指すポインタ。
51 // 1は、SO_REUSEADDRオプションを有効にすることを意味する。
52 // 第5引数: sovalのサイズをバイト単位で指定。
53 // これによりsetsockopt関数が正しいサイズのデータを処理できるようになる。
54 soval = 1;
55 if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &soval, sizeof(soval)) == -1) {
56 perror("setsockopt");
57 return 1;
58 }
59
60 // サーバーアドレス構造体の設定
61 serv_addr.sin_family = AF_INET; // インターネットアドレスファミリー
62 serv_addr.sin_addr.s_addr = INADDR_ANY; // 任意のIPアドレスからの接続を受け入れる
63 serv_addr.sin_port = htons(PORT); // ポート番号の設定(ネットワークバイト順)
64
65 // bind: ソケットにローカルアドレスとポートを関連付ける。
66 // これにより、ソケットが指定されたアドレスとポートでリッスンを開始できるようになる。
67 // 第1引数:アドレスを関連付ける対象のソケットのファイルディスクリプタ。
68 // 事前に socket 関数で作成されたソケットを指定する。
69 // 第2引数:バインドするアドレスとポートの情報を格納した構造体へのポインタ。
70 // 第3引数:バインドするアドレス構造体のバイト数。
71 if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
72 perror("bind");
73 return 1;
74 }
75
76 // listen: 接続要求の待ち受けを開始(リスニング状態にする)
77 // 第1引数:クライアントからの接続要求を受け入れるためにリスニング状態にするソケット。
78 // bind 関数でアドレスとポートにバインドされたソケット。
79 // 第2引数:待機キューのサイズ。ソケットに対して待機する接続要求の最大数を指定する。
80 // キューがいっぱいになると、新しい接続要求は拒否されるか、接続の試行が失敗する。
81 if (listen(listen_fd, 5) == -1) {
82 perror("listen");
83 return 1;
84 }
85
86 // epollインスタンスにリスニングソケットを追加
87 struct epoll_event ev;
88 ev.events = EPOLLIN; // 読み取りイベントを監視
89 ev.data.fd = listen_fd; // イベントに関連付けるファイルディスクリプタ
90
91 // epoll_ctl: epollインスタンスに対して、ファイルディスクリプタに関連する操作を行う
92 // 第1引数:epoll_create関数によって作成されたepollインスタンスのファイルディスクリプタ。
93 // イベントの監視対象を管理するためのepollインスタンスを識別する。
94 // 第2引数:epoll_ctlに渡す操作タイプを指定する。
95 // EPOLL_CTL_ADDは、新しいファイルディスクリプタをepollインスタンスに追加し、
96 // そのファイルディスクリプタに対するイベントの監視を開始することを示す。
97 // その他の操作には、EPOLL_CTL_MOD(既存のファイルディスクリプタの設定変更)や
98 // EPOLL_CTL_DEL(ファイルディスクリプタの削除)がある。
99 // 第3引数:epoll_ctlで操作する対象のファイルディスクリプタ。
100 // ここでは、リスニングソケットのファイルディスクリプタが指定している。
101 // EPOLL_CTL_ADD操作を使って、このソケットをepollインスタンスに追加し、
102 // イベントの監視対象とする。
103 // 第4引数:監視対象のファイルディスクリプタとそのイベント設定を格納したstruct epoll_event型の
104 // 構造体へのポインタ。struct epoll_eventには次のフィールドがある。
105 // events: 監視するイベントの種類を指定。例えば、EPOLLINは読み取り可能な状態、
106 // EPOLLOUTは書き込み可能な状態を示す。
107 // data: イベントに関連するデータ(ここではファイルディスクリプタ)を格納する。
108 // 例えば、data.fdにリスニングソケットのファイルディスクリプタが格納して、
109 // どのソケットでイベントが発生したかを識別する。
110 if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
111 perror("epoll_ctl");
112 return 1;
113 }
114
115 printf("Server listening on port %d\n", PORT);
116
117 struct epoll_event events[MAX_EVENTS]; // 発生したイベントを格納する配列
118 while (1) {
119 // epoll_wait: イベントが発生するまで待機
120 // 第1引数:epoll_create関数で作成したepollインスタンスのファイルディスクリプタ。
121 // epoll_ctl関数で設定した監視対象のソケットイベントを待機するために使用される。
122 // 第2引数:発生したイベント情報(例えばどのファイルディスクリプタでイベントが発生したか)を格納する配列。
123 // 第3引数:events配列のサイズ。epoll_wait関数は、最大でMAX_EVENTS個のイベントを配列に格納する。
124 // これにより、一度のepoll_wait呼び出しで処理する最大のイベント数を指定します。
125 // 第4引数:タイムアウトの指定。-1を指定することで無限に待機する。
126 // つまり、イベントが発生するまで処理をブロックし続ける。
127 // 他の値を指定することで、指定したミリ秒数だけ待機することも可能。
128 // 0を指定すると、即時で戻される。
129 // 戻り値 :events配列に格納された発生したイベントの数
130 int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
131 if (nfds == -1) {
132 perror("epoll_wait");
133 return 1;
134 }
135
136 // 発生したイベントを処理
137 for (int i = 0; i < nfds; ++i) {
138 if (events[i].data.fd == listen_fd) {
139 // 新しい接続要求がある場合
140
141 // accept: 接続要求を待機しているリスニングソケットに対する接続要求を受け入れ、
142 // 新しい接続用のソケットを作成する。
143 // 第1引数:接続要求を受け入れるため、接続要求を待機しているソケットのファイルディスクリプタ。
144 // 第2引数:接続要求を受け入れたクライアントのアドレス情報を格納するための構造体へのポインタ。
145 // この構造体にクライアントのIPアドレスやポート番号が設定される。
146 // 第3引数:client_addr構造体のサイズを示す変数へのポインタ。
147 // 接続要求が受け入れられると、accept関数がこの変数に実際のアドレスサイズを設定する。
148 // 最初は、この変数にsizeof(client_addr)の値を設定しておく。
149 // 関数呼び出し後にこの変数には、実際に使われたサイズが格納される。
150 // 戻り値 :新しく作成された接続用ソケットのファイルディスクリプタを返す。
151 conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
152 if (conn_fd == -1) {
153 perror("accept");
154 continue; // 次のイベント処理に進む
155 }
156 printf("Accepted connection from %s:%d\n",
157 inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
158
159 // 新しいクライアントソケットをepollインスタンスに追加
160 ev.events = EPOLLIN; // 読み取りイベントを監視
161 ev.data.fd = conn_fd; // イベントに関連付けるファイルディスクリプタ
162 if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
163 perror("epoll_ctl");
164 close(conn_fd); // ソケットを閉じる
165 }
166 } else {
167 // 既存のクライアントソケットからのデータ受信
168
169 // recv: ソケットからデータを受信して指定されたバッファに格納する。
170 // 第1引数:データを受信する対象のソケットのファイルディスクリプタ。
171 // epoll_wait関数でイベントが発生したソケットのファイルディスクリプタであり、
172 // 接続されたクライアントのソケットファイルディスクリプタである。
173 // epollイベントのデータ部に格納されている。
174 // 第2引数:受信したデータを格納するためのバッファ。バッファサイズを超えてデータが送られることはない。
175 // 第3引数:bufferのサイズ。バッファサイズにより、一度に受信できるデータの最大量が決まる。
176 // 第4引数:フラグ引数で、受信の動作を制御する。例えば、MSG_OOB(アウトオブバンドデータ)、
177 // MSG_PEEK(データを読み取ってもキューから削除しない)などを指定できる。
178 // 0を指定すると、デフォルトの受信動作が行われる。
179 // 戻り値 :成功時は受信したバイト数。
180 // 受信データが空の場合(例えば、クライアントが接続を閉じた場合)、0が返されることがある。
181 // 失敗時は-1。
182 char buffer[1024];
183 ssize_t bytes_received = recv(events[i].data.fd, buffer, sizeof(buffer), 0);
184 if (bytes_received <= 0) {
185 // クライアントが切断した場合
186 printf("Client disconnected\n");
187 epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL); // epollインスタンスから削除
188 close(events[i].data.fd); // ソケットを閉じる
189 } else {
190 // データを受信した場合(エコーする)
191
192 // データをエコーバックする。HTTP/1.1 形式のレスポンスを作成する。
193 // (レスポンスヘッダーが未指定の場合、古いHTTP/0.9形式でレスポンスされることで、
194 // クライアント側のcurlコマンドでエラーとなることがある。)
195 // テンプレートhttp_response_templateを元に、HTTPレスポンスをフォーマットして
196 // http_response バッファに書き込む。テンプレート内のフォーマット指定子(%zu や %s)は、
197 // 対応する引数 (bytes_received や buffer) に置き換えられる。
198 buffer[bytes_received] = '\0'; // 受信データの終端
199 char http_response[BUFFER_SIZE];
200 const char *http_response_template =
201 "HTTP/1.1 200 OK\r\n"
202 "Content-Length: %zu\r\n"
203 "Content-Type: text/plain\r\n"
204 "Connection: close\r\n"
205 "\r\n"
206 "%s";
207 snprintf(http_response, sizeof(http_response), http_response_template, bytes_received, buffer);
208
209 // send: 指定されたソケットにデータを送信する。
210 // 第1引数:データを送信する対象のソケットのファイルディスクリプタ。
211 // epoll_wait関数でイベントが発生したソケットのファイルディスクリプタであり、
212 // 接続されたクライアントのソケットファイルディスクリプタである。
213 // 第2引数:送信するデータが格納されているバッファ。
214 // 第3引数:送信するデータのサイズ。
215 // 第4引数:送信の動作を変更するためのフラグ(例えば、MSG_OOB(アウトオブバンドデータ)、
216 // MSG_NOSIGNAL(シグナルを送信しない)など)を指定する。
217 // 0を指定すると、デフォルトの送信動作が行われる。
218 send(events[i].data.fd, http_response, strlen(http_response), 0);
219 //send(events[i].data.fd, buffer, bytes_received, 0);
220 }
221 }
222 }
223 }
224
225 // サーバーソケットを閉じる
226 close(listen_fd);
227 return 0;
228}
このサンプルコードは、クライアントからの接続を受け入れ、データを受信してエコーバックするシンプルなサーバーを実装しています。実際のアプリケーションでは、エラーハンドリングやプロトコルの処理を適切に実装する必要があります。
以下はクライアント側のサンプルコードです。 epoll と直接関連するものではないので、参考程度としてください。
1#include <stdio.h>
2#include <stdlib.h>
3#include <unistd.h>
4#include <string.h>
5#include <arpa/inet.h>
6
7#define PORT 8080 // サーバーがリッスンしているポート番号
8#define SERVER_IP "127.0.0.1" // サーバーのIPアドレス(ローカルホスト)
9
10int main() {
11 int sock_fd; // ソケットディスクリプタ
12 struct sockaddr_in serv_addr; // サーバーのアドレス情報
13 char buffer[1024] = "Hello, Server!"; // サーバーに送信するメッセージ
14
15 // ソケットの作成
16 sock_fd = socket(AF_INET, SOCK_STREAM, 0);
17 if (sock_fd < 0) {
18 perror("socket"); // ソケット作成エラーの表示
19 return 1;
20 }
21
22 // サーバーのアドレス情報の設定
23 serv_addr.sin_family = AF_INET; // アドレスファミリーをIPv4に設定
24 serv_addr.sin_port = htons(PORT); // ポート番号を設定(ネットワークバイトオーダーに変換)
25
26 // IPアドレスをテキストからバイナリ形式に変換
27 if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
28 perror("inet_pton"); // IPアドレス変換エラーの表示
29 close(sock_fd); // ソケットを閉じる
30 return 1;
31 }
32
33 // サーバーに接続
34 if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
35 perror("connect"); // 接続エラーの表示
36 close(sock_fd); // ソケットを閉じる
37 return 1;
38 }
39
40 // サーバーにデータを送信
41 send(sock_fd, buffer, strlen(buffer), 0);
42 printf("Message sent to server\n");
43
44 // サーバーからの応答を受信
45 ssize_t bytes_received = recv(sock_fd, buffer, sizeof(buffer) - 1, 0);
46 if (bytes_received > 0) {
47 buffer[bytes_received] = '\0'; // 受信したデータの終端にヌル文字を追加
48 printf("Received from server: %s\n", buffer); // サーバーからの応答を表示
49 } else if (bytes_received == 0) {
50 printf("Server closed the connection\n"); // サーバーが接続を閉じた場合のメッセージ
51 } else {
52 perror("recv"); // 受信エラーの表示
53 }
54
55 // ソケットを閉じる
56 close(sock_fd);
57
58 return 0;
59}
実際に動作確認してみます。 サーバ側のプログラムのファイル名を server.c としてコンパイルし、起動します。待受状態になります。
1$ gcc server.c -o server
2$ ./server
3Server listening on port 8080
次にクライアント側のプログラムのファイル名を client.c としてコンパイルします。続けてこのクライアント側プログラムを別端末から実行します。
1$ gcc client.c -o client
2$ ./client
3Message sent to server
4Received from server: HTTP/1.1 200 OK
5Content-Length: 14
6Content-Type: text/plain
7Connection: close
8
9Hello, Server!
10$
正常にレスポンスできました。サーバ側の端末を確認すると、接続要求の受け入れログと切断のログが出力されていました。
1$ ./server
2Server listening on port 8080
3Accepted connection from 127.0.0.1:35810
4Client disconnected
なお、サーバ側プログラムの動作確認だけ実施したければ、クライアント側プログラムを作成せず、代わりにcurlコマンドで確認することもできます。
1$ curl -X POST -d $'Hello, Server!\n' http://127.0.0.1:8080
2POST / HTTP/1.1
3Host: 127.0.0.1:8080
4User-Agent: curl/7.76.1
5Accept: */*
6Content-Length: 15
7Content-Type: application/x-www-form-urlencoded
8
9Hello, Server!
10$
マニュアル
ここから下は epoll(7) — Linux manual page の自動翻訳結果です。
NAME
epoll - I/Oイベント通知機能
SYNOPSIS
#include <sys/epoll.h>
DESCRIPTION
epoll API は、poll(2) と同様のタスクを実行します。つまり、複数のファイル記述子を監視して、それらのいずれかで I/O が可能かどうかを確認します。 epoll API は、エッジ トリガーまたはレベル トリガーのインターフェイスとして使用でき、監視される多数のファイル記述子に適切に拡張できます。
epoll API の中心的な概念は epoll インスタンスです。これはカーネル内のデータ構造であり、ユーザー空間の観点からは、次の 2 つのリストのコンテナーと見なすことができます。
対象リスト (epoll セットとも呼ばれる): プロセスが監視対象として登録したファイル記述子のセット。
準備完了リスト: I/O の「準備ができている」ファイル記述子のセット。 準備完了リストは、対象リスト内のファイル記述子のサブセット (より正確には、ファイル記述子の参照のセット) です。 準備完了リストは、これらのファイル記述子に対する I/O アクティビティの結果として、カーネルによって動的に設定されます。
epoll インスタンスを作成および管理するために、次のシステム コールが提供されています。
epoll_create(2) は、新しい epoll インスタンスを作成し、そのインスタンスを参照するファイル記述子を返します。 (より新しい epoll_create1(2) は、epoll_create(2) の機能を拡張します。)
特定のファイル記述子への関心は epoll_ctl(2) 経由で登録され、epoll インスタンスの関心リストに項目が追加されます。
epoll_wait(2) は I/O イベントを待機し、現在利用可能なイベントがない場合は呼び出しスレッドをブロックします。 (このシステム コールは、epoll インスタンスの準備完了リストから項目をフェッチすると考えることができます。)
レベルトリガーとエッジトリガー
epoll イベント配布インターフェイスは、エッジ トリガー (ET) とレベル トリガー (LT) の両方として動作できます。 2 つのメカニズムの違いは次のように説明できます。 次のシナリオが発生すると仮定します。
- パイプの読み取り側 (rfd) を表すファイル記述子が epoll インスタンスに登録されます。
- パイプ ライターは、パイプの書き込み側に 2 KB のデータを書き込みます。
- epoll_wait(2) の呼び出しが実行され、rfd が準備完了のファイル記述子として返されます。
- パイプリーダーは rfd から 1 kB のデータを読み取ります。
- epoll_wait(2) の呼び出しが完了します。
EPOLLET (エッジトリガー) フラグを使用して rfd ファイル記述子が epoll インターフェースに追加されている場合、ファイル入力バッファーに利用可能なデータがまだ存在しているにもかかわらず、手順 5 で行われた epoll_wait(2) の呼び出しがおそらくハングします。 一方、リモート ピアは、すでに送信したデータに基づく応答を期待している可能性があります。 その理由は、エッジ トリガー モードでは、監視対象のファイル記述子に変更が発生した場合にのみイベントが配信されるためです。 したがって、ステップ 5 で、呼び出し元は入力バッファー内にすでに存在するデータを待つことになる可能性があります。 上記の例では、2 で行われた書き込みにより rfd 上のイベントが生成され、そのイベントは 3 で消費されます。4 で行われた読み取り操作はバッファ データ全体を消費しないため、epoll_wait(2) の呼び出しは完了します。 ステップ 5 では、無期限にブロックされる可能性があります。
EPOLLET フラグを使用するアプリケーションは、複数のファイル記述子を処理するタスクがブロッキング読み取りまたは書き込み不足になることを避けるために、非ブロッキング ファイル記述子を使用する必要があります。 epoll をエッジ トリガー (EPOLLET) インターフェイスとして使用する推奨される方法は次のとおりです。
- ノンブロッキング ファイル記述子を使用する。 そして
- read(2) または write(2) が EAGAIN を返した後でのみイベントを待機します。
対照的に、レベルでトリガーされるインターフェイス (EPOLLET が指定されていない場合のデフォルト) として使用される場合、epoll は単に高速な poll(2) であり、同じセマンティクスを共有するため、epoll が使用される場所であればどこでも使用できます。
エッジ トリガーの epoll であっても、データの複数のチャンクを受信すると複数のイベントが生成される可能性があるため、呼び出し元には EPOLLONESHOT フラグを指定して、epoll_wait( によるイベントの受信後に関連するファイル記述子を無効にするように epoll に指示するオプションがあります) 2)。 EPOLLONESHOT フラグが指定されている場合、epoll_ctl(2) と EPOLL_CTL_MOD を使用してファイル記述子を再装備するのは呼び出し側の責任です。
複数のスレッド (または、子プロセスが fork(2) 経由で epoll ファイル記述子を継承している場合はプロセス) が、同じ epoll ファイル記述子と、エッジ用にマークされている対象リスト内のファイル記述子を待機している epoll_wait(2) でブロックされている場合、 トリガーされた (EPOLLET) 通知が準備完了になり、スレッド (またはプロセス) の 1 つだけが epoll_wait(2) から起動されます。 これは、一部のシナリオでの「雷鳴のような群れ」の目覚めを回避するための有用な最適化を提供します。
autosleep との相互作用
システムが /sys/power/autosleep 経由で自動スリープ(autosleep) モードにあり、デバイスをスリープから復帰させるイベントが発生した場合、デバイス ドライバーは、そのイベントがキューに入れられるまでのみデバイスを起動したままにします。 イベントが処理されるまでデバイスを起動したままにするには、epoll_ctl(2) EPOLLWAKEUP フラグを使用する必要があります。
EPOLLWAKEUP フラグが構造体 epoll_event のイベント フィールドに設定されている場合、システムは、イベントがキューに入れられた瞬間から、イベントを返す epoll_wait(2) 呼び出しを通じて、後続の epoll_wait(2) 呼び出しまで起動したままになります。 イベントによってそれ以降もシステムが起動したままになる場合は、2 回目の epoll_wait(2) 呼び出しの前に別の wake_lock を取得する必要があります。
/proc インターフェース
次のインターフェイスを使用して、epoll によって消費されるカーネル メモリの量を制限できます。
/proc/sys/fs/epoll/max_user_watches (Linux 2.6.28 以降)
これは、ユーザーがシステム上のすべての epoll インスタンスにわたって登録できるファイル記述子の合計数の制限を指定します。 制限は実際のユーザー ID ごとに行われます。 登録された各ファイル記述子のコストは、32 ビット カーネルでは約 90 バイト、64 ビット カーネルでは約 160 バイトです。 現在、max_user_watches のデフォルト値は、利用可能な低メモリの 1/25 (4%) を登録コスト (バイト単位) で割ったものです。
推奨される使用例
レベル トリガー インターフェイスとして使用される epoll の使用法は、poll(2) と同じセマンティクスを持ちますが、エッジ トリガーによる使用法については、アプリケーション イベント ループの停止を回避するためにさらに明確にする必要があります。 この例では、listener は listen(2) が呼び出されたノンブロッキング ソケットです。 関数 do_use_fd() は、read(2) または write(2) によって EAGAIN が返されるまで、新しい準備完了ファイル記述子を使用します。 イベント駆動型ステート マシン アプリケーションは、EAGAIN を受信した後、現在の状態を記録して、次に do_use_fd() を呼び出したときに、以前に停止した場所から read(2) または write(2) を継続できるようにする必要があります。
1#define MAX_EVENTS 10
2struct epoll_event ev, events[MAX_EVENTS];
3int listen_sock, conn_sock, nfds, epollfd;
4
5/* Code to set up listening socket, 'listen_sock',
6 (socket(), bind(), listen()) omitted. */
7
8epollfd = epoll_create1(0);
9if (epollfd == -1) {
10 perror("epoll_create1");
11 exit(EXIT_FAILURE);
12}
13
14ev.events = EPOLLIN;
15ev.data.fd = listen_sock;
16if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
17 perror("epoll_ctl: listen_sock");
18 exit(EXIT_FAILURE);
19}
20
21for (;;) {
22 nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
23 if (nfds == -1) {
24 perror("epoll_wait");
25 exit(EXIT_FAILURE);
26 }
27
28 for (n = 0; n < nfds; ++n) {
29 if (events[n].data.fd == listen_sock) {
30 conn_sock = accept(listen_sock,
31 (struct sockaddr *) &addr, &addrlen);
32 if (conn_sock == -1) {
33 perror("accept");
34 exit(EXIT_FAILURE);
35 }
36 setnonblocking(conn_sock);
37 ev.events = EPOLLIN | EPOLLET;
38 ev.data.fd = conn_sock;
39 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
40 &ev) == -1) {
41 perror("epoll_ctl: conn_sock");
42 exit(EXIT_FAILURE);
43 }
44 } else {
45 do_use_fd(events[n].data.fd);
46 }
47 }
48}
エッジ トリガー インターフェイスとして使用する場合、パフォーマンス上の理由から、(EPOLLIN|EPOLLOUT) を指定することで epoll インターフェイス (EPOLL_CTL_ADD) 内にファイル記述子を 1 回追加できます。 これにより、EPOLL_CTL_MOD を使用して epoll_ctl(2) を呼び出す EPOLLIN と EPOLLOUT の間の継続的な切り替えを回避できます。
質問と回答
- インタレストリストに登録されているファイルディスクリプタを区別するためのキーは何ですか?
キーは、ファイル記述子番号と開いているファイルの説明 (「オープン ファイル ハンドル」とも呼ばれ、開いているファイルのカーネルの内部表現) の組み合わせです。
- 同じファイル記述子を epoll インスタンスに 2 回登録するとどうなりますか?
おそらくEEXISTが得られるでしょう。 ただし、重複した (dup(2)、dup2(2)、fcntl(2) F_DUPFD) ファイル記述子を同じ epoll インスタンスに追加することは可能です。 これは、重複したファイル記述子が異なるイベント マスクで登録されている場合に、イベントをフィルタリングするのに役立つテクニックとなります。
- 2 つの epoll インスタンスが同じファイル記述子を待つことはできますか? その場合、イベントは両方の epoll ファイル記述子に報告されますか?
はい、イベントは両方に報告されます。 ただし、これを正しく行うには、慎重なプログラミングが必要な場合があります。
- epoll ファイル記述子自体は、poll/epoll/selectable ですか?
はい。 epoll ファイル記述子に待機中のイベントがある場合、読み取り可能であることが示されます。
- epoll ファイル記述子を独自のファイル記述子セットに入れようとするとどうなるでしょうか?
epoll_ctl(2) 呼び出しは失敗します (EINVAL)。 ただし、別の epoll ファイル記述子セット内に epoll ファイル記述子を追加できます。
- epoll ファイル記述子を UNIX ドメインソケット経由で別のプロセスに送信できますか?
はい、ただし、受信プロセスでは対象リストにファイル記述子のコピーが存在しないため、これを行うのは意味がありません。
- ファイル記述子を閉じると、そのファイル記述子はすべての epoll 対象リストから削除されますか?
はい、ただし次の点にご注意ください。 ファイル記述子は、開いているファイル記述への参照です (open(2) を参照)。 ファイル記述子が dup(2)、dup2(2)、fcntl(2) F_DUPFD、または fork(2) によって複製されるたびに、開いている同じファイル記述子を参照する新しいファイル記述子が作成されます。 開いているファイル記述は、それを参照しているすべてのファイル記述子が閉じられるまで存在し続けます。
ファイル記述子は、基礎となる開いているファイル記述を参照するすべてのファイル記述子が閉じられた後にのみ、対象リストから削除されます。 これは、対象リストの一部であるファイル記述子が閉じられた後でも、同じ基礎となるファイル記述を参照する他のファイル記述子が開いたままであれば、そのファイル記述子に関するイベントが報告される可能性があることを意味します。 これを防ぐには、ファイル記述子を複製する前に (epoll_ctl(2) EPOLL_CTL_DEL を使用して) 対象リストから明示的に削除する必要があります。 あるいは、アプリケーションはすべてのファイル記述子が閉じられていることを確認する必要があります (dup(2) または fork(2) を使用するライブラリ関数によってファイル記述子がバックグラウンドで複製されている場合、これは困難になる可能性があります)。
- epoll_wait(2) 呼び出しの間に複数のイベントが発生した場合、それらは結合されますか、それとも別々に報告されますか?
それらは結合されます。
- ファイル記述子に対する操作は、すでに収集されているがまだ報告されていないイベントに影響しますか?
既存のファイル記述子に対して 2 つの操作を実行できます。 この場合、削除は無意味です。 変更すると、利用可能な I/O が再読み取りされます。
- EPOLLET フラグ (エッジ トリガー動作) を使用する場合、EAGAIN までファイル記述子の読み取り/書き込みを継続する必要がありますか?
epoll_wait(2) からイベントを受信すると、そのファイル記述子が要求された I/O 操作の準備ができていることがわかります。 次の (非ブロッキング) 読み取り/書き込みで EAGAIN が返されるまで、準備が整っていると考える必要があります。 ファイル記述子をいつ、どのように使用するかは完全にユーザー次第です。
パケット/トークン指向のファイル (データグラム ソケット、正規モードのターミナルなど) の場合、読み取り/書き込み I/O 空間の終わりを検出する唯一の方法は、EAGAIN まで読み取り/書き込みを続けることです。
ストリーム指向のファイル (パイプ、FIFO、ストリーム ソケットなど) の場合、読み取り/書き込み I/O スペースが使い果たされた状態は、ターゲット ファイル記述子から読み取られる、またはターゲット ファイル記述子に書き込まれるデータの量をチェックすることによって検出することもできます。 たとえば、一定量のデータの読み取りを要求して read(2) を呼び出したときに、read(2) が返すバイト数が少ない場合は、ファイル記述子の読み取り I/O スペースが使い果たされたと確信できます。 write(2) を使用して書き込む場合も同様です。 (監視対象のファイル記述子が常にストリーム指向のファイルを参照していることが保証できない場合は、この後者の手法は避けてください。)
考えられる落とし穴とそれを回避する方法
- 飢餓(エッジトリガー)
大量の I/O スペースがある場合、それを空にしようとすると、他のファイルが処理されなくなり、枯渇が発生する可能性があります。 (この問題は epoll に固有のものではありません。)
解決策は、準備完了リストを維持し、関連するデータ構造内でファイル記述子を準備完了としてマークすることです。これにより、アプリケーションはどのファイルを処理する必要があるかを覚えておきながら、すべての準備完了ファイルの間でラウンドロビンを実行できるようになります。 これは、すでに準備ができているファイル記述子に対して受け取る後続のイベントの無視もサポートします。
- イベント キャッシュを使用する場合...
イベント キャッシュを使用する場合、または epoll_wait(2) から返されたすべてのファイル記述子を保存する場合は、そのクロージャーを動的にマークする方法を必ず提供してください (つまり、前のイベントの処理によって引き起こされたものです)。 epoll_wait(2) から 100 個のイベントを受信し、イベント #47 で条件によりイベント #13 が閉じられたとします。 構造を削除し、イベント #13 のファイル記述子を close(2) しても、イベント キャッシュには依然としてそのファイル記述子を待機しているイベントがあると表示され、混乱が生じる可能性があります。
これに対する 1 つの解決策は、イベント 47 の処理中に epoll_ctl(EPOLL_CTL_DEL) を呼び出してファイル記述子 13 を削除し、close(2) してから、関連するデータ構造を削除済みとしてマークし、クリーンアップ リストにリンクすることです。 バッチ処理中にファイル記述子 13 の別のイベントが見つかった場合、ファイル記述子が以前に削除されていたことがわかり、混乱は生じません。
VERSIONS
他のシステムでも同様のメカニズムを提供するものがあります。 たとえば、FreeBSD には kqueue があり、Solaris には /dev/poll があります。
STANDARDS
Linux.
HISTORY
Linux 2.5.44. glibc 2.3.2.
NOTES(注意事項)
epoll ファイル記述子を介して監視されているファイル記述子のセットは、プロセスの /proc/pid/fdinfo ディレクトリ内の epoll ファイル記述子のエントリを介して表示できます。 詳細については、proc(5) を参照してください。
kcmp(2) KCMP_EPOLL_TFD オペレーションを使用して、epoll インスタンスにファイル記述子が存在するかどうかをテストできます。
SEE ALSO(こちらも見てください)
epoll_create(2), epoll_create1(2), epoll_ctl(2), epoll_wait(2), poll(2), select(2)