【Linux】可重入函数 volatile关键字 以及SIGCHLD信号

简介: 【Linux】可重入函数 volatile关键字 以及SIGCHLD信号

一、可重入函数

1、引入

我们来先看一个例子来帮助我们理解什么是可重入函数:

假设我们现在要对一个链表进行头插,在执行到第10行代码时,突然进程的时间片到了,进程被切换了,一会等进程再度切换回来时,当前进程要处理信号,而信号处理函数是sighandler,而sighandler里面也进行了头插,等进程从内核态返回到用户态时,继续执行第11行的代码,这时我们再观察链表的结构会发现链表中出现了节点丢失的问题,而造成这种问题的根源是我们的insert函数同时被两个执行流给进入了。

node_t node1, node2, *head;
int main()
{
  ...
  insert(&node1);
  ...
}
void insert(node_t*p)
{
  p->next = head;
  head = p;
}
void sighandler(int signo)
{
  insert(&node2);
}

由这个问题衍生出了一种函数分类的方式:

  • 如果一个函数同时被多个执行流进入所产生的结果没有问题,该函数被称为可重入函数
  • 如果一个函数同时被多个执行流进入所产生的结果有问题,该函数被称为不可重入函数
  • 可重入函数主要用于多任务环境中,一个可重入的函数通常来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;
  • 不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

2、可重入函数的判断

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

  1. 函数体内使用了静态(static)的数据结构或者变量;
  2. 调用了mallocfree,因为malloc也是用全局链表来管理堆的。
  3. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

二、volatile关键字

1、引入

volatile是C语言的一个关键字,该关键字的作用是保证内存数据的可见性

我们来先来看一段代码,这里我们不加入volatile关键字并开启编译器优化选项,优化级别是-O2

这段代码的意思是:我们让进程一直运行,直到我们给进程发送2号信号以后,进程再退出。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int flag = 0;
void handler(int signo)
{
    printf("捕捉到了%d号信号\n", signo);
    // 将flag置为1
    flag = 1;
    printf("已经将flag置为%d\n", flag);
}
int main()
{
    signal(2, handler);
    printf("进程正在运行...\n");
    while (!flag);  // 当flag == 1时,进程退出。
    printf("运行结束!\n");
    return 0;
}

运行结果:

可以看到,我们明明都已经让flag = 1了但是进程中的循环依然没有结束,这时为什么呢?下面我们一起来分析这个过程:


代码中的main函数和handler函数在触发时是两个独立的执行流,而while循环是在main函数当中的,而且main执行流里面并没有使用过handler函数(signal函数只是对2号信号进行了捕捉,没有调用过handler函数),所以在编译器编译时检测到在main函数中对flag变量没有做过修改操作,而且由于while循环运行时需要频繁使用flag变量,所以编译器可以将flag变量的值用一个寄存器进行保存,以后每次使用flag变量直接去寄存器里面取数据,不必每次都要将内存中的flag搬运到寄存器里面然后让CPU去计算。

可是不巧的是我们给当前进程发送了2号信号,让另外一个执行流更改了内存中的flag变量,而由于编译器的优化,认为flag变量不会改变导致内存中的flag变量改变以后也没有将寄存器中的数据同步修改,而CPU运算使用的数据又是寄存器中的数据,这就导致了内存数据的不可见,于是while循环就会一直运行,导致了上面的问题。

为了让编译器每次都要去内存取数据来进行计算,我们可以在flag变量前面加上volatile关键字。

#include <stdio.h>
...
volatile int flag = 0;
void handler(int signo)
{
   ...
}
int main()
{
    ...
}

再次运行程序,发现运行结果符合预期!

2、关于编译器的优化的简单讨论

上面的代码如果我们不开启优化,就算不加上volatile关键字也是能正常运行的,可见编译器的优化不是越高越好。

如何理解编译器的优化?

编译器的本质是将代码翻译成01的二进制序列,所以编译器的优化是在你编写的代码上动手脚,也就是说编译器的优化其实改变了一些最终翻译成01二进制以后的执行逻辑。

三、SIGCHLD信号

在一前我们讲过用waitwaitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,也很麻烦。

wait与waitpid的使用介绍

上面使用waitwaitpid其实都是父进程主动检查子进程是否处于僵尸状态,那么有没有一种方法能够让子进程主动告诉父进程自己处于僵尸状态呢?

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

下面就是一个对SIGCHLD信号的一个使用:

在父进程中我们创建了10个子进程,这10个子进程退出时都会给父进程发送SIGCHLD信号,由于父进程回收其中一个子进程时,其他子进程也有可能同时给父进程发送SIGCHLD信号,而pending表又没有办法同时存储多个信号,所以我们就要进行循环回收子进程,而为了不影响父进程的执行流程我们可以选择非阻塞等待。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
pid_t id = 0;
void WaitProcess(int signo)
{
    printf("捕捉到了%d号信号,正在处理...\n", signo);
    while (1)
    {
        pid_t ret = waitpid(-1, NULL, WNOHANG);
        if (ret > 0)
        {
            printf("等待子进程%d成功,父进程%d\n", ret, id);
        }
        else
        {
            break;
        }
    }
    printf("WaitProcess, done\n");
}
int main()
{
    signal(SIGCHLD, WaitProcess);
    int i = 0;
    // 创建10个子进程
    for (i = 0; i < 10; i++)
    {
        id = fork();
        // 子进程
        if (id == 0)
        {
            int cnt = 5;
            //睡眠cnt秒以后退出
            while (cnt--)
            {
                printf("我是子进程,我的pid是:%d,ppid是:%d\n", getpid(), getppid());
                sleep(1);
            }
            exit(0);
        }
    }
    // 父进程一直休眠
    while (1)
    {
        sleep(1);
    }
    return 0;
}

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

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
pid_t id = 0;
int main()
{
  // 对SIGCHLD设置为忽略,这样产生的子进程退出时不会形成僵尸状态。
    signal(SIGCHLD, SIG_IGN);
    int i = 0;
    // 创建10个子进程
    for (i = 0; i < 10; i++)
    {
        id = fork();
        // 子进程
        if (id == 0)
        {
            int cnt = 5;
            //睡眠cnt秒以后退出
            while (cnt--)
            {
                printf("我是子进程,我的pid是:%d,ppid是:%d\n", getpid(), getppid());
                sleep(1);
            }
            exit(0);
        }
    }
    // 父进程一直休眠
    while (1)
    {
        sleep(1);
    }
    return 0;
}

相关文章
|
9月前
|
安全 Linux
【Linux】阻塞信号|信号原理
本教程从信号的基本概念入手,逐步讲解了阻塞信号的实现方法及其应用场景。通过对这些技术的掌握,您可以更好地控制进程在处理信号时的行为,确保应用程序在复杂的多任务环境中正常运行。
324 84
|
10月前
|
存储 Linux
linux中的目录操作函数
本文详细介绍了Linux系统编程中常用的目录操作函数,包括创建目录、删除目录、读取目录内容、遍历目录树以及获取和修改目录属性。这些函数是进行文件系统操作的基础,通过示例代码展示了其具体用法。希望本文能帮助您更好地理解和应用这些目录操作函数,提高系统编程的效率和能力。
366 26
|
10月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
203 26
|
10月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
207 17
|
12月前
|
Linux
【Linux】System V信号量详解以及semget()、semctl()和semop()函数讲解
System V信号量的概念及其在Linux中的使用,包括 `semget()`、`semctl()`和 `semop()`函数的具体使用方法。通过实际代码示例,演示了如何创建、初始化和使用信号量进行进程间同步。掌握这些知识,可以有效解决多进程编程中的同步问题,提高程序的可靠性和稳定性。
636 19
|
12月前
|
Linux Android开发 开发者
linux m、mm、mmm函数和make的区别
通过理解和合理使用这些命令,可以更高效地进行项目构建和管理,特别是在复杂的 Android 开发环境中。
631 18
|
12月前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
540 13
|
Linux Shell
Linux系统编程:掌握popen函数的使用
记得在使用完 `popen`打开的流后,总是使用 `pclose`来正确关闭它,并回收资源。这种做法符合良好的编程习惯,有助于保持程序的健壮性和稳定性。
720 6
|
3月前
|
Linux 应用服务中间件 Shell
二、Linux文本处理与文件操作核心命令
熟悉了Linux的基本“行走”后,就该拿起真正的“工具”干活了。用grep这个“放大镜”在文件里搜索内容,用find这个“探测器”在系统中寻找文件,再用tar把东西打包带走。最关键的是要学会使用管道符|,它像一条流水线,能把这些命令串联起来,让简单工具组合出强大的功能,比如 ps -ef | grep 'nginx' 就能快速找出nginx进程。
二、Linux文本处理与文件操作核心命令
|
3月前
|
Linux
linux命令—stat
`stat` 是 Linux 系统中用于查看文件或文件系统详细状态信息的命令。相比 `ls -l`,它提供更全面的信息,包括文件大小、权限、所有者、时间戳(最后访问、修改、状态变更时间)、inode 号、设备信息等。其常用选项包括 `-f` 查看文件系统状态、`-t` 以简洁格式输出、`-L` 跟踪符号链接,以及 `-c` 或 `--format` 自定义输出格式。通过这些选项,用户可以灵活获取所需信息,适用于系统调试、权限检查、磁盘管理等场景。
321 137