信号保存与捕捉
- 1.相关概念
- 2.信号在内核中的示意图
- 3.信号集
- 4.信号集操作函数
- 5.内核态与用户态
- 6.信号捕捉
- 7.sigaction
- 8.可重入函数
- 8.volatile
- 9.SIGCHLD信号
🌟🌟hello,各位读者大大们你们好呀🌟🌟
🚀🚀系列专栏:【Linux的学习】
📝📝本篇内容:信号在内核中的情况;信号集;信号集操作函数;内核态和用户态;信号捕捉;sigaction;可重入函数;volatile;SIGCHLD信号
⬆⬆⬆⬆上一篇: 信号产生
💖💖作者简介:轩情吖,请多多指教(>> •̀֊•́ ) ̖́-
1.相关概念
①实际执行信号的处理动作称为信号递达
②信号从产生到递达之间的状态,称为信号未决
③进程可以选择阻塞(Block)某个信号
④被阻塞的信号产生时将保持在未决状态,直到进程解决对此信号的阻塞,才执行递达的动作
⑤注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
2.信号在内核中的示意图
①pending表:位图结构,比特位的位置,表示哪一个信号,比特位的内容代表是否收到信号,设置方法类似于:uint32_t pending=0;pending|=(1<<(signo-1))
②block表:位图结构,比特位的位置,表示哪一个信号,比特位的内容,代表是否对应的信号被阻塞
③handler表:函数指针数组,该数组下标,表示信号编号,数组的特定下标内容,表示该信号的递达动作,其中SIG_DFL是默认处理方法,SIG_IGN是忽略,可以作为signal的参数传递
④每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志位
⑤如果在进程解除对某信号的阻塞前这种信号产生过多次,POSIX.I允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达前产生多次只记一次,而实时信号在递达前产生多次,可以依次放在一个队列中(我们这里不考虑实时信号)。
3.信号集
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
4.信号集操作函数
#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);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
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参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下面说明了how参数的可选值
SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,mask=set
#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handle(int signo)
{
cout<<"我是对应的"<<signo<<"信号"<<endl;
}
int main()
{
signal(2,handle);
sigset_t set,oset;
sigemptyset(&set);//初始化
sigemptyset(&oset);
sigaddset(&set,2);
sigprocmask(SIG_SETMASK,&set,&oset);//屏蔽掉2号信号
while(1)
{
cout<<"我是一个进程,我在运行......"<<endl;
sleep(1);
}
return 0;
}
可以发现2号信号已经被屏蔽了
5.内核态与用户态
用户态:执行自己写的代码的时候,进程所处的状态
内核态:执行OS的代码的时候,进程所处的状态
所有的进程0~3GB是不同的,每一个进程都要有自己的用户级页表
所有的进程3~4GB是一样的,每一个进程都可以看到同一张内核级页表,所有进程都可以通过统一的窗口看到同一个OS
把物理内存中的OS数据和代码与虚拟地址中的OS代码和数据通过页表建立映射,因此,OS运行的本质其实都是在进程的地址空间内运行的,所以说所谓的系统调用的本质就是如果调用动态库中的方法,在自己的地址空间中进行函数跳转并返回
为了防止出现随意访问OS的数据和代码才出现了用户态和内核态,用户无法直接更改,OS提供的所有的函数调用,内部在正式执行调用逻辑时,会去修改执行级别
CPU中有一个寄存器叫做CR3寄存器:
3:表征正在运行的进程执行的级别是用户态
0:表征正在运行的进程执行的级别是内核态
进程调度:OS是一个软件,本质上是一个死循环,有一个硬件叫做OS时钟硬件,它的作用是记录时间,这就是为什么关机了在开机,右下角的时间还是准确的,它是一直在工作的,OS时钟硬件每隔很短的时间会向OS发送时钟中断,此时OS要执行对应的中断方法,检测当前进程的时间片,如果时间到了,就调用schedule()系统函数来将对应的进程的上下文等进行保存并切换下一个进程。
因此无论进程如何切换,3~4GB是不变的,看到OS内容,与进程切换无关。
切换到内核态:
①进程时间片到了,需要切换,就要执行进程的切换逻辑
②系统调用
③中断
④异常
回归到信号:
当进程从内核态切换到用户态的时候,进程会在OS指导下,进行信号的检测与处理
如果一个信号之前被block(屏蔽),当他解除block的时候,对应的信号会被立即递达
6.信号捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,称为信号捕捉
7.sigaction
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。
act和oact指向sigaction结构体:
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
sa_flags字段包含一些选项,但我们设为0即可,sa_sigaction是实时信号的处理函数,设为nullptr即可
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void handle(int signo)
{
sigset_t set;
cout << "我是被捕捉的" << signo << "号信号" << endl;
int cnt = 30;
while (cnt--)
{
sigpending(&set);
for (int i = 1; i <= 31; i++)
{
// 打印一下对应的pending表
if (sigismember(&set, i))
{
cout << 1;
}
else
cout << 0;
}
cout<<endl;
sleep(1);
}
}
int main()
{
cout << "pid:" << getpid() << endl;
sigset_t set;
sigaddset(&set, 3);
sigaddset(&set, 4);
sigaddset(&set, 5);
struct sigaction act;
struct sigaction oact;
memset(&act, 0, sizeof(act));
act.sa_handler = handle;
act.sa_mask = set; // 在执行2号命令的处理函数时,同时也屏蔽掉3,4,5号信号
memset(&oact, 0, sizeof(oact));
sigaction(2, &act, &oact);
while (1)
{
}
return 0;
}
8.可重入函数
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函
数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从
sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步
之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称
为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
总结:不同执行流中同一个函数被重复进入,如果没问题 ,该函数就是可重入函数,如果有问题就是不可重入函数
8.volatile
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
int flag=0;
void handle(int)
{
cout<<"change flag 0 to 1"<<endl;;
flag=1;
}
int main()
{
signal(2,handle);
while(!flag);
cout<<"The process exits success";
return 0;
}
大家看这个代码,按照正常的来说,当你使用2号信号的时候他就会正常运行结束,但是如果我们使用编译器的时候进行一个优化呢?
可以发现经过优化后,并没有正常终止,也就是说flag的值没有变化
解释:
由于flag的值在main函数中从来没有发生过任何变化,对于编译器来说,因此这样循环每次从内存中去取值太过于麻烦了,所以进行了优化,把value放在一个寄存器里面,每次while时,从寄存器拿,而寄存器一直为0,因此不断循环
加上volatile保证内存可见性,告诉编译器,保证每次检测都从内存中进行数据读取,不要用寄存器的数据,让内存数据可见
本质上编译器优化就是通过对代码进行了修改而已,CPU只会执行代码
9.SIGCHLD信号
用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进
程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用waitpid清理子进程即可。
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
pid_t pid=0;
void handle(int signo)
{
//回收对应的子进程
waitpid(pid,nullptr,0);
exit(2);
}
int main()
{
signal(SIGCHLD,handle);//当父进程收到子进程发来的信号时,进行捕捉
pid=fork();
if(pid==0)
{
//子进程
while(1);
{
//让子进程不终止
}
}
cout<<pid<<endl;
while (1)
{
//让父进程不终止
}
return 0;
}
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
pid_t pid=0;
/*void handle(int signo)
{
//回收对应的子进程
waitpid(pid,nullptr,0);
exit(2);
}*/
int main()
{
signal(SIGCHLD,SIG_IGN);//当父进程收到子进程发来的信号时,进行捕捉
pid=fork();
if(pid==0)
{
//子进程
while(1);
{
//让子进程不终止
}
}
cout<<pid<<endl;
while (1)
{
sleep(1);
cout<<"我是父进程,我在进行工作...."<<endl;
}
return 0;
}
可以看到子进程结束后并没有产生僵尸进程也没有影响父进程的工作。
🌸🌸信号保存和捕捉的知识大概就讲到这里啦,博主后续会继续更新更多Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪