volatile、可重入和不可重入函数以及SIGCHILD信号

可重入函数

volatile、可重入和不可重入函数以及SIGCHILD信号_第1张图片

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因 为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函 数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从 sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步 之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只 有一个节点真正插入链表中了。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称 为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

volatile、可重入和不可重入函数以及SIGCHILD信号_第2张图片

如果一个函数符合以下条件之一则是不可重入的:

调用了malloc或free,因为malloc也是用全局链表来管理堆的。

调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

volatile(保持内存可见性)

我们先来看看程序:

gcc -o $@ $^ -O3 -std=c++11

编译时加上 -O3选项进行优化(在CPU寄存器中优化)。

int quit = 0;

void myhandler(int signo)
{
    cout << "已收到信号 :" << signo <

volatile、可重入和不可重入函数以及SIGCHILD信号_第3张图片

 volatile、可重入和不可重入函数以及SIGCHILD信号_第4张图片

由于在main中只进行比较,并不改变quit,从内存中取结果的过程被省去了。

要保持内存可见性 ,则可以通过添加volatil关键字解决,每次使用数据都从内存中取,再放回去。

volatile int quit = 0;

volatile、可重入和不可重入函数以及SIGCHILD信号_第5张图片

 

SIGCHLD信号

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号 的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理 函数中调用wait清理子进程即可。

如果不对SIGCHLD信号处理函数做处理,结果如下:

int main()
{
    // 显示的设置对SIGCHLD进行忽略
    // signal(SIGCHLD, SIG_IGN);

    printf("我是父进程, %d, ppid: %d\n", getpid(), getppid());

    pid_t id = fork();
    if (id == 0)
    {
        printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid());
        Count(10);
        exit(1);
    }

    while (1)
        sleep(1);
    return 0;
}

volatile、可重入和不可重入函数以及SIGCHILD信号_第6张图片

子进程退出后变成僵尸代码,等待父进程回收。

我们利用子进程在终止时会给父进程发SIGCHLD信号,实现自动回收。

但有可能同时有许多子进程在同一时刻退出,会出现信号丢失,所以要在信号处理函数中一次非阻塞回收所有僵尸进程。

void handler(int signo)
{
    //1. 我有非常多的子进程,在同一个时刻退出了

    //waitpid(-1) -> while(1)
    //2. 我有非常多的子进程,在同一个时刻只有一部分退出了
    while(1)
    {
        pid_t ret = waitpid(-1, NULL, WNOHANG);//非阻塞式等待
        if(ret == 0) break;
    }
}

void Count(int cnt)
{
    while (cnt)
    {
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}

int main()
{
    // 显示的设置对SIGCHLD进行忽略
    // signal(SIGCHLD, SIG_IGN);

    signal(SIGCHLD, handler);

    printf("我是父进程, %d, ppid: %d\n", getpid(), getppid());

    pid_t id = fork();
    if (id == 0)
    {
        printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid());
        Count(5);
        exit(1);
    }

    while (1)
        sleep(1);
    return 0;
}

volatile、可重入和不可重入函数以及SIGCHILD信号_第7张图片

 实现自动回收。

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作 置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽 略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。

signal(SIGCHLD, SIG_IGN);

volatile、可重入和不可重入函数以及SIGCHILD信号_第8张图片

 和我们自己写的信号处理函数效果相同。

你可能感兴趣的:(链表,数据结构)