前言:本节内容主要是一些linux信号的周边知识或者补充知识。 对于信号的学习, 学习了信号概念, 产生, 保存与捕捉就已经算是认识我们的信号了。 如果想要知道更多关于信号的知识也可以看一下本篇文章。
ps:本篇内容适合了解信号的友友们进行观看哦
目录
sigaction
信号递达后pending位图清零的时机
信号处理时的屏蔽
sa_mask
函数重入
volatile关键字
SIGCHLD
sigaction
这里面第一个参数signum就类似于signal里面的signum, 都是用来重新设置某个信号的处理动作。 第二个参数我们会发现它的类型名称和我们的函数名称是一样的, 第三个参数也是这个类型。 那么这里的第二个参数其实是一个输入型参数。 第三个参数是一个输出型参数。其中第二个参数就是传送我们自己定义的自定义捕捉方法。 第三个参数就是将对应的信号的老的处理方法给我们传出来。
然后我们看一下sigaction类型的定义:
上面就是sigaction的定义, 但是里面有许多成员都是和实时信号相关的, 我们不关心。 我们只关心第一个和第三个成员变量。 其中第一个就是我们将来捕捉信号要执行的处理方法。
信号递达后pending位图清零的时机
当我们在处理某个信号的时候, 内核会将当前信号的pending位图由1清零。问题是, 是我们处理之前清零呢, 还是处理之后清零呢 ? 这个我们就可以在handler函数里面打印一下我们的pending位图, 就能观察到到底是在哪里清零的, 下面的实验方法就是——我们ctrl + C, 这个时候给进程发送信号pending位图2号位被置为1. 此时我们处理这个信号, 我们在捕捉信号里面打印pending位图, 如果是处理之前清零, 那么就打印一串0, 但是如果是处理之后清零, 那么就打印一串1。下面为代码:
代码:
#include<iostream> #include<cstring> #include<signal.h> #include<unistd.h> using namespace std; void PrintPending() //打印位图结构 { sigset_t set; sigpending(&set); for (int signo = 1; signo <= 31; signo++) { if (sigismember(&set, signo)) cout << "1" << endl; else cout << "0"; } cout << endl; } void handler(int signo) { PrintPending(); cout << "catch a signal: " << signo << endl; } int main() { struct sigaction act, oact; //这里可以省略struct,不省略是因为能够提醒我们是系统结构体。 memset(&act, 0, sizeof(act)); memset(&oact, 0, sizeof(oact)); act.sa_handler = handler; sigaction(2, &act, &oact); while (true) { cout << "I am a process: " << getpid() << endl; sleep(1); } return 0; }
运行结果:
信号处理时的屏蔽
那么, 这里还有一个问题就是操作系统会将当前的信号加入到信号屏蔽字当中。 当信号处理函数返回的时候, 自动恢复原来的信号屏蔽字。也就是说, 当某个信号在被处理的时候, 是不能再次被递达的。 防止信号捕捉的时候, 发生各种嵌套调用。
那么如何证明信号捕捉的时候, 这个信号确实被屏蔽了呢? 这里我们用一个程序来进行实验。 ——实验的流程就是我们的处理信号里面是一个死循环, 循环打印pending位图。 然后循环外会打印一句捕捉到信号。如果我们执行2号信号时没有屏蔽2号信号, 那么我们发送几次2号信号, 那么都应该被递达, 那么捕捉到信号就会频发的打印。 但是如果屏蔽的话就应该不会被递达, 也就只打印一句捕捉到信号:
#include<iostream> #include<cstring> #include<signal.h> #include<unistd.h> using namespace std; void PrintPending() //打印位图结构 { sigset_t set; sigpending(&set); for (int signo = 1; signo <= 31; signo++) { if (sigismember(&set, signo)) cout << "1" << endl; else cout << "0"; } cout << endl; } void handler(int signo) { cout << "catch a signal: " << signo << endl; while (true) { PrintPending(); } } int main() { struct sigaction act, oact; //这里可以省略struct,不省略是因为能够提醒我们是系统结构体。 memset(&act, 0, sizeof(act)); memset(&oact, 0, sizeof(oact)); act.sa_handler = handler; sigaction(2, &act, &oact); while (true) { cout << "I am a process: " << getpid() << endl; sleep(1); } return 0; }
运行结果:
可以看到只有第一次ctrl + C递达了信号, 其他时候的信号都没有被递达。 并且第二次ctrl + c后pending表2号为变成了1, 说明这个信号此时未决状态。 也可以推测被屏蔽了。
sa_mask
另外sigaction里面还有一个成员变量交sa_mask。 这个sa_mask是sigset_t类型, 如哦正在处理2号信号, 那么2号信号就会自动被系统屏蔽。 如果我还想屏蔽更多信号呢? 也就是说, 我们在捕捉2号期间, 如果sa_mask默认, 那么就只会屏蔽2号信号。 但是如果我们想要屏蔽更多的信号, 就要用sa_mask进行设置。
代码:
#include<iostream> #include<cstring> #include<signal.h> #include<unistd.h> using namespace std; void PrintPending() //打印位图结构 { sigset_t set; sigpending(&set); for (int signo = 1; signo <= 31; signo++) { if (sigismember(&set, signo)) cout << "1"; else cout << "0"; } cout << endl; } void handler(int signo) { cout << "catch a signal: " << signo << endl; while (true) { PrintPending(); sleep(1); } } int main() { struct sigaction act, oact; //这里可以省略struct,不省略是因为能够提醒我们是系统结构体。 memset(&act, 0, sizeof(act)); memset(&oact, 0, sizeof(oact)); act.sa_handler = handler; sigemptyset(&act.sa_mask); //先清空一下samask sigaddset(&act.sa_mask, 1); //添加1号比特位为1 sigaddset(&act.sa_mask, 3); //添加1号比特位为1 sigaction(2, &act, &oact); while (true) { cout << "I am a process: " << getpid() << endl; sleep(1); } return 0; }
运行结果:
然后我们就会发现, 处理2号信号的时候, 1, 3号信号都被屏蔽了。
函数重入
假如我们有下面的节点定义, 头插函数和自定义捕捉动作
现在我们有下面这个链表
现在我们执行insert。
此时的链表结构是这样的:
因为信号捕捉又是插入一个新节点:
所以, 就会再次进入insert函数插入新节点。那么此时的链表节点就是这样的:
最后, 信号捕捉完毕后, 再将原本的insert函数执行完, 就变成了最终结果, 如下:
以上这种情况就是一个函数被重复进入了, 也叫做函数重入。 并且, 上面的listnode3我们称为节点发生了丢失。 即因为节点丢失导致的内存泄漏问题。
上面有两个问题, 这两个问题合起来就是, 函数重入导致的节点丢失问题。 而我们把这种重入后会有问题的函数称为不可重入函数。即: 如果一个函数被重复进入的情况下, 出错了, 或者可能出错, 我们就把他叫做不可重入函数。 否则叫做可重入函数。 可不可重入是特点, 不含褒贬。 目前我们学过的大部分函数, 都是不可重入的。
volatile关键字
volatile在信号的角度怎么理解呢? 这里我们通过一串代码来进行理解。 首先写下下面的代码:
int flag = 0;
int main()
{
while (!flag);
cout << "process quit success" << endl;
return 0;
}
对于这串代码, 这串代码正常情况下是不能退出的, 会一直死循环。但是我们这里重新定义一下2号信号的处理方法:
int flag = 0;
void handler(int signo)
{
cout << "catch a signal: " << signo << endl;
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag);
cout << "process quit success" << endl;
return 0;
}
这样, 如果我们捕捉2号后, flag被置为1. 那么就应该退出循环, 然后打印程序退出成功。 那么真实情况下是这样吗?
确实是我们预测的这样。 但是, 在极端情况下, handler和main是属于不同的执行流的。 编译器在编译的过程中会发现在main函数中, 没有任何地方去修改flag, 因为我们的while循环只是在对flag做检测, 并没有任何地方去修改。 所以编译器就可能会对flag做优化, 在优化条件下, flag变量就有可能被直接优化到cpu的寄存器当中, 为什么? 因为我们的flag在一个执行流中没有被修改; 而且, 我们也知道, while循环本身就能进行判断, 这种判断是一种计算, 计算本身都是在cpu中进行的。 而且cpu只会进行两种计算, 一种叫做算数计算, 一种叫做逻辑计算。 而这里的while判断就是逻辑运算, 所以就必须把flag放到cpu中进行计算。 那么flag既不会被修改, 又会到cpu中进行逻辑运算。那么flag就有可能会被优化到寄存器当中。
那么我们如何控制它进行优化呢?那么我们打开man g++, 然后/-O, 就能看到下面这个:
有-O1, -O2等等。 这里面的123是优化级别。 -O就是控制优化等级。
这里我们直接控制优化等级3, 观察效果:
我们就会发现程序退出不了了。 为什么呢? 正常来说, 我们的2号信号被捕捉后, flag置为1. 逻辑反就是0, 那么程序就应该被退出。 为什么没有退出呢?
那么我们看下面这张图:
左边是cpu, 右边是物理内存。 我们需要知道的是, 不管怎么优化, 我们对应的变量, 一定会在内存里面开辟。 我们一开始cpu做检测, 就是将内存里面的变量加载到寄存器, 然后做逻辑检测, 检测之后有结果了再控制逻辑。 但是现在不做这个工作了, 只是在第一次的时候将flag直接放到寄存器里, 然后cpu从寄存器里面直接计算。 也就是说,我们的cpu计算flag不再从内存里面拿了, 而是先保存到寄存器中,以后做检测直接从寄存器里面拿。 问题是这样对于flag如果不做修改会有用, 但是我们的flag会做修改。 flag后续修改后cpu并不会检测到, 所以我们的程序就一直死循环了。
而volatile的核心作用就是防止编译器过度优化, 保持内存的可见性!!!凡是用volatile做修饰的变量, 就是在警告编译器不要做任何优化。
SIGCHLD
子进程退出并不是单纯的静悄悄的退出, 当它退出的时候, 它会向父进程发送信号, 这个信号就是SIGCHLD(17号。 怎么证明会发送这个信号呢? ——是不是我们父进程对17号信号进行捕捉, 然后创建一个子进程, 等到子进程退出的时候看看会不会捕捉到17号信号就可以了? 所以, 下面为代码:
void handler(int signo)
{
cout << "catch a signal: " << signo << endl;
}
int main()
{
signal(17, handler);
pid_t id = fork(); //子进程返回0, 父进程返回子进程pid
if (id == 0)
{
while (true)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(1);
break;
}
exit(0);
}
while (true)
{
cout << "I am a father process: " << getpid() << endl;
sleep(1);
}
return 0;
}
然后我们就会看到运行结果, 一秒过后, 父进程就受到了一个signal信号!
所以, 我们就知道, 子进程在等待的时候, 我们可以采用基于信号的方式进行等待!等待的好处是什么?
- 可以获取子进程的退出状态。
- 释放子进程的僵尸。
- 通过阻塞或者非阻塞轮询, 我们虽然不知道父子进程谁先运行, 但是父进程一定是最后退出的!
今天的基于信号方式进行等待还是要调用wait/waitpid这样的接口。 父进程一直得保证自己是一直在运行的(子进程不要孤儿) 那么如何做? ——就是把子进程等待写入信号的捕捉函数当中!!
我们利用下面的代码进行验证,我们想要的现象是前五秒程序父进程在跑, 子进程也在跑。 然后子进程退出, 接下来的五秒子进程变成教室, 父进程还在跑。 最后子进程被回收!
void handler(int signo)
{
sleep(5);
pid_t rid = waitpid(-1, nullptr, 0);
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << "catch a signal: " << signo << endl;
}
int main()
{
signal(17, handler);
pid_t id = fork(); //子进程返回0, 父进程返回子进程pid
if (id == 0)
{
while (true)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
while (true)
{
cout << "I am a father process: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果:
我们就会发现中间正好五秒僵尸继承, 和我们预想的是一样的。
但是, 如果有多个子进程呢? 比如有10个子进程, 那么我们如果有一个子进程先退出。 然后捕捉信号, 然后其他子进程再一起退出。 但是这个时候17号信号屏蔽了。 所以其他子进程的信号阻塞了, 丢失了, 所以也就意味着我们最终只能够回收一两个进程。 其他的进程无法被回收。 遇到这种情况, 怎么才能正常处理呢? ——我们只要使用一个循环加非阻塞轮询, 就可以成功的解决这个问题,代码如下:
void handler(int signo)
{
sleep(5);
pid_t rid;
while (pid_t rid = waitpid(-1, nullptr, WNOHANG) > 0)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << "catch a signal: " << signo << endl;
}
}
int main()
{
signal(17, handler);
pid_t id = fork(); //子进程返回0, 父进程返回子进程pid
if (id == 0)
{
while (true)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
while (true)
{
cout << "I am a father process: " << getpid() << endl;
sleep(1);
}
return 0;
}
这样, 我们的程序就能够将多个进程同时回收了!
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!