目录
生活中的信号
Linux中的信号
前台进程与后台进程
信号的产生
核心转储 core dump
编辑信号的其他相关概念
信号处理的三种方式
信号在内核中的表示示意图
sigset_t 类型
信号集操作函数
sigprocmask
sigpending
综合练习
用户态与内核态
信号的捕捉过程
sigaction函数
可重入函数
volatile关键字
SIGCHLD信号
生活中的信号
● 信号没有产生时,我们就知道如何处理对应的信号
比如你现在在家里,但你仍然知道红灯停,绿灯行
● 信号的产生是异步的,也就是我们不知道信号什么时候到来
比如你点了外卖后开始打游戏,打游戏期间你也不知道什么时候外卖员会给你打电话
● 信号产生了,可以不立即处理,而是在合适的时候处理
比如电话通知你外卖到了,你可以暂时不取,等打完游戏再取外卖
● 信号产生后可以稍后处理,这就要求你必须要有暂时保存信号的能力
比如电话通知你外卖到了,你可以暂时不取,你的大脑中已经保存了外卖到了的信息,你是可以稍后取外卖的
Linux中的信号
● 在OS中,上述的"我,你,我们"本质都是进程,信号在OS中就是一种向目标进程发送通知消息的一种机制!
● 在进程被设计的时候,源代码中就内置了很多对信号的识别和处理动作,因此进程没有收到任何信号之前,进程就认识所有信号,并且知道当信号到来时应该如何处理对应的信号
● kill -l 查看系统中的信号
其中,每个信号都有数字和大写的英文单词,显然是宏定义,数字就是信号的编号!
普通信号从 1-31, 实时信号从34-64, 没有0号信号,可以理解为0表示进程没有收到任何信号,也就是进程代码是正常执行完毕的!本篇博客只介绍普通信号!
● 系统中每个进程都维护了一张函数指针数组表,数组下标和信号编号强相关,数组内容就是相应信号的处理方法,因此进程知道如何处理相应的信号!
前台进程与后台进程
● 前台进程只能有1个,因为键盘只有1个,能接收键盘输入的必然是前台进程
● 后台进程可以有多个,一般耗时任务比较长的任务会放到后台执行,比如下载任务
● 前台进程可以接收键盘输入,可以被ctrl + c 终止掉,此时bash是后台进程,无法执行其他指令
● 后台进程不能接收键盘输入,无法被ctrl + c 终止掉,此时bash是前台进程,可以执行其他指令
● 当前台进程(bash除外)被杀掉之后, bash会自动把自己提到前台
● 前台进程如果被暂停(ctrl+z),会立刻变为后台进程
● 启动前台进程: ./process, 启动后台进程: ./process &
● jobs指令: 查看系统中已经启动的任务
● fg 任务编号,将后台任务提为前台任务
● bg 任务编号,启动后台暂停的任务
信号的产生
OS如何知道键盘有数据了?
● OS是硬件的管理者,硬件有很多,如果OS一直轮询(比如每隔一段时间)检测硬件的状态,时间间隔长了OS不能及时得到有效信息,时间间隔短了OS太忙了,显然不现实!
● OS在设定时,就用到了中断技术: cpu内部除了有运算器,还有控制器,OS不直接和外设在数据层面上打交道,但是要和外设在控制信息层面上打交道!
● cpu上有针脚(有编号), 和主板的硬件电路直接来连接,主板和外设直接连,外设可以向cpu发送光电信号,这种信号就叫做中断信号,而针脚编号叫做中断号!
● 外设向cpu发送的中断信号会被存放在cpu寄存器中,就可以被程序读取了,就把硬件信息转化成了软件信息
● 为了对外部的中断信号做出相应,系统中存在一张中断向量表,本质是函数指针数组,数组下标是中断号,数组内容是相应硬件的读取方法
● 硬件中断信号的发出对于OS是异步的,而信号本质其实就是用软件来模拟中断的行为
如何理解向目标进程发送信号?
● 一个进程可能会收到多个信号,因此进程要对多个信号进行管理
● 管理信号的两个核心问题:是否收到了信号,收到了几号信号
● 显然task_struct中只需要维护一张位图即可,比特位的位置表示信号编号,比特位的内容表示是否收到了对应信号,由于只有31个普通信号,只需要一个32位的整数充当位图即可
● 发送信号本质是将位图对应的比特位由0改1,因此发信号本质是写信号
● OS是进程的管理者,无论是何种方式产生的信号,最终都是OS向目标进程发信号
信号产生的三种方式
1.键盘产生
● 键盘输入的数据分两种,一种是常规输入数据,一种是控制数据(如组合键,表示向进程发信号)
● 组合键表示向进程发送对应的信号,如 ctrl + c == 2号信号,ctrl + z == 19号信号,ctrl + \ == 3号信号
● 我们可以使用signal函数修改信号的默认处理行为,同时可以验证键盘组合键等同于发信号
sighandler_t signal(int signum, sighandler_t handler);
功能: 将编号为signum的信号的默认处理动作改为自定义处理行为(又叫信号捕捉,handler方法)
参数: signum表示信号编号,handler是信号的自定义处理行为
返回值: 返回的是本次修改为自定义处理行为前的信号处理方式
#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
void func(int signo)
{
cout << "收到了2号新号" << endl;
exit(1);
}
int main()
{
signal(2, func);
while(true)
{
cout << "I am running..., pid :" << getpid() << endl;
sleep(1);
}
return 0;
}
● 9号信号被称为管理员信号,是不能被自定义捕捉的,否则一个进程对所有信号自定义捕捉后就永远无法被终止了!
#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
void func(int signo)
{
cout << "收到了9号信号" << endl;
exit(1);
}
int main()
{
signal(9, func);
while(true)
{
cout << "I am running..., pid :" << getpid() << endl;
sleep(1);
}
return 0;
}
● man 7 signal 查看信号的默认处理动作
2.系统调用/函数产生
kill系统调用:
int kill(pid_t pid, int sig);
功能: 向pid进程发送sig信号
参数: 进程pid, 信号编号sig
返回值: 成功返回0,失败返回-1
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
#include <cstring>
#include <unistd.h>
using namespace std;
static void Usage(const string& proc)
{
cout << "\nUsage: " << proc << " -sigNumber processId" << endl << endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
int sigNumber = stoi(argv[1] + 1);
int processId = stoi(argv[2]);
kill(processId, sigNumber);
return 0;
}
raise函数:
int raise(int sig);
功能: 哪个进程调用raise,就向哪个进程发送sig信号
参数: 信号编号sig
返回值: 成功返回0,失败返回非0
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
}
int main()
{
signal(2, handler);
while(1)
{
raise(2);
sleep(1);
}
return 0;
}
abort函数:
void abort(void);
功能: 哪个进程调用abort函数,直接收到 SIGABRT 信号,直接终止当前进程
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
using namespace std;
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
}
int main()
{
signal(6, handler);
abort(); //即使6号信号被捕捉了,进程也会被abort直接终止掉!
while(1)
{
cout << "I am running" << endl;
sleep(1);
}
return 0;
}
3.异常
● cpu中有一个状态寄存器,当发生除零异常,状态寄存器中的溢出标志位从0变1,cpu显示异常
● os是硬件的管理者,当os发现cpu异常,会立即发信号杀掉引起异常的进程
● 杀掉进程后,状态寄存器的溢出标志位就会从1变0,cpu恢复正常了
验证除零异常会收到8号信号
#include <iostream>
#include <sys/types.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
sleep(1);
//exit(0);
}
int main()
{
signal(8, handler); //捕捉除0异常
int a = 10;
a /= 0; //不能直接除0,是数学的规定
return 0;
}
● 我们对8号信号进行了自定义捕捉,因此发生除0错误时会执行自定义handler方法
● 如果handler中加了exit函数,执行handler方法后进程会立即退出,这是意料之中
● 但如果handler中没有exit函数,最后会死循环执行打印语句
● 当进程发生除0异常,cpu的状态寄存器的溢出标志位从0变1,会通知os自己出异常了,os会处理异常,而我们对信号处理方法进行了自定义捕捉,因此os在屏幕上打印了一句话就认为自己处理完了,实际上cpu的状态寄存器的溢出标志位仍然是1,当调度到该进程时,依然处于异常状态,循环往复
验证段错误异常会收到11号信号
● 段错误本质是一种越界行为,比如空指针解引用,0号虚拟地址空间对应的物理空间并没有分配给用户,当用户对空指针解引用时,会去访问0号虚拟地址空间对应的物理空间,此时MMU硬件单元会转化出错,通知os,os向进程发送信号
#include <iostream>
#include <sys/types.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
sleep(1);
//exit(0);
}
int main()
{
signal(11, handler); //捕捉段错误异常
int* p = nullptr;
*p = 2;
return 0;
}
4.软件条件
● 匿名管道通信时,读端关闭,写端还在写,OS检测到读端文件描述符已经关闭了,就会直接向写端发送SIGPIPE信号杀掉写端进程,此时信号的产生的来源是软件条件!
● 不是所有的信号都是软硬件异常产生的!还有让进程暂停、继续等其他事情,说明还有非异常问题也会产生信号!
● Linux中的闹钟本质就是非异常问题产生的信号
OS中的时间
● 电脑一开机就知道时间,是因为纽扣电池在固定时间间隔给计算机硬件发信号,记录时间戳
● 电脑用上多年,时间可能不太准了,意味着纽扣电池寿命不久了
其他进程都是被OS调度的,那么OS开始是如何跑起来的呢?
● OS的本质就是一个死循环 while(1) { pause() }
● CMOS硬件周期性的,高频率的,向cpu发送时钟中断(也就是cpu的主频),os会去中断向量表中找到对应CMOS中断号的方法,也就是os的调度方法: while(1) { pause() }
● 所以说OS的执行是基于硬件中断的!
OS中的闹钟
● 闹钟在任何OS天然就是要被支持的,而OS内可以设置多个闹钟, 要把闹钟管理起来,先描述,再组织, 定闹钟,就创建一个内核数据结构对象 struct clock { ... }
● 结构体中有什么字段? 比如闹钟响的时间;要能够很好进行时间比对,比如时间戳! 闹钟id, 用户名, 用户id, 什么时间点设置的,闹钟响了向谁发信号等等
● 如何组织设置的闹钟, 如何知道哪些超时了? 只需要按时间为键值建立大/小堆,对闹钟的管理就变成了对数据结构堆的增删改查
alarm系统调用:
unsigned int alarm(unsigned int seconds);
功能: 设置一个闹钟,闹钟响之后,进程会收到SIGALRM信号,进程终止
参数: 闹钟在seconds秒之后响
返回值: 在本次调用alarm之前,没有设置过闹钟,则返回0;如果已经设置过闹钟,则旧的闹钟会被新的闹钟取代,并返回设置过的闹钟的剩余时间;如果参数传0,代表取消历史闹钟,并返回历史闹钟的剩余时间
对比访问外设和访问内存的速度差异
第一份代码访问了外设, 而第二份代码没有访问外设,因此速度差异非常明显!
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
int cnt = 0;
int main()
{
alarm(1);
while(true)
{
cout << "alarm : " << cnt++ << endl;
}
return 0;
}
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
int cnt = 0;
void handler(int signo)
{
cout << "get a signal : " << signo << ", alarm : " << cnt++ << endl;
exit(0);
}
int main()
{
signal(14, handler);
alarm(1);
while(true)
{
cnt++;
}
return 0;
}
定一个闹钟只响一次:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
int n = 0;
void handler(int signo)
{
n = alarm(0); //设置0就是将历史闹钟取消掉, 历史闹钟如果还没响,就返回历史闹钟剩余时间
cout << "result: " << n << endl;
exit(0);
}
int main()
{
signal(14, handler);
cout << "pid : " << getpid() << endl;
alarm(15); //闹钟15秒后响起
while(true)
{
sleep(1);
cout << "running ..." << endl;
}
return 0;
}
定一个闹钟每隔2秒响一次:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
int n = 0;
void handler(int signo)
{
alarm(2); //2秒之后响
cout << "result: " << n << endl;
}
int main()
{
signal(14, handler);
cout << "pid : " << getpid() << endl;
alarm(2); //闹钟2秒后响起
while(true)
{
sleep(1);
cout << "running ..." << endl;
}
return 0;
}
核心转储 core dump
● 当进程收到信号终止运行后,会将当前进程的核心上下文数据写到一个磁盘上的临时文件,这个磁盘上的临时文件就是核心转储文件
● 核心转储文件的名字一般是 进程pid.core 或 core.进程pid
● 云服务器下默认是把core dump选项关闭的,否则进程一旦异常终止就会形成核心转储文件,会占据较大的磁盘空间
● gdb core.pid 可以很好的排查定位出错位置和出错原因
信号的其他相关概念
● 信号递达: 实际执行信号的处理动作
● 信号未决: 信号从产生到递达之间的状态
● 信号阻塞: 信号是允许被阻塞的,阻塞的意思是暂时不进行递达,直到解除对信号的阻塞才能抵达该信号, 信号被阻塞时处于未决状态!
ps1: 注意区分信号忽略和信号阻塞,忽略本身就是对信号做了处理,处理方式就是忽略,也就是忽略信号是递达了的!而信号阻塞是进程主动屏蔽了特定信号,所以信号产生后就无法被递达,直到解除屏蔽才能被递达
ps2: 信号是未决的,不一定被阻塞了,也可能是没来的及处理, 而信号被阻塞了,一定是未决的
信号处理的三种方式
1.默认动作
2.自定义捕捉
3.忽略处理
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "handler :" << signo << endl;
}
int main()
{
cout << "getpid :" << getpid() << endl;
cout << "自定义行为" << endl;
signal(2, handler); //自定义
sleep(5);
cout << "恢复默认行为" << endl;
signal(2, SIG_DFL); //恢复默认处理动作
sleep(10);
return 0;
}
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "handler " << signo << endl;
}
int main()
{
cout << "getpid: " << getpid() << endl;
signal(2, handler);
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, 2); //将2号信号添加到自己定义的block信号集中
sigprocmask(SIG_BLOCK, &block, &block);
while(true)
{
sleep(1);
}
return 0;
}
总结
信号的默认处理和忽略动作,第二个参数都是大写的英文单词,本质就是宏,将0/1强制类型转化成函数指针类型,所以使用signal函数时,系统会先判断参数是否为0/1,如果为0/1,执行默认处理动作/对信号进行忽略,不为0/1,才去执行回调函数,进行自定义捕捉!
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
信号在内核中的表示示意图
● 每个进程都维护了三张表,block表和pending表是位图表,handler表是函数指针数组,三张表的下标表示信号编号(下标从0开始,信号编号从1开始,对应加1减1即可)
● 三张表的含义:
pending位图表:发信号本质是修改pending位图,对应位置从0改1
handler表: 是一个函数指针数组,保存的是对对应信号的处理方法
block位图表: 是否对特定的信号进行屏蔽(阻塞)
● 进程启动时,pending表和handler表就被设置好了,利用pending表和handler表,我们就可以识别并且处理信号了
● 具体到1个信号,三张表应该横着看,比如:
第一行: 没有收到1号信号,对1号信号没有屏蔽,处理行为是默认动作
第二行: 收到了2号信号,对2号信号屏蔽了,尽管处理动作是忽略,但由于信号阻塞,不会处理
第三行: 没有收到3号信号,对3号信号屏蔽了,即便将来收到了3号信号,也不会进行处理
● 能不能执行handler表中对应的方法,最终还是要看block表中的信号是否被阻塞,而最终执行了handler表中的方法后,pending位图中的比特位会由1置为0;当进程收到了信号,但信号被阻塞了,不能执行方法,但是一旦解除阻塞了,信号就要立即被处理!
● 如果在进程解除对某信号的阻塞之前,这种信号产生过多次,将如何处理?
POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次, 而实时信号在递达之前产生多次可以依次放在一个队列里
● 所有对信号的操作都是围绕三张表展开的,而三张表都是内核数据结构,用户无法直接修改,OS要给我们提供系统调用来访问修改三张表
● 因为三张表都是内核数据结构,因此OS必须提供对应的系统调用来修改三张表, handler数组改直接可以拿着下标改,但是另外两张表要用户自己用位操作修改位图吗? 对用户要求太高了,因此OS不仅提供了系统调用,还提供了特定的数据类型(比如pid_t是OS提供的)
sigset_t 类型
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
● 在内核角度,三张表叫block位图表、pending位图表、handler表,在用户角度,block位图表又叫阻塞信号集(或信号屏蔽字), pending位图表又叫未决信号集
● sigset_t 类型本质就是信号集,该类型可以表示每个信号的"有效"或"无效"状态,比如在阻塞信号集中,“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态
● sigset_t 虽然是OS提供的,但是使用时就当成普通类型来用就行, 在底层本质是位图结构
● 对信号集的操作,我们也不必自己去用位操作修改,直接使用系统提供的信号集操作函数即可
信号集操作函数
sigprocmask
● 调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集), 成功返回0,失败返回-1
● how指的是我们要对信号屏蔽字做哪种操作,可以传递以下三个字段
● oldset是一个输出型参数,无论对block表进行何种操作,都是对block表的修改,把block表修改了,如果后续想恢复呢? 传入oldset参数就可以把旧的信号屏蔽字的内容带出来!
● 代码演示(屏蔽2号信号)
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "handler " << signo << endl;
}
int main()
{
cout << "getpid: " << getpid() << endl;
signal(2, handler);
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, 2); //将2号信号添加到自己定义的block信号集中
sigprocmask(SIG_BLOCK, &block, &block);
while(true)
{
sleep(1);
}
return 0;
}
● 结论: 9号信号是管理员信号,无法被屏蔽
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "handler " << signo << endl;
}
int main()
{
cout << "getpid: " << getpid() << endl;
signal(2, handler);
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
for(int signo = 1; signo <= 31; signo++)
{
sigaddset(&block, signo);
}
sigprocmask(SIG_SETMASK, &block, &block);
while(true)
{
cout << "我已经屏蔽了所有的信号,来打我呀!" << endl;
sleep(1);
}
return 0;
}
sigpending
sigpending用于获取三张表中的pending表,以输出型参数的方式带出来!
综合练习
1. 屏蔽2号信号,每隔1s循环打印pending位图
2. 10s之后解除对2号信号的屏蔽,每隔1s循环打印pending位图
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
void handler(int signo)
{
cout << "handler " << signo << endl;
}
void PrintPending(const sigset_t& pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
signal(2, handler);
cout << "getpid: " << getpid() << endl;
//1.屏蔽2号信号
sigset_t set, oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set, 2);
sigprocmask(SIG_BLOCK, &set, &oset);
//2.让进程不断获取当前进程的pending表
int cnt = 0;
sigset_t pending;
while(true)
{
sigpending(&pending);
PrintPending(pending);
sleep(1);
cnt++;
if(cnt == 10)
{
cout << "解除对2号信号的屏蔽, 2号信号准备递达" << endl;
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
}
return 0;
}
问题: 信号被递达,pending位图的对应位置会从1改为0,这个过程发生在什么时候? 是处理完信号之前还是处理完信号后呢??
结论: 信号在要被递达时,pending位图对应位置就已经从1改0了!
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
void PrintPending(const sigset_t& pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
sigset_t pending;
cout << "############################" << endl;
sigpending(&pending);
PrintPending(pending);
cout << "############################" << endl;
cout << "handler " << signo << endl;
}
int main()
{
signal(2, handler);
while(true)
{
sleep(1);
}
return 0;
}
用户态与内核态
● 用户态是一种受控的状态, 比如操作上有权限约束,代码上会出现越界访问或野指针问题, 能够访问的资源是有限的,不能直接访问硬件资源,必须借助系统调用
● 内核态是一种OS的工作状态,能够访问大部分系统资源,系统调用背后,就包含了从用户态到内核态的身份转变
● 进程地址空间中, 用户态只能访问自己的[0, 3GB], 而内核态可以让用户以OS的身份访问[3, 4GB]
● 每一个进程都会有对应的进程地址空间,页表,而此处的页表指的是内存级页表,有n个进程就有n个用户级页表
● OS也是需要加载到内存的,OS代码和数据到内存的映射靠的是内核级页表,无论有多少个进程,内核级页表只有1个,因为OS的代码和数据只有1份
● 无论进程如何切换调度,cpu都能找到OS的代码运行,因为有内核级页表的存在
● 进程所有代码的执行,都可以在自己的地址空间内通过跳转的方式,进行调用和返回
● Linux操作系统中: cpu内部存在cs寄存器,有2个比特位,组合有4种,但是只使用2种,1表示内核态,3表示用户态, 用户态和内核态的切换都会修改两个比特位
● cpu内还有一套寄存器:CR寄存器,比如CR1(保存引发缺页中断/异常,比如野指针,越界访问的虚拟地址), CR3寄存器(保存当前进程页表信息, 物理地址!)
信号的捕捉过程
● 用户代码中调用了系统调用,需要从用户态转变为内核态,才有权限调用系统调用
● 调用完系统调用之后,会检测进程是否收到了信号,如果没有收到信号,会直接回到用户态;如果收到了信号,且处理动作为忽略,那么pending信号集对应位置由1改0,回到用户态如果处理动作为默认,则去执行默认行为,如直接杀掉进程,回到用户态
● 如果处理动作是自定义捕捉信号,则需要去回调处于用户态的代码,内核态当然可以直接执行用户态代码,但代码中可能会有恶意访问os的行为,因此回调用户态代码,需要从内核态转变到用户态
● 自定义捕捉完信号后再回到用户态调用系统调用的地方,继续向下执行代码
sigaction函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能: 对信号自定义捕捉
参数: signum是信号编号, act和oldact都是输出型参数,可以修改对信号处理的自定义行为以及获取上次对信号处理的动作
返回值:成功返回0,失败返回-1
其中,sigaction结构体的定义如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
我们只用到了其中两个参数:
● sa_handler:自定义的信号捕捉函数
● sa_mask:希望屏蔽的信号添加到该信号集中
注:
1. 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。
2.如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
● 使用sigaction函数达到和signal函数一样的效果,比如对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;
sigaction(2, &act, &act); //对2号信号进行自定义捕捉
while(true)
{
sleep(1);
}
return 0;
}
● 验证正在处理当前信号时,会自动屏蔽掉OS不断发来的相同的信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void PrintPending(const sigset_t& pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
while(true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
}
int main()
{
cout << "getpid : " << getpid() << endl;
struct sigaction act, oact;
act.sa_handler = handler;
sigaction(2, &act, &oact); //对2号信号进行自定义捕捉
while(true) sleep(1);
return 0;
}
● 使用sigaction函数屏蔽指定的一堆信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void PrintPending(const sigset_t& pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
while(true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
}
int main()
{
cout << "getpid : " << getpid() << endl;
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3); //将3号信号也屏蔽掉
sigaction(2, &act, &oact); //对2号信号进行自定义捕捉
while(true) sleep(1);
return 0;
}
● 研究进程同时收到多个信号的处理情况
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void PrintPending(const sigset_t& pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
sleep(1);
}
int main()
{
signal(2, handler);
signal(3, handler);
signal(4, handler);
signal(5, handler);
sigset_t mask, omask;
sigemptyset(&mask);
sigemptyset(&omask);
sigaddset(&mask, 2);
sigaddset(&mask, 3);
sigaddset(&mask, 4);
sigaddset(&mask, 5);
sigprocmask(SIG_SETMASK, &mask, &omask);
cout << "getpid :" << getpid() << endl;
int cnt = 20;
while(true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
cnt--;
sleep(1);
if(cnt == 0)
{
sigprocmask(SIG_SETMASK, &omask, nullptr);
cout << "cancel 2,3,4,5 block" << endl;
}
}
return 0;
}
结论: 当信号从内核态返回到用户态时,会检测是否还有信号没有处理,如果还有信号没有处理,则会将剩余信号处理完之后返回,而处理其余信号的顺序是不固定的,主要依据的是信号优先级
可重入函数
● 一个函数被多个执行流同时进入而不会出现任何问题,则该函数称为可重入函数;否则称为不可重入函数
● 函数中用到了全局变量或全局数据,一般是不可重入的
● 可重入函数或不可重入函数描述的只是函数的特征,并不代表函数的好坏
● 大多数函数都是不可重入函数
volatile关键字
volatile是C语言中的关键字,作用是保持内存的可见性
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int flag = 0;
void handler(int signo)
{
cout << "signo : " << signo << endl;
flag = 1;
cout << "change flag to " << flag << endl;
}
int main()
{
signal(2, handler);
cout << "getpid : " << getpid() << endl;
while(!flag);
cout << "quit nomal!" << endl;
return 0;
}
当编译代码时如果不优化以及 O0级别 的优化结果都在预期之内,但是当采用 O1 级别优化时,发现卡住了,分析代码就知道卡在了while(!flag)循环处,但是flag不是改成1了吗,为啥还在循环??
稍微修改了一下代码,在定义flag时使用volatile关键字进行修饰,此时尽管O1级别 优化也可以正常跑完代码了!
经过O1级别优化,形成的汇编代码会有所变化,因为main函数中flag是只读的,因此以后cpu检测flag值时,只有第一次会从内存中读取数据到cpu寄存器中,以后就只在cpu内部做检测即可,因此你发送2号信号将flag值改了,改的是物理内存的flag值,不会影响到cpu的判断了!
而volatile的作用就是保持内存的可见性, 意思是所有使用flag的操作,都要从内存中拿数据!
SIGCHLD信号
● 子进程在退出的时候,不是静悄悄退出的,会给父进程发送17号信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "子进程给父进程说他要退了!!!" << endl;
}
int main()
{
signal(SIGCHLD, handler);
pid_t id = fork();
if(0 == id)
{
int cnt = 5;
while(cnt--)
{
cout << "I am child process" << endl;
sleep(1);
}
exit(0);
}
sleep(10);
return 0;
}
● 既然子进程退出时会向父进程发信号,而父进程因为一些客观原因(比如网络服务)一直在运行,因此父进程可以直接在handler函数中等待回收子进程
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
//回收子进程
waitpid(-1, nullptr, 0); //阻塞等待
}
int main()
{
signal(SIGCHLD, handler);
pid_t id = fork();
if(id == 0)
{
//child
cout << "child is running" << endl;
sleep(5);
exit(10);
}
while(1)
{
sleep(1); //父进程一直在进行某种服务
}
return 0;
}
● fork可能创建了多个子进程,有可能这些子进程同时退出了,都同时向父进程发送17号信号,而根据上文提到的,父进程收到第一个17号信号之后就阻塞17号信号了,也就意味着其他子进程发送的17号信号就被丢弃了,父进程只能处理一个子进程,其他子进程都是僵尸了!因此我们要在handler中循环式等待处理子进程
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
pid_t id = 0;
while(id = waitpid(-1, nullptr, 0))
{
if(id < 0) break; //没有需要回收的子进程了!
cout << "回收子进程 : " << id << endl;
}
}
int main()
{
signal(SIGCHLD, handler);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
cout << "child is running" << endl;
sleep(5);
exit(10);
}
}
while(true)
{
sleep(1); // 父进程一直在进行某种服务
}
return 0;
}
● 不一定所有的进程都会退出,比如在有6个进程退出了,有4个进程不退出,handler函数中等待回收子进程就会阻塞住,父进程就做不了其他事情了! 因此在等待子进程时必须是非阻塞等待!
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;
void handler(int signo)
{
cout << "get a signo : " << signo << endl;
pid_t id = 0;
while(id = waitpid(-1, nullptr, WNOHANG))
{
if(id <= 0) break; //没有需要回收的子进程了!
cout << "回收子进程 : " << id << endl;
}
}
int main()
{
signal(SIGCHLD, handler);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
cout << "child is running" << endl;
sleep(5);
exit(10);
}
}
while(true)
{
sleep(1); // 父进程一直在进行某种服务
}
return 0;
}
● 如果要获取子进程的退出信息,那么父进程必须等待子进程,但如果只是为了回收子进程的僵尸状态,则等待不是必须的, Linux支持对SIGCHLD信号进行忽略处理,所有的子进程都不需要父进程进行等待了,自动回收!
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main()
{
signal(SIGCHLD, SIG_IGN);
for(int i = 0; i < 10; i++)
{
pid_t id = fork();
if(0 == id)
{
cout << "child is running" << endl;
sleep(5);
exit(10);
}
}
while(true) sleep(1);
return 0;
}