对输出消息感到困惑(关于信号处理程序和 sigsuspend)

Confused about the output message (about signal handler and sigsuspend)

为了更好地理解sigsuspend 的概念,我做了如下两个修改,得到了不同的输出信息,这让我很困惑。

The code is from csapp Chapter8 figure 8-42 about sigsuspend.

  1. 添加第 10 行 printf("Reap child %d\n", pid);
   /* $begin sigsuspend */
   #include "csapp.h"
  
   volatile sig_atomic_t pid;
  
   void sigchld_handler(int s)
   {
       int olderrno = errno;
       pid = Waitpid(-1, NULL, 0);
       printf("Reap child %d\n", pid); //Line 10
       errno = olderrno;
  }
 
  void sigint_handler(int s)
  {
  }
 
  int main(int argc, char **argv)
  {
      sigset_t mask, prev;
 
      Signal(SIGCHLD, sigchld_handler);
      Signal(SIGINT, sigint_handler);
      Sigemptyset(&mask);
      Sigaddset(&mask, SIGCHLD);
 
      while (1) {
          Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
          if (Fork() == 0) /* Child */
              exit(0);
          /* Wait for SIGCHLD to be received */
          pid = 0;
          while (!pid)
              Sigsuspend(&prev);
 
          /* Optionally unblock SIGCHLD */
          Sigprocmask(SIG_SETMASK, &prev, NULL);
 
          /* Do some work after receiving SIGCHLD */
          printf(".");
      }
      exit(0);
  }
  /* $end sigsuspend */

在另一个终端用kill -9 processID杀掉进程,得到如下信息,搞得我很疑惑,为什么每个Reap child前都有两个.

Reap child 11880
..Reap child 11881
..Reap child 11882
..Reap child 11883
..Reap child 11884
..Reap child 11885
..Reap child 11886
..Reap child 11887
..Reap child 11888
..Reap child 11889
..Reap child 11890
..Reap child 11891
..Reap child 11892
..Reap child 11893
..Reap child 11894
..Reap child 11895
..Reap child 11896
..Reap child 11897
..Reap child 11898
..Reap child 11899
..Reap child 11900
Killed

然后,添加第 30 行 printf("Create child %d\n", getpid());

   /* $begin sigsuspend */
   #include "csapp.h"
  
   volatile sig_atomic_t pid;
  
   void sigchld_handler(int s)
   {
       int olderrno = errno;
       pid = Waitpid(-1, NULL, 0);
      printf("Reap child %d\n", pid);
      errno = olderrno;
  }
 
  void sigint_handler(int s)
  {
  }
 
  int main(int argc, char **argv)
  {
      sigset_t mask, prev;
 
      Signal(SIGCHLD, sigchld_handler);
      Signal(SIGINT, sigint_handler);
      Sigemptyset(&mask);
      Sigaddset(&mask, SIGCHLD);
 
      while (1) {
          Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
          if (Fork() == 0) {/* Child */
              printf("Create child %d\n", getpid()); //Line 30
              exit(0);
          }
          /* Wait for SIGCHLD to be received */
          pid = 0;
          while (!pid)
              Sigsuspend(&prev);
 
          /* Optionally unblock SIGCHLD */
          Sigprocmask(SIG_SETMASK, &prev, NULL);
 
          /* Do some work after receiving SIGCHLD */
          printf(".");
      }
      exit(0);
  }
  /* $end sigsuspend */

在另一个终端使用kill -9 processID终止进程,得到的消息正是我想象的。

Create child 15080
Reap child 15080
.Create child 15081
.Reap child 15081
.Create child 15082
.Reap child 15082
.Create child 15083
.Reap child 15083
.Create child 15084
.Reap child 15084
.Create child 15085
Killed

为什么仅仅加了printf行就会有这样的差别?

点的问题是缓冲问题。

stdout 连接到交互式终端(即 shell)时,它默认为 行缓冲 ,这意味着输出实际上是写入的(刷新)换行。

因为你打印

printf(".");

没有换行符,它保存在内部 stdout 缓冲区中,当您打印换行符时(就像在 sigchld_handler 函数中),进程退出或缓冲区变为已满

现在是重要的部分:这个 stdout 缓冲区由子进程继承!

所以当子进程退出时它会刷新缓冲区并打印一个点。并且将调用信号处理程序打印换行符,同时刷新缓冲区并打印一个点。

您可以通过在打印点后显式刷新缓冲区来解决此问题:

printf(".");
fflush(stdout);

执行此操作后,当下一个子进程被派生时缓冲区将为空。


如果仔细观察“working”输出,您仍然有两个点,一个在“Create child”输出之前由子进程打印,另一个在“Reap child”输出之前由父进程打印.