目录
基本概述
类比生活
信号其实就像我们日常生活中遇见的红绿灯一样。
那么我们为什么能识别红绿灯?
有人教育过我们——让你在大脑中记住了对应的红绿灯属性或者行为。
比如:一个原始人来到十字路口他是不认识红绿灯的!(没有这种经历)。
信号亦是如此,只有让进程提前“学习过”信号,他才知道当它接收到几号信号该做什么事情!
信号到来我们必须立马去做吗?
就像我们的外卖来了,但是现在云顶之弈地下魔盗团开出了200块两个妮蔻!我们肯定选择先梭哈,而不是去拿外卖。
这就体现了信号的异步,当前进程可能正在处理更重要的信号所以现在给它发其他信号它不会去做。
外卖来了但是我们没空,那我们大脑里面需不需要记住这个信号呢?
当然需要,只有我们的大脑记住这个外卖在楼下,一会儿才能想起来去拿!
这也说明了,信号的到来,进程必须记住这个信号!也证明了此信号已经被处理。
如果一个信号是发给进程的,而进程要保存,那么应该保存在哪里呢?
保存在进程控制块task_struct里面。
如何保存?
根据是否收到了指定的信号[1,31]来将相应的比特位由0置1;
struct task_struct
{
unsigned int signal;
}
如何理解信号的发送?
发送信号的本质:修改PCB中的信号位图比如我们发9号信号,就让PCB里面信号位图中的9号位图由0置1。
PCB的管理者是OS,谁有权力修改PCB中的内容呢?
OS
无论未来我们学习多少种发送信号的方式,本质都是通过OS向进程发送信号的。OS必须要提供发送信号处理信号的相关系统调用。
ctrl c:热键——本质是ctrl c是一个组合键->OS->OS将ctrl+c解释成为2号信号 ——2)SIGINT
相关系统调用接口
signal函数
signal函数可以捕捉一个信号,将信号的默认动作改为自定义动作。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "进程捕捉到了一个信号,信号编号是:" << signo << endl;
exit(0);
}
int main()
{
// 这里是signal函数的调用,并不是handler的调用
// 仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用
// 一般这个方法不会执行,除非受到对应的信号
signal(2, handler);
while (true)
{
cout << "pid :" << getpid() << endl;
sleep(1);
}
return 0;
}
这样的话我们再向该死循环进程发送信号的时候,就不会执行默认动作了,而是执行我们的handler中的动作。
kill函数
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
返回值:
成功返回0,不成功返回-1;
模拟实现kill命令
#include <iostream> #include <unistd.h> #include <cstdio> #include <sys/types.h> #include <string> #include <signal.h> #include <signal.h> using namespace std; // 写一个使用手册 static void Usage(const string &proc) { cout << "\nUsage: " << proc << " pid sig\n" << endl; } // ./myprocess pid signo int main(int argc, char *argv[]) { // 判断接受到的参数数量是否为3 如果不为3 // 则展示出使用手册 if (argc != 3) { Usage(argv[0]); exit(1); } // 由于通过命令行参数传入的都是字符串 // 然而我们想要使用的却是输入的整型,因此需要用atoi转换一下 pid_t pid = atoi(argv[1]); int signo = atoi(argv[2]); int n = kill(pid, signo); if (n != 0) { perror("kill"); } // 1、通过键盘发送信号 // while (true) // { // cout << "hello wrold" << getpid() << endl; // sleep(1); // } return 0; }
raise函数
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <string>
#include <signal.h>
#include <signal.h>
using namespace std;
int main(int argc, char *argv[])
{
// raise()给自己发送任意信号
int cnt =0;
while(cnt<=10)
{
printf("cnt: %d\n",cnt++);
sleep(1);
if(cnt>=5) raise(3);
}
return 0;
}
使用raise给自己发送了3号信号,raise可以给自己发送任意信号。
abort函数
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <string>
#include <signal.h>
#include <signal.h>
using namespace std;
int main(int argc, char *argv[])
{
int cnt =0;
while(cnt<=10)
{
printf("cnt: %d\n",cnt++);
sleep(1);
if(cnt>=5) abort();
}
return 0;
}
abort函数可以给自己发送指定信号6) SIGABRT。
小总结
关于信号处理的行为的理解:有很多的情况,进程收到大部分的信号,默认处理动作都是终止进程。
信号的意义呢?
信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以一样!
硬件异常
除0错误
while(true)
{
cout<<"我在运行————————"<<endl;
sleep(1);
int a=666;
a/=0;
}
运行上述代码结果如下:
[mwb@VM-16-14-centos lesson2]$ ./mysignal
我在运行————————
Floating point exception/0对应着我们的8号信号
8) SIGFPE
为什么/0,会终止进程?
当前进程会收到来自OS的信号。
硬件异常产生信号信号的产生不一定非得用户显示的发送!
CPU的运算异常!
CPU里面有一个状态寄存器
当我们÷0的时候会导致计算的值溢出
会使状态寄存器中的溢出标记位由0变1
证明本次计算是非法的
CPU的运算异常-->OS必须知道-->因为OS是软硬件资源的管理者CPU的运算状态是要OS知晓的
OS通过看CPU的状态寄存器可以知道
1、CPU内出错了
2、谁导致CPU出异常的(谁引起的)所以OS再向进程发送信号来终止进程
为什么会一直打印?
收到信号不一定会引起进程退出
如果进程没有退出,有可能还会被调度--CPU内部
的寄存器只有一份,但是寄存器中的内容,属于当前进程的上下文
一旦出异常,我们有没有能力去处理异常呢?
没有!
当进程被切换的时候,就有无数次状态寄存器被保存和恢复的过程
所以每一次恢复的时候,就让OS识别到了CPU内部的状态寄存器中的
溢出标记位是1说白了由于该进程没有退出 然而寄存器中的内容属于当前进程的上下文
也就是这个溢出标记位没有改变仍然是1 当我们每次调度这个进程的时候OS就还能识别到CPU内部的状态寄存器中的溢出标记位仍然是1
野指针错误
while (true)
{
cout << "我在运行————————" << endl;
sleep(1);
int *p = nullptr;
// p = nullptr;
*p = 100;
}
运行结果如下:
[mwb@VM-16-14-centos lesson2]$ ./mysignal
我在运行————————
Segmentation fault11) SIGSEGV
OS是怎么知道我们野指针了呢?
当我们访问野指针的时候是访问了不属于我们的虚拟地址
当该虚拟地址通过页表向物理地址进行映射的时候有一个硬件叫做
MMU(集成在CPU上的 )
他是负责处理CPU的内存访问请求的计算机硬件。
当我访问的地址越界的时候,MMU因为越界访问发生异常。OS 识别到报错,将报错转化为信号发送给进程。
软件条件
我们之前学管道的时候学过,当一个管道的写端正在写入数据,读端正在读取数据的时候。
此时我们将读端关闭,由于OS不允许我们有任何浪费资源的行为,OS会向写端进程发送SIGPIPE信号将写进程终止。
alarm函数
int main(int argc, char *argv[])
{
// 4、软件条件
alarm(1);
int cnt = 0;
while (true)
{
cout << "cnt: " << cnt++ << endl;
}
}
运行结果:
14) SIGALRM
cnt: 88267
cnt: 88268
cnt: 88269
cnt: 88270
cnt: 88271
cnt: 88272
cnt: 88273
cnt: 88274
cnt: 88275
cnt: 88276
cnt: 88277Alarm clock
优化可求大概CPU一秒的运算次数!
int cnt = 0;
void catchSig(int signo)
{
cout << "获取一个信号,信号编号是:" << signo << endl;
cout << "累加的次数为cnt: " << cnt << endl;
exit(1);
}
int main(int argc, char *argv[])
{
// 4、软件条件
// 统计1s左右,我们的计算机能够将数据累计多少次
signal(SIGALRM, catchSig);
alarm(1);
while (true)
{
cnt++;
}
}
运行结果:
[mwb@VM-16-14-centos lesson2]$ ./mysignal
获取一个信号,信号编号是:14
累加的次数为cnt: 558925150
[mwb@VM-16-14-centos lesson2]$ ./mysignal
获取一个信号,信号编号是:14
累加的次数为cnt: 552961715
[mwb@VM-16-14-centos lesson2]$ ./mysignal
获取一个信号,信号编号是:14
累加的次数为cnt: 556320461
[mwb@VM-16-14-centos lesson2]$
解析:
由于第一次计算的时候每计算一次就会打印一次,我们之前提到过。外设的速度极慢,不停地访问外设导致大量的时间用来做IO的操作而不是累加,因此拖慢了我们统计次数的节奏,所以倘若我们只计算次数,在最后进程将要退出之前打印cnt累加的次数(只访问一次外设)因此速度会快不少 ,再加上我们使用的是云服务器当我们在打印的时候是通过网络显示到本地显示器上,IO就更慢了。
小总结
为什么设闹钟就是软件条件呢?
计算机里面你的闹钟就是用软件实现的!
任意一个进程,都可以通过alarm系统调用在内核中设置闹钟,OS内可能会存在着很多的闹钟,那么OS要不要管理这些闹钟呢?
要! 先描述,再组织!
struct alarm
{
uint64_t when;//未来的超时时间
int type; //闹钟类型,一次性的,还是周期性
task_struct *p;//这个闹钟跟哪个进程相关
struct alarm *next;
}
OS会周期性的检测这些闹钟。
curr_timestamp > alarm.when:超时了
OS 发送SIGALARM - > alarm.p;操作系统内部对闹钟的管理其实就是对链表的增删查改
Term与Core
man 7 signal可以查看。
Term 是正常结束
Core 是操作系统除了要终止还要做其他的
有核心转储。
core
Core 是操作系统除了要终止还要做其他的有核心转储。
在云服务器上,默认如果进程是core退出的,我们暂时看不到明显的现象。
ulimit -a
查看当前系统给我们当前系统设置的各种资源上限
ulimit -c +所要设置的core file
核心转储
当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据转储到磁盘中 -- 核心转储!
为什么要有核心转储呢?
当我们程序崩溃的时候,我们最想知道为什么崩溃?在哪里崩溃?
OS为了便于后期我们做调试,他会讲我们进程运行期间出现崩溃的代码的相关上下文数据全部给我们dump到磁盘当中,用来支持调试!
如何支持调试呢?
首先在形成可执行程序的时候 带上-g选项 以便于支持调试
然后 gdb mysignal
core-file core.xxxx
这个操作叫做事后调试
如果是以Term 那就是正常退出
捕捉不到的kill命令
void catchSig(int signo)
{
cout << "获取一个信号,信号编号是:" << signo << endl;
// alarm(1);
// exit(1);
}
int main(int argc, char *argv[])
{
// 对31个信号全部捕捉
for (int signo = 1; signo <= 31; signo++)
{
signal(signo, catchSig);
}
while (true)
{
cout << "我在运行: " << getpid() << endl;
sleep(1);
}
}
如果把1~31号信号全部捕捉了,并且捕捉了不退出进程那么我们再发送信号进程就不会退出了
除了9号信号和19号
9号:OS不允许对9号信号进行捕捉,就算我们捕捉了也没用。
19号:并没有让进程退出而是暂停了,我们使用18号可以让进程继续运行。
阻塞信号
基本概念
信号产生与信号递达之间有个时间窗口,进程就必须暂时将信号保存起来,通过位图进行保存。
1、实际执行信号的处理动作称为信号递达。
2、信号从产生到递达之间的状态,称为信号未决。
3、进程可以选择阻塞某个信号。
4、被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
5、注意,阻塞和忽略是不同的,只要信号被阻塞就不会被递达,而忽略是在递达之后可选的一种处理动作。
信号在内核中的示意图:
pending位图
基本概念
第一个比特位就代表第一个信号,如果为1则表示收到该信号,为0表示没有收到该信号。
a、比特位的位置 b、比特位的内容
发信号
OS向进程发信号 其实就是向目标进程的pending位图当中设置比特位,将pending位图中比特位由0置1。
因此发信号其实实际意义上为写信号更准确。
因为PCB属于OS系统内核数据结构 只有OS有权利去修改pending位图,所以发送信号的载体只能是OS,不能是其他人。
block位图
基本概念
比特位的位置: 信号编号。比特位的内容:是否阻塞了对应的信号。
举例说明
下面是一个pending位图和block位图。
pending: 0000 0000 0000 0000 0000 0000 0000 1000
block: 0000 0000 0000 0000 0000 0000 0000 1000
因为阻塞了,所以4号信号不会递达,除非未来解除阻塞。
通过位运算来判断
通过函数来判断某个signo信号是否递达或者是否阻塞
if(((1<<(signo -1)) & pcb->block)
{
// signo信号使被阻塞的,不递达
}
else
{
if((1<<(signo-1))&pcb->block)
{
//递达该信号
}
}
handler函数指针数组
handler_t handler[32] = {0};
基本概念
数组,是有下标的!
a. 数组的位置(下标) 信号的编号
b. 数组下标对应的内容,表示对应的信号的处理方法
通过handler来解释signal函数
//可以对指定的信号设置特定的捕捉/回调方法
signal(signo,handler);
拿着这个signo信号在对应着这个数组的位置当中查找对应的位置。
将handler用户层设置的方法,将该函数的地址填入到 对应handler[32]数组对应的下标处,未来当我们的信号产生时,修改比特位,并且该比特位没有被阻塞,信号被OS递达时,OS就立马拿着这个信号,收到之后 根据你这个信号找到对应的信号编号然后访问到对应的handler数组的方法。
结论:
如果一个信号没有产生,并不妨碍它先被阻塞。(pending位图和block位图是独立的)。
进程为何能识别信号呢?
因为程序员为每个进程都设计了这三种数据结构(pending位图、block位图、handler表)。
用户态和内核态
信号是如何实现捕捉的?
信号产生的时候,不会被立即处理,而是在合适的时候。从内核态返回用户态的时候,进行处理。
用户态
1、访问OS自身的资源(getpid,waitpid)
2、访问硬件资源(printf,write,read)
内核态
什么时候需要切换到内核态?
用户为了访问内核或者硬件资源,必须通过系统调用完成访问。
比如:
vector 自动扩容 向OS要资源 -- 申请内存
切换需要注意什么?
调用系统调用的时候,普通用户无法以自身用户态的身份调用系统调用必须把身份切换为内核态。
实际执行系统调用的“人”是进程,但身份其实是内核,往往系统调用比较费时间一些,因此尽量避免频繁调用系统调用。
我们一个进程在实际执行的时候,它一定要把自己的上下文信息投递到CPU当中,在CPU当中,一定会存在大量的寄存器。
寄存器:
1、可见寄存器
2、不可见寄存器
进程切换的时候上下文数据怎么处理?
凡是和当前进程强相关的,上下文数据。
每个进程在切换的时候,可以把自己的上下文带走。当它回来的时候,可以再把它的上下文拿回来。
CR3寄存器
表征当前进程的运行级别:0:内核态 3 :用户态
再谈进程地址空间
每一个进程都有自己独立的用户级页表!
映射到对应的物理内存处!
操作系统在内存中只有独一份!内核级页表只有一份就够了。
内核级页表
每一个进程都有进程地址空间(用户空间独占)内核空间(被映射到了每一个进程的3~4G处)
进程要访问OS的接口,其实只需要在自己的地址空间上进行跳转就可以了。
每一个进程都有3~4GB,都会共享一个内核级页表,无论进程如何切换,会不会更改任何的3~4G。
用户,凭什么能够执行访问内核的接口或者数据?
OS捕捉到你这个行为的时候,会对你做权限认证。看CPU中的CR3中的运行级别是否为内核态。
那么如何才能切换成内核态呢?
系统调用,起始的位置会帮我们做切换的。
CR3中的 3 ---> 0 == 用户态 ---> 内核态
信号的捕捉流程
基本概念
信号产生的时候,不会被立即处理,而是在合适的时候-->从内核返回用户态的时候,进行处理-->曾经我一定是进入了内核态-->系统调用/进程切换
通过block和pending来判断
先查block表,如果block为0,那么没有阻塞,pending也为0说明没收到信号。
block为1为阻塞 直接就不管了
block为1 不管了
block为0 pending为1 处理handler对应匹配的方法
分为 默认(执行默认的操作,大部分都会使进程终止) 忽略(将对应pending位图由1置为0 不处理) 自定义
handler的自定义操作
基本概念
跳转到用户态执行对应的handler方法
sigset_t信号集
基本概念
sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态
在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
为了使我们操作系统更好使用信号,则提供了以下接口:
#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
sigprocmask函数可以用于读取或更改进程的信号屏蔽字(阻塞信号集)
函数原型
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
set:输入型参数
oset:输出型参数
set为非空指针的话,说明要更改进程的信号屏蔽字。(如何操作看how)
oset为非空指针的话,则读取当前信号屏蔽字通过oset传出。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里面,然后根据set和how参数更改信号屏蔽字。
sigpending
sigpending函数可以用于读取进程的未决信号集
1、默认情况:我们的所有信号都是不被阻塞的
2、默认情况:如果一个信号被屏蔽了,该信号不会被递达
如果给2号屏蔽了(屏蔽可以理解为阻塞),那么ctrl c就不会执行对应的动作了(默认,忽略,自定义)
并且,在屏蔽之后还收到了2号信号,2号信号不被递达就会一直保存在pending位图里面。
如果我们还不断打印进程对应的pending信号集位图。收到2号前000000000
收到2号后第二个比特位由0置1。
代码示例1:
屏蔽2号和3号信号,并且对2号3号进行pending信号集的重复打印
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <vector>
using namespace std;
// #define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31
static void show_pending(const sigset_t &pending)
{
for (int signo = MAX_SIGNUM; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
cout << "1";
else
cout << "0";
}
cout << "\n";
}
static vector<int> sigarr = {2, 3};
int main()
{
// 1.先尝试屏蔽指定的信号
sigset_t block, oblock, pending;
// 1.1 初始化
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
// 1.2 添加要屏蔽的信号
// sigaddset(&block, BLOCK_SIGNAL);
for (const auto &sig : sigarr)
sigaddset(&block, sig);
// 1.3 开始屏蔽,设置进内核(进程)
sigprocmask(SIG_SETMASK, &block, &oblock);
// 2.遍历打印pending信号集
while (true)
{
// 2.1 初始化
sigemptyset(&pending);
// 2.2 获取
sigpending(&pending);
// 2.3 打印它
show_pending(pending);
// 3.慢一点
sleep(1);
}
return 0;
}
代码示例2:
屏蔽2号信号之后 打印pending信号集然后再解除屏蔽
再打印pending信号集
因此可以看到pending位图
0000 0000 0000 0000 0000 0000 0000 0000
--->
0000 0000 0000 0000 0000 0000 0000 0010
--->
0000 0000 0000 0000 0000 0000 0000 0000
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <vector>
using namespace std;
// #define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31
static void show_pending(const sigset_t &pending)
{
for (int signo = MAX_SIGNUM; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
cout << "1";
else
cout << "0";
}
cout << "\n";
}
// static vector<int> sigarr = {2, 3};
static vector<int> sigarr = {2};
static void myhandler(int signo)
{
cout << signo << "号信号已经被递达!!" << endl;
}
int main()
{
for (const auto &sig : sigarr)
signal(sig, myhandler);
// 1.先尝试屏蔽指定的信号
sigset_t block, oblock, pending;
// 1.1 初始化
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
// 1.2 添加要屏蔽的信号
// sigaddset(&block, BLOCK_SIGNAL);
for (const auto &sig : sigarr)
sigaddset(&block, sig);
// 1.3 开始屏蔽,设置进内核(进程)
sigprocmask(SIG_SETMASK, &block, &oblock);
// 2.遍历打印pending信号集
int cnt = 10;
while (true)
{
// 2.1 初始化
sigemptyset(&pending);
// 2.2 获取
sigpending(&pending);
// 2.3 打印它
show_pending(pending);
// 3.慢一点
sleep(1);
if (cnt-- == 0)
{
// 让之前保存的覆盖现在的
sigprocmask(SIG_SETMASK, &oblock, &block);
// 一旦特定信号进行接触屏蔽
// 一般OS至少马上递达一个信号
cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
}
}
return 0;
}
sigaction
函数原型
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction实际上是一个结构体:
//这里忽略了一些用不到的变量
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
}
act:输入型参数 oact:输出型参数
成功返回0,失败返回-1;
代码示例1:
捕捉2号信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "get a signo: " << signo << endl;
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, &oact);
while (true)
sleep(1);
return 0;
}
代码示例2:
向同一个进程连续的发送相同的信号
#include <iostream>
#include <signal.h>
#include<cstdio>
#include <unistd.h>
using namespace std;
void Count(int cnt)
{
while(cnt)
{
printf("cnt: %2d\r",cnt--);
fflush(stdout);
sleep(1);
}
cout<<endl;
}
void handler(int signo)
{
cout << "get a signo: " << signo <<"正在处理中…………"<< endl;
Count(20);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, &oact);
while (true)
sleep(1);
return 0;
}
结论
1、当我们正在进行递达某一个信号期间,同类型信号无法递达!
——当当前信号正在被捕捉的信号,系统会自动将当前信号加入到进程的信号屏蔽字,pending位图表示相同信号的比特位只有一位!!!!
为什么发送多个相同信号却能收到两个信号?
因为第一个已经递达了,只不过还在sleep。位图此时已经被由1置为0了,然后第一个进行对应的handler方法。
因此第二个可以继续对pending位图进行修改。
但是当第二个信号将pending位图置为1的时候后边的信号再对pending位图进行修改就没意义了,由于第一个在sleep第二个此时正在未决状态,再修改pending位图也是1--->1,没意义。因为修改的是同一个位图。
2、当信号完成捕捉动作,系统又会自动解除对该信号的屏蔽。
一般一个信号被接触屏蔽的时候,会自动进行递达当前屏蔽信号,如果该信号已经被pending的话,没有就不做任何动作。
代码示例3:
当我们捕捉二号信号的时候,同时屏蔽三号信号
#include <iostream>
#include <signal.h>
#include<cstdio>
#include <unistd.h>
using namespace std;
void Count(int cnt)
{
while(cnt)
{
printf("cnt: %2d\r",cnt--);
fflush(stdout);
sleep(1);
}
cout<<endl;
}
void handler(int signo)
{
cout << "get a signo: " << signo <<"正在处理中…………"<< endl;
Count(20);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);// 当我们正在处理某一种信号的时候,我们也想屏蔽其他信号
// 就可以添加到这个mask当中
// 在sa_mask中添加3号信号,从而实现我们正在捕捉二号信号但是同时可以屏蔽3号信号
sigaddset(&act.sa_mask,3);
sigaction(SIGINT, &act, &oact);
while (true)
sleep(1);
return 0;
}
代码解析:
我们连续发送2号信号和三号信号暂时是不会退出进程的但是当两个二号信号sleep过后,也就是这两个2号信号处理过后,就轮到我们发送的3号信号了,由于我们发送的三号信号并没有进行捕捉,默认行为还是可以退出进程的。因此,最后我们看到的现象就是退出进程。
结论:
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来
的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果
在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需
要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
可重入函数
代码
示意图
解析
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
不可重入函数的条件:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile
示意图:
代码示例1:
// makefile:
mysignal:mysignal.c
gcc -o $@ $^ -O3
.PHONY:clean
clean:
rm -f mysignal
// ************************
// mysignal.c :
#include <stdio.h>
#include <signal.h>
int quit = 0;
void handler(int signo)
{
printf("%d 号信号,正在被捕捉!\n", signo);
printf("quit: %d", quit);
quit = 1;
printf("--> %d\n", quit);
}
int main()
{
signal(2, handler);
while (!quit);
printf("注意:我是正常退出的\n");
return 0;
}
运行现象:没有退出
[mwb@VM-16-14-centos lesson4]$ ./mysignal
2 号信号,正在被捕捉!
quit: 0--> 1
解析:
由于makefile中我们使用的是O3优化。本来我们检测quit的值是在物理内存中检测的,可是优化后把quit=0整到CPU的寄存器里面了,因此物理内存中的quit改变了,但while并不去物理内存中去检测了,而是只看CPU寄存器里面的。
加上valatile后
// volatile :保持内存可见性
// 检测quit值的时候不要优化到寄存器里面
// 我要时时刻刻在内存中检测quit的值
volatile int quit = 0;
void handler(int signo)
{
printf("%d 号信号,正在被捕捉!\n", signo);
printf("quit: %d", quit);
quit = 1;
printf("--> %d\n", quit);
}
int main()
{
signal(2, handler);
while (!quit)
;
// 由于quit在while中只被做检测,不做修改
// O3优化后while检测quit的时候就不去物理内存中去检测了
// 而是直接检测CPU寄存器内的quit数据
// 当物理内存中的数据改变的时候,CPU寄存器里的数据是不变的
printf("注意:我是正常退出的\n");
return 0;
}
现象:
[mwb@VM-16-14-centos lesson4]$ ./mysignal
2 号信号,正在被捕捉!
quit: 0--> 1
注意:我是正常退出的
[mwb@VM-16-14-centos lesson4]$
SIGCHLD
代码示例:
子进程在死亡的时候,会向父进程直接发送SIGCHLD信号的。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void Count(int cnt)
{
while (cnt)
{
printf("cnt: %2d\r", cnt--);
fflush(stdout);
sleep(1);
}
printf("\n");
}
void handler(int signo)
{
printf("pid: %d, %d 号信号,正在被捕捉!\n", getpid(), signo);
}
int main()
{
signal(SIGCHLD, handler);
printf("我是父进程,pid: %d,ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0)
{
printf("我是子进程,pid: %d,ppid:%d我要退出啦\n", getpid(), getppid());
Count(5);
exit(1);
}
while (1)
sleep(1);
return 0;
}
运行结果
[mwb@VM-16-14-centos lesson4]$ ./mysignal
我是父进程,pid: 21482,ppid:4819
我是子进程,pid: 21483,ppid:21482我要退出啦
cnt: 1
pid: 21482, 17 号信号,正在被捕捉!
^\Quit
[mwb@VM-16-14-centos lesson4]$
使用waitpid在handler方法中等待子进程
void handler(int signo)
{
// 1、我又非常多的子进程,在同一个时刻退出了
// 因此我们用waitpid等待的时候应该while循环的等待子进程退出
while (1)
{
// 2、我有非常多的子进程,在同一个时刻只有一部分退出了
// 因此我们在等待的时候父进程根本不知道会有几个进程退出
// 因此会一直等待,然而此时我们还是阻塞等待
// 父进程什么都干不了,只能静静的等待子进程
// pid_t ret = waitpid(-1, NULL, 0);
// 因此应该非阻塞等待
// 当有子进程退出的时候,我们等待你
// 当没有子进程退出的时候父进程不会阻塞在那里
pid_t ret = waitpid(-1, NULL, WNOHANG);
if (ret == 0)
break; // 当ret==0说明这一轮的进程已经回收完了 父进程就不会一直阻塞等待了
}
printf("pid: %d, %d 号信号,正在被捕捉!\n", getpid(), signo);
}