文章目录
- 可重入函数
- volatile
- volatile和const同时修饰变量
- SIGCHLD信号
可重入函数
当一个函数可以被两个流调用,我们称该函数具有重入特征
如果一个函数被重入后可能导致内存泄漏的问题,我们称该函数为不可重入函数,反之,一个函数被重入后不会导致内存泄漏,我们称该函数为可重入函数。
将一个节点new_node插入单链表分为两步,首先记录要插入位置的前后两个节点,prev和next,第一步是将new_node的next指针指向next节点,第二步时将prev的next指针指向new_node。
假设现在的main执行流正在执行insert函数,将new_node1插入单链表,在insert执行完第一步——将new_node1的next指针指向next节点后,由于进程收到一个信号,需要递达该信号,并且递达方式为自定义,包含insert函数,该insert插入一个节点new_node2,并且插入的位置的new_node1相同,信号的递达完成,new_node2被插入链表,进程将返回执行handler之前的代码,即执行insert的第二步,将prev节点的next指向new_node1,这样一来new_node1也被插入链表,由于两节点插入的位置一样,现在的实际情况是只有new_node1被插入链表,而new_node2在链表之外,这是一个明显的内存泄漏问题。
由于insert存在内存泄漏问题,所以insert是一个不可重入函数
但是可重入函数和不可重入函数函数没有优劣之分,是否能重入只是区别两个函数的方式,我们学习的大部分函数都是不可重入函数,STL库中的函数基本都是不可重入函数。由于函数重入导致的内存泄漏问题也只是在多个执行流执行进程时才会发生。
volatile
volatile属于C语言的易变关键字,其作用是告诉编译器该关键字修饰的变量可能发生变化,每次读取该变量的值时必须从内存中读取,以得到一个正确的值,防止编译器做自以为是的优化。
经过了信号的学习,可以通过一段有关信号的代码验证volatile的作用
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
// 自定义handler将flag的值设置为1
void handler(int signo)
{
flag = 1;
printf("flag->1\n");
}
int main()
{
// 设置2号信号的handler方法
signal(2, handler);
// 如果flag的值为0,程序会卡在死循环中
while (!flag);
// flag的值不为0,程序会走到printf,打印语句
printf("进程正常退出\n");
return 0;
}
这段demo根据flag的值决定程序是否会死循环,如果flag的值为0,程序死循环,flag的值不为0,程序会正常退出。flag作为一个全局变量,其初始值为0,如果不修改flag的值,程序会陷入死循环,这里可以通过捕捉2号信号,并自定义2号信号的handler方法,将flag的值设置为1,并打印"flag->1\n"这条语句。
所以一开始的程序会死循环,当按下Ctrl+C时,就意味着向前台进程发送2号信号,也就是将flag的值设置为1,程序退出死循环,打印"进程正常退出\n"这条语句。
运行这段demo,运行结果和预期相同
gcc编译器有一些高优化级别的选项,-O2表示以较高的优化级别编译这段demo,带上这个选项后,再运行程序,通过截图可以看出不论发送多少次2号信号,程序都不能退出死循环,也就是说程序认为flag的值依然为0。
导致这样结果的原因是:while信号的判断条件是!flag,也就是说判断条件只需要进行单纯的值判断,不需要进行计算,而while循环之前也没有对flag的值进行修改(signal只是设置了信号的自定义handler方法,虽然方法修改了flag的值,但编译器只能做语法检测,不能做逻辑判断,因此编译器认为在while之前,flag的值没有被修改。而循环需要不断的读取flag的值,每次从内存中读取flag的速度比每次从寄存器中读取的速度慢很多,而flag的值又没有修改,因此编译器将flag的值优化到寄存器中,这样每次的读取就能从寄存器中读取,有效的提高了程序运行的速度。
所以在寄存器中,flag的值始终为0,即使2号信号的自定义handler方法将内存中的flag值修改为1,由于cpu没有重新向内存中读取数据,所以寄存器的flag值不会因为内存的flag值改变。因此不论进程收到多少次2号信号,flag的值都为0,程序一直卡在死循环中。
所以编译的优化使得进程屏蔽了内存上的flag值,进程只可见寄存器上的flag值。而volatile的作用就是使得内存可见,强制程序每次读取flag的值都要从内存中读取。
在对flag添加了volatile修饰后,重新编译程序并带上-O2选项,可以看到,由于volatile对flag的修饰,在编译器的高优化下,2号信号的handler方法依然可以修改fla的值使程序退出死循环。
volatile和const同时修饰变量
volatile的作用是告诉编译器其修饰的变量很可能发生变化,需要编译器从内存中读取变量的值,而const的作用是告知编译器其修饰的变量不能被修改,如果有代码对const修饰的变量进行修改,编译将报错。
因此,const对于编译器来说,只需要在编译器期间检查const修改的变量是否被修改即可,const属于一种编译时就能完成语法检查的关键字。而volatile则是在程序运行起来后,才能实现其作用的关键字,编译器在编译期间无法对volatile修饰的变量做什么语法检查。
而volatile和const一起修饰一个变量,保证了该变量在后续代码中不会被修改,而在运行期间,其可能被修改,而编译器总是需要从内存中读取它的值,保证了程序不会过度优化出错。
SIGCHLD信号
当父进程fork创建子进程后,子进程退出时,会向父进程发送SIGCHLD信号,以表示子进程的退出。对于SIGCHLD信号,进程的递达方式为忽略,所以子进程的退出总是默默的,但我们可以捕捉父进程的SIGCHLD信号,并设置其自定义handler方法,以观察子进程的退出
void handler(int signo)
{
cout << "父进程接收到子进程退出的信号:" << signo << "父进程pid:" << getpid() << endl;
}
int main()
{
signal(SIGCHLD, handler);
pid_t id = fork();
if (id == 0)
{
// child
while (1)
{
cout << "我是子进程,pid:" << getpid() << endl;
sleep(1);
}
exit(1);
}
// parent
while (1)
{
cout << "我是父进程,pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
19号信号:暂停一个进程,18号信号:唤醒一个进程
可以看到,除了子进程的退出,子进程的暂停与继续都会向父进程发送SIGCHLD信号
之前回收子进程资源时,总是父进程调用waitpid以阻塞或者非阻塞的方式,提前进行子进程的等待,经过了信号的学习,我们就可以通过捕捉子进程退出时向父进程发送的SIGCHLD信号,并自定义handler方法使父进程调用waitpid回收子进程资源。
// mypro.cc
void handler(int signo)
{
// 第一个参数为-1表示等待任意的子进程
pid_t id = waitpid(-1, nullptr, 0);
if (id > 0) // 等待成功
{
cout << "父进程成功地回收了子进程资源," << "父进程pid:" << getpid() << endl;
}
}
int main()
{
signal(SIGCHLD, handler);
pid_t id = fork();
if (id == 0)
{
// child
int cnt = 5;
while (cnt)
{
cout << "我是子进程,pid:" << getpid() << "cnt:" << cnt << endl;
cnt--;
sleep(1);
}
exit(1);
}
// parent
while (1)
{
cout << "我是父进程,pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
修改代码,在父进程对SIGCHLD的handler方法中添加对子进程的回收。运行以上demo
但是如果父进程有很多的子进程,这些子进程同时退出,也就是说很多子进程同时向父进程发送SIGCHLD信号,当进程在递达一个信号时,会将该信号加入信号屏蔽字,也就是阻塞该信号,所以进程在递达信号期间最多只会收到一次信号。这就造成了有些子进程虽然退出了,但是SIGCHLD信号没有被父进程接收,导致其资源无人回收,因此子进程陷入僵尸状态,无法释放资源,造成了内存泄漏。
using namespace std;
void handler(int signo)
{
pid_t id = waitpid(-1, nullptr, 0);
if (id > 0) // 等待成功
{
cout << "父进程成功地回收了子进程资源," << "父进程pid:" << getpid() << endl;
}
}
int main()
{
signal(SIGCHLD, handler);
// 创建10个子进程,它们的退出时间可能会重叠
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
int cnt = 5;
while (cnt)
{
cout << "我是子进程,pid:" << getpid() << " cnt:" << cnt << endl;
cnt--;
sleep(1);
}
exit(1);
}
}
// parent
while (1)
{
cout << "我是父进程,pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
由于子进程在同一时间退出,可能造成有的子进程SIGCHLD信号没有被父进程接收到的情况,所以SIGCHLD的handler方法不能只回收一次子进程资源,应该多次调用waitpid回收子进程,并清理处于僵尸状态的子进程,当还有子进程在运行却没有退出时,父进程不再调用waitpid,至此完成了一次handler方法,信号递达完成,父进程可以去执行自己的代码了。
如果程序没有对SIGCHLD进行自定义handler,系统对SIGCHLD的默认处理方式是忽略,当子进程退出时,会陷入僵尸状态等待资源被释放,以上demo运行结果,以及运行期间有关的进程情况
父进程调用signal设置SIGCHLD信号的递达方法为系统提供的SIG_IGN后,子进程退出时将不再向父进程发送SIGCHLD信号,而是直接退出并释放资源,不会陷入僵尸状态。
系统对SIGCHLD信号的默认递达方法SIG_DFL,在该递达方法下,进程依然接收SIGCHLD信号,只是不对其做处理,当SIGCHLD信号的递达方法被设置为SIG_IGN时,系统会对SIGCHLD进行特殊处理,使该进程的子进程退出时不再发送SIGCHLD信号,并且退出时会自动释放资源,不会陷入僵尸态。
运行结果,子进程没有陷入僵尸态