赞
踩
目录
我们从两方面来认识信号:
从生活方面:
拿个生活中的例子:
你在网上买了件东西,之后只需要等待快递的到来,在这期间你会去干自己的其它事情,但是你知道你有一个快递。
在网上你买了一个东西就是信号的注册,快递员该你打电话要你拿一下快递,就是给你发送了一个信号。你收到信号之后,你知道怎么去处理这个信号,在这里就是去拿快递。但是你也不一定立马去拿,你可能会等你忙完现在的事在去处理。
在这期间你也不知道快递员什么时候会打电话给你,但是你也不是一直在等它,而是在做自己的事情,所以这就是异步的。
在这里你就是进程,操作系统就是快递员,信号就是快递。
操作系统给进程发生一个信号,进程收到信号后,知道怎么去处理这个信号。
从技术应用方面:
当我们运行一个前台进程,按下ctrl + c组合键时,进程会退出
这是因为当我们按下ctrl + c 时,产生了一个硬件中断,被操作系统获取到,然后系统发送了一个信号给前台进程。前台进程收到信号后,退出了进程。
为什么我们知道这里是一个信号?
首先介绍一个系统调用接口:
注意:
前台进程:是当前正在使用的程序,
后台进程:是在当前没有使用的但是也在运行的进程,包括那些系统隐藏或者没有打印的程序。后台进程运行时,可以其它运行前台进程。
一个bash终端只能运行一个前台进程。
信号是进程之间事件异步通知的一种方式,属于软中断。
信号就是一个消息,告诉进程一个事件,进程受到信号之后会知道怎么处理这个信号。
1~31为普通信号,34~64为实时信号。
注意:signal是修改了当前进程对信号的处理方式,等收到改变的信号时,直接实行自定义的函数
系统为了安全,9号进程不能被捕捉。
比如上面的ctrl + c 就给进程发送了2号信号SIGINT。而ctrl + \可以给进程发送3号信号SIGQUIT。
通过按键组合的方式来给进程发送信号。
kil命令是通过系统调用kill实现的。
这里补充一个知识点:强制类型转化和转化
强制类型转化并没有真正改变数据的值,在内存种原来怎么保存就怎么保存。
转化会将数据的值改变。
上面要将char * 类型转化成int类型,不是强制类型转化。
注意:abort函数一定会成功终止进程。不管有没有重新捕捉信号。
在匿名管道中,当读进程关闭时,写进程会收到系统发来的13号信号,终止写进程。系统发给写进程的13号信号就是软件条件生成的信号。
这里也有一个函数alarm,相当于设置一个闹钟,告诉内核多少秒后,发送一个SIGALRM信号给当前进程。
这里有一个现象:
同样将count++,1秒后发送SIGALARM信号给进程,同样时间:上面count才加到21095,下面加到了491153364,差了1000倍。
这是因为上面的代码要不断往屏幕打印,屏幕是外设,在不断进行I/O,时间消耗多。
进程有很多,可能alarm闹钟也会有很多,OS需要管理闹钟(才能知道哪个alarm是哪个进程,什么时候去发送信号等)。
OS管理闹钟需要先描述后组织,所以会有对应的数据结构来描述和组织闹钟。
硬件异常产生信号就是硬件发现进程的某种异常,而硬件是被操作系统管理。硬件会将异常通知给系统,系统就会向当前进程发送适当的信号。
例如:野指针的情况
原因:由于p是野指针,p指针变量里保存的是随机值,进程执行到野指针这一行。进程在页表中找映射的物理内存时,硬件mmu会发现该虚拟地址是一个 野指针,会产生异常,由于操作系统管理硬件,硬件会将异常发送给系统。系统会发送适当的信号给当前进程。
这里有个现象:
上面代码有野指针,系统会发送11号信号给进程。但是在循环里面并没有野指针,但是发现一直在打印,说明系统一直在往进程发送11号信号,这是因为硬件异常并没有消除。
只能向进程发送终止信号,终止进程才能结束。
所以在语言层面,出现的异常,大多数都是硬件异常,导致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
示意图:
源代码:
信号记录就是将进程收到的信号,在位图(阻塞位图和未决位图)对应的位置进行置1。
由于进程PCB中有对应数据结构,保证了记录操作的实施。
处理信号,就是进程收到信号,当进程对该信号不阻塞时,会在handle函数指针数组中找到对应的递达方法,来处理当前信号。
注意:当进程收到某信号,并不是立马进行处理的,而是等到合适的时机才进行处理。
处理信号有三种方法:
1.使用默认方法
2.忽略此信号
3.自定义捕捉
由于是由默认方法和忽略信号,就是在handle数组对应信号数组中填入SIG_DEL和SIG_IGN。很好理解,下面来说明一下自定义捕捉信号。
如果信号处理动作是用户自定义的函数,在信号递达时,就是调用的这个函数,这被称作捕捉信号。
进程收到信号不是立马处理信号,是在合适的时候处理信号的,合适的时候是当计算机从内核态切换成用户态时,检测并处理信号。
这里简单了解一下计算机的用户态和内核态。
计算机在运行程序时,会有两种状态,用户态和内核态。
当程序运行的是用户自己编写的代码,并没有涉及中断,异常会在系统调用时,计算机会处于用户态。
当程序运行到中断,异常或者系统调用时,计算机会处于内核态。内核态就相当于是操作系统。
但一个程序在运行时,可能在不断进行内核态和用户态的切换。
内核态的权限比用户态高。
计算机中怎么能实现用户态和内核态的互相切换?
因为在虚拟地址空间有两个区域,一个是用户区,一个是内核区。其中,用户区映射的是当计算机处于用户态时,要执行的代码和数据。内核区映射的是计算机处于内核态时,要执行的代码和数据。
当计算机处于用户态时,在虚拟地址空间的用户区,通过用户级页表,找到代码和数据执行。
当计算机处于内核态时,在虚拟地址空间的内核区,通过内核级页表,找到代码和数据执行。注意内核级页表每个进程是相同的,因为只有一个操作系统,每个进程虚拟地址空间内核区页表映射在物理内存同一位置。
怎么知道计算机现在处于用户态还行内核态?
在CPU中有一个寄存器CR0,里面有标志位记录了计算机处于内核态还是用户态。
信号捕捉示意图:
我们发现当我们自定义信号处理函数,会发生4次内核态和用户态相互转化的过程。如果没有自定义信号处理函数,只有2次用户态相互转化。
进程收到信号不是立马处理信号,而是在当计算机从内核态切换成用户态时,检测并处理信号。
就是因为内核态权限高,如果自定义信号处理函数中有非法动作,比如修改操作系统,在内核态能处理,但是用户态不能处理,这样会导致安全隐患。毕竟自定义信号处理函数是用户写的。
不会执行,操作系统在执行该信号时,会将进程block位图中信号位置设为1,阻塞该信号。一种信号只能同时处理一个,但是可以同时处理多种信号。
进程PCB中有两个位图,分别是block(阻塞位图)和pending(未决位图)。每个位置只有0和1两种状态,可以通过sigset_t类型定义的变量来存储阻塞位图和未决位图的位的信息,再通过其它系统调用来向阻塞位图或者未决位图赋值。
sigset_t就是信号集。
虽然sigset_t定义的变量的存储位图的位信息,但是我们不能使用位运算来修改sigset_t定义的变量。要通过函数,因为在不同平台下,sigset_t定义的变量并不是一个整数。
查看源码:
oldset为输出型参数,返回未修改前的阻塞位图信息。
上面有介绍一个
这里再介绍一个:功能一样,只是参数不同
编写代码使用上面的函数:
- 1 #include <stdio.h>
- 2 #include <unistd.h>
- 3 #include <signal.h>
- 4 //打印未决信号
- 5 void ShowBlock(sigset_t pending){
- 6 int i=0;
- 7 for(i=1; i<=31; i++){
- 8 //信号存在打印1
- 9 if(sigismember(&pending,i)){
- 10 printf("1");
- 11 }
- 12 //不存在打印0
- 13 else{
- 14 printf("0");
- 15 }
- 16 }
- 17 printf("\n");
- 18
- 19 }
- 20 void handle(int signo){
- 21 printf("i am signal %d\n",signo);
- 22 }
- 23
- 24 int main(){
- 25 //
- 26 struct sigaction act;
- 27 struct sigaction oact;
- 28 act.sa_flags=0;
- 29 sigemptyset(&act.sa_mask);
- 30 //自定义处理函数
- 31 act.sa_handler=handle;
- 32 //替换2号信号的处理函数
- 33 sigaction(2,&act,&oact);
- 34
- 35
- 36 sigset_t pending;
- 37 sigset_t block;
- 38 sigset_t oblock;//旧阻塞位图
- 39 sigemptyset(&block);
- 40 sigemptyset(&oblock);
- 41 sigaddset(&block,2);
- 42 sigprocmask(SIG_SETMASK, &block, &oblock);//将2号信号阻塞
- 43 int count=0;
- 44 while(1){
- 45 //必须将信号阻塞才能看到未决,不然就递达了。
- 46 sigemptyset(&pending);
- 47 sigpending(&pending);//获取未决信号
- 48 ShowBlock(pending);
- 49 sleep(1);
- 50 count++;
- 51 //10秒后还原阻塞位图
- 52 if(count==10){
- 53 //将阻塞信号还原
- 54 sigprocmask(SIG_SETMASK, &oblock, &block);
- 55 }
- 56 }
- 57
- 58 return 0;
- 59 }
说明,一个进程可能有多个执行流。比如说信号。当自定义捕捉信号函数,当信号没来时,信号不会执行处理信号函数,只会执行自己的代码。当信号来了,会去执行处理信号的函数。
可重入函数就是,当一个执行流进入一个函数,当中又进入了这个函数,不会出现错误的函数。
不可重入函数就是,当一个执行流进入一个函数,当中又进入了这个函数,会出现错误的函数。
比如:链表的插入函数,当执行头插入函数时,需要将新节点执行当前肉节点,再将新节点地址保存到头节点里。
如果符合以下条件之一则是不可重入的:
父进程创建子进程需要以调用wait或者waitpid来等待子进程退出,不然子进程会变成僵尸进程。
但是再子进程退出时,会向父进程发送SIGCHLD信号。我们可以自定义SIGCHLD信号的捕捉方式,可以使得父进程不以阻塞状态等子进程退出。或者,如果父进程将SIGSHLD信号以忽略方式处理,同样子进程不会变成僵尸状态,会释放掉自己的数据结构和空间。
子进程退出会向父进程发送SIGCHLD信号
父进程对SIGCHLD处理方式如果为SIG_ING忽略,子进程不会进入僵尸状态,会自动清理子进程的空间和数据结构
在网络中,当一段已经调用close关闭socket返回的文件描述符。另一端向其发送消息,会受到SIGPIPE信号。默认处理方式会终止进程。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。