筑波大学 システム情報系 情報工学域
新城 靖
<yas@cs.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2023/2023-07-12
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2023/
http://www.coins.tsukuba.ac.jp/~yas/
HTTP の内容(content)は、バイトの並び。バイト単位のループでもプログラムは書けるが、 遅いので、ある程度の大きさの塊(buffer size単位)で処理するのが普通。 テキストだけ扱うならば、行の並びと仮定しても問題ない。
ChatGPT は、データ構造とアルゴリズムの相似を無視して、次のような酷いコー ドを生成することがある。人間は、真似しないように。
read(socket, buffer, BUFFER_SIZE - 1);
request_line = strtok(buffer, "\r\n");
strtok(request_line, " ");
file_name = strtok(NULL, " ");
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_receive_request( FILE *in, char *filename, size_t size )
{
int count;
char **vec;
....
string_split(requestline,' ',&count,&vec); /* 内部で malloc() を呼んでいる */
...
if( ... ) {
...
/* free_string_vector(count,vec) がない */
return( 0 );
}
...
free_string_vector( count,vec ); /* 内部で free() を呼んでいる */
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 ); /* 普通は使わない。*/
C言語では、関数から直接 return できる値は 1 つだけ。複数の値を返すため
に、引数にポインタを使う方法がよく使われる。
int fstat(int fd, struct stat *statbuf); pid_t wait(int *wstatus); long int strtol(char *nptr, char **endptr, int base); int string_split( char *str, char del, int *countp, char ***vecp ); int fdopen_sock( int sock, FILE **inp, FILE **outp );呼び出す時には、結果を受けとるためにポインタではない変数を用意して、 & を使うのが普通。
struct stat fs; if (fstat(fd, &fs) < 0) ... int status; if (wait(&status) < 0) ... int portno; char *s, *p; portno = strtol( s, &p, 10 ); portno = strtol( s, 0, 10 ); /* endptr が不用な時 */ int count; char **vec; if( string_split( str, c, &count, &vec ) < 0 ) ... FILE *in, *out ; if( fdopen_sock(com,&in,&out) < 0 ) ...
int snprintf(char * restrict str, size_t size,
const char * restrict format, ...);
正解。第2引数に、第1引数のサイズを入れる。
char buf[BUFSIZE];
snprintf( buf, BUFSIZE, "%s", s );
char *buf;
buf = malloc( BUFSIZE );
snprintf( buf, BUFSIZE, "%s", s );
間違い。第2引数に、入力データの長さを入れる。
入力が大きければ、バッファ・オーバーフローが生じる。
char buf[BUFSIZE];
snprintf( buf, strlen(s), "%s", s );
size に入力から計算した値を入れてはいけ
ない。
strlcpy(), strlcat() でも同様。
size_t strlcpy(char *dst, const char *src, size_t size); size_t strlcat(char *dst, const char *src, size_t size);
char buf[BUFSIZE];
snprintf( buf, BUFSIZE, "%s", s );
正解2。
char buf[BUFSIZE];
snprintf( buf, sizeof(buf), "%s", s );
正解3。
char *buf;
buf = malloc( BUFSIZE );
snprintf( buf, BUFSIZE, "%s", s );
以下は間違い。
char *buf;
buf = malloc( BUFSIZE );
snprintf( buf, sizeof(buf), "%s", s );
sizeof(buf) は、ポインタ変数のサイズ sizeof(char *) を返す。
文字操作,文字列操作ライブラリ参照。
~yas/syspro/cc/sizeof.c を実行して、予想通りの結果が表示されるか確認しなさい。
どうしても、Markdown を使いたければ、本来の Markdown の思想に照らして、 フォーマッタなしでも採点者が見やすいように、折り返す等して工夫すること。
サーバ側
$ 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 azalea16 1231
[7512] accepting (fd==4) to [::]:1231
[7512] accepting incoming connections (acc==4) ...
[7512] connection (fd==5) from [2001:2f8:3a:1711::230:39]:52941
[7512] received (fd==5) 5 bytes, [012
]
^C
$
クライアント側(その1)。
$ telnet azalea16.coins.tsukuba.ac.jp 1231
Trying 2001:2f8:3a:1711::230:30...
Connected to azalea16.coins.tsukuba.ac.jp.
Escape character is '^]'.
012
012
クライアント側(その2)。
$ telnet azalea16.coins.tsukuba.ac.jp 1231
Trying 2001:2f8:3a:1711::230:30...
Connected to azalea16.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc
クライアント(その2)は、クライアント(その1)が終了するまでサービス
を受けられない。
クライアント側(その1)。
$ telnet azalea16.coins.tsukuba.ac.jp 7
Trying 2001:2f8:3a:1711::230:30...
Connected to azalea16.coins.tsukuba.ac.jp.
Escape character is '^]'.
012
012
クライアント側(その2)。
$ telnet azalea16.coins.tsukuba.ac.jp 7
Trying 2001:2f8:3a:1711::230:30...
Connected to azalea16.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc
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()システム・コールによるプロセスのコピー
echo-server-fork-fdopen.c]
1:
2: /*
3: echo-server-fork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork版)
4: ~yas/syspro/ipc/echo-server-fork-fdopen.c
5: */
6: #include <stdio.h>
7: #include <stdlib.h> /* exit() */
8: #include <sys/types.h> /* socket(), wait4() */
9: #include <sys/socket.h> /* socket() */
10: #include <netinet/in.h> /* struct sockaddr_in */
11: #include <sys/resource.h> /* wait4() */
12: #include <sys/wait.h> /* wait4() */
13: #include <netdb.h> /* getnameinfo() */
14: #include <string.h> /* strlen() */
15: #include <unistd.h> /* getpid(), gethostname() */
16:
17: extern void echo_server( int portno, int ip_version );
18: extern void delete_zombie();
19: extern void echo_receive_request_and_send_reply( int com );
20: extern int echo_receive_request( char *line, size_t size, FILE *in );
21: extern void echo_send_reply( char *line, FILE *out );
22: extern void print_my_host_port( int portno );
23: extern void tcp_sockaddr_print( int com );
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 = 46; /* Both IPv4 and IPv6 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: tcp_sockaddr_print( acc );
57: while( 1 )
58: {
59: delete_zombie();
60: printf("[%d] accepting incoming connections (acc==%d) ...\n",
61: getpid(),acc );
62: if( (com = accept( acc,0,0 )) < 0 )
63: {
64: perror("accept");
65: exit( -1 );
66: }
67: tcp_peeraddr_print( com );
68: if( (child_pid=fork()) > 0 ) /* parent */
69: {
70: close( com );
71: }
72: else if( child_pid == 0 ) /* child */
73: {
74: close( acc );
75: echo_receive_request_and_send_reply( com );
76: exit( 0 );
77: }
78: else
79: {
80: perror("fork");
81: exit( -1 );
82: }
83: }
84: }
85:
delete_zombie() は、ゾンビ・プロセス(後述)を消去するものである。 (改善の余地がある。) echo サービスとしての処理は、fork() システムコールで分身を作り、子プロ セス側で行う。子プロセスで呼び出す echo_receive_request_and_send_reply() は、 echo-server-nofork-fdopen.cの echo_receive_request_and_send_reply() とまったく同じである。 親プロセスは、すぐにaccept() に戻る。
親プロセスは、次の処理を繰り返す。
getpid() は、自分自身の PID (Process ID (identifier)) を返すシステムコー ルである。PID (pid_t) は、32 ビット(システムによっては16ビット)の整数 である。実行結果で printf() の表示に、PID が表示されているが、同じではないことに 注意しなさい。
ゾンビ・プロセスも、ps コマンドで表示される。オペレーティング・システム のカーネルにあるプロセスの一覧表のエントリを消費している。
exec を伴う fork() では、wait するのが普通なので、ゾンビ・プロセスがす ぐに消える。シェルから実行されたプロセスは、シェルがすぐに wait して消 す。
インターネットのサーバの場合、いちいち wait したくないことが普通(個々の クライアントで独立して動きたい)。しかし、wait しないと、ゾンビ・プロセ スが溜まっていく一方になるので工夫して wait する。wait を怠れば、ゾンビ・ プロセスが溢れて、そのうちにプロセスが作れなくなる(fork() 失敗して、-1 を返すようになる。)
親が先に死ぬと、親プロセスは、PID 1 になる。 PID 1 のプロセスは、wait してゾンビを削除する。
86: void
87: delete_zombie()
88: {
89: pid_t pid ;
90:
91: while( (pid=wait4(-1,0,WNOHANG,0)) >0 )
92: {
93: printf("[%d] zombi %d deleted.\n",getpid(),pid );
94: continue;
95: }
96: }
97:
wait4() システムコールを使って、終了した子プロセスをwait してあげてい
る。ただし、WNOHANG オプションを付けてあるので、終了した子プロセスがい
なければ、待たずに返ってくる。
+」は、追加された行、
「-」は、削除された行、
「!」は、修正された行を意味する。
$ diff -c echo-server-nofork-fdopen.c echo-server-fork-fdopen.c
*** echo-server-nofork-fdopen.c 2023-06-20 11:32:17.840379552 +0900
--- echo-server-fork-fdopen.c 2023-06-20 11:32:17.852379674 +0900
***************
*** 1,7 ****
/*
! echo-server-nofork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork無し版)
! ~yas/syspro/ipc/echo-server-nofork-fdopen.c
*/
#include <stdio.h>
#include <stdlib.h> /* exit() */
--- 1,7 ----
/*
! echo-server-fork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork版)
! ~yas/syspro/ipc/echo-server-fork-fdopen.c
*/
#include <stdio.h>
#include <stdlib.h> /* exit() */
***************
*** 15,20 ****
--- 15,21 ----
#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 )
***************
*** 54,59 ****
--- 56,62 ----
tcp_sockaddr_print( acc );
while( 1 )
{
+ delete_zombie();
printf("[%d] accepting incoming connections (acc==%d) ...\n",
getpid(),acc );
if( (com = accept( acc,0,0 )) < 0 )
***************
*** 62,68 ****
exit( -1 );
}
tcp_peeraddr_print( com );
! echo_receive_request_and_send_reply( com );
}
}
--- 65,97 ----
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 azalea16 1231
[576556] accepting (fd==3) to [::]:1231
[576556] accepting incoming connections (acc==3) ...
[576556] connection (fd==4) from [2001:2f8:3a:1711::231:16]:40086
[576556] accepting incoming connections (acc==3) ...
[576786] received (fd==4) 5 bytes, [012
]
[576556] connection (fd==4) from [2001:2f8:3a:1711::231:16]:58222
[576556] accepting incoming connections (acc==3) ...
[577021] received (fd==4) 5 bytes, [abc
]
[576786] received (fd==4) 5 bytes, [345
]
[577021] received (fd==4) 5 bytes, [def
]
[576786] received (fd==4) 5 bytes, [789
]
[576786] connection (fd==4) closed.
[577021] connection (fd==4) closed.
クライアント側(その1)。
$ telnet azalea16.coins.tsukuba.ac.jp 1231
Trying 2001:2f8:3a:1711::231:16...
Connected to azalea16.coins.tsukuba.ac.jp.
Escape character is '^]'.
012
012
345
345
^]
789
789
^]
telnet> ^D
Connection closed.
$
クライアント側(その2)。
$ telnet azalea16.coins.tsukuba.ac.jp 1231
Trying 2001:2f8:3a:1711::231:16...
Connected to azalea16.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 azalea16 1231
[576556] accepting (fd==3) to [::]:1231
[576556] accepting incoming connections (acc==3) ...
[576556] connection (fd==4) from [2001:2f8:3a:1711::231:16]:40086
[576556] accepting incoming connections (acc==3) ...
[576786] received (fd==4) 5 bytes, [012
]
[576556] connection (fd==4) from [2001:2f8:3a:1711::231:16]:58222
[576556] accepting incoming connections (acc==3) ...
[577021] received (fd==4) 5 bytes, [abc
]
[576786] received (fd==4) 5 bytes, [345
]
[577021] received (fd==4) 5 bytes, [def
]
[576786] received (fd==4) 5 bytes, [789
]
[576786] connection (fd==4) closed.
[577021] connection (fd==4) closed.
ps コマンドで見ると、状態(STAT) がゾンビを意味する Z と表示される。
$ ps l | grep echo
0 1013 576556 528849 20 0 2776 944 inet_c S+ pts/3 0:00 ./echo-server-fork-fdopen 1231
1 1013 576786 576556 20 0 0 0 - Z+ pts/3 0:00 [echo-server-for]
1 1013 577021 576556 20 0 0 0 - Z+ pts/3 0:00 [echo-server-for]
0 1013 581798 580312 20 0 21188 2200 pipe_r S+ pts/5 0:00 grep echo
$
echo-server-fork-fdopen.c では、
delete_zombie() でゾンビを消そうとはしている。しかし、accept() で止まっ
ている状態なので、どこかのクライアントから接続要求が来ない限りは、
delete_zombie() が呼ばれないので、ゾンビとして残っている。
この状態で接続要求が来ると、ゾンビが回収される。 クライアント
$ telnet azalea16.coins.tsukuba.ac.jp 1231
Trying 2001:2f8:3a:1711::231:16...
Connected to azalea16.coins.tsukuba.ac.jp.
Escape character is '^]'.
サーバ側
[576556] connection (fd==4) from [2001:2f8:3a:1711::231:16]:42974
[576556] zombi 576786 deleted.
[576556] zombi 577021 deleted.
[576556] accepting incoming connections (acc==3) ...
ゾンビが回収された後の ps コマンドの結果:
$ ps l | grep echo
0 1013 576556 528849 20 0 2776 944 inet_c S+ pts/3 0:00 ./echo-server-fork-fdopen 1231
1 1013 581860 576556 20 0 2776 96 wait_w S+ pts/3 0:00 ./echo-server-fork-fdopen 1231
0 1013 582039 580312 20 0 21188 2196 pipe_r S+ pts/5 0:00 grep echo
$
Firefox 等のブラウザで画像が表示できることを確認し、 そのことをレポートに記載しなさい。スクリーンショットは不用である。 実行結果として、サーバ側で、複数同時に受け付けていることを示すログをつ けなさい。複数接続できることを確認するには、telnet のように遅いクライア ントを用いるとよい。単に Web ブラウザで複数のページを開けても、通信その ものは短時間で終わるので、複数接続できることの確認にはならない。
注意: 上限を設定する目的は、攻撃等でサーバの CPU 時間が奪われることやプ ロセスが増えすぎることを防ぐことにある。プロセスの終了を busy wait で待っ てはならない。待つならば、accept() や wait() のように、CPU 時間を消費し ない形で待つようにしなさい。
ヒント: 最大数まで fork() したら、accept() をやめて wait して子プロセスを待つ方法が考えられる。
Keep-Alive に対応しなさい。
ヒント:子プロセスの終了を、 シグナル SIGCHLD で知る方法もある。複数の子プロセスが終了しても、シグナルは1回 しか送られないことがあることに注意しなさい。
シグナルを使うと、accept() システムコールがエラーで戻って来るシステムも ある。その場合、エラー番号が EINTR なら単純に終了しないで、再び accept() に向うべきである。
この課題では、複数のクライアントに対するサービスの同時提供を実現するた めに、fork() を使いなさい。select() や Pthread を用いてはならない。
ヒント: IPv4 と IPv6 専用のプロセスやスレッドを作成する。
ヒント: IPv4 の acc と IPv6 の acc を2つ作成し、 両方同時に監視する。
なお同時に扱えるクライアント数は、IPv4 と IPv6 で個別に数えても 統合して数えてもどちらでも良い。たとえば、最大2と設定した場合、 個別に数えて IPv4 で 2, IPv6 で 2、合計 4 としてもよい。