对于信号,主要涉及到信号的产生、保存和捕获,之前谈到了信号的产生,这里主要介绍信号产生后如何进行保存和捕捉处理的原理。
相关概念
- 实际执行处理信号的动作称为信号递达Delivery
- 信号从产生到递达的过程称为信号未决Pending
- 进程可以阻塞、忽略某个信号。
- 被阻塞就只有产生和未决,忽略是在递达后的处理。
之前讲过OS发送信号给进行,会在进程PCB表上的位图标记 0-》1,并回调函数指针的方法。
实际上,内核会位PCB的信号维护三张表Block(阻塞表)、Pending(未决)、handler(函数指针数组)
说明:
描述这一过程:
在信号没有创建之前,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找到内核级页表,就能找到对应的内核内容。
进程的信号在合适的时候被处理,从内核转到用户级,先检测再处理。
描述这一过程
注意:
在信号调用handler方法时,就会将pending表上的1-》0
如果处理完毕后,pending表上还有信号没被处理,则会执行handler方法。
抽象图帮助记忆
一共会经过四次身份切换,只有在第一次身份切换时,才进行信号的检测与处理!
是位图类型
它在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;
因为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);
注意:
sigset_t 创建后,需要初始化(置位/清零)
我们操作的表与进程中的表没有关系。需要继续调用系统调用写入。
sigprocmask函数可以用于读取或更改进程的信号屏蔽字(阻塞信号集),该函数原型如下:
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
一般而言,阻塞和未决表在进行修改时,都会保存上一张表
参数说明:
how参函数
想要如何操作信号屏蔽字,此参数有三个可选值:
返回值:
#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<
sigaction
捕捉信号,除了之前谈到的signal之外,sigaction对特定信号捕捉。
#include
int sigaction(int signum, const struct sigaction *act,struct sigaction *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);
};
是一个函数指针 SIG_IGN是,忽略处理, SIG_DEF是默认处理,handler自定义处理。
实时信号的处理函数。
是阻塞信号集,就和之前谈到的阻塞位图一致‘
通常设为0
举例使用
1 #include
2 #include 3 #include 4 #include 5 6 void handler(int signo) 7 { 8 std::cout<<"get a signo: "<
信号的主体部分已经介绍完毕,下面还有几个相关知识点:
保持内存的可见性
程序提高优化级别(例如debug到relase的转变)会使当前只读变量放进寄存器,而如何后续的变量由信号触发变化,信号变化handler是在内存中,这时候就会对一个变量形成俩份,导致寄存器中保持的不受改变。
volatile关键字声明在类型前,告诉编译器不要做过度的优化,保持内存的可见性。
看上去不那么实用的信号
为了避免出现僵尸进程,子进程在结束后,父进程需要等待,回收资源。等待方式可以是阻塞等待,也可以是轮询等待。但是这俩种方式都有延迟。
GIGCHILD信号,在子进程结束后,立马会去父进程发送信号,让父进程捕捉信号。
但是这个方法也有缺点,假设有多个子进程同时发送信号,但是父进程,没来得急回收,就会导致信号被阻塞,实际最后子进程不能完全被父进程杀掉。
那就必须调用自己的方法,handler函数不断的调用waitpid。
实际上
父进程调用signal或sigaction函数将SIGCHLD信号的处理动作设置为SIG_IGN,子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
系统默认的忽略动作和用户用signal或sigaction函数自定义的忽略通常是没有区别的,但这是一个特列。此方法对于Linux可用,但不保证在其他UNIX系统上都可用。故而SIGCHLD是比较不实用的信号。