标题:[Linux] 信号(singal)详解
@水墨不写bug
(图片来源于网络)
目录
一、认识信号
1、认识信号
2、信号特点
3、基本概念
二、信号的产生(5种方式)
三、信号的保存
正文开始:
一、认识信号
1、认识信号
信号,联系生活,比如你看到钟表知道现在的时间,于是提醒自己这会儿要要干啥了;再比如,在过马路时,看到信号灯是红色,你会停下来等待,直到信号灯变绿。这都是信号的例子,其实信号简而言之,就是一种简易的信息传递方式。
2、信号特点
随时可能产生,信号的产生是没有预告的,与你的生活是异步的。
你能识别信号(可以想想一下,如果不能识别信号,生活会变得怎么样),并且知道得到这个信号之后该怎么做。
当你收到信号的时候,你可能正在做更加重要的事(比如在期末考),这时需要把信号保存起来,暂不处理。
转到OS层面:
在进程中,信号可能随时产生,(这意味着OS需要不停的检测信号是否产生),信号的产生和进程异步。
进程能够识别信号,并且知道得到这个信号之后该怎么做(处理信号)。
进程可能正在做更加重要的事情,于是可能会把信号暂不处理。
综上,OS需要组织信号,并且需要在合适的时候处理信号。
3、基本概念
信号是LINUX OS 提供的一种,向指定进程发送特定信息的方式。目的是让进程做识别和处理。
二、信号的产生(5种方式)
1.通过kill指令,向特定的进程发送信号。
首先可以通过 kill -l 查看所有的信号类型:
对于不同种类的信号,OS会有不同的处理方式(后文将会有详细解释)
2.键盘可以产生信号。
ctrl + c :SIGINT(2号信号)
ctrl + \ = | : SIGQUIT(3号信号)
(见上图)
3.系统调用可以产生信号。
int kill(pid_t id,int sig);——kill命令的底层
void abort(void); ——产生 SIGABRT;
4.软件条件可以产生信号。
比如:
管道read fd关闭,write fd还在写入,OS发送SIGPIPE (13号信号)来杀死write进程。
alarm系统调用可以产生信号:
5.异常可以产生信号。
对于非法访问操作,被OS检测到,OS会发送信号。
OS为什么可以检测到信号?
以发生除0错误为例,当发生除0时,CPU内部的eflag(寄存器)的溢出标记位被置1,表明本次运算的结果发生溢出,计算结果不可信。
CPU是硬件,OS要管理硬件,于是OS会检测到CPU的这个标记位的错误信息,并通过发送信号的方式,终止发生错误的进程。
总结:OS通过检测硬件的标记位信息,来判断是否发生了错误。
三、信号的保存
信号的保存和处理其实是密切联系的。信号的保存通过三张表实现的——block位图、pending位图、handler函数指针表。
概念引入:
信号递达:实际执行信号的过程称为信号的递达(Delivery)。
信号未决:信号从产生到递达之间状态称为信号未决(pending)。
信号阻塞:进程可以阻塞某一信号,意味着这个信号一旦产生,永远不递达,一直是未决状态,直到主动解除阻塞为止,才执行递达的动作。
注意:
信号的阻塞与其是否未决无关。
阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
通过查看kill -l:
发现:
普通信号一共31个。如果用一个位图来存储,需要31个bit位,于是Linux OS提供了一个专门的数据类型用作位图:sigset_t 。
本章开头所说的2张位图和一张函数指针表实际就是存储在进程的task_struct中的:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),和函数指针表(handler)表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志(设置为1),直到信号递达才清除该标志(设置为0)。
在上图的例子中:
- SIGHUP信号未阻塞也未产生过;当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达;虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有可能会对SIGINT进行自定义处理,进而解除阻塞之后可能会产生意想不到的结果。
- SIGQUIT信号未产生过;SIGQUIT产生之后,一旦产生SIGQUIT信号将被解除阻塞,它的处理动作是用户自定义函数sighandler。
到这里,我们可以总结:
两张位图和一张函数指针数组,可以实现让进程识别信号!!
实验:检测信号的保存
注意:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。如何验证(常规信号在递达之前产生多次只记一次)呢?
通过下面的这一份实验测试,可以验证。但是你可能会对实验中使用的关于信号的函数接口有疑惑,鉴于此,我会在注释中尽可能详细注明;并在后续的文章内容中详细讲解系统的信号部分的接口的使用。
#include <signal.h>
#include <unistd.h>
#include <iostream>using std::cout;
using std::endl;
bool loop = true;void Print(sigset_t &pending)
{for (int sig = 31; sig > 0; sig--) // 没有0号信号,信号的范围1——31,两闭{if (sigismember(&pending, sig)){cout << 1;}else{cout << 0;}}cout << endl;
}
/*一旦检测到3号信号,会走这里的处理逻辑,此时吧loop置false,使得对2号信号的处理逻辑结束。*/
void donesig2(int sig)
{cout << "get sig 3" << endl;cout << "loop = false, done sig2" << endl;loop = false;
}
void sigcb(int sig)
{loop = true;cout << "get a sig:" << sig << endl;while (loop){sigset_t pending;sigpending(&pending);//函数接口:获取目前的pending位图Print(pending);//打印出pending位图,便于观察sleep(1);signal(3, donesig2);//检测3号信号——在处理信号的同时依然可以接受并处理信号}
}int main()
{struct sigaction ac, oac;//一个结构体类型,内部存储有维护信号系统的一系列变量ac.sa_flags = 0;//暂时设为0/*sa_mask是一个sigset_t类型的位图(sigset_t是一个专门用于维护31个信号位的类型)此处这个函数的作用是把这个位图的所有位置全置0(初始化)但是这个位图目前还没有被设置进操作系统的信号位图*/sigemptyset(&ac.sa_mask);/*sa_handler是结构体内部的一个成员,是一个函数指针类型,需要用户自定义实现,也就是当进程接受到特定信号之后需要做的处理动作*/ac.sa_handler = sigcb;while (true){//这是一个和上述的结构体类型同名称的一个函数//参数:(需要屏蔽的信号,需要设置结构体类型,老的结构体类型,目的是为了保存设置之前的数据,防止用户想要撤回操作)sigaction(2, &ac, &oac);//对2号信号进行特殊处理sleep(1);cout << "I am process:" << getpid() << endl;}return 0;
}
完~
未经作者同意禁止转载