Linux学习笔记---014
Linux的信号篇章及基本知识储备
前言:
前篇开始了解学习了Linux的磁盘文件等相关知识内容,接下来学习关于Linux的信号篇章的基本知识,深入地了解这个强大的开源操作系统。
/知识点汇总/
信号概念和基本知识储备
生活中,红路灯信号…
a、信号可能随时产生 – 信号的产生和用户,异步的,非同步
b、信号能够被认识/识别 – 识别并处理
c、信号的接收后,该处理 — 识别并处理
d、信号暂停/中断/触发 – 暂时不处理,需要记得信号上下文,之后合适的时候处理
查看信号的指令:kill -l
信号列表中:1~31号信号为普通信号 35~64号为实时信号/系统信号(不用管)
特殊的编号:没有32,32,34和0号
1、什么是信号?
1.1、信号的基本介绍
Linux信号是Linux系统响应某些条件而产生的一个事件,它在软件层次上对中断机制进行模拟,是一种异步通信方式。具体来说,Linux信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式,用于通知进程发生了异步事件。
当一个信号发送给一个进程时,操作系统会中断该进程正常的控制流程,此时任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么该函数将被执行;否则,就执行默认的处理函数。进程之间可以互相通过系统调用kill发送软中断信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。
基本概念概括:
信号:Linux系统提供的一种,向指定进程发送特定事件的方式,做识别和处理操作。 信号产生是异步的。
1.2、信号的基本分类
Linux中的信号被分类为标准信号和实时信号,每个信号都有一个唯一的编号。常见的信号包括SIGINT(键盘中断,如用户按下Ctrl+C)、SIGTERM(终止进程)、SIGKILL(强制终止进程)、SIGSEGV(访问非法内存地址)等。这些信号可以由系统内部事件产生,也可以通过终端按键或进程间通信产生。
1.3、Linux信号的作用主要体现在以下几个方面
1.进程间通信:进程可以通过向其他进程发送信号的方式进行通信,例如某个进程在完成了某项工作之后,可以向另一个进程发送SIGUSR1信号,通知其进行下一步的操作。
2.处理异常:信号可以被用来处理程序中的异常情况,例如当一个进程尝试访问未分配的内存或者除以0时,系统会向该进程发送SIGSEGV或SIGFPE信号,用于处理这些异常情况。
3.系统调试:信号可以用于程序的调试,例如在程序运行时,可以向该进程发送SIGUSR2信号,用于打印程序的状态信息等。
信号的处理概括:
a、默认动作
b、忽略动作
c、自定义处理(信号的捕捉)
进程处理信号,不做任何设置,就为系统默认的,即终止自己,暂停,忽略
kill -ll
查看信号表信息 – 对应的信号默认动作等
捕捉信号函数:
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/utime.h>
#include <sys/stat.h>
#include <string.h>
using namespace std;
void hander(int sig)
{
cout << "get signal: " << sig << endl;
}
//捕捉信号
void test_signal()
{
//对信号的捕捉,只需要捕捉一次,后续使用一直有效。
//捕捉信号
signal(2, hander);
signal(3, hander);
signal(4, hander);
signal(5, hander);
int n = 50;
while (n--)
{
cout << "hello signal" << endl;
sleep(1);
}
}
1).当2号信号始终不产生呢?
答:那么hander就不会被调用。
2).signal可不可以对多个信号进行捕捉呢?
答:可以,写多个捕捉即可。且捕捉一次,生命周期内就一直有效了
signal(2, hander);
signal(3, hander);
signal(4, hander);
signal(5, hander);
3).SIGINT信号默认动作是什么呢?
答:终止进程
4).SIGINT是什么呢?
答:Ctrl + C – 终止进程 – 本质是给目标进程发送2号信号
5).如何理解信号的发送和保存?浅度理解
答:进程 – task_struct – struct – 成员变量 – 用来保存位图,位图就是用来保存收到的信号的。
1~31号信号
发送信号:就是修改指定进程PCB中信号保存的指定位图。0-》1,写信号。
内核数据结构,只有谁有资格修改内核结构对象中的值呢? — OS
6).如果我把所有信号的捕捉了,会怎么处理呢? – 9号信号不允许自定义捕捉。
答:导致出异常,通过9号信号杀死,所以9号信号自然不允许捕捉。
7).如何理解信号的发送呢?
答:OS.不管信号以什么方式产生,但是发送只能是由OS。因为本质只有OS能够去修改位图。
2、信号产生
2.1、信号产生的5种方式
1).可通过kill命令,向指定的进程里发送指定的信号
2).键盘可以产生信号!SIGINT , SIGQUIT(比如组合键:Ctrl + C,Ctrl + /)
3).系统调用
系统调用:
#include <sys/types.h>
#include <signal.h>
#include <cstdlib.h>
int kill(pid_t pid, int sig);
int raise(int sig); --- (kill(getpid(),xxx) =等价= raise(sig))
void abort(void); -- SIGABRT
4).软件条件
管道 – 读关闭,写一直执行 – SIGPIPE(13号信号)
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
SIGALRM(14号信号)
a、验证:I/O — 属于内存级I/O很慢
b、理解闹钟 — 提供了很多定时器,
时间:时间戳
操作系统要对闹钟做管理?先描述,再组织
struct alarm
{
time_t expired;//未来的超时时间 = second是+ Now();
pid_t pid;
func_t times;
…
}
最大堆、最小堆
c、闹钟的返回值
alarm(0) – 取消闹钟/当前闹钟
设置一次,就默认触发一次
5)异常
a、程序为什么会崩溃?
答:非法访问/操作,导致OS给进程发送了信号
b、程序为什么崩溃了会退出呢?可以不退出吗?
答:默认是异常就退出,可以不退出,那么就捕捉异常,推荐还是终止进程(因为,虽然捕捉异常了,但是进程一直被调度有异常又捕捉调度,死循环)。
寄存器只有一套,但是寄存器里的数据是属于每一个进程的 – 硬件上下文的保存与恢复
c、CPU是如何知道运算是正常还是异常的呢?
OS是软硬件资源的管理者,OS要随时处理这种硬件问题,向目标进程发送信号。
3、信号保存(三张表)
3.1、解决历史遗留问题,core、term
首先core和term都为终止进程,区别在于:
term异常终止
core异常终止,但是会形成一个debug文件
测试代码:
#define _CRT_SECURE_NO_WARNINGS 1
//core和term历史问题
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void printPending(sigset_t& pending)
{
cout << "currnt process pid[" << getpid() << "]current process pending: ";
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
cout << 1;
}
else
cout << 0;
}
cout << endl;
}
void hender(int signo)
{
cout << signo << "2号信号递达" << endl;
//验证,之前还是之后 -- 之前
cout << "------------------------" << endl;
sigset_t pending;
sigpending(&pending);
printPending(pending);
cout << "------------------------" << endl;
}
//屏蔽2号信号和解除2号信号屏蔽
int main()
{
//0.捕捉2号信号
signal(SIGINT, hender);
//1.屏蔽2号信号 ---添加
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, SIGINT);//写到这一步,我们有没有修改到当前进行的内核block表呢1/0?
//没有,真正的修改需要调用sigprocmask
//1.1设置进入进程的block表中
sigprocmask(SIG_BLOCK, &block_set, &old_set);//真正的修改当前进行的内核block表,完成了对2号信号的屏蔽
int cnt = 10;
while (true)
{
//2.获取当前进程的pending信号集
sigset_t pending;
sigpending(&pending);
//3.打印pending信号集
printPending(pending);
//4.解除对2号信号的屏蔽 -- 同样需要用sigprocmask
if (cnt == 0)
{
cout << "解除对2号信号的屏蔽 1 --》 0" << endl;
sigprocmask(SIG_SETMASK, &old_set, &block_set);
}
//5.由于解除2号信号屏蔽后,执行2号信号的默认动作就会终止进程,所以可以捕获一下
//--》0.捕捉2号信号
//5.做自定义捕捉动作
sleep(1);
}
return 0;
}
相关指令:
ulimit -c
ulimit -a
gdb test_code
加载code文件:
core-file core
得到core文件的结论:协助我们进行debug的文件。— 事后调试
ulimit -c 0
ulimit -a
ulimit -c 10240(id)
ulimit -a
3.2、理解几个概念
1).实际执行信号的处理动作就称为信号递达。
2).信号已经产生但是还未处理的状态称为信号未决。
3).进程可以选择阻塞某个信号。(阻塞一个信号,那么对应的信号一旦产生,永不递达,一直未决,直到主动解除阻塞)
a、一个信号如果阻塞和他有没有未决有关系吗? – 无关
b、底层struct结构三张表:
pending表:
特定的位图int,bite位的位置表示信号编号(即数组下标,方便索引信号处理方法),bite位的内容0/1表示:信号是否收到. ---- 未决信号集(pending表)
handler表:
hander[32] – 函数指针数组
block表:
一张位图,与pending类型一样int,比特位的位置代表:信号的编号,比特位的内容表示:信号是否阻塞。
小结1:两张位图表 + 函数指针数组表 == 实现进程识别信号。
小结2:被阻塞的信号产生时将始终保持未决状态,直到进程解除对此信号的阻塞,才能去执行递达的动作。
4).阻塞和忽略是不同的,只要信号被阻塞就不会被递达而忽略是在递达之后可选的处理动作之一。
5).为了方便使用和访问这三张表,提供了一个sigset_t类型维护。
a、两个位图即未决和阻塞标志可以使用相同的数据类型,sigset_t来存储,sigset_t称为信号集.
b、将阻塞信号集也叫做当前进程的信号屏蔽字(signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
//位图
struct bits
{
uint32_t bits[400];//400*32
};
//40
//40/sizeof((unit32_t)*8) = 1;-->bit[1]
//40%sizeof((unit32_t)*8) = 8;-->bit[1]:8
int main()
{
//OS给用户提供的一个用户级的数据类型
//但是禁止用户修改,所以提供了函数集
sigset_t bits;//用户级的位图(Linux)
return 0;
}
OS给用户提供的一个用户级的数据类型,但是禁止用户修改,所以提供了函数集
sigset_t bits;//用户级的位图(Linux)
3.3、信号集操作函数集
#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);
sigprocmask: – bolck表
获取读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how,const sigset_t* set,sigset_t* oldset);
参数int how有三个选项:
a、SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
b、SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
c、SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set
参数const sigset_t* set:输入型参数,比如输入1111与参数一进行运算。
参数sigset_t* oldset:输出型参数,用于保留原本的表属性+数据,便于恢复数据。(输出型:保存老的信号屏蔽字返回给用户)
sigpending: — pending表
获取当前进程的pending位图
#include <signal.h>
int sigpending(sigset_t* set);//输出型参数
handler表:hander[32] – 函数指针数组
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
位图
//struct bits
//{
// uint32_t bits[400];//400*32
//};
40
40/sizeof((unit32_t)*8) = 1;-->bit[1]
40%sizeof((unit32_t)*8) = 8;-->bit[1]:8
//
//int main()
//{
// //OS给用户提供的一个用户级的数据类型
// //但是禁止用户修改,所以提供了函数集
// sigset_t bits;//用户级的位图(Linux)
// return 0;
//}
void printPending(sigset_t& pending)
{
cout << "currnt process pid[" << getpid() << "]current process pending: ";
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
cout << 1;
}
else
cout << 0;
}
cout << endl;
}
void hender(int signo)
{
cout << signo << "2号信号递达" << endl;
//验证,之前还是之后 -- 之前
cout << "------------------------" << endl;
sigset_t pending;
sigpending(&pending);
printPending(pending);
cout << "------------------------" << endl;
}
//屏蔽2号信号和解除2号信号屏蔽
int main()
{
//0.捕捉2号信号
signal(SIGINT, hender);
//1.屏蔽2号信号 ---添加
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, SIGINT);//写到这一步,我们有没有修改到当前进行的内核block表呢1/0?
//没有,真正的修改需要调用sigprocmask
//1.1设置进入进程的block表中
sigprocmask(SIG_BLOCK, &block_set, &old_set);//真正的修改当前进行的内核block表,完成了对2号信号的屏蔽
int cnt = 10;
while (true)
{
//2.获取当前进程的pending信号集
sigset_t pending;
sigpending(&pending);
//3.打印pending信号集
printPending(pending);
//4.解除对2号信号的屏蔽 -- 同样需要用sigprocmask
if (cnt == 0)
{
cout << "解除对2号信号的屏蔽 1 --》 0" << endl;
sigprocmask(SIG_SETMASK, &old_set, &block_set);
}
//5.由于解除2号信号屏蔽后,执行2号信号的默认动作就会终止进程,所以可以捕获一下
//--》0.捕捉2号信号
//5.做自定义捕捉动作
sleep(1);
}
return 0;
}
小结:
1.解除屏蔽,一般会立即处理当前被解除的信号(如果被pending),立即递达
2.pending位图对应的信号也要被清0,是在递达之前?递达之后?
答:之前
4、信号处理(默认、忽略、自定义捕捉)
处理信号就是递达信号
signal(2,hender);//自定义捕捉
signal(2,SIGIGN);//忽略一个信号
signal(2,SIG_DFL);//信号的默认处理动作
信号可能不会被立即处理,而是在合适的时候处理,即从进程的内核态返回到用户态的时候,进行处理。
在内核态切换回用户态的时候,对信号进行检测和处理。
操作系统能不能直接转过去执行用户提供的hander方法呢?
答:能,但不建议,因为hander由用户提供的,盲目以内核身份去执行就容易导致不可避免的错误。
即,必须以用户的身份去执行hander。
执行结束后,通过特定的sigreturn()返回给用户调用处或中断点继续执行main函数。
抽象倒8字图形理解:
用户态和内核态,以红线分割,交与4个交点。
4个交点表示,信号捕捉的过程会经历4次状态的切换。
5、内核态 VS 用户态
5.1、再谈地址空间
内核级页表只有一份。无论进程如何切换,总能找到OS
我们访问OS,其实还是在我们的地址空间中进行的。和我们访问库函数没区别。
5.2、谈谈键盘输入数据的过程
键入电平信号标记中断号,寄存器识别转数据。
以函数指针数组(中断向量表)维护,中断号就是数组下标。读取到中断就根据中断号索引,再通过函数指针调用相应执行方法等。
我们学习的信号,就是模拟中断实现的。
信号:纯软件
中断:软件+硬件
5.3、谈谈如何理解OS如何正常的运行?
1.如何理解系统调用?
操作系统提供了一张函数指针数组表 — sys_call系统调用表。
所以我们只要找到特定数组下标(系统调用号)的方法,就能执行系统调用了。
1.1、执行任意系统调用需要哪些条件?
a、系统调用号
b、系统调用函数指针数组
比如:
pid_t fork()
{
mov 2 eax;//系统调用号放入寄存器中
}
2.OS是如何运行的?
操作系统的本质是一个死循环。
硬件信号发送中断死循环调度
具体比如:
时钟 --》 CPU --> 检测时间片,时间片到了就执行切换进程等中断操作。
其次,外部中断的目的,不就是想让CPU内部寄存器形成一个中断号的数字吗?
那么可以直接由内部直接形成数字呢? — 可以
但是这种操作就属于直接去访问执行操作系统,基于内核级的操作,需要严谨并且不能有误的,所以存在风险。
OS是不相信外界的,不相信用户。用户无法直接跳转到3~4GB地址空间范围的。用户必须在特定条件下,才能跳转到OS内部执行代码和数据。如何限制用户无法跳转呢?
简单理解,就是硬件层面(CPU)配合其中的CS(code semgment代码段),
使用用户级代码时就指向内存空间的用户自己代码段的开始地址(统称为代码区的范围),
其次,使用内核空间的代码段时,就结合系统调用(用户访问内核通过系统调用)指向内核区的代码段开始地址。
也验证了系统调用中会有一个特定数字标记(寄存器保存的状态标记位),0:内核,3:用户
3—》0可访问外部
5.4、总结大致流程理清思路
总结大致流程理清思路:int main()
User Mode
1.在执行主控制流程的某条指令时,因为中断、异常或系统调用进入内核;—用户态
–>2
2.内核处理完异常准备回用户模式之前先处理当前进程中可以递达的信号;;—内核态
–>do_signal() 3
3.如果信号的处理动作自定义的信号处理函数则回到用户模式执行信号处理函数(而不是回到主控制流程);—内核态
–>void sighandler(int) 4
4.信号处理函数返回时执行特殊的系统调用sigreturn再次进入内核;—用户态
–>sys_sigtrturn() 5
5.返回用户模式从主控制流程中上次被中断的地方继续向下执行;—内核态
–>1
6、捕捉信号的操作
6.1、接口函数
man signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
man sigaction
#include <signal.h>
int sigaction(int signum,const struct sigaction* act,struct sigaction* oldact);
功能:特定方法执行自定义捕捉,捕捉执行的方法由struct sigaction结构体内的函数指针void (sa_handler)(int)指向。
参数:
int signum – 用户指定的信号(特定方法)
const struct sigaction act — 输入型参数(因为const)
struct sigaction* oldact — 输出型参数 – 保存出去用于恢复以前的处理方法(对应的之前讲过的三张表(两张位图表,一张handler表)中的handler表对用的handler保存出去)
测试代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
void Print(sigset_t &pending)
{
//遍历信号集
for (int sig = 31;sig > 0; sig--)
{
if (sigismember(&pending, sig))//2
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << std::endl;
}
//当前如果正在对2号信号进程处理时,默认2号信号会被自动屏蔽
//对2号信号处理完成时,会自动解除对2号信号的屏蔽
//为什么?
//因为系统不允许同一个信号被同时连续处理。
void handler(int signum)
{
std::cout << "get a sig: " << signum << std::endl;
//加一个睡眠,测试正在对2号信号进程处理时,是否被屏蔽
//sleep(1);
while (true)
{
//查看验证,可看到此时在pending表里屏蔽着不被处理
sigset_t pending;
sigpending(&pending);
Print(pending);
sleep(1);
//验证2号信号处理完,会自动解除对2号信号的屏蔽
//sleep(3);
//break;//处理完毕,注释//exit(1);
}
exit(1);
}
int main()
{
//代码可读性,以及区分同名sigaction
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);//重点
//如果你还想处理2号(OS对2号信号自动屏蔽),同时还可以把其他信号设置进sa_mask,就对其他信号进行屏蔽
//比如指定的添加3号信号
sigaddset(&act.sa_mask, 3);//可能同时,限制2号和3号信号进行屏蔽了。
//所以sa_mask这个结构体成员字段,就是可以由用户指定用来屏蔽指定信号之外的其他信号的。
act.sa_flags = 0;
//调用sigaction捕捉
sigaction(2, &act, &oact);//与signal的使用上没什么大的区别
while (true)
{
std::cout << "I am a process,pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
结论:
当前如果正在对n号信号进程处理时,默认n号信号会被自动屏蔽
对n号信号处理完成时,会自动解除对n号信号的屏蔽 – n指特定信号
6.2、扩展问题
如果你还想处理2号(OS对2号信号自动屏蔽),同时还可以把其他信号设置进sa_mask,就对其他信号进行屏蔽
比如指定的添加3号信号
sigaddset(&act.sa_mask, 3);//可能同时,限制2号和3号信号进行屏蔽了。
所以sa_mask这个结构体成员字段,就是可以由用户指定用来屏蔽指定信号之外的其他信号的。
那么可以屏蔽全部信号吗?比如9号kill信号
答:不用想也是不行的,否则该进程就无法被控制了。
可以至少知道9号是不行的。
信号的大致路线/流程总结:
1.产生和发送 --》 2.保存 --》 3.处理
5种产生,发送都是由OS发送 三张表(block表(信号是否被屏蔽)、pending表(是否收到信号)、handler表(信号的处理)) 递达、未决、阻塞等,三种处理方式(捕捉、默认、忽略)自定义,用户态/内核态,屏蔽
7、补充三个子问题
7.1、可重入函数
结合单链表的insert操作,insert在调度handler处理函数再次被执行insert,导致insert函数操作被重入了,
那么很可能导致回到main主控制流中断后继续执行时,出现节点丢失,即内存泄漏问题。
所以大部分函数是不可重入了,能重入的函数,属于具备可重入特征。
所以可重入函数,描述的是函数的特点。一般是属于使用局部变量的函数。而全局的函数就属于不可重入。
7.2、volatile关键字
C语言关键字volatile主要用途
在C语言中,volatile关键字是一个类型限定符,它告诉编译器被修饰的变量可能在程序执行过程中被意想不到地改变,这些改变可能来自于程序的外部(例如,由硬件或其他并发线程导致的)。因此,编译器在处理volatile变量时,不会对其进行优化假设,它会确保每次读写该变量时都从内存中读取或写入,而不是从寄存器或其他缓存中。
测试代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
//int gflag = 0;
//引入volatile关键字修饰,使得保持内存可见性,都从内存去取gflag
volatile int gflag = 0;
void changedata(int signo)
{
std::cout << "get a signo: " << signo << " ,change gflag 0->1" << std::endl;
gflag = 1;
}
int main()//主函数没有任何操作对gflag进行修改
{
signal(2, changedata);
while (!gflag);//while函数体不要写内容
//判断条件属于逻辑运算
//CPU的运算,算术运算和逻辑运算
//联想编译器优化
//理解本质是由CPU对gflag不断的检测。
//gflag由内存加载到CPU寄存器进行逻辑运算,true/flase
std::cout << "process quit normal" << std::endl;
return 0;
}
编译器优化等级:
man g++ //可查看优化级别
g++ test_volatile.cc -O0(大写字母O+数字0,表示优化级别,且O0表示不做优化)
g++ test_volatile.cc -O1(编译器优化级别1)— 导致寄存器隐藏了内存中的真实值,从而使得我们想要的输出得不到
说明,编译器的优化也要合适的使用,否则会导致编译器过度优化。所以要求编译器至少保持一种的方案,也就是优化要满足,保持内存的可见性。
所以基于语言级别,提供了一个关键字volatile,来方便编译器识别优化方案。
7.3、SIGCHID信号
子进程退出时,是干脆的直接退出吗?
答:不是,会给父进程发送信号 – SIGCHID信号
测试代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types,h>
#include <sys/wait.h>
int main()
{
//不想产生僵尸,直接手动回收
//父进程执行
//手动的设置对SIGCHLD进行忽略即可
signal(SIGCHLD,SIG_IGN);//就不会向父进程发送SIGCHLD信号了
//补充:查信号集表可知,SIGCHLD本身的默认动作不就是Ign忽略吗?为什么还要手动设置呢?
//答:系统的忽略时属于它自己有一套的动作,并不需要管的意思,而我们自己设置的SIG_IGN才是真的忽略它。
pid_t id = fork();
if (id == 0)
{
//child
int cnt = 5;
while (cnt)
{
std::cout << "child running" << std::endl;
cnt--;
sleep(1);
}
exit(1);
}
//father
while (true)
{
std::cout << "child running" << std::endl;
sleep(1);
}
}
问题1:如果是十个子进程,且同时退出呢?
答:十个子进程同时退出,意味着同时收到SIGCHLD,但是pending来不及记录全部信号,也就是只能同一时间记录标记一个信号。
所以就会导致只能回收一个子进程,所以有多个子进程时,最好分开处理,比如用循环依次标记回收处理。
问题2:如果有十个子进程,5个退出,另外5个永远不退出呢?
答:waidpid就会死循环的等,导致阻塞,再进一步导致main函数无法返回
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types,h>
#include <sys/wait.h>
void notice(int signo)
{
std::cout << "get a signal: " << signo << " pid: " << getpid() << std::endl;
while (true)
{
//pid_t rid = waitpid(-1, nullptr);//导致只会收一个
//pid_t rid = waitpid(-1, nullptr, 0);//导致阻塞
pid_t rid = waitpid(-1, nullptr, WNOHANG);//设置非阻塞模式
if (rid > 0)
{
std::cout << "wait child success, rid: " << rid << std::endl;
}
else if (rid < 0)
{
std::cout << "wait child success, rid: " << rid << std::endl;
break;
}
else
{
std::cout << "wait child success, rid: " << rid << std::endl;
break;
}
}
}
void DoOtherThing()
{
std::cout << "DoOtherThing-" << std::endl;
}
int main()
{
signal(SIGCHLD, notice);
pid_t id = fork();
if (id == 0)
{
std::cout << "I am child process, pid: " << getpid() << std::endl;
sleep(3);
exit(1);
}
//father
while (true)
{
DoOtherThing();
}
return 0;
}
8、测试用例
#define _CRT_SECURE_NO_WARNINGS 1
//信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/utime.h>
#include <sys/stat.h>
#include <string.h>
using namespace std;
void hander(int sig)
{
cout << "get signal: " << sig << endl;
}
//捕捉信号
void test_signal()
{
//对信号的捕捉,只需要捕捉一次,后续使用一直有效。
//捕捉信号
signal(2, hander);
signal(3, hander);
signal(4, hander);
signal(5, hander);
int n = 50;
while (n--)
{
cout << "hello signal" << endl;
sleep(1);
}
}
//kill系统调用
void test_signal2(int argc, char* argv[])
{
if (argc != 3)
{
cerr << "Usage: " << argv[0] << "signal pid" << endl;
return ;
}
pid_t pid = stoi(argv[2]);
int signum = stoi(argv[1]);
kill(pid, signum);
}
void hander2(int sig)
{
cout << "get raise: " << sig << endl;
}
//raise系统调用
void test_signal3()
{
int cnt = 20;
signal(3, hander2);
while (cnt--)
{
sleep(2);
raise(3);//3号信号
}
}
void hander3(int sig)
{
cout << "get abort: " << sig << endl;
}
//abort系统调用
void test_signal4()
{
int cnt = 20;
//signal(3, hander3);
signal(SIGABRT, hander3);
while (cnt--)
{
sleep(2);
abort();//6号,异常终止信号
}
}
void hander4(int sig)
{
cout << "get abort: " << sig << endl;
}
//捕捉不做处理 -->导致出异常,通过9号信号杀死,所以9号信号自然不允许捕捉。
void test_signal5()
{
int cnt = 20;
for(int i = 1;i<=31;i++)
signal(i, hander4);
while (cnt)
{
sleep(1);
cout << "hello signal,pid : " << getpid() << endl;
}
}
//验证I/O,设置全局变量
int cnt = 1;
void hander6(int sig)
{
alarm(1);
//cout << "get alarm: " << sig << endl;
cout << "cnt: " << cnt << "get alarm sig: " << sig << endl;
exit(1);
}
//alarm
void test_signal6()
{
//int cnt = 1;
signal(SIGALRM, hander6);
//设置闹钟
//alarm(5);//5s闹钟,5s后收到SIGALRM(14号信号 -- 默认是终止信号)
alarm(1);
//sleep(4);
//int n = alarm(2);//alarm(0);//表示取消闹钟,上一个闹钟的剩余时间
//cout << "n : " << n << endl;
//sleep(10);
while (cnt)
{
//sleep(1);
//cout << "cnt: " << cnt << endl;
cnt++;//只++
}
}
void hander7(int sig)
{
cout << "get err: " << sig << endl;
}
//异常触发信号
void test_signal7()
{
//程序为什么会崩溃?
//答:非法访问/操作,导致OS给进程发送了信号
signal(SIGFPE, hander7);
//signal(SIGSEGV, hander7);
//程序为什么崩溃了会退出呢?可以不退出吗?
//答:默认是异常就退出,可以不退出,那么就捕捉异常,推荐还是终止进程。
int s = 10;
s /= 10;//SIGFPE信号 -- 浮点数错误
//int* p = nullptr;
//*p = 100;//SIGSEGV信号 -- 野指针
while (true)
{
cout << "hello sig pid:" << getpid() << endl;
//int s = 10;
//s /= 10;
sleep(1);
//int* p = nullptr;
//*p = 100;
}
}
int main(int argc,char* argv[])
{
test_signal();
test_signal2(argc, argv);
test_signal3();
test_signal4();
test_signal5();
test_signal6();
test_signal7();
return 0;
}