处理SIGCHLD信号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void sig_chld(int signo) {
    pid_t pid;
    int stat;

    // wait()是为了清理僵死进程
    // pid = wait(&stat);
    // printf("child %d terminated\n", pid);

    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) 
        printf("child %d terminated\n", pid);
    return;
}

wait版本行不通的原因

若服务器和客户端在同一主机上,则该信号处理函数只执行过一次(根据UNP的描述是这样的,但是本人实测,信号处理器函数执行了两次)。 但是如果我们在不同的主机上运行客户和服务器,那么信号处理器函数一般执行两次:一次是由第一个产生的信号引起的,由于另外4个信号在信号处理函数第一次执行时发生,因为该处理函数仅仅再被调用一次,从而留下3个僵死进程。 不过有的时候,根据FIN到达主机的时机,信号处理函数可能会执行3次甚至4次

根据本人猜想,如果在相同主机测试,由于没有网络数据传播时延的影响,本机的客户端所有五个FIN都几乎在同一时间传递给服务器, 从而使得服务器的5个子进程基本在同一时刻终止,从而5个SIGCHLD在同一时刻发送给父进程,都在第一个信号处理函数执行之前发生, 而Unix信号一般是不排队的(这里详见TLPI对应的小节),因此信号处理函数只执行一次。

但是如果在不同主机测试,由于网络数据传播,各个FIN到达服务器的时间差较大一些,导致FIN以不可忽略的时差到达服务器。 例如在处理第一个SIGCHLD信号时,其他FIN才到达,从而引起信号处理函数执行多次。

1
2
3
4
5
6
7
➜  bin git:(main) ✗ ./TCPSERV03 &       
[1] 65001
➜  bin git:(main) ✗ ./TCPCLI04 127.0.0.1
hell
hell
child 65324 terminated                                   
child 65325 terminated

waitpid()版本行得通

清理僵尸进程

1
2
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
    printf("child %d terminated\n", pid);
  • waitpid(-1, &stat, WNOHANG):这部分代码是关键。waitpid 函数用于等待子进程的状态改变。

    • pid = -1:表示等待任何子进程。
    • &stat:是一个指向 int 的指针,用于存储子进程的终止状态。
    • WNOHANG:这个选项告诉 waitpid 非阻塞运行。如果没有子进程终止,waitpid 将立即返回,而不是阻塞等待。
  • while 循环:这个循环会一直执行,直到没有子进程终止为止。waitpid 返回值会是:

    • 大于 0 的值:表示终止的子进程的 PID,表明有一个子进程结束。
    • 0:表示没有子进程终止。
    • -1:表示没有更多的子进程或者调用失败。

代码总结

这段代码实现了一个信号处理函数 sig_chld,用于处理 SIGCHLD 信号。当一个子进程终止时,父进程会收到 SIGCHLD 信号,触发这个处理函数。该函数使用 waitpid 结合 WNOHANG 选项来处理所有终止的子进程,并避免产生僵尸进程。僵尸进程会占用系统资源,因此在接收到 SIGCHLD 信号时及时清理子进程的退出状态是很重要的。

这种处理方式确保了父进程可以继续处理多个子进程的终止,而不会因为某个子进程的终止而导致阻塞,从而避免产生僵尸进程。

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
➜  bin git:(main) ✗ ./TCPSERV04 &       
[1] 56786
➜  bin git:(main) ✗ ./TCPCLI04 127.0.0.1
htl
htl
child 56992 terminated                                   
child 56993 terminated
child 56994 terminated
child 56995 terminated
child 56996 terminated

不在循环内部使用wait()的原因

在信号处理函数中使用 waitpid() 而不是 wait() 的原因主要涉及到以下几个方面:

1. 避免遗漏子进程的终止

  • waitpid() 在循环中与 WNOHANG 选项一起使用,可以确保所有已终止的子进程都被处理。因为 waitpid() 在每次调用时返回一个已终止的子进程的 PID,当没有更多子进程终止时返回 0。通过循环调用 waitpid(),你可以确保每个已终止的子进程都被正确处理,从而避免遗漏。

  • 相比之下,wait() 只能一次等待一个子进程终止。如果有多个子进程在短时间内终止,而 wait() 只被调用一次,可能会遗漏处理其中的一些子进程。这些未处理的子进程就会成为僵尸进程,浪费系统资源。

2. 非阻塞的等待

  • waitpid()WNOHANG 选项结合使用是非阻塞的,它允许信号处理程序检查所有子进程是否终止,而不会因没有终止的子进程而阻塞。这样,信号处理函数可以迅速返回,继续处理其他任务。

  • 在没有循环时,调用wait()不会导致出现阻塞等待,因为当进入该处理函数时,说明必然有函数终止了,只不过是1个还是多个的问题 无论是一个还是多个,wait()都只能处理掉一个,然后返回,退出处理器函数

  • 如果在循环中调用 wait(),一旦没有子进程终止,wait() 就会阻塞,这会导致整个信号处理程序挂起,不能立即返回。 例如,第一个进程终止后,父进程进入了处理器函数,处理了第一个进程之后,便会继续循环等待下一个终止进程, 若是下一个进程迟迟不进入终止状态,则父进程则需要一直阻塞等待。 阻塞在信号处理程序中通常是不被推荐的,因为这可能导致系统其他部分的延迟或不良的响应时间。

3. 处理多个子进程的终止

  • 在某些情况下,多个子进程可能在非常短的时间内几乎同时终止。如果仅调用 wait(),信号处理程序可能只能处理一个子进程的终止。使用 waitpid() 的循环可以确保处理所有已终止的子进程。

4. 控制和灵活性

  • waitpid() 提供了更多的控制和灵活性。例如,使用 waitpid() 可以指定等待特定的子进程,或根据不同的选项处理子进程。相比之下,wait() 的功能比较有限,只能简单地等待任意一个子进程的终止。