system(3) 常用于执行 shell 命令
#include <stdlib.h> #include <stdio.h> int main(int argc, char *argv[]) { int ret = system("ls -la"); printf("ret: %d\n", ret); return ret; }
正常情况下,命令执行成功会返回 0。
system(3) 在执行的命令结束时会发出 SIGCHLD 信号,收到 SIGCHLD 信号的线程会从系统调用(如:read,write)中断返回,errno 为 EINTR(4: Interrupted system call)。
应用程序应该重试被中断的系统调用,但很多时候是通过第三方库间接进行系统调用,而这些库并未考虑周到,误以为系统调用失败。
另外还有信号标志 SA_RESTART ,用于自动重试被中断的系统调用,考虑到它只支持部分系统调用,而且我用到的平台不支持这一标志,所以不作考虑。
那么该如何避免 SIGCHLD 信号中断系统调用呢?
根据 system(3) 函数的 源码 ,其关键逻辑如下:
- 保存信号掩码
- 设置信号掩码为阻塞(SIG_BLOCK) SIGCHLD 信号
- fork(3)
子进程
重置为保存的信号掩码,然后 execl(3)
父进程
等待子进程(waitpid(3))结束,重置为保存的信号掩码
子进程结束时操作系统会给父进程发送 SIGCHLD 信号,父进程会遍历(通常从主线程找起)一个不阻塞 SIGCHLD 的线程进行处理。
在整个应用中忽略 SIGCHLD 信号会导致获取不到子进程退出码
在应用程序最开始的时候(main 函数),添加以下语句
signal(SIGCHLD, SIG_IGN);
子线程会继承主线程的信号处理,也就不会导致系统调用被中断。 signal(3) 设置的是整个进程的信号处理,由所有线程共享,千万不要以为子线程继承设置后就跟主线程没有关系了,只有程序中有代码动了 SIGCHLD 的处置方式,所有线程都会受影响。
然而,它会导致 system(3) 总是返回 -1,errno 为 ECHILD(10: No child processes),无法判断命令执行是否成功。
c - system() function while SIGCHLD is ignored - Stack Overflow 或 man wait(2)
POSIX.1-2001 指明,如果将 SIGCHLD 置为 SIG_IGN,或者为 SIGCHLD 指定 SA_NOCLDWAIT 标志(见 sigaction(2)),子进程结束后将不会成为僵尸进程,调用 wait() 或 waitpid() 将阻塞到所有子进程结束后返回错误,errno 设置为 ECHILD。
POSIX.1-2001 specifies that if the disposition of SIGCHLD is set to SIG_IGN or the SA_NOCLDWAIT flag is set for SIGCHLD (see sigaction(2)), then children that terminate do not become zombies and a call to wait() or waitpid() will block until all children have terminated, and then fail with errno set to ECHILD.
If the parent is not interested it can say so explicitly (before the fork) using
signal(SIGCHLD, SIG_IGN);
or
struct sigaction act;
act.sa_handler = something;
act.sa_flags = SA_NOCLDWAIT;
sigaction (SIGCHLD, &act, NULL);
and as a result it will not hear about deceased children, and children will not be transformed into zombies. Note that the default action for SIGCHLD is to ignore this signal; nevertheless signal(SIGCHLD, SIG_IGN) has effect, namely that of preventing the transformation of children into zombies. In this situation, if the parent does a wait(), this call will return only when all children have exited, and then returns -1 with errno set to ECHILD.
system(3) 调用 waitpid(3) 时,子进程已经被系统自动回收,消失得无影无踪,也就取不到子进程的返回值。
使用 popen(3)/pclose(3) 来代替 system(3) 通过分析标准输出来判断命令执行是否成功
int system2(const char* command, char* output, size_t output_size) { FILE* p = popen(command, "r"); if (p) { memset(output, '\0', output_size); fread(output, output_size - 1, 1, p); pclose(p); return 0; } return -1; }
使用示例
char output[255]; if (0 == system2("mkdir /test; echo ret=$?", output, sizeof(output)) && strstr(output, "ret=0")) { printf("mkdir /test successed"); } else { printf("mkdir /test failed"); }
在整个应用中阻塞 SIGCHLD 信号可能导致出现僵尸进程
signal(3) 无法阻塞一个信号,只支持忽略(SIG_IGN)和恢复缺省处理(SIG_DFL)。
阻塞( SIG_BLOCK)和取消阻塞(SIG_UNBLOCK)用于信号掩码(Signal Mask),如 sigprocmask(3) ,多线程下请使用 pthread_sigmask(3)。
主线程在创建子线程之前阻塞 SIGCHLD 信号
sigset_t set; sigemptyset(&set); sigaddset(&set, SIGCHLD); pthread_sigmask(SIG_BLOCK, &set, NULL);
通过继承主线程的信号处理,子线程调用 system(3) 创建子进程时能够保证其中的 waitpid(3) 调用成功获取子进程的退出码。
man signal(7)
通过 fork(2) 创建的子进程继承父进程的信号掩码,该信号掩码即使在 execve(2) 后仍得以保留。
A child created via fork(2) inherits a copy of its parent's signal mask; the signal mask is preserved across execve(2).
system(3) 在替换进程为新程序(execl(3))之前,会重置为保存的信号掩码,也就是阻塞 SIGCHLD 信号状态,子进程继承这一掩码可能会产生问题。
在整个应用中阻塞 SIGCHLD 信号会导致一种常用的回收僵尸进程的方法失效
Reap zombie processes using a SIGCHLD handler 有详细描述
The method described here has two steps:
- Define a handler for SIGCHLD that calls waitpid.
- Register the SIGCHLD handler.
这种回收僵尸进程的方法不但我们自已不能使用,并且我们调用的子进程也不能使用,除非子进程聪明到先清除 SIGCHLD 信号掩码。
相关 BUG 报告 271 – SSHD should unblock SIGCHLD - POSIX signal blocks survive exec()
通过 system(3) 启动 sshd,有用户尝试登录,sshd 会再 fork(3) 一个孙进程,然后在 SIGCHLD 信号处理函数中通过 waitpid(3) 回收孙进程,但是从父进程(调用 system(3)的进程)继承而来信号掩码阻塞了 SIGCHLD 信号,导致孙进程结束后成为僵尸进程。
在调用 system(3) 前暂时取消对 SIGCHLD 的阻塞
// 封装 system(3) ,一方面避免中断系统调用,另一方面避免出现僵尸孙进程. // 请记得全局堵塞 SIGCHLD 信号. int system2(const char* command) { // 调用 system(3) 前取消对 SIGCHLD 的阻塞 sigset_t set; sigemptyset(&set); sigaddset(&set, SIGCHLD); pthread_sigmask(SIG_UNBLOCK, &set, NULL); int code = system(command); // 调用 system(3) 后恢复对 SIGCHLD 的阻塞 sigemptyset(&set); sigaddset(&set, SIGCHLD); pthread_sigmask(SIG_BLOCK, &set, NULL); return code; }
在调用 system(3) 前暂时取消对 SIGCHLD 的阻塞,使子进程继承到正确的信号掩码,调用返回后恢复对 SIGCHLD 的阻塞,可以解决这个问题。
另外还有 popen(3)/pclose(3) 也可以用来创建子进程,也要相应进行替换。
进程的信号处理状态可在 proc 文件系统看到
如进程 pid 为 5526 ,获取到的进程忽略的信号发下
# grep SigIgn /proc/5526/status SigIgn: 0000000000001004
这是十六进制掩码,转化为二进制
$ node -e 'console.log((0x0000000000001004).toString(2))' 1000000000100
表示信号 3(SIGQUIT) 和 信号 13 (SIGPIPE)被屏蔽。