LINUX C:创建与监控多个子进程
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <signal.h>
#include <stdio.h>
//子进程个数
#define SUB_PRO_COUNT 10
//处理子进程的退出信号
void sub_quit_signal_handle(int sig);
//父进程的事件循环
void ParentCycle();
//子进程的事件循环
void ChildCycle();
int main(void)
{
pid_t pid;
int i;
//创建SUB_PRO_COUNT个子进程
for(i=0; i<SUB_PRO_COUNT; i++)
{
pid = fork();
if( 0 == pid || -1 == pid )
break;
}
//创建子进程失败
if( -1 == pid )
{
printf("No. %d: fork error\n", i);
}
//子进程的事件循环
else if( 0 == pid )
{
ChildCycle();
}
//父进程的事件循环
else
{
ParentCycle();
}
return 0;
}
void sub_quit_signal_handle(int sig)
{
int status;
//获取退出的那个子进程的状态
int quit_pid = wait(&status);
printf("sub process %d quit, exit status %d\n", quit_pid, status);
//新创建一个子进程
pid_t pid = fork();
if( 0 == pid )
ChildCycle();
}
void ParentCycle()
{
printf("Parent process %d\n", getpid());
signal(SIGCHLD, sub_quit_signal_handle);
while(1)
pause();
}
void ChildCycle()
{
printf("create sub process id: %d, parent id: %d\n", getpid(), getppid());
while(1)
pause();
}
Linux: 关于 SIGCHLD 的更多细节
僵尸进程
何为僵尸进程?
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid
获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程
成为僵尸进程的因素
- 子进程 先于 父进程退出;
- 子进程的状态信息,没有被父进程回收;
那么问题来了,子进程退出了,父进程怎么知道呢?
对该机制有稍微了解的话,不难得知一个关键因素:SIGCHLD
。正是这个SIGCHLD
起到了通知的作用,所以后面的处理也是基于它而实现。
僵尸进程处理方案
- 父进程捕获
SIGCHLD
信号,则显示调用wait
或waitpid
; -
父进程直接忽略该信号。
signal(SIGCHLD, SIG_IGN)
,这样子进程直接会退出。需要注意的是,虽然进程对于 `SIGCHLD`的默认动作是忽略,但是还是显示写出来,才能有效;
- 把父进程杀了,子进程直接过继给
init
,由init
伺候着。
不用担心init
会挂着一堆僵尸,init
本身的设计就有专门回收的处理,所以有多少回收多少;
SIGCHLD 还能干嘛
刚才我们在处理到父子进程相关的问题时,多多少少接触到SIGCHLD
, 那么,只有在回收子进程的时候才需要用到么?感觉好浪费不是么?
别担心 ~ 这个作用肯定是不止这样的!
其实对于SIGCHLD
,我们一般的理解是,子进程退出发送的信号,但其实不是的,这个信号代表的含义是:
子进程状态变更了,例如停止、继续、退出等,都会发送这个信号通知父进程。而父进程就能通过 wait/waitpid 来获悉这些状态了。
看起来有点意思,我们仿佛能借此做些有趣的事情了。
wait / waitpid 相关知识
#include <sys/wait.h>
pid_t wait(int * statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
wait
相对来说会常用点,因为不需要指定 pid
,而waitpid
就在一些需要指定特定pid
时才会比较常见,那么它们之间的关系就真的是只是这样么?
其实wait
是对waitpid
的封装,专门用来回收子进程退出的信息,同样的,它简单粗暴的设置成了堵塞方式,如果没有任何子进程退出,那么就堵塞住。
而waitpid
功能非常强大,pid
和options
都提供了非常灵活的用法:
pid:
< -1: 取该 pid 的绝对值,如果任何子进程的进程组ID等于该值,则该进程组的任一子进程中的进程状态发生变化,都会触发`waitpid`的回调;
== -1: 监听范围扩大到任意子进程,也就是 wait(status);
== 0: 监听进程组ID和父进程一样的子进程;
> 0: 监听该pid的子进程;
options:
WNOHANG: 调用时,指定的 pid 仍未结束运行,则 wait 立即返回 0;
WUNTRACED: 当子进程被暂停时,则立即返回子进程的 pid;
WCONTINUED: 当被暂停的子进程,又被信号恢复时,则立即返回子进程的pid;
而下面这些宏,将搭配status
一起使用:
WIFEXITED(status): 当子进程调用 exit、_exit或者正常从 main 返回等正常结束时,返回 true
--> WEXITSTATUS(status): 获取上面的 exit_code
WIFSIGNALED(status): 当子进程被信号杀死时,返回 true;
--> WTERMSIG(status): 获取信号的值(int)
WIFSTOPPED(status): 当自己弄成被信号暂停执行时,返回 true;
--> WSTOPSIG(status): 获取该信号的值
WIFCONTINUED(status): 子进程接收到SIGCONT信号继续执行时,返回 true
最小实践
我们来个最小的 demo
来说明上面的怎么用:
#include<stdio.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<unistd.h>
int main(){
int pid;
if((pid = fork()) == 0){
while(1){
printf("Child: %d\n", getpid());
sleep(1);
}
}
else{
int status;
pid_t w;
while(1){
// 希望堵塞,所以没用 WNOHANG
w = waitpid(pid, &status,WCONTINUED | WUNTRACED);
if(WIFEXITED(status)){
printf("子进程正常退出,状态码: %d\n", WEXITSTATUS(status));
exit(0);
} else if(WIFSIGNALED(status)){
printf("子进程被信号杀死了! 信号值: %d\n", WTERMSIG(status));
exit(0);
} else if(WIFSTOPPED(status)){
printf("子进程被信号暂停了! 信号值: %d\n", WSTOPSIG(status));
} else if(WIFCONTINUED(status)){
printf("子进程又恢复继续运行了\n");
}
}
}
}
终端输出:
Child: 10848
Child: 10848 # 子进程的 pid
子进程被信号暂停了!信号值:21 # kill -SIGTTIN 10848
子进程又恢复继续运行了 # kill -SIGTTIN 10848
...
子进程被信号暂停了! 信号值: 19 # kill -SIGSTOP 10848
子进程又恢复继续运行了 # kill -SIGTTIN 10848
...
子进程被信号杀死了! 信号值: 15 # kill -SIGTERM 10848
如果自己在子进程上面加个退出,就会打印:正常退出了
结语
在上面的实验中,我们已经发现通过SIGCHLD
除了用来回收子进程,还能获悉子进程的状态!
在操作系统上,也有很多利用这个在工作的,例如:后台进程,如果向标准输入读取内容时,是会被暂停的
为什么呢?
因为后台进程,是和终端断开连接的,当它从标准输入读取内容时,终端的驱动程序会发现这个操作,会发一个 SIGTTIN 给后台进程,让其暂停,并且通知用户,只有用户通过 fg 命令将其转换成 前台进程时,才能继续工作
正是有这样的一套机制,所以我们也能做出很多比较实在的东西了~
原文:
https://segmentfault.com/a/1190000015060304?utm_source=tag-newest