全局变量的异步I/O问题

全局变量的异步I/O问题同样属于时序竞态问题,其本质就是多个进程或者同一个进程中的多个时序(如主控程序和信号捕捉时的用户处理函数)对同一个变量进行修改时,它们的执行顺序不一样就会导致该变量最终的值不一样,从而产生不一样的结果。

多个进程或者同一个进程中的多个时序对同一个变量进行操作时,应该尽量避免使用这种变量。在编程时也应当尽量避免使用全局变量。如果非用不可,则必须考虑该全局变量的使用顺序问题,可以采用加锁的方法对全局变量进行访问。如果加锁的方式无法解决,则直接就不访问该变量,直到等待其它进程或时序访问完之后才进行访问,总之确保变量正确的访问顺序。

//分析如下父子进程交替数数程序,重点分析程序中3sleep函数的作用,如果取消掉用户处理函数中的两个sleep函数会发生什么问题

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int n = 0, flag = 0;  //定义两个全局变量(注意了)

void sys_err(char *str)
{
    perror(str);
    exit(1);
}

void do_sig_child(int num)  //子进程的用户处理函数
{
    printf("I am child  %d\t%d\n", getpid(), n);
    n += 2;
    flag = 1;  //对全局变量的修改
    sleep(1);
}

void do_sig_parent(int num)   //父进程的用户处理函数
{
    printf("I am parent %d\t%d\n", getpid(), n);
    n += 2;
    flag = 1;  //对全局变量的修改
    sleep(1);
}

int main(void)
{
    pid_t pid;
    struct sigaction act;

    if ((pid = fork()) < 0)
        sys_err("fork");

    else if (pid > 0) {
        n = 1;          //父进程从1开始数
        sleep(1);       //父进程睡眠1s确保在父进程向子进程发信号之前,子进程完成了对信号的注册
        act.sa_handler = do_sig_parent;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR2, &act, NULL);             //注册自己的信号捕捉函数,父进程使用SIGUSR2信号

        do_sig_parent(0);   //父进程先进行数数,从1开始

        while(1) {
            /* wait for signal */;
           if (flag == 1) {                         //父进程数数完成
                kill(pid, SIGUSR1);
                flag = 0;                        //标志已经给子进程发送完信号
            }
        }

    } else if (pid == 0){
        n = 2;      //子进程从2开始数
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR1, &act, NULL);

        while(1) {
            /* wait for signal */;
            if (flag == 1) {
                kill(getppid(), SIGUSR2);
                flag = 0;
            }
        }
    }

    return 0;
}

SIGUSR1SIGUSR2信号。用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。

上述函数的正常执行结果本应该是:父进程数1、3、5、7、······;子进程数2、4、6、8、·······,且它们之间交替数数。

父进程中的第一个sleep函数确保在父进程向子进程发送信号前,子进程已经完成了对信号的注册(因为子进程有可能失去CPU时间太长而未完成对信号的注册),否则会导致子进程收到信号被终结。

在父进程中的全局变量flag在用户处理函数中和主控程序(while循环中)都会被修改(子进程也一样),但是正确的执行顺序必须是:父进程完成数数→用户处理函数置flag为1→父进程发信号→主控程序置flag为0。flag为1确保向进程发送信号,flag为0确保信号只是发送一次,不重复发送。但是,如果其中某一个进程(父进程或子进程)在while循环中刚发送完信号就失去了CPU,还未对flag进行修改,此时另一个进程处理完信号后,再次向该进程发送信号,此时该进程接收到信号不会接着执行flag=0的操作了,会马上去处理信号,信号处理完后,才会回到主控程序执行flag=0的操作,此时显然顺序发生了颠倒,导致最终flag错误置为0。因此,该进程处理完信号后再也不会发送信号了,另一个进程也再也不会收到信号,从而更不会再发信号。两个进程都在while循环中重复判断条件,但是条件永远不满足。因此,用户捕捉函数中的两个sleep函数的作用就是确保,一个进程在向两一个进程发送信号前,另一个进程主控程序中的flag=0的操作已经执行了,确保变量值修改的正确性。

如何解决该问题呢?可以使用后续章节讲到的“锁”机制。当操作全局变量的时候,通过加锁、解锁来解决该问题(互斥访问,进程同步)。

现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步IO可能造成的问题。

上述问题虽然可以通过sleep函数来解决,但是sleep函数会导致数数效率太低,可以取消全局变量flag,让发送信号的操作在用户处理函数中完成即可。

//程序的修改和优化

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int n = 0;
pid_t pid;

void sys_err(char *str)
{
    perror(str);
    exit(1);
}

void do_sig_child(int num)
{
    printf("I am child  %d\t%d\n", getpid(), n);
    n += 2;
    kill(getppid( ) , SIGUSR2);
}

void do_sig_parent(int num)
{
    printf("I am parent %d\t%d\n", getpid(), n);
    n += 2;
    kill(pid , SIGUSR1);
}

int main(void)
{
    struct sigaction act;

    if ((pid = fork()) < 0)
        sys_err("fork");

    else if (pid > 0) {
        n = 1;
        sleep(1);
        act.sa_handler = do_sig_parent;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR2, &act, NULL);            
        do_sig_parent(0);

        while(1) {
            ;
        }
    } else if (pid == 0){
        n = 2;
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR1, &act, NULL);

        while(1) {
            ;
        }
      }

    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值