目录
在本篇博客中,作者将会带领你理解进程信号。
一.什么是信号
信号是什么?
在讲解Linux中的信号前,首先我们来了解一下我们生活中的信号。
红绿灯:红绿灯就是我们生活中信号的一种,我们就可以相当于进程,当我们看到绿灯时,我们就会通行,而看到红灯时,就是停下来等待,我们之所以会这么做,那是因为能看到红绿灯所发出来的信号。
当然,当我们看到红绿灯时,也不一定就会通行,我们也有可能又更重要的事情要做。
那么,在Linux中,信号又是什么呢?
我们先来写一份程序来讲解。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
while(true)
{
cout<<"hello linux: "<<getpid()<<endl;
sleep(1);
// raise(9);
}
return 0;
}
当这个程序运行起来的时候,会进行一个死循环,导致我们的程序无法正常退出,通常这个时候,我们会用ctrl+c键将程序结束掉。
那么这个ctrl+c是如何将程序结束的呢?
其实这个ctrl+c键是在向程序发送一个SIGINT的信号,使我们的程序退出。
这就是信号。
我们不仅可以使用ctrl+c键来结束程序,还能使用ctrl+\来结束程序,这个操作同样的是向我们的进程发送信号,只不过信号不同罢了。
从上面我们知道了两种信号的退出方式,那么信号到底还有那些呢?
在Linux中,我们可以使用kill -l的命令来查看所有的信号,如下图所示。
通过图,我们可以发现,信号分为1~31和34~64,其中1~31号为普通信号,34~64号为实时信号,在本篇博客中,我只谈论1~31号的普通信号。
其实信号本质上也是宏定义,例如2号信号SIGINT,在代码中就是一个这样的宏定义:
#define SIGINT 2
那么当一个进程收到信号后,就一定会做相应的动作去处理吗?
答案是不一定,当一个进程收到了信号,它有3种方式处理这个信号:
1.执行该信号的默认动作。
2.忽略此信号。
3.提供一个信号处理函数,当收到这个信号时,做我们自定义的动作,这种也叫捕捉,在后面会讲到。
那么信号又是被保存到那的呢?
首先,我们要理解的是,当一个进程收到信号的时候,一定是操作系统给我们的进程发送了信号,那么操作系统给进程发送信号的本质是什么,以及进程收到的信号又是保存在哪里的呢?
首先,我们要先知道当一个程序被加载到内存中成为进程的时候,操作系统会为这个进程在内存中创建一个PCB进程控制块来维护这个进程,其中,信号就会被保存在这个PCB进程控制块中,所以操作系统给进程发送信号的本质就是,修改该进程的PCB中的有关信号的变量。
对于信号的讲解,我们可以把信号的整个生命周期分为3个阶段:信号的产生,信号的保存,信号的处理。
如下图所示。
二.信号的产生
通过上面,我们知道了信号大概是什么了,那么信号又是如何产生的呢?
信号的产生可以分为4种:
1.通过终端按键产生信号。
2.调用系统函数向进程发送信号。
3.硬件异常产生信号。
4.由软件条件产生信号。
1.通过终端按键产生信号
通过终端按键产生信号,就上面的代码那种方式,通过ctrl+c或者ctrl+\来向进程发送信号。
其中ctrl+c对应的是2号信号,ctrl+\对应的是3号信号,如下图所示。
当我们在终端输出ctrl+c或者ctrl+\来结束进程的时候,这两个动作默认结束的是我们的前台进程,因为linux只允许我们在前台中,有一个进程在运行。
通过终端向我们的进程发送信号还有一种办法就是使用kill命令,kill命令的用法如下:
kill -信号 pid : 例如 kill -9 1111
通过kill命令加上我们的信号的编号以及进程的id,就可以向该进程发送指定的信号。
其中9号信号叫SIGKILL,这个信号是将该进程杀掉,类似于我们在windows中,通过任务管理器来将进程结束掉。
2.调用系统函数向进程发送信号
在Linux中,操作系统给我们提供一些系统函数给我们,以便我们用代码的形式,向进程发送信号。
kill系统函数
首先介绍的第一个是kill系统函数。我们先来看一下kill系统函数的手册。
kill系统函数有两个参数: 进程id,信号编号。
kill函数会向该pid发送sig信号。
返回值:如果发送信号成功,则返回0,否则返回-1,并设置errno来表示错误
我来演示一下用法。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
while(true)
{
cout<<"hello linux: "<<getpid()<<endl;
sleep(1);
kill(getpid(),9);
}
return 0;
}
运行结果如下:
程序运行一秒后,就会向进程发送9号信号将进程杀掉。
这个kill系统调用和通过终端按键通过kill命令来给进程发送信号的其实本质上是一样的,所以我们可以使用kill系统调用来模拟实现出来一个通过kill按键的命令。
首先,我们先创建test.cc和mysignal.cc两个源文件,代码如下:
test.cc
在这个程序中,我写了一个死循环,后通过mysignal.cc来结束掉这个进程。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
while(true)
{
cout<<"hello linux: "<<getpid()<<endl;
sleep(1);
}
return 0;
}
mysignal.cc
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
void state()
{
cout<<"请正确输入命令: 程序 数字 pid"<<endl;
}
int main(int argc,char* argv[])
{
//模拟kill命令
if(argc != 3)//如果输入的命令不为三个,则进行提示
{
state();
}
else
{
int pid = atoi(argv[2]);//将字符串转换为数字
int sign = atoi(argv[1]);//将字符串转换为数字
int ret = kill(pid,sign);
}
return 0;
}
此时,我们模拟的kill命令使用就完成了,我来演示一下用法。
raise函数
除了有kill系统函数可以给进程发送信号外,还有一些函数也能给进程发送信号,在这里介绍一下raise函数,我们先来看一下它的手册。
raise这个函数是给自己这个进程发送信号,参数为信号编号。
返回值:如果发送成功,则返回0,否则返回非0。
我们来演示一下用法,我会通过raise函数来给自己发送9号信号,将自己杀掉。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main(int argc,char* argv[])
{
while(true)
{
cout<<"hello linux: "<<getpid()<<endl;
sleep(1);
raise(9);//给自己发送9号信号
}
return 0;
}
运行效果:
abort函数
同样的,还有其他函数也可以给自己发信号,这里介绍的是abort函数,我们先来看一下手册。
这个函数的作用是给自己发送6号信号:SIGABRT
其中这个函数没有返回值。
我来演示一下用法。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main(int argc,char* argv[])
{
while(true)
{
cout<<"hello linux: "<<getpid()<<endl;
sleep(1);
abort();
}
return 0;
}
运行结果:
信号的意义
看到这里,你应该已经见过绝大多数的信号了,对于这些信号,我们会发现,绝大多数信号发给进程后, 做的默认动作都是终止进程,那既然那么多信号做的默认动作都是终止进程,为什么还要将信号分为那么多种呢?
那是因为不同的信号代表着不同的异常。
当一个进程异常终止的时候,它终止的原因有很多,例如:数组越界,空指针等等错误,通过不同的信号,我们就可以知道进程是因为那种错误而导致异常的。
所以信号的意义是:信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以相同。
3.硬件异常产生信号
在上面的两个发送信号的方法中,都是我们主动向进程发送信号,其实信号也不一定要我们主动发送,也有可能因为硬件异常而被检测到,后通知操作系统内核,再由操作系统内核向进程发送信号。
怎么理解呢?
我们通过代码来解释。
在下面这个代码中,我做了一个除0的操作,这种操作大家都知道是不合理的。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main(int argc,char* argv[])
{
int a = 100;
int b = 0;
int c = a/b;
return 0;
}
运行结果:
当程序运行起来的时候,显示发生了异常,并把进程终止掉,这是因为CPU在计算100/0的时候,它发现了这种行为是不合理的,同时操作系统也捕捉到了这种异常,然后操作系统向进程发送了8号信号,也就是浮点数异常错误给进程。
这种就是由硬件异常产生的信号。
如下图所示。
4.由软件条件产生信号
由软件条件所产生的信号,我也举一些例子来说明。
alarm函数
alarm函数是给进程设定一个闹钟,当运行到特定的秒数的时候,该进程就会终止掉。我们来看一下它的手册。
alarm函数的参数是一个整形,即秒数。
返回值:如果该进程提前结束,则返回还剩余的秒数,否则返回0。
我们来演示一下用法。
在下面这个程序中,我写了一个死循环,但是又给程序设定了一个闹钟,虽然程序的代码是一个死循环,但是又闹钟的存在,程序会在一秒后终止掉。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main(int argc,char* argv[])
{
alarm(1);//设定一个闹钟,一秒后结束
while(true)
{
;
}
return 0;
}
运行结果:
如上面这种发送信号终止进程的方式,就是由软件条件所产生的信号来完成的。
由软件条件所产生的信号不仅仅有这些,还有其他的方式,例如管道等等,这里就不再过多解释。
总结
1.上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者2.信号的处理是否是立即处理的?在合适的时候3信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?信号需要被进程暂时记录下来,记录在进程的PCB进程控制块中4.一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?能知道5.如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?OS向进程发送信号的本质是,修改进程PCB进程控制块中,有关信号的变量
三.信号的捕捉
再上面所用到的信号中,当进程收到信号后,绝大多是都是执行默认动作,即终止进程。
但是在开头我们讲到,当进程收到信号后,一般可以有三种行为:执行相应的动作,忽略动作,自定义动作。
那么忽略动作和自定义动作又如何操作呢?
在Linux中,操作系统给我们提供了一个系统函数signal,这个系统函数可以捕捉信号,使其改变原来的动作。
什么意思呢?
比如说,正常来说,当进程收到ctrl+c所发送的信号呢,进程会被终止掉,但是我们使用signal系统函数将信号进行一个捕捉,就可以执行我们自定义的动作了。
signal函数
对于信号的捕获,操作系统给我们提供了一个signal系统函数,我们来看一下它的手册。
首先我们来看一下signal函数的参数,这个函数的参数有两个,其中signum为要捕捉的信号编号,handler是一个函数指针。
对于第一个参数来说很好理解,我们只需要传我们需要捕捉那个信号即可,那么第二个参数是什么意思呢?
当一个进程收到一个信号后,一般会执行相应的行为,但是我们对信号进行捕捉其目的是为了收到该信号后能有其他的行为,所以这个handler函数指针所指向的函数就是我们的新行为。
同时这个函数指针所指向的函数的参数为该信号。
光看这段话,很难理解,我们来看代码来理解。
在下面这段代码中,我对2号信号进行捕捉,同时写了一个死循环,不让程序正常退出。
原本的情况下,程序死循环的时候,我们可以通过ctrl+c的方式将进程结束掉,这是2号信号的默认动作,但是现在我用signal函数将2号信号进行捕捉,即进程收到2号信号的默认行为,改变成我们的CatchSign函数这个行为,所以当程序运行起来后,我们使用ctrl+c给进程发送2号信号时,不再是将进程结束掉,而是运行CatchSign这个函数的代码。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
void CatchSign(int sign)
{
cout<<"我捕捉了2号信号"<<endl;
}
int main(int argc,char* argv[])
{
signal(2,CatchSign);
while(true)
{
cout<<"hello linux: "<<getpid()<<endl;
sleep(1);
}
return 0;
}
运行效果:
这个时候,我就不能使用ctrl+c来结束程序了,而是通过命令kill -9 [pid]来将这个进程结束掉。
这就叫信号的捕捉。
不会被捕捉的信号
理解了什么是信号的捕捉后,有的人可能就会有一些想法,如果我写了一个死循环,同时将所有的信号都进行捕捉,捕捉后的信号都只仅仅输出一句话,那么我这个进程是不是就永远都无法退出了呢?
答案是否定的,在所有的信号中,SIGKILL信号,即9号信号永远都不会被捕捉,其目的就是为了避免这种情况发生。
对于这种情况,我们也可以写个代码来验证一下。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
void CatchSign(int sign)
{
cout<<"我捕捉了"<<sign<<"号信号"<<endl;
}
int main(int argc,char* argv[])
{
for(int i = 1; i<=31 ;i++)
{
signal(i,CatchSign);//将1~31号信号都捕捉
}
while(true)
{
cout<<"hello linux: "<<getpid()<<endl;
sleep(1);
}
return 0;
}
当这个程序运行起来的时候,除了9号信号,其他所有信号都会被捕捉,即,用9号信号之外的信号,都不能结束掉程序,唯独9号信号可以。
四.核心转储
什么是核心转储?
首先我们来看一下,绝大多数进程收到信号后的默认动作都是终止进程,但是这些终止进程的方式又有一些区别,什么区别呢?
我们可以通过man 7 signal命令来查看手册,进入手册后,我们往下翻,可以看到这一片内容,如下图所示。
在Action一列中,我们可以看到有多种选项:Term, Core, Ign, Cont, Stop。
其中,我先讲Term和Core的区别。
大家都是终止进程,为什么有些信号的动作是Term,有些信号是Core呢?这两个有什么区别呢?
Term动作的信号,就单单的只是将进程终止掉,而Core动作的信号,不仅会将进程终止掉,还会进行一个核心转储。
那么什么是核心转储,核心转储就是将程序运行异常时的数据保存起来,以便我们debug。
怎么理解呢,我们来看一下代码以及现象。
首先这里有一个前置条件。
有些人的Linux机器不会进行核心转储,那是因为没有设置。
我们可以通过ulimit -a命令来查看是否设置。
红色的那一行就是查看核心转储是否被设置,如果为0,则没有设置,所以是看不到核心转储的,所以我们可以通过ulimit -c 1024命令来设置。
这个时候,我们来写一份错误的代码,如下:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main(int argc,char* argv[])
{
int a = 100;
int b = 0;
int c = 100/0;
return 0;
}
当程序运行起来的时候,程序会异常终止,并且在当前目录下,生成一个core文件,如下图所示。
这个文件里面保存着,我们的程序异常终止时的核心数据,但是这些数据我们是看不懂的。
但是这些数据可以通过gdb调试来帮助我们定位到那一行的错误。
具体怎么操作呢?我们接着往下看。
首先,我们需要使用debug的方式将程序编译好:
g++ mysignal.cc -o mysignal -std=c++11 -g
然后使用gdb调试:
gdb mysignal
再输入:
core-file core.7411
它就能自动的帮我将错误定位到那一行,如下图所示:
这就是Core动作的意义。
五.信号的保存
信号的产生部分以及一些其他内容讲完后,我们再来看一下信号的保存。
对于信号的保存,在上面,我们只是笼统的讲了一下信号是被保存在进程的PCB进程控制块中的,那么具体的细节又是怎样的呢?我们来看一下。
在讲解之前,我们先来了解一下关于信号的其他概念:
1.实际执行信号的处理动作称为信号递达。
2.信号从产生到递达之间的状态,称为信号未决。
3.进程可以选择阻塞某个信号。
4.被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
5.注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
在上面我们提到了信号是可以阻塞的,但是具体的细节又是怎样的呢?我们来看一下。
在每一个进程的PCB进程控制块中,里面有三个这样的变量:
unsigned int pending、unsigned int block、handler_t handler[32]
我们分别来讲解一下。
unsigned int pending:
这个pending变量是用来保存我们的信号的,其中这个pending是一个位图结构,那个比特位为1,就代表着那个信号,如下图所示。
unsigned int block:
同时在PCB进程控制块中还有一个block的变量,在这个变量中,存储的是阻塞掉的信号,如下图所示。
信号的阻塞就是这样来完成的,某个比特位为1,则进程收到这个比特位对于的信号时,不会执行动作,直到信号解除阻塞。
handler_t handler[32]:
同时,在PCB进程控制块中,还会存储着一个函数指针数组handler_t handler[32] ,这个数组里面保存的都是各个信号所对应的动作,当我使用signal来捕捉信号时,其实就是将我们自己自定义的函数替换到这个对应的下标中。
这3个变量用图可以这样来表示:
信号的捕捉流程
大概清楚了上面的概念后,我们接下来讲一下信号是如何被捕捉的。
信号的捕捉流程:当信号产生的时候,不会被立即处理,而是在合适的时候处理,即从内核态返回到用户态的时候进行处理。
用户态和内核态
在上面我们讲到了信号被处理是从内核态返回用户态的时候被处理的,那么用户态和内核态又是什么呢?
当我们的一个进程正在运行的时候,这个进程可以被分为用户态和内核态。
什么意思呢?
用户态就是以用户的身份去执行我们的代码,而内核态就是以操作系统内核的身份去执行我们的代码。
那么这两种形态又有什么区别呢?
当我们在写代码的时候,或多或少的都会去访问操作系统自身的资源和硬件资源
对于操作系统资源来说,常见的有:getpid、waitpid等等系统调用。
对于硬件资源来说,常见的有:printf、write、read等等与硬件有关的资源。
当我们的程序需要执行有关操作系统资源或者硬件资源的代码的时候,会将自己的身份从用户态切换到内核态去执行。
总结:当我们访问硬件资源或者操作系统资源时,会将当前进程的执行身份从用户态改成内核态,当执行完后,再从内核态返回用户态,从内核态返回到用户态时,进行信号的捕捉。
如下图所示:
进程地址空间的进一步理解
我们又该怎么理解一个进程变成内核态后去执行操作系统的方法呢?
这个时候就要更加深一步的理解进程的地址空间。
在32位机器下,我们的内存大小是4GB,其中这4GB的空间分为两部分:用户空间和内核空间,用户空间分到了3GB大小,内核空间分到了1GB大小,在进程地址空间中,通过这1GB的内核空间大小,就可以找到我们操作系统中的方法。
如下图所示。
六.信号集的操作
对信号的产生、保护和处理都大概理解后,接下来我们来看一下信号集的操作。
什么是信号集?
在前面我们讲到了在一个进程的PCB中,有pending位图和block位图,其中pending位图保存的信号,而block位图保存的是阻塞的信号,那么如果我想主动阻塞某个信号应该要怎么做呢?
sigset_t
首先在Linux中,信号集是一个sigset_t的结构体,在这个结构体里面保存我们信号的位图结构,通过操作这个sigset_t结构体,我们可以主动屏蔽某个信号。
信号集操作函数
首先我们先来介绍一下信号集的操作函数。
sigemptyset
信号集位图初始化函数, 调用这个函数后,set参数的信号集位图就会全部被置成0。
sigfillset
调用这个函数后,set参数的信号集位图会全部被置成1。
sigaddset
调用这个函数,可以将某个信号添加到我们的信号集位图中。
sigdelset
调用这个函数,可以将某个信号从我们的信号集位图中删除。
sigismember
调用这个函数,可以检测某个信号是否在我们的信号集位图中。
sigprocmask
在上面我们所讲到的函数都只不过是将我们的信号设置到我们的sigset_t结构体中而已,并没有设置进操作系统内核的数据结构中(即PCB中的pending位图和block位图),所以如果我们要想真正的屏蔽某个信号,还需要sigprocmask这个函数。
这个函数有三个参数:how,set,oset
其中how参数有三个选项:SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK
SIG_BLOCK:将set信号集添加到PCB中的block位图。
SIG_UNBLOCK:将set信号集从block位图中删除。
SIG_SETMASK:将原本的block位图全部重置后,再将set信号集添加到block位图中。
oset参数:
我们将set信号集设置进去后,可能也想将原来的信号集保存起来,通过oset参数,就可以将原来的信号集保存到oset中。
sigpending
sigpending函数我可以用来获取当前PCB中的pending信号集,方便我们查看。
操作使用
信号集操作函数介绍完后,我们就可以来使用一下了。
接下来我会写一个代码,这个代码的流程是,我会屏蔽一个信号,屏蔽这个信号后,再给程序发送这个信号,同时将pending信号集给打印出来。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
#define SIGNAL 2
void ShowPending(sigset_t& pending)
{
for(int i = 31; i>0; i--)
{
if(sigismember(&pending,i))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
int main(int argc,char* argv[])
{
sigset_t block,oblock,pending;
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
sigaddset(&block,SIGNAL);
sigprocmask(SIG_SETMASK,&block,&oblock);
while(true)
{
sigemptyset(&pending);
sigpending(&pending);
ShowPending(pending);
sleep(1);
}
return 0;
}
当我们的代码运行起来后,2号信号就会被屏蔽掉,同时当我们给程序发送2号信号时,在打印的pending位图,我们可以看到。
七.补充
对于信号的理解,我们已经知道的差不多了,接下来在补充一些内容。
sigaction
在上面我们讲到了,使用signal函数可以进行信号的捕捉,但是对于信号的捕捉不只只有signal这个函数,还有sigaction这个函数也可以对信号进行捕捉,我们来看一下。
从手册中我们可以看到,sigaction函数有三个参数:信号编号、两个结构体的指针。
其中这个sigaction结构体的成员如下一副图所示。
在sigaction这个结构体里面有四个成员变量:
sa_handler:一个函数指针,指向我们捕捉信号后的处理动作方法。
sa_mask:要阻塞的信号集。
sa_flags和sa_sigaction在这里我们不关心,先看前两个即可。
我来写一个代码来演示一下。
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
#define SIG 2
void handler(int sig)//捕捉信号的新方法
{
cout<<"我捕捉了"<<sig<<"信号"<<endl;
}
int main()
{
struct sigaction act,oldact;
act.sa_handler = &handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIG,&act,&oldact);
while(true)
{
sleep(1);
}
return 0;
}
如上面的代码所示。
但是在上面的代码中,我们没有用到sa_mask参数,如果在捕捉信号的时候,我们也想阻塞其他的信号,就可以使用sa_mask来完成。
如下面代码所示。
这样我们在处理2号信号的时候,就不会被3号信号所打扰。
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
using namespace std;
#define SIG 2
void Count()
{
int cnt = 10;
while(cnt--)
{
printf("%d\r",cnt);
fflush(stdout);
sleep(1);
}
}
void handler(int sig)
{
cout<<"我捕捉了"<<sig<<"信号"<<endl;
Count();
}
int main()
{
sigset_t block;
sigemptyset(&block);
sigaddset(&block,3);
struct sigaction act,oldact;
act.sa_handler = &handler;
act.sa_flags = 0;
act.sa_mask = block;
sigaction(SIG,&act,&oldact);
while(true)
{
sleep(1);
}
return 0;
}
总结,sigaction和signal函数相比,都是捕捉信号,但是sigaction的功能更加强大。
信号的重复发送
在上面的sigaction使用中,应该不难理解。
接下来我来讲解一个问题:
如果我将一个信号捕捉,并且捕捉后这个信号的处理动作非常久,还没等这个处理动作结束时,我再发送一次或者多次的信号会怎么样呢?
为了演示这个问题,我写了如下的代码来演示。
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
using namespace std;
#define SIG 2
void Count()//倒计时
{
int cnt = 10;
while(cnt--)
{
printf("%d\r",cnt);
fflush(stdout);
sleep(1);
}
}
void handler(int sig)//捕捉信号后的处理动作
{
cout<<"我捕捉了"<<sig<<"信号"<<endl;
Count();//在这个处理动作中,进行一个10秒的倒计时
}
int main()
{
struct sigaction act,oldact;
act.sa_handler = &handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIG,&act,&oldact);
while(true)
{
sleep(1);
}
return 0;
}
当我们的程序运行起来后,我会将连续的给进程发送2号信号,我们来看一下现象。
从图中我们可以看到,当程序运行起来的时候,我们连续给进程发送大量的2号信号,最后可以看到的结果是,信号只被处理了两次。
这是为什么呢?
当我们的进程正在处理一个信号的时候,会先主动将这个信号屏蔽掉,再进行信号的处理,当这个信号处理完成后,再解除屏蔽,这样对于上面的现象我们就很好的理解了。
首先,第一次发送2号信号的时候,pending位图由
00000000 00000000 00000000 00000000
变为
00000000 00000000 00000000 00000010
后处理信号的时候,先屏蔽2号信号,即将block位图由
00000000 00000000 00000000 00000000
变成
00000000 00000000 00000000 00000010
此时再将pending位图由
00000000 00000000 00000000 0000010
变为
00000000 00000000 00000000 0000000
后才进行信号的处理动作。
再处理信号的期间,当我们再给进程发送大量的2号信号时,又将pending位图又
00000000 00000000 00000000 00000000
变为
00000000 00000000 00000000 00000010
但是因为此时这个时候block位图将2号信号阻塞了,所以信号不会被递达
所以pending位图一直保持着00000000 00000000 00000000 00000010这个状态
当第一次的信号处理动作完成后,block位图才将2号信号解除屏蔽,解除屏蔽后才将第二次的2号信号进行递达。
可重入函数
在这里我们再来看一下可重入函数的概念。
如果你实现过带哨兵位的单链表,那么你一定知道单链表是如何头插的。
如下图所示。
正常来说,我们的头插就如上图所示。
但是现在在头插的时候,由于一些原因,导致某种信号被递达,在头插的时候触发了信号的递达,而在这个信号的处理动作中,也有insert函数,就可以导致下面这种情况。
当发生这种情况时,说明insert函数是不可重入函数。
如果没有发生错误,则说明该函数是可重入函数。