【Linux】 信号的保存 | 捕捉

对于信号,主要涉及到信号的产生、保存和捕获,之前谈到了信号的产生,这里主要介绍信号产生后如何进行保存和捕捉处理的原理。

一、信号的保存

1.阻塞信号

相关概念

  • 实际执行处理信号的动作称为信号递达Delivery
  • 信号从产生到递达的过程称为信号未决Pending
  • 进程可以阻塞、忽略某个信号。
  • 被阻塞就只有产生和未决,忽略是在递达后的处理。


2.内核结构

之前讲过OS发送信号给进行,会在进程PCB表上的位图标记 0-》1,并回调函数指针的方法。

实际上,内核会位PCB的信号维护三张表Block(阻塞表)、Pending(未决)、handler(函数指针数组)

【Linux】 信号的保存 | 捕捉_第1张图片

说明:

  • Pending表就是标记我们之前谈到的位图,0/1标记某一位是否收到信号。
  • Block表也是位图 ,代表某一信号是否被阻塞,对特定的信号进行屏蔽。
  • Handler表是函数指针数组,内容 :0 表示默认 1是忽略,还有用户自定回调函数。

描述这一过程:
在信号没有创建之前,Block表中的某一位先会被设置位0和1,标记是否被阻塞。

信号产生时,会在进程控制块的Pengding表中将对应信号位的0-》1。

再校验Block表,如果表上的比特位是1 ,代表被阻塞,将不会递达。直到阻塞被解除。

总结:

进程task_struct中会维护三张表。三张表共同维护信号的识别。

信号的阻塞,不会影响产生。

一个信号没有被递达,并且接收到多次,pending表会默认最后一次发送的信号。


信号的捕捉

信号在什么时候被处理?

进程从内核态到用户态的时候,进行信号的检测和处理

用户态是一种受控的状态,能访问的资源是有限的。

内核态是OS的一种工作状态,能访问到大部分资源。

系统调用必定发生身份从用户态到内核态的转变,因为我们无法通过用户态进行系统调用,

系统调用是比较费时间的,要避免频繁的系统调用。

用户态和内核态

如何对用户态和内核态进行区分?

在CPU上有一个CR3寄存器,是一个2比特位的。00 01 10 11

1表示内核,3表示用户态

进程如何调用系统调用接口?

用户态只能访问自己的【0,3】GB的内存空间。

内核态能让用户以OS的身份访问【3,4】GB。

进程需要被加载到内存中,OS需要维护进程PCB。实际上我们平时说的页表是【0,3】GB的用户级别页表。每一份进程都需要维护一张

【3,4】GB有对应的内核级页表。因为内核级的内容不会被用户身份访问,所以只需要维护一张内核级页表,这个页表将给CS寄存器保存。

故如果进程需要调用系统调用,就像我们平常调用库函数一样,就是在进程地址空间中跳跃。

先在CR3寄存器中切换身份,然后通过寄存器CS找到内核级页表,就能找到对应的内核内容。

【Linux】 信号的保存 | 捕捉_第2张图片


内核如何实现捕捉

进程的信号在合适的时候被处理,从内核转到用户级,先检测再处理。

【Linux】 信号的保存 | 捕捉_第3张图片

描述这一过程

  • CPU执行用户代码时,会先以用户态执行,遇到系统调用接口时,会切换身份调用系统调用。执行完成后到进程的PCB内查看信号列表,如果pending表全为0,或者pending为 1 block为 1阻塞也直接返回。
  • 如果pending为1,block为0,且handler存在自定义的方法,则会将内核态切换为用户态,调用用户的方法。(这一切换是为了防止内内容被用户破坏)。执行完毕后会现在内核态检测信号的位置,通过特定系统调用返回。

注意:
在信号调用handler方法时,就会将pending表上的1-》0

如果处理完毕后,pending表上还有信号没被处理,则会执行handler方法。

抽象图帮助记忆

【Linux】 信号的保存 | 捕捉_第4张图片

一共会经过四次身份切换,只有在第一次身份切换时,才进行信号的检测与处理!


信号操作函数

sigset_t

位图类型

它在Linux下的定义

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
	unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
 
typedef __sigset_t sigset_t;
  • 对于panding表,0表示没接收到信号,1表示接收到信号
  • 对于block表,0表示信号没有被阻塞,1表示信号被阻塞

sigset_t函数

因为sigset_t 是位图,它的每一位比特位可以表示pending表和block表的内容,因此我们可以通过逻辑关系的方法修改比特位的内容,但是这样过于繁琐。就由下面这些函数操作位图。

#include 
 
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);

  • sigemptyset函数:将位图表设置为全0,(清0)
  • sigfillset函数:将位图上的所有比特位置为有效信号(置位)
  • sigaddset函数:将signo的信号置为有效信号
  • sigdelset函数:将signo信号置零
  • sigisemember函数:判断signo信号是否在位图中。

注意:
sigset_t 创建后,需要初始化(置位/清零)

我们操作的表与进程中的表没有关系。需要继续调用系统调用写入。


sigprocmask

sigprocmask函数可以用于读取或更改进程的信号屏蔽字(阻塞信号集),该函数原型如下:

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

一般而言,阻塞和未决表在进行修改时,都会保存上一张表

参数说明:

  • oset不为空时,读取当前进程的阻塞表输出到oset表中
  • set不为空时,依旧how和set更改当前的阻塞表
  • set和oset都不为空时,会保存旧表更改阻塞表

how参函数

想要如何操作信号屏蔽字,此参数有三个可选值:

  • 1.SIG_BLOCK:就是把对应信号的bit位改为1,即就是阻塞该信号
  • 2.SIG_UNBLOCK:就是把对应信号的bit位改为0,即就是使该信号不阻塞
  • 3.SIG_SETMASK:设置当前信号屏蔽字为set所指向的值

返回值:

  • 调用成功返回0,失败返回-1

sigpending函数

       #include 

       int sigpending(sigset_t *set);

操作pending表,读取当前进程的pending表,通过set返回

成功返回0,失败返回-1


下面是举例运用

使用sigprocmask函数阻塞2号信号和40号信号

要求:阻塞2号信号和40号信号, 分别给进程发送5次2号信号和5次40号信号,观察结果

 1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 
  6 
  7 void pendingPrin(sigset_t* s)
  8 {
  9   for(int i=31;i>0;i--)
 10   {
 11     if(sigismember(s,i))
 12     {
 13       std:: cout<<1;
 14     }
 15     else std::cout<<0;
 16   }
 17   std::cout<

【Linux】 信号的保存 | 捕捉_第5张图片


sigaction

捕捉信号,除了之前谈到的signal之外,sigaction对特定信号捕捉。

#include 
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

 参数说明:

  • signum:要捕捉的信号编号
  • act:不为空,则为用户自定行为。
  • oldact:导出原信号的处理行为。

act和oldact都是sigacgtion类型的结构体,定义如下

struct sigaction {
	void(*sa_handler)(int);
	void(*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask;
	int        sa_flags;
	void(*sa_restorer)(void);
};
  • 第一个参数:sa_handler

是一个函数指针 SIG_IGN是,忽略处理, SIG_DEF是默认处理,handler自定义处理。

  • 第二个参数:sa_sigaction

实时信号的处理函数。

  • 第三个参数是:sa_mask

是阻塞信号集,就和之前谈到的阻塞位图一致‘

  • 选项sa_flags

通常设为0

举例使用

  1 #include
  2 #include 
  3 #include 
  4 #include 
  5 
  6 void handler(int signo)
  7 {
  8   std::cout<<"get a signo: "<

【Linux】 信号的保存 | 捕捉_第6张图片


相关知识

信号的主体部分已经介绍完毕,下面还有几个相关知识点:

volatile关键字

保持内存的可见性

程序提高优化级别(例如debug到relase的转变)会使当前只读变量放进寄存器,而如何后续的变量由信号触发变化,信号变化handler是在内存中,这时候就会对一个变量形成俩份,导致寄存器中保持的不受改变。

volatile关键字声明在类型前,告诉编译器不要做过度的优化,保持内存的可见性。


SIGCHLD信号

看上去不那么实用的信号

为了避免出现僵尸进程,子进程在结束后,父进程需要等待,回收资源。等待方式可以是阻塞等待,也可以是轮询等待。但是这俩种方式都有延迟。

GIGCHILD信号,在子进程结束后,立马会去父进程发送信号,让父进程捕捉信号。

但是这个方法也有缺点,假设有多个子进程同时发送信号,但是父进程,没来得急回收,就会导致信号被阻塞,实际最后子进程不能完全被父进程杀掉。

那就必须调用自己的方法,handler函数不断的调用waitpid。

实际上

父进程调用signal或sigaction函数将SIGCHLD信号的处理动作设置为SIG_IGN,子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

系统默认的忽略动作和用户用signal或sigaction函数自定义的忽略通常是没有区别的,但这是一个特列。此方法对于Linux可用,但不保证在其他UNIX系统上都可用。故而SIGCHLD是比较不实用的信号。


你可能感兴趣的:(linux,linux,运维,服务器)