プロセス間通信(2)/サーバ側

システム・プログラム

                                       電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~yas/classes/syspro-2003/2003-05-26
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~yas/
http://www.is.tsukuba.ac.jp/~yas/index-j.html

■今日の重要な話

■復習と訂正

4月24日

◆バッファ・オーバーフロー

文字列のバッファの大きさに注意する。 以下の例は、悪い例なので真似をしてはいけない。
----------------------------------------------------------------------
   1:	/*
   2:	        string-cpycat.c -- 文字列のコピーと結合(悪い例)
   3:	        ~yas/syspro/cc/string-cpycat.c
   4:	        Start: 2002/04/21 23:02:39
   5:	*/
   6:	
   7:	#include <stdio.h> /* stderr */
   8:	#include <stdlib.h> /* malloc() */
   9:	#include <string.h> /* strcpy(), strcat(), strlen() */
  10:	
  11:	main()
  12:	{
  13:	    char buf[100];
  14:	        strcpy(buf,"hello");
  15:	        strcat(buf,",world");
  16:	        strcat(buf,"\n");
  17:	        if( strlen(buf) >= 99 ) /* too late */
  18:	        {
  19:	            fprintf(stderr,"buffer overflow\n");
  20:	            exit( 1 );
  21:	        }
  22:	        printf("%s",buf );
  23:	        free( buf );
  24:	}
----------------------------------------------------------------------

文字列と文字の配列

注意:この例は、悪い例なので真似をしてはいけない。

strcpy() で初期化して、strcat() で後ろに付け加えていく。

100 バイトのバッファでは、buf[0] からbuf[98]まで文字のデータが入れられる。 最後のbuf[99] には、文字ではなく0 を入れる。buf[100] は使ってはいけない。 (次の別のデータが入っている。)

strlen(buf) が 100 を越えた時には、使ってはいけない場所を使ってしまった ことを意味する。これを、「バッファ・オーバーフロー」という。 そもそも事後にチェックするのは、本当は遅い。そもそも、バッファ・オーバー フローが起きないように気を付けながらプログラムを書くべきである。

◆snprintf()

strcpy(), strcat() の代りに snprintf() が便利な場合がある。
----------------------------------------------------------------------
   1:	/*
   2:	        string-snprintf.c -- snprintf を使った文字列のコピーと結合
   3:	        ~yas/syspro/cc/string-snprintf.c
   4:	        Start: 2002/04/21 23:02:39
   5:	*/
   6:	
   7:	#include <stdio.h> /* stderr, snprintf() */
   8:	#include <stdlib.h> /* malloc() */
   9:	
  10:	main()
  11:	{
  12:	    char buf[100];
  13:	        if( snprintf(buf,100,"hello%s\n",",world") >=sizeof(buf) )
  14:	        {
  15:	            fprintf(stderr,"buffer overflow\n");
  16:	            exit( 1 );
  17:	        }
  18:	        printf("%s",buf );
  19:	}
  20:	
----------------------------------------------------------------------
snprintf() は、第1引数に、バッファの番地、第2引数にバッファの長さ(最 後の0を含む)をとる。第3引数以降は、printf() と同じである。snprintf() では、けっしてバッファ・オーバーフローは起きない。起きそうになると、負 の数を返す。(成功すると、バイト数(最後の0は含まない)を返す。)

snprintf() は、%s 以外に %d や %c も使える。

注意:Linux (Glibc を使っている)では、snprintf() の仕様が変更された。 古い仕様(glibc 2.0.6以前)では、snprinf() は、エラーが起きると -1 を返 す。新しい仕様(glibc 2.1以降)では、snprintf() は、必要なバイト数、すな わち、バッファが無限大であるときに書き込まれる文字列の長さ(最後の0を除 く)を返す。これは、strlen() が返すものと同じである。従って、新しい snprintf() では、結果とバッファの大きさを比較して正確に書き込まれたか を検査する。(snprintf() は、決してバッファ・オーバーフローを起こすこと はないが、バッファが足りない時には意図していない結果が保存されているこ とになる。)

■TCP/IPの基本的な考え方

通信路の開設

■echo-server-fork

TCP/IP のポート番号 7 (echo) では、受け取ったデータをそのまま返すサー ビスを提供している。以下は、これと同じような機能を提供するサーバである。 複数の接続先(クライアント)の要求を同時に処理するために、クライアント ごとに fork() システム・コールで専用の子プロセスを作っている。

----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        echo-server-fork.c -- 受け取った文字列をそのまま返すサーバ(fork版)
   4:	        ~yas/syspro/ipc/echo-server-fork.c
   5:	        Start: 1997/06/09 19:46:40
   6:	*/
   7:	#define _USE_BSD        /* wait4() */
   8:	#include <stdio.h>
   9:	#include <sys/types.h>  /* socket(), wait4() */
  10:	#include <sys/socket.h> /* socket() */
  11:	#include <netinet/in.h> /* struct sockaddr_in */
  12:	#include <sys/resource.h> /* wait4() */
  13:	#include <sys/wait.h>   /* wait4() */
  14:	#include <netdb.h>      /* getnameinfo() */
  15:	
  16:	extern  void echo_server( int portno );
  17:	extern  void echo_reply( int com );
  18:	extern  void print_my_host_port( int portno );
  19:	extern  void tcp_peeraddr_print( int com );
  20:	extern  void sockaddr_print( struct sockaddr *addrp, int addr_len );
  21:	extern  tcp_acc_port( int portno );
  22:	extern  void delete_zombie(void);
  23:	extern  ssize_t writen(int fd, const void *vptr, size_t n);
  24:	extern  ssize_t readline(int fd, void *vptr, size_t maxlen);
  25:	
  26:	main( int argc, char *argv[] )
  27:	{
  28:	    int portno ;
  29:	        if( argc >= 3 )
  30:	        {
  31:	            fprintf( stdout,"Usage: %s [portno] \n",argv[0] );
  32:	            exit( -1 );
  33:	        }
  34:	        if( argc == 2 )
  35:	            portno = strtol( argv[1],0,10 );
  36:	        else
  37:	            portno = getuid();
  38:	        echo_server( portno );
  39:	}
----------------------------------------------------------------------

引数として、ポート番号を取る。ポート番号が与えられなければ、そのプロセ スの UID (User ID) から生成する。UID は、個人個人を識別するための番号 (16ビット程度、システムによっては 32 ビット)である。この課題では、(1台 のコンピュータでは)個人ごとに別々の UID を使う必要がある。上のように UID から生成する方法は、一般的ではない。(筑波大学情報学類のシステムで はうまく働く。)


----------------------------------------------------------------------
  40:	
  41:	void
  42:	echo_server( int portno )
  43:	{
  44:	    int acc,com ;
  45:	    pid_t child_pid ;
  46:	        acc = tcp_acc_port( portno );
  47:	        if( acc<0 )
  48:	            exit( -1 );
  49:	        print_my_host_port( portno );
  50:	        while( 1 )
  51:	        {
  52:	            delete_zombie();
  53:	            if( (com = accept( acc,0,0 )) < 0 )
  54:	            {
  55:	                perror("accept");
  56:	                exit( -1 );
  57:	            }
  58:	            tcp_peeraddr_print( com );
  59:	            if( (child_pid=fork()) > 0 ) /* parent */
  60:	            {
  61:	                close( com );
  62:	            }
  63:	            else if( child_pid == 0 ) /* parent */
  64:	            {
  65:	                close( acc );
  66:	                echo_reply( com );
  67:	                printf("[%d] connection (fd==%d) closed.\n",getpid(),com );
  68:	                close( com );
  69:	                exit( 0 );
  70:	            }
  71:	            else
  72:	            {
  73:	                perror("fork");
  74:	                exit( -1 );
  75:	            }  
  76:	        }
  77:	}
----------------------------------------------------------------------

tcp_acc_port() は、引数で与えられたポート番号を使って接続要求受付用ポー トを作成し、そのファイル記述子(ソケット)を返す。print_my_host_port() は、 telnet で接続する時のヒントを表示する。

サーバのプログラムの特徴は、内部に無限ループを持っていることである。 サーバは、普通の状態では、終了しない。

delete_zombie() は、ゾンビ・プロセス(後述)を消去するものである。 (改善の余地がある。)

accept() は、接続要求を待つシステムコールである。クライアントから接続 が来るまで、システムコールを実行したまま止まっているように見える。 接続要求が届くと、システムコールからリターンする。

tcp_peeraddr_print() は、通信相手を表示する関数である。 (echo サービスでは、説明用である。)

echo サービスとしての処理は、fork() システムコールで分身を作り、子プロ セス側で行う。親プロセスは、すぐにaccept() に戻る。

getpid() は、自分自身の PID (Process ID (identifier)) を返すシステムコー ルである。PID (pid_t) は、16 ビット(システムによっては32ビット)の整数 である。実行結果で printf() の表示に、PID が表示されているが、同じではないことに 注意しなさい。


----------------------------------------------------------------------
  78:	
  79:	#define BUFFERSIZE      1024
  80:	
  81:	void
  82:	echo_reply( int com )
  83:	{
  84:	    char line[BUFFERSIZE] ;
  85:	    int rcount ;
  86:	    int wcount ;
  87:	
  88:	        while( (rcount=readline(com,line,BUFFERSIZE)) > 0 )
  89:	        {
  90:	            printf("[%d] read(%d,,) %d bytes, %s",getpid(),com,rcount,line );
  91:	            fflush( stdout );
  92:	            if( (wcount=writen(com,line,rcount))!= rcount )
  93:	            {
  94:	                 perror("write");
  95:	                 exit( 1 );
  96:	            }
  97:	        }
  98:	}
  99:	
----------------------------------------------------------------------

この echo_reply() は、特定のクライアント専用のecho サービスを提供する。 クライアントからの要求が続く限り、動作する。クライアントからの要求は、 readline() で読込んでいる。

fflush() は、printf() の内部のバッファ(stdoutのバッファ)に溜っているデー タを書き出すものである。

クライアントには、write() ではなく writen() で結果を送り返している。

readline() と writen() については、 先週のページ に説明がある。


----------------------------------------------------------------------
 100:	void
 101:	print_my_host_port( int portno )
 102:	{
 103:	    char hostname[100] ;
 104:	        gethostname( hostname,sizeof(hostname) );
 105:	        hostname[99] = 0 ;
 106:	        printf("run telnet %s %d \n",hostname, portno );
 107:	}
 108:	
----------------------------------------------------------------------

print_my_host_port() は、telnet で接続する時のヒントを表示する。 gethostname() システムコールで自分自身のホスト名を取り出している。(正 式には、gethostname() の結果とインターネット的なホスト名(IPアドレスと 対応している))が一致して異ないことがある。


----------------------------------------------------------------------
 109:	void
 110:	tcp_peeraddr_print( int com )
 111:	{
 112:	    struct sockaddr_storage addr ;
 113:	    int addr_len ;
 114:	        addr_len = sizeof( addr );
 115:	        if( getpeername( com, (struct sockaddr *)&addr, &addr_len  )<0 )
 116:	        {
 117:	            perror("tcp_peeraddr_print");
 118:	            return;
 119:	        }
 120:	        printf("[%d] connection (fd==%d) from ",getpid(),com );
 121:	        sockaddr_print( (struct sockaddr *)&addr, addr_len );
 122:	        printf("\n");
 123:	}
 124:	
 125:	void
 126:	sockaddr_print( struct sockaddr *addrp, int addr_len )
 127:	{
 128:	    char host[BUFFERSIZE] ;
 129:	    char port[BUFFERSIZE] ;
 130:	        if( getnameinfo(addrp, addr_len, host, sizeof(host),
 131:	                        port, sizeof(port), NI_NUMERICHOST|NI_NUMERICSERV)<0 )
 132:	            return;
 133:	        printf("%s:%s", host, port );
 134:	}
----------------------------------------------------------------------

tcp_peeraddr_print() は、通信相手(peer)のアドレス(TCP/IPの場合、IPアド レスとポート番号)を表示する。通信相手のアドレスは、getpeername() システムコールで得られる。

IP アドレスは、IPv4 では、32 ビット(int)であり、 ポート番号は、16 ビット(sort)である。 ここでは、getnameinfo() ライブラリ関数を用いてホスト名とポート番号の 文字列表現に変換している。この時、NUMERIC と指定しいるので、 IPv4 では、ドット「.」で区切られた10進数4つになる。

getnameinfo() が使われている部分では、以前は、gethostbyaddr() が使われ ていた。


----------------------------------------------------------------------
 135:	
 136:	tcp_acc_port( int portno )
 137:	{
 138:	    struct sockaddr_in addr ;
 139:	    int addr_len ;
 140:	    int s ;
 141:	
 142:	        if( (s = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
 143:	        {
 144:	            perror("socket");
 145:	            return( -1 );
 146:	        }
 147:	
 148:	        addr.sin_family = AF_INET ;
 149:	        addr.sin_addr.s_addr = INADDR_ANY ;
 150:	        addr.sin_port = htons( portno );
 151:	
 152:	        if( bind(s,(struct sockaddr *)&addr,sizeof(addr)) < 0 )
 153:	        {
 154:	            perror("bind");
 155:	            fprintf(stderr,"port number %d is already used. wait a moment or kill another program.\n", portno );
 156:	            return( -1 );
 157:	        }
 158:	        if( listen( s, 5 ) < 0 )
 159:	        {
 160:	            perror("listen");
 161:	            close( s );
 162:	            return( -1 );
 163:	        }
 164:	        return( s );
 165:	}
 166:	
----------------------------------------------------------------------

tcp_acc_port() は、 通信路の開設 の仕事のうち、サーバ側で接続要求受付用ポートを作る関数である。 まず、クライアント側と同様に、ソケットを、socket() システムコールで作成している。 PF_INET と SOCK_STREAMの組み合わせ なので、TCP を使うことを意味する。

socket() の引数で、PF_INET の変りに、AF_INET と書いてもよい。ここでは、 Protocol を選んでいるので、PF_ が正しいが、実際には、PF_INET と AF_INET は同じであり、また、多くのテキストで混在されて使われいる。

ソケットが作成できたら、bind() システムコールで、サーバ側で利用するア ドレス(IPアドレスとポート番号)を設定する。IP アドレスは、IPv4 では普通、 INADDR_ANY を指定する。複数の IP アドレスがある時には、どれに要求が来 ても受け付ける。特定の IP アドレスを指定すると、そのアドレスに来た要求 だけを受け付けるようになる。

ポート番号は、引数で与えられたものを、htons() でネットワーク・バイトオー ダに変換して与える。

次に、listen() システムコールにより、要求受け付けを開始する。第2引数 は、最大何個のクライアントを接続要求待ちで待たせるか(待ち行列の長さ) を指定する。echo-server-fork() では、子プロセスを fork() していてサー バはすぐに accept() しているので、子プロセスの待ち行列がでることはない ように思えるかもしれない。実際には、システムが重くなり、fork() が滞る と待ち行列ができる。重たいサーバを設計する時には、キューの長さを調節す る。Apache (WWW サーバ) などでは、500 程度になっていることがある。

bind() する addr を、getaddrinfo() で調べる流儀(IPv6風)もある。この場 合、getaddrinfo()の第一引数には、NULL を入れ、hints.ai_flags には AI_PASSIVE を設定する。


----------------------------------------------------------------------
 167:	void
 168:	delete_zombie()
 169:	{
 170:	    pid_t pid ;
 171:	    while( (pid=wait4(-1,0,WNOHANG,0)) >0 )
 172:	    {
 173:	        printf("[%d] zombi %d deleted.\n",getpid(),pid );
 174:	        continue;
 175:	    }
 176:	}
<以下省略>
----------------------------------------------------------------------

wait4() システムコールを使って、終了した子プロセスをwait してあげてい る。ただし、WNOHANG オプションを付けてあるので、終了した子プロセスがい なければ、待たずに返ってくる。

実行例。

サーバ側。 サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。

注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。


----------------------------------------------------------------------
% ./echo-server-fork 1231 [←]
run telnet adonis9.coins.tsukuba.ac.jp 1231 
[30566] connection (fd==4) from 130.158.86.71:34091
[30567] read(4,,) 5 bytes, 012
[30566] connection (fd==4) from 130.158.86.29:33984
[30569] read(4,,) 5 bytes, abc
[30569] read(4,,) 5 bytes, def
[30569] connection (fd==4) closed.
[30567] read(4,,) 5 bytes, 345
[30567] connection (fd==4) closed.
^C
% []
----------------------------------------------------------------------
クライアント側(その1)。
----------------------------------------------------------------------
% telnet adonis9.coins.tsukuba.ac.jp 1231 [←]
Trying 130.158.86.29...
Connected to adonis9.coins.tsukuba.ac.jp.
Escape character is '^]'.
012[←]
012
345[←]
345
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------
クライアント側(その2)。
----------------------------------------------------------------------
% telnet adonis9.coins.tsukuba.ac.jp 1231 [←]
Trying 130.158.86.29...
Connected to adonis9.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc[←]
abc
def[←]
def
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------

図1サーバが accept()している時にクライアントが connect()した所

図1サーバが accept()している時にクライアントが connect()した所

図2 サーバで accept()からreturnした所。fork() する直前。

図2 サーバで accept()からreturnした所。fork() する直前。

図3サーバで親がcomをclose()、子が acc を close() した所。

図3サーバで親がcomをclose()、子が acc を close() した所。

図4 複数のクライアントが接続した時

図4 複数のクライアントが接続した時

■ゾンビ・プロセス

exit(2) システム・コールで終了したり、ソフトウェア割り込み(kill(2))で 強制終了させれたプロセスは、親プロセスが wait() するまで、形だけのこっ ている。このようなプロセスは、ゾンビ(Zombie)と呼ばれる。

ps -l コマンドで見ると、ゾンビの状態(S) は、Z と表示される。

% ps aux | egrep echo [←]
yas      30500  0.0  0.1  1364  376 pts/4    S    23:32   0:00 ./echo-server-for
yas      30501  0.0  0.0     0    0 pts/4    Z    23:33   0:00 [echo-server-for 
yas      30503  0.0  0.0     0    0 pts/4    Z    23:33   0:00 [echo-server-for 
yas      30511  0.0  0.2  2536  704 pts/2    S    23:37   0:00 egrep echo
% []
echo-server-fork.c では、 delete_zombie() でゾンビを消そうとはしている。しかし、accept() で止まっ ている状態なので、どこかのクライアントから接続要求が来ない限りは、 delete_zombie() が呼ばれないので、ゾンビとして残っている。

この状態で接続要求が来ると、ゾンビが回収される。


----------------------------------------------------------------------
% ./echo-server-fork 1231 [←]
run telnet adonis9.coins.tsukuba.ac.jp 1231 
[30500] connection (fd==4) from 130.158.86.21:40073
[30501] read(4,,) 5 bytes, 012
[30500] connection (fd==4) from 130.158.86.29:36041
[30503] read(4,,) 5 bytes, abc
[30503] read(4,,) 5 bytes, def
[30503] connection (fd==4) closed.
[30501] read(4,,) 5 bytes, 345
[30501] connection (fd==4) closed.
[30500] connection (fd==4) from 130.158.86.21:40074
[30500] zombi 30503 deleted.
[30500] zombi 30501 deleted.
----------------------------------------------------------------------

■echo-server-select

echo-server-fork.c では、クライアントから接続要求を受け付けるたびに、新しいプロセスを作っ ていた。以下の echo-server-select.cでは、1つのプロセスで実行し ている。

----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        echo-server-select.c -- 受け取った文字列をそのまま返すサーバ(select版)
   4:	        ~yas/syspro/ipc/echo-server-select.c
   5:	        Start: 1997/06/09 19:53:33
   6:	*/
   7:	#include <stdio.h>
   8:	#include <sys/types.h>  /* socket(), time(), select() */
   9:	#include <sys/socket.h> /* socket() */
  10:	#include <netinet/in.h> /* struct sockaddr_in */
  11:	#include <sys/time.h>   /* select() */
  12:	#include <unistd.h>     /* select() */
  13:	#include <netdb.h>      /* getnameinfo() */
  14:	
  15:	extern  void echo_server( int portno );
  16:	extern  void echo_reply_select( int com );
  17:	extern  int  echo_reply_once( int com );
  18:	extern  void print_my_host_port( int portno );
  19:	extern  void tcp_peeraddr_print( int com );
  20:	extern  void sockaddr_print( struct sockaddr *addrp, int addr_len );
  21:	extern  tcp_acc_port( int portno );
  22:	extern  ssize_t writen(int fd, const void *vptr, size_t n);
  23:	extern  ssize_t readline(int fd, void *vptr, size_t maxlen);
  24:	
  25:	main( int argc, char *argv[] )
  26:	{
  27:	    int portno ;
  28:	        if( argc >= 3 )
  29:	        {
  30:	            fprintf( stdout,"Usage: %s [portno] \n",argv[0] );
  31:	            exit( -1 );
  32:	        }
  33:	        if( argc == 2 )
  34:	            portno = strtol( argv[1],0,10 );
  35:	        else
  36:	            portno = getuid();
  37:	        echo_reply_select( portno );
  38:	}
  39:	
----------------------------------------------------------------------

main() 関数の部分は、echo-server-fork とほとんど同じである。

----------------------------------------------------------------------
  40:	void
  41:	echo_reply_select( int portno )
  42:	{
  43:	    int acc,com ;
  44:	    fd_set readfds,readfds_save ;
  45:	    int i,n ;
  46:	
  47:	        acc = tcp_acc_port( portno );
  48:	        if( acc<0 )
  49:	            exit( -1 );
  50:	        print_my_host_port( portno );
  51:	
  52:	        FD_ZERO( &readfds_save );
  53:	        FD_SET( acc,&readfds_save );
  54:	        while( 1 )
  55:	        {
  56:	            readfds = readfds_save ;
  57:	            n = select( FD_SETSIZE,&readfds,0,0,0 );
  58:	            if( n <= 0 )
  59:	            {
  60:	                perror("select");
  61:	                exit( 1 );
  62:	            }
  63:	            if( FD_ISSET(acc,&readfds) )
  64:	            {
  65:	                FD_CLR( acc,&readfds );
  66:	                if( (com = accept( acc,0,0 )) < 0 )
  67:	                {
  68:	                    perror("accept");
  69:	                    exit( -1 );
  70:	                }
  71:	                FD_SET( com, &readfds_save );
  72:	                tcp_peeraddr_print( com );
  73:	            }
  74:	            for( i=0 ; i<FD_SETSIZE ; i++ )
  75:	            {
  76:	                if( FD_ISSET(i,&readfds) )
  77:	                {
  78:	                    if( echo_reply_once( i )<=0 )
  79:	                    {
  80:	                        printf("[%d] connection (fd==%d) closed.\n",getpid(),i );
  81:	                        close( i );
  82:	                        FD_CLR( i,&readfds_save );
  83:	                    }
  84:	                }
  85:	            }
  86:	        }
  87:	}
  88:	
----------------------------------------------------------------------

tcp_acc_port() で、接続受付け用ポートに対応したソケットを 作り、print_my_host_port() で表示している。fork版と同じ。

ループに入る前に、fd_set 形の変数 readfds_save を初期化している。 fd_set は、ファイル記述子(file descriptor) の set (集合)を意味する。内 部的には、ビットの並びで実現されていることが多い。次のような操作がある。

FD_ZERO(&fds)
初期化する(集合を空にする)
FD_SET(fd,&fds)
fd を集合に加える(ビットを1にする)
FD_CLR(fd,&fds)
fd を集合から取り除く(ビットを0にする)
FD_ISSET(fd,&fds)
その fd が集合に含まれているかを調べる。
初期値は、tcp_acc_port() で作成した接続受付け用ポートに対応したソケッ トを入れる。

無限ループを含むのは、fork() 版と同じ。

readfds = readfds_save は、select() の実行で壊されるので、保存してある。 「壊される」とは、プロセスからシステムへの方向に値を送るだけでなく、同 じ場所に、システムからプロセスの方向へ結果が返される(同じ場所に上書き される)ことを意味する。

select() は、引数で指定された集合が、入力可能かどうかを調べている。入 力可能でなければ、入力可能になるまで待つ。

第3引数以降を使えば、出力可能かを調べたり、無限に待つのではなくて、あ る指定された時間だけ待つこともできる。

もし、要求受付け用ポートに対応したソケット acc が入力可能ならば、 accept() すると止まらずに処理が進むことを意味する。処理とは、クライア ントとの間の通信用ポートに対応したソケットが作ることである。fork() 版 とは違い、この accept() の所で待つことはない。

accept() が成功したら、結果として返されたファイル記述子を、 readfds_save に加える。次のループから select() の監視の対象になる。

for 文で、残りのファイル記述子について調べる。一度に複数のファイル記 述子がセットされている可能性がある。

もしセットされているファイル記述子を見つけたら、echo_reply_once() を呼 び出す。普通は、0 より大きい数が返ってくる。そして次のセットされている ファイル記述子について echo_reply_once() の処理を続ける。

特殊な場合として、echo_reply_once() は、クライアントが接続を切った場合 には、0 以下の値を返す。その場合は、close() でファイル記述子を解放し、 readfds_save からも FD_CLR() で取り除く。

select版では、fork版とは異なり、ゾンビ・プロセスは発生しない。


----------------------------------------------------------------------
  89:	#define BUFFERSIZE      1024
  90:	
  91:	int
  92:	echo_reply_once( int com )
  93:	{
  94:	    char line[BUFFERSIZE] ;
  95:	    int rcount ;
  96:	    int wcount ;
  97:	
  98:	        if( (rcount=readline(com,line,BUFFERSIZE)) > 0 )
  99:	        {
 100:	            printf("[%d] read(%d,,) %d bytes, %s",getpid(),com,rcount,line );
 101:	            fflush( stdout );
 102:	            if( (wcount=writen(com,line,rcount))!= rcount )
 103:	            {
 104:	                 perror("write");
 105:	                 exit( 1 );
 106:	            }
 107:	        }
 108:	        return( rcount );
 109:	}
<以下省略>
----------------------------------------------------------------------

echo_reply_once() は、fork版のecho_reply() とは異なり、readlin() を1 回しか呼び出さない。 そして、それに対応する writen() を1回だけ実行する。 (if文とwhile文の違いに注意する。)

readline() の結果(多くの場合は、読込んだバイト数)をそのまま返す。0 以 下の数を返した時にもそのまま返す。

実行例。

サーバ側。サーバは、終了しないので、最後に、^C を押して、割り 込みを掛けて終了させる。

注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。


----------------------------------------------------------------------
% ./echo-server-select 1231 [←]
run telnet adonis9.coins.tsukuba.ac.jp 1231 
[30747] connection (fd==4) from 130.158.86.71:34092
[30747] read(4,,) 5 bytes, 012
[30747] connection (fd==5) from 130.158.86.29:33988
[30747] read(5,,) 5 bytes, abc
[30747] read(5,,) 5 bytes, def
[30747] connection (fd==5) closed.
[30747] read(4,,) 5 bytes, 345
[30747] connection (fd==4) closed.
^C
% []
----------------------------------------------------------------------
クライアント側(その1)。
----------------------------------------------------------------------
% telnet adonis9.coins.tsukuba.ac.jp 1231 [←]
Trying 130.158.86.29...
Connected to adonis9.coins.tsukuba.ac.jp.
Escape character is '^]'.
012[←]
012
345[←]
345
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------
クライアント側(その2)。
----------------------------------------------------------------------
% telnet adonis9.coins.tsukuba.ac.jp 1231 [←]
Trying 130.158.86.29...
Connected to adonis9.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc[←]
abc
def[←]
def
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------

図4 複数のクライアントが接続した時(select())

図4 複数のクライアントが接続した時(select())

◆DoS攻撃

このプログラムでは、readline() を使っているので、クライアントから \nが 送られてこないと、サーバ全体の処理が停止してしまう。select() の結果、 本来ならブロックしないはずであるが、\n が来たらブロックしてしまう。

readline() の代りにread() を使えば、そのような問題は生じない。しかし、 行単位で何か処理を行うようなサーバを作るには、「クライアントごとに」、 少なくとも1行をためるためのバッファを設ける必要がある。

クライアントが意図的に '\n' を送らないことがある。これも、DoS 攻撃 (Denial of Service攻撃、サービス運用妨害攻撃)の一種である。 その他に、writen() に対して、クライアントが read() しないという攻撃も ある。

echo-server-fork.c では、クライアントご とにプロセス(スレッド)が存在するので、安全に readline() を使うことが できる。ただし、プロセスをコピーする処理が重たいので、過剰な connect() による接続要求による DoS 攻撃には弱くなる。(fork() の処理ばかりして、 通常の業務ができなくなる。)

サーバ・プログラムを作成する時には、DoS 攻撃に強いものを作成するように 気をつける。

■練習問題

★練習問題 45 ゾンビの効率的な消去

echo-server-fork.c では、accept() している途中で、子プロセスが終了する とゾンビになってしまう。子プロセスが終了したら直ちにゾンビを回収するよ うにしなさい。

ヒント:子プロセスの終了を、ソフトウェア割り込み(signal, 後述) (SIGCHLD)で知る方法もある。複数の子プロセスが終了しても、割り込みは1 回しか起こらないことがあることに注意しなさい。

ソフトウェア割込みを使うと、accept() システムコールがエラーで戻って来 るシステムもある。その場合、エラー番号が EINTR なら単純に終了しないで、 再び accept() に向うべきである。

★練習問題 46 SO_REUSEADDR

サーバを実行すると、次のようなエラーが出ることがある。

----------------------------------------------------------------------
% ./echo-server-fork 1231 [←]
bind: Address already in use
port number 1231 is already used. wait a moment or kill another program.
% []
----------------------------------------------------------------------
このエラーは、実際に同じポート番号を別のプロセスが使っている時にも出る が、既に以前に使っていたプロセスが終了している時にも出ることがある。 後者の場合、次のオプションを付けると、この状態が緩和されることがある。
setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, 0, 0);
この効果を確かめなさい。

また、同じポート番号の再利用を制限している理由を考えなさい。

★練習問題 47 DoS攻撃対策

echo-server-selec.cに含まれている \n を送らないことによるDoS攻撃に 強くなるように、改良しなさい。

★練習問題 48 poll() の利用

System V 系の Unix には select() システムコールがなく poll() システムコールだけが存在することがある。 echo-server-selec.cを poll() システムコールを使って書き換えなさい。

★練習問題 49 TCP/IPのストリームとpipe()(1)

pipe() の代りに、TCP/IP のストリームを使って、異なるホスト上のプロセス を接続して、フィルタの処理を行わせてみなさい。

ヒント:例題 pipe-rw-nodup.cを tcp_connect() やtcp_acc_port() を使って書き直すとよい。

プログラムを1つだけ作るのではなく、tcp_connect() をするものと tcp_acc_port() をするものの2つを作るとよい。tcp_acc_port() をする側で は、fork() をして、複数のクライアントに対応するのではなく、通 信要求受付用の socket() を close() して、1つの通信先に専念するものでよい。

この課題では、fork() は、しなくてもよい。パイプは、fork() でできた親子 関係のあるプロセス間で通信を行うものであるが、TCP/IPでは、親子関係がな いプロセス間で通信ができる。

TCP/IP なら本来双方向で使える。しかし、この練習問題だと単方向だけを使 う。こうすると、パイプと同じような使い方ができる。

★練習問題 50 TCP/IPのストリームとpipe()(2)

練習問題49 で、3つ以上のプロセスを接続できるようにしなさい。

ヒント:練習問題28 3個のプロセスをパイプで結ぶに相当する。

プログラムを1つだけ作るのではなく、先頭用、中間用、末尾用の3つを作る とよい。中間用のものは、うまく作ると、何個でもはさみ込めるようになる。

★練習問題 51 TCP/IPのストリームとpipe()(3)

練習問題49 または 練習問題50 で、外部のプログラムを実行できるようにしなさい。

ヒント:dup(), dup2(), close() などで、標準入出力を切り替えて、 execve() などで、プログラムを実行する。

たとえば、シェルに次のように打ち込むことを考える。

% command1 | command2 | command3
この時、シェルは、fork() しながら2つのパイプで3つのプロセスを結び、 execve() など command1, command2, command3 を実行する。この問題では、 パイプではなくTCP/IP 結ぶ。そして、close(), dup(), close() して、 execve() でcommand1, command2, command3 を実行する。

この課題では、tcp_accept() 側で fork() を行う必要はない。1回のコマン ドを実行したら終了するようにする。

★練習問題 52 IPアドレスをホスト名に逆変換する

上の tcp_peeraddr_print() は、IP アドレスを数字で表示していた。 これを、ホスト名で表示するようにしなさい。

★練習問題 53 localhost

telnet で接続先として localhost (127.0.0.1) を指定するとどうなるか。 localhost は、どのホストからも自分自信を指す名前である。

★練習問題 54 httpサーバ

http サーバを作成しなさい。これは、受け取ったファイル名のファイルを open() して、内容を read() し、それを接続先に writen() で返すようにす るとよい。

この課題では、特にセキュリティに気をつけなさい。必ず次の条件を満たすよ うなサーバを作りなさい。

~yas/syspro/ipc/http-server-fork.c に、常に同じ内容を返すhttp サーバが ある。これを出発点にしてもよい。その場合、次の手順で開発するとよい。

  1. requestline を解析し、ファイル名を取り出す関数を単体で(httpサーバ とは別に) 開発する。
  2. 与えられたファイル名から、HTTP の応答のヘッダを画面に表示する関数 を単体で開発する。
  3. 与えられたファイル名から、HTTP の応答の本体を画面に表示する関数を 単体で開発する。
  4. 1. から 3. を http-server-fork.c に1つずつ組み込む。
大きなプログラムは、小さな部品に分解して独立して開発することが大事であ る。

実際の WWW ブラウザ (Netscapeなど)で、動作を確認しなさい。telnet だけ では、きちんと HTTP のプロトコルに従っているか確認できないので、不十分 である。

★練習問題 55 fingerサーバ

finger サーバを作成しなさい。これは、受け取った文字列を引数にして、 finger コマンドを実行するとよい。そして、実行結果を、接続先に返すよう にするとよい。

finger コマンドのプロセスと finger サーバの間は、パイプで接続しなさい。 今までの例題で利用した方法を組み合わせることで実現することができるはず である(pipe(),fork(),dup(),close(),execve()など)。popen() ライブラリ 関数を利用してもよい。

finger コマンドのプロセスと finger サーバ間をパイプで結ぶ代わりに、 finger コマンドを exec する前に、配管工事をして、標準出力(1)をTCP/IPの ストリームに接続する方法もある。

受け取った文字列をそのまま popen() や system() などに渡すのは危険である。 というのも、これらの引数は、シェル(/bin/sh)が解釈するからである。 popen() や system() を使う場合には、 finger コ マンドが受付けるのに相応しいもの( isalpha() や isdigit() の並び)かどう かを検査しなさい。もし、シェルが解釈する特殊な文字列(|;<> など)が含まれていると、意図しないプログラムが実行させられることがある。

fork() や execve() を用いて直接 /usr/bin/finger を実行する方法はシェル を経由しないので、比較的安全である。

★練習問題 56 HTTP Proxy

HTTP proxy 作りなさい。次の機能を、1つ以上付けなさい。


Last updated: 2003/05/26 00:42:17
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>