目录
一、进程信号
1、信号的概念
信号是一个软件中断
生活当中“媳妇过马路的例子”
媳妇叫吃饭只是一个信号告诉你要吃饭,但不是强制要吃饭
1.1只是告诉有这样一个信号,但是具体这个信号怎么处理,什么时候处理由进程决定,所以是软中断
2、信号的产生
2.1硬件产生(按键盘中的按键):
- ctrl+c:2号信号 SIGINT,按下ctrl+c其实是进程收到了2号信号,2号信号导致进程的退出。
- ctrl+z :20号信号SIGTSTP,
ctrl+|:3号信号SIGQUIT
kill -l 命令可以查看操作系统定义的信号值以及信号所对应的名称
那这些信号又都是干什么的呢?
我们可以看到SIGINT的默认动作是term终止
通过kill命令也是可以发送信号的
2.2软件产生:
1、kill函数
int kill(pid_t pid,int sig);
- 参数:
- pid_t:要给那个进程发,就填那个进程的pid
- sig:要给进程发送的信号
使用: 我们可以给当前进程发送一个2号信号,然后再运行一下看看结果
运行一下:
发现这个进程终止了
2.raise函数:
- 作用:谁调用给谁发送信号
- 参数:给调用的进程发送信号的信号值
- 使用:给自己发送一个2号信号,终止进程
我们的代码是在这个进程里执行了raise(2)
一运行:我们发现进程同样在raise调用后就终止了
3.kill- [num] [pid] 可以给进程发送信号
我们可以看到进程被暂停了
3.信号的种类
kill -l 可以罗列信号
- 非实时信号(非可靠信号):
- 特点:信号可能会丢失(1-31)
- 实时信号(可靠信号):
- 特点:信号不会丢失(33-64)
4.信号的处理方式
操作系统对信号的处理方式(man 7 signal
term core cont ign stop
- 默认处理方式:
- SIG_ DFL,操作系统当中已经定义号信号的处理方式了
- 例如2号信号->终止进程
- 11->终止进程,并且产生核心转储文件
忽略处理方式:
- SIG_ IGN, 该信号为忽略处理(僵尸进程)
- 进程收到忽略处理的方式的信号后,是不进行处理的,例如之前学过的僵尸进程的产生:
- 子进程先于父进程退出,子进程在退出的时候会给父进程发送一个SIGCHLD信号,而父进程对这个信号的处理方式是忽略处理的,导致父进程并没有回收子进程的退出状态信息,从而子进程变成了僵尸进程
- 自定义处理方式:
- 程序员可以更改信号的处理方式,定义一 个函数,当进程收到该信号的时候, 调用程序猿自己写的函数。(第7个小点涉及到)
5.信号的注册
5.1基础概念了解:
- 一个进程收到一个信号,这个过程称之为注册
- 信号的注册和注销并不是一个过程,是两个独立的过程
内核中信号注册位图以及s igqueue队列的的了解
- 都是task_ struct结构体内部的内容
- 每一个进程都有自己独有的注册位图和sigqueue队列
5.2信号的注册:
- 位图更改为1,添加Sigqueue节点到sigqueue队列
- 信号在注册的时候,会将信号对应的比特位从0修改为1,表示当前进程收到了该信号。
- 还需要哎sigqueue队列中添加一个sigqueue节点,队列在操作系统内核当中本质上是一个双向链表(先进先出的特性
注册的过程大致如下:
-
5.3 实时信号和非实时信号在注册时的区别:
- 非实时信号(非可靠信号)的注册
- 第一次注册:修改sig位图(0-1),修改sigqueue队列。
- 第二次注册:相同信号值的信号,在前一个信号未被处理的前提下:修改sig位图(1->1),并不会添加sigqueue节点。
- 总结:再次添加,不会添加sigqueue节点
- 实时信号(可靠信号)的注册
- 第一次注册:修改sig位图(0-1),修改sigqueue队列。
- 第二次注册:相同信号值的信号:修改sig位图(1->1),添加sigqueue节点到sigqueue队列中。
- 再次添加,会再次添加siquque节点
非实时信号容易信号丢失的原因就是再次注册的时候不会添加sigqueue节点
6.统一下对信号的理解
这里分为:收到信号之前,收到信号,处理信号三个部分
7.信号的注销:
-
7.2可靠信号的注销
- 1.将对应的信号的sigqueue节点进行出队操作
- 2.判断s igqueue队列当中还有相同信号的S igqueue节点吗
- 如果有:则比特位不变
- 如果没有:则比特位改变位0
8.信号的自定义处理方式
8.2 函数
- 1.sighandler_t signal (int signum, sighandler_t handler);
- 作用:
- 在调用signal函数的时候,我们给函数的第二个参数传递一个回调函数的地址,当我们收到第一个参数所定义的信号值时,就会调用回调函数,执行回调函数的功能。
- 参数:
- signum:信号值
- handler:更改为哪一 个函数处理,接受一 个函数地址,函数指针(回调函数)。
- typedef void (*sighandler_ t)(int);
- 代码验证:
- 我们写一个代码测试当进程收到2号信号的时候,会发生什么,看还是不是终止进程了
知道了信号的自定义处理方式以后,我们就有了一个大胆的想法:
来看下下面的这段代码:
那么,我这个进程不就是“刀枪不入”了吗?
我们来看看下面的情况
kill 其他的信号的时候,都不起作用,只有9号信号起作用了,这是为什么呢?
因为9号信号是强杀信号,不能被自定义处理,(否则那不就和癌细胞一样了吗。。。)
3.int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact)
对于这个函数我们也可以来写代码验证一下:
我们可以发现,这个函数能起到和signal函数一样的效果
我们还可以利用这个函数进行风骚一点的操作
我们来看下面这段代码:
这个代码里面我们实际上对2号信号进行了两次的更改,我们来执行下这个代码:
8.3两个函数在内核中的原理:
内核当中的结构体
插播一下对进程内核代码的处理
首先我们要知道进程内核代码的位置
知道了位置之后,我们就能找到了
vim sched.h以后我们便进入了界面
那么怎样更好的查找各个结构体的定义及其使用的地方呢?
我们可以通过ctags 来给源代码创建索引关系
9.信号的捕捉流程
- 当我们的进程从内核态切换回用户态时,会调用do_signal,检查进程是否收到了信号。
一个进程由操作系统给他注册了一个信号,这个进程是从用户空间切换到内核空间的时候,才去处理这个信号,
调用dosignal函数判断进程有没有收到信号,有就处理,处理完成的时候,这个信号要从这个进程当中注销掉,
注销掉之后就回到用户态继续执行他的代码,也有可能在处理信号的时候,这个进程直接就终止掉了
10.信号的阻塞
-
10.2接口:
- int sigprocmask (int how, const sigset_ t *Set, sigset_ t *oldset);(无法阻塞9号和19号信号)
参数:
- how:想让s igp rocmask做什么事情
- SIG_ BLOCK: 设置某个信号为阻塞状态
- SIG_ UNBL.OCK :设置 某个信号为非阻寒状态
- SIG_ SETMASK :用第二个参数“set”,替换原来的阻寨位图。 ( 替换的意思)
- set :新设置的阻塞位图
- 根据传递进的函数变量计算新的阻塞位图:
- 1.阻塞单个信号,阻塞多个信号(只要将相应的信号位图设置为1即可)
- 2.接触阻塞单个信号/解除阻塞多个信号。
- oldset :原来老的阻塞位图
原理解析:
- 当how为SIG_ _BLOCK时,函 数会根据set,计算新的阻塞位图, 方式为:
- block(new) = block(old) | set;新的block位图和旧的block位图按位或运算。
- 当how为为SIG_ _UNBLOCK时,函数会根据set,计算新的阻塞位图, 方式为:
- block(new) = block(old) & (^ 'set);将传入新的位图先取反,然后和老的位图进行按位与操作
- 当how为为SIG_ SETMASK时,函 数会根据set,计算新的阻塞位图,方式为:
- block (new) = set;
测试代码:
我们调用sigprocmask函数,想要将set位图当中2号信号对应的比特位置为1
为了实现代码我们先要知道set的一些函数
这时候确实是验证了阻塞的设置,但并没有验证阻塞并不影响信号的注册,所以要再次修改下代码
我们现在来验证下阻塞并不影响信号的注册,且可靠信号不会丢信号,非可靠信号会丢信号
思路:
代码:
我们来运行一下,观察结果:
可以看到,可靠信号是不会丢失的,但非可靠信号可以丢失
11.配合信号解决僵尸进程:
在我们之前解决僵尸进程只能调用wait函数或者waitpid函数,但是在调用这两个函数都面临一个问题那就是在调用时,
父进程要不一直处于阻塞等待子进程退出状态,要不一直要配合循环和waitpid的非阻塞状态使用,
我们的父进程就无法做任何的事情。这里我们可以运用信号,对子进程进行回收。
我们来看下这段代码
我们运行一下代码:
我们可以看到,能够收到信号,而且将父进程从wait中解放出来
12.volatile关键字:
- 作用:
- 保证内存可见性
- 每次CPU要计算的数据都是从内存中获取,拒绝编译时优化的方案(从寄存器当中获取)gcc/gt+的编译选项“-00, 01, -02,-03,优化级别时越来越高。(理解优化级别越高,程序可能执行的越快)优化级别越高就从寄存器中取值的可能性越大
我们来写一个代码来实验一下
我们运行发现按下ctrl+c向进程输入2号信号,进程直接结束了,这是因为我们在编译时没有对程序进行优化,这时候是从内存中拿的
我们优化为O3级别
这时候我们再按下Ctrl,可以发现并没有退出,说明并没有从内存中取值,而是从寄存器中直接取值
但当我们给另一个volatile关键字之后,即使怎样优化,也是从内存中拿值的