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

Linux信号的产生,处理

程序员文章站 2022-07-12 10:29:57
...

信号的概念:
在日常的学习中,当一个进程在执行时,按下crtl-C时就会产生一个硬件中断.(这也是一种信号)整个过程如下:
Linux信号的产生,处理

信号的产生:

  1. 通过键盘
    Core Dump:
    Core Dump其实是指一个文件,当一个进程因为异常终止时,可以将进程的内存数据全部保存到磁盘中,文件名通常就是core,所以这就是Core Dump.并且可以通过调试这个文件来查看我们的错误原因,这种方式叫做事后调试.
    注意:在Linux中,我们需要手动改变core文件的大小,因为默认该文件的大小为0;
  2. 通过命令(向指定信号发送指定命令)
    例如:下面的这个例子:
int main(int argc,char* argv[])                                                                                      
 14 {
 15     if(argc != 3)
 16     { 
 17         printf("usage:kill [-option] [number]\n");
 18         return 1;
 19     } 
 20     kill(atoi(argv[2]),atoi(argv[1]));
 21     return 0;
 22 }

int kill(pid_t pid,int singo);
函数作用:是给指定的进程发送指定的信号.
3. 通过异常

信号的处理:

  1. 忽略信号
  2. 执行信号的默认处理行为
  3. 自己写一个函数去捕捉信号
    下面我们通过一个函数来简单的检测一下我们电脑的速度:(就是上面的第三种处理信号的方式)
  1 #include <stdio.h>
  2 #include<stdlib.h>
  3 #include<signal.h>                                                                                                     
  4 #include<unistd.h>
  5 
  6 int count = 0;
  7 void hander(int signo)
  8 {
  9     printf("signo:%d,count:%d\n",signo,count);
 10     //printf("signo:%d\n",signo);
 11     exit(1);   //收到信号就终止程序
 12 }
 13 int main()
 14 {
 15     signal(14,hander);     
 16     alarm(1);  //1秒后向进程发送闹钟信号
 17     while(1)
 18     { 
 19         count++;    //只进行++,不进行I/O输出,这样效率又会提升很多   
 20         //printf("count = %d",count++); //如果每加一次就输出一次就会不停的进行I/O,那么速度将会非常的慢
 21     } 
 22 }

该函数的作用:不停的在1秒钟之内不停地数数,当一秒钟后就会被SIGALARM信号终止.在该进程中设置闹钟就是向操作系统设置闹钟.
Linux信号的产生,处理

阻塞信号:

信号未决(pending):收到了信号但没有处理得过程.
信号递达(delivery):处理收到的信号.
一个进程可以选择将某个信号阻塞,某个信号一旦阻塞,不再会递达,一直处于未决状态,直到该进程被解除阻塞,才会执行递答动作.
信号在内核中的表示方式:(是通过数据结构中的位图实现的,位图的下表就是的序号,位图的内容就是信号的状态,1表示收到了信号,0表示没有收到该信号)

对于普通信号,如果同时收到多个相同的信号.那么只会处理一次.
而对于实时信号,就会收到一次处理一次.

Linux信号的产生,处理

每个信号都有两个标志位分别表示阻塞和未决,还有一个对于该信号的处理方式.

Linux信号的产生,处理

如果处理信号的方式选择自定义的捕捉信号:那么内核中是怎么实现信号的捕捉的呢?
下面我们画一张图来模拟一下信号的处理过程:
Linux信号的产生,处理
总结:在信号的捕捉过程中,一共要进行4次的内核到用户的转变.

下面这个例子就是设置信号屏蔽字:

  1 #include <stdio.h>                                                                                                     
  2 #include<stdlib.h>
  3 #include<signal.h>
  4 #include<unistd.h>
  5 
  6 void hander(int signo)
  7 {
  8     printf("sig = %d\n",signo);
  9     //exit(1);
 10 }
 11 void Print(sigset_t *set)
 12 {
 13     int i = 1;
 14     for(i = 1;i < 32;++i)
 15     {
 16         if(sigismember(set,i))
 17         {
 18             printf("1");
 19         }
 20         else
 21         {
 22             printf("0");
 23         }
 24     }
 25     printf("\n");
 26 }
 27 int main()
 28 {
 29     //1.捕捉信号
 30     //2.设置信号屏蔽字
 31     //3.读取信号屏蔽字
 32     signal(SIGINT,hander);
 33     sigset_t set,oldset;
 34     sigemptyset(&set);
 35     sigaddset(&set,SIGINT);
 36     sigprocmask(SIG_BLOCK,&set,&oldset);
 37     int count = 0;
 38     while(1)
 39     {
 40         ++count;
 41         if(count == 5)
 42         {
 43             count = 0;
 44             printf("解除信号屏蔽字\n");
 45             sigprocmask(SIG_SETMASK,&oldset,NULL);
 46             sleep(1);
 47             printf("重新设置信号屏蔽字\n");
 48             sigprocmask(SIG_BLOCK,&set,NULL);
 49         }
 50         sleep(1);
 51         sigset_t p;
 52         sigpending(&p);  //获取当前进程的未决信号集
 53         Print(&p);
 54     }
 55     return 0;
 56 }                    

可重入函数
当同一个函数在不同的执行流中被调用时,如果在第一次调用还没有返回时再次进入该函数时,就成为重入.
比如:在进行单链表的插入时,在插入刚进行了一部分时,出现了硬件中断然后进程切换到内核,在返回用户态之前要进行检查有没有递达信号,于是就会切换到对应的信号处理函数中执行相应的代码,如果此时在该函数中也有对应的插入函数,那么就会在执行结束由于系统调用后再次进入内核态,然后从内核切换到用户态执行上次中断的地方.即将剩余的插入函数执行完毕.当插入函数结束时,此时就会在同一个地方有两个节点,此时就出现了错乱.
所以,在上面这中情况下是不可重入的.

总结:在下面几种情况下是不建议使用重入函数的:
1.使用了非常量的全局变量/静态变量.例如:malloc或free函数,malloc是由全局链表来管理的;比如标准库I/O库函数.
2.调用了其它不可重入的函数.

在上述例子中,如果插入是原子操作,那么就不会产生错乱.

volatile限定符

在C语言中的使用:例如:

  9     const int number = 10;                                                                                             
 10     printf("number = %d\n",number);
 11     int *p = (int*)&number;
 12     *p = 20;
 13     printf("number = %d\n",number);

结果:
g++中执行的结果是:10,10;
当加了volatile关键字后,就变成了10,20;

因为加了const后会进行优化,将const修饰的变量直接拷贝一份到寄存器中,而在使用时直接从寄存器中访问,这样就提高了访问效率.就造成了内存的不可见性,在对该变量进行修改时,就只在内存中修改了而寄存器中的却没有改变.所以加volatile关键字就可以保证每次读取时从内存中读取.
又如下面这个例子:

  7 int value = 1;                                                                                                
  8 void Handler(int signo)
  9 {
 10     (void)signo;
 11     value = 0;
 12 }
 13 int main()
 14 {
 15     signal(SIGINT,Handler);
 16     while(value);
 17     return 0;
 18 }

结论:如果是上面这种情况,那么当程序执行时按下ctrl+c就会终止程序.但是,在gcc编译器中,将优化级别加到O3,那么此时程序就不会终止.当在全局变量value前加上volatile关键字,那么按下ctrl+c就会停下来.

竞态条件
在进程调度时,产生的问题.
在这里:要学习一个函数:

int sigsuspend(const sigset_t *sigmask);

该函数的功能是可以进行原子操作.将解除信号屏蔽和挂起等待信号这两部合成一个原子操作.
注意:该函数的参数不能包含需要解除的信号.

SIGCHLD信号

该信号是一种避免产生僵尸进程的有效方法.wait()和waitpid()函数在清理僵尸进程时,父进程可以阻塞式的等待子进程的结束,也可以非阻塞式(轮询)的等待子进程的结束.
那么,子进程在结束时会给父进程发送SIGCHLD信号,内核对于这个信号的默认处理动作是忽略.如果父进程自定义的实现SIGCHLD信号的处理函数,那么子进程在结束时就会发送SIGCHLD信号,从而父进程就会调用函数去清理子进程.
例如:下面的例子:

  8 void Handler(int sig)
  9 {
 10     (void)sig;
 11     while(1)                                                                                                           
 12     {
 13         int ret = waitpid(-1,NULL,WNOHANG);
 14         if(ret > 0)
 15         {
 16             printf("ret = %d\n",ret);
 17             continue;
 18         }   
 19         else if(ret == 0)
 20         {
 21             break;
 22         }
 23         else
 24         {
 25             break;
 26         }
 27     }       
 28 }       
 29     
 30 int main()
 31 {
 32     signal(SIGCHLD,Handler);  //父进程可以做自己的事情,不用轮训的查看子进程是否结束
 33     int i = 0;
 32     for(i = 0;i < 20;++i)
 35     {     
 36         pid_t id = fork();
 37         if(id < 0)
 38         { 
 39             perror("fork()");
 40             return 1;
 41         } 
 42         if(id == 0)
 43         { 
 44             printf("child = %d\n",getpid());
 45             sleep(3);
 46             exit(0);
 47         } 
 48     }     
 49     while(1)
 50     {     
 51         printf("father\n");
 52         sleep(1);
 53     }     
 54     return 0;                                                                                                          
 55 }  

在Linux中,还有一种有效简洁处理子进程的方式:就是自定义的采用忽略的处理方式.

 32     signal(SIGCHLD,SIG_IGN);
 33     int i = 0;
 34     for(i = 0;i < 20;++i)
 35     {
 36         pid_t id = fork();
 37         if(id < 0)
 38         {
 39             perror("fork()");
 40             return 1;
 41         }
 42         if(id == 0)
 43         {
 44             printf("child = %d\n",getpid());
 45             sleep(3);
 46             exit(0);
 47         }
 48     }
 49     while(1)
 50     {
 51         printf("father\n");
 52         sleep(1);
 53     }

上面的方法也可以处理僵尸进程.将SIGCHLD的处理动作置为SIG_IGN,这样在子进程结束时就会自动清理,不会产生僵尸进程,也不会通知父进程.