volatile+SIGCHLD信号+可重入函数(了解)

索引

  • volatile
      • 1.gcc -O含义及其作用
      • 2.证明其内存可见性
  • 深入理解SIGCHLD信号
      • SIGCHLD总结
  • 可重入函数

volatile

保存内存的可见性,告知编译器,该关键字修饰的变量不允许被优化,对该变量的任何操作都必须在内存中操作。

1.gcc -O含义及其作用

gcc提供了为了满足用户不同程度的优化需求,提供了很多优化选项,用来对(编译时间,目标文件长度,执行效率)进行平衡。下面大致介绍几个优化级别

-O0:不做任何优化,这是默认的编译选项。
-O和-O1:对程序做部分编译优化,对于大函数,优化编译占用稍微多的时间和相当大的内存,编译器会尝试缩小代码的尺寸以及缩短执行时间,但并不执行需要占用大量编译器的优化。
-O2:比O1高级,进行更多的优化。Gcc将执行几乎所有的不包含时间和空间这种的优化,此时编译器不进行循环打开()以及内联函数,与O1比较而言,O2优化增加了编译时间的基础上,提高了生成代码的执行效率。
O3:在O2的基础上做进一步优化。

优化的代码可能给调试带来问题,可能会改变代码的结构,eg:对分支的合并和消除
还有可能给内存操作顺序改变带来问题。eg:对于某些依赖内存操作顺序而进行的逻辑,需要做严格的处理后才能进行优化

2.证明其内存可见性

此时程序的编译:
g++ -o $@ $^ -std=c++11 -O2

int flag = 0;
void handler(int signo)
{
    printf("变量的值已经修改了 flag:%d\n", flag);
    flag = 1;
}
int main()
{
    signal(2, handler);
    while (!flag)
        ;
    printf("process 正常退出\n");

    return 0;
}

volatile+SIGCHLD信号+可重入函数(了解)_第1张图片
上述代码Gcc编译时进行了O2优化,编译器编译的时候看到循环中没有语句,此时其循环的判断条件除第一次从内存中提取之外,其他时间都是从寄存器提取,因此,当我们CTRL+C时,命名条件的flag = 1,但程序依旧不会跳出循环。
如果我们在一样的代码上添加volatile
volatile+SIGCHLD信号+可重入函数(了解)_第2张图片
由此可见volatile的作用其保持内存的可见性.

深入理解SIGCHLD信号

父进程创建子进程后,之前讲述的都是父进程用waitpid()和wiat()函数清理僵尸进程,此时既可以阻塞式等待:此时父进程无法进一步处理自己的工作,直到子进程退出。
非阻塞式等待:父进程采取轮循检测的方式等待子进程退出。
实际上在子进程退出的时候会给父进程发送一个信号:SIGCHLD信号,只不过父进程对该信号的默认处理方式是忽略。下面验证一下子进程退出的时候,子进程是否会发送SIGCHLD信号。
实验设计:
父进程fork()创建子进程之后,子进程exit()退出,给SIGCHLD信号设计自定义捕捉函数。

void ChildHandler(int signo)
{
    // 此时阻塞式等待任何一个子进程。
    pid_t id = waitpid(-1, nullptr, 0);
    if (id > 0)
    {
        cout << "父进程等待成功" << endl;
    }
}
int main()
{
    signal(SIGCHLD, ChildHandler);
    pid_t id = fork();
    if (id == 0)
    {
        cout << "我是子进程" << endl;
        sleep(3);
        exit(1);
    }
    while (true)
    {
        cout << "main running..." << endl;
        sleep(1);
    }
    return 0;
}

volatile+SIGCHLD信号+可重入函数(了解)_第3张图片
其实上述的自定义捕捉函数是有bug的,如果此时父进程创建了很多子进程并且子进程同时退出的话,就会有多个SIGCHLD信号被发送,会导致该信号被堵塞,此时就会产生很多僵尸进程

void ChildHandler(int signo)
{
    // 此时阻塞式等待任何一个子进程。
    pid_t id = waitpid(-1, nullptr, 0);
    if (id > 0)
    {
        cout << "父进程等待成功" << endl;
    }
}
int main()
{
    signal(SIGCHLD, ChildHandler);
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            int cnt = 10;
            while (cnt)
            {
                cout << "我是子进程,pid: " << getpid() << "  cnt:" << cnt-- << endl;
                sleep(1);
            }
            cout << "子进程退出进入僵尸状态;" << endl;

            exit(1);
        }
    }

    while (true)
    {
        cout << "main running..." << endl;
        sleep(1);
    }
    return 0;
}

volatile+SIGCHLD信号+可重入函数(了解)_第4张图片

由于信号同时在处理时继续发送信号此时信号会被阻塞,所以上述代码会出现僵尸进程。
所以我们循环式读取直到waitpid()调用失败,此时跳出循环表示已经全部等待完成,就不会出现僵尸进程。

void ChildHandler(int signo)
{
    while (true)
    {

        // 此时阻塞式等待任何一个子进程。
        pid_t id = waitpid(-1, nullptr, 0);
        if (id > 0)
        {
            cout << "父进程等待成功" << endl;
        }
    }
}

volatile+SIGCHLD信号+可重入函数(了解)_第5张图片
此时并不会出现僵尸进程,但是此情况针对的是子进程同时退出,子进程也有可能不同时退出。如下所示。

void ChildHandler(int signo)
{
    while (true)
    {

        // 此时阻塞式等待任何一个子进程。
        pid_t id = waitpid(-1, nullptr, 0);
        if (id > 0)
        {
            cout << "父进程等待成功" << endl;
        }
    }
}
int main()
{
    signal(SIGCHLD, ChildHandler);
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {

            int cnt = 10;

            if (i < 6)
            {
                cnt = 5;
            }
            else
            {
                cnt = 50;
            }
            while (cnt)
            {
                cout << "我是子进程,pid: " << getpid() << "  cnt:" << cnt-- << endl;
                sleep(1);
            }
            cout << "子进程退出进入僵尸状态;" << endl;

            exit(1);
        }
    }

    while (true)
    {
        cout << "main running..." << endl;
        sleep(1);
    }
    return 0;
}

volatile+SIGCHLD信号+可重入函数(了解)_第6张图片
此时就会出现子进程一直在运行,但是父进程不会运行自己的代码,因为此时自定义函数中的等待方式是阻塞式等待,此时只要我们将阻塞式等待变成非阻塞式等待即可。

void ChildHandler(int signo)
{
    while (true)
    {

        // 此时非阻塞式等待任何一个子进程。
        pid_t id = waitpid(-1, nullptr, WNOHANG);
        if (id > 0)
        {
            cout << "父进程等待成功 child pid:" << getpid() << endl;
        }
        else if (id == 0)
        {
            cout << "还有子进程没有退出,父进程要去忙自己的事情了" << endl;
            break;
        }
        else
        {
            cout << "父进程等待所有的子进程结束" << endl;
            break;
        }
    }
}
int main()
{
    signal(SIGCHLD, ChildHandler);
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {

            int cnt = 10;

            if (i < 6)
            {
                cnt = 5;
            }
            else
            {
                cnt = 20;
            }
            while (cnt)
            {
                cout << "我是子进程,pid: " << getpid() << "  cnt:" << cnt-- << endl;
                sleep(1);
            }
            cout << "子进程退出进入僵尸状态;" << endl;

            exit(1);
        }
    }

    while (true)
    {
        cout << "我是父进程,我正在运行:" << getpid() << endl;
        cout << "main running..." << endl;
        sleep(1);
    }
    return 0;
}

volatile+SIGCHLD信号+可重入函数(了解)_第7张图片

SIGCHLD总结

上述是使用该信号的自定义捕捉函数,那么如果我们不使用他呢?
sigactionSIGCHLD的处理动作设置成SIG_IGN(忽略),这样fork()出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程,系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例,该方法只对Linux可用,但不保证在其他的UNIX系统上都可用。
这里就不做模拟了。
但是设置了SIG_IGN后,可不仅仅是自动释放子进程这一个内容,这是一个函数,函数可以修改父进程的相关属性,进而被子进程继承下去,如果没有设置忽略的话,子进程在没有waitpid()的情况下是会产生僵尸进程的,但是设置SIG_IGN就不会产生僵尸进程,子进程会自动释放!

可重入函数

当两个不同的控制流程调用同一个函数,访问同一个局部变量或参数造成程序的错乱,该函数就是不可冲入函数。
符合下列条件之一的就是不可冲入函数:

1.调用了malloc 或 free,因为malloc也是用全局链表来管理堆的
2.调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

你可能感兴趣的:(linux,c++,开发语言)