目录
一.信号的概念
1.1 认识信号
我们从两方面来认识信号:
从生活方面:
拿个生活中的例子:
你在网上买了件东西,之后只需要等待快递的到来,在这期间你会去干自己的其它事情,但是你知道你有一个快递。
在网上你买了一个东西就是信号的注册,快递员该你打电话要你拿一下快递,就是给你发送了一个信号。你收到信号之后,你知道怎么去处理这个信号,在这里就是去拿快递。但是你也不一定立马去拿,你可能会等你忙完现在的事在去处理。
在这期间你也不知道快递员什么时候会打电话给你,但是你也不是一直在等它,而是在做自己的事情,所以这就是异步的。
在这里你就是进程,操作系统就是快递员,信号就是快递。
操作系统给进程发生一个信号,进程收到信号后,知道怎么去处理这个信号。
从技术应用方面:
当我们运行一个前台进程,按下ctrl + c组合键时,进程会退出
这是因为当我们按下ctrl + c 时,产生了一个硬件中断,被操作系统获取到,然后系统发送了一个信号给前台进程。前台进程收到信号后,退出了进程。
为什么我们知道这里是一个信号?
首先介绍一个系统调用接口:
注意:
前台进程:是当前正在使用的程序,
后台进程:是在当前没有使用的但是也在运行的进程,包括那些系统隐藏或者没有打印的程序。后台进程运行时,可以其它运行前台进程。
一个bash终端只能运行一个前台进程。
- ctrl + c产生的信号只能发送给前台进程,一个进程如果在后台运行,该进程收不到该信号。运行程序时最后加一个&,让进程在后台运行。
- shell可以同时运行一个前台进程和多个后台进程,也就是说一个bash终端只能运行一个前台进程,但是后台可能会有多个后台进程在运行。
- 为什么说信号堆进程控制是异步的?因为一个进程在做自己的事情,信号不知道什么时候来,进程可能在任何时候收到信号而终止,所以是异步的。异步的意思是,不知道什么时候会发送信号。
1.2信号的概念
信号是进程之间事件异步通知的一种方式,属于软中断。
信号就是一个消息,告诉进程一个事件,进程受到信号之后会知道怎么处理这个信号。
1.3 查看系统定义的信号
1~31为普通信号,34~64为实时信号。
- 每一个信号都有一个编号和一个宏定义名称。
1.4 信号的处理方式
- 忽略信号。忽略信号也是处理了信号
- 执行信号的默认处理动作
- 利用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 名词概念
- 实际执行信号的处理动作叫信号递达
- 信号从产生到递达之间的状态叫信号未决
- 一个进程可以自己选择阻塞某个信号
- 被阻塞的信号,产生号就保持在未决状态,直到进程解除阻塞,才会进行递达操作。
- 注意:阻塞和忽略信号不同,阻塞信号,信号处于未决状态,并不是递达信号。忽略,是在递达信号。
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 #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 }
七.补充
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信号。默认处理方式会终止进程。