筑波大学 システム情報系 情報工学域 新城 靖 <yas@cs.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2014/2014-08-01
/index.html
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2014/
http://www.coins.tsukuba.ac.jp/~yas/
HTTP の内容(content)は、バイトの並び。バイト単位のループでもプログラムは書けるが、 遅いので、ある程度の大きさの塊(buffer size単位)で処理するのが普通。 テキストだけ扱うならば、行の並びと仮定しても問題ない。
http_receive_request( FILE *in, char *filename, size_t size ) { char *buf; buf = malloc( BUFSIZE ); ... /* free(buf) がない */ return( 1 ); }
http_receive_request( FILE *in, char *filename, size_t size ) { char *buf; buf = malloc( BUFSIZE ); ... if( ... ) { ... /* free(buf) がない */ return( 0 ); } ... free( buf ); return( 1 ); }
http_send_reply( FILE *out, char *filename ) { FILE *f; f = fopen(...); ... /* fclose( f ) がない */ return; }
http_send_reply( FILE *out, char *filename ) { FILE *f; f = fopen(...); ... if( ... ) { ... /* fclose( f ) がない */ return( 0 ); } ... fclose( f ); return; }以上のプログラムは、間違い。真似しないように。
http_send_reply( FILE *out, char *filename ) { auto char buf[BUFSIZE]; ... return; }auto と書かなくても OK 。
http_send_reply( FILE *out, char *filename ) { char buf[BUFSIZE]; ... return; }
FREAD(3) BSD Library Functions Manual FREAD(3) NAME fread, fwrite -- binary stream input/output LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <stdio.h> size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);型を合わせるために、ポインタ変数を使わなくてもよい。
char *buf; buf = malloc( BUFSIZE ); ... fread( buf, 1, BUFSIZE, in );以下でも OK。C言語で配列変数の名前を書くと、先頭要素のポインタの意味に なる。
char buf[BUFSIZE]; ... fread( buf, 1, BUFSIZE, in );以下でも同じ。
char buf[BUFSIZE]; ... fread( &buf[0], 1, BUFSIZE, in );
char buf[BUFSIZE]; ... fread( &buf, 1, BUFSIZE, in ); /* 普通は使わない。*/
サーバ側
$ cp ~yas/syspro/ipc/echo-server-nofork-fdopen.c .
$ make echo-server-nofork-fdopen
cc echo-server-nofork-fdopen.c -o echo-server-nofork-fdopen
$ ./echo-server-nofork-fdopen
Usage: ./echo-server-nofork-fdopen portno {ipversion}
$ ./echo-server-nofork-fdopen 1231
run telnet crocus39.coins.tsukuba.ac.jp 1231
[457] accepting incoming connections (acc==3) ...
[457] connection (fd==4) from 130.158.86.247:50290
[457] received (fd==4) 5 bytes, [012
]
^C
$
クライアント側(その1)。
$ telnet crocus39.coins.tsukuba.ac.jp 1231
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
012
012
クライアント側(その2)。
$ telnet crocus39.coins.tsukuba.ac.jp 1231
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc
クライアント(その2)は、クライアント(その1)が終了するまでサービス
を受けられない。
クライアント側(その1)。
$ telnet crocus39.coins.tsukuba.ac.jp 1231
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
012
012
345
345
^]
telnet> ^D
Connection closed.
$
クライアント側(その2)。
$ telnet crocus39.coins.tsukuba.ac.jp 1231
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc
abc
サーバ側
$ ./echo-server-nofork-fdopen 1231
run telnet crocus39.coins.tsukuba.ac.jp 1231
[457] accepting incoming connections (acc==3) ...
[457] connection (fd==4) from 130.158.86.247:50290
[457] received (fd==4) 5 bytes, [012
]
[457] received (fd==4) 5 bytes, [345
]
[457] connection (fd==4) closed.
[457] accepting incoming connections (acc==3) ...
[457] connection (fd==4) from 130.158.86.248:50290
[457] received (fd==4) 5 bytes, [abc
]
1: /* 2: fork-hello.c -- 画面に文字列を表示するプログラム 3: ~yas/syspro/proc/fork-hello.c 4: Start: 2001/05/13 23:19:01 5: */ 6: 7: #include <stdio.h> 8: #include <unistd.h> /* fork() */ 9: 10: int 11: main() 12: { 13: fork(); 14: fork(); 15: fork(); 16: printf("hello\n"); 17: }実行すると、画面には 8 回 hello と表示される。
$ ls fork-hello.c
$ ls: fork-hello.c: No such file or directory
$ cp ~yas/syspro/proc/fork-hello.c .
$ make fork-hello
cc fork-hello.c -o fork-hello
$ ./fork-hello
hello
hello
hello
hello
$ hello
hello
hello
hello
プロセスは、1回のfork() で、2倍に増える。3回のforkで、2 3 個
に増える。親子関係は複雑。
図? fork()システム・コールによるプロセスのコピー
1: 2: /* 3: echo-server-fork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork版) 4: ~yas/syspro/ipc/echo-server-fork-fdopen.c 5: Created on 2004/05/09 19:57:07 6: */ 7: #include <stdio.h> 8: #include <stdlib.h> /* exit() */ 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: #include <string.h> /* strlen() */ 16: #include <unistd.h> /* getpid(), gethostname() */ 17: 18: extern void echo_server( int portno, int ip_version ); 19: extern void delete_zombie(); 20: extern void echo_receive_request_and_send_reply( int com ); 21: extern int echo_receive_request( char *line, size_t size, FILE *in ); 22: extern void echo_send_reply( char *line, FILE *out ); 23: extern void print_my_host_port( int portno ); 24: extern void tcp_peeraddr_print( int com ); 25: extern void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len ); 26: extern int tcp_acc_port( int portno, int pf ); 27: extern int fdopen_sock( int sock, FILE **inp, FILE **outp ); 28: 29: int 30: main( int argc, char *argv[] ) 31: { 32: int portno, ip_version; 33: 34: if( !(argc == 2 || argc==3) ) { 35: fprintf(stderr,"Usage: %s portno {ipversion}\n",argv[0] ); 36: exit( 1 ); 37: } 38: portno = strtol( argv[1],0,10 ); 39: if( argc == 3 ) 40: ip_version = strtol( argv[2],0,10 ); 41: else 42: ip_version = 4; /* IPv4 by default */ 43: echo_server( portno, ip_version ); 44: } 45:main() は、 echo-server-nofork-fdopen.c と同じである。
46: void 47: echo_server( int portno, int ip_version ) 48: { 49: int acc,com ; 50: pid_t child_pid ; 51: 52: acc = tcp_acc_port( portno, ip_version ); 53: if( acc<0 ) 54: exit( 1 ); 55: print_my_host_port( portno ); 56: while( 1 ) 57: { 58: delete_zombie(); 59: printf("[%d] accepting incoming connections (acc==%d) ...\n", 60: getpid(),acc ); 61: if( (com = accept( acc,0,0 )) < 0 ) 62: { 63: perror("accept"); 64: exit( -1 ); 65: } 66: tcp_peeraddr_print( com ); 67: if( (child_pid=fork()) > 0 ) /* parent */ 68: { 69: close( com ); 70: } 71: else if( child_pid == 0 ) /* child */ 72: { 73: close( acc ); 74: echo_receive_request_and_send_reply( com ); 75: exit( 0 ); 76: } 77: else 78: { 79: perror("fork"); 80: exit( -1 ); 81: } 82: } 83: } 84:
delete_zombie() は、ゾンビ・プロセス(後述)を消去するものである。 (改善の余地がある。) echo サービスとしての処理は、fork() システムコールで分身を作り、子プロ セス側で行う。親プロセスは、すぐにaccept() に戻る。
getpid() は、自分自身の PID (Process ID (identifier)) を返すシステムコー ルである。PID (pid_t) は、32 ビット(システムによっては16ビット)の整数 である。実行結果で printf() の表示に、PID が表示されているが、同じではないことに 注意しなさい。
85: void 86: delete_zombie() 87: { 88: pid_t pid ; 89: 90: while( (pid=wait4(-1,0,WNOHANG,0)) >0 ) 91: { 92: printf("[%d] zombi %d deleted.\n",getpid(),pid ); 93: continue; 94: } 95: } 96:wait4() システムコールを使って、終了した子プロセスをwait してあげてい る。ただし、WNOHANG オプションを付けてあるので、終了した子プロセスがい なければ、待たずに返ってくる。
+
」は、追加された行、
「-
」は、削除された行、
「!
」は、修正された行を意味する。
$ diff -c echo-server-nofork-fdopen.c echo-server-fork-fdopen.c *** echo-server-nofork-fdopen.c 2014-07-28 15:59:10.000000000 +0900 --- echo-server-fork-fdopen.c 2014-07-31 11:25:38.000000000 +0900 *************** *** 1,8 **** /* ! echo-server-nofork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork無し版) ! ~yas/syspro/ipc/echo-server-nofork-fdopen.c ! Created on 2004/05/09 19:08:47 */ #include <stdio.h> #include <stdlib.h> /* exit() */ --- 1,8 ---- /* ! echo-server-fork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork版) ! ~yas/syspro/ipc/echo-server-fork-fdopen.c ! Created on 2004/05/09 19:57:07 */ #include <stdio.h> #include <stdlib.h> /* exit() */ *************** *** 16,21 **** --- 16,22 ---- #include <unistd.h> /* getpid(), gethostname() */ extern void echo_server( int portno, int ip_version ); + extern void delete_zombie(); extern void echo_receive_request_and_send_reply( int com ); extern int echo_receive_request( char *line, size_t size, FILE *in ); extern void echo_send_reply( char *line, FILE *out ); *************** *** 46,51 **** --- 47,53 ---- echo_server( int portno, int ip_version ) { int acc,com ; + pid_t child_pid ; acc = tcp_acc_port( portno, ip_version ); if( acc<0 ) *************** *** 53,58 **** --- 55,61 ---- print_my_host_port( portno ); while( 1 ) { + delete_zombie(); printf("[%d] accepting incoming connections (acc==%d) ...\n", getpid(),acc ); if( (com = accept( acc,0,0 )) < 0 ) *************** *** 61,67 **** exit( -1 ); } tcp_peeraddr_print( com ); ! echo_receive_request_and_send_reply( com ); } } --- 64,96 ---- exit( -1 ); } tcp_peeraddr_print( com ); ! if( (child_pid=fork()) > 0 ) /* parent */ ! { ! close( com ); ! } ! else if( child_pid == 0 ) /* child */ ! { ! close( acc ); ! echo_receive_request_and_send_reply( com ); ! exit( 0 ); ! } ! else ! { ! perror("fork"); ! exit( -1 ); ! } ! } ! } ! ! void ! delete_zombie() ! { ! pid_t pid ; ! ! while( (pid=wait4(-1,0,WNOHANG,0)) >0 ) ! { ! printf("[%d] zombi %d deleted.\n",getpid(),pid ); ! continue; } } $
サーバ側。 サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。
注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。
$ ls echo-server-fork-fdopen.c
ls: echo-server-fork-fdopen.c: No such file or directory
$ cp ~yas/syspro/ipc/echo-server-fork-fdopen.c .
$ ls -l echo-server-fork-fdopen.c
-rw-r--r-- 1 yas prof 6001 7 31 11:28 echo-server-fork-fdopen.c
$ make echo-server-fork-fdopen
cc echo-server-fork-fdopen.c -o echo-server-fork-fdopen
$ ./echo-server-fork-fdopen
Usage: ./echo-server-fork-fdopen portno {ipversion}
$ ./echo-server-fork-fdopen 1231
run telnet crocus39.coins.tsukuba.ac.jp 1231
[496] accepting incoming connections (acc==3) ...
[496] connection (fd==4) from 130.158.86.247:50426
[496] accepting incoming connections (acc==3) ...
[499] received (fd==4) 5 bytes, [012
]
[496] connection (fd==4) from 130.158.86.248:50351
[496] accepting incoming connections (acc==3) ...
[502] received (fd==4) 5 bytes, [abc
]
[502] received (fd==4) 5 bytes, [def
]
[499] received (fd==4) 5 bytes, [345
]
[499] connection (fd==4) closed.
[502] connection (fd==4) closed.
クライアント側(その1)。
$ telnet crocus39.coins.tsukuba.ac.jp 1231
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
012
012
345
345
^]
telnet> ^D
Connection closed.
$
クライアント側(その2)。
$ telnet crocus39.coins.tsukuba.ac.jp 1231
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc
abc
def
def
^]
telnet> ^D
Connection closed.
$
図1 サーバが accept() で接続要求を待っている
図2 サーバが accept()している時にクライアントが connect()した所
図3 accept() の結果、通信用ポート com が作られる
図4 fork() して、親子に別れる。
図5 親は、com を close()、子は acc を close() する
図6 親は、再び accept() で待ち、子は、特定のクライアントに対して read()/write() する。
図7 サーバが別のクライアントから接続要求を受け付ける
図8 accept() の結果、通信用ポート com が作られる
図9 fork() して、親子に別れる。
図10 親は、com を close()、子は acc を close() する
図11 親は、再び accept() で待ち、子は、特定のクライアントに対して read()/write() する。
$ ./echo-server-fork-fdopen 1231
run telnet crocus39.coins.tsukuba.ac.jp 1231
[496] accepting incoming connections (acc==3) ...
[496] connection (fd==4) from 130.158.86.247:50426
[496] accepting incoming connections (acc==3) ...
[499] received (fd==4) 5 bytes, [012
]
[496] connection (fd==4) from 130.158.86.248:50351
[496] accepting incoming connections (acc==3) ...
[502] received (fd==4) 5 bytes, [abc
]
[502] received (fd==4) 5 bytes, [def
]
[499] received (fd==4) 5 bytes, [345
]
[499] connection (fd==4) closed.
[502] connection (fd==4) closed.
ps コマンドで見ると、状態(STAT) がゾンビを意味する Z と表示される。
$ ps l | egrep echo
1013 496 384 0 31 0 2443008 556 - S+ s000 0:00.00 ./echo-server-fork-fdopen 1231
1013 499 496 0 0 0 0 0 - Z+ s000 0:00.00 (echo-server-fork)
1013 502 496 0 0 0 0 0 - Z+ s000 0:00.00 (echo-server-fork)
1013 586 515 0 31 0 2461016 384 - S+ s001 0:00.00 egrep echo
$
echo-server-fork-fdopen.c では、
delete_zombie() でゾンビを消そうとはしている。しかし、accept() で止まっ
ている状態なので、どこかのクライアントから接続要求が来ない限りは、
delete_zombie() が呼ばれないので、ゾンビとして残っている。
この状態で接続要求が来ると、ゾンビが回収される。 クライアント
$ telnet crocus39.coins.tsukuba.ac.jp 1231
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
[496] connection (fd==4) from 130.158.86.247:50455
[496] zombi 502 deleted.
[496] zombi 499 deleted.
[496] accepting incoming connections (acc==3) ...
ゾンビが回収された後の ps コマンドの結果:
$ ps l | egrep echo
1013 496 384 0 31 0 2443008 556 - S+ s000 0:00.00 ./echo-server-fork-fdopen 1231
1013 587 496 0 31 0 2451200 232 - S+ s000 0:00.00 ./echo-server-fork-fdopen 1231
1013 589 515 0 31 0 2461016 380 - S+ s001 0:00.00 egrep echo
$
実行結果として、サーバ側で、複数同時に受け付けていることを示すログをつ けなさい。複数接続できることを確認するには、telnet のように遅いクライア ントを用いるとよい。単に Web ブラウザで複数のページを開けても、通信その ものは短時間で終わるので、複数接続できることの確認にはならない。
注意: 上限を設定する目的は、攻撃等でサーバの CPU 時間が奪われることやプ ロセスが増えすぎることを防ぐことにある。プロセスの終了を busy wait で待っ てはならない。待つならば、accept() や wait() のように、CPU 時間を消費し ない形で待つようにしなさい。
Keep-Alive
に対応しなさい。
ヒント:子プロセスの終了を、ソフトウェア割り込み(signal, SIGCHLD)で知 る方法もある。複数の子プロセスが終了しても、割り込みは1回しか起こらな いことがあることに注意しなさい。
ソフトウェア割込みを使うと、accept() システムコールがエラーで戻って来 るシステムもある。その場合、エラー番号が EINTR なら単純に終了しないで、 再び accept() に向うべきである。
この課題では、複数のクライアントに対するサービスの同時提供を実現するた めに、fork() を使いなさい。select() や Pthread を用いてはならない。
ヒント: IPv4 と IPv6 専用のプロセスやスレッドを作成する。
ヒント: IPv4 の acc と IPv6 の acc を2つ作成し、 両方同時に監視する。
なお同時に扱えるクライアント数は、IPv4 と IPv6 で個別に数えても 統合して数えてもどちらでも良い。たとえば、最大2と設定した場合、 個別に数えて IPv4 で 2, IPv6 で 2、合計 4 としてもよい。