system函数与信号
使用在写在2019年来临前的倒数0.5小时的system_test()函数来调用如下loop_echo程序:
#include <stdio.h>
#include <string.h>
#include <signal.h>
#define BUFSZ 1024
static void handler(int sig)
{
printf("<.q> to exit\n");
}
int main(void)
{
struct sigaction sa;
printf("###################################\n");
char buf[BUFSZ] = {0};
/* 捕捉SIGQUIT和SIGINT信号 */
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if (sigaction(SIGINT, &sa, NULL) < 0) {
perror("sigaction");
return -1;
}
if (sigaction(SIGQUIT, &sa, NULL) < 0) {
perror("sigaction");
return -1;
}
while (1) {
/* 获取终端输入被回显,直至用户键入.q才退出 */
fgets(buf, BUFSZ, stdin);
if(strstr(buf, ".q")) {
puts("Bye\n");
break;
}
puts(buf);
bzero(buf, BUFSZ);
}
return 0;
}
程序主流程main.c是这样的:
#include <stdio.h>
#include <signal.h>
#include "system_test.h"
static void handler(int sig)
{
printf("Caught SIGINT\n");
}
static void handler2(int sig)
{
printf("Caught SIGCHLD\n");
}
int main(void)
{
struct sigaction sa, sa2;
/* 捕捉SIGINT和SIGCHLD信号 */
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
sigemptyset(&sa2.sa_mask);
sa2.sa_flags = 0;
sa2.sa_handler = handler2;
if (sigaction(SIGINT, &sa, NULL) < 0) {
perror("sigaction");
return -1;
}
if (sigaction(SIGCHLD, &sa2, NULL) < 0) {
perror("sigaction");
return -1;
}
/* 做创建子进程操作 */
//....
system_test("./loop_echo");
return 0;
}
编译运行:
由运行结果可见:
(1)当我们发送SIGINT信号给loop_echo程序时,main进程仍能捕捉到该信号;
(2)当loop_echo进程退出时,main进程可以收到子进程退出时内核发出的SIGCHLD信号;
我们知道,运行main程序后会有3个进程在进程:①main、②bash、③loop_echo,它们在干什么?
① main:阻塞在waidpid()中,即system_test()的父进程(即本main进程)的代码片段:
default:
while (waitpid(cpid, &status, 0) == -1) {
if (errno == EINTR) { /* 被信号中断时继续回去等待,重要 */
continue;
}
perror("waitpid");
return -1;
}
return status;
② bash:调用loop_echo并占用终端
③ loop_echo:正在和用户进行交互
我们知道在终端中发出一个信号时发给所在终端的进程组,那么main和loop_echo自然会都收到该信号。然而,让main进程对这个信号都做出响应是没有意义的,这会让使用system_test()的程序员会对此行为困惑不已,因为程序员让主进程去调用system_test()进而执行某个新进程,实际上就是放弃对该新进程的控制权,而只阻塞在waitpid()中。
另外一个很严峻的信号时SIGCHLD。由上运行可见,当由system_test()锁创建的子进程loop_echo退出时产生了SIGCHLD信号,该信号可能被main进程所捕获(在system_test()有机会调用waitpid()之前主进程的信号处理函数可能会率先得以执行去收集子进程状态,这将是竞争条件,所以需要保证system_test()要先回收自己创建的子进程);假设main()程序还直接创建了其它子进程并在SIGCHLD的信号处理函数调用wait()去回收子进程,那么main进程会误以为wait回收的是自己直接创建的该子进程,事实上却是system_test()子进程。
因此,POSIX.1要求system()忽略SIGINT和SIGQUIT,阻塞SIGCHLD信号。
忽略SIGINT和SIGQUIT是因为主进程已经放弃新进程的控制权,主进程不应该对新进程的SIGINT和SIGQUIT做出响应;阻塞SIGCHLD信号时确保system()所创建的子进程退出时主进程不会去回收该子进程。为什么是阻塞?因为system()退出时还要恢复主进程对SIGCHLD的响应,由于此时system()已经回收自己所创建的子进程了,那么恢复主进程对SIGCHLD的响应,收到system()所创建的子进程退出的SIGCHLD信号后,假设再去回收该子进程的已经回收失败,因为该子进程已经不存在了。需要注意,可能在阻塞期间main进程所直接创建的子进程会退出,由于SIGCHLD被阻塞所以主进程并不能去做出响应和回收,恢复之后,对这些进程仍然进行回收。这就是为什么使用阻塞的原因。
综上分析,system_test()对信号的正确处理后的实现为:
#include <stdio.h>
#include <signal.h>
#include "system_test.h"
#include <sys/wait.h>
static void handler(int sig)
{
printf("Caught SIGINT\n");
}
static void handler2(int sig)
{
printf("Caught SIGCHLD\n");
}
int main(void)
{
struct sigaction sa, sa2;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
sigemptyset(&sa2.sa_mask);
sa2.sa_flags = 0;
sa2.sa_handler = handler2;
if (sigaction(SIGINT, &sa, NULL) < 0) {
perror("sigaction");
return -1;
}
if (sigaction(SIGCHLD, &sa2, NULL) < 0) {
perror("sigaction");
return -1;
}
system_test("./loop_echo"); //阻塞在system_test()中的waitpid(pid, &status, 0)中
return 0;
}
编译运行:
由运行可知,main进程还是会收到system_test()创建的子进程退出时系统发出的SIGCHLD信号(因为该信号一直处于未决状态),此时若main进程的SIGCHLD再去回收system_test()创建的子进程的退出信息将无法获取。修改main.c的handler2函数,增加waitpid():
static void handler2(int sig)
{
printf("Caught SIGCHLD\n");
pid_t childPid;
while ((childPid = waitpid(-1, NULL, WNOHANG)) > 0) {
printf("childPid = %d\n", childPid);
}
if (childPid < 0) {
perror("waitpid");
}
}
运行:
这样一来,main.c的handler2()函数就只回收到main进程直接创建的子进程的退出信息了。