信号的定义
首先需要明白的是,信号和信号量是两个不同的东西,他们间没有任何关系。
信号:本质是一种通知机制,用户或者操作系统(OS)通过发送一定的信号,来通知目标进程,某些事件已经发生,你可以在后续进行处理(是一种处理异步事件的方式)
信号量:本质是进程间通信处理同步互斥的一种机制
结合进程,产生的信号结论:
进程要处理信号必须具备”识别“信号的能力
程序员给予进程识别信号的能力
信号的产生是随机的,进程可能正在忙自己的事情
进程会临时的记录下对应的信号,方便后续合适的时候处理
一般而言,信号的产生相对于进程而言是异步的
如果想在linux中查找信号,或是查找信号的作用,可以输入以下命令:
//查看所有信号
kill -l
//查看所有信号的作用
man 7 signal
信号是如何产生的?
信号发送的本质是操作系统向目标进程写信号
1.通过终端按钮产生
可以通过键盘+组合键来向进程发送信号,例如Ctrl+C就对应着2号信号的产生。为了更好演示信号的产生,我们可以通过代码来查看:
//1.终端获取信号,在进程中添加获取某个信号后的自定义方法
void func(int signalnum)
{
cout << "进程获取信号:" << signalnum << " pid:" << getpid() << endl;
}
int main()
{
for(int i = 1;i <= 31;i++)
{
//设置捕获时使用自定义函数的信号
signal(i,func);
}
while(true)
{
cout << "进程运行中 pid:" << getpid() << endl;
sleep(1);
}
}
在上述代码中,我们使用signal函数(头文件:signal.h)对每一个信号进行“声明”,代表他们只要收到这个信号就调用func函数
可以看出,当所有信号被捕获(9号信号SIGKILL和19号信号SIGSTOP除外),我们可以使用终端按钮来进行信号的发送(图中获取了Ctrl+C和Ctrl+Z这两个信号)
补充一个知识点:
核心转储(core dump):是操作系统在进程收到某些信号而终止运行时,会将此进程内有关的状态和内容“吐”入磁盘中。在一个进程被杀死后的status中,核心转储位于右边数起第八位,如果想要获取核心转储是0是1(1代表核心转储开启,0代表关闭(一般来说都是默认关闭的)),可以选择位操作: (status>>7) & 1
如果想要设置核心转储开始,可以使用:ulimit -c 10200 设置核心转储功能文件的大小为10200(当接收到core的信号后,会生成core文件)
核心转储功能的主要目的是为了调试,生成的 core文件可以在 gdb中使用 core-file加载进去,以显示错误的地方以及退出的信号码
2.调用系统函数向进程发信号
本质是:系统调用接口 --> 执行OS提供的系统调用接口代码 -->OS提取参数,或设置特定的数值 --> OS向目标进程写信号 -->修改对应进程的信号标记位 -->进程后续处理信号 --> 执行动作
可以调用三个系统函数:
调用kill函数可以指定发送某个信号给指定的进程
int kill(pid_t pid, int sig); 可以向pid代表的目标进程发送一个信号
调用raise函数可以向本进程发送信号
int raise(int sig); 可以向本进程发送一个信号
调用abort可以停止当前进程,表示异常终止信号(SIGABRT)
void abort(void); 产生SIGABRT信号并发送给自己
3.由软件条件产生信号
SIGPIPE是一种由管道产生的信号,如果管道中写满了或者读空,那么OS则会监测到并发送一个信号到进程
闹钟信号(alarm函数和SIGALRM)可以设定一个秒级别的闹钟
4.硬件异常产生信号
在程序中出现的除0错误、段错误(野指针问题)或越界问题本质上都是硬件异常产生的中断
除0错误的本质:因为进行计算的是cpu这个硬件,cpu内部是有寄存器的,有一个状态寄存器(位图),有对应的状态标记位,会进行状态标记位的监测,里面有溢出标记位,会有计算完毕后的监测,如果溢出标记位是1,OS会马上意识到有溢出问题,找到当前谁真正运行然后停止它
段错误的本质:野指针、越界 = 非法地址 = MMU转化的时候一定会报错!本质上也是一个硬件问题
void handler(int sig)
{
cout << "sig:" << sig << endl;
}
int main()
{
signal(8,handler);
int a = 0;
a /= 0;
return 0;
}
上述的代码运行后,会收到8号信号SIGFPE浮点错误,本质上就是个硬件错误
信号是如何被保存的?
信号的处理方式有:默认(进程自带的,程序员写好的逻辑)、忽略(也是信号处理的一种方式)、自定义动作(捕捉信号)。
保存中,涉及了一些概念:执行信号的处理动作称为信号递达(Delivery)、信号从产生到递达的状态,称为信号未决(Pending),收到后暂时不做处理就是未决状态、进程可以选择阻塞(Block),某个信号被阻塞的信号将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
进程的PCB中会有三个信号相关的表,分别是pending(未决表)、block(阻塞表)、handler(处理表)
OS收到信号后,会更改PCB中的pending表,之后信号进行检查,是否pending为1且block未被阻塞(为0),如果满足条件,则执行对应的handler处理
我们可以使用一些系统接口,来对阻塞表进行操作,操作流程如下:
代码如下:
int main()
{
//先设置个可捕获2号信号做测试
signal(2,catchSig);
//1.创建信号集,前两个是阻塞信号集的now和old,后边用于存储未决信号集
sigset_t blockset,oldbset;
sigset_t pending;
//2.初始化信号集
sigemptyset(&blockset);
sigemptyset(&oldbset);
sigemptyset(&pending);
//3.修改阻塞信号集
sigaddset(&blockset,2);
//4.设置到进程集中
int n = sigprocmask(SIG_BLOCK,&blockset,&oldbset);
assert(n==0);
//因为在release状态下assert被省略,n需要被使用一下,否则会报警说n未使用
(void)n;
cout << "阻塞2号进程成功" << endl;
//5.重复打印一下pending集,确保2号信号其实是收到的,只是被阻塞了而已
while(1)
{
sigpending(&pending);
showPending(pending);
sleep(1);
}
return 0;
}
信号如何被捕捉且处理的?
在执行主控制流程时遇到中断、异常或系统调用,就会进入内核中
在内核中进行异常处理
处理完毕后会进行信号检测,如果信号的处理动作是自定义的,内核需要先返回用户模式再进入处理函数中进行处理
处理完毕后,又通过特殊的系统调用sigreturn再次进入内核
在内核中进行完收尾工作后,继续返回主执行流进行接下来的操作
信号的相关联的处理动作是可以被修改的:
void sigcb(int signum)
{
cout << "捕获到一个信号:" << signum << endl;
}
int main()
{
struct sigaction act,oact;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = sigcb;
//此函数的作用是检查或修改与指定信号相关联的处理动作
sigaction(2,&act,&oact);
cout << "defalut action:" << (int)(oact.sa_handler) << endl;
while(1) sleep(1);
return 0;
}