【Linux】进程信号篇

发布时间:2023-04-26 08:30

文章目录

  • 信号的概念与种类
  • 信号的产生
    • 硬件产生方式
    • 软件产生方式
    • 扩展--根据信号值判断代码出错原因
  • 信号的处理方式
  • 信号的注册
  • 信号的注销
  • 信号的自定义处理方式
  • 信号的阻塞
  • 信号的捕捉流程
  • 扩展

信号的概念与种类

  1. 概念
    信号是一个软中断,这里有一个形象的例子来说明一下它的含义:
    \"【Linux】进程信号篇_第1张图片\"
    所以说:信号只是告诉我们有这样一个信号,但是具体这个信号如何处理,什么时候处理是由进程决定的,所以是软中断
  2. 种类
    我们可以通过kill -l 命令来查看所有的信号
    \"【Linux】进程信号篇_第2张图片\"
    注意:总共有62个信号!没有32和33号信号
    其中
    1–31号信号被称为非实时信号也叫非可靠信号,它在使用的过程中信号可能会丢失
    34–64号信号被称为实时信号也叫可靠信号,它在使用的过程中信号不会丢失

信号的产生

硬件产生方式

  1. Ctrl + c
    产生的是2号信号SIGINT
    是一个中断信号
    \"在这里插入图片描述\"

  2. Ctrl + z
    产生的是20号信号SIGTSTP
    是一个暂停信号
    \"在这里插入图片描述\"

  3. Ctrl + \\
    产生的是3号信号SIGQUIT
    是一个退出信号
    \"【Linux】进程信号篇_第3张图片\"

  4. kill命令向进程发送信号
    通过 kill -[信号值] [pid]向进程发送信号
    \"【Linux】进程信号篇_第4张图片\"

软件产生方式

  1. kill函数
 #include 
 int kill (pid_t pid,int sig)

\"【Linux】进程信号篇_第5张图片\"

  1. raise函数
 int raise(int sig);

\"【Linux】进程信号篇_第6张图片\"
其实raise函数内部调用的依旧是kill函数

int raise(int sig)
{
	return kill(get(pid),sig);
}

代码测试
\"【Linux】进程信号篇_第7张图片\"
结果:与预期一致
\"【Linux】进程信号篇_第8张图片\"

扩展–根据信号值判断代码出错原因

回想之前在学习gdb调试的时候,有一种情况就是对崩溃后产生的coredump文件进行调试,进而确定程序崩溃的原因
现在我们可以通过对崩溃程序产生的coredump文件进行调试,通过信号值判断程序崩溃原因。
产生coredump文件的方式以及限制因素:
\"【Linux】进程信号篇_第9张图片\"

  1. 解引用空指针、野指针(垂悬指针)
    \"【Linux】进程信号篇_第10张图片\"
    \"【Linux】进程信号篇_第11张图片\"
  2. 除0
    \"【Linux】进程信号篇_第12张图片\"
    \"【Linux】进程信号篇_第13张图片\"
  3. 越界访问
    \"【Linux】进程信号篇_第14张图片\"
    \"【Linux】进程信号篇_第15张图片\"
    这样都不崩溃?
    原因:
    操作系统容忍进程访问不属于自己的内存,但是有个前提条件,越界访问的内存,没有分配给其他进程所使用
    既然这样,我们来个更过分的:
    \"【Linux】进程信号篇_第16张图片\"
    终于验证到了,现在调试一下产生的coredump文件
    \"在这里插入图片描述\"
    \"【Linux】进程信号篇_第17张图片\"
  4. double free
    \"【Linux】进程信号篇_第18张图片\"
    \"在这里插入图片描述\"
    gdb调试产生的coredump文件
    \"【Linux】进程信号篇_第19张图片\"

信号的处理方式

  1. 通过 man 7 signal 查看OS对信号的处理方式
    \"【Linux】进程信号篇_第20张图片\"

  2. 默认处理方式
    SIG_DFL
    在操作系统当中已经定义好信号的处理方式

  3. 忽略处理方式
    SIG_ING 忽略处理
    联想到僵尸进程的产生原因:
    现在给出详细解释:

    子进程先于父进程退出, 子进程在退出的时候会给父进程发送SIGCHLD信号,而父进程接收到这个信号后,是忽略处理的,从而导致了父进程没有回收子进程的退出状态信息,因此子进程就变成了僵尸进程!

  4. 自定义处理方式
    程序员可以更改信号的处理方式,定义一个函数,当进程收到该信号的时候,调用程序员自己写的函数。

信号的注册

  1. 概念
    一个进程收到一个信号,这个过程称之为注册
    信号的注册和信号的注销是两个独立的过程

  2. 内核中信号注册位图以及sigqueue队列的理解
    2.1 task_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位表示一个信号是足够的

上面说了这么多,理解起来可能会有一点绕,下面我就通过图示的方式解释一下:
\"【Linux】进程信号篇_第21张图片\"
在理清楚他们之间的关系后,现在开始整整意义上的理解这个数组是如何被当做位图来使用的。
\"【Linux】进程信号篇_第22张图片\"
可能有人又有疑问了:
既然数组的一个元素就可以搞定,为何又大费周章,给一个数组呢?
原因是为了后续可能会扩展的信号提供空间

  1. 注册
    位图更改为1,添加sigqueue节点到sigqueue队列
    信号在注册的时候,会将信号对应的bit位由0改为1,表示当前进程收到了该信号
    在sigqueue队列中添加一个sigqueue节点。队列在OS内核当中本质上就是一个双向链表(具有先进先出的特性)
    \"【Linux】进程信号篇_第23张图片\"

信号的注销

主要分为可靠信号和费可靠信号
\"【Linux】进程信号篇_第24张图片\"

信号的自定义处理方式

含义:让程序员自己定义某一个信号的处理方式,当进程收到该信号后就会执行程序员自定义的处理方式

  1. signal函数+测试代码
sighandler_t signal(int signum,sighandler_t handler);

\"【Linux】进程信号篇_第25张图片\"
9号信号(强杀)是不能被程序员自定义处理的函数
测试代码:
\"【Linux】进程信号篇_第26张图片\"
\"【Linux】进程信号篇_第27张图片\"
2. sigaction函数+测试代码

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact)

\"【Linux】进程信号篇_第28张图片\"
后两个参数都是一个结构体指针,这里展开说明,该结构体的定义如下图:
\"【Linux】进程信号篇_第29张图片\"
下面对这五个成员变量进行说明:
\"【Linux】进程信号篇_第30张图片\"
测试代码:
情景一:自定义信号处理方式执行后手动结束进程
\"【Linux】进程信号篇_第31张图片\"
\"【Linux】进程信号篇_第32张图片\"
情景二:进程收到2号信号后先去执行自定义处理方式的函数,然后在该函数内部,再将2号信号的处理方式恢复为原来的方式
\"【Linux】进程信号篇_第33张图片\"
\"【Linux】进程信号篇_第34张图片\"

  • 结合内核代码进行理解
    \"【Linux】进程信号篇_第35张图片\"

信号的阻塞

  • 理解要点
    信号注册是注册,信号阻塞是阻塞**。信号的阻塞并不会影响信号的注册**
    进程收到这个信号之后,由于阻塞,暂时不处理该信号。
  • 内核代码理解
struct task_struct
{
    ........
    sigset_t blocked;
    .........
}

sigset_t blocked是一个位图,当要阻塞一个信号的时候,将该信号对应的比特位设置为1即可

  • 接口
int sigprocmask(int how,const sigset_t* set,sigset_t* oldset)

\"【Linux】进程信号篇_第36张图片\"

  • 代码验证
    验证方式:阻塞“全部”信号,进程不退出,查看进行收到每一个信号(通过test函数实现)时的状态。
    \"【Linux】进程信号篇_第37张图片\"
    \"【Linux】进程信号篇_第38张图片\"
    \"【Linux】进程信号篇_第39张图片\"
    通过kill -9 将进程杀死
    \"【Linux】进程信号篇_第40张图片\"
    也可以通过kill - 19来停止这个进程
    \"【Linux】进程信号篇_第41张图片\"
    \"在这里插入图片描述\"

结论:9号和19号信号不能被阻塞

信号的捕捉流程

  1. 信号的处理时机
    从内核态切换到用户态的时候,会调用do_signal函数处理信号,该函数会判断是否有信号并做出相应的操作:
    \"【Linux】进程信号篇_第42张图片\"
  2. 处理信号的时候,不同的处理方式
    \"【Linux】进程信号篇_第43张图片\"
  3. 画图理解这个过程
    \"【Linux】进程信号篇_第44张图片\"
  4. 常见的进入内核的方式
    调用系统调用函数
    内存访问越界,访问空指针
    调用库函数

扩展

  1. 父子进程 + 进程等待 + 自定义信号处理方式
    我们前面学习的进程等待,父进程在等待子进程退出,回收它的退出状态信息的时候,有两种方式,分别是:
    wait接口,阻塞等待
    waitpid接口,非阻塞等待,搭配循环使用
    这两种方式父进程在等待子进程期间都是无法执行其他活动的
    由于父进程无法执行其他代码,导致父进程的效率低下,我们可以使用信号的方式来解决这个问题.具体思路如下:

我们都知道,父进程回收子进程退出信息接收到的是SIGCHLD信号,因此我们可以将该信号的处理方式自定义一下,然后在接收到该信号的时候,再转去执行自定义处理方式里面的wait函数来回收子进程的退出状态信息
通过上面的方式,我们就会“解放”父进程,让他在等待子进程退出的同时能够去执行其他代码!
\"【Linux】进程信号篇_第45张图片\"
\"【Linux】进程信号篇_第46张图片\"

  1. volatile关键字
    \"【Linux】进程信号篇_第47张图片\"
    结合自定义信号处理函数进行代码验证:
    情景一:不使用volatile关键字。构造一个循环场景,通过信号执行自定义的信号处理函数改变相应的循环判断条件,观察现象
    \"【Linux】进程信号篇_第48张图片\"
    \"【Linux】进程信号篇_第49张图片\"

\"【Linux】进程信号篇_第50张图片\"
现在来一个小改动
\"【Linux】进程信号篇_第51张图片\"
其余不改变,我们再次观察现象:
\"【Linux】进程信号篇_第52张图片\"

情景二:使用volatile关键字。构造一个循环场景,通过信号执行自定义的信号处理函数改变相应的循环判断条件,观察现象
\"【Linux】进程信号篇_第53张图片\"
\"【Linux】进程信号篇_第54张图片\"
正常退出,原因是加了volatile关键字,因此每次都会从内存读取数据,说以值改变编译器每次都会读取到新的数据。

以上就是对进程信号相关内容的梳理!各位看官,感觉有所帮助,还请一键三连~~
\"【Linux】进程信号篇_第55张图片\"

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号