信号(2)
文章目录
递达-阻塞-未决
- 实际执行信号的处理动作叫做信号递达(delivery)
信号处理方式
- 自定义
- 默认
- 忽略
- 信号从产生到递达之间的状态叫做信号未决(pending)
本质上就是这个信号被暂存在task_struct 信号位图里面,未决
我先不知道咋搞,先保存着
- 进程可以阻塞(block)某个信号
本质是OS允许进程暂时屏蔽指定的信号,
阻塞过程中
- 该信号依旧是未决的
- 该信号不会被递达,直到解除阻塞,才可以递达
忽略和阻塞有区别吗?
区别特别大,忽略是递达的一种方式
阻塞是没有被递达,一个独立状态,解除阻塞后可以被忽略、
信号表
在信号当中是有3张表格的
分别是handler,pending,block
pending表:确认一个进程是否收到信号
handler表
void (*handler[32])(int):函数指针数组,里面放的就是一个一个的函数指针
pending :比特位的位置代表是哪一个信号,比特位位置的0,1代表是否收到了信号
1代表收到了信号但处于未决状态(还未递达),0代表没收到信号,或者已经递达了block表:本质上也是叫做位图结构,uint32_t block:(也叫做信号屏蔽字),比特位的位置代表信号的编号,比特位的内容代表信号是否被屏蔽阻塞,阻塞的信号无法被递达,
伪代码如下:
1 |
|
进程通过这三张表是可以识别信号的,
不要认为只有接口才可以算是system call 的接口
我们也要意识到,OS 也会给用户提供数据类型,
sigset_t
信号集,未决和阻塞标志可以使用相同的数据类型sigset_t 来存储,这个类型可以标识每个信号状态处于何种状态(阻塞还是未决),阻塞信号集也叫当前进程的信号屏蔽字,这里的屏蔽应该是阻塞而不是忽略
sig函数
int sigemptyset(sigset_t * set); 把所有的比特位全部置0,
int sigfillset(sigset_t * set)把所有的比特位全部置1
int sigaddset(sigset_t* set,int signo )把特定的信号加入到位图当中
int sigdelset(sigset_t *set,int signo)把特定的信号在位图当中去掉
int sigismember(const sigset_t *set,int signo)检查在位图当中是否有这个信号(成功放回1,失败放回0)
sigprocmask
统一修改的是block位图
int sigprocmask(int how,sigset* set,sigset* oset)
成功返回0,失败返回-1
set是一个输入型参数(把我们要的数据传进去),我们自己设置好的
oset是一个输出型参数(返回老的信号屏蔽字—block位图),没有修改之前的屏蔽字
how(修改的方法):
- SIG_UNBLOCK:set里面包含了我们希望当前信号屏蔽字中解除的信号,相当于mask=mask&~set
- SIG_BLOCK:set里面包含了我们希望添加到信号屏蔽字中的信号
- SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set
9号信号没有被屏蔽掉
1 |
|
sigpending
int sigpending(set_t *set)
,输出型参数
不对pending位图进行修改,而只是单纯的获得pending位图
(类似waitpid里的status)
pendind位图不是让我们修改的,而是OS,我们只需要获取就可以了
如果我们的进程先屏蔽掉2号信号,再不断的获取当前进程的pending表,然后手动发送2号信号,因为2号信号不会被递达,所以,不会的获取当前进程的pending位图,打印出(00000010),发送后不能被递达就会永远保留再pending位图里面
signal就是修改handler表
1 |
|
信号发送后
信号是在合适的时候处理,因为信号的产生是异步的,也就意味着当前进程可能正在做更重要的事情,
信号可以延时处理(取决于OS和进程)
什么是”合适“的时候,因为信号是保存在进程的PCB中,pending位图里面,检测,处理(检测,忽略,自定义)
当进程从内核态放回到用户态的时候,进行上面的检测并处理工作
- 内核态:执行OS 的代码和数据时候,计算机所处于的状态就叫做内核态,OS的代码的执行全部都是在内核态
- 用户态:用户代码和数据被访问或者执行的时候,所处于的状态,我们自己写的代码全部都是在用户态中执行的,
主要是权限大小的区别
我们很经常在用户态调到内核态,再从内核态转移到用户态:系统调用(open),从普通用户变成了一个
用户调用系统函数的时候,除了进入函数,还有身份的变化,从用户的身份变成内核的身份。
死循环:也有可能进入内核态,因为死循环太久了,就被OS 给回收了,这个之后就执行了进程替换,一定会有用户到内核,内核到用户
信号的处理流程
为何一定要切换成为用户态,才能执行信号捕捉方法,
OS 是可以执行用户的代码,但是OS 不相信任何人,轻易不执行别人的代码,身份特殊,不能直接执行用户的代码
(用户做了不合理的动作,那OS 也能搞它的代码,很危险)
sigaction
和signal差不多
修改的是handler函数指针数组(handler表)
信号捕捉,注册信号,对特定的信号进行处理
#include<signal.h>
int sigaction(int signo,const struct sigaction* act,struct sigaction* oact)
act 是一个输入型参数(把动作方法填到这里面)
oact是一个输出型参数(带回老的信号,不想要的设置位nullptr)
struct sigaction {
void (*sa_handler)(int);//方法void (*sa_sigaction)(int, siginfo_t *, void *);//实时信号
sigset_t sa_mask;int sa_flags;//选项void (*sa_restorer)(void);//实时信号
};
1 |
|
信号捕捉特性
- 当我们处理某一个函数调用的时候,首先要把要执行的信号先加到屏蔽字里面,当信号处理函数返回的时候就自动恢复原来的信号屏蔽字,这样就保证了再处理信号时,如果这种信号再次产生,它就会被阻塞到处理完为止。(如果我们收到一个信号但是不屏蔽它,再信号处理函数内部,再收到信号,handler方法就会被不断的被调用,处理完才能处理下一个)
- 如果再调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外的一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时,会自动恢复
volatile
gcc volatile.c -O3( 编译器优化 )
1 |
|
编译器优化之后,直接把flag放到了cpu寄存器上面,不在内存里面了
但是在signal调用函数的时候修改了flag,这个是对内存当中的flag修改
cpu检测都不是内存,所以我们无论怎么ctrl c都不会退出
cpu访问内存就被屏蔽掉了
而我们加入volatile之后,
作用:告诉编译器,不要对我这个变量做任何优化,读取必须是贯穿模式的从内存到cpu,不要读取中间缓冲区寄存器中的数据,永远都能看到内存,
保持内存的可见性