进程信号详解
1.引入信号的概念
信号是软件中断。它给我们提供了一种能够异步处理事件的方法。事实上,进程并不知道信号何时到来。
比如,当我们的某一个进程失去控制,而我们想让他终止运行时,通常使用Ctrl+c的方式使进程强制终止,Ctrl+c虽然由硬件产生,但是它在操作系统内核中会被视作SIGINT信号(signal interrupt 中断信号)。
信号也与版本有关,不同的操作系统所支持的信号种类和数量有差异(如下图),本篇博客仅讲解linux操作系统中的信号。
2.信号的生命周期
linux下进程一般经过4个步骤:
信号的产生–>信号的注册–>信号的注销–>信号的处理,下面我们分步讲解信号的各个阶段。
3.信号的产生
在linux操作系统中,有很多条件能够产生信号:
-
通过硬件中断产生信号:
比如在中断使用Ctrl+c,Delete等按键可以产生硬件中断。
-
程序执行出现异常会产生信号
在程序中,某些错误会让操作系统内核该程序对应的进程产生相应的信号。 比如,当程序中有对无效内存做引用时,操作系统内核会产生并发送SIGSEGV信号给该进程
-
使用者用kill(1)在命令行,向某个进程发送指定的信号,(或者在某个进程调用kill(2)函数接口亦可)
在另一个终端给某个进程发送 kill -n pid 即可给pid所对应的进程发送n号信号。 在某个进程中使用int kill(pid_t pid,int sig)向pid进程发送sig号信号。
-
当进程满足某些软件条件时,也会产生信号
比如当子进程退出后,会给父进程发送SIGCHLD信号,提示父进程接收他的退出信息,方便释放子进程资源。
4.信号的注册
4.0递达/未决/未决信号集/未决信号链
在讲解信号注册之前,要先理解两个概念:
递达:执行信号所代表的系统调用函数叫做递达。
未决:从信号产生到递达之前的之一段时间叫信号未决。
下面我们来讲讲信的注册。
首先,要明白信号是如何存储的。
在进程PCB中有3张位图block,pending,handler,他们分别标志这阻塞信号,未决信号,以及当前信号的处理方式。
struct sigpending pending://未决信号的数据成员
struct sigpending{
struct sigqueue *head, **tail;//信号集的头尾指针
sigset_t signal; //未决信号集,每一位都标志着一个信号。
};
struct sigqueue{ //未决信号链
struct sigqueue *next;
siginfo_t info;
}
信号在进程中注册指的就是:
信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,
并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。
只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。
4.1可靠信号与不可靠信号
linux下进程信号有62种,分为两类,其中1-31号为不可靠信号,34-64为可靠信号。
不可靠信号又称非实时信号,当一个非实时信号要插入sigqueue未决信号链之前,操作系统会先查看pending位图该信号的位置是否已经被置为1,如果是,则直接丢弃该信号。
可靠信号又称实时信号,实时信号不管pending位图中该信号位置是0还是1,都会插入sigqueue中。
所以,我们可以这样想,sigqueue中最多只能有一个不可靠信号的节点信息,但可能有多个可靠信号的信息。
4.2 信号集及操作函数
信号集被定义为一种数据类型:
typedef struct
{
unsigned long sig[_NSIG_WORDS];
} sigset_t;
信号集用来描述信号的集合,每个信号占用一位。
Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。
下面是为信号集操作定义的相关函数:
#include <signal.h>
int sigemptyset(sigset_t *set)
//初始化由set指定的信号集,信号集里面的所有信号被清空;(每创建一个信号集一定要记得调用)
int sigfillset(sigset_t *set)
//调用该函数后,set指向的信号集中将包含linux支持的64种信号,即填充信号集;
int sigaddset(sigset_t *set, int signum);
//在set指向的信号集中加入signum信号;
int sigdelset(sigset_t *set, int signum)
//在set指向的信号集中删除signum信号;
int sigismember(const sigset_t *set, int signum)
//判定信号signum是否在set指向的信号集中。
int sigpending(sigset_t *set);
// 将当前pending集合(信号注册集合)中的信号取出来放到set中
对阻塞信号集操作主要用到的是sigprocmask函数
int sigprocmask(int how,sigset_t *set,sigset_t oldset);
how:
SIG_BLOCK 在进程当前阻塞信号集中添加set指向信号集中的信号
SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞
SIG_SETMASK 更新进程阻塞信号集为set指向的信号集
set:与阻塞信号集进行交互的信号集
oldset:保存当前阻塞信号集,方便回退。
说了这么多,我们来实际操作一下:
1 #include<iostream>
2 #include<unistd.h>
3 #include<signal.h>
4 using namespace std;
5
6 void sig_print(sigset_t *set)
7 {
8 int i=0;
9 for (;i<32;i++)//打印set
10 {
11 if (sigismember(set,i))
12 {
13 cout<<"1";
14 }
15 else
16 {
17 cout<<"0";
18 }
19
20 }
21 cout<<endl;
22
23 }
24
25 int main()
26 {
27 sigset_t s,p;//创建信号集s,p
28
29 sigemptyset(&s);//将信号集的内容置空
30 sigaddset(&s,SIGINT);//将SIGINT信号添加进信号集s中
31 sigprocmask(SIG_BLOCK,&s,NULL);
//将信号集s中的信号添加进当前进程的block位图。这里也就是将SIGINT阻塞了。
32
33
34 while(1)
35 {
36 sigpending(&p);//取出当前进程的pending位图放进p中
37 sig_print(&p);
38 sleep(1);
39 }
40
41
42
43 return 0;
44 }
这段程序就是将SIGINT信号阻塞掉,然后持续输出pending位图,那么我们来运行一下试试:
我们发现,就算使用Ctrl+c也无法结束这个进程,原因就是Ctrl+c这个硬件中断在操作系统看来就是一个SIGINT信号,发送给该进程后,由于block中阻塞了该信号,所以进程迟迟不能结束。
但是我们还是能够用Ctrl+\结束掉进程的,Ctrl+\给进程发送的是SIGQUIT信号。
5.信号的注销
如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。
对于不可靠信号(非实时信号)来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);
而对于可靠信号(实时信号)来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则执行完相应的处理函数后应该把信号在进程的未决信号集中删除(信号注销完毕)。否则待该信号的所有sigqueue处理完毕后再在进程的未决信号集中删除该信号。
6.信号的处理
信号的处理分成3种:
-
默认处理方式(1.生成core文件然后结束进程 2.继续执行 3.直接结束进程 4.暂停此进程 5.想父进程发送我已经执行结束,等待回收资源.)
-
忽略处理方式(如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。)
-
自定义处理方式,让信号按照你自定义的函数执行。
我们主要讨论的是第三种,自定义的处理方式。
6.1信号的捕捉
在进程处理信号之前,进程要先捕捉到这个信号,那么,进程是怎么捕捉到信号的呢?是信号刚注册完毕就立即捕捉并处理吗?如果信号阻塞又怎样呢?这些都是我们要考虑的问题。
下面我们用一个例子来讲讲信号从产生到处理的全过程。
举例:
-
用户注册了SIGQUIT信号的处理函数sighandler.。
-
SIGINT注册进进程的pending位图中,sighandler注册进handler位图中。
-
当前正在执行main函数,这时发生中断或异常切换到内核态.
-
在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达.
-
进程识别到SIGINT信号的处理方式是用户自定义函数且信号无阻塞。
-
内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数. sighandler和main函数使用不同的堆栈空间,他们之间不存在调用和被调用的关系,是两个独立的控制流程.
-
sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态
-
如果没有新的信号抵达,这次再返回用户态就是恢复main函数的上下文继续执行了
6.2 信号处理函数
在这里插入代码片
6.3 cure-dump
后续补充