レベルトリガ機能とエッジトリガ機能
epoll(7)には、レベルトリガ機能とエッジトリガ機能を使い分けられる特徴があります。
ディスクリプタを経由して、一方からデータが書かれたとします。そのディスクリプタを監視している側では、ディスクリプタがready状態になったことを検知し、ディスクリプタを介してデータを読み込みます。
その際に一部のデータのみ読み出し、ディスクリプタが管理しているデータが残存している状況でselect(2),poll(2)などの多重I/O用関数に再度管理させたとします。
すると、多重IO用関数はそのディスクリプタをすぐにready状態にします。つまりディスクリプタが管理しているデータをすべて読み込まない限りはready状態が解除されません。これをレベルトリガ機能と言います。
エッジトリガ機能はこの逆で、いったんready状態になったものを再度管理させた時、ディスクリプタにデータが残っていても管理されます。残っているデータをどうするかは、完全にアプリケーション任せです。
残ったまま再度データが書き込まれれば、ディスクリプタはready状態になり、残ったデータから読み込むことになります。全データをすべて読み書きしたい場合は、read(2)/write(2)がエラー(リターンコードが -1)かつerrnoにEAGAINが返されるまで処理を続行します。
select(2), poll(2)はレベルトリガ機能だけしか使えませんが、epoll(7)は両機能を使い分けることができます。デフォルトはレベルトリガ機能です。エッジトリガ機能として使う場合、以下の点を意識して行う必要があります。
- 対象ディスクリプタを
epoll(7)管理下にする際、EPOLLET フラグを付加する。 - 対象ディスクリプタはノンブロッキング設定にする。
- 対象ディスクリプタを通してすべてのデータを
read(2) / write(2)する際 EAGAIN が返されたら処理を終える。
なお(ややこしいことに)レベルトリガ機能を使用しつつエッジトリガ機能と同様の動作をさせることができます。その際は以下の処理が必要となります。
- 対象ディスクリプタを
epoll(7)管理下にする際、EPOLLONESHOT フラグを付加する。 - 対象ディスクリプタはノンブロッキング設定にする。
- 対象ディスクリプタを通してすべてのデータを
read(2)/write(2)する際EAGAINが返されたら処理を終える。 - 処理を終えた際、epoll_ctl(2)を実行、その際のフラグとしてEPOLL_CTL_MODフラグを使用する。
上記より、EPOLL_CTL_MODフラグを使用してepoll_ctl(2)を実行するのはユーザー(プログラマ)に任されています。
サンプルプログラムとしてepoll_level_sample.c, epoll_edge_sample.c, epoll_level_sample_oneshot.cを用意しました。ループバックアドレスを通してコネクトし、スレッドから文字列"hello"を1秒間隔で3回書き込みます。メインプロセスではepoll(7)を使ってソケットを管理していますが、ready状態になったら1バイトだけ読み込んで再度epoll(7)管理にしています。その時の挙動を見ます。
レベルトリガのサンプル
まずはレベルトリガ機能の動作を見ます。
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <sys/epoll.h>
#define MAXCONNECTS 3
int pipefd[2];
int wrap_accept( int sock );
int client_read( int sock );
void * client_threaed( void * arg );
int main( ) {
struct sockaddr_in srv_addr;
struct epoll_event ev, evs[MAXCONNECTS];
int epfd, nfds, fd, i;
int loop_flg = 1;
int sock = socket( AF_INET, SOCK_STREAM, 0 );
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons( 0 );
srv_addr.sin_addr.s_addr = htonl( INADDR_ANY );
bind( sock, ( struct sockaddr *)&srv_addr, sizeof( srv_addr ));
listen( sock, SOMAXCONN );
socklen_t socklen = sizeof( srv_addr );
getsockname( sock, ( struct sockaddr *)&srv_addr, &socklen);
pipe( pipefd );
pthread_t pid;
pthread_create( &pid, 0, &client_threaed, &srv_addr.sin_port );
epfd = epoll_create( MAXCONNECTS );
ev.events = EPOLLIN;
ev.data.fd = sock;
epoll_ctl( epfd, EPOLL_CTL_ADD, sock, &ev );
ev.events = EPOLLIN;
ev.data.fd = pipefd[0];
epoll_ctl( epfd, EPOLL_CTL_ADD, pipefd[0], &ev );
while( loop_flg ) {
nfds = epoll_wait( epfd, evs, MAXCONNECTS, -1 );
for( i = 0; i < nfds; i ++ ) {
if( evs[i].data.fd == sock ) {
fd = wrap_accept( sock );
ev.events = EPOLLIN;
ev.data.fd = fd;
epoll_ctl( epfd, EPOLL_CTL_ADD, fd, &ev );
}
else if( evs[i].data.fd == pipefd[0] ) {
loop_flg = 0;
break;
}
else {
if( client_read( evs[i].data.fd ) == 0 ) {
printf( "connection close ...\n" );
close( evs[i].data.fd );
ev.events = 0;
ev.data.fd = 0;
epoll_ctl( epfd, EPOLL_CTL_DEL, evs[i].data.fd, &ev );
}
}
}
}
return 0;
}
int wrap_accept( int sock ) {
struct sockaddr_in addr;
socklen_t len = sizeof( addr );
return accept( sock, ( struct sockaddr *)&addr, &len );
}
int client_read( int sock ) {
static int cnt;
char c = 0;
int rtn = read( sock, &c, 1 );
if( rtn == 0 ) {
return 0;
}
cnt ++;
printf( "cnt:%2d [%c]\n", cnt, c );
return rtn;
}
void * client_threaed( void * arg ) {
unsigned short int port = *( unsigned short int * )arg;
pthread_detach( pthread_self( ));
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons( ntohs( port ));
addr.sin_addr.s_addr = htonl( INADDR_LOOPBACK );
int sock = socket( AF_INET, SOCK_STREAM, 0 );
connect( sock, ( struct sockaddr *)&addr, sizeof( addr ));
int i;
for( i = 0; i < 3; i ++ ) {
sleep( 1 );
write( sock, "hello", 5 );
}
close( sock );
sleep( 1 );
close( pipefd[1] );
return 0;
}
makeして実行すると、次のとおりに出力されます。
# ./epoll_level_sample cnt: 1 [h] cnt: 2 [e] cnt: 3 [l] cnt: 4 [l] cnt: 5 [o] cnt: 6 [h] cnt: 7 [e] cnt: 8 [l] cnt: 9 [l] cnt:10 [o] cnt:11 [h] cnt:12 [e] cnt:13 [l] cnt:14 [l] cnt:15 [o] connection close ... #
データが残っている間はepoll(7)ですぐにready状態になるので、すぐに呼ばれます。
エッジトリガのサンプル
次にエッジトリガ機能の動作を見ます。ファイルサイズが130行を超えるため、ダウンロードして参照してください(epoll_edge_sample.c)。
makeして実行すると、次のように出力されます。
# ./epoll_edge_sample cnt: 1 [h] cnt: 2 [e] cnt: 3 [l] #
擬似クライアント(スレッド)が1秒間隔で3回書き込むことでready状態になるのは3回だけなので、3文字しか出力されません。4バイト目が出力する場合、クライアントでソケットを閉じることでready状態となりepoll(7)が検知して残ったデータを読み込んでいるためです。
次は下記の通りコンパイルし、実行してください。
# make ET # ./epoll_edge_sample cnt: 1 [h] cnt: 1 [e] cnt: 1 [l] cnt: 1 [l] cnt: 1 [o] cnt: 2 [h] cnt: 2 [e] cnt: 2 [l] cnt: 2 [l] cnt: 2 [o] cnt: 3 [h] cnt: 3 [e] cnt: 3 [l] cnt: 3 [l] cnt: 3 [o] connection close ... #
今回はレベルトリガと同じように出力されました。read(2)にてEAGAINが返るまで読み込んだために、すべての情報を取得できました。
エッジトリガ風レベルトリガのサンプル
さらにレベルトリガ機能の動作を見ます。こちらもファイルサイズが130行を超えるため、ダウンロードして参照してください(epoll_level_sample_oneshot.c)。
1バイト読み出した後、あえてepoll_ctl(2)を実行する前にsleep(1)を実行しています。makeして実行すると、次のとおりエッジトリガと同じように出力されます。
# ./epoll_level_sample_oneshot cnt: 1 [h] cnt: 2 [e] cnt: 3 [l] cnt: 4 [l] #
次は下記の通りコンパイルし、実行してください。
# make ET # ./epoll_level_sample_oneshot cnt: 1 [h] cnt: 1 [e] cnt: 1 [l] cnt: 1 [l] cnt: 1 [o] cnt: 2 [h] cnt: 2 [e] cnt: 2 [l] cnt: 2 [l] cnt: 2 [o] cnt: 3 [h] cnt: 3 [e] cnt: 3 [l] cnt: 3 [l] cnt: 3 [o] connection close ... #
レベルトリガと同じように出力されました。read(2)にてEAGAINが返るまで読み込んだために、すべての情報を取得できました。
まとめ
多重I/Oの種類の紹介については以上となります。以降の回では、epoll(7)で実現できる機能の種類、および多重I/Oの性能測定について言及していきたいと思います。
