【Linux】信号的保存&信号的捕捉&信号集&零碎知识点总结
- 一、信号的保存
- 1.1 信号几种概念
- 1.2 信号在内核中的表示
- 二、信号的捕捉
- 了解用户态和内核态
- 2.1 捕捉过程
- 2.2 信号的捕捉方法
- 2.3 信号捕捉规则
- 2.4 多信号屏蔽问题
- 三、信号集
- 3.1 概念
- 3.2 信号集(sigset_t)操作函数
- 四、可重入函数
- 五、volatile关键字
- 六、SIGCHLD
一、信号的保存
1.1 信号几种概念
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
1.2 信号在内核中的表示
原理图,一图速览内核中信号的基本数据结构构成
在进程内部要保存信号周边的信息,有3种数据结构与之是强相关的,pending表,Block表,Handler表。
- 进程可能在任何时候收到OS给它发送的信号,该信号可能暂时不被处理,所以需要暂时被保存,进程为了保存信号采用位图来保存,这个位图就是pending位图,被置于pending位图的信号处于未决状态。OS向进程发信号就是向目标进程的pending位图设置比特位,从0到1就是当前进程收到该信号,所以发信号应该是写信号,PCB属于OS内核结构,只有OS有权力修改pending位图,所以发送信号的载体只能是OS。
- block位图:block位图比特位的位置代表信号标号,比特位的内容代表是否阻塞了该信号
- Handler表,内核中有指针指向该数组,这个数组称为当前进程所匹配的信号递达的所有方法,数组的位置(下标)代表信号的编号,数组下标对应的内容表示对应信号的处理方法.
⚠️重点:我们之前所谈到的信号接口signal(signo,handler)的本质是:根据信号编号,找到数组对应的下标,然后将用户层设置的handler函数的地址填到数组对应下标处,等信号产生时候,修改pending表比特位,根据Block表判断该比特位是否被阻塞。信号被递达时,OS拿到信号根据信号位置得到信号的编号,进而访问数组得到信号的处理方法。
二、信号的捕捉
信号产生的时候,可能不会立即处理,会在合适的时候从内核态返回用户态的时候处理。
了解用户态和内核态
-
用户为了访问内核或者硬件资源必须要使用系统调用,系统调用是OS提供的接口,而普通用户不能以用户态的身份执行系统调用,必须让自己的身份切换成成内核态。
-
一个进程在执行时必须把上下文信息切换到CPU中,CPU中有大量的寄存器,寄存器可分为可见寄存器(eax,ebx…),不可见寄存器(状态寄存器…),凡是和当前进程强相关的,是上下文数据。
寄存器中还有非常多的寄存器在进程中有特定作用,寄存器可以指向进程PCB,也可以保存当前用户级的页表,指向页表起始地址
寄存器中还有CR3寄存器:表征当前进程的运行级别:0表示内核态,3表示用户态
进程如何去OS中执行方法
- 以前所说的进程地址空间0-3G是用户级页表,通过用户级页表映射到不同的物理空间处,而除了用户级页表之外,还有内核级页表,OS为了维护从虚拟地址到物理地址之间的OS级别的代码所构成的内核级映射表。
- 3G-4G是OS内部的映射,所以进程建立映射的时候不仅仅把用户的代码、数据和进程产生关联,每一个进程都要通过内核级页表和OS产生关联,而每一个进程都有自己的地址空间,其中用户级空间自己占有,而内核空间是被映射到了每一个进程的3-4G空间,每一个进程都可以通过页表映射到OS,而且每个进程看到的OS都是一样的,所以进程要访问OS的接口,其实只需要在自己的地址空间上进行跳转就可以了
- 每一个进程都有3-4GB,都会共享一个内核级页表,无论进程如何切换,都不会更改任何的3-4GB。
用户通过什么能够执行访问内核的接口或者数据
- 当要访问3-4G任何一个地址时,OS立马读取CPU中的CR3寄存器,得到运行状态。所以系统调用接口起始的位置会帮我们把用户态变成内核态,从3号状态改成0号状态。
OS是如何通过系统调用把用户态变成内核态的:
中断汇编指令int 80就是陷入内核,简单理解把状态由用户态改成内核态。调用结束时再切回来
2.1 捕捉过程
通过系统调用,陷入内核,OS返回用户态之前,会在进程的上下文中搜索可以传递的信号,OS能从CPU寄存器中拿到task_struct找到进程,查3张表,先查block表:block为0说明没被阻塞,继续看pending,pending为0继续下一个…
理论上是可以从内核态访问用户态(waitpid),但是实际上我们不能以内核态去访问用户态,OS不相信任何用户
简化图:
2.2 信号的捕捉方法
signal:
#include <signal.h>
typedef void (*sighandler_t)(int);//函数指针
sighandler_t signal(int signum, sighandler_t handler);
sigaction:
act:结构体对象,输入型参数;oldact:输出型参数,获取特定信号老的处理方法
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);//回调方法
void (*sa_sigaction)(int, siginfo_t *, void *);//处理实时信号,不关心
sigset_t sa_mask;//信号集
int sa_flags;//设置0就行
void (*sa_restorer)(void);
};
RETURN VALUE:返回值
sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.
2.3 信号捕捉规则
- 当我们进行正在递达某一个信号期间,同类型信号无法递达:当前信号正在被捕捉,系统会自动将当前信号加入到进程的信号屏蔽字(即BLOCK表),自动将2号信号屏蔽。
- 而当信号完成捕捉动作,系统又会自动解除对该信号的屏蔽
- 一般一个信号被解除屏蔽的时候,如果该信号已经被pending的话,会自动进行递达当前屏蔽信号,没有就不做任何动作
- 进程处理信号的原则是串行的处理同类的信号,不允许递归式处理
- 信号的自定义捕捉行为在执行之前就会把pending对应的信号比特位由1置0,然后再去执行行为,避免重复抵达问题
2.4 多信号屏蔽问题
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void Count(int cnt)
{
while(cnt)
{
printf("cnt:%d\r",cnt);
fflush(stdout);
cnt--;
sleep(1);
}
cout<<endl;
}
void handler(int signo)
{
cout<<"get a signo"<<signo<<"正在处理信号"<<endl;
Count(20);//倒计时打印
}
int main()
{
struct sigaction act,oact;
act.sa_handler=handler;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
//屏蔽多个信号可以把信号添加到sa_mask
sigaddset(&act.sa_mask,3);
sigaction(SIGINT,&act,&oact);
while (true)
{
sleep(1);
}
return 0;
}
三、信号集
3.1 概念
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
3.2 信号集(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);
sigprocmask :读取或更改进程的信号屏蔽字(阻塞信号集)Block表
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信
号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后
根据set和how参数更改信号屏蔽字
sigpending :读取pending信号集
#include <signal.h>
int sigpending(sigset_t *set)
调用成功则返回0,出错则返回-1。
关于这些函数的具体使用可参考我的码云
四、可重入函数
在信号层面解释:如果在main中和在handler中,该函数被重复调用,此时出现冲突问题,则该函数(比如list.insert)称为不可重入函数;相反如无冲突,则是可重入函数。
例如:main函数调用insert,向链表head插入Node1,insert只做了第一步,然后就被中断(或者因为信号原因执行信号捕捉),此时进程挂起,然后回到用户态之前检查有信号待处理,于是执行sighandler方法,sighandler也调用了insert函数,把Node2头插到链表里,Node2的next结点指向下一个结点位置,下一步就是head指向Node2,完成Node2的头插,信号捕捉完之后就成功把Node2插入,接下来回到main执行流,对Node1完成插入的第二步动作,此时把head指向Node1,最后只有Node1真正插入到链表之中。
而我们目前大部分情况下用的接口,全部都是不可重入的,重入不重入是特性。
识别不可重入函数
调用了malloc或free,因为malloc也是用全局链表来管理堆的。调用了标准I/O库 函数。标准I/O库的汗多实现都以不可重入的方式使用全局数据结构。
五、volatile关键字
volatile保持内存可见性
#include <stdio.h>
#include <signal.h>
int quit = 0;
void handler(int signo)
{
printf("%d号信号,正在被捕捉\n",signo);
printf("quit:%d",quit);
quit = 1;
printf("->%d\n",quit);
}
int main()
{
signal(2,handler);
while(!quit);
printf("注意,我是正常退出的\n");
return 0;
}
以上代码正常编译是以下情况
但是增加了-O3优化后变成以下情况
mysignal:mysignal.c
gcc -o $@ $^ -O3
具体原因:
寄存器的存在遮盖了物理内存当中quit变量存在的事实
解决办法:int quit = 0;增加关键字 volatile-->volatile int quit=0
六、SIGCHLD
父进程可以用wait和waitpid函数清理僵尸进程【子进程先于父进程退出后,子进程的PCB需要其父进程释放,但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程】,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式),这两种方式都是局限性。
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略。
要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGNsignal(SIGCHLD, SIG_IGN);
,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。