欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux信号的有关概念及使用

程序员文章站 2022-07-12 10:31:04
...

一、信号的基本概念

为了理解信号,先从我们最熟悉的场景说起,比如我们过马路时的交通信号灯,当我们看到红灯的时候,就会停下来等待,当我们看到绿灯的时候,会选择通行。这个红灯就是传给我们的信号。
对于Linux中的信号,我们需要先了解:

1、Ctrl-C产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C这种控制键产生的信号。
3. 前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步 (Asynchronous)的

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

Linux信号的有关概念及使用
我们可以观察到:每个信号都有一个编号和⼀一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2 。
注:1至31号信号,是非实时信号,发送的信号可能会丢失,不支持信号排队编号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信号的有关概念及使用
改变后:

Linux信号的有关概念及使用
我们写一个死循环,在前台运行这个程序:
 1 #include<stdio.h>
  2 
  3 int main(){
  4     printf("pid is : %d\n",getpid());
  5     while(1);
  6     return 0;
  7 }

运行,,然后在终端键入Ctrl-\:
Linux信号的有关概念及使用

由此可见,ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的Resource Limit值,这样就可以产⽣生Core Dump了。 我们用调试一下,使用core文件,看哪里出错了:
Linux信号的有关概念及使用
2. 调用系统函数向进程发信号
我们首先在后台执行死循环程序:
 1 #include<stdio.h>
  2 
  3 int main(){
  4     while(1);
  5     return 0;
  6 }
然后用kill命令给它发SIGSEGV信号,如图:

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

我们需要了解几个函数:
1、kill函数、raise函数
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)

Linux信号的有关概念及使用

Linux信号的有关概念及使用
这两个函数都是成功返回0,错误返回-1
2、abort函数

abort函数使当前进程接收信号而异常终止,所以总是成功,无返回值。
Linux信号的有关概念及使用
3. 由软件条件产生信号
SIGPIPE是一种由软件条件产⽣的信号,我们首先认识一个函数alarm。

Linux信号的有关概念及使用
顾名思义,就是一个闹钟。
设定一个闹钟,告诉内核在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信号的有关概念及使用
在上图的例子中, 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信号的有关概念及使用

参数:
how的可选值:
         SIG_BLOCK(添加到当前信号屏蔽字的信号)
         SIG_UNBLOCK(解除当前信号屏蔽字中解除阻塞的信号)
         SIG_SETMASK(设置当前信号屏蔽字为set所指向的值)
set:更改进程当前信号屏蔽字
oldset:保存原有的信号屏蔽字
4、sigpending函数----读取当前进程的未决信号集
Linux信号的有关概念及使用
参数:
       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信号的有关概念及使用
结果,我们阻塞了SIGINT信号,当键盘输如组合键Ctrl-C时,使得SIGINT信号处于未决状态,不被处理。

Linux信号的有关概念及使用
五、捕捉信号

1、捕捉信号的过程
捕捉信号,即信号处理的handler自定义方法
如下图,为信号捕捉的过程:
Linux信号的有关概念及使用

2、sigaction函数 ----读取和修改与指定信号相关联的处理动作
Linux信号的有关概念及使用
参数:act和oldact指向sigaction结构体

sigaction结构体如下:
Linux信号的有关概念及使用

2、pause函数--使调用进程挂起直到有信号递达
如果信号的处理动作为忽略,则进程处于挂起状态;如果处理动作为终止进程,则进程终止;如果处理动作为捕捉,则调用了信号处理函数后返回-1。
Linux信号的有关概念及使用

六、用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信号的有关概念及使用
结果:

Linux信号的有关概念及使用