目录
1. 可重入函数
2. volatile
3. SIGCHLD信号
Linux!🌷
1. 可重入函数
先来谈一下重入函数的概念:重入函数便是在该函数还没有执行完毕便重复进入该函数(一般发生在多线程中);
可重入函数:一个函数一旦重入,对原函数功能不会出现问题(内存泄漏等);
不可重入函数:一个函数一旦重入,对原函数功能有可能出现问题;
什么意思呢?下面给一个具体的例子方便大家理解:
- main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
- 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
注意:可重入函数/不可重入函数,只是一种特性而已,并无优劣好坏之分;
2. volatile
volatile其实是C语言中的一个关键字,还是比较冷门的,虽然冷门但不意味着不重要;
下面给出几个例子方便大家理解:
示例一:
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int signo)
{
printf("change 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 条件不满足,退出循环,进程退出;
示例二:gcc 进行一定程度的优化
对 makefile 文件进行修改再编译,-O3表示优化级数;
优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。
示例三:volatile进行修饰
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int signo)
{
printf("change flag 0 to 1!\n");
flag = 1;
}
int main()
{
signal(2,handler);
while(!flag);
printf("process quit normal!\n");
return 0;
}
可以看到结果正确;
使用 volatile 修饰 flag 保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对 flag 变量的任何操作,都必须在真实的内存中进行操作;
vloatile 还有一个作用:防止指令重排(一条C/C++指令进行编译后可能形成多条汇编代码,
volatile可保证多条汇编代码的顺序不会发生改变),这个了解下就好;
3. SIGCHLD信号
进程一章讲过用 wait 和 waitpid 函数清理僵尸进程 , 父进程可以阻塞等待子进程结束 , 也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式) 。采用第一种方式 , 父进程阻塞了就不能处理自己的工作了;采用第二种方式 , 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。
其实, 子进程在终止时会给父进程发 SIGCHLD 信号 , 该信号的默认处理动作是忽略, 父进程可以自定义 SIGCHLD 信号的处理函数, 这样父进程只需专心处理自己的工作, 不必关心子进程了, 子进程终止时会通知父进程 , 父进程在信号处理函数中调用wait 清理子进程即可。
事实上 , 由于 UNIX 的历史原因 , 要想不产生僵尸进程还有另外一种办法 : 父进程调用 sigaction 将SIGCHLD 的处理动作置为SIG_IGN, 这样 fork 出来的子进程在终止时会自动清理掉 , 不会产生僵尸进程 , 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略通常是没有区别的 , 但这是一个特例。此方法对于 Linux 可用 , 但不保证在其它UNIX 系统上都可用。
下面编写代码对上述所说的内容进行一个验证:
代码1:子进程在退出时候会发送SIGCHLD信号?
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
printf("get a signal,signo:%d\n",signo);
}
int main()
{
//对信号捕捉
signal(SIGCHLD,handler);
//创建子进程
if(fork()==0)
{
//child
printf("I am a child,I quit...!\n");
sleep(1);
return 0;
}
//parent
while(1)
{
;
}
return 0;
}
经过证实,子进程在退出时会发送 17)SIGCHLD 信号;
代码2:可以在SIGCHLD自定义捕捉时,waitpid子进程?
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void handler(int signo)
{
printf("get a signal!signo:%d\n",signo);
sleep(1);
while(1)
{
waitpid(-1,NULL,WNOHANG);
}
}
int main()
{
//捕捉信号
signal(SIGCHLD,handler);
//创建子进程
if(fork()==0)
{
//child
printf("I am a child!\n");
sleep(1);
return 0;
}
while(1);
return 0;
}
由上我们可以看到子进程的3种状态的切换,S(在1S输出I am a child)——>Z(sleep1S后才waitpid)——>被清理;
代码3:直接在收到SIGCHLD时,进行忽略处理,不形成僵尸,直接处理;
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//捕捉信号
signal(SIGCHLD,SIG_IGN);
//创建子进程
if(fork()==0)
{
//child
printf("I am a child!\n");
sleep(1);
return 0;
}
while(1);
return 0;
}