1.可重入函数
在数据结构初阶时我们学习过链表,其中当然也学习过链表头插。在此我们复习一下链表头插,我们使用画图来演示。
newnode->next = head->next;head->next = newnode;下面我们假设今天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
2.1从信号角度理解volatile的作用
今天我们站在信号的角度上理解一下valatile。
#include <stdio.h>#include <signal.h>int flags = 0;void handler(int signo){ flags = 1; printf("flags: 0 -> 1\n");}int main(){ signal(2,handler); while(!flags); printf("进程是正常退出的! flags: %d \n",flags); return 0;}我们来看一下这段简单的C语言代码,在标准情况下,程序运行起来时,键入CTRL-C,2号信号被捕捉,执行自定义动作,修改flags=1,while条件不满足,退出循环,进程退出。
但是,我们注意while(!flags)这条语句是检测,是逻辑判断。因此由CPU进行,每次循环检测都要读一下flags这个值,在正常情况下就应该这么做。但是当编译器优化等级很高时,在当前执行流下对flags没有做任何修改,因此会把flags这个值优化到CPU的寄存器内,因此在后续的判断中,CPU只会读取寄存器内flags内的值。但是当我们键入ctrl-c时,向进程发送2号信号。进程捕捉到2号信号会自定义调用handler方法。会将flags的值由0->1,这里注意由于flags是存在内存中的,我们改变的是内存中flags的值,而CPU寄存器内flags的值却没有变。因此CPU读取的flags的值却并没有变。所以当我们键入ctrl-c时,while循环也是不结束的。进程也不会退出。
myproc:myproc.c gcc -o $@ $^ -O2.PHONY:cleanclean: rm -f myprocgcc中有不同的优化等级,我们在makefile中使用-O2 对gcc编译器进行优化。
我们再次将程序运行起来
那么如何解决呢,我们可以使用volatile关键字。volatile关键字就是要告诉编译器,不准对flags做任何优化,每次CPU计算的时候,拿内存的数据,都必须在内存中拿。
#include <stdio.h>#include <signal.h>//保持内存的可见性volatile int flags = 0;void handler(int signo){ flags = 1; printf("flags: 0 -> 1\n");}int main(){ signal(2,handler); while(!flags); printf("进程是正常退出的! flags: %d \n",flags); return 0;}2.2volatile的作用
根据上面的例子我们可以总结出volatile的作用:
- volatile作用:保持内存的可见性,告知编译器,该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
3.SIGCHLD信号
在进程一章时,我们知道进程退出。其中,子进程退出的时候,不是默默地退出。而是会给父进程发送一个信号。这个信号就是SIGCHLD信号。该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数。这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
3.1SIGCHLD信号的验证
我们用C++代码来验证一下当子进程退出时,父进程捕捉SIGCHLD信号。这段代码也不难......
#include <iostream>#include <signal.h>#include <unistd.h>using namespace std;void handler(int signo){ cout<<"子进程退出啦,我确实收到了信号"<<signo<<"我是: pid: "<<getpid()<<endl;}int main(){ signal(SIGCHLD,handler); pid_t id = fork(); if(id == 0) { //child while(true) { cout<<"我是子进程:"<<getpid()<<endl; sleep(1); } exit(0); } //parent while(true) { cout<<"我是父进程:"<<getpid()<<endl; sleep(1); } return 0;}我们使用man 7 siganl查看信号发现,SIGCHLD不仅当子进程退出时可以返回给父进程,也可以当子进程暂停。
我们再来验证一下暂停,我们发送19号信号是暂停进程,18号信号是恢复进程
此时我们来查看当子进程暂停时的状态,我们可以使用下面命令查看
ps axj | grep myproc
如果我们杀掉子进程查看状态:发现子进程一进僵尸
注:事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
(本篇完)