1. 阻塞信号
1.1 信号其他相关常见概念
- 实际执行信号的处理过程称为信号递达。
- 信号从产生到递达之间的状态称为信号未决。
- 进程可以选择阻塞(Block)某个信号。
- 被阻塞的信号保持在未决状态,直至进程解除对该信号的阻塞,才执行递达操作。
- 阻塞和忽略是不同的,阻塞是一种状态,忽略是一种处理信号的方法
1.2 信号在内核中的表示示意图
每个信号都有两个标志位,表示阻塞(block)和未决(pending),还有一个函数指针以表示信号的处理动作。
1.3 信号处理的过程
- 当进程接受到一个信号(2号信号)时,在其pcb结构体内的“信号标记图”中的pending位图中对应的比特位就会由0置1。
- 当人为设置了阻塞信号(2号信号和3号信号)时,“信号标记图”中的block位图中对应的比特位就会由0置1。
- 当进程收到信号,并且该信号并未被阻塞时,才会去找handler中对应的函数指针。SIG_DFL和SIG_IGN是两个宏。
(int)handler[signal]==0 ;//执行默认动作 处理完毕
(int)handler[signal]==1;//执行忽略动作,处理完毕
如果上述两个都不满足,才会去调用函数。
handler[signal]();
handler里面的参数:
1. SIG_IGN:忽略该信号
2. SIG_DFL:执⾏系统默认动作
3. 处理函数名:⾃定义信号处理函数
2. sigset_t 类型
由于信号需要的是标记位图,非0即1。操作系统给了我们一个新的数据类型sigset_t,用户可以直接使用该类型与自定义类型和内置类型没有区别。它本质上就是一个位图,但是不允许用户进行位操作(~ / & / |),添加/修改位图的值需要通过OS系统提供的接口。
一个sigset_t 类型被称为信号集。
3.信号集操作函数
#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() 初始化该信号集,全部置0。
- sigfillset() 初始化该信号集,全部置1。
- sigaddset() 将对应信号编号的比特位置1。
- sigdelset() 将对应信号编号的比特位置0 。
- sigismember() 查看在set中 signo编号信号状态是0还是1 。
3.1 sigprocmask
调用sigprocmask可以读取或者更改进程的阻塞信号集
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//记得传 &set &oset
返回值:若成功则为0,若出错则为-1
set : 是我们自己通过sigaddset() 和 sigdelset() 设置好的 希望操作的信号形成的信号集
oset : 是一个输出型参数。方便我们需要恢复之前的信号屏蔽集。
how参数的选项及说明
SIG_BLOCK | set包含了我们希望添加到阻塞信号集的信号 mask=mask|set |
SIG_UNBLOCK | set包含了我们希望从阻塞信号集解除的信号 mask=mask&~mask |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当 于mask=set |
使用
int main()
{
sigset_t bset,obset;
sigset_t pending;
sigemptyset(&bset);
sigemptyset(&obset);
sigaddset(&bset,2);//添加想要操作的(2号)信号
int n=sigprocmask(SIG_BLOCK,&bset,nullptr);//将2号信号设为阻塞状态
//obset里面保存的没修改之前的信号集
assert(n==0); //不为0直接报错
(void)n;
int m=sigprocmask(SIG_SETMASK,&obset,nullptr);
//因为obset 只设置了2号信号 所以可以接着用以解除2号信号的屏蔽
assert(m==0);
(void)m;
return 0;
}
3.2 sigpending
#include <signal.h>
int sigpending(sigset_t* set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
static void showPending(sigset_t &pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
int main()
{
sigset_t pending;
sigemptyset(&pending);
sigaddset(&bset, 2);
int n = sigprocmask(SIG_BLOCK, &bset, &obset);
assert(n == 0);
(void)n;
while (true)
{
//获取当前进程的pending信号集
sigpending(&pending);
showPending(pending);
sleep(1);
}
return 0;
}
4.验证问题
4.1 对所有信号进行捕捉 如何终止程序
写一个for循环对每个信号都进行捕捉。
void catchSig(int signum)
{
std::cout << "获取一个信号: " << signum << std::endl;
}
int main()
{
for(int sig = 1; sig <= 31; sig++) signal(sig, catchSig);
while(true) sleep(1);
return 0;
}
结论 9号信号无法被捕捉
Bash: kill -9 pid
依然可以杀死程序
4.2 观察pending信号集的变化
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <assert.h>
//请放到Linux环境下执行代码
static void handler(int signum)
{
std::cout << "捕捉 信号: " << signum << std::endl;
// 不要终止进程,exit
}
static void showPending(sigset_t& pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
int main()
{
//捕捉2号信号
signal(2, handler);
sigset_t bset, obset,pending;
//初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
//添加要进行屏蔽的信号
sigaddset(&bset, 2 /*sigint*/);
//设置set到内核中
int n = sigprocmask(SIG_BLOCK, &bset, &obset);
assert(n == 0);
(void)n;
std::cout << "block 2 号信号成功...., pid: " << getpid() << std::endl;
// 打印当前进程的pending
int count = 0;
while (true)
{
//获取pending信号集
sigpending(&pending);
//显示pending信号集中
showpending(pending);
sleep(1);
count++;
if (count == 20)
{
std::cout << "解除对于2号信号的block" << std::endl;
int n = sigprocmask(SIG_SETMASK, &obset, nullptr);
assert(n == 0);
(void)n;
}
}
}
运行结果
4.3 阻塞所有信号 如何终止程序
结论 无法阻塞9号信号和19号信号
5. 信号的处理时间
信号相关的数据字段是在进程pcb内部,pcb内部的数据属于内核数据,在内核范畴,只有当用户进入内核状态时,我们才有权限进行修改。那我们究竟是如何在内核态和用户态之间相互切换并对信号进行处理的呢?
在内核态中,从内核态返回用户态时,进行信号检测和处理。
5.1 内核态和用户态
用户态是一个受管控的状态。
内核态是一个操作系统执行自己代码(系统调用等)的一个状态,具备非常高的优先级。
如何进入内核态
进行了系统调用、缺陷陷阱、异常、时间片结束被OS调度等。(由于调度的原因,进程类似的会周期性的进入内核态以切换)
操作系统是为了用户而服务的,所以执行用户的代码为主。所以操作系统势必回到用户态。
5.2 内核地址空间
进程地址空间分为 用户地址空间(0~3G)和内核地址空间(3~4G)。
用户地址空间是根据进程的不同而不用的(上下文数据,开辟的空间等),但是操作系统是惟一的,并不是给每个进程都打开一个操作系统。所以还有一张内核级页表,可以被所有进程看到,只有一份就够了,都指向物理内存的同一部分。
当我们需要调用系统接口(open)时,从函数调用跳转为内核态,在内核地址空间找到存储位置,通过页表映射找到物理内存的地点。
5.3 信号处理的流程
用一张图来表示用户态和内核态之间的切换
交点就是发生状态的改变。
调用自定义信号处理函数时为什么要回到用户态?内核态就有能力去执行用户的代码。
怕用户写的自定义函数有非法操作以至于程序出错。——操作系统不相信任何人,不能用内核态执行用户的代码。
调用自定义函数之后,为什么不能从用户态直接转回main函数?
当我们执行完信号处理工作后,仍有问题需要解决,比如修改pending信号集等需要操作系统来完成的善后工作。
我们进程在哪里调了接口同时会将上下文数据写给操作系统,要回到main函数的哪个位置,这些属性数据都在操作系统那里。
6. sigaction函数对信号的捕捉
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
调用成功则返回0,出错则返回- 1
signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。
act和oact指向sigaction结构体 。
sigaction 结构体
实验代码及运行结果
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signum)
{
cout<<"获取了一个信号"<<signum<<endl;
}
int main()
{
//内核数据类型 是在用户栈定义的 记得需要传给操作系统
struct sigaction act,oact;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
act.sa_handler=handler;
//捕获二号信号 设置当前调用进程的pcb中
sigaction(2,&act,&oact);
cout<<"default action:"<<(int)(oact.sa_handler)<<endl;
while(true) sleep(1);
return 0;
}
注意:
g++ -o $@ $^ -std=c++11 -fpermissive
一定要加 -fpermissive 不然报错无法形成可执行程序
如果在程序前面加上
signal(2,SIG_IGN);
6.1 程序连着收到同一个信号
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字。
这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
6.1.1 测试代码及结果
void handler(int signum)
{
cout<<"获取了一个信号"<<signum<<endl;
cout<<"获取了一个信号"<<signum<<endl;
cout<<"获取了一个信号"<<signum<<endl;
cout<<"获取了一个信号"<<signum<<endl;
cout<<"获取了一个信号"<<signum<<endl;
sigset_t pending;
int c=10;
while(true)
{
sigpending(&pending);
showPending(pending);
c--;
if(!c) break;
sleep(1);
}
}
信号收到2号信号后,开始处理2号信号,再次发送2号信号,pending位图由0置1,待第一次的二号信号处理完毕,再开始处理第二次的2号信号。所以出现pending位图 0->1->0的过程以验证了阻塞。
6.2 sa_mask
7.可重入函数
链表头插进行一半,OS处理信号时,信号的自定义处理方法又调用了头插函数,这样导致的程序出现的错误。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
8.volatile
有时候编译器自动优化会很厉害,我们如果在代码中没有设计对一个变量flag的改变(-- 、++、赋值),高级的自动优化会将第一次读到的flag数值放在寄存器中,不再通过内存来读取flag的数值。而如果我们需要信号的方式,来改变flag,使进程有不同的结果。当前这种优化就是负面的。
volatile关键字
保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量
的任何操作,都必须在真实的内存中进行操作