[ Linux ] 可重入函数,volatile 关键字,SIGCHLD信号

目录

1.可重入函数

2.volatile

2.1从信号角度理解volatile的作用

2.2volatile的作用

3.SIGCHLD信号

3.1SIGCHLD信号的验证


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:clean
clean:
	rm -f myproc

gcc中有不同的优化等级,我们在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系统上都可用。

(本篇完)

猜你喜欢

转载自blog.csdn.net/qq_58325487/article/details/128282708