マルチプロセスブロックI/Oサーバ設計


UnixネットワークプログラミングにおけるTCPクライアント/サーバプログラム例の第1例は,マルチプロセスのブロックI/Oを用いて設計されたサーバであるが,ここで注意すべき点をメモする.
サーバ側のソース:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <netinet/in.h>
#include <errno.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>

void sig_chld(int signo)
{
    pid_t pid;
    int stat;
 
    while( (pid = waitpid(-1, &stat, WNOHANG)) > 0 )
    {  
        printf("child %d terminated
", pid); } return ; } void str_echo(int sockfd) { ssize_t n; char buff[50000]; again: while( (n = read(sockfd, buff ,sizeof(buff))) > 0 ) { write(sockfd, buff, n); } if(n <0 && errno == EINTR) goto again; else if(n < 0) perror("read error"); } int main() { int listenfd, connfd; pid_t childpid; socklen_t client; struct sockaddr_in clientaddr; struct sockaddr_in serveraddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); //bzero(&serveraddr, sizeof(serveraddr)); memset(&serveraddr, 0, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(3333); serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)); listen(listenfd, 5); signal(SIGCHLD, sig_chld); for( ; ; ) { client = sizeof(clientaddr); if( (connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &client)) < 0 ) { if(errno == EINTR) continue; else perror("accept error"); } if( (childpid = fork()) == 0 ) { close(listenfd); str_echo(connfd); close(connfd); exit(0); } close(connfd); } }

問題1:サブプロセス回収問題
サーバは、クライアントからの接続要求を受信して接続を確立するたびに、サブプロセスを作成してクライアントとのインタラクションを実行し、親プロセスはクライアントの接続要求をリスニングし続けます.サブプロセスの実行が完了して終了した後、親プロセスが回収されていない場合、サブプロセスはゾンビプロセスになり、はい、リソースは解放されません.
親プロセスがサブプロセスリソースを回収する方法はwaitまたはwaitpid関数を呼び出すことです.ただし、親プロセスのループでwait関数を直接使用すると、親プロセスがその関数にブロックされ、クライアント接続要求を受信できなくなります.waitpid関数は非ブロックモードに設定できますが、親プロセスがaccept関数にブロックされている場合、この時点で終了したサブプロセスは回収できません.
解決策は信号処理関数を利用することである.サブプロセスが終了すると、カーネルはサブプロセスの親プロセスにSIGCHLD信号を送信します.親プロセスは、信号をキャプチャし、信号処理関数を呼び出してsig_を処理します.chld()関数でいいです.sig_chld設計ではwait関数ではなくwaitpid関数が使用されます.wait関数が非ブロックに設定できないためです.複数のサブプロセスがほぼ同時に終了すると、親プロセスはいずれかの信号をキャプチャし、信号処理関数を実行する過程で、他のSIGCHLD信号は無視され、大量のゾンビプロセスが発生するためsig_chld()関数はwileループを使用して、同時に終了する複数のサブプロセスを回収します.
void sig_chld(int signo)
{
    pid_t pid;
    int stat;
 
    while( (pid = waitpid(-1, &stat, WNOHANG)) > 0 )
    {  
        printf("child %d terminated
", pid); } return ; }

WNOHANGはwaitpid関数が非ブロックであることを設定し、関数呼び出し時に終了したサブプロセスを使用しない場合は0を返し、ループが終了し、プログラムが中断開始を実行し続ける場所を設定します.複数のサブプロセスが終了すると、ループはwaitpid関数を複数回実行し、ゾンビプロセスの発生を回避します.
signal関数を使用して信号をキャプチャすると、割り込みが発生します.親プロセスがaccept関数でブロックされている場合、割り込みはaccept関数に-1を返し、errnoをEINTRに設定する可能性があります.ここでは、スローシステム呼び出しの基本ルールについて説明する.スローシステム呼び出しにブロックされたプロセスが信号をキャプチャし、対応する信号処理関数が返されると、システム呼び出しはEINTRエラーを返す可能性がある.一部のカーネルは、中断されたシステム呼び出しを自動的に再起動し、一部のカーネルは、自分でプログラムを作成してシステム呼び出しを再起動する必要があります.
for( ; ; )
    {
        client = sizeof(clientaddr);
        if( (connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &client)) < 0 )
        {
	    if(errno == EINTR)
    		continue;
	    else
		perror("accept error");
	}

再起動可能なシステム呼び出しはread、write、select、openなどである.connect関数は再起動できません.ソケットを再作成します.