Linux 进程信号之信号的保存

序言:
上一个博客我们详细介绍了许多的信号的衍生知识和信号的产生一共五种(键盘,软件条件,硬件异常,系统命令和系统调用),这篇博客我们将继续介绍信号的生命历程之信号的保存。

如果还没有看上一篇博客的快去看吧!
《Linux 进程信号之信号的产生》
一、信号相关的概念
1、信号递达(Delivery)
实际处理信号的动作就叫做信号递达,我们在上次的博客中说到处理信号的方式有三种方式:默认,忽略,自定义。
2、信号未决(Pending)
当信号已经产生但还没有到合适的时候被处理,这种状态就是信号未决。
3、阻塞(Block)
阻塞又叫做屏蔽,是指信号产生以后被阻止递达一直处于信号未决的状态。

注意:
1、信号阻塞可以发生在如何阶段。
2、信号阻塞产生以后信号只能在信号未决状态当信号解除了以后才会被递达。
3、信号阻塞和忽略动作的区别?
忽略是一种信号递达以后的一种处理方式,而阻塞是一种控制信号无法被递达的一种手段,换一句话来说,信号阻塞就是你有事情没有办法去学习,忽略就是你有时间但不就是不去学习。
二、信号保存在内核中是如何实现的?
我们上个博客说到普通信号可以选择在合适的时间点去处理,那么进程是如何记住曾经收到了这个信号呢?答案是在进程的PCB有对应的结构体成员变量去记录。
下面我们来看一下:

我们看见在PCB中有三个表分别描述:是否阻塞,是否未决,捕捉方法。
block表和pending表的数据结构都是位图,我们可以简单理解成它们是unsigned int类型的,unsigned int 一共32个比特位,正好我们的普通信号31个。
2.1、block表
block表是记录信号是否阻塞。
比特位的位置:信号编号。
比特位的内容:是否要阻塞这个信号,1代表这个信号被阻塞,0代表这个信号没有被阻塞。
2.2、pending表
pending表是记录信号是否需要递达。
比特位的位置:信号编号。
比特位的内容:这个信号是否被未决,0代表这个信号没有产生或者产生但已经被捕捉了,1代表这个信号产生了但还是处于信号未决的状态。
2.3、handler表
上面我们说过信号的捕捉方法有三种方式:默认,忽略,自定义
SIG_DFL:按照信号的默认动作处理。
SIG_IGN:按照忽略处理信号。
typedef void (*__sighandler_t)(int);#define SIG_DFL ((__sighandler_t)0) /* default signal handling */
#define SIG_IGN ((__sighandler_t)1) /* ignore signal */
#define SIG_ERR ((__sighandler_t)-1) /* error return from signal */
handler:按照用户的提供的处理方法处理。
handler是一个函数指针数组,信号标号就是数组的下标。
下面我们来看一下sighandler的类型:

handler:sighandler handler[31]
2.4、如何去理解这三张表?
我们要把这三张表拿到一起理解。
下面我们来看一下第一个信号SIGHUP的处理:

想要解读一个信号是否会被捕捉,捕捉动作是什么样的?就需要一次看一行。
第一行:
block表为0代表这个信号没有被阻塞如果信号到来它可以被递达。
pending表为0代表现在这个信号还没有产生。
handler表SIG_DFL代表这个信号采用的是默认捕捉。
这一行看完我们可以知道:SIGHUP信号还没有被产生,如果产生了它的不会被阻塞,它的捕捉方法是默认捕捉。
下面我们再来看SIGINT:
block表为1代表这个信号被阻塞了信号不会被递达会一直处于信号未决的状态。
pending表为1代表信号产生了但还是未决状态。
handler表SIG_IGN代表这个信号采用的是忽略。
这一行看完我们可以知道:SIGINT信号已经产生了,但由于被阻塞了无法递达,如果解开阻塞被捕捉的方法是忽略。
如何用unsigned int类型去实现位图?

如果我现在想去访问第32位比特位我应该怎么办?
1、32 % 8 = 4
2、32 / 8 = 0
第一步是确定他在第几个字节,第二步是确定它在当前字节的哪个比特位。
注意:
如果一个进程同时收到一个信号多次会怎么办?
如果一个进程收到一个信号多次,这个信号如果是普通信号的话只会记录一次,如果是及时信号的话,会用一个队列记录它们。
// 内核结构 2.6.18
struct task_struct {
...
/* signal handlers */
struct sighand_struct *sighand;
sigset_t blocked
struct sigpending pending;
...
}struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG]; // #define _NSIG 64
spinlock_t siglock;
};struct __new_sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void); /* Not used by Linux/SPARC */
__new_sigset_t sa_mask;
};struct k_sigaction {
struct __new_sigaction sa;
void __user *ka_restorer;
};/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);struct sigpending {
struct list_head list;
sigset_t signal;
}
三、sigset_t
sigset_t的数据结构是位图,所以它可以被用在block和pending表中,我们可以在上面的内核代码中看见block和pending表都间接或直接包含了sigset_t。
sigset_t叫做信号集,在阻塞信号集中它的每个位置表示 “阻塞” 和 “否阻塞” 所以阻塞信号集又叫做信号屏蔽字(这个和我们以前学的umask类似),在未决信号集中它的每个 位置表示 “未递达” 和 “递达” 。
3.1、信号集操作函数
对于block和pending表的修改我们可以先学习如何对sigset_t进行修改,下面我们将介绍函数来对sigset_t进行增删改查操作。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
1、sigemptyset
set:可以传入一个sigset_t类型的指针。
可以将传入的sigset_t里面的全部bit位全部置0。
2、sigfillset
set:可以传入一个sigset_t类型的指针。
可以将传入的sigset_t里面的全部bit位全部置1。
3、sigaddset
set:可以传入一个sigset_t类型的指针。
signo:想要对哪个bit位置1的下标。
可以将传入的sigset_t指定的bit位置1。
4、sigdelset
set:可以传入一个sigset_t类型的指针。
signo:想要对哪个bit位置0的下标。
可以将传入的sigset_t指定的bit位置0。
5、sigismember
set:可以传入一个sigset_t类型的指针。
signo:想要查哪个bit位是1的下标。
可以判断对应的bit位是否为1。
这四个函数都是成功返回0,出错返回-1。sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
注意:
在使⽤sigset_ t类型的变量之前,⼀定要调 ⽤sigemptyset或sigfillset做初始化,使信号集处于
确定的状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删
除某种有效信号。
3.2 sigprocmask

sigprocmask:可以读取和修改进程的block表。
how:有三种选项可以选对应三种不同的功能。

set:按照how的选项要求传入的sigset_t类型的set。
oldset:备份老的信号屏蔽字,以便后续恢复。
返回值:成功返回0,失败返回-1.
3.3、sigpending

sigpending:可以获取pending表的内容,通过set参数传出。
set:传入一个sigset_t类型,会将进程的pending表中的内容拷贝一份到传入的set。
返回值:成功返回0,失败返回-1;
为什么sigpending函数不提供修改位图的选项?
因为pending是记录信号是否产生和未决的,想要修改对pending位图修改其实我们在上一个博客就已经说过了那就是五种产生信号的方式。
3.4、一个信号是处理完成以后把pending表对应的比特位修改还是处理之后?
我们说过信号产生进程会选择一个合适的时间去处理,那么当要处理信号的时候,是在将要处理的时候就把pending位图修改,还是处理完成之后再去修改对应的pending位图呢?
答案是在处理之前,那么下面我们写一份代码来证明一下。
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <signal.h>
void Print(sigset_t set)
{for(int i = 1 ; i <= 32 ; i++){if(sigismember(&set , i)){std::cout <<"1";}else{std::cout << "0";}}std::cout<<std::endl;
}
void handler(int sig)
{sigset_t set;sigemptyset(&set);while(true){sleep(2);std::cout << "在信号处理中";sigpending(&set);Print(set);}
}
int main()
{sigset_t set;signal(SIGINT , handler);sigemptyset(&set);sigset_t block,oblock;sigemptyset(&block);sigaddset(&block , SIGINT);sigprocmask(SIG_BLOCK, &block , &oblock);int i = 0;while(true){if(i==5){sigprocmask(SIG_SETMASK , &oblock ,nullptr);}sleep(1);std::cout << "正常的运行";sigpending(&set);Print(set);i++;}
}
如果在信号处理的时候查看pending表如果发现比特位的值由1变成了0,则说明更改pending表的值是在信号处理之前,如果pending表的比特位还是1则说明更改pending表的值是在信号处理之后的。

信号从1变成了0,这个变化是在信号处理之前改变的。
总结:
这篇博客我们详细介绍了信号是如何在进程中保存的,在进程中我们用三张表来描述信号的是否被阻塞,是否被递达,进程采用什么方法去处理信号,在然后我们介绍了sigset_t和它相关的函数,我们可以用这些函数修改sigset_t类型从而有了修改进程三个表的可能,再然后我们介绍了sigprocmask这个函数是用来查看和修改进程的block表,sigpending这个函数可以查看进程的pending表,我们可以发信号去修改进程的pending表,最后我们证明了pending表的修改是在信号处理之前的。
=========================================================================
本篇关于命名管道的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持和修正!!!

