👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、可重入函数
- 二、volatile关键字
- [三、SIGCHLD 信号](#三、SIGCHLD 信号)
-
-
- [3.1 概念](#3.1 概念)
- [3.2 基于信号的方式回收子进程](#3.2 基于信号的方式回收子进程)
- [3.3 多个子进程回收](#3.3 多个子进程回收)
- [3.4 基于3.3更加优雅的方案](#3.4 基于3.3更加优雅的方案)
-
- 四、相关代码
一、可重入函数
-
main
函数调用insert
函数向一个链表head
中插入节点node1
,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换sighandler
函数。 -
sighandler
也调用insert
函数向同一个链表head
中插入节点node2
。 -
插入操作的两步都做完之后从
sighandler
返回内核态,再次回到用户态就从main
函数调用的insert
函数中继续往下执行,也就是开始执行head=p
。当做完这步时,我们发现:只有一个节点node1
真正插入链表中了,而另一个节点丢失了,最终导致了内存泄漏了。
因此,我们可以得出以下概念
可以被重复进入的函数称为可重入函数
如果一个函数被重复进入的情况下,出现出错了或者可能出错,那么我们称这个函数叫做不可重入函数
如果一个函数符合以下条件之一则是不可重入的:
- 调用了内存管理相关函数,诸如
malloc
、new
等。- 调用了标准
I/O
库函数#include <iostream>
,因为其中很多实现都以不可重入的方式使用数据结构。
了解概念即可,在线程部分我们会经常提到 ~
二、volatile关键字
借助全局变量flag
设计一个死循环的场景,在此之前将2
号信号进行自定义动作捕捉,具体动作为:将flag
改为 1,可以终止main
函数中的循环体
cpp
#include <signal.h>
#include <iostream>
using namespace std;
int flag = 0;
void handler(int signum)
{
cout << "捕捉" << signum << "号信号" << endl;
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag)
{
;
}
cout << "进程正常退出" << endl;
return 0;
}
【程序结果】
一切都符合我们的预期。2
号信号发出后,循环结束,程序正常退出。
这段代码能符合我们预期般的正确运行是因为当前编译器默认的优化级别很低,没有出现意外情况。
那什么是意外情况呢,往下看这四点:
-
handler
和main
属于两个不同的执行流,即它们之间并没有调用和被调用关系。 -
因此,编译器在编译
main
函数时,发现没有对flag
变量进行修改,而while
循环仅仅是对flag
做检测,并且这个检测是在做逻辑运算,那么就要将flag
值加载到CPU
寄存器上,然后再CPU
进行计算。 -
通过以上两个条件:①
flag
值被加载到CPU
寄存器上。②flag
值没有被修改。那么有可能编译器在编译的时候会对flag
进行优化。 -
在优化条件下,
flag
变量可能被直接优化到CPU
内的寄存器中,这样循环中对flag
的检查将只会读取寄存器中的值,而不是每次都从内存中读取。这种优化是为了提高程序的性能。注意:内存中也会为flag
变量开辟空间。
那么在Linux
中如何进行优化呢?
通过指令查询gcc
优化级别的相关信息
bash
# g++和gcc都可以
man g++
/-O1
数字越大,优化级别越高。注:默认优化级别是-O0
。
让我们重新编译上面的程序,并指定优化级别为O1
编译成功后,再次运行程序
此时得到了不一样的结果:这是因为编译器做了优化。2
号信号捕捉后,将flag
变量修改为1
,这个修改只是在内存中修改,并没有在CPU
的寄存器修改。而每次循环中对flag
的检查只会读取寄存器中的值,那么while
循环判断始终为真,导致死循环。因此完全符合我们以上的理论。
所以我们为了防止这样编译器的过度优化,导致内存不可见 。我们可以给这个变量带上 volatile
关键字。
volatile
是一个关键字。主要有两个方面的作用:
- 通常用于告诉编译器,某个变量可能会在程序的其他地方被意外修改,因此编译器不应该对这个变量进行优化。
- 保证内存的可见性。
- 总结一句话:防止编译器过度优化,保证内存的可见性。
所以我们代码改为如下:
cpp
#include <signal.h>
#include <iostream>
using namespace std;
volatile int flag = 0;
void handler(int signum)
{
cout << "捕捉" << signum << "号信号" << endl;
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag)
{
;
}
cout << "进程正常退出" << endl;
return 0;
}
【程序结果】
三、SIGCHLD 信号
3.1 概念
在进程控制时,不可否认的是:父进程必须等待子进程退出后回收子进程,主要是避免子进程变成僵尸进程占用系统资源、造成内存泄漏。
那么父进程是如何知道子进程退出了呢?
在之前的讲解中,父进程waitpid()
要么就是设置为阻塞式专心等待,要么就是设置为WNOHANG
非阻塞式等待,这两种方法都需要父进程主动去检测子进程的状态。
如今学习了进程信号相关知识后,可以思考一下:难道子进程真的是静悄悄的退出的吗?还是真的依靠父进程的每次询问?
答案当然不是,子进程在退出后,会向父进程发送17
号信号SIGCHLD
。子进程而是通过发送17
号信号通知父进程,我要退出了。这样可以解放父进程,不必再去主动检测。
我们可以验证一下,当子进程退出是否真的有17
号信号?
cpp
#include <signal.h>
#include <unistd.h>
#include <iostream>
using namespace std;
void handler(int signum)
{
cout << "who am I: " << getpid()
<< ", catch a signum: " << signum << endl;
}
int main()
{
signal(17, handler);
pid_t id = fork();
if (id == 0) // 子进程
{
while (true)
{
cout << "I am child: " << getpid() << ", ppid: " << getppid() << endl;
sleep(1);
break;
}
exit(17); // 打印一句后,子进程直接退出
}
// 父进程
while (true)
{
cout << "I am father: " << getpid() << endl;
sleep(1);
}
return 0;
}
【程序结果】
如上足可以证明SIGCHLD
是被子进程发出的!
3.2 基于信号的方式回收子进程
所以利用这个17
号信号,可以采用基于信号的方式进行等待!
回顾等待的好处:
- 获取子进程的退出状态,释放子进程的僵尸。
- 虽然不知道父子谁先运行,但是我们清楚,一定是
father
最后退出。
所以还是要调用wait/waitpid
这样的接口。并且基于信号的方式进行等待还要保证父进程不能退出。
接下来,我们可以将子进程等待方法写入到信号捕捉函数中!
cpp
#include <signal.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void handler(int signum)
{
pid_t rid = waitpid(-1, nullptr, 0);
cout << "who am I: " << getpid()
<< ", catch a signum: " << signum
<< ", 子进程" << rid << "回收成功!" << endl;
}
int main()
{
signal(17, handler);
pid_t id = fork();
if (id == 0) // 子进程
{
while (true)
{
cout << "I am child: " << getpid()
<< ", ppid: " << getppid() << endl;
sleep(5);
break;
}
cout << "子进程退出..." << endl;
exit(17); // 打印一句后的5秒,子进程直接退出
}
// 父进程
while (true) // 父进程不能退出
{
cout << "I am father: " << getpid() << endl;
sleep(1);
}
return 0;
}
【程序结果】
3.3 多个子进程回收
虽然成功了,但还是有点缺陷。以上代码是在只有一个子进程的场景,代码没问题,但如果是涉及多个子进程回收时,这个代码就有问题了。
原因是:SIGCHLD
是一个信号,在信号处理中我们验证过(如下)
如果多个子进程同时退出,可能只有多个进程中的某一个进程在执行handler
方法,那么执行的过程中就已经将这个信号屏蔽了,导致(n - 1)
个进程无法被回收,都是僵尸进程,造成内存泄漏。
-
解决方案:自定义捕捉函数中,采取
while
循环式回收,有很多进程都需要回收没问题,排好队一个个来就好了,这样就可以确保多个子进程同时发出SIGCHLD
信号时,可以做到一一回收 -
细节:多个子进程运行时,可能有的退了,有的没退(不是同时退),这会导致退了的子进程发出信号后,触发自定义捕捉函数中的循环等待机制,回收完已经退出了的子进程后,会阻塞式的等待还没有退出的子进程,如果子进程一直不退,就会一直被阻塞,所以我们需要把进程回收设为
WNOHANG
非阻塞式等待。
cpp
void handler(int signum)
{
pid_t rid;
while ((waitpid(-1, nullptr, WNOHANG)) > 0)
{
cout << "who am I: " << getpid()
<< ", catch a signum: " << signum
<< ", 子进程" << rid << "回收成功!" << endl;
}
}
3.4 基于3.3更加优雅的方案
其实还有一种更加优雅的子进程回收方案
由于 UNIX
历史原因,要想子进程不变成僵尸进程,可以把 SIGCHLD
的处理动作设为 SIG_IGN
忽略,这里的忽略是个特例,只是父进程不对其进行处理,转变成由操作系统对其负责,自动清理资源并进行回收。
也就是说,直接在父进程中使用 signal(SIGCHLD, SIG_IGN)
就可以优雅的解决子进程回收问题,父进程相当于啥也不用干(前提是你不想获取子进程的退出信息)。
注意:
SIGCHLD
的默认处理动作SIG_DFL
是忽略(什么都不做),而忽略动作SIG_IGN
是让操作系统帮忙回收,父进程不必关心。- 此方法对于
Linux
可用,但不保证在其它UNIX
系统上都可用。
四、相关代码
本篇博客的代码:点击跳转