什么是信号
生活中的信号:闹钟,红绿灯,鸡叫声,,,,,,
这些场景触发的时候我们就知道自己该做什么了。 在信号产生之前,我们也知道遇到什么信号该做什么事情。
同样的,我们受到信号以后也不一定马上会去执行,也有可能信号发出来了,我们收不到的情况。
例如闹钟在客厅响了,你戴着耳机在卧室和女朋友一起愉快的开黑玩游戏,这时候你就听不到信号,听到了也不想管。
同样的信号产生后,是OS给进程发送的,向进程内核数据结构task_struct写入信号数据。
同样的道理,进程收到信号以后也像我们人一样收到信号。 未收到的时候知道该如何处理,不立即处理,暂时保存信号数据,
到时候再处理,也有可能不处理或者收不到信号
也就是进程处理信号的方式:
1,忽略此信号。
2,执行该信号的默认处理动作。
3,提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉
(Catch)一个信号。
如何产生信号:
1,键盘产生(只作用于前台进程,./test & 就变成了后台进程)
后台程序基本上不和用户交互,优先级别稍微低一点
前台的程序和用户交互,需要较高的响应速度,优先级别稍微高一点
2,程序中的异常问题,导致进程受到信号而退出
3,通过系统调用去产生信号
4,软件条件产生信号。(例如在匿名管道中,读端关闭,写端收到13信号后关闭)
例如进程在死循环的时候,我们在键盘上输入ctrl+c,实际上就是通过OS给该进程发送了2号信号
kill -l 可以查看信号对应的编号信息。
man 7 signal 可以查看受到编号信号后进程的默认动作。
signal函数的功能(捕捉信号后自己处理)
其中9号进程无法被定义。
Core Dump(核心转储)
1,在Linux中,当一个进程退出的时候,它的退出码和退出信号都会被设置(正常情况)
2,如果进程异常,进程的退出信号会被设置表明进程退出的原因,如果必要,OS会设置core dump标志位。
什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁
盘上,文件名通常是core,这叫做Core Dump。默认是不允许产生core文件的,
因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许
产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c
1024
有什么作用呢? 方便我们调试,寻找到错误所在的地方。
但是要注意一下,Linux下默认是release版本,编译的时候要 + -g
说到这里,顺便复习下gdb的调试功能把
r 是运行
l 列出代码
s 逐语句
b 是设置断点
info b 查看断点
p sum 查看sum的值,值显示一次
display sum 常显示
undispaly 不显示
d x 删除编号x的断点
kill,raise,alarm系统调用
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
alarm()函数 比较简单,就不做演示了。
int main()
10 {
11 int pid = fork();
12 if(pid < 0)
13 {
14 perror("fork");
15 exit(-1);
16 }
17
18 if(pid == 0)
19 {
20 sleep(5);
21 printf("I am a child");
22 kill(getppid(),2); // 给父进程发送2号信号ctrl c终止
23 }
24 else
25 {
26 while(1)
27 {
28 printf("I am panret\n");
29 sleep(1);
30 }
31
32 }
35 return 0;
36 }
4 int main()
15 {
16 int count = 0;
17 while(1)
18 {
19 printf("hello chen\n");
20 count++;
21
22 if(count == 10)
23 raise(9); //自己给自己发送9号信号
24 }
25
26 return 0;
27 }
28
再度理解OS给进程发送信号
实际上就是OS向进程控制块写入数据
信号递达(如何处理信号的动作):1,自定义捕捉。 2,默认处理。3,忽略
未决:信号暂存于task_struct中,(还未处理)
阻塞:OS运行进程暂时屏蔽指定信号
1,该信号还是未决状态,2,该信号不能被处理,直到解除阻塞
下图的SIG_DEL是默认动作,SIG_IGN是忽略动作。
总结:
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
因为OS是进程的管理者,所以无论信号如何产生,最终都是通过OS来发送给进程的。
信号的处理是否是立即处理的?
在合适的时候(就是由内核态转换为用户态的时候,后面详细说明)
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
信号肯定会被立即记录,记录在进程的PCB中
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
肯定知道。如果不知道的话,进程收到信号难道原地发呆吗?
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
当信号产生后,OS找到对应进程的PCB,向PCB中写入相关的信号数据,将进程PCB中的pending位图相应位置置1,
如果block位图中的相应信号编号位置是阻塞,则等到解除阻塞后再执行。
如果不是阻塞,则从handler表(函数指针数组)进行递达。
递达方式有忽略,默认,自定义。
忽略直接将pending位图直接置0.
默认,OS直接执行。
如果是自定义,由内核态进入用户态,切换成用户身份后来执行相应的代码和数据,然后返回内核态,再返回用户态最终处理完成。
信号集操作函数
OS提供系统调用接口,那肯定也提供了相应的数据类型。
例如sigset_t 是信号集类型,只能由OS提供的系统调用去处理
#include <signal.h>
int sigemptyset(sigset_t *set); //全部置0
int sigfillset(sigset_t *set); //全部置1
int sigaddset (sigset_t *set, int signo); //相应位置置1
int sigdelset(sigset_t *set, int signo); //相应位置置0
int sigismember(const sigset_t *set, int signo) //检查相应位置是否为1
sigprocmask 和sigpending
来段代码操作一下
8 void showsigpending(sigset_t* set)
9 {
10 int i = 1;
11 for(i = 1; i <= 31; i++)
12 {
13 if(sigismember(set,i)) //如果未决存在,则输出1
14 printf("1");
15 else
16 printf("0");
17 }
18 fflush(stdout);
19 printf("\n");
20 }
21
22
23 int main()
24 {
25 sigset_t s,p;
26 sigemptyset(&s);
27 sigisemptyset(&p); //这个用来获取未决状态
28
29 sigaddset(&s,2); //将s里面信号集中的二号信号设为1 ctrl + c
30 sigprocmask(SIG_SETMASK,&s,NULL); // 阻塞2号,不关心之前的信号屏蔽字
31
32 while(1)
33 {
sigpending(&p); //获取当前信号的未决 集
35 showsigpending(&p);
36 sleep(1);
37 }
41 return 0;
42 }
自定义捕捉详解
我们分析一下递达方式:自定义捕捉,当我们发送特定信号的时候,去执行用户给的自定义函数。
要说清这个,需要知道用户态和内核态的区别。
用户态:只能执行用户的代码,使用用户的数据所处的状态,其中用户以进程为代表(可理解为进程就是用户,但并不准确)
内核态:只能执行OS的代码和数据,所处的状态。
之间的根本区别:就是权限问题。 OS是进程的管理者,意味着内核态的权限非常大。
解析:
1过程,为什么要切换内核态呢? OS是进程的管理者,进程出现异常,肯定由OS发送信号通知。
3过程为什么要返回用户态去执行呢? 如果不返回在内核态执行用户态的代码这肯定是不允许的,为什么呢?
内核态的权限非常大,万一来一个 rm -rf / ,OS不就凉凉了吗。
4过程,执行完后为什么要返回呢?main 和handler不属于调用的关系,完全是两个独立的执行流,所以无法从handler返回到main()
所以handler()执行完后,只能借助相应的系统调用函数,返回内核后,在从内核返回main的执行流
简单记忆:数学符号无穷大