【Linux】信号 —— 信号的产生 | 信号的保存 | 信号的处理 | 可重入函数 | volalite关键字 | SIGCHLD
信号
- 1. 信号的产生 - 信号发送前
- 1.1 键盘产生
- 1.2 异常
- 1.3 系统调用
- 1.4 软件条件
- 1.5 理解发送信号
- 2. 信号的保存 - 信号发送中
- 2.1 相关概念 & 内核结构
- 2.2 sigset_t 及一系列系统调用函数
- 3. 信号的处理 - 信号发送后
- 3.1 内核如何实现信号捕捉
- 3.2 sigaction
- 4. 可重入函数
- 5. volalite关键字
- 6. SIGCHLD信号
people change
前排声明:以后更新文章会慢一些吧,如上文所言,people change… (哈哈哈距离我刚开始写下这句话时候已过去一个月了,果然,人生总是在不经意的想法中出现转折,曾经认为理所当然的事儿… 所以顺其自然…
“而且我们都是学计算机的,我们知道如果把宇宙所有的原子结构保存,配上超强的算法,一切都是必然的,我们决定不了的”
“你一上升到这个高度我就讨论不了了”
…
“我只是想说顺其自然”
a zing never lies @小边小边别发愁
生活中有许多关于信号的场景:比如红绿灯,你daddy的脸色。。。
这些场景触发时,我立马就知道我接下来该做什么,这来自于天生or后天习得,并不是它们真的发生时我才知道该怎么做。同理,进程具有识别并处理信号的能力是远远早于信号产生的,这是由那些编写操作系统的工程师写好的。
在生活中,我们收到信号时,不一定会立即处理,因为当前我可能做着更重要的事儿。同理,信号随时都可能产生(异步),进程收到某种信号时,不一定是立即处理的,而是在合适的时候处理。
比如有一小伙儿跟你表白了,你虽然没有立即回复(此时你正向闺蜜群组求助哈哈哈),但你也记着要给个答复。既然有时信号不能被立即处理,进程就需要先将信号保存起来,以供合适时机处理。那应该保存在哪里呢?struct task_struct
,信号的发送即向进程控制块写入信号数据。task_struct是一个内核数据结构,内核不相信任何人,只相信自己,无论以何种方式发送信号,本质都是通过OS发送的,那一定是操作系统向task_struct写入信号数据。
kill -l 查看系统支持的信号列表
前31个(1 ~ 31)是普通信号,后31个(34 ~ 64)是实时信号 ——
一般进程收到信号的处理方案有三种情况 ——
-
默认动作:终止自己、暂停等
-
忽略动作:是一种信号处理的方式,只不过就是什么也不干
-
自定义动作(信号的捕捉):比如如下的
signal
方法,修改处理信号的动作,由默认动作变为自定义动作
1. 信号的产生 - 信号发送前
信号产生的方式有哪些呢?
1.1 键盘产生
写一段死循环函数,可以ctrl+c终止程序。我们键盘ctrl+C
时,本质是向指定进程发送2号信号SIGINT
。那你怎么证明?可以通过signal函数为信号指定处理函数:
signal - ANSI C signal handling
#include <signal.h>
typedef void (*sighandler_t)(int); //函数指针 - 返回值为void,参数为int
sighandler_t signal(int signum, sighandler_t handler);
修改进程对信号的默认处理动作 ——
signal
:实际上这些大写的信号都是#define 整数
定义出来的宏handler
:注册函数。注册时不是调用这个函数,只有信号到来时,函数才会被调用。
代码如下 ——
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void handler(int signo)
{
printf("get a signal: %d, pid: %d\n", signo, getpid());
exit(1);
}
int main()
{
int sig;
for(sig = 1; sig<=31; sig++)
{
//通过signal注册对1~31号信号的处理动作,改成我们自定义的动作
signal(sig, handler);
}
while(1)
{
printf("people change...pid: %d\n", getpid());
sleep(1);
}
return 0;
}
发送信号时,执行自定义方法 ——
信号的产生方式的其中一种是通过键盘产生,键盘产生的信号只能终止前台进程,杀掉后台进程&
得用kill命令 ——
kill -9 [pid]
我们还可以设置不同信号的处理方式 ——
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void handler(int signo)
{
switch(signo)
{
case 2:
printf("Ayayaya~\n");
break;
case 3:
printf("people change a lot\n");
break;
case 9:
printf("I am tired\n");
break;
default:
printf("signo: %d\n", signo);
break;
}
//exit(1);
}
int main()
{
int sig;
for(sig = 1; sig<=31; sig++)
{
//通过signal注册对1~31号信号的处理动作,改成我们自定义的动作
signal(sig, handler);
}
while(1)
{
printf("people change...pid: %d\n", getpid());
sleep(1);
}
return 0;
}
发现9号信号不可被捕捉(自定义) ——
为什么呢?we will talk about it later…
1.2 异常
信号产生,也可能由于程序中存在异常问题,使进程收到信号并退出。
写一段野指针解引用代码 ——
int* p = NULL;
*p = 100;
进程为什么会崩溃呢?本质是因为进程收到了11号信号SIGSEGV
。而且如果不exit会刷屏,说明不断收到11号信号。
除0 ——
int a = 10;
a /= 0;
除0收到的是8号信号SIGFPE
,浮点数异常 ——
综上,在win或Linux环境下,进程崩溃的本质是进程收到了对应的信号,并执行默认处理动作(杀死进程)。那为什么会收到信号呢?
软件上的错误通常会体现在硬件或其它软件上,而操作系统是硬件管理者,就应该对硬件的健康负责,找到有问题的进程并发送信号反馈,终止进程。我们在C++中的捕捉异常,实际上就是在处理信号。
进程崩溃时,你最关心的是崩溃的原因,这是通过waitpid()的status参数的低7位来获取退出信号。(忘了的宝子去复习进程控制吧~)
从前从前我们就知道,在Linux中,进程正常退出时,它的退出码和退出信号都会被设置;进程异常退出时,它的退出信号会被设置,表明进程退出的原因。另外我还想知道是在哪一行崩溃的喂?
从前从前我们挖了这样一个坑:如有必要,OS会设置退出信息中的core dump
标志位,并将进程在内存中的数据转储到磁盘中,方便我们后期调试。
在云服务器上,core dump被关掉了,我们把它打开~
ulimit -a 查看系统资源
ulimit -c 10240
这样就允许你core dump了,(但也不是所有的信号都会core dumped),此时再运行,还生成了core文件 ——
我们打开生成的core.10225,发现是人类看不懂的乱码,因为是把磁盘内容直接拷下来的,我们也不关心啦~
我们需要小小修改一下Makefile文件,编译时带上-g
选项,允许用gdb调试。用core-file命令,得知错误原因及错误行数 ——
这种方案叫做事后调试。曾经我们程序崩溃,我们只能注释掉定位或者一行一行的单步调试。
若进程出现异常时被core dump,core dump
位会被设置为1,我们来通过位操作获取退出信息 ——
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
if(fork() == 0)
{
while(1)
{
printf("I am child...\n");
int a = 10;
a /= 0;
}
}
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %d, exit sig:%d, core dump flag: %d\n", (status>>8)&0xFF, status&0x7F, (status>>7)&1); return 0;
}
1.3 系统调用
通过系统调用产生信号 ——
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig); //给任意进程发送任意信号
int raise(int sig); //给自己发任意信号
#include <stdlib.h>
void abort(void); //给自己发送SIGABRT信号
kill命令就是调用kill函数实现的 ——
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
static void Usage(const char* proc)
{
printf("Usage:\n\t%s signo who", proc);
}
// ./mytest signo who
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
int signo = atoi(argv[1]);
int who = atoi(argv[2]);
kill(who, signo);
return 0;
}
1.4 软件条件
通过某种软件(OS),来触发信号的发送,在系统层面设置定时器,或某种操作导致条件不就绪等这样的场景下,触发的信号。
在进程间通信匿名管道的4种场景中,写端疯狂写,若此时读端关闭了读fd,则写端会**收到13号信号SIGPIPE
**退出,这就是一种软件条件触发的信号发送。今天我们再介绍 ——
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
设置一个计时器/闹钟,延迟n秒后发送14号信号SIGALRM
——
- 返回值:可以认为alarm被正常触发时,返回值就是0;或者是距离设定时间余下的秒数。
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
void handler(int signo)
{
printf("well, I recieved %d signal\n", signo);
}
int main()
{
int sig;
for(sig = 1; sig <= 31;sig++)
{
signal(sig, handler);
}
int ret = alarm(20);
sleep(5);
ret = alarm(0); //取消闹钟
printf("autually, it still left %d seconds\n", ret);
return 0;
}
我们不断打印count,统计1s中server对count可以递增到三万多次;然后屏蔽掉printf,而只是在捕捉信号处打印count,发现count达到了5亿次 ——
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
int count = 0;
//没有设置alarm信号(14)的捕捉/自定义动作, 执行默认动作:终止进程
//void handler(int signo)
//{
// printf("after 1s... count: %d\n", count); // exit(1);
//}
int main()
{
signal(SIGALRM, handler);
alarm(1); /
while(1)
{
//printf("people change...%d\n", count);
count++;
}
return 0;
}
因为IO是非常慢的。。
1.5 理解发送信号
信号产生方式的种类非常多,但就算信号产生的方式千差万别,最终一定是通过OS向目标进程发送信号。
如何理解OS给进程发送信号?朴素的理解就是OS发送信号数据给task_struct,我们要进一步来谈 ——
我们发现信号的编号是有规律的**[1, 31],进程内部一定要有对应的数据变量,来保存记录是否收到了对应的信号。那用什么数据类型,来标识进程是否收到信号**呢?唔唔唔!位图!
struct task_struct{
//进程的各种属性
uint32_t sigs;
};
比特位的位置(第几个),代表的是哪一个信号;比特位的内容(0/1),代表是否收到信号 ——
//比如这个,表示收到3号信号
00000000 00000000 00000000 00000100
综上,“信号的发送”更准确的说是信号的写入,即OS向指定进程的task_struct的信号位图(pending
位图)比特位 置为1。
2. 信号的保存 - 信号发送中
信号随时都可能产生(异步),进程收到某种信号时不一定是立即处理的,因为当前我可能做着更重要的事儿,而是把它暂存起来在合适的时候处理。怎么保存信号?合适又是什么时候?
2.1 相关概念 & 内核结构
信号有三张表:pending
、block
、handler
,我们姑且不关心它们底层的数据结构,其中pending
表就是用来写入接收到的信号的位图 ——
- 实际执行信号的处理动作称为信号递达(Delivery) —— 自定义捕捉、默认、忽略
- 信号从产生到递达之间的状态,称为信号未决(Pending) —— 本质是这个信号被暂存在task_struct的信号位图
pending
中。 - 进程可以选择阻塞信号(Block) —— 本质是OS允许进程暂时屏蔽指定信号,表示该信号仍然是未决的,该信号不会被递达,直到解除阻塞方可执行递达的动作
注意:阻塞和递达中的忽略有区别吗?yes,忽略是递达的一种方式,而阻塞是递达前的独立状态。
grep -ER 'SIG_DFL | SIG_IGN' /usr/include 递归查找一下这两个宏
#define SIG_DFL ((__sighandler_t)0) /* Default action */
#define SIG_IGN ((__sighandler_t)1) /* ignore signal */
block
表:信号屏蔽字,阻塞位图,表示是否阻塞,不影响接收信号但影响递达信号。是一种状态位图,同样的也是uint32_t block;
无符号整数类型:比特位的位置,代表信号的编号;比特位的内容,代表信号是否被阻塞(屏蔽)。这些信号不会被递达直到解除阻塞。pending
表:未决位图,保存的是已经收到但没有被递达的信号。比特位的位置(第几个),代表的是哪一个信号;比特位的内容(0/1),代表是否收到信号。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。所以你看,发送信号时都要给pid和信号数。handler
表:函数指针数组,每个信号的编号就是该数组的下标。实际上,我们执行signal
方法,对特定的信号注册自定义方法的本质就是,把handler函数地址填入到信号编号对应下标的 handler表中,这样执行的就是自定义方法。
内核中对信号会做类似如下检测 ——
int ishandled(int signo)
{
if(block & signo) { //该信号被block
//根本就不看是否收到该信号
}
else if(pending & signo) { //该信号没被block,且已经收到了
handler_array[signo](signo); /*回调函数*/
return 0;
}
return 1;
}
综上,这张表应该横着看,进程内置了识别信号的方式,[三元组] 是否被屏蔽→是否被递达(→handler),即你是谁?你现在能否处理?怎么处理?
2.2 sigset_t 及一系列系统调用函数
从上图来看,每个信号只有一个1bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,这种类型无法直接对其进行位操作,因为这是极不安全的,就必须通过系统调用接口。因为不同OS对于sigset_t位图结构的实现是不一样的,因此不能让用户直接修改。它定义的变量和我们之前的int、double没有任何区别,都是在用户栈上。
💛 下面是信号集的各种操作。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
但这只是对用户层定义的set
进行的操作,要想设置进对应进程,还需要如下函数。
💛 sigpromask
读取或更改进程的信号屏蔽字(阻塞信号集),本质是修改block表。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
set
:输入型参数。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改;oset
:输出型参数。如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出;如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。how
——SIG_BLOCK
:把set中的信号屏蔽字添加到进程的信号屏蔽字中,mask = mask|setSIG_UNBLOCK
:把set中的信号屏蔽字在进程信号屏蔽字的那些抹掉,mask = mask&~set,和权限掩码类似SIG_SETMASK
:设置当前进程的信号屏蔽字为set,mask = set
来~ 用起来 ——
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
int main()
{
sigset_t set, oset;
sigemptyset(&set);
sigemptyset(&oset);
//屏蔽2号信号
sigaddset(&set, 2);
// 设置当前进程屏蔽字,获取当前进程老的屏蔽字
sigprocmask(SIG_SETMASK, &set, &oset);
while(1)
{
printf("people change..\n");
sleep(1);
}
return 0;
}
发现ctrl+c不能再终止进程 ——
但是就算设置屏蔽9号信号,也没法把它屏蔽,因为9号信号被称为管理员信号,不能被屏蔽,也不能被捕捉,只能执行默认行为。
💛 sigpending
不对pending位图做修改,因为修改一定是OS做的,这儿只是单纯的获取进程的pending表
#include <signal.h>
int sigpending(sigset_t *set);
sigprocmask
:输出型参数
还有signal()是用来修改handler表的~
来用一下吧 ——
① 我们不断的获取并打印当前进程的pending位图00000000 00000000 00000000 00000000
;如果我的进程预先屏蔽掉2号信号,因为2号信号不会被递达,所以预期打印的pending位图是01000000 00000000 00000000 00000000
(注意打印位图不能直接通过位操作,必须通过信号集操作函数)。
② 当然也可以恢复2号信号的接收,把老的屏蔽字再传回去就行了;由于2号信号的默认动作是终止进程,我们看不到现象,所以我们signal()指定处理程序。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void show_pending(sigset_t* pending)
{
printf("current process pending:");
int i;
for(i = 1; i<=31; i++)
{
if(sigismember(pending, i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
void handler(int signo)
{
printf("signal %d has been recieved and handled...\n", signo);
}
int main()
{
signal(2, handler);
sigset_t set, oset;
sigemptyset(&set);
sigemptyset(&oset);
//屏蔽2号信号
sigaddset(&set, 2);
// 设置当前进程屏蔽字,获取当前进程老的屏蔽字
sigprocmask(SIG_SETMASK, &set, &oset);
int count = 0;
sigset_t pending;
while(1)
{
sigemptyset(&pending);
sigpending(&pending);
//打印pending表
show_pending(&pending);
sleep(1);
count++;
if(count == 8)
{
//10s后解除对2号信号的屏蔽,处理2号信号(1->0)
sigprocmask(SIG_SETMASK, &oset, NULL);
printf("SIGINT can be delivered\n");
}
}
return 0;
}
3. 信号的处理 - 信号发送后
3.1 内核如何实现信号捕捉
因为信号的产生是异步的,当前进程可能正在做更重要的事儿,需要暂存并在合适的时对信号延时处理 (取决于OS和进程)。
**什么是合适,信号何时被处理?**信号是保存在进程的PCB的pending位图中,要经历处理(检测->递达[默认、忽略、自定义]),当进程从内核态 返回到 用户态时,会进行如上的检测并处理。
执行和访问用户代码和数据时所处的状态叫做用户态,我们自己写的代码全都是在用户态执行的;执行OS的代码和数据时,计算机所处的状态是内核态。它们的主要区别在于权限。
用户调用system call函数,除了进入函数,身份也会发生切换,从用户身份变为内核身份。
用户的身份是以进程为代表的。用户的数据和代码一定要被加载到内存;OS的数据和代码也是要加载到内存的。OS的代码是怎样被执行到的呢?是通过系统级页表映射到物理内存的代码和数据,用户级页表一个进程一个;而内核页表由所有进程共享。
这样以来,一个进程就可以通过地址空间看到用户&内核的所有内容,那如何确定到底以什么身份执行程序呢?是通过CPU内的CR3
寄存器保存当前进程的状态:用户态使用的是用户级页表,只能访问用户的数据和代码;内核态使用的是内核级页表,只能访问内核的数据和代码。
无论进程之间如何切换,也能执行的是一份OS代码,因为都是通过3~4G的地址空间,通过同一张内核页表映射而来。
所谓的系统调用,本质就是进程的切化成为内核身份,根据内核页表找到系统函数,再执行。大部分情况下,OS是可以在进程的上下文直接运行的。
为什么一定要切换为用户态执行信号捕捉方法?OS理论上是可以执行用户的代码,但是OS身份特殊,不能轻易执行用户代码,因为可能执行恶意代码。
3.2 sigaction
作用和signal()完全类似,它修改的就是handler函数指针数组(但是这个函数还考虑了实时函数,可能更复杂,但是我们暂时不考虑啦~) ——
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
-
signum
:注册一个信号。 -
act
:输入型参数,调用方法。我们发现这个结构体类型和函数名一致,虽然我们自己写并不建议,但是便于记忆。-
sa_handler:将sa_handler赋值为常数
SIG_IGN
传给sigaction表示忽略信号,赋值为常数SIG_DFL
表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。 -
sa_mask:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号过程中,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止,即信号不会多次捕捉,不会排队!也就是说普通信号可能会被丢弃,但是有些信号不能被丢失 —— 实时信号,内核中是用链表组织起来的。
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
-
-
oldact
:输出型参数,带回老的信号catch方法,当然不想获取设为NULL
就行啦~
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
void handler(int signo)
{
printf("get a signo: %d\n", signo);
}
int main()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
// act.sa_handler = SIG_IGN; //忽略信号
// act.sa_handler = SIG_DFL; //默认 - 一般以接收2号信号为结束
//本质是修改当前进程的handler函数指针数组的特定内容
sigaction(2, &act, NULL);
while(1)
{
printf("I am missing you~\n");
sleep(1);
}
return 0;
}
测试sa_mask
,它是signet_t
类型,无法直接进行位操作,必须使用信号集操作函数。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
void handler(int signo)
{
while(1)
{
//正在处理信号... 现实中你不要这样干哦
printf("get a signo: %d\n", signo);
sleep(1);
}
}
int main()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
//本质是修改当前进程的handler函数指针数组的特定内容
sigaction(2, &act, NULL);
while(1)
{
printf("I am missing you~\n");
sleep(1);
}
return 0;
}
观察发现处理2号信号的过程中,3号信号被忽略了。
4. 可重入函数
- insert函数一旦重入,有可能出现问题 —— 该函数不可被重入
- insert函数一旦重入,不会出现问题 —— 可重入函数
我们所学到的大部分函数,STL、boast库中的函数,大部分都是不可重入的。
5. volalite关键字
#include<stdio.h>
#include<signal.h>
int flag = 0;
void handler(int signo)
{
flag = 1;
printf("change flag 0 → 1\n");
}
int main()
{
signal(2, handler);
while(!flag);
printf("这个进程是正常退出的!\n");
return 0;
}
带优化选项后,编译器在main函数中没有检测到对flag的修改,于是把flag优化到寄存器中,但是这儿会出问题,屏蔽了内存的修改 ——
如何解决呢?在flag前添加关键字volatile
——
为什么呢?volatile
的作用是,告诉编译器,不要对我这个变量做任何优化,读取必须贯穿式的读取内存,而不要读取中间缓冲区、寄存器 —— 保持内存的可见性。
还有一个作用 ——防止指令重排。嗯嗯有意思。
6. SIGCHLD信号
进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地轮巡检测查询是否有子进程结束等待清理。采用第一种方,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD
信号,该信号的默认处理动作是忽略。父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
void get_child(int signo)
{
//waitpid()
printf("get a signal: %d, pid: %d\n", signo, getpid());
}
int main()
{
pid_t id = fork();
signal(SIGCHLD, get_child);
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("我是子进程: %d\n", getpid());
sleep(1);
cnt--;
}
exit(0);
}
//parent
while(1);
return 0;
}
但是会出现僵尸进程 ——
如果在信号捕捉函数处waitpid()回收子进程,这样就不用父进程主动等待了。
如果我不回收子进程呢?可以显式设置忽略17号信号,当进程退出后,会自动释放僵尸进程。
signal(SIGCHLD, SIG_IGN);
这种方式,只在Linux平台下有效。
处理多个子进程,并且WNOHANG
防止有子进程,了解即可 ——
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
while ((id = waitpid(-1, NULL, WNOHANG)) > 0)
{
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
if (fork() == 0)
{ // child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while (1)
{
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}