Linux进程信号篇
Posted Suk-god
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程信号篇相关的知识,希望对你有一定的参考价值。
文章目录
信号的概念与种类
- 概念
信号是一个软中断,这里有一个形象的例子来说明一下它的含义:
所以说:信号只是告诉我们有这样一个信号,但是具体这个信号如何处理,什么时候处理是由进程决定的,所以是软中断 - 种类
我们可以通过kill -l
命令来查看所有的信号
注意:总共有62个信号!没有32和33号信号
其中
1–31号信号被称为非实时信号,也叫非可靠信号,它在使用的过程中信号可能会丢失
34–64号信号被称为实时信号,也叫可靠信号,它在使用的过程中信号不会丢失
信号的产生
硬件产生方式
-
Ctrl + c
产生的是2号信号SIGINT
是一个中断信号
-
Ctrl + z
产生的是20号信号SIGTSTP
是一个暂停信号
-
Ctrl + \\
产生的是3号信号SIGQUIT
是一个退出信号
-
kill命令向进程发送信号
通过kill -[信号值] [pid]
向进程发送信号
软件产生方式
- kill函数
#include <signal.h>
int kill (pid_t pid,int sig)
- raise函数
int raise(int sig);
其实raise函数内部调用的依旧是kill函数
int raise(int sig)
return kill(get(pid),sig);
代码测试
结果:与预期一致
扩展–根据信号值判断代码出错原因
回想之前在学习gdb调试的时候,有一种情况就是对崩溃后产生的coredump文件进行调试,进而确定程序崩溃的原因
现在我们可以通过对崩溃程序产生的coredump文件进行调试,通过信号值判断程序崩溃原因。
产生coredump文件的方式以及限制因素:
- 解引用空指针、野指针(垂悬指针)
- 除0
- 越界访问
这样都不崩溃?
原因:
操作系统容忍进程访问不属于自己的内存,但是有个前提条件,越界访问的内存,没有分配给其他进程所使用
既然这样,我们来个更过分的:
终于验证到了,现在调试一下产生的coredump文件
- double free
gdb调试产生的coredump文件
信号的处理方式
-
通过
man 7 signal
查看OS对信号的处理方式
-
默认处理方式
SIG_DFL
在操作系统当中已经定义好信号的处理方式 -
忽略处理方式
SIG_ING 忽略处理
联想到僵尸进程的产生原因:
现在给出详细解释:
子进程先于父进程退出, 子进程在退出的时候会给父进程发送SIGCHLD
信号,而父进程接收到这个信号后,是忽略处理的,从而导致了父进程没有回收子进程的退出状态信息,因此子进程就变成了僵尸进程! -
自定义处理方式
程序员可以更改信号的处理方式,定义一个函数,当进程收到该信号的时候,调用程序员自己写的函数。
信号的注册
-
概念
一个进程收到一个信号,这个过程称之为注册
信号的注册和信号的注销是两个独立的过程 -
内核中信号注册位图以及sigqueue队列的理解
2.1task_struct
结构体内部有一个结构体变量struct sigpending pending
,struct sigpending结构体有两个成员变量,一个是struct list_head list
(双向链表),另一个是sigset_t signal
(数组)
2.2 我们具体研究这个 sigset_t signal 数组:
sigset_t
本质上是一个结构体,它的内部成员变量是一个数组:unsigned long sig[_NSIG_WORDS]
对于这个数组,操作系统并没有将它当做数组来使用,而是把它看做是位图
2.3 在Linux的64位平台下,long 以及 unsigned long占8个字节,也就是64个bit位,而目前信号的数量只有62个,所以每个bit位表示一个信号是足够的
上面说了这么多,理解起来可能会有一点绕,下面我就通过图示的方式解释一下:
在理清楚他们之间的关系后,现在开始整整意义上的理解这个数组是如何被当做位图来使用的。
可能有人又有疑问了:
既然数组的一个元素就可以搞定,为何又大费周章,给一个数组呢?
原因是为了后续可能会扩展的信号提供空间
- 注册
位图更改为1,添加sigqueue节点到sigqueue队列
信号在注册的时候,会将信号对应的bit位由0改为1,表示当前进程收到了该信号
在sigqueue队列中添加一个sigqueue节点。队列在OS内核当中本质上就是一个双向链表(具有先进先出的特性)
信号的注销
主要分为可靠信号和费可靠信号
信号的自定义处理方式
含义:让程序员自己定义某一个信号的处理方式,当进程收到该信号后就会执行程序员自定义的处理方式
- signal函数+测试代码
sighandler_t signal(int signum,sighandler_t handler);
9号信号(强杀)是不能被程序员自定义处理的函数
测试代码:
2. sigaction函数+测试代码
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact)
后两个参数都是一个结构体指针,这里展开说明,该结构体的定义如下图:
下面对这五个成员变量进行说明:
测试代码:
情景一:自定义信号处理方式执行后手动结束进程
情景二:进程收到2号信号后先去执行自定义处理方式的函数,然后在该函数内部,再将2号信号的处理方式恢复为原来的方式
- 结合内核代码进行理解
信号的阻塞
- 理解要点
信号注册是注册,信号阻塞是阻塞**。信号的阻塞并不会影响信号的注册**
进程收到这个信号之后,由于阻塞,暂时不处理该信号。 - 内核代码理解
struct task_struct
........
sigset_t blocked;
.........
sigset_t blocked是一个位图,当要阻塞一个信号的时候,将该信号对应的比特位设置为1即可
- 接口
int sigprocmask(int how,const sigset_t* set,sigset_t* oldset)
- 代码验证
验证方式:阻塞“全部”信号,进程不退出,查看进行收到每一个信号(通过test函数实现)时的状态。
通过kill -9 将进程杀死
也可以通过kill - 19来停止这个进程
结论:9号和19号信号不能被阻塞
信号的捕捉流程
- 信号的处理时机
从内核态切换到用户态的时候,会调用do_signal函数
处理信号,该函数会判断是否有信号并做出相应的操作:
- 处理信号的时候,不同的处理方式
- 画图理解这个过程
- 常见的进入内核的方式
调用系统调用函数
内存访问越界,访问空指针
调用库函数
扩展
- 父子进程 + 进程等待 + 自定义信号处理方式
我们前面学习的进程等待,父进程在等待子进程退出,回收它的退出状态信息的时候,有两种方式,分别是:
wait接口,阻塞等待
waitpid接口,非阻塞等待,搭配循环使用
这两种方式父进程在等待子进程期间都是无法执行其他活动的
由于父进程无法执行其他代码,导致父进程的效率低下,我们可以使用信号的方式来解决这个问题.具体思路如下:
我们都知道,父进程回收子进程退出信息接收到的是SIGCHLD信号,因此我们可以将该信号的处理方式自定义一下,然后在接收到该信号的时候,再转去执行自定义处理方式里面的wait函数来回收子进程的退出状态信息。
通过上面的方式,我们就会“解放”父进程,让他在等待子进程退出的同时能够去执行其他代码!
- volatile关键字
结合自定义信号处理函数进行代码验证:
情景一:不使用volatile关键字。构造一个循环场景,通过信号执行自定义的信号处理函数改变相应的循环判断条件,观察现象
现在来一个小改动
其余不改变,我们再次观察现象:
情景二:使用volatile关键字。构造一个循环场景,通过信号执行自定义的信号处理函数改变相应的循环判断条件,观察现象
正常退出,原因是加了volatile关键字,因此每次都会从内存读取数据,说以值改变编译器每次都会读取到新的数据。
以上就是对进程信号相关内容的梳理!各位看官,感觉有所帮助,还请一键三连~~
以上是关于Linux进程信号篇的主要内容,如果未能解决你的问题,请参考以下文章