性能
シグナル駆動I/Oは、ディスクリプタのI/Oをハンドラで行い、主処理では別のことを行いたい時に使用されるのを想定しているので、性能値の比較結果がそのまま優劣になるわけではありませんが、ある種の指標にはなると思います。
サーバとクライアントで情報のやり取りが行われた場合の処理時間を計測します。クライアントはスレッドで行い、プロトコルはUDPなのでコネクションの確立を行う必要がないためシリアライズする必要もないので、スレッド内でソケットの作成を行います。
ソケット作成後、クライアント(=スレッド)から1バイトのデータを決められた処理回数回データを送受信し、開始から終了までの時間を計測します。コネクション数、繰り返し送受信数はプログラム引数で指定します。処理終了後にクライアント内の処理時間を計測し、画面に出力します。
SIGIO用プログラムは100行を超えていますが、下記の通りとなります。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/time.h>
#include <signal.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <assert.h>
#define TV2SEC(tv) ((double)((tv).tv_sec) + (double)((tv).tv_usec / 1000000.0))
int make_udp_sock( in_port_t * port );
void regist_signal( int signo, void ( *func )( int sig ));
void sig_io( int signo );
void * client_threaed( void * arg );
int loop;
int sock;
int main( int argc, char ** argv ) {
if( argc < 3 ) {
printf( "%s <fork> <loop>\n", argv[0] );
return( 1 );
}
int conn = atoi( argv[1] );
loop = atoi( argv[2] );
in_port_t port;
sock = make_udp_sock( &port );
regist_signal( SIGIO, sig_io );
int i;
pthread_t pid[conn];
for( i = 0; i < conn; i ++ ) {
pthread_attr_t attr;
pthread_attr_init( &attr );
pthread_attr_setstacksize( &attr, PTHREAD_STACK_MIN );
pthread_create( &pid[i], &attr, &client_threaed, &port );
}
double * result[conn];
for( i = 0; i < conn; i ++ ) {
pthread_join( pid[i], ( void ** )&result[i] );
printf( "%4d\t%f\n", i + 1, *result[i] );
}
close( sock );
return 0;
}
int make_udp_sock( in_port_t * port ) {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons( 0 );
addr.sin_addr.s_addr = INADDR_ANY;
int sock = socket( AF_INET, SOCK_DGRAM, 0 );
bind( sock, ( struct sockaddr *)&addr, sizeof( addr ));
fcntl( sock, F_SETOWN, getpid( ));
fcntl( sock, F_SETFL, O_ASYNC | O_NONBLOCK );
socklen_t len = sizeof( addr );
getsockname( sock, ( struct sockaddr *)&addr, &len );
*port = addr.sin_port;
return sock;
}
void regist_signal( int signo, void ( *func )( int sig )) {
sigset_t block;
sigemptyset( &block );
sigaddset( &block, signo );
struct sigaction sa = {
.sa_handler = func,
.sa_flags = SA_RESTART,
.sa_mask = block,
};
sigaction( signo, &sa, 0 );
}
void sig_io( int signo ) {
( void )signo;
char c;
struct sockaddr_in addr;
socklen_t len = sizeof( addr );
int rtn;
while( 1 ) {
rtn = recvfrom( sock, &c, 1, 0, ( struct sockaddr * )&addr, &len );
if( rtn < 0 && errno == EAGAIN ) {
break;
}
rtn = sendto( sock, &c, 1, 0, ( struct sockaddr * )&addr, sizeof( addr ));
assert( rtn > 0 );
}
}
void * client_threaed( void * arg ) {
in_port_t port = *( in_port_t * )arg;
struct timeval tv1, tv2;
char c = 0;
int i, rtn;
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons( ntohs( port ));
addr.sin_addr.s_addr = htonl( INADDR_LOOPBACK );
int fd = socket( AF_INET, SOCK_DGRAM, 0 );
gettimeofday( &tv1, 0 );
for( i = 0; i < loop; i ++ ) {
rtn = sendto( fd, &c, 1, 0, ( struct sockaddr *)&addr, sizeof( struct sockaddr_in ));
assert( rtn > 0 );
socklen_t len = sizeof( struct sockaddr_in );
rtn = recvfrom( fd, &c, 1, 0, ( struct sockaddr * )&addr, &len );
assert( rtn > 0 );
}
close( fd );
gettimeofday( &tv2, 0 );
double * time_rtn = malloc( sizeof( double ));
*time_rtn = TV2SEC( tv2 ) - TV2SEC( tv1 );
return ( void * )time_rtn;
}
スレッドからデータの送受信を繰り返すので、スレッドの切替などの時間が加わるために実際の処理時間とは異なる可能性が高いですが、傾向くらいはつかめると思います。
比較用のデータは、poll(2)とepoll(7)でほぼ同様の動作をするプログラムを動作させて取得しました(poll_udp.c/epoll_udp.c)。
# ./poll_udp 200 1000 # ./epoll_udp 200 1000 # ./sigio_performance 200 1000
検証数値は、コネクション数を200、処理回数を1,000回としました。その時の傾向を見ます。結果は下記の通りです。
上記性能結果より、SIGIOの高速性が分かると思います。また、性能測定をしていて気付きましたが、SIGIOはUDPを使用した通信に強いようです。
UDPプロトコルは送受信データの結果を保証していません。UDPを使用した場合パケット落ちがあっても何ら不思議なことではないのです。
事実、上記のデータ取得方法にて、コネクション数や処理回数を増やすと、あっという間にサスペンドします。これは今回作成したプログラムにパケット落ち時の考慮がなされておらず、送信したパケットをただ待っているので、1個でも届かなければプログラムがサスペンドしてしまっているからです。
しかし、SIGIOの性能測定用プログラムでは、かなり大きなパラメータでも、上記性能値とそん色ない数値で動作したことを確認しました。
UDPプロトコルを使用する際のパケット落ち対策としてシグナル駆動I/Oはかなり使えるかもしれません。
まとめ
個人的には、シグナル駆動I/Oには相応のメリットを感じています。
UDPの特徴である高速性を損なわず、(若干かもしれませんが)信頼性を高める手段であると思います。
実際、時刻同期用デーモンであるNTPDは、シグナル駆動I/Oを利用し、ハンドラ内でディスクリプタを通して情報を受信し、メインプロセスへキューなどを使用して情報を蓄え、メインプロセスではキューから情報を取り出し、加工して送信しています。
サーバの時刻との差分を極力少なく、かつ高速に送受信しなくてはいけないNPTDならではの実装といえるでしょう。
ただし、シグナル駆動I/Oは使える局面が限られる上、シグナルハンドラ特有の問題も以前残るため、使用する際は綿密な設計やアルゴリズムが求められるでしょう。また、今回は試していませんが、情報の順序性についても検証が必要でしょう。
次回は、非同期I/Oについて説明する予定です。

