通过我们上一篇文章的学习,我们知道信号的生命周期包括四个阶段:预备、信号产生、信号保存、信号处理
。同时我们还接触到了信号的保存位置:信号被保存在进程的 task_struct
中。知道了信号发送的本质就是修改进程 task_struct中的位图结构
。
在本片文章中,我将带领大家更加深入学习信号阻塞、捕捉、保存的知识。
对于信号的发送我们还要树立两点共识:1.信号发送是以操作系统为载体,向目标进程发送信号的。2.因为我们的信号不会被立即处理,因此信号产生和信号递达之间就会产生一个简单的时间窗口,在这个时间窗口中,信号已经收到了但是没有被立即处理,因此我们需要将信号保存起来。
pending位图
、block位图
、指向 hander函数指针数组的指针
。pending位图
默认为0,它可以表示为32个比特位,比特位的位置表示信号编号,比特位的内容(0 or 1)表示的是是否收到该信号。block位图
默认也为0,它也可表示32个比特位,比特位的位置表示信号编号,比特位的内容表示 是否阻塞该信号。指针指向 hander函数指针数组
,我们可以把它简称为 hander表
,数组的下标表示信号的编号,数组下标对应的函数内容表示 对应信号的处理方法。总结:1.如果一个信号没有产生,并不妨碍它被阻塞。2.进程为什么能识别信号?因为每个信号都有自己对应的 pending位图、block位图 和 hander表。
图示2:
通过上面的学习,我们知道:信号产生的时候,不会被立即处理,而是会在合适的时候被处理。
那么问题来了,究竟合适的时候是什么时候呢?答案是:从内核态返回用户态的时候,进行处理。
用户态
和 内核态
。 CR3
的寄存器,它表征当前进程的运行级别:0-内核态,3-用户态。用户级页表
和 内核级页表
。总结:1.用户访问OS的过程:运行到特定代码 -> 系统调用(起始位置会更改CR3寄存器)-> 查看CR3寄存器(确认运行状态) -> 跳转到内核空间进行访问 -> 访问完成 -> 更改CR3寄存器 -> 返回并继续执行下一行代码。
用户态 -> 内核态
)。内核态 -> 用户态
)用户态 -> 内核态
)。内核态 -> 用户态
)。信号捕捉巧记图:红色圆圈代表操作,绿色圆圈代表状态切换(4个操作 + 4次状态切换),如果信号的执行方法为默认或者忽略,则不会再沿图示路径进行下去。
通过学习信号的捕捉过程,我们就可以更加深入理解到本节开头时候的话:信号产生的时候,不会被立即处理,而是会在合适的时候被处理,即从内核态返回用户态的时候。
综合我们学习的知识,我们可以得出:信号产生之后不会立即递达,而是会在合适的时候递达,因此我们的信号在这个时间周期内需要被保存。信号被保存在进程的 task_struct
中,信号发送(保存)的本质就是修改进程 task_struct中的位图结构
。
这里我们再复习一下信号递达和信号未决的知识点,方便后面的学习:
sigset_t
来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作 sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
#include
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置为1,表示该信号集的有效信号包括系统支持的所有信号。sigemptyset
或 sigfifillset
做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用 sigaddset
和 sigdelset
在该信号集中添加或删除某种有效信号。sigismember
是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。调用函数 sigprocmask
可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
oset
是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。set
是非空指针,则更改进程的信号屏蔽字。how
指示如何更改。#include
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
总结:sigprocmask - 修改block位图(阻塞信号集/信号屏蔽字),sigpending - 获取pending位图(未决信号集),signal - 修改信号处理方法。
#include
#include
#include
#include
// #define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31
using namespace std;
// static vector sigarr = {2,3};
static vector<int> sigarr = { 2 };
static void show_pending(const sigset_t& pending)
{
for (int signo = MAX_SIGNUM; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
cout << "1";
}
else cout << "0";
}
cout << "\n";
}
static void myhandler(int signo)
{
cout << signo << " 号信号已经被递达!!" << endl;
}
int main()
{
for (const auto& sig : sigarr) signal(sig, myhandler);
// 1. 先尝试屏蔽指定的信号
sigset_t block, oblock, pending;
// 1.1 初始化
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
// 1.2 添加要屏蔽的信号
for (const auto& sig : sigarr) sigaddset(&block, sig);
// 1.3 开始屏蔽,设置进内核(进程)
sigprocmask(SIG_SETMASK, &block, &oblock);
// 2. 遍历打印pengding信号集
int cnt = 10;
while (true)
{
// 2.1 初始化
sigemptyset(&pending);
// 2.2 获取它
sigpending(&pending);
// 2.3 打印它
show_pending(pending);
// 3. 慢一点
sleep(1);
if (cnt-- == 0)
{
sigprocmask(SIG_SETMASK, &oblock, &block); // 一旦对特定信号进行解除屏蔽,一般OS要至少立马递达一个信号!
cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
}
总结:我们可以通过信号集操作函数初始化信号集,并将需要屏蔽的信号加入屏蔽信号集中,然后用 sigprocmask函数将信号集内容射入内核,然后通过 sigpending函数查看 pending信号集。上面的示例显示,当我们屏蔽2号信号之后,我们输入 ctrl+C 后会将信号存储于 pending信号集中,而不会递达,即不会执行 signal函数中的 myhandler方法。
#include
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
知识点1:
sigaction
函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo
是指定信号的编号。act
指针非空,则根据act修改该信号的处理动作。oact
指针非 空,则通过oact传出该信号原来的处理动作。sa_handler
赋值为常数SIG_IGN
传给sigaction表示忽略信号,赋值为常数SIG_DFL
表示执行系统默认动作,赋值为一个函数指针
表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。知识点2:
sa_mask
字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flflags/sa_flags
字段包含一些选项,本章的代码都把sa_flflags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。 #include
#include
#include
#include
using namespace std;
void Count(int cnt)
{
while(cnt)
{
printf("cnt: %2d\r", cnt);
fflush(stdout);
cnt--;
sleep(1);
}
printf("\n");
}
void handler(int signo)
{
cout << "get a signo: " << signo << "正在处理中..." << endl;
Count(20); //调用计时程序
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask); // 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
sigaddset(&act.sa_mask, 3); //对3号信号也添加屏蔽
sigaction(SIGINT, &act, &oact); //SIGINT为2号信号
while(true) sleep(1);
return 0;
}
总结:代码运行时,在第一个信号递达过程中(计数器开始计时),我们再向该进程发送2号信号则无法递达,第二次发送的2号信号将被保存在 pending位图中,等待第一次发送的信号递达完成之后才会执行对应方法,第3、4…次的信号发送均会失效/丢失。
insert函数
向一个链表head中插入节点node1,插入操作分为两步(如上图insert代码所示),刚做完第一步的 时候,因为硬件中断(该进程的时间片到了)使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数
,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。不可重入函数
,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数
。如果一个函数符合以下条件之一则是不可重入的:
[ldx@localhost code_test]$ cat sig.c
#include
#include
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag);
printf("process quit normal\n");
return 0;
}
[ldx@localhost code_test]$ cat Makefile
sig : sig.c
gcc -o sig sig.c #-O2 #使用#号屏蔽优化,02为优化级别
.PHONY : clean
clean :
rm - f sig
[ldx@localhost code_test]$ ./sig
^ Cchage flag 0 to 1
process quit normal
标准情况下,键入
Ctrl-C
,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不满足,退出循环,进程退出。
[ldx@localhost code_test]$ cat sig.c
#include
#include
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag);
printf("process quit normal\n");
return 0;
}
[ldx@localhost code_test]$ cat Makefile
sig : sig.c
gcc -o sig sig.c -O2 #放开屏蔽,设置优化级别02
.PHONY : clean
clean :
rm - f sig
[ldx@localhost code_test]$ ./sig
^ Cchage flag 0 to 1
^ Cchage flag 0 to 1
^ Cchage flag 0 to 1
我们的代码在编译过程中,编译器会对其进行优化,优化有不同级别,优化情况下,键入
Ctrl-C
,2号信号被捕捉,执行自定义动作,修改flag=1
,但是while
条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显while
循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。while
检测的flag其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要volatile
。
[ldx@localhost code_test]$ cat sig.c
#include
#include
volatile int flag = 0; //在全局变量前加volatile关键字
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag);
printf("process quit normal\n");
return 0;
}
[ldx@localhost code_test]$ cat Makefile
sig : sig.c
gcc - o sig sig.c - O2
.PHONY : clean
clean :
rm - f sig
[ldx@localhost code_test]$ . / sig
^ Cchage flag 0 to 1
process quit normal
volatile
作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
我们需要根据实际应用场景(优化级别比较高且存在需要更新的判断变量),判断我们是否需要添加
volatile
关键字。
下面是信号的知识点汇总,方便大家对应回顾
信号的预备,信号的基本概念。
信号的产生,信号的产生方法,发送本质。
信号捕捉(用户态内核态 & OS接口的访问方法 & 捕捉过程)
信号的保存,保存位置,保存方法,未决与递达的概念,信号阻塞,信号集及其操作,修改信号屏蔽字的方法,查看pending位图的方法,多次发送同一信号的现象。
信号处理,信号递达。
信号阻塞 & 信号捕捉 & 信号的保存 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!