信号的声明周期主要分为三个部分,即信号产生、信号保存和信号递达。但是在这之前,要先知道什么是信号。所以在这一篇里将按照概念预备->信号产生->信号保存->信号递达的顺序讲解。
一、信号的概念
大家应该知道,在system V标准中,存在着一种进程间通信方式——信号量。要注意,信号量和信号其实完全是两个东西,它们之间毫无关系。就好比老婆饼和老婆一样,虽然名字相似,但是两者之间有非常大的区别。
区分好了信号和信号量之后,就可以来了解信号了。在介绍系统中的信号的概念前,先来初步推导一个信号的是什么。
信号与进程的关系
以红绿灯举例,在现实生活中,公路上很多地方都存在红绿灯。大家在看到红绿灯时都会对它的状态产生一定的反应,例如红灯停,绿灯行。这就说明我们是可以识别红绿灯的。而要识别红绿灯的基础就是,我们要认识红绿灯,并能对它有行为产生。这很好理解。要知道“红灯停,绿灯行”,那我们就要有两个动作,首先是能识别当前红绿灯的颜色,第二个就是根据颜色的不同产生不同的行为。
但是大家有没有想过,为什么我们可以识别红绿灯呢?这其实就是因为在过去,有人教育过我们,告诉我们在不同的灯亮时要做出不同的行为。而我们的大脑也记住了红绿灯的属性和对应的行为。
当你在公路上等红绿灯时,如果绿灯亮了,你就会穿过公路。但是,如果当绿灯亮时,你的一个好朋友从后面喊你名字,让你先别走,等他一起。你听到后还会过红绿灯吗?很明显不会,因为此时比起绿灯行这一行为,你有了更为重要的事要做。这就说明了,当信号到来时,我们并不一定会立即处理信号。因为绿灯随时可能亮,即信号随时可能产生,而此时你可能有更为重要的事要做。
如果此时绿灯亮起,但我们在做其他事,假设这个绿灯亮60s,前30s我们在做其他事。而到了后30s,我们就要准备过马路了。但是,在做完事情的30s后,为什么我们会继续过马路呢?那是因为我们的脑海中保存了绿灯这一信号。
同样的,你在公路上等红绿灯,当绿灯亮起时,一般人的默认动作都是过马路。但是有些人可能比较独特,他过公路前要先跳个舞或者唱首歌才会过。此时他的动作就和一般人不同,属于自定义动作。还有一些人,在绿灯亮起时,可能突然想起自己有其他事要做,就不过马路了。此时他们就是忽略了绿灯这一信号,做其他事去了,这就是忽略动作。这也就说明了,在面对信号时,有三种处理动作,即默认动作、自定义动作和忽略动作。
从上面的例子中,就可以推导出四个结论。
认知信号,就必须要认识信号的属性和有对应的行为
当信号到来时,我们不一定会立即处理信号
信号需要被记录
处理信号时,我们可能有不同的动作,归纳起来就是默认动作、自定义动作和忽略动作。
首先要有一个共识,那就是“信号是操作系统给进程发的”。再结合上面的四个结论,就可以推导进程与信号的关系了。
(1)信号传递给进程,进程需要通过认识和动作来识别信号。
(2)当信号到来时,进程可能在执行更重要的代码,无法立即处理信号。
(3)进程本身需要有对信号的保存功能
(4)进程在处理信号时,一般有三种动作(默认、自定义、忽略)。进程处理信号这一行为,被叫做“信号被捕捉”
信号的保存
上文中说了,为了让进程在执行完自己的代码后要记得处理信号,所以进程本身需要有对信号的保存功能。那么信号被保存在哪里呢?大家都知道,进程有自己的进程PCB,里面保存了进程的各种属性,当然,也就包括了信号。所以,信号其实是被保存在进程的PCB中的,保存的是是否收到指定信号。
那么进程如何保存信号呢?在谈这个之前,先在linux下输入“kill -l”命令查看linux系统中有哪些信号:
可以看到,进程中一共有62个信号。其中,1~31号信号是普通信号;34~64号信号是实时信号,在这里,我们只讲普通信号,不讲实时信号。
大家应该都对31这个数字不陌生了。既然这里有1~31号信号,还要保存它们,那我们就可以用一个32bit位的数据来保存它。即一个unsigned int类型的数字。在这个数字中有32个bit位,每个bit位的位置就被看成是一个信号,而对应的数字0、1就是是否接收到信号。例如如果进程接收到了2号信号,那么就把这个数字的第二个bit位由0置1,这就表示该进程此时接收到了2号信号。当信号处理完后,再将第二个bit位由1置0即可。
通过使用上述的位图,就可以很轻松让一个进程保存31个信号的接收情况了。
信号的发送
信号发送的本质,其实就是修改PCB中信号的位图结构。
二、信号的三种动作
大家知道,进程的PCB是由OS来管理维护的,既然如此,那么谁有权利修改PCB中保存的位图结构呢?很明显,只有OS有这个权利。所以,所有的信号都是由OS向目标进程发送的。既然是由OS发送的信号,如果用户想要向一个目标进程发送信号,就必须要使用系统调用。
信号的默认动作
在上文中讲过,信号一共有三种动作,即默认、自定义和忽略。先来看信号的默认动作。首先写上以下测试代码:
该程序每隔1s就会打印一次。运行该程序的话就可以看到程序在不断打印。但是在它打印时,按下键盘上的“ctrl c”组合键:
此时程序被终止。其实这里的“ctrl c”组合键就是向该进程发送了一个2号信号。大家可能不知道这些信号的作用是什么,现在在linux下输入“man 7 signal”命令,并往下翻,就可以看到对各个信号的解释:
查看里面的2号信号,可以看到,在Aciton下写的是“Term”,这其实是单词“terminate”的缩写,意思是终止。再看解释,这里对2号信号的解释是“从键盘中断”。那么2号信号的作用就很明显了,就是一个从键盘上发送给进程的信号,用于中断进程。而上面所按下的“ctrl c”组合键,就是向进程发送了2号信号进而让进程结束。
在上图中所写的进程的动作,其实都是信号的默认动作。但是上文说过,信号有三种动作,那么除了默认动作,就应该还有自定义动作和忽略动作。
信号的自定义动作
信号的动作是可以让用户自定义的。要实现这一行为,可以使用signal()函数。
在这个函数里面,它的第一个参数signum是信号的编号。而第二个参数大家可能不太理解。此时就可以看看该函数上面的描述。很明显,这里是重命名了一个返回值为void,参数为int的函数指针。既然第二个参数是一个函数指针,那就说明该参数就是我们要给对应信号设置的自定义动作。
为了验证这一点,写下如下测试代码:
在这个程序里面,就是捕获了2号信号,并对该信号设置了一个自定义动作。运行该程序:
可以看到,虽然我们在程序中写了捕获函数,但是运行时却并没有作用。原因是这里仅仅只是设置了信号捕获,并没有进行调用。只有当对应的信号被传入时,signal()函数才会起作用。按下“ctrl c”组合键,向进程传入2号信号:
此时2号信号被传入,但是却没有终止程序,而是打印了一句话。这也就说明该信号此时的默认动作被修改成了我们所设置的自定义动作。当然,此时有人就可能会有一个问题,既然此时2号信号的默认动作被修改,无法终止进程,那么我们怎么终止这个进程呢?很简单,其实大部分的信号的默认动作都是“终止进程”,所以我们还可以用“kill -信号编号 进程pid”命令显式调用其他信号终止指定进程。
三、信号的产生/发送方式
通过键盘产生信号
产生信号的第一种方式,就是用键盘热键产生信号。这一方式很容易理解。写出如下测试代码:
当该程序运行起来时,就会死循环打印一句话。要终止该进程很简单,直接用“ctrl c”组合键即可。
这就是通过键盘向进程发送了一个2号信号,终止了该进程。
调用系统函数向进程发信号
2.1 kill()函数
要调用系统函数向进程发送信号,就可以使用kill()函数。
该函数中的第一个参数pid为要发送的进程的pid,第二个参数sig是进程的编号。为了更好的演示,写出以下测试程序:
mykill.cpp文件:
mytest.cpp文件:
在这里,用一个mykill程序输入对应的命令,用于终止mytest程序。首先运行mytest程序,该程序会循环打印。
然后再运行mykill程序:
此时出现如下报错。这就是我们自己写的报错提示。原因是该程序中限定了main()函数的第一个参数的数量要等于3,即输入的命令+参数要有3个才可执行。现在我们再运行mykill程序,但是将进程的pid和信号传入:
通过这一方式,我们用命令行的方式手动终止了一个进程。在mykill程序里。这其实就模拟出了命令行命令“kill -信号编号 进程pid”。这同时也说明了kill命令的底层其实就是调用了“kill()函数”来实现的。只是这里我们用一个程序模拟出来了kill命令。
2.2 raise()函数
该函数不同于kill()可以给任意进程发送信号,它只能给自己发送信号。
该函数只有一个参数sig,代表的就是信号的编号。写出以下测试程序: