目录
总结
一,信号保存
1.1 阻塞信号
2.2 信号在内核(操作系统)中的表示
2.3 系统接口
2.3.1 sigset_t信号集
2.3.2 信号集的操作函数
2.3.3 sigprocmask
2.3.4 sigpending
2.4 实验样例
三,信号处理
3.1 信号捕捉
3.2 sigaction接口
3.3 实验样例
总结
总结
上一篇我们主要讲了进程信号的信号发出,本文主要讲解后两个部分,信号保存和信号处理
一,信号保存
1.1 阻塞信号
在讲解信号是如何保存之前,我们需要先认识一下什么是阻塞信号
首先,在我们进程信号中,实行执行信号的处理动作称为递达 ;而信号从产生到递达的过程叫做未决。而在上面两个过程中,我们可以选择阻塞某个信号,被阻塞的信号会一直处于未决的状态无法递达,直到信号阻塞被解决,信号才会被执行。注意,这里的阻塞和忽略是两码事,阻塞行为是在未决和递达直接,而忽略是递达的一种处理方式。
2.2 信号在内核(操作系统)中的表示
上一遍我们简单讲了一下信号是在内核创建的task_struct中以位图的数据结构表示的,本文就详细讲解一下信号在task_struct中的储存结构
信号在task_struct中的存储结构如上图所示,在结构体中存储的有三个变量block,pending,handler用来表示信号,接下来我们就详细解释一下这三个的变量的含义。
- block:32位整形,位图数据结构,其中每个比特位的含义对应的是上面2图中前31个常见的信号,每比特位的内容的含义是有没有阻塞对应信号,1表示阻塞,0表示未阻塞
- pending:32位整形,位图数据结构,其中每个比特位的含义对应的是上面2图中前31个常见的信号,每比特位的内容的含义是有没有接收到对应信号,1表示存在,0表示不存在
- handler:一个函数指针数组,其中每个下标表示的是图二对应的前31个信号,每个下标的内容则是对应信号的处理方式,SIG_DFL为默认处理方式,SIG_IGN为忽略,函数指针则是自定义处理方式。
当一个信号发送过来时,在储存结构中的流程如下,先在pending中将信号对应比特位置为1,然后在block中检查信号是否被阻塞,如果被阻塞则等待阻塞状态消失再进行处理,否则在合适的时间进行处理,处理时在根据handler中信号对应处理方式进行处理
上面的task_struct中的信号储存结构,我们需要注意该结构是由内核直接进行管理,而我们用户无法直接插手管理,只能通过内核提供的系统接口控制信号的储存,接下来我们就讲一下内核提供的控制信号储存的系统接口
2.3 系统接口
我们将系统接口主要是先将每个接口的用法含义讲一下,最后在写一个程序用一下这些接口
2.3.1 sigset_t信号集
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
2.3.2 信号集的操作函数
在Linux下我们可以通过下面的指令查找信号集的操作函数
man sigemptyset
- sigemptyset:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
- sigfillset:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
- sigaddset和sigdelset:指的是增加或者删除set中的某种有效信号
- sigismember:检测set中是否由某种信号
注意:在使用sigset_t前必须要用sigemptyset或者sigfillset初始化,使信号集处于确定的初始状态。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1
2.3.3 sigprocmask
现在我们讲一下如何读取改进进程的信号屏蔽字(阻塞信号集)
如图想要修改进程的阻塞信号集,需要用系统接口sigprocmask,其中how为修改方式,如果set为非空信号集,则将阻塞信号集set为基地修改阻塞信号集,然后为了保证安全性,会将修改前的阻塞信号集备份放到oldset中传回。how的修改方式有如下几种:
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
2.3.4 sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
2.4 实验样例
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void pendingprint(sigset_t* pendingset)
{
int i=1;
for(i=1;i<=31;i++)
{
if(sigismember(pendingset,i)==1)
{
cout<<1;
}
else
{
cout<<0;
}
}
}
int main()
{
sigset_t set,oset;
//1.1 初始化
sigemptyset(&set);
sigemptyset(&oset);
//1.2 向屏蔽信号集群加入SIGINT(ctrl+c)
sigaddset(&set,SIGINT);
//1.3 改进进程的屏蔽信号集
sigprocmask(SIG_BLOCK,&set,&oset);
while(true)
{
//2.1获取进程的pending信号集
sigset_t pendingset;
sigpending(&pendingset);
//2.2打印pendingset信号集
pendingprint(&pendingset);
cout<<endl;
sleep(1);
}
return 0;
}
代码如上所示,主要分两个板块第一个就是对信号集完成初始化并且添加SIGINT信号,然后修改进程的屏蔽信号集;第二步是打印观察进程的pending信号集,主要观察当我们发送SIGINT前后的变化
结果如上,我们发现当我们发送SIGINT信号号,其确实由于阻塞信号集而一直处于未决的阶段
三,信号处理
首先,信号处理并不是立即处理,而是寻找合适的时机进行处理。为什么呢?因为信号的产生是异步的,当信号发送过来时进程可能在处理更重要的事情,会等待合适的时机处理信号。那么合适的时机又是什么呢?指的是当进程从内核态切换到用户态时,进程会在内核的指导下,进行信号的检测以及处理。
那么什么是内核态和用户态呢?
首先,用户态指运行用户自己的代码时的状态;内核态指的是发生时钟中断或者调用系统接口是会由用户态转变为内核态。由于内核态讲起来比较复杂,因此我们这里大致讲一下,在正常的32位4G的进程地址空间中,前3G属于用户区,存储的是堆栈代码数据等,3G-4G的空间存储的是操作系统的代码和数据,而CPU中会有一个专门的寄存器记录进程的状态,0是内核态,3是用户态。
用户态转为内核态主要有两种方式:
- 每隔一段时间硬件会向内核发送时钟中断,此时会由用户态转为内核态,内核态切换进程也是在这个时候
- 系统调用,当调用系统接口时,会转化为内核态调用接口
3.1 信号捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。而信号的捕捉具体方式如下图
在上图中我们以信号SIGHA为自定义处理方式为例子,在执行某条代码时,可能会因为时钟中断,异常,系统调用等行为进入内核处理,当内核处理完准备回用户模式之前,会检测当前过程中可以递送的信号,如图SIGHA的处理方式为自定义,当要处理SIGHA时会回到用户模式调用SIGHA的自定义处理函数,处理后会调用sigreturm再次回到内核,然后在返回用户模式之前会再次检测当前进程中是否有可以递送的信号,并重复上面的行为
3.2 sigaction接口
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。sigaction结构体具体如下:
在该结构体中,sa_handler为制定编号所自定义的处理方式,sa_mask则是在处理过程中需要屏蔽的额外的信号,为什么有sa_mask呢?我们需要了解到,当我们在处理一个信号时,进程会将该信号自动加入进程的阻塞集中,这样就保证了在处理这个信号时,即使又接收到了这个信号,它也会被阻塞到该信号处理结束为止。
3.3 实验样例
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void pendingprint(sigset_t* pendingset)
{
int i=1;
for(i=1;i<=31;i++)
{
if(sigismember(pendingset,i)==1)
{
cout<<1;
}
else
{
cout<<0;
}
}
}
void handler(int signal)
{
int i=0;
//2.2 定时15秒后结束处理观察发生什么
while(i<15)
{
//2.3 打印进程的pending信号集
sigset_t pendingset;
sigpending(&pendingset);
pendingprint(&pendingset);
cout<<endl;
i++;
sleep(1);
}
}
int main()
{
//1.1 创建结构体act oldact
struct sigaction act;
struct sigaction oldact;
//1.2 将act中的处理方式改为自定义处理方式
act.sa_handler=handler;
act.sa_flags=0;
//1.3添加额外的需要阻塞的信号3,4,5
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);
//2.1 修改2信息号的处理方式为自定义处理
sigaction(2,&act,&oldact);
while(true)
{
cout<<getpid()<<endl;
sleep(1);
}
return 0;
}
代码如上图,我们主要修改2信号的处理方式为handler,handler主要是在15秒内一直打印pending信号集,我们可以借此观察在处理信号时对sa_mask中信号的阻塞效果,以及处理结束后的情况,结果如下
如图,当我们运行后发送信号2,我们看到进程循环打印pending信号集,此时当我们发送信号3,4我们发现由于我们把其加入了阻塞信息集,导致2,3,4信号都被阻塞,当15秒后handler结束,我们发现信号阻塞消失,信号由操作系统处理
总结
linux的进程信号到这里就结束了,希望铁子们能够有所收货。