文章目录
- 一、可重入函数
- 二、volatile
- 三、SIGCHLD信号
一、可重入函数
假设有一个不带头的单链表,要进行头插操作,在我们数据结构阶段都已经学习过,我们可以有以下的步骤:
要将node1头插到单链表中,调用insert函数,第一步将p的next指向head,再将head的值赋为p,但是在学习了信号之后,我们就要考虑一个问题,如果在第一个结束之后,将p连在了后边,此时收到一个信号,而此信号被自定义捕捉,而捕捉函数内部又是一个头插函数,此时的insert函数被重复进入了,将node2头插,但是在头插成功之后,返回主函数,又将node1的地址给了head,此时node2就会造成内存泄露,说明这个函数是不可重入的。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱.
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构
二、volatile
在c语言中我们就接触过这个关键字,但是当时我们并不清楚这个关键字的含义,今天站在信号的角度上重新理解一下:
我们先来看这段程序:
#include <stdio.h>
#include <signal.h>
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;
}
标准情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不满足,退出循环,进程退出。
而当我们改变一下优化的强度,优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。
sig:sig.c
gcc -o $@ $^ -O3
.PHONY:clean
clean:
rm -rf sig
如何解决呢?很明显需要 volatile.
#include <stdio.h>
#include <signal.h>
volatile 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;
}
当在全局变量前加上volatile之后,我们会发现程序照应可以正常运行了,这是因为volatile关键字可以保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作,就是每次处理这个变量之前,都去内存中找,而不是使用寄存器中保存的该变量的值。
三、SIGCHLD信号
在进程的等待章节,我们知道了可以使用wait和waitpid系统接口来等待进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,我子进程在终止时也会给父进程发送一个信号,如果父进程收到信号后将子进程清理,就不用一直阻塞等待或者轮询等待子进程了,子进程会给父进程发送SIGCHLD信号,是17号信号,他的默认处理动作是忽略,所以我们平时才没有注意到这个信号,如果我们自定义捕捉这个信号,并且在捕捉函数中去处理这个信号,那么是不是父进程就不用一直等待了呢?
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include<unistd.h>
#include<sys/wait.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0){//child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此处手动设置处理动作为SIG_IGN与默认处理动作为忽略是不一样的。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
signal(SIGCHLD,SIG_IGN);
pid_t cid;
if((cid = fork()) == 0){//child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
当我们不想获得子进程的退出状态,并且不想出现僵尸进程时,可以手动传入SIG_IGN,让系统在子进程运行完成之后,直接清理掉。