1.上节回顾
上节主要学习了信号发送前,信号发送中。
1.信号发送前
信号产生的方式:
1.键盘 2.进程异常 3.系统调用 4.软件条件
附加:
1.信号产生的方式有非常多,但是最终发送都是由OS统一发送
2.core dump && waitpid-> core dump flag
3.信号发送之后,不是被立即处理的,而是在合适的时候
a.进程就需要有保存信号的能力
b.“合适”:是什么时候?
2.信号发送中
需要记住一张最重要的图

pending:保存信号,已经收到但是还没有被抵达的信号
OS发送信号的本质:修改目标进程的pending位图
bolck:状态位图,表示那些信号不应该被抵达,直到解除阻塞!block也叫信号屏蔽字
hander:函数指针数组,【31】,每个信号的编号就是该数组的下标
3.信号发送后
?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???????? |
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递 达。
2.信号集操作函数?
sigset_t
类型对于每种信号用一个
bit
表示
“
有效
”
或
“
无效
”
状态
,
至于这个类型内部如何存储这些
bit
则依赖于系统
实现
,
从使用者的角度是不必关心的
,
使用者只能调用以下函数来操作
sigset_ t
变量
,
而不应该对它的内部数据做
任何解释
,
比如用
printf
直接打印
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);
函数
sigemptyset
初始化
set
所指向的信号集
,
使其中所有信号的对应
bit
清零
,
表示该信号集不包含 任何有
效信号。
函数
sigfifillset
初始化
set
所指向的信号集
,
使其中所有信号的对应
bit
置位
,
表示 该信号集的有效信号包括系
统支持的所有信号。
注意
,
在使用
sigset_ t
类型的变量之前
,
一定要调 用
sigemptyset
或
sigfifillset
做初始化
,
使信号集处于确定的
状态。初始化
sigset_t
变量之后就可以在调用
sigaddset
和
sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回
0,
出错返回
-1
。
sigismember
是一个布尔函数
,
用于判断一个信号集的有效信号中是否包含
某种 信号
,
若包含则返回
1,
不包含则返回
0,
出错返回
-1
。
3.sigpending
#include <signal.h>
sigpending
读取当前进程的未决信号集
,
通过
set
参数传出。调用成功则返回
0,
出错则返回
-1
。 下面用刚学的几个函数做个实验。
程序如下:
我们屏蔽掉2号信号(相当于在block中2号设置为1),此时我们发送二号信号,就没有任何作用
#include<iostream>
using namespace std;
#include<signal.h>
#include<unistd.h>
int main()
{
sigset_t iset,oset;
sigemptyset(&iset); //初始化为全0
sigemptyset(&oset); //初始化为全0
sigaddset(&iset,2); //屏蔽二号信号
sigprocmask(SIG_SETMASK,&iset,&oset);
while(1)
{
cout<<"hello c++"<<endl;
}
}
下面我们屏蔽二号信号,向进程发送二号信号,二号信号被屏蔽所以不会被抵达,我们向进程发送二号信号,知识二号信号一定会保存在pending表中我们打印pending表
? ??
#include<iostream>
using namespace std;
#include<signal.h>
#include<unistd.h>
void show_pending(sigset_t *set)
{
for(int i = 1; i<=31; ++i)
{
if(sigismember(set,i)) //查看当前信号是否被设置
cout<<"1";
else
cout<<"0";
}
cout<<endl;
}
int main()
{
sigset_t iset,oset;
sigemptyset(&iset); //初始化为全0
sigemptyset(&oset); //初始化为全0
sigaddset(&iset,2); //屏蔽二号信号
sigprocmask(SIG_SETMASK,&iset,&oset);
sigset_t pending;
while(1)
{
sigemptyset(&pending); //初始化为全0
sigpending(&pending); //查看pending表
show_pending(&pending);
sleep(1);
}
}

可以看出当我们屏蔽掉2号信号,在发送二号信号,则该信号不会被递达会被保存在pending位图中,该位置会被设置为1
下面在观察,我们屏蔽二号信号,在打印pending表后,中途我们解除对2号信号的屏蔽,在观察结果
#include<iostream>
using namespace std;
#include<signal.h>
#include<unistd.h>
void show_pending(sigset_t *set)
{
for(int i = 1; i<=31; ++i)
{
if(sigismember(set,i)) //查看当前信号是否被设置
cout<<"1";
else
cout<<"0";
}
cout<<endl;
}
void hander(int signo)
{
cout<<"this is signo"<<endl;
}
int main()
{
sigset_t iset,oset;
sigemptyset(&iset); //初始化为全0
sigemptyset(&oset); //初始化为全0
signal(2,hander); //捕捉信号,2号信号默认是进程退出,自己捕捉方便观察
sigaddset(&iset,2); //屏蔽二号信号
sigprocmask(SIG_SETMASK,&iset,&oset);
sigset_t pending;
int count = 0;
while(1)
{
sigemptyset(&pending); //初始化为全0
sigpending(&pending); //查看pending表
show_pending(&pending);
if(count == 20)
{
sigprocmask(SIG_SETMASK,&oset,nullptr);
}
else
{
++count;
}
sleep(1);
}
}

?可以看出当发送二号信号是会被屏蔽,pending位图2号位被设置为1,当打印达到20次时,解除屏蔽,信号会被立即抵达,并会调用我们自己写的信号捕捉方法
信号什么时候被处理?
因为信号是被保存在进程PCB中 ,pending位图中,处理方式有:
a.默认
b.忽略
c.自定义
那到底是什么时候处理信号,前面我们也说过信号的处理是在合适的时候
当进程从内核态返回到用户态的时候,进行上面的检查并进行处理工作!
那什么是用户态和内核态
我们先感性的认识如下图

?例如当用户调用open()系统调用的时候,会嵌入到内核,在内核中执行open方法在返回到用户态
我们在理性的认识

?在CPU中会有一个CR3寄存器
CR3寄存器的改变与操作系统的关联主要是由于进程切换,每当进程切换时,CR3的内容需要被操作系统修改。
先了解一下进程切换的具体内容,从本质上说,每个进程切换由两部分组成:
1、切换页全局目录以安装一个新的地址空间
2、切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包括CPU寄存器。
总结
1.当执行系统调用的时候,我们会从用户态到内核态的转变,CPU中CR3寄存器会设置相应的标志位
2.进程切换的时候,操作系统的内核代码通过内核页表,让每个进程看到都是同一份,而用户空间的代码和数据可以多分,相当于内核是一个基础而用户的代码和数据就相当于钩子挂在内核上运行的
3.信号处理机制

?那什么内核不能直接执行用户空间的代码呢,不是内核态可以看到内核的代码也可以看到用户的代码吗?其实理论上可以实现的,但是如果有人恶意破坏的话,就完了,比如在hander方法中写了(rm rf /)的实现,你又是内核态去执行的,可能会删掉所有的文件和数据,那不是就拉闸了吗
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码
是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行
main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号
SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler
和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返
回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复
main函数的上下文继续执行了
4.sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
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);
};
?
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo
是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传
出该信号原来的处理动作。act和oact指向sigaction结构体:
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动 作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回 值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信 号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来
的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果
在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需
要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flflags字段包含一些选项,本章的代码都
把sa_flflags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。
void hander(int signo)
{
cout<<"this is signo"<<endl;
}
int main()
{
struct sigaction iset;
memset(&iset,0,sizeof(iset));
iset.sa_handler= hander;
sigemptyset(&iset.sa_mask);
sigaction(2,&iset,nullptr);
while(1)
{
cout<<"hello c++" << endl;
sleep(1);
}
}

?
|