信号(2)

文章目录

递达-阻塞-未决

  • 实际执行信号的处理动作叫做信号递达(delivery)

信号处理方式

  1. 自定义
  2. 默认
  3. 忽略
  • 信号从产生到递达之间的状态叫做信号未决(pending)

本质上就是这个信号被暂存在task_struct 信号位图里面,未决
我先不知道咋搞,先保存着

  • 进程可以阻塞(block)某个信号

本质是OS允许进程暂时屏蔽指定的信号,
阻塞过程中

  1. 该信号依旧是未决的
  2. 该信号不会被递达,直到解除阻塞,才可以递达

忽略和阻塞有区别吗?

区别特别大,忽略是递达的一种方式
阻塞是没有被递达,一个独立状态,解除阻塞后可以被忽略、

信号表

在信号当中是有3张表格的
分别是handler,pending,block

pending表:确认一个进程是否收到信号
handler表

void (*handler[32])(int):函数指针数组,里面放的就是一个一个的函数指针

  • pending :比特位的位置代表是哪一个信号,比特位位置的0,1代表是否收到了信号
    1代表收到了信号但处于未决状态(还未递达),0代表没收到信号,或者已经递达了

  • block表:本质上也是叫做位图结构,uint32_t block:(也叫做信号屏蔽字),比特位的位置代表信号的编号,比特位的内容代表信号是否被屏蔽阻塞,阻塞的信号无法被递达,

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool ishandler(int signo)
{
if(block&sig)
{
//根本就不看是否收到该型号
}
else
{
//该信号没有被block
if(signo&pending)
{
//说明没有被 blockl,而且已经收到了,可以调用这个型号了
handler_arr[signo](signo);//执行对应的回调方法
return true;
}
}


}

进程通过这三张表是可以识别信号的,
在这里插入图片描述

不要认为只有接口才可以算是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<unistd.h>
#include<iostream>

#include<signal.h>
using namespace std;

int main()
{
//虽然sigset_t 是一个位图结构,但是不同的OS 实现是不一样的,不能让用户使用直接修改该变量,
//需要使用特定的函数,

//这里的set是一个变量,所以这个set在哪里保存着呢
//set是一个变量,也是在栈空间保存着
//用户栈上保存(地址空间)
sigset_t iset,oset;//有了这个类型
// set | =1;
//外面必须使用set对应的接口函数
sigemptyset(&iset);//清空
sigemptyset(&oset);

//外面想对2,5,6三个信号做屏蔽
sigaddset(&iset,2);//把2号信号添加进去
sigaddset(&iset,9);//把2号信号添加进去,9号信号无法被屏蔽掉的

//设置当前信号的屏蔽字
//获取当前进程老的屏蔽字
// sigprocmask(SIG_SETMASK,&iset,&oset);//把2号信号给屏蔽掉了
sigprocmask(SIG_UNBLOCK,&iset,&oset);//把2号信号给屏蔽掉了
sigprocmask(SIG_BLOCK,&iset,&oset);//把2号信号给屏蔽掉了
//ctrl c 给屏蔽掉了,不会被递达的

while(1)
{
cout<<"hello"<<endl;
}

return 0;
}

sigpending

int sigpending(set_t *set)
,输出型参数
不对pending位图进行修改,而只是单纯的获得pending位图
(类似waitpid里的status)
pendind位图不是让我们修改的,而是OS,我们只需要获取就可以了

如果我们的进程先屏蔽掉2号信号,再不断的获取当前进程的pending表,然后手动发送2号信号,因为2号信号不会被递达,所以,不会的获取当前进程的pending位图,打印出(00000010),发送后不能被递达就会永远保留再pending位图里面

signal就是修改handler表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include<unistd.h>
#include<iostream>

#include<signal.h>
using namespace std;



void showpend(sigset_t& pending)
{
//检测一下信号是否存在在里面
int i=0;
for(i=1;i<32;i++)
{
if(sigismember(&pending,i))//成功放回1,失败放回0,
{
//检查信号是否存在pending位图里面
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}

void handler(int signo)
{
cout<<"2号信号被递达了,已经完成了处理 "<<signo<<endl;
}
int main()
{
//虽然sigset_t 是一个位图结构,但是不同的OS 实现是不一样的,不能让用户使用直接修改该变量,
//需要使用特定的函数,

//这里的set是一个变量,所以这个set在哪里保存着呢
//set是一个变量,也是在栈空间保存着
//用户栈上保存(地址空间)


signal(2,handler);//捕捉一下2号信号
sigset_t iset,oset;//有了这个类型
// set | =1;
//外面必须使用set对应的接口函数
sigemptyset(&iset);//清空
sigemptyset(&oset);

//外面想对2,5,6三个信号做屏蔽
sigaddset(&iset,2);//把2号信号添加进去
// sigaddset(&iset,9);//把2号信号添加进去,9号信号无法被屏蔽掉的

//设置当前信号的屏蔽字
//获取当前进程老的屏蔽字
sigprocmask(SIG_SETMASK,&iset,&oset);//把2号信号给屏蔽掉了
// sigprocmask(SIG_UNBLOCK,&iset,&oset);//把2号信号给解除屏蔽了
// sigprocmask(SIG_BLOCK,&iset,&oset);//把2号信号给屏蔽掉了
//ctrl c 给屏蔽掉了,不会被递达的
sigset_t pending;
int cnt=0;
while(1)
{

sigemptyset(&pending);//把他里面的数据都给他清空一下
sleep(1);
sigpending(&pending);//获取了pending位图
showpend(pending);
cout<<"hello"<<endl;
cnt++;
if(cnt==10)
{
//把2号信号解除屏蔽
//老的屏蔽字没有对2号进行屏蔽
//在这之前2号信号都已经被屏蔽掉了,所以前面用ctrl c是没什么用的
sigprocmask(SIG_SETMASK,&oset,NULL);//在这里之后才可以有用的
cout<<"恢复对2号屏蔽字的使用,可以被递达了"<<endl;
//被递达了之后,又会,我们无法看到恢复屏蔽,2号信号的默认动作是终止进程,所以看不到现象
}
}

return 0;
}

信号发送后

信号是在合适的时候处理,因为信号的产生是异步的,也就意味着当前进程可能正在做更重要的事情,
信号可以延时处理(取决于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;



void handler(int signo)
{
//信号捕捉动作
cout<<"signo="<<signo<<endl;
}

int main()
{
struct sigaction act;//定义了一个sigaction类型
memset(&act,0,sizeof(act));
act.sa_handler=handler;
sigaction(2,&act,nullptr);//对2号信号进行注册了
//本质是修改当前进程的handler函数指针数组特定的内容
while(true)
{
cout<<"hello "<<endl;
sleep(1);
}
return 0;
}

在这里插入图片描述
信号捕捉特性

  1. 当我们处理某一个函数调用的时候,首先要把要执行的信号先加到屏蔽字里面,当信号处理函数返回的时候就自动恢复原来的信号屏蔽字,这样就保证了再处理信号时,如果这种信号再次产生,它就会被阻塞到处理完为止。(如果我们收到一个信号但是不屏蔽它,再信号处理函数内部,再收到信号,handler方法就会被不断的被调用,处理完才能处理下一个
  2. 如果再调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外的一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时,会自动恢复

volatile

gcc volatile.c -O3( 编译器优化 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<signal.h>
volatile int flag=0;//内存里面读取flag,来检测flag


void handler(int signo)
{
flag=1;
printf("change flag 0 to 1\n");
}
int main()
{
signal(2,handler);
while(!flag);//发送2号信号之后就会停止循环
printf("这个进程是正常退出的\n");
return 0;
}

编译器优化之后,直接把flag放到了cpu寄存器上面,不在内存里面了
但是在signal调用函数的时候修改了flag,这个是对内存当中的flag修改
cpu检测都不是内存,所以我们无论怎么ctrl c都不会退出
cpu访问内存就被屏蔽掉了

而我们加入volatile之后,
作用:告诉编译器,不要对我这个变量做任何优化,读取必须是贯穿模式的从内存到cpu,不要读取中间缓冲区寄存器中的数据,永远都能看到内存,
保持内存的可见性

在这里插入图片描述


信号(2)
http://example.com/2022/05/14/信号(2)/
作者
Zevin
发布于
2022年5月14日
许可协议