目录
1.信号的概念:
- 信号是一个软中断
- 软中断:
- 例如:看到绿灯你可以选择走或选择不走。红绿灯只是一个提醒你可以选择走或不走
- 1.1只是告诉有这样一个信号,但是具体这个信号怎么处理,什么时候处理由进程决定,所以是软中断。
2.信号的产生
-
2.1硬件产生(按键盘中的按键):
- ctrl+c:2号信号 SIGINT,按下ctrl+c其实是进程收到了2号信号,2号信号导致进程的退出。
- ctrl+z :20号信号SIGTSTP,
- ctrl+|:3号信号SIGQUIT
-
2.2软件产生:
-
1.kill函数
- 参数:
- pid_t:要给那个进程发,就填那个进程的pid
- sig:要给进程发送的信号
- 使用:我们用getpid获取当前进程pid然后给当前进程发送一个3号信号
- 我们发现一运行直接终止了
-
2.raise函数:
- 作用:给当前进程发送一个信号
- 参数:给自己发送信号的信号值
- 使用:给自己发送一个3号信号,退出信号。
-
- 可以发现一运行直接退出了。
-
- 底层实现原理:
- 调用kill函数实现将kill函数封装。用getpid获取当前进程的进程号传给kill函数,然后自己只需要获取用户传入的信号值,然后传递给kill函数即可。
-
3.kill- [num] [pid] 可以给进程发送信号
- 可以看到进程暂停了
-
-
4.当常见的错误导致程序崩溃时进程收到信号
-
1.解引用空指针
-
2.除0:
- 运行一下我们可以看到、
-
3.double free:多次释放同一个地址
- 我们gdb调试coredump文件可以发现返回6号信号
-
3.信号的种类
-
kill -l 可以罗列信号
- 非实时信号(非可靠信号):
- 特点:信号可能会丢失(1-31)
- 实时信号(可靠信号):
- 特点:信号不会丢失(33-64)
4.信号的处理方式
- 操作系统对信号的处理方式(man 7 signal)
- 默认处理方式:
- SIG_ DFL,操作系统当中已经定义号信号的处理方式了
- 例如2号信号->终止进程
- 11->终止进程,并且产生核心转储文件
- 忽略处理方式:
- SIG_ IGN, 该信号为忽略处理(僵尸进程)
- 进程收到忽略处理的方式的信号后,是不进行处理的,例如当子进程先于父进程退出时,就会给父进程发送SIGCHLD信号,而父进程收到这个信号之后,就会忽略处理,导致父进程没有回收子进程的退出状态信息,从而子进程变成了僵尸进程。
- 自定义处理方式:
- 程序员可以更改信号的处理方式,定义一 个函数,当进程收到该信号的时候, 调用程序猿自己写的函数。(第7个小点涉及到)
5.信号的注册
-
5.1基础概念了解:
- 一个进程收到一个信号,这个过程称之为注册信号的注册和注销并不是一个过程,是两个独立的过程
- 内核中信号注册位图以及s igqueue队列的的了解
- task_ struct结构体内部
- struct S igpending pending;
- siget_ t
- 一个进程收到一个信号,这个过程称之为注册信号的注册和注销并不是一个过程,是两个独立的过程
-
5.2信号的注册:
- 位图更改为1,添加Sigqueue节点到sigqueue队列:
- 信号在注册的时候,会将信号对应的比特位从0修改为1,表示当前进程收到了该信号。
- 还需要哎sigqueue队列中添加一个sigqueue节点,队列在操作系统内核当中本质上是一个双向链表(先进先出的特性)
-
实时信号和非实时信号在注册时的区别:
- 非实时信号(非可靠信号)的注册
- 第一次注册:修改sig位图(0-1),修改sigqueue队列。
- 第二次注册:相同信号值的信号:修改sig位图(1->1),并不会添加sigqueue节点。
- 总结:再次添加,不会添加sigqueue节点
- 实时信号(可靠信号)的注册
- 第一次注册:修改sig位图(0-1),修改sigqueue队列。
- 第二次注册:相同信号值的信号:修改sig位图(1->1),添加sigqueue节点到sigqueue队列中。
- 再次添加,会再次添加siquque节点
6.信号的注销:
-
6.1非可靠信号的注销
- 1.将信号对应的s ig位图当中的比特位置为0(1-0)
- 2.将对应的信号的sigqueue节点进行出队操作
-
6.2可靠信号的注销
- 1.将对应的信号的sigqueue节点进行出队操作
- 2.判断s igqueue队列当中还有相同信号的S igqueue节点吗
- 如果有:则比特位不变
- 如果没有:则比特位改变位0
7.信号的自定义处理方式
-
7.1 概念:
- 自定义处理方式, 就是让程序猿自己定义某一个信号的处理方式,例如原来我们的2号信号是一个终止命令我们可以通过自己定义来让他做其他的事,例如打印一句话。
7.2 函数
- 1.sighandler_t signal(int signum, sighandler_t handler);
- 作用:
- 在调用signal函数的时候,我们给函数的第二个参数传递一个回调函数的地址,当我们收到第一个参数所定义的信号值时,就会调用回调函数,执行回调函数的功能。
- 参数:
- signum:信号值
- handler:更改为哪一 个函数处理,接受一 个函数地址,函数指针(回调函数)。
- typedef void (*sighandler_ t)(int);
- 代码验证:
- 我们写一个代码测试当进程收到2号信号和9号信号的时候,是否会调用回调函数
- 我们运行代码按下ctrl+c向进程发送2号信号,可以发现执行回调函数将收到的2号信号打印出来了。
- 我们向信号传递9号信号
- 可以发现函数并不会执行回调函数,而是直接将进程强杀了,所以可以得知9号信号是无法被用户更改处理方式的。
- 2.int sigemptyset(sigset_t *set);(将信号位图初始化为全0)
- 3.int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
- 参数:
- signum:要更改的信号值
- act:要将信号的处理方式更改为act
- oldact:原来信号的处理方式,要依赖于函数填充。
- struct sigaction
- 使用方式:
- void (*sa_ handler)(int); //保存信号处理方式(默认)的函数指针
- void (*sa_ sigaction)(int, siginfo _t *,void *);// (也是保存原来信号的处理方式的函数指针),但是没有使用。当要使用的时候,配合sa_ flags- 起使用。当sa_ falgs的值为SA_ SIGINF0的时候,信号按照sa_ sigaction保存的函数地址进行处理
- sigset_ t a_ mask;//当进程在处理信号的时候,如果还在收到信号, 则放到该信号位图当中,后续再放到进程的信号位图当中使用时需要用int sigemptyset(sigset_t *set);函数将位图每一位置为0
- int sa_ f lags;
- void (*sa_ restorer)(void); //保留字段
- 代码测试:
- 我们运行代码:
7.3原理:
内核当中的结构体
8.信号的捕捉流程
- 当我们的进程从内核态切换回用户态时,会调用do_signal,检查进程是否收到了信号。
-
8.1信号的处理时机:
- 当从内核态切换会用户态的时候,会调用do_ S igna l函数处理信号
- 有,就处理信号(信号的处理方式(默认,“ 忽略, , 自定义) )
- 没有,就直接返回会用户态
-
8.2处理信号的时候:不同的处理方式
- 默认:
- 忽略:直接在内核就处理结束。
- 自定义处理:
- 调用程序猿自己定义的处理函数进行处理
- 执行用户自定义的处理函数(用户空间)
- 调用sigreturn( )再次回到操作系统内核(内核空间)
- 再次调用会调用do_ signa L函数处理信号
- 调用sys_ sigreturn函数回到用户空间, 继续执行代码
-
8.3常见的进入到内核的方式:
- 调用系统调用函数,
- 内存访问越界,”访问空指针
- 调用库函数
9.信号的阻塞
-
9.1信号阻塞的特性:
- 信号的注册是信号注册, 信号阻塞是信号阻塞。信号的阻塞并不会干扰信号的注册,而是说进程收到这个信号之后,由于阻塞, 暂时不处理该信号 。
-
9.2内核代码:
- struct task_ struct{
- sigset_ t blocked; (位图)它和前面的sig位图一样都是当信号阻塞时将相关的bit位置为1。
-
9.3加上信号阻塞之后,理解信号的处理;
- 进入内核,返回之前, 会调用do_ signa 1函数处理信号
- 有信号要处理,则先判断该信号是否阻塞,如果没阻塞, 在处理信号。 如果阻塞,则不处理 。当不阻塞再处理。
-
9.4接口:
- 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;
- 测试代码:
- 运行结果我们发现给当前进程无论发送什么信号都没有作用。
- 我们给进程发送2号信号发现没有反应
- 发送9号信号
- 我们接下来验证实时信号和非实时信号这里
- 我们运行之后看结果
10.配合信号解决僵尸进程:
- 在我们之前解决僵尸进程只能调用wait函数或者waitpid函数,但是在调用这两个函数都面临一个问题那就是在调用时,父进程要不一直处于阻塞等待子进程退出状态,要不一直要配合循环和waitpid的非阻塞状态使用,我们的父进程就无法做任何的事情。这里我们可以运用信号,对子进程进行回收。
- 我们看运行结果
11.volatile关键字:
- 作用:
- 保证内存可见性
- 每次CPU要计算的数据都是从内存中获取,拒绝编译时优化的方案(从寄存器当中获取)gcc/gt+的编译选项“-00, 01, -02,-03,优化级别时越来越高。(理解优化级别越高,程序可能执行的越快)优化级别越高就从寄存器中取值的可能性越大
- 我们写一个代码,验证一下
- 我们运行发现按下ctrl+c向进程输入2号信号,进程直接结束了,这是因为我们在编译时没有对程序进行优化
- 我们优化为O1级别
- 运行发现仍然可以退出也就是从内存中读取数据
- 但是当我们优化到一定级别CPU在计算时就为了提高运行效率不从内存中取数据了,就直接从寄存器中取数据,导致我们更改的g_val的数值在程序运行时显示不改变。这样就会导致一定错误产生。
- 我们在变量前边加上volatile关键字就会保证内存可见性,CPU在处理数据时每次从内存中读取。
看到这里如果觉得有用不如点个赞再走吧!!!