システム・プログラム
電子・情報工学系
新城 靖
<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
----------------------------------------------------------------------
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 を越えた時には、使ってはいけない場所を使ってしまった ことを意味する。これを、「バッファ・オーバーフロー」という。 そもそも事後にチェックするのは、本当は遅い。そもそも、バッファ・オーバー フローが起きないように気を付けながらプログラムを書くべきである。
----------------------------------------------------------------------
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() は、決してバッファ・オーバーフローを起こすこと はないが、バッファが足りない時には意図していない結果が保存されているこ とになる。)
----------------------------------------------------------------------
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 を使うとプログラムが動かないことがある。
クライアント側(その1)。---------------------------------------------------------------------- % ./echo-server-fork 1231run 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 %
----------------------------------------------------------------------
クライアント側(その2)。---------------------------------------------------------------------- % telnet adonis9.coins.tsukuba.ac.jp 1231Trying 130.158.86.29... Connected to adonis9.coins.tsukuba.ac.jp. Escape character is '^]'. 012
012 345
345 ^] telnet> quit
Connection closed. %
----------------------------------------------------------------------
---------------------------------------------------------------------- % telnet adonis9.coins.tsukuba.ac.jp 1231Trying 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()した所
図2 サーバで accept()からreturnした所。fork() する直前。
図3サーバで親がcomをclose()、子が acc を close() した所。
図4 複数のクライアントが接続した時
ps -l コマンドで見ると、ゾンビの状態(S) は、Z と表示される。
echo-server-fork.c では、 delete_zombie() でゾンビを消そうとはしている。しかし、accept() で止まっ ている状態なので、どこかのクライアントから接続要求が来ない限りは、 delete_zombie() が呼ばれないので、ゾンビとして残っている。% ps aux | egrep echoyas 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 1231run 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.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 (集合)を意味する。内 部的には、ビットの並びで実現されていることが多い。次のような操作がある。
無限ループを含むのは、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 を使うとプログラムが動かないことがある。
クライアント側(その1)。---------------------------------------------------------------------- % ./echo-server-select 1231run 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 %
----------------------------------------------------------------------
クライアント側(その2)。---------------------------------------------------------------------- % telnet adonis9.coins.tsukuba.ac.jp 1231Trying 130.158.86.29... Connected to adonis9.coins.tsukuba.ac.jp. Escape character is '^]'. 012
012 345
345 ^] telnet> quit
Connection closed. %
----------------------------------------------------------------------
---------------------------------------------------------------------- % telnet adonis9.coins.tsukuba.ac.jp 1231Trying 130.158.86.29... Connected to adonis9.coins.tsukuba.ac.jp. Escape character is '^]'. abc
abc def
def ^] telnet> quit
Connection closed. %
----------------------------------------------------------------------
図4 複数のクライアントが接続した時(select())
readline() の代りにread() を使えば、そのような問題は生じない。しかし、 行単位で何か処理を行うようなサーバを作るには、「クライアントごとに」、 少なくとも1行をためるためのバッファを設ける必要がある。
クライアントが意図的に '\n' を送らないことがある。これも、DoS 攻撃 (Denial of Service攻撃、サービス運用妨害攻撃)の一種である。 その他に、writen() に対して、クライアントが read() しないという攻撃も ある。
echo-server-fork.c では、クライアントご とにプロセス(スレッド)が存在するので、安全に readline() を使うことが できる。ただし、プロセスをコピーする処理が重たいので、過剰な connect() による接続要求による DoS 攻撃には弱くなる。(fork() の処理ばかりして、 通常の業務ができなくなる。)
サーバ・プログラムを作成する時には、DoS 攻撃に強いものを作成するように 気をつける。
ヒント:子プロセスの終了を、ソフトウェア割り込み(signal, 後述) (SIGCHLD)で知る方法もある。複数の子プロセスが終了しても、割り込みは1 回しか起こらないことがあることに注意しなさい。
ソフトウェア割込みを使うと、accept() システムコールがエラーで戻って来 るシステムもある。その場合、エラー番号が EINTR なら単純に終了しないで、 再び accept() に向うべきである。
このエラーは、実際に同じポート番号を別のプロセスが使っている時にも出る が、既に以前に使っていたプロセスが終了している時にも出ることがある。 後者の場合、次のオプションを付けると、この状態が緩和されることがある。---------------------------------------------------------------------- % ./echo-server-fork 1231bind: 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);この効果を確かめなさい。
また、同じポート番号の再利用を制限している理由を考えなさい。
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 なら本来双方向で使える。しかし、この練習問題だと単方向だけを使 う。こうすると、パイプと同じような使い方ができる。
練習問題49 で、3つ以上のプロセスを接続できるようにしなさい。
ヒント:練習問題28 3個のプロセスをパイプで結ぶに相当する。
プログラムを1つだけ作るのではなく、先頭用、中間用、末尾用の3つを作る とよい。中間用のものは、うまく作ると、何個でもはさみ込めるようになる。
ヒント: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回のコマン ドを実行したら終了するようにする。
上の tcp_peeraddr_print() は、IP アドレスを数字で表示していた。 これを、ホスト名で表示するようにしなさい。
http サーバを作成しなさい。これは、受け取ったファイル名のファイルを open() して、内容を read() し、それを接続先に writen() で返すようにす るとよい。
この課題では、特にセキュリティに気をつけなさい。必ず次の条件を満たすよ うなサーバを作りなさい。
~/public_html)以下のファ
イルしかアクセスできないようにすること。それには、ファイルを open() す
る時に、頭に HOME 環境変数の値と "/public_html" を付加する方法がある。
(他の方法もある。)~ は、open() システム・コールでは使えない。
~ は、シェルや emacs が気を効かせてホーム・ディレクトリを調べて
置き換えている。
.. が含まれていたら、アクセスを拒否すること。
.. は、1つ上のディレクトリを意味する。.. を許せば、
~/public_html 以外の場所、たとえば、
../Mail で、電子メールが盗まれる危険性がある。
---------------------- Content-Type: 拡張子 ---------------------- text/html .html text/plain .txt image/gif .gif image/jpeg .jpeg ----------------------
~yas/syspro/ipc/http-server-fork.c に、常に同じ内容を返すhttp サーバが ある。これを出発点にしてもよい。その場合、次の手順で開発するとよい。
実際の WWW ブラウザ (Netscapeなど)で、動作を確認しなさい。telnet だけ では、きちんと HTTP のプロトコルに従っているか確認できないので、不十分 である。
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 を実行する方法はシェル を経由しないので、比較的安全である。
HTTP proxy 作りなさい。次の機能を、1つ以上付けなさい。