当前位置:   article > 正文

Linux之信号_linux 信号

linux 信号

目录

一.信号的概念

        1.1 认识信号

         1.2信号的概念

         1.3 查看系统定义的信号

        1.4 信号的处理方式 

 二.产生信号

         2.1 通过终端按键来产生信号

        2.2 通过系统函数来向进程发送信号

         2.3 由软件条件产生信号

         2.4 硬件异常产生信号

        2.5 总结

三.信号记录和信号处理

        3.1 名词概念

        3.2 在内核中信号在内存中的表示

        3.3 信号记录

        3.4 信号处理

        3.4.1 捕捉信号

四.信号集操作函数

        4.1 sigset_t 类型

         4.2信号集操作函数

五.修改进程阻塞位图和获取进程未决位图

         5.1 修改阻塞位图系统调用

        5.2 获取未决位图信息

六.自定义捕捉函数

七.补充

        7.1 可重入函数

       7.2 SIGCHID信号


一.信号的概念

        1.1 认识信号

         我们从两方面来认识信号:

从生活方面:

拿个生活中的例子:

        你在网上买了件东西,之后只需要等待快递的到来,在这期间你会去干自己的其它事情,但是你知道你有一个快递。

        在网上你买了一个东西就是信号的注册,快递员该你打电话要你拿一下快递,就是给你发送了一个信号。你收到信号之后,你知道怎么去处理这个信号,在这里就是去拿快递。但是你也不一定立马去拿,你可能会等你忙完现在的事在去处理。

        在这期间你也不知道快递员什么时候会打电话给你,但是你也不是一直在等它,而是在做自己的事情,所以这就是异步的。

        在这里你就是进程,操作系统就是快递员,信号就是快递。

        操作系统给进程发生一个信号,进程收到信号后,知道怎么去处理这个信号。

从技术应用方面:

当我们运行一个前台进程,按下ctrl + c组合键时,进程会退出

        这是因为当我们按下ctrl + c 时,产生了一个硬件中断,被操作系统获取到,然后系统发送了一个信号给前台进程。前台进程收到信号后,退出了进程。 

为什么我们知道这里是一个信号?

首先介绍一个系统调用接口:

 注意:

        前台进程:是当前正在使用的程序,

        后台进程:是在当前没有使用的但是也在运行的进程,包括那些系统隐藏或者没有打印的程序。后台进程运行时,可以其它运行前台进程。

        一个bash终端只能运行一个前台进程。

  • ctrl + c产生的信号只能发送给前台进程,一个进程如果在后台运行,该进程收不到该信号。运行程序时最后加一个&,让进程在后台运行。

  •  shell可以同时运行一个前台进程和多个后台进程,也就是说一个bash终端只能运行一个前台进程,但是后台可能会有多个后台进程在运行。
  • 为什么说信号堆进程控制是异步的?因为一个进程在做自己的事情,信号不知道什么时候来,进程可能在任何时候收到信号而终止,所以是异步的。异步的意思是,不知道什么时候会发送信号。

         1.2信号的概念

        信号是进程之间事件异步通知的一种方式,属于软中断。

        信号就是一个消息,告诉进程一个事件,进程受到信号之后会知道怎么处理这个信号。

         1.3 查看系统定义的信号

1~31为普通信号,34~64为实时信号。

  • 每一个信号都有一个编号和一个宏定义名称。

        1.4 信号的处理方式 

  1.  忽略信号。忽略信号也是处理了信号
  2. 执行信号的默认处理动作
  3. 利用signal系统调用,提供一个信号处理函数,要求在内核处理该信号时切换到用户态执行这个函数,这种方式称为捕捉一个信号。就像上面的代码,将2号信号捕捉为一个handler函数。

注意:signal是修改了当前进程对信号的处理方式,等收到改变的信号时,直接实行自定义的函数

        系统为了安全,9号进程不能被捕捉。

 二.产生信号

         2.1 通过终端按键来产生信号

        比如上面的ctrl + c 就给进程发送了2号信号SIGINT。而ctrl + \可以给进程发送3号信号SIGQUIT。

        通过按键组合的方式来给进程发送信号。

        2.2 通过系统函数来向进程发送信号

  • kill 系统调用,作用:给当进程为pid的进程发送信号

kil命令是通过系统调用kill实现的。

这里补充一个知识点:强制类型转化和转化

        强制类型转化并没有真正改变数据的值,在内存种原来怎么保存就怎么保存。

        转化会将数据的值改变。

        上面要将char * 类型转化成int类型,不是强制类型转化。

  •  raise函数:作用:给当前进程发送信号

  •  abort函数,作用:使当前进程收到6号信号,后异常终止。

注意:abort函数一定会成功终止进程。不管有没有重新捕捉信号。

         2.3 由软件条件产生信号

        在匿名管道中,当读进程关闭时,写进程会收到系统发来的13号信号,终止写进程。系统发给写进程的13号信号就是软件条件生成的信号。

这里也有一个函数alarm,相当于设置一个闹钟,告诉内核多少秒后,发送一个SIGALRM信号给当前进程。

 这里有一个现象:

同样将count++,1秒后发送SIGALARM信号给进程,同样时间:上面count才加到21095,下面加到了491153364,差了1000倍。

这是因为上面的代码要不断往屏幕打印,屏幕是外设,在不断进行I/O,时间消耗多。

        进程有很多,可能alarm闹钟也会有很多,OS需要管理闹钟(才能知道哪个alarm是哪个进程,什么时候去发送信号等)。

        OS管理闹钟需要先描述后组织,所以会有对应的数据结构来描述和组织闹钟。 

         2.4 硬件异常产生信号

        硬件异常产生信号就是硬件发现进程的某种异常,而硬件是被操作系统管理。硬件会将异常通知给系统,系统就会向当前进程发送适当的信号。

例如:野指针的情况

        原因:由于p是野指针,p指针变量里保存的是随机值,进程执行到野指针这一行。进程在页表中找映射的物理内存时,硬件mmu会发现该虚拟地址是一个 野指针,会产生异常,由于操作系统管理硬件,硬件会将异常发送给系统。系统会发送适当的信号给当前进程。

这里有个现象:

        上面代码有野指针,系统会发送11号信号给进程。但是在循环里面并没有野指针,但是发现一直在打印,说明系统一直在往进程发送11号信号,这是因为硬件异常并没有消除。

        只能向进程发送终止信号,终止进程才能结束。 

        所以在语言层面,出现的异常,大多数都是硬件异常,导致OS发送信号,来终止进程。

        2.5 总结

信号是由OS发出来的,上面的四种产生情况,是操作系统发出信号的触发条件。

  • 上面所有信号的产生都会需要操作系统的参与,为什么?

因为操作系统是进程的管理者,只有操作系统能管理进程。

  • 信号处理是什么时候被处理的?

在合适的时候处理,并不是信号来了就处理。所以需要记录下来。

  • OS怎么向进程发送信号?

        普通信号有31个,进程PCB中有一个数据结构,是位图(只需要一个整数即可)。来记录当前进程是否收到对应位置的信号,OS向进程发送信号时,将对应位置置1即可。

        实时信号是由链表构成的,一个时间可以收到多个信号,不会丢失。

补充一个概念:

Core Dump:

        什么是Core Dump?当一个进程异常终止时,在异常终止前,会把进程用户空间的内存数据全部保存到硬盘上。文件名通常较core + 进程pid。

        事后调试:进程异常终止时因为由bug,出现异常后可以使用调试器gdb检查core文件来了解错误原因。

        云服务器默认不允许产生core文件,因为core文件由大小,大小限制可以自己设置。如果一个进程总是挂掉,导致core文件很多,占用空间。可以通过ulimit -c 设置core 文件大小。

        gdb + 要调试的可执行程序

        core file + core文件 就可以查看错误信息。

        在waitpid中第二个参数接收进程返回状态,其中第8位为core dump,0为不需要core dump,1为需要core dump

三.信号记录和信号处理

        3.1 名词概念

  1. 实际执行信号的处理动作叫信号递达
  2. 信号从产生到递达之间的状态叫信号未决
  3. 一个进程可以自己选择阻塞某个信号
  4. 被阻塞的信号,产生号就保持在未决状态,直到进程解除阻塞,才会进行递达操作。
  5. 注意:阻塞和忽略信号不同,阻塞信号,信号处于未决状态,并不是递达信号。忽略,是在递达信号。

        3.2 在内核中信号在内存中的表示

示意图:

 源代码:

        3.3 信号记录

        信号记录就是将进程收到的信号,在位图(阻塞位图和未决位图)对应的位置进行置1。

        由于进程PCB中有对应数据结构,保证了记录操作的实施。

        3.4 信号处理

        处理信号,就是进程收到信号,当进程对该信号不阻塞时,会在handle函数指针数组中找到对应的递达方法,来处理当前信号。

        注意:当进程收到某信号,并不是立马进行处理的,而是等到合适的时机才进行处理。

        处理信号有三种方法:

        1.使用默认方法

        2.忽略此信号

        3.自定义捕捉

        由于是由默认方法和忽略信号,就是在handle数组对应信号数组中填入SIG_DEL和SIG_IGN。很好理解,下面来说明一下自定义捕捉信号。

        3.4.1 捕捉信号

        如果信号处理动作是用户自定义的函数,在信号递达时,就是调用的这个函数,这被称作捕捉信号。

        进程收到信号不是立马处理信号,是在合适的时候处理信号的,合适的时候是当计算机从内核态切换成用户态时,检测并处理信号。

 这里简单了解一下计算机的用户态和内核态。

计算机在运行程序时,会有两种状态,用户态和内核态。

当程序运行的是用户自己编写的代码,并没有涉及中断,异常会在系统调用时,计算机会处于用户态。

当程序运行到中断,异常或者系统调用时,计算机会处于内核态。内核态就相当于是操作系统。

但一个程序在运行时,可能在不断进行内核态和用户态的切换。

内核态的权限比用户态高。

计算机中怎么能实现用户态和内核态的互相切换?

        因为在虚拟地址空间有两个区域,一个是用户区,一个是内核区。其中,用户区映射的是当计算机处于用户态时,要执行的代码和数据。内核区映射的是计算机处于内核态时,要执行的代码和数据。

        当计算机处于用户态时,在虚拟地址空间的用户区,通过用户级页表,找到代码和数据执行。

        当计算机处于内核态时,在虚拟地址空间的内核区,通过内核级页表,找到代码和数据执行。注意内核级页表每个进程是相同的,因为只有一个操作系统,每个进程虚拟地址空间内核区页表映射在物理内存同一位置。

 怎么知道计算机现在处于用户态还行内核态?

         在CPU中有一个寄存器CR0,里面有标志位记录了计算机处于内核态还是用户态。

信号捕捉示意图:

        我们发现当我们自定义信号处理函数,会发生4次内核态和用户态相互转化的过程。如果没有自定义信号处理函数,只有2次用户态相互转化。

        进程收到信号不是立马处理信号,而是在当计算机从内核态切换成用户态时,检测并处理信号。

  • 内核态权限比用户态高,为什么执行自定义信号处理函数还需要从内核态切换到用户态?

        就是因为内核态权限高,如果自定义信号处理函数中有非法动作,比如修改操作系统,在内核态能处理,但是用户态不能处理,这样会导致安全隐患。毕竟自定义信号处理函数是用户写的。

  • 如果不断受到一个信号,该信号处理动作为自定义的,而自定义函数中有系统调用,执行系统调用,会要从用户态切换到内核态,当从内核态切换到用户态时,又受到同样等信号,需要处理吗?在处理信号时,有内核态到用户态的情况,在这过程中有收到相同信号,需要处理吗?

        不会执行,操作系统在执行该信号时,会将进程block位图中信号位置设为1,阻塞该信号。一种信号只能同时处理一个,但是可以同时处理多种信号。

四.信号集操作函数

        4.1 sigset_t 类型

        进程PCB中有两个位图,分别是block(阻塞位图)和pending(未决位图)。每个位置只有0和1两种状态,可以通过sigset_t类型定义的变量来存储阻塞位图和未决位图的位的信息,再通过其它系统调用来向阻塞位图或者未决位图赋值。

        sigset_t就是信号集。

        虽然sigset_t定义的变量的存储位图的位信息,但是我们不能使用位运算来修改sigset_t定义的变量。要通过函数,因为在不同平台下,sigset_t定义的变量并不是一个整数。

查看源码:

         4.2信号集操作函数

  • 再使用sigset_t变量前,由先调用sigemptyset或者 sigfillset函数,初始化变量。
  • 上面4个的返回值都是成功返回1,失败返回0,sigismumber存在返回1,不存在返回0,失败返回-1。

五.修改进程阻塞位图和获取进程未决位图

         5.1 修改阻塞位图系统调用

         oldset为输出型参数,返回未修改前的阻塞位图信息。

        5.2 获取未决位图信息

六.自定义捕捉函数

上面有介绍一个

 这里再介绍一个:功能一样,只是参数不同

  •  sa_mask,之前有说到过当在处理一个信号的自定义函数时,这个信号会被系统阻塞,直到处理完。如果还想阻塞其它的信号,可以设置sa_mask。
  • sigaction多用于实时信号。

编写代码使用上面的函数:

  1. 1 #include <stdio.h>
  2. 2 #include <unistd.h>
  3. 3 #include <signal.h>
  4. 4 //打印未决信号
  5. 5 void ShowBlock(sigset_t pending){
  6. 6 int i=0;
  7. 7 for(i=1; i<=31; i++){
  8. 8 //信号存在打印1
  9. 9 if(sigismember(&pending,i)){
  10. 10 printf("1");
  11. 11 }
  12. 12 //不存在打印0
  13. 13 else{
  14. 14 printf("0");
  15. 15 }
  16. 16 }
  17. 17 printf("\n");
  18. 18
  19. 19 }
  20. 20 void handle(int signo){
  21. 21 printf("i am signal %d\n",signo);
  22. 22 }
  23. 23
  24. 24 int main(){
  25. 25 //
  26. 26 struct sigaction act;
  27. 27 struct sigaction oact;
  28. 28 act.sa_flags=0;
  29. 29 sigemptyset(&act.sa_mask);
  30. 30 //自定义处理函数
  31. 31 act.sa_handler=handle;
  32. 32 //替换2号信号的处理函数
  33. 33 sigaction(2,&act,&oact);
  34. 34
  35. 35
  36. 36 sigset_t pending;
  37. 37 sigset_t block;
  38. 38 sigset_t oblock;//旧阻塞位图
  39. 39 sigemptyset(&block);
  40. 40 sigemptyset(&oblock);
  41. 41 sigaddset(&block,2);
  42. 42 sigprocmask(SIG_SETMASK, &block, &oblock);//将2号信号阻塞
  43. 43 int count=0;
  44. 44 while(1){
  45. 45 //必须将信号阻塞才能看到未决,不然就递达了。
  46. 46 sigemptyset(&pending);
  47. 47 sigpending(&pending);//获取未决信号
  48. 48 ShowBlock(pending);
  49. 49 sleep(1);
  50. 50 count++;
  51. 51 //10秒后还原阻塞位图
  52. 52 if(count==10){
  53. 53 //将阻塞信号还原
  54. 54 sigprocmask(SIG_SETMASK, &oblock, &block);
  55. 55 }
  56. 56 }
  57. 57
  58. 58 return 0;
  59. 59 }

七.补充

        7.1 可重入函数

        说明,一个进程可能有多个执行流。比如说信号。当自定义捕捉信号函数,当信号没来时,信号不会执行处理信号函数,只会执行自己的代码。当信号来了,会去执行处理信号的函数。

        可重入函数就是,当一个执行流进入一个函数,当中又进入了这个函数,不会出现错误的函数。

        不可重入函数就是,当一个执行流进入一个函数,当中又进入了这个函数,会出现错误的函数。

比如:链表的插入函数,当执行头插入函数时,需要将新节点执行当前肉节点,再将新节点地址保存到头节点里。

 如果符合以下条件之一则是不可重入的:

  • 调用了malloc和free,因为malloc也是用全局链表管理堆的。
  • 调用标志I/O库函数,标志I/O库函数很多实现都以不可重入的方式使用全局数据结构。

       7.2 SIGCHID信号

        父进程创建子进程需要以调用wait或者waitpid来等待子进程退出,不然子进程会变成僵尸进程。

        但是再子进程退出时,会向父进程发送SIGCHLD信号。我们可以自定义SIGCHLD信号的捕捉方式,可以使得父进程不以阻塞状态等子进程退出。或者,如果父进程将SIGSHLD信号以忽略方式处理,同样子进程不会变成僵尸状态,会释放掉自己的数据结构和空间。

子进程退出会向父进程发送SIGCHLD信号

父进程对SIGCHLD处理方式如果为SIG_ING忽略,子进程不会进入僵尸状态,会自动清理子进程的空间和数据结构

        7.3 SIGPIPE信号

        在网络中,当一段已经调用close关闭socket返回的文件描述符。另一端向其发送消息,会受到SIGPIPE信号。默认处理方式会终止进程。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/460122
推荐阅读
相关标签
  

闽ICP备14008679号