Linux:进程信号---信号的保存与处理
文章目录
- 1. 信号的保存
- 1.1 信号的状态管理
- 2. 信号的处理
- 2.1 用户态与内核态
- 2.2 信号处理和捕捉的内核原理
- 2.3 sigaction函数
- 3. 可重入函数
- 4. Volatile
- 5. SIGCHLD信号
- 序:在上一章中,我们对信号的概念及其识别的底层原理有了一定认识,也知道了信号产生的五种方式,以及core dump是什么,而本章将着重对信号的保存与处理进行讲解,去深入了解信号处理的底层逻辑,去了解什么是用户态和内核态,以及用户态和内核态转换的时机,本章还会浅谈可重入函数以及Volatile关键字
1. 信号的保存
1.1 信号的状态管理
对于普通信号而言,对于进程(是给进程的PCB发)而言,要识别自己有没有收到信号,以及收到了哪一个信号。
task_struct{
int signal;// 0000 ...... 0000普通信号,位图管理信号
}
1. 比特位的内容是0还是1,表明是否收到
2. 比特位的位置(第几个),表明信号的编号
3. 所谓的 “发信号” ,本质就是操作系统去修改task_struct的信号位图对应的比特位(写信号!!!)
问题一:信号为什么要保存?
进程收到信号之后,可能不会立即处理这个信号。此时信号不会被处理,就要有一个时间窗口。信号的范围[1,31],每一种信号都要有自己的一种处理方法.
信号的几种状态:
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号在内核中的表示示意图:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
sigpending函数:
其中的set参数是一个输出型参数,他会将当前的pending表传出。
sigprocmask函数:
第一个参数how:
参数 | 含义 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号 |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号 |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值 |
第二个参数:表示要传入的block阻塞表
第三个参数:表示之前的block旧表
问题二:我要是将所以信号都进行屏蔽,信号不就不会被处理了吗?
然而操作系统不会给你这个机会的,比如9号信号和19号信号,用户就屏蔽不了
2. 信号的处理
2.1 用户态与内核态
问题一:信号是什么时候被处理的?
当我们的进程从内核态返回到用户态的时候,进行信号好的检测和处理
当我们在调用系统调用时,操作系统会将我们的用户身份转化为内核身份,然后由操作系统帮我把函数执行完,返回时,再把我的内核身份换回用户身份 ------ 操作系统是自动会做“身份”切换的,用户身份变成内核身份,或者反着来!
问题二:什么是用户态和内核态?
内核态:允许你访问操作系统的代码和数据
用户态:只能访问用户自己的代码和数据
2.2 信号处理和捕捉的内核原理
问题三:信号是如何被处理的?
操作系统不信任用户,不仅仅体现在不让用户访问自己,也体现在操作系统不会访问用户自己写的代码!!!所以当在内核态处理信号时,会先将该信号的pending置0,然后去执行,要是执行到了自定义行为,那么进程就要先由内核状态转化为用户态,去执行这个自定义行为,然后再变为内核态继续在操作系统中执行系统操作,然后再回到用户态,返回值。(基于用户捕捉代码)
问题四:内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
在这个过程中,一共发生了四次状态的转换,所谓的信号的识别,其实就是在进程进入内核态时,顺手完成的任务。CPU内部的信号int 80(是一条汇编语句) 从用户态陷入内核态,这样就有权利去访问操作系统的数据了。
2.3 sigaction函数
sigaction函数:
sigaction结构体:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
由于我们目前只研究普通信号,所以,该结构体当中的第二、第四和第五个参数我们不做讨论,感兴趣的小伙伴可以自行去搜索。
该函数的第一个参数是要处理的信号数字,第二个参数表示要传入的sigaction结构体,第三个参数是输出型参数,他会保存上一次的sigaction结构体的数据。
直观的代码和运行结果能让我们直接看到这个函数的作用:
#include
#include
#include
#include
using namespace std;
void PrintPending()
{
sigset_t myset;
sigpending(&myset);
for(int signo=31;signo>=1;signo--)
{
if(sigismember(&myset,signo)){
cout<<"1";
}else{
cout<<"0";
}
}
cout<
运行结果如下:
由上图可知,在处理某个信号之前,该信号的pending表对应的值会先置0,然后才会执行对应的处理行动。
总结:如果某个信号正在进行处理,那么,在这个期间,这个信号的信号屏蔽字将会变成1,此时,无论外界再发送多少个该信号,都不会执行,只会将该信号对应的pending表中的值置1,但是不是执行,因为此时该信号已经阻塞了,这就防止了当该信号处理时,被重复执行,之后当这个信号处理完毕,才会取消阻塞,然后继续执行之前发的信号,并在信号执行期间继续阻塞该信号!!!
3. 可重入函数
当我们执行insert函数时,进行到一半,执行完p->next=head;语句后,触发了信号,执行此时信号中也有insert函数,然后执行信号中的insert,执行信号处理后,再继续执行main函数中的语句head=p;此时,由于函数重入,引发节点丢失,导致内存泄漏!!!
如果一个函数,被重复进入的情况下,出错了,或者可能出错,就叫做不可重入函数。否则就叫做可重入函数。
4. Volatile
在g++中是有优化选项的,编译器在编译的时候,有不同的优化级别
源代码:
#include
#include
#include
#include
using namespace std;
int flag=0;
void handler(int signo)
{
cout<<" change flag 0->1 "<
1. O0的优化:
sigproc:sigproc.cpp
g++ -o $@ $^ -O1 -g -std=c++11
结果如图:
2. O1的优化:
sigproc:sigproc.cpp
g++ -o $@ $^ -O1 -g -std=c++11
结果如图:
3. O3的优化:
sigproc:sigproc.cpp
g++ -o $@ $^ -O3 -g -std=c++11
结果如图:
问题一:为什么优化过后,程序退不出去?
此时volatile int flag=0;使用volatile关键字修饰flag就能防止编译器过度优化,保持内存的可见性!!!
5. SIGCHLD信号
当子进程退出时,不是静悄悄的退出,子进程在退出的时候,会主动向父进程发送SIGCHLD(17号)信号。所以在进行进程等待的时候,我们可以采用基于信号的方式进行等待
问题一:等待的好处是什么?
1. 获取子进程的退出状态,释放子进程的僵尸。
2. 虽然不知道父子进程谁先运行(由调度器决定),但是我们清楚,一定是父进程先退出!!!
所以,还是要调用wait/waitpid这样的接口,且父进程必须保证自己是一直在运行的!
问题二:我们必须等待吗?必须调用wait吗?
Signal(17,SIG_IGN);表示忽略17号信号,子进程会自动退出!!!
总结:
本章带大家理解了信号的保存与处理,知道了信号的不同的保存状态,以及处理信号时的内核态和用户态的转化原理,还扩展了可重入函数和Volatile关键字,以及SIGCHLD信号,最后,希望这篇文章对大家有一定帮助。