进程信号
信号是什么?
信号就是一个软件中断:通知进程发生了某个事件,打断进程当前的操作,去处理这个事件。
信号是多种多样的,并且一个信号对应一个事件,这样才能做到收到一个信号后,知道到底是一个是什么事件,应该如何处理。(但是要保证必须识别这个信号)
信号种类:
一共62种(使用kill -l命令进行查看)
- 1~31是非可靠信号 (Mac终端只能看到这31个)
- 34~64是可靠信号
信号的生命周期
产生->进程中的注册->进程中的注销->信号的捕捉处理
信号的产生
硬件:
- ctrl+c 强制中断程序的执行(进程已经终止)
- ctrl+l 发送一个exit信号(从管理员root退回到你的普通用户)
- ctrl+z 任务中止(暂停的意思)
软件:
kill -signum
pid命令:kill默认发送15号信号 kill -signum pid 向指定的进程发出指定信号
kil杀死一个进程的原理:
向进程发送一个信号,信号有对应的时间,进程放下手头工作去处理这个事件,然而事件的处理结果就是让进程退出
通过终端按键产生信号
- SIGINT的默认处理动作是终止进程
- SIGQUIT的默认处理动作是终止进程并且Core Dump
core dumped:核心转储--程序异常退出时,保存程序的运行信息,便于事后调试—默认关闭
ulimit -c 设置核心转储文件的最大大小 ulimit -c1024[kb] 则最大大小为1M
gdb ./main -> core-file core.pid -> 事后的命令调试
调用系统函数向进程发信号
首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号(Mac发送SEGV)。
- ./signal 后面为什么要加 &?
& 的作用是将进程切换到后台,我们把它称为job。切换到后台时会输出相关job信息,以前面的输出为[1]27753例:[1]表示jobID是1,27753表示进程ID是27753。切换到后台的进程,仍然可以用ps命令查看。
- 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGSEGV 4568 或 kill -11 4568 , 11是信号SIGSEGV的编号。以往遇到的段错误都是由非法内存访问产生的,而这个程序本身没错, 给它发SIGSEGV也能产生段错误。
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。
abort函数使当前进程接收到信号而异常终止。
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
由软件条件产生信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
例如:
- 当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。
- 当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
信号在程序中的注册
如何让进程知道自己收到了某个信号?
pcb->struct sigpending-> struct sigset_t
- sigset_t 这个结构体中只有一个数组成员,这个数组实现了一个位图 — 称之为未决信号集合(收到了但是没有处理的信号集合)
- 给一个进程发送一个信号,就会将这个位图对应位置1,表示进程当前收到了这个信号
在pcd中有一个未决的信号集合,pending集合,信号的注册就是指在这个pending集合中标记对应信号数值的二进制位为1
- 位图只有0/1,也就是表示是否收到了这个信号,但是无法表示收到多少个这样的信号
- 信号的注册不仅会修改位图,还会为信号组织一个sigqueue节点添加到pcb的sigqueue链表中
是否可靠信号注册的区别:
-
1~31非可靠信号的注册:
-
若信号还未注册(位图为0),则会创建一个sigqueue节点并修改位图为1
-
若已经注册(位图为1),则什么都不做
-
-
34~64可靠信号的注册:
- 每次注册信号,不管是否已经注册,每次都会添加一个sigqueue节点(信号信息),添加到链表中,并修改位图
信号的注销
为了保证一个信号只会被处理一次,因此是先注销再处理;在pcb中删除当前信号信息
将pending位图置0,删除信号节点
- 若信号是非可靠信号(只有一个节点),因此删除节点后,直接将pending位图置0(非可靠信号在没有处理之前只会注册一次)
- 若信号是可靠信号(可能注册多次,有多个节点),则删除后,需要判断是否还有相同节点,没有的话才会重置位图为0;
信号的捕捉处理
信号的处理:
其实每一个信号都对应有自己的事件处理函数,信号到来,去处理这个事件就是去执行这个处理函数;执行完毕事件就处理完了
信号的捕捉处理:
信号的递达,当信号到来的时候去回调信号的处理函数
- 默认处理:操作系统中原定义好的每个信号的处理方式
- 忽略处理:处理方式就是忽略,什么都不做
- 自定义处理:自己定义一个事件函数,使用这个函数替换内核中默认的处理函数(信号到来就会调用我们定义的函数)
自定义信号的捕捉流程:
信号的处理是在程序运行从内核态切换回用户态之前,默认/忽略直接在内核中完成处理,而用户自定义信号处理方式,则需要返回用户态执行回调函数,完成后返回内核态,最终没有信号处理了,再返回程序主控程序
- 当程序在用户态主控流程运行的时候,因为系统调用/中断/异常(切换到内核态运行)
- 完成功能后,在返回用户态主控流程之前,调用do_signal函数去处理未决信号
- 若信号的处理方式是忽略/默认,则直接在内核态完成
- 若信号的处理方式是用户自定义,会切换到用户态执行用户自定义的处理函数,执行完毕后
- 调用 sigreturn 返回内核态运行,当没有信号待处理的时候,则返回用户态主控流程
信号捕捉函数signal
sighandler_t signal(int signum, sighandler_t handler) - 修改信号的回调函数
参数:
- typedef void (*sighandler_t)(int signum); 定义了一个名称叫sighandler_t的函数指针类型
- handler:SIG_DFL(默认处理方式),SIG_IGN(忽略处理方式)/ 用户自己定义的一个没有返回值,有一个int型参数的函数地址
sigaction函数相比signal函数更为复杂,但更具灵活性,下面具体介绍她的结构和用法:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
- signum:要操作的信号。
- act:要设置的对信号的新处理方式。
- oldact:原来对信号的处理方式。 如果不需要设置
返回值:0 表示成功,-1 表示有错误发生。
如果本文有帮助到您,留个赞呐~~