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

システム・プログラム

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

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

■今日の重要な話

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

通信絽の開設

■echo-server-fork

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

----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        echo-server-fork.c -- 受け取った文字列をそのまま返すサーバ(fork版)
   4:	        ~yas/syspro-2001/ipc/echo-server-fork.c
   5:	        $Header: /home/lab2/OS/yas/syspro-2001/ipc/RCS/echo-server-fork.c,v 1.9 2001/05/27 13:38:59 yas Exp $
   6:	        Start: 1997/06/09 19:46:40
   7:	*/
   8:	#include <stdio.h>
   9:	#include <sys/types.h>  /* socket(), time() */
  10:	#include <sys/socket.h> /* socket() */
  11:	#include <netinet/in.h> /* struct sockaddr_in */
  12:	
  13:	extern  void echo_server( int portno );
  14:	extern  void echo_reply( int com );
  15:	extern  void print_host_port( int portno );
  16:	extern  void tcp_peeraddr_print( int com );
  17:	extern  tcp_acc_port( int portno );
  18:	extern  ssize_t writen(int fd, const void *vptr, size_t n);
  19:	extern  ssize_t readline(int fd, void *vptr, size_t maxlen);
  20:	
  21:	main( int argc, char *argv[] )
  22:	{
  23:	    int portno ;
  24:	        if( argc >= 3 )
  25:	        {
  26:	            fprintf( stdout,"Usage: %s host port\n",argv[0] );
  27:	            exit( -1 );
  28:	        }
  29:	        if( argc == 2 )
  30:	            portno = atoi( argv[1] );
  31:	        else
  32:	            portno = getuid();
  33:	        echo_server( portno );
  34:	}
  35:	
  36:	void
  37:	echo_server( int portno )
  38:	{
  39:	    int acc,com ;
  40:	    pid_t child_pid ;
  41:	        acc = tcp_acc_port( portno );
  42:	        if( acc<0 )
  43:	            exit( -1 );
  44:	        print_host_port( portno );
  45:	        while( 1 )
  46:	        {
  47:	            if( (com = accept( acc,0,0 )) < 0 )
  48:	            {
  49:	                perror("accept");
  50:	                exit( -1 );
  51:	            }
  52:	            tcp_peeraddr_print( com );
  53:	            if( (child_pid=fork()) > 0 ) /* parent */
  54:	            {
  55:	                close( com );
  56:	            }
  57:	            else if( child_pid == 0 ) /* parent */
  58:	            {
  59:	                close( acc );
  60:	                echo_reply( com );
  61:	                printf("[%d,%d] connection closed.\n",getpid(),com );
  62:	                close( com );
  63:	                exit( 0 );
  64:	            }
  65:	            else
  66:	            {
  67:	                perror("fork");
  68:	                exit( -1 );
  69:	            }  
  70:	        }
  71:	}
  72:	
  73:	#define BUFFERSIZE      1024
  74:	
  75:	void
  76:	echo_reply( int com )
  77:	{
  78:	    char line[BUFFERSIZE] ;
  79:	    int rcount ;
  80:	    int wcount ;
  81:	
  82:	        while( (rcount=readline(com,line,BUFFERSIZE)) > 0 )
  83:	        {
  84:	            printf("[%d,%d] read() %d bytes, %s",getpid(),com,rcount,line );
  85:	            fflush( stdout );
  86:	            if( (wcount=writen(com,line,rcount))!= rcount )
  87:	            {
  88:	                 perror("write");
  89:	                 exit( 1 );
  90:	            }
  91:	        }
  92:	}
  93:	
  94:	void
  95:	print_host_port( int portno )
  96:	{
  97:	    char hostname[100] ;
  98:	        gethostname( hostname,sizeof(hostname) );
  99:	        hostname[99] = 0 ;
 100:	        printf("run telnet %s %d \n",hostname, portno );
 101:	}
 102:	
 103:	void
 104:	tcp_peeraddr_print( int com )
 105:	{
 106:	    struct sockaddr_in addr ;
 107:	    int addr_len ;
 108:	    union {
 109:	        int i ;
 110:	        unsigned char byte[4] ;
 111:	    } x ;
 112:	        addr_len = sizeof( addr );
 113:	        if( getpeername( com, &addr, &addr_len  )<0 )
 114:	        {
 115:	            perror("print_peeraddr");
 116:	        }
 117:	        x.i = addr.sin_addr.s_addr ;
 118:	        printf("[%d,%d] connection from %d.%d.%d.%d:%d\n",getpid(),com,
 119:	               x.byte[0],x.byte[1],x.byte[2],x.byte[3],
 120:	               ntohs( addr.sin_port ));
 121:	}
 122:	
 123:	int
 124:	tcp_acc_port( int portno )
 125:	{
 126:	    struct hostent *hostent ;
 127:	    struct sockaddr_in addr ;
 128:	    int addr_len ;
 129:	    int s ;
 130:	
 131:	        if( (s = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
 132:	        {
 133:	            perror("socket");
 134:	            return( -1 );
 135:	        }
 136:	
 137:	        addr.sin_family = AF_INET ;
 138:	        addr.sin_addr.s_addr = INADDR_ANY ;
 139:	        addr.sin_port = htons( portno );
 140:	
 141:	        if( bind(s,&addr,sizeof(addr)) < 0 )
 142:	        {
 143:	            perror( "bind" );
 144:	            fprintf(stderr,"port number %d is already used. wait a moment or kill another program.\n", portno );
 145:	            return( -1 );
 146:	        }
 147:	        listen( s, 5 );
 148:	        return( s );
 149:	}
<省略>
 171:	ssize_t                 /* Write "n" bytes to a descriptor. */
 172:	writen(int fd, const void *vptr, size_t n)
<省略>
 222:	ssize_t
 223:	readline(int fd, void *vptr, size_t maxlen)
<省略>
----------------------------------------------------------------------

実行例。

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


----------------------------------------------------------------------
% ./echo-server-select [←]
run telnet adonis1 1231 
[20764,4] connection from 130.158.86.1:23599
[20766,4] read() 5 bytes, 012
[20764,4] connection from 130.158.86.9:20199
[20767,4] read() 5 bytes, abc
[20767,4] read() 5 bytes, def
[20767,4] connection closed.
[20766,4] read() 5 bytes, 345
[20766,4] connection closed.
^C
% []
----------------------------------------------------------------------
クライアント側(その1)。
----------------------------------------------------------------------
% telnet adonis1 1231 [←]
Trying 130.158.86.1...
Connected to adonis1.
Escape character is '^]'.
012[←]
012
345[←]
345
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------
クライアント側(その2)。
----------------------------------------------------------------------
% % telnet adonis1 1231 [←]
Trying 130.158.86.1...
Connected to adonis1.
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 と表示される。 CMD は、defunct (故人となった、消滅した)と表示される。

% ps -lu $USER [←]
  F S   UID   PID  PPID  C PRI NI  P    SZ:RSS      WCHAN TTY     TIME CMD
 b0 S  1231 13105 13104  0  39 20  *   618:228   802737f0 pts/2   0:02 tcsh 
 90 Z  1231 13614 13609  0   0 -   -     - -            - -       0:00 <defunct>
 b0 S  1231 13609 13105  0  60 20  *   359:44    8079a958 pts/2   0:00 echo-serv 
 b0 S  1231 13076 13075  1  39 20  *   614:221   802737f0 pts/1   0:01 tcsh 
 b0 R  1231 13623 13076  3  61 20  0   397:79           - pts/1   0:00 ps 
 90 Z  1231 13617 13609  0   0 -   -     - -            - -       0:00 <defunct>
 b0 T  1231 13142 13076  0  60 20  *  2229:1052         - pts/1   0:15 emacs 
% []

■echo-server-select

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

----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        echo-server-select.c -- 受け取った文字列をそのまま返すサーバ(select版)
   4:	        ~yas/syspro-2001/ipc/echo-server-select.c
   5:	        $Header: /home/lab2/OS/yas/syspro-2001/ipc/RCS/echo-server-select.c,v 1.3 2001/05/27 13:42:18 yas Exp $
   6:	        Start: 1997/06/09 19:53:33
   7:	*/
   8:	#include <stdio.h>
   9:	#include <sys/types.h>  /* socket(), time(), select() */
  10:	#include <sys/socket.h> /* socket() */
  11:	#include <netinet/in.h> /* struct sockaddr_in */
  12:	#include <unistd.h>     /* select() */
  13:	#include <bstring.h>    /* select() */
  14:	#include <sys/time.h>   /* select() */
  15:	
  16:	extern  void echo_server( int portno );
  17:	extern  void echo_reply_select( int com );
  18:	extern  void print_host_port( int portno );
  19:	extern  void tcp_peeraddr_print( int com );
  20:	extern  tcp_acc_port( int portno );
  21:	extern  ssize_t writen(int fd, const void *vptr, size_t n);
  22:	extern  ssize_t readline(int fd, void *vptr, size_t maxlen);
  23:	
  24:	main( int argc, char *argv[] )
  25:	{
  26:	    int portno ;
  27:	        if( argc >= 3 )
  28:	        {
  29:	            fprintf( stdout,"Usage: %s host port\n",argv[0] );
  30:	            exit( -1 );
  31:	        }
  32:	        if( argc == 2 )
  33:	            portno = atoi( argv[1] );
  34:	        else
  35:	            portno = getuid();
  36:	        echo_reply_select( portno );
  37:	}
  38:	
  39:	void
  40:	echo_reply_select( int portno )
  41:	{
  42:	    int acc,com ;
  43:	    fd_set readfds,readfds_save ;
  44:	    int i,n ;
  45:	
  46:	        acc = tcp_acc_port( portno );
  47:	        if( acc<0 )
  48:	            exit( -1 );
  49:	        print_host_port( portno );
  50:	
  51:	        FD_ZERO( &readfds_save );
  52:	        FD_SET( acc,&readfds_save );
  53:	        while( 1 )
  54:	        {
  55:	            readfds = readfds_save ;
  56:	            n = select( FD_SETSIZE,&readfds,0,0,0 );
  57:	            if( n <= 0 )
  58:	            {
  59:	                perror("select");
  60:	                exit( 1 );
  61:	            }
  62:	            if( FD_ISSET(acc,&readfds) )
  63:	            {
  64:	                FD_CLR( acc,&readfds );
  65:	                if( (com = accept( acc,0,0 )) < 0 )
  66:	                {
  67:	                    perror("accept");
  68:	                    exit( -1 );
  69:	                }
  70:	                FD_SET( com, &readfds_save );
  71:	                tcp_peeraddr_print( com );
  72:	            }
  73:	            for( i=0 ; i<FD_SETSIZE ; i++ )
  74:	            {
  75:	                if( FD_ISSET(i,&readfds) )
  76:	                {
  77:	                    if( echo_reply_once( i )<=0 )
  78:	                    {
  79:	                        printf("[%d,%d] connection closed.\n",getpid(),i );
  80:	                        close( i );
  81:	                        FD_CLR( i,&readfds_save );
  82:	                    }
  83:	                }
  84:	            }
  85:	        }
  86:	}
  87:	
  88:	#define BUFFERSIZE      1024
  89:	
  90:	int
  91:	echo_reply_once( int com )
  92:	{
  93:	    char line[BUFFERSIZE] ;
  94:	    int rcount ;
  95:	    int wcount ;
  96:	
  97:	        if( (rcount=readline(com,line,BUFFERSIZE)) > 0 )
  98:	        {
  99:	            printf("[%d,%d] read() %d bytes, %s",getpid(),com,rcount,line );
 100:	            fflush( stdout );
 101:	            if( (wcount=writen(com,line,rcount))!= rcount )
 102:	            {
 103:	                 perror("write");
 104:	                 exit( 1 );
 105:	            }
 106:	        }
 107:	        return( rcount );
 108:	}
<以下省略>
----------------------------------------------------------------------

実行例。

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


----------------------------------------------------------------------
% ./echo-server-select [←]
run telnet adonis1 1231 
[20937,4] connection from 130.158.86.1:23650
[20937,4] read() 5 bytes, 012
[20937,5] connection from 130.158.86.9:20203
[20937,5] read() 5 bytes, abc
[20937,5] read() 5 bytes, def
[20937,5] connection closed.
[20937,4] read() 5 bytes, 345
[20937,4] connection closed.
^C
% []
----------------------------------------------------------------------
クライアント側(その1)。
----------------------------------------------------------------------
Trying 130.158.86.1...
Connected to adonis1.
Escape character is '^]'.
012[←]
012
345[←]
345
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------
クライアント側(その2)。
----------------------------------------------------------------------
% telnet adonis1 1231 [←]
Trying 130.158.86.1...
Connected to adonis1.
Escape character is '^]'.
abc[←]
abc
def[←]
def
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------

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

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

◆DoS攻撃

このプログラムでは、readline() を使っているので、クライアントから '\n' が送られてこないと、サーバ全体の処理が停止してしまう。read() を使えば、 そのような問題は出ないが、行単位で何か処理を行うようなサーバを作るには、 クライアントごとに行のバッファを設ける必要がある。

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

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

■練習問題

★練習問題 32 ゾンビの消去

echo-server-fork.c では、ゾンビ・プロセスがどんどん残ってしまう。これ を残らないようにしなさい。

ヒント:accept() で止まる前に、wait() する方法がある。ただし、単純に wait() すると、一つのクライアントが終了するまで他のクライアントが接続 できなくなり、せっかく fork() した意味がなくなってしまう。よって、子プ ロセスが終了したときだけ、wait() したい。それには、waitpid() や wait3(), wait4() で、WNOHANG オプションを使い、終了したプロセスが存在 する間、それらを全て待ち消去すればよい。

ヒント:子プロセスの終了を、 ソフトウェア割り込み(後述) (SIGCHLD)で知る方法もある。複数の子プロセスが終了しても、割り込みは1 回しか起こらないことがあることに注意しなさい。accept() システムコール がエラーで戻って来ることがあることにも注意しなさい。

★練習問題 33 accept() での EINTR の扱い

echo-server-fork.c では、accept() シス テムコールがエラーを返した時、単純にサーバを終了させている。実際には、 ソフトウェア割り込み(後述)を使った時には、EINTR (Interrupted function call) というエラーが返ることがある。その場合は、再び accept() を実行し てよい。そうなるように、プログラムを書き換えなさい。

★練習問題 34 SO_REUSEADDR

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

----------------------------------------------------------------------
% ./echo-server-fork [←]
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);
この効果を確かめなさい。

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

★練習問題 35 DoS攻撃対策

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

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

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

★練習問題 37 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 なら本来双方向で使える。しかし、この練習問題だと単方向だけを使 う。こうすると、パイプと同じような使い方ができる。

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

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

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

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

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

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

ヒント: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回のコマン ドを実行したら終了するようにする。

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

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

普通は、ホスト名からIPアドレスを調べる手続き gethostbyname() が使われ る。gethostbyaddr() は、その逆を行う手続きである。

★練習問題 41 localhost

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

★練習問題 42 fingerサーバ

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

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

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

★練習問題 43 httpサーバ

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

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

★練習問題 44 HTTP Proxy

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