前言
谈到等待子进程,首先想到的就是 SIGCHLD 信号与 wait 函数族,本文试图厘清二者的方方面面,以及组合使用时可能的坑。
单独使用 SIGCHLD 的场景
使用 signal 捕获信号
下面是一段典型的代码片段:
代码语言:javascript复制 1 #include "../apue.h"
2 #include <sys/wait.h>
3
4 #define CLD_NUM 2
5 static void sig_cld (int signo)
6 {
7 pid_t pid = 0;
8 int status = 0;
9 printf ("SIGCHLD receivedn");
10 if (signal (SIGCHLD, sig_cld) == SIG_ERR)
11 perror ("signal error");
12 if ((pid = wait (&status)) < 0)
13 perror ("wait(in signal) error");
14 printf ("pid (wait in signal) = %dn", pid);
15 }
16
17 int main ()
18 {
19 pid_t pid = 0;
20 __sighandler_t ret = signal (SIGCHLD, sig_cld);
21 if (ret == SIG_ERR)
22 perror ("signal error");
23 else
24 printf ("old handler %xn", ret);
25
26 for (int i=0; i<CLD_NUM; i)
27 {
28 if ((pid = fork ()) < 0)
29 perror ("fork error");
30 else if (pid == 0)
31 {
32 sleep (3);
33 printf ("child %u exitn", getpid ());
34 _exit (0);
35 }
36
37 sleep (1);
38 }
39
40 for (int i=0; i<CLD_NUM; i)
41 {
42 pause ();
43 printf ("wake up by signal %dn", i);
44 }
45
46 printf ("parent exitn");
47 return 0;
48 }
父进程启动了两个子进程,在 SIGCHLD 信号处理器中调用 wait 等待已结束的子进程,回收进程信息,防止产生僵尸进程 (zombie)。上面的代码会有如下的输出:
代码语言:javascript复制old handler 0
child 28542 exit
SIGCLD received
pid (wait in signal) = 28542
wake up by signal 0
child 28543 exit
SIGCLD received
pid (wait in signal) = 28543
wake up by signal 1
parent exit
使用 sigaction 捕获信号
当然捕获 SIGCHLD 也可以使用 sigaction 函数:
代码语言:javascript复制 1 #include "../apue.h"
2 #include <sys/wait.h>
3
4 #define CLD_NUM 2
5 static void sig_cld (int signo, siginfo_t *info, void* param)
6 {
7 int status = 0;
8 if (signo == SIGCHLD)
9 {
10 if (info->si_code == CLD_EXITED ||
11 info->si_code == CLD_KILLED ||
12 info->si_code == CLD_DUMPED)
13 {
14 //printf ("child %d dien", info->si_pid);
15 if (waitpid (info->si_pid, &status, 0) < 0)
16 perror ("wait(in signal) error");
17 printf ("pid (wait in signal) = %dn", info->si_pid);
18 }
19 else
20 {
21 printf ("unknown signal code %dn", info->si_code);
22 }
23 }
24 }
25
26 int main ()
27 {
28 pid_t pid = 0;
29 struct sigaction act;
30 sigemptyset (&act.sa_mask);
31 act.sa_sigaction = sig_cld;
32 act.sa_flags = SA_SIGINFO | SA_NOCLDSTOP;
33 int ret = sigaction (SIGCHLD, &act, 0);
34 if (ret == -1)
35 perror ("sigaction error");
36
37 for (int i=0; i<CLD_NUM; i)
38 {
39 if ((pid = fork ()) < 0)
40 perror ("fork error");
41 else if (pid == 0)
42 {
43 sleep (3);
44 printf ("child %u exitn", getpid ());
45 _exit (0);
46 }
47
48 sleep (1);
49 }
50
51 for (int i=0; i<CLD_NUM; i)
52 {
53 pause ();
54 printf ("wake up by signal %dn", i);
55 }
56
57 printf ("parent exitn");
58 return 0;
59 }
输出是一样的。关于 signal 与 sigaction 的区别,有以下几点:
- 使用 sigaction 可以避免重新安装信号处理器的问题;
- 使用 sigaction 可以在 wait 之前得知是哪个子进程结束了。这是通过指定 SA_SIGINFO 标志位,并提供带 siginfo_t 参数的信号处理器来实现的 (info->si_pid 就是结束进程的进程号);
- 使用 sigaction 可以获取除子进程结束以外的状态变更通知,例如挂起、继续,默认接收相应通知,除非指定 SA_NOCLDSTOP 标志。而对于 signal 而言,没有办法不接收子进程非结束状态的通知 (此时调用 wait 可能会卡死);
- 使用 sigaction 可以自动 wait 已结束的子进程,只要指定 SA_NOCLDWAIT 标志即可。此时在信号处理器中不用再调用 wait 函数了。
当使用 SA_NOCLDWAIT 标志位时,使用 systemtap 还是可以观察到子进程还是向父进程发送了 SIGCHLD 信号的:
代码语言:javascript复制30049 cldsig 30048 cldsig 17 SIGCHLD
30050 cldsig 30048 cldsig 17 SIGCHLD
很有可能是系统内部自动 wait 了相关子进程。另外在使用 SA_NOCLDWAIT 时,可以不指定信号处理器,此时 sa_sigaction 字段可以设置为 SIG_DFL。关于 SIGCHLD 信号,有以下几点需要注意:
- 如果在注册信号之前,就已经有已结束但未等待的子进程存在,则事件不会被触发;
- 可以为 SIGCHLD 注册一个处理器,也可以忽略该信号 (SIG_IGN),忽略时系统自动回收已结束的子进程;
当正常捕获 SIGCHLD 时,使用 systemtap 是可以观察到子进程向父进程发送的 SIGCHLD 信号的:
代码语言:javascript复制29877 cldsig 29876 cldsig 17 SIGCHLD
29878 cldsig 29876 cldsig 17 SIGCHLD
29876 cldsig 27771 bash 17 SIGCHLD
当忽略 SIGCHLD 时,是观察不到的,只能看到父进程结束时向 bash 发送的 SIGCHLD 信号:
代码语言:javascript复制29893 cldsig 27771 bash 17 SIGCHLD
这里注意一下二者在细节处的一点区别。
- 还有一个 SIGCLD 信号 (看清楚,只差了一个字母),在大多数 unix like 系统中与 SIGCHLD 表现一致,在某些古老的 unix 系统上,可能有独特的表现需要注意,这方面请参考 apue 第十章第七节
在我的环境 (CentOS 6.7) 该信号被定义为 SIGCHLD,因此是完全等价的。
屏蔽信号
关于使用信号等待子进程,最后需要说的一点就是信号的竞争行为,对上面的例子稍加修改,就可以演示一下:
代码语言:javascript复制 1 #include "../apue.h"
2 #include <sys/wait.h>
3
4 #define CLD_NUM 2
5 void pid_remove (pid_t pid)
6 {
7 printf ("remove pid %un", pid);
8 }
9 void pid_add (pid_t pid)
10 {
11 printf ("add pid %un", pid);
12 }
13
14 static void sig_cld (int signo)
15 {
16 pid_t pid = 0;
17 int status = 0;
18 printf ("SIGCHLD receivedn");
19 if (signal (SIGCHLD, sig_cld) == SIG_ERR)
20 perror ("signal error");
21 if ((pid = wait (&status)) < 0)
22 perror ("wait(in signal) error");
23 printf ("pid (wait in signal) = %dn", pid);
24 pid_remove (pid);
25 }
26
27 int main ()
28 {
29 pid_t pid = 0;
30 __sighandler_t ret = signal (SIGCHLD, sig_cld);
31 if (ret == SIG_ERR)
32 perror ("signal error");
33 else
34 printf ("old handler %xn", ret);
35
36 for (int i=0; i<CLD_NUM; i)
37 {
38 if ((pid = fork ()) < 0)
39 perror ("fork error");
40 else if (pid == 0)
41 {
42 //sleep (3);
43 printf ("child %u exitn", getpid ());
44 _exit (0);
45 }
46
47 sleep (1);
48 pid_add (pid);
49 }
50
51 sleep (1);
52 printf ("parent exitn");
53 return 0;
54 }
父进程在启动子进程后需要将它的信息通过 pid_add 添加到某种数据结构中,当收到 SIGCHLD 信号后,又通过 pid_remove 将它从这个数据结构中移出。在上面的例子中,子进程一启动就退出了,快到甚至父进程还没有来得及执行 pid_add 就先执行了 pid_remove,这很容易导致潜在的问题。(注意,为了能更好的呈现信号竞争的问题,这里故意在父进程 sleep 之后调用 pid_add),执行结果如下:
代码语言:javascript复制old handler 0
child 31213 exit
SIGCLD received
pid (wait in signal) = 31213
remove pid 31213
add pid 31213
child 31214 exit
SIGCLD received
pid (wait in signal) = 31214
remove pid 31214
add pid 31214
parent exit
可以看到,remove 总是在 add 之前执行。而解决方案也很直接,就是在 pid_add 完成之前,我们需要屏蔽 SIGCHLD 信号:
代码语言:javascript复制 1 for (int i=0; i<CLD_NUM; i)
2 {
3 sigset_t mask;
4 sigemptyset(&mask);
5 sigaddset(&mask, SIGCHLD);
6 sigprocmask(SIG_BLOCK, &mask, NULL);
7 if ((pid = fork ()) < 0)
8 perror ("fork error");
9 else if (pid == 0)
10 {
11 sigprocmask(SIG_UNBLOCK, &mask, NULL);
12 //sleep (3);
13 printf ("child %u exitn", getpid ());
14 _exit (0);
15 }
16
17 sleep (1);
18 pid_add (pid);
19 sigprocmask(SIG_UNBLOCK, &mask, NULL);
20 }
这里用到了 sigprocmask 去屏蔽以及解除某种信号的屏蔽。新的代码运行结果如下:
代码语言:javascript复制old handler 0
child 31246 exit
add pid 31246
SIGCLD received
pid (wait in signal) = 31246
remove pid 31246
child 31247 exit
SIGCLD received
pid (wait in signal) = 31247
remove pid 31247
add pid 31247
parent exit
可以看到一切正常了,add 这次位于 remove 之前。总结一下,使用 SIGCHLD 信号适合异步等待子进程的场景,并且通常搭配 wait 来回收子进程。
单独使用 wait 函数族的场景
典型代码如下:
代码语言:javascript复制 1 #include "../apue.h"
2 #include <sys/wait.h>
3
4 #define CLD_NUM 2
5 int main ()
6 {
7 pid_t pid = 0;
8 for (int i=0; i<CLD_NUM; i)
9 {
10 if ((pid = fork ()) < 0)
11 perror ("fork error");
12 else if (pid == 0)
13 {
14 sleep (3);
15 printf ("child %u exitn", getpid ());
16 _exit (0);
17 }
18
19 sleep (1);
20 }
21
22 int status = 0;
23 for (int i=0; i<CLD_NUM; i)
24 {
25 if ((pid = wait (&status)) < 0)
26 perror ("wait error");
27
28 printf ("pid = %dn", pid);
29 }
30
31 printf ("parent exitn");
32 return 0;
33 }
与之前场景不同的是,这里父进程同步等待启动的子进程结束。上面的代码会有如下输出:
代码语言:javascript复制child 28583 exit
child 28584 exit
pid = 28583
pid = 28584
parent exit
关于wait函数族,需要注意以下几点:
- wait 用于等待任何一个子进程,相当于 waitpid (-1, status, 0); 当没有任何子进程存在时,返回 -1,errno 设置为 ECHILD;
- waitpid 相对于 wait 的优势在于:
- 可以指定子进程 (组) 来等待;
- 可以捕获子进程除结束以外的其它状态变更通知,如挂起 (WUNTRACED)、继续 (WCONTINUED) 等;
- 可以不阻塞的测试某个子进程是否已结束 (WNOHANG);
- wait 函数族可被信号中断,此时返回 -1,errno 设置为 EINTR,必要时需要重启 wait;
总结一下,使用 wait 函数族适合同步等待子进程,例如某种命令执行器进程,通常配合 waitpid 来回收子进程。
混合使用同步 wait 与异步 wait 函数族的场景
其实前面已经提到 SIGCHLD 要搭配 wait 使用,但那是异步使用 wait 的单一场景,而这里讲的混合,是指同时在信号处理器与执行流程中使用 wait。例如 bash,它除了在主线程中同步等待前台正在运行的子进程,还必需在信号处理器中异步接收后台运行子进程的状态反馈,这样就不得不混合使用 wait。同步等待某个子进程一般使用 waitpid,而在信号处理器中一般使用 wait,典型的代码如下所示:
代码语言:javascript复制 1 #include "../apue.h"
2 #include <sys/wait.h>
3 #include <errno.h>
4
5 #define CLD_NUM 2
6
7 static void sig_cld (int signo)
8 {
9 pid_t pid = 0;
10 int status = 0;
11 printf ("SIGCLD receivedn");
12 if (signal (SIGCLD, sig_cld) == SIG_ERR)
13 perror ("signal error");
14
15 if ((pid = wait (&status)) < 0)
16 perror ("wait(in signal) error");
17 else
18 printf ("pid (wait in signal) = %dn", pid);
19 }
20
21 int main ()
22 {
23 pid_t pid = 0;
24 __sighandler_t ret = signal (SIGCLD, sig_cld);
25 if (ret == SIG_ERR)
26 perror ("signal error");
27 else
28 printf ("old handler %xn", ret);
29
30 for (int i=0; i<CLD_NUM; i)
31 {
32 if ((pid = fork ()) < 0)
33 perror ("fork error");
34 else if (pid == 0)
35 {
36 if (i % 2 == 0) {
37 // simulate background
38 sleep (3);
39 }
40 else {
41 // simulate foreground
42 sleep (4);
43 }
44
45 printf ("child %u exitn", getpid ());
46 _exit (0);
47 }
48
49 sleep (1);
50 }
51
52 int status = 0;
53 printf ("before wait pid %un", pid);
54 if (waitpid (pid, &status, 0) < 0)
55 printf ("wait %u error %dn", pid, errno);
56 else
57 printf ("wait child pid = %dn", pid);
58
59 sleep (2);
60 printf ("parent exitn");
61 return 0;
62 }
父进程启动两个子进程,第一个休眠 3 秒后退出,第二个休眠 4 秒后退出,由于父进程同步等待的是第二个子进程,因此第二个进程模拟前台进程,第一个进程模拟后台进程。运行输出如下:
代码语言:javascript复制old handler 0
before wait pid 2481
child 2480 exit
SIGCLD received
pid (wait in signal) = 2480
wait 2481 error 4
child 2481 exit
SIGCLD received
pid (wait in signal) = 2481
parent exit
此时同步等待的 waitpid 被信号中断了 (EINTR),此种情况下,我们需要重启 waitpid:
代码语言:javascript复制 1 int status = 0;
2 while (1) {
3 printf ("before wait pid %un", pid);
4 if (waitpid (pid, &status, 0) < 0)
5 {
6 int err = errno;
7 printf ("wait %u error %dn", pid, err);
8 if (err == EINTR)
9 continue;
10 }
11 else
12 printf ("wait child pid = %dn", pid);
13
14 break;
15 }
新的代码输出如下:
代码语言:javascript复制old handler 0
before wait pid 2513
child 2512 exit
SIGCLD received
pid (wait in signal) = 2512
wait 2513 error 4
before wait pid 2513
child 2513 exit
SIGCLD received
wait(in signal) error: No child processes
wait child pid = 2513
parent exit
可以看到两个进程退出时,都收到了 SIGCHLD 信号,只是前台进程被 waitpid 优先等待到了,所以信号处理器中的 wait 返回的 ECHILD 错误。但是如果还有其它子进程在运行,信号处理器里的 wait 会卡死。
忽略信号
之前提到,可以使用 SIG_IGN 来自动回收子进程,这里试一下使用 SIG_IGN 来代替 sig_cld,看看有什么改观:
代码语言:javascript复制old handler 0
before wait pid 2557
child 2556 exit
child 2557 exit
wait 2557 error 10
parent exit
同样的,两个子进程都走了忽略信号,而同步等待的 waitpid 因没有进程可等返回了 ECHILD。因为 waitpid 是指定进程等待的,所以即使还有其它子进程存在,这个也会返回错误,不会卡死在那里。相比上面的方法,似乎好了一点,但是因为我们没有安装处理器,所以无从得知哪个后台进程结束了,这并不是我们想到的结果。
使用 sigaction
之前提到,可以使用 sigaction 代替 signal 以获取更多的控制,我们看看换新的方式捕获信号,会不会有一些改变,新的代码逻辑如下:
代码语言:javascript复制 1 #include "../apue.h"
2 #include <sys/wait.h>
3 #include <errno.h>
4
5 #define CLD_NUM 2
6
7 static void sig_cld (int signo, siginfo_t *info, void* param)
8 {
9 int status = 0;
10 if (signo == SIGCHLD)
11 {
12 if (info->si_code == CLD_EXITED ||
13 info->si_code == CLD_KILLED ||
14 info->si_code == CLD_DUMPED)
15 {
16 if (waitpid (info->si_pid, &status, 0) < 0)
17 err_ret ("wait(in signal) %u error", info->si_pid);
18 else
19 printf ("pid (wait in signal) = %dn", info->si_pid);
20 }
21 else
22 {
23 printf ("unknown signal code %dn", info->si_code);
24 }
25 }
26 }
27
28 int main ()
29 {
30 pid_t pid = 0;
31 struct sigaction act;
32 sigemptyset (&act.sa_mask);
33 act.sa_sigaction = sig_cld;
34 act.sa_flags = SA_SIGINFO | SA_NOCLDSTOP;
35 int ret = sigaction (SIGCHLD, &act, 0);
36 if (ret == -1)
37 perror ("sigaction error");
38
39 for (int i=0; i<CLD_NUM; i)
40 {
41 if ((pid = fork ()) < 0)
42 perror ("fork error");
43 else if (pid == 0)
44 {
45 if (i % 2 == 0) {
46 // simulate background
47 sleep (3);
48 }
49 else {
50 // simulate foreground
51 sleep (4);
52 }
53
54 printf ("child %u exitn", getpid ());
55 _exit (0);
56 }
57
58 sleep (1);
59 }
60
61 int status = 0;
62 while (1) {
63 printf ("before wait pid %un", pid);
64 if (waitpid (pid, &status, 0) < 0)
65 {
66 int err = errno;
67 printf ("wait %u error %dn", pid, err);
68 if (err == EINTR)
69 continue;
70 }
71 else
72 printf ("wait child pid = %dn", pid);
73
74 break;
75 }
76
77 sleep (2);
78 printf ("parent exitn");
79 return 0;
80 }
运行输出如下:
代码语言:javascript复制before wait pid 2585
child 2584 exit
pid (wait in signal) = 2584
wait 2585 error 4
before wait pid 2585
child 2585 exit
wait(in signal) 2585 error: No child processes
wait child pid = 2585
parent exit
结果与使用 signal 很相似,但是因为在信号处理器中我们能明确的知道是哪个子进程终结了,使用的是 waitpid 而不是 wait,所以即使还有其它子进程在运行,也不会在信号处理器的 waitpid 中卡住。因此结论是无论使用 signal 还是 sigaction,同步等待的 waitpid 总比 SIGCHLD 信号处理器中的 wait(xxx) 具有更高的优先级。
当然,这个前提是在父进程同步 waitpid 之前子进程还没有结束;如果要等待的子进程先结束了,SIGCHLD 当然先被执行,这种情况下,建议先使用 sigprocmask 屏蔽 SIGCHLD 信号,然后在 waitpid 之前解除屏蔽。虽然不能保证完全解决信号竞争的问题,也能极大的缓解此种情况。退一步讲,假如出现了信号竞争导致同步等待的 waitpid 返回 ECHILD,我们也能从这些错误码中得知发生的事情,不会出现卡死的情况。出于好奇,我们看一下改使用 SIG_IGN 后的运行效果:
代码语言:javascript复制before wait pid 2613
child 2612 exit
child 2613 exit
wait 2613 error 10
parent exit
与使用 signal 时并无二致,仍然是忽略信号占了上风。因此结论是无论使用 signal 还是 sigaction,当忽略 SIGCHLD 信号时,信号优先于 wait 被忽略。出于同样的原因,这种方式我们并不采纳。
使用 SA_NOCLDWAIT
之前提到,sigaction还有一种高级的忽略 SIGCHLD 的方式,即指定 SA_NOCLDWAIT 标志位,同时给信号处理器指定 SIG_DFL,这种情况下,我们看看输出会有什么变化:
代码语言:javascript复制before wait pid 2719
child 2718 exit
child 2719 exit
wait 2719 error 10
parent exit
可以看到,与使用 SIG_IGN 并无二致。我们可以为 SIGCHLD 提供一个处理器,虽然在此信号处理器中无需再次等待子进程,但是我们拥有了获取子进程信息的能力,相对而言,比 SIG_IGN 更有用一些。新的输出如下:
代码语言:javascript复制before wait pid 2737
child 2736 exit
pid (auto wait in signal) = 2736
wait 2737 error 4
before wait pid 2737
child 2737 exit
pid (auto wait in signal) = 2737
wait 2737 error 10
parent exit
可以看到,同步 waitpid 仍然返回 ECHILD,显然是信号更具有优先级。好了,事情至此就全明了了,对于混合使用同步与异步 wait 的应用来说,最佳的方法应该是同步 waitpid 等待前台进程,使用sigaction 注册 SIGCHLD 信号处理器异步等待后台进程,且不设置 SA_NOCLDWAIT 标志位。在处理器中也应使用 waitpid 等待子进程,如返回 ECHILD 错误,证明该子进程是前台进程,已经被同步 wait 掉了,不需要任何处理;否则作为后台进程处理。
后记
说了这么一大堆,可能有的人会说了,我又不需要写一个 shell,需要用到这么复杂的知识吗? 确实,没有多少人会有机会写一个 shell,但是并非只有 shell 才有混合使用同步、异步等待子进程的场景,考虑下面这个场景:
代码语言:javascript复制 1 #include "../apue.h"
2 #include <unistd.h>
3 #include <sys/wait.h>
4
5 #define PAGER "${PAGER:-more}"
6
7 #define USE_SIG 2
8 static void sig_cld (int signo)
9 {
10 pid_t pid = 0;
11 int status = 0;
12 printf ("SIGCLD receivedn");
13 if (signal (SIGCLD, sig_cld) == SIG_ERR)
14 perror ("signal error");
15
16 if ((pid = wait (&status)) < 0)
17 perror ("wait(in signal) error");
18
19 printf ("pid (wait in signal) = %dn", pid);
20 }
21
22 void install_handler (__sighandler_t h)
23 {
24 __sighandler_t ret = signal (SIGCLD, h);
25 if (ret == SIG_ERR)
26 perror ("signal error");
27 else
28 printf ("old handler %xn", ret);
29 }
30
31 int main (int argc, char *argv[])
32 {
33 int n = 0;
34 #if USE_SIG == 1
35 install_handler (sig_cld);
36 #elif USE_SIG == 2
37 install_handler (SIG_IGN);
38 #endif
39
40 char line[MAXLINE] = { 0 };
41 FILE *fpin = NULL, *fpout = NULL;
42 if (argc != 2)
43 err_quit ("usage: ppage <pathname>");
44
45 fpin = fopen (argv[1], "r");
46 if (fpin == NULL)
47 err_sys ("can't open %s", argv[1]);
48
49 fpout = popen (PAGER, "w");
50 if (fpout == NULL)
51 err_sys ("popen %s error", PAGER);
52
53 while (fgets (line, MAXLINE, fpin) != NULL) {
54 if (fputs (line, fpout) == EOF)
55 err_sys ("fputs error to pipe");
56 }
57
58 if (ferror (fpin))
59 err_sys ("fgets error");
60
61 int ret = pclose(fpout);
62 if (ret == -1)
63 err_sys ("pclose error");
64 else
65 printf ("worker return %dn", ret);
66
67 return 0;
68 }
程序运行后打开参数指定的文件,读取并将它通过管道传递给 more 命令。随后通过 pclose 等待 more 命令结束。这期间为了保证其它子进程 (假设存在) 能正常回收,使用 SIG_IGN 注册了 SIGCHLD 信号。运行程序,退出 more 后有如下输出:
代码语言:javascript复制pclose error: No child processes
pclose 失败了,为什么呢?答案就是前面说过的,pclose 内部存在着一个隐式的 waitpid 在同步等待 more 子进程,而此时 SIGCHLD 被注册为忽略取得了优先权,导致 waitpid 失败从而导致 pclose 返回错误。可见,当程序中存在 pclose、system 等隐式 wait 调用时,如果同时需要 SIGCHLD 信号处理,则一定不能:
- 注册为忽略 SIG_IGN;
- 通过 sigaction 注册并设置 SA_NOCLDWAIT 标志位;
否则相应的调用会失败。顺便说一下,之前发现同步等待的 waitpid 没有被中断的情况只在忽略信号的时候产生,而前面也证明了忽略信号时,系统压根不产生 SIGCHLD 信号,这两者似乎到现在是对上了……
下载
场景 1&2 测试代码
场景3 测试代码
使用popen的场景
完整的shell示例