linux_时序竞态-pause函数-sigsuspend函数-异步I/O-可重入函数-不可重入函数

接上一篇:linux_信号捕捉-signal函数-sigaction函数-sigaction结构体

  今天来分享时序竞态的知识,关于时序竞态的问题,肯定会和cpu有关,也会学习两个函数,pause函数,sigsuspend函数, 也会分享什么是可重入函数和不可重入函数,话不多说,上一碗时序竞态的大菜:

此博主在CSDN发布的文章目录:【我的CSDN目录,作为博主在CSDN上发布的文章类型导读

在介绍时序竞态之前,先介绍一下pause函数

1.pause函数

函数作用:
  调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu) 直到有信号递达将其唤醒。
头文件:
  #include <unistd.h>
函数原型:
  int pause(void);
函数参数:
  无
返回值:
  返回值:-1 并设置errno为EINTR
    ① 如果信号的默认处理动作是终止进程,则进程终止,pause函数么有机会返回。
    ② 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
    ③ 如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause返回-1】
    errno设置为EINTR,表示“被信号中断”。想想我们还有哪个函数只有出错返回值。
    ④ pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。

1.1.例子–pause函数运用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void donothing(int signo)
{
    
    
}
unsigned int mysleep(unsigned int seconds) 
{
    
    
    unsigned int ret;
    struct sigaction act, oldact;
    act.sa_handler = donothing;
    sigemptyset(&act.sa_mask);//信号集清零
    act.sa_flags = 0;
//注册信号捕捉函数
    sigaction(SIGALRM, &act, &oldact);
    alarm(seconds);         //定时固定的秒数  1 
    pause();                //挂起
    ret = alarm(0);  
    sigaction(SIGALRM, &oldact, NULL);  //恢复SIGALRM 默认处理方式
    return ret;
}
int main(void)
{
    
    
    mysleep(5);
    return 0;
}

2.时序竞态

  时序竞态: 由于进程之间执行的顺序不同,导致同一个进程多次运行后产生了不同结果的现象。
竞态问题总结:
  竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。
   不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。
   这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。

3.时序竞态问题1-信号处理

  为什么会有时序竞态的问题产生,是因为cpu在执行进程的时候,一个进程只执行一个时间片段,所以,你写的程序运行的时候,有时候执行千次万次看似没什么问题,可是某一次突然就崩了,当你去查问题的时候,复查了很长的时间,都没有找到问题,这种问题的出现的概念可能是千万分之一,不容易发生,但一发生就是致命问题,而这种问题还不易发现,只能通过我们日常写代码的经验来避免。
  例如在1.1的例子中,在调用alarm函数后,失去CPU,CPU去执行别的进程了,当执行别的进程的时间大于定时的时间后,会发生什么问题, 如下图。
在这里插入图片描述

  就这样,本能定时1s的程序,成了永久阻塞了,这种情况,是可能发生的,而发生的几率,可能就是千万分之一。
  设想在商业代码中出现这种错误,那后果则是毁灭性的。
  当然,在上述案例中,也有解决方法,那就是利用信号的屏蔽机制来解决,这就得说一下另一个函数sigsuspend了。

3.1.解决时序问题1-sigsuspend函数

函数作用:
  挂起等待信号。
头文件:
  #include <signal.h>
函数原型:
  int sigsuspend(const sigset_t *mask);
函数参数:
  mask:调用该函数期间决定信号屏蔽字得集合
返回值:
  错误返回-1,并设置errno以指示错误(通常为EINTR)。
  EINTR:被一个信号中断。

  可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。
  原子操作:cpu在执行这个函数就会把他执行完,不会停止

3.2.例子-解决例1.1时序竞态问题1代码:

#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void sig_alrm(int signo)
{
    
    
    /* nothing to do */
}
unsigned int mysleep(unsigned int nsecs)
{
    
    
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    /*为SIGALRM设置捕捉函数,一个空函数*/
    newact.sa_handler = sig_alrm;
	//将信号集清零
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
	//注册信号捕捉函数,oldact保留原有的信号集
    sigaction(SIGALRM, &newact, &oldact);

    /*设置阻塞信号集,阻塞SIGALRM信号*/
    sigemptyset(&newmask);//将信号集清零
    sigaddset(&newmask, SIGALRM);//将SIGALRM信号加入信号集,置1
	//屏蔽SIGALRM信号,设置信号屏蔽字,oldmask保留原有的信号集
    sigprocmask(SIG_BLOCK, &newmask, &oldmask); //原子操作,即调用该函数期间不能失去cpu

    //定时nsecs秒,到时后可以产生SIGALRM信号
    alarm(nsecs);

    /*构造一个调用sigsuspend临时有效的阻塞信号集,
     *  在临时阻塞信号集里解除SIGALRM的阻塞*/
    suspmask = oldmask;		//
    sigdelset(&suspmask, SIGALRM);	//在suspmask集合中清除对SIGALRM函数的屏蔽

    /*sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集
     *  这个信号集中不包含SIGALRM信号,同时挂起等待,
     *  当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集*/
    sigsuspend(&suspmask); 

    unslept = alarm(0);
    //恢复SIGALRM原有的处理动作,呼应前面注释1
    sigaction(SIGALRM, &oldact, NULL);

    //解除对SIGALRM的阻塞,呼应前面注释2
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    return(unslept);
}
int main(void)
{
    
    
while(1)
{
    
    
        mysleep(2);
        printf("Two seconds passed\n");
    }
    return 0;
}

4.时序竞态问题2-全局变量异步I/O

  分析如下父子进程交替数数程序。
  当捕捉函数里面的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;
        sleep(1);
        act.sa_handler = do_sig_parent;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR2, &act, NULL);             //注册自己的信号捕捉函数  父使用SIGUSR2信号
        do_sig_parent(0);						  
        while (1) {
    
    
            /* wait for signal */;
           if (flag == 1) {
    
                             //父进程数数完成
                kill(pid, SIGUSR1);
                flag = 0;                        //标志已经给子进程发送完信号
            }
        }
    } 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) {
    
    
            /* waiting for a signal */;
            if (flag == 1) {
    
    
                kill(getppid(), SIGUSR2);
                flag = 0;//分析,若是在cpu执行到此处时,收到父进程得信号,在flag还未被改完,就去执行do_sig_child该函数,会怎么样?
            }
        }
    }
    return 0;
}			

  示例中,通过flag变量标记程序实行进度。flag置1表示数数完成。flag置0表示给对方发送信号完成。
   问题出现的位置,在父子进程kill函数之后需要紧接着调用 flag,将其置0,标记信号已经发送。但,在这期间很有可能被kernel调度,失去执行权利,而对方获取了执行时间,通过发送信号回调捕捉函数,从而修改了全局的flag。
  如何解决该问题呢?
  可以使用后续会分享到的“锁”机制。 当操作全局变量的时候,通过加锁、解锁来解决该问题。
现在,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步IO可能造成的问题。

5.时序竞态问题3-可/不可重入函数

  一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”。根据函数实现的方法可分为“可重入函数”和“不可重入函数”两种。

  可重入函数:函数内不能含有全局变量及static变量,不能使用malloc、free等。
  不可重入函数:函数内含有全局变量及static变量,使用malloc、free,是标准I/O函数。

所以,我们的信号捕捉函数应该设计为可重入函数。
信号处理程序可以调用的可重入函数可参阅man 7 signal。

以上就是本次的分享了,希望能对广大网友有所帮助。

猜你喜欢

转载自blog.csdn.net/qq_44177918/article/details/130299093
今日推荐