一、信号的基本概念

过马路时的交通信号灯,当我们看到红灯的时候,就会停下来等待,当我们看到绿灯的时候,会选择通行。这个红灯就是传给我们的信号。

对于Linux中的信号,我们需要先了解:

1、Ctrl-C产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C这种控制键产生的信号。

3. 前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步 (Asynchronous)的

我们先利用kill -l命令察看系统定义的信号列表:


Linux信号的有关概念及使用_#include

我们可以观察到:每个信号都有一个编号和⼀一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2 。

非实时信号,发送的信号可能会丢失,不支持信号排队。编号34以上的是实时信号,支持信号排队,发送的多个实时信号都会被接收,本章只讨论编号34以下的信号,不讨论实时信号。

二、信号的产生方式

1、通过终端按键产生的信号

我们知道,SIGINT(2号)的默认处理动作是终止进程,SIGQUIT(3号)的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。

我们首先了解一下Core Dump是什么?

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump(核心转储)。

进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。 

注意:系统默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K。

来看一下:


Linux信号的有关概念及使用_前台进程_02

改变后:


Linux信号的有关概念及使用_信号_03

我们写一个死循环,在前台运行这个程序:

1 #include<stdio.h>
2
3 int main(){
4 printf("pid is : %d\n",getpid());
5 while(1);
6 return 0;
7 }

运行,,然后在终端键入Ctrl-\:


Linux信号的有关概念及使用_阻塞_04


由此可见,ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的Resource Limit值,这样就可以产⽣生Core Dump了。 我们用调试一下,使用core文件,看哪里出错了:


Linux信号的有关概念及使用_阻塞_05

2. 调用系统函数向进程发信号

我们首先在后台执行死循环程序:

1 #include<stdio.h>
2
3 int main(){
4 while(1);
5 return 0;
6 }

然后用kill命令给它发SIGSEGV信号,如图:



Linux信号的有关概念及使用_信号_06

注:6070是a.out进程的id。之所以要再次回车才显示 Segmentation fault ,是因为在6070进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用户的输入交错在一起,所以等用户输入命令之后才显示。 

我们需要了解几个函数:

1、kill函数、raise函数

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)


Linux信号的有关概念及使用_前台进程_07


Linux信号的有关概念及使用_前台进程_08

这两个函数都是成功返回0,错误返回-1

2、abort函数

abort函数使当前进程接收信号而异常终止,所以总是成功,无返回值。

Linux信号的有关概念及使用_#include_09

3. 由软件条件产生信号

SIGPIPE是一种由软件条件产⽣的信号,我们首先认识一个函数alarm。

Linux信号的有关概念及使用_捕捉信号_10

顾名思义,就是一个闹钟。

设定一个闹钟,告诉内核在senconds秒后给当前进程发14号SIGALRM信号,该信号的默认动作是终止当前进程。
参数设为0,表示取消闹钟
参数设为n,表示n秒后发送信号
函数返回值:是0或以前设定闹钟时间还余下的秒数

下面有一个例子,作用是2秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止,如下:

1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int main(){
5 int count=10;
6 alarm(1);
7 for(;1;count++){
8 printf("count %d\n",count);
9 }
10 return 0;
11 }
~

4、硬件产生信号

硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。

例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。

再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为 SIGSEGV信号发送给进程。

三、信号的处理方式

1.忽略。
2.执行默认动作。
3.提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。

四、阻塞信号

1、信号的实质

我们先了解几个概念:

信号递达(Delivery):实际执行信号的处理动作(第三点的三种方式)

信号未决(Pending):信号从产生到递达之间的状态

阻塞 (Block ):进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

产生信号的实质就是操作系统将信号位图的对应位置的比特位由0改为1,其实每个信号都有两个标志位,就是上文介绍的阻塞 (Block)、信号未决(Pending), 还有一个函数指针表示处理动作(handler)。结构如下图:

Linux信号的有关概念及使用_前台进程_11

在上图的例子中, SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。 SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigsett来存储 ,sigsett称为信号集。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。


2、信号集操作函数

1).sigemptyset函数:初始化set所指向的信号集,使其所有信号对于的bit清零
2).sigfillset函数:初始化set所指向的信号集,使其所有信号对于的bit置1
3).sigaddset函数:添加signum信号
4).sigdelset函数:删除signum信号
5).sigismember函数:判断signum信号是否在信号集中


四个函数都是成功返回0,出错返回-1。

sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

3、sigprocmask函数----读取或更改进程的信号屏蔽字(阻塞信号集)

Linux信号的有关概念及使用_信号_12

参数:

how的可选值:

         SIG_BLOCK(添加到当前信号屏蔽字的信号)

         SIG_UNBLOCK(解除当前信号屏蔽字中解除阻塞的信号)

         SIG_SETMASK(设置当前信号屏蔽字为set所指向的值)
set:更改进程当前信号屏蔽字
oldset:保存原有的信号屏蔽字

4、sigpending函数----读取当前进程的未决信号集


Linux信号的有关概念及使用_信号_13

参数:

       set:读取当前进程的未决信号集,通过set参数传出

返回值:调用成功则返回0,出错则返回-1。

我们举一个例子:

1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4
5 void printsigset(sigset_t *set){
6 int i=0;
7 for(;i<32;i++){
8 if(sigismember(set,i)){
9 putchar('1');
10 }else{
11 putchar('0');
12 }
13 }
14 puts("");
15 }
16
17 int main(){
18 sigset_t s,p;
19 sigemptyset(&s);
20 sigaddset(&s,SIGINT);
21 sigprocmask(SIG_BLOCK,&s,NULL);
22 while(1){
23 sigpending(&p);


24         printsigset(&p);
25 sleep(1);
26 }
27 return 0;
28 }

解释:


Linux信号的有关概念及使用_捕捉信号_14

我们阻塞了SIGINT信号,当键盘输如组合键Ctrl-C时,使得SIGINT信号处于未决状态,不被处理。


Linux信号的有关概念及使用_阻塞_15

五、捕捉信号

1、捕捉信号的过程

信号处理的handler自定义方法

如下图,为信号捕捉的过程:

Linux信号的有关概念及使用_#include_16

2、sigaction函数 ----读取和修改与指定信号相关联的处理动作

Linux信号的有关概念及使用_捕捉信号_17

参数:act和oldact指向sigaction结构体

sigaction结构体如下:

Linux信号的有关概念及使用_阻塞_18

2、pause函数--使调用进程挂起直到有信号递达

如果信号的处理动作为忽略,则进程处于挂起状态;如果处理动作为终止进程,则进程终止;如果处理动作为捕捉,则调用了信号处理函数后返回-1。


Linux信号的有关概念及使用_阻塞_19

六、用alarm和pause实现sleep()函数

1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4 void sig_alrm(int signo){
5
6 }
7 unsigned int mysleep(unsigned int nsecs){
8 struct sigaction new,old;
9 unsigned int unslept=0;
10 new.sa_handler=sig_alrm;
11 sigemptyset(&new.sa_mask);
12 alarm(nsecs);
13 pause();
14 unslept=alarm(0);
15 sigaction(SIGALRM,&old,NULL);
16 return unslept;
17 }
18 int main(){
19 while(1){
20 mysleep(5);
21 printf("5 seconds passed\n");
22 }
23 return 0;
24   }


解释:


Linux信号的有关概念及使用_信号_20

结果:


Linux信号的有关概念及使用_#include_21