一、wait、waitpid函数

函数返回值利用

  • 函数wait和waitpid均返回两个值:已终止子进程的进程ID号,以及通过statloc指针返回 的子进程终止状态(一个整数)
  • 我们可以调用三个宏来检查终止状态,并辨别子进程是正常终 止、由某个信号杀死还是仅仅由作业控制停止而已。另有些宏用于接着获取子进程的退出状态、 杀死子进程的信号值或停止子进程的作业控制信号值。在域套接字传递描述符的文章中中,我们将为此目的使用宏 WIFEXITED和WEXITSTATUS

两个函数的区别

  • 如果调用wait的进程没有已终止的子进程,不过有一个或多个子进程仍在执行,那么wait 将阻塞到现有子进程第一个终止为止
  • waitpid函数就等待哪个进程以及是否阻塞给了我们更多的控制
二、wait、waitpid使用起来的区别

第一步

  • 为了演示,我们把TCP客户程序修改为下面代码所示
int
main(int argc, char **argv)
{
    int i, sockfd[5];
    struct sockaddr_in servaddr;

    if (argc != 2)
        err_quit("usage: tcpcli <IPaddress>");

    for (i = 0; i < 5; i++) {
        sockfd[i] = Socket(AF_INET, SOCK_STREAM, 0);

        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(SERV_PORT);
        Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
        Connect(sockfd[i], (SA *) &servaddr, sizeof(servaddr));
    }

    str_cli(stdin, sockfd[0]); /* do it all */
    exit(0);
} 
  • 客户建立5个与服务器的连接,随后在调用str_cli函数时仅用第一个连接(sockfd[0])。建立多个连接的目的是从并发服务器上派生多个子进程,如下图

UNP编程:10---TCP之(子进程终止处理:wait、waitpid)_wait

第二步

  • 如果此时5个客户端同时断开
  • 当客户终止时,所有打开的描述符由内核自动关闭(我们不调用close,仅调用exit),且 所有5个连接基本在同一时刻终止。这就引发了5个FIN,每个连接一个,它们反过来使服务器 的5个子进程基本在同一时刻终止。这又导致差不多在同一时刻有5个SIGCHLD信号递交给父进 程

UNP编程:10---TCP之(子进程终止处理:wait、waitpid)_子进程终止处理_02

第三步

  • 如果服务端使用sig_chld信号处理函数(见文章:javascript:void(0))调用wait函数来处理子进程
  • 建立一个信号处理函数并在其中调用wait并不足以防止出现僵死进程。本问题在于:
    • 所有5个信号都在信号处理函数执行之前产生,而信号处理函数只执行一次,因为Unix信号一般是不排队的
    • 更严重的是,本问题是不确定的。在我们刚刚运行的例子中,客户与服务器在同一个 主机上,信号处理函数执行1次,留下4个僵死进程。但是如果我们在不同的主机上运行客户和 服务器,那么信号处理函数一般执行2次:一次是第一个产生的信号引起的,由于另外4个信号 在信号处理函数第一次执行时发生,因此该处理函数仅仅再被调用一次,从而留下3个僵死进程。 不过有的时候,依赖于FIN到达服务器主机的时机,信号处理函数可能会执行3次甚至4次

第四步

  • 此时我们就需要改进sig_chld函数调用waitpid来处理子进程
  • 正确的解决办法是调用waitpid而不是wait,下面代码给出了正确处理SIGCHLD的sig_chld函 数
  • 这个版本管用的原因在于:我们在一个循环内调用waitpid,以获取所有已终止子进程的状 态。我们必须指定WNOHANG选项,它告知waitpid在有尚未终止的子进程在运行时不要阻塞。我们前面文章介绍的sig_chld信号处理函数(见文章:javascript:void(0))不能在循环内调用wait,因为没有办法防止wait在正运行的子进程尚有未终止时阻塞
void
sig_chld(int signo)
{
    pid_t pid;
    int stat;
    while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
        printf("child %d terminated\n", pid);
    return;
}
三、最终程序

下面给出了我们的服务器程序的最终版本。它正确处理accept返回的EINTR,并建立一个给所有已终止子进程调用waitpid的信号处理函数

  • 当fork子进程时,必须捕获SIGCHLD信号
  • 当捕获信号时,必须处理被中断的系统调用
  • SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数以免留下僵死进程
int
main(int argc, char **argv)
{
	int					listenfd, connfd;
	pid_t				childpid;
	socklen_t			clilen;
	struct sockaddr_in	cliaddr, servaddr;
	void				sig_chld(int);

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);

	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

	Listen(listenfd, LISTENQ);

	Signal(SIGCHLD, sig_chld);	/* must call waitpid() */

	for ( ; ; ) {
		clilen = sizeof(cliaddr);
		if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
			if (errno == EINTR)
				continue;		/* back to for() */
			else
				err_sys("accept error");
		}

		if ( (childpid = Fork()) == 0) {	/* child process */
			Close(listenfd);	/* close listening socket */
			str_echo(connfd);	/* process the request */
			exit(0);
		}
		Close(connfd);			/* parent closes connected socket */
	}
}