はじめに
前回は、各種多重I/Oの紹介や性能値を測定し、傾向を紹介しました。今回は、ディスクリプタの入出力イベントで発生するシグナルイベントを補足することでデータを取得する入出力モデルについて説明していきたいと思います。
連載概要
- 第1回:ディスクリプタの概要
- 第2回:イベント用ディスクリプタ「eventfd」の特徴
- 第3回:タイマー用ディスクリプタ「timerfd」の特徴
- 第4回:シグナル用ディスクリプタ「signalfd」の特徴
- 第5回:多重I/O「Multiplex I/O」の種類の特徴、使い方
- 第6回:多重I/Oの性能とC10K問題
- 第7回:シグナル駆動I/Oの特徴、使い方
- 第8回:非同期I/O「Asynchronous I/O」の使い方と性能差
- 第9回:ファイルディスクリプタパッシングの特徴、使い方
サンプルプログラムは100行前後程度までは画面に記載します。全プログラムは圧縮してページ上部よりダウンロード可能にしています。make
コマンドでコンパイルできます。i386/x86_64環境で動作確認済みです。
プログラムのボリューム上、エラー処理や引数チェックなどを省いているので、あらかじめご了解ください。また使用法を誤るとシステムに重大な影響を与える可能性があります。利用する場合は責任のとれる環境において実行するよう、お願いします。
当トピックでは、実際にプログラムを通して動作確認や性能測定を行うことで、個人的な見解を述べさせていただきます。あくまで個人的な感想に基づいているので、反論や指摘などあるかと思います。指摘や質問などは大歓迎なので、その際はぜひご連絡ください。可能な限りの対応に努めます。
シグナル駆動I/Oについて
シグナル駆動I/Oとは、ディスクリプタ上で発生する入出力イベントとして発生するSIGIOを捕捉するためのシグナルハンドラを用意し、ハンドラ内でファイルディスクリプタからデータを取得するI/Oです。
シグナルハンドラでは複雑で時間のかかる処理はなるべく回避しなくてはいけません。複数のファイルディスクリプタが存在する時、シグナルハンドラ内では、どのファイルディスクリプタからデータを吸い上げるのか調べる手間がかかってしまいます。さらに、TCPソケットの場合はコネクションを行った時や切断時にもSIGIOが発生し、かつハンドラ内ではどのような理由で呼ばれたのかまでは分かりません。
そこで利用する局面としては、プロセス中に使用するファイルディスクリプタはなるべく少なく(できれば1個だけ)、かつコネクションレスなディスクリプタである必要があります。
もちろんシグナルハンドラを使用することによるデメリットも、そのまま適用されてしまいます。
また、シグナルはキューイングされないので、ハンドラ内でデータ処理中に同じシグナルが2回連続で発生すると、処理されるデータは1回だけになってしまい、2回目のデータが読みだされません。
それを回避するため、ディスクリプタは非ブロッキング設定を行う必要があります。
以上のような縛りもあるので、使用できる条件がかなり限られますが、メインプロセスとは別の場所でデータの送受信を行いたい場合には利用できるでしょう。
サンプルプログラム
サンプルプログラムは次のとおりです。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <pthread.h> #include <signal.h> #include <errno.h> #include <sys/ioctl.h> in_port_t make_udp_sock( ); void regist_signal( int signo, void ( *func )( int sig )); void sig_io( int signo ); void * client_threaed( void * arg ); int sock = 0; int main( ) { in_port_t port = make_udp_sock( ); regist_signal( SIGIO, sig_io ); pthread_t pid; pthread_create( &pid, 0, &client_threaed, &port ); pthread_join( pid, 0 ); close( sock ); return 0; } in_port_t make_udp_sock( ) { struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons( 0 ); addr.sin_addr.s_addr = INADDR_ANY; 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 ); return addr.sin_port; } void regist_signal( int signo, void ( *func )( int sig )) { struct sigaction sa = { .sa_handler = func, .sa_flags = SA_RESTART, }; sigemptyset( &sa.sa_mask ); sigaddset( &sa.sa_mask, signo ); sigaction( signo, &sa, 0 ); } void sig_io( int signo ) { ( void )signo; char getline[1024]; struct sockaddr_in udpaddr; socklen_t len = sizeof( udpaddr ); while( 1 ) { ssize_t rtn = recvfrom( sock, getline, sizeof( getline ), 0, ( struct sockaddr * )&udpaddr, &len ); if( rtn < 0 && errno == EAGAIN ) { break; } printf( "sig_io:[%zd] [%.*s]\n", rtn, ( int )rtn, getline ); } } void * client_threaed( void * arg ) { int port = *( unsigned short int * )arg; 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_DGRAM, 0 ); int i; for( i = 0; i < 3; i ++ ) { sleep( 1 ); sendto( sock, "hello", 5, 0, ( struct sockaddr *)&addr, sizeof( addr )); } close( sock ); return 0; }
実装する際のポイントとしては、下記の通りです。
- SIGIOシグナルイベントに対するハンドラを用意すること
- プロセス内で管理するディスクリプタは1個、かつグローバルであること
- ディスクリプタのオーナを設定すること
(fcntlのF_SETOWN) - ディスクリプタでシグナル駆動I/Oを許可すること
(fcntl F_SETFLでO_ASYNC) - ディスクリプタの非ブロッキング設定をすること
(fcntl F_SETFLでO_NONBLOCK)