linux 时序竞态

pause函数

调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu) 直到有信号递达将其唤醒。

int pause(void);	

返回值:-1 并设置errno为EINTR
返回值:
① 如果信号的默认处理动作是终止进程,则进程终止,pause函数没有机会返回。
② 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
③ 如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause返回-1,errno设置为EINTR,表示“被信号中断”。
④ pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。

例:使用pause和alarm来实现sleep函数。

#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
void sig_deal(int signo)
{
}

unsigned int mysleep(unsigned int n)
{
    struct sigaction newact, oldact;
    unsigned int ret;

    newact.sa_handler = &sig_deal;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);//SIGALRM信号回调函数

    alarm(n); 
	
    int pause_ret=pause();
	if(pause_ret==-1&&errno==EINTR)
		printf("pause success\n");

    ret = alarm(0);//取消定时器,返回旧闹钟余下秒数。
    sigaction(SIGALRM, &oldact, NULL);//恢复SIGALRM信号旧有的处理方式

    return ret;
}


int main(void)
{
    while(1){
        mysleep(2);
        printf("2s passed\n");
    }

    return 0;
}

在这里插入图片描述

时序问题分析

回顾,借助pause和alarm实现的mysleep函数。设想如下时序:
1.注册SIGALRM信号处理函数(sigaction…)
2.调用alarm(1) 函数设定闹钟1秒。
3.函数调用刚结束,开始倒计时1秒。当前进程失去cpu,内核调度优先级更高的进程(有多个)取代当前进程。当前进程无法获得cpu,进入就绪态等待cpu。
4.1秒后,闹钟超时,内核向当前进程发送SIGALRM信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
5.优先级高的进程执行完,当前进程获得cpu资源,内核调度回当前进程执行。SIGALRM信号递达,信号设置捕捉,执行处理函数sig_deal。
6.信号处理函数执行结束,返回当前进程主控流程,pause()被调用挂起等待。(欲等待alarm函数发送的SIGALRM信号将自己唤醒)
7.SIGALRM信号已经处理完毕,pause永远不会等到。

解决时序问题

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

int sigsuspend(const sigset_t *mask);	

sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。
可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。

例:改进后的mysleep。

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

void sig_alrm(int signo)
{
    /* nothing to do */
}

unsigned int mysleep(unsigned int n)
{
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    //1.为SIGALRM设置捕捉函数,一个空函数
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    //2.设置阻塞信号集,阻塞SIGALRM信号
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);   //信号屏蔽字,阻塞SIGALRM信号

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

    /*4.构造一个调用在sigsuspend函数执行过程中临时有效的阻塞信号集suspmask,
     *  在临时阻塞信号集里解除SIGALRM的阻塞*/
    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);

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

    unslept = alarm(0);
    //6.恢复SIGALRM原有的处理动作
    sigaction(SIGALRM, &oldact, NULL);

    //7.解除对SIGALRM的阻塞
    sigprocmask(SIG_SETMASK, &oldmask, NULL);

    return unslept;
}

int main(void)
{
    while(1)
	{
        mysleep(2);
        printf("2s passed\n");
    }

    return 0;
}

整理一下,其实解决该问题的核心思路就是先把SIGALRM信号屏蔽,然后利用sigsuspend函数(系统调用是原子操作)把解除SIGALRM信号屏蔽和执行该信号的回调函数这两步一气呵成。

但是我认为,这个函数可以解决时序竞态,但是定时的时间肯定就不准了(如果信号发出的时候,当前进程没有拿到cpu,那么虽然该信号被阻塞了,但是当前进程还是要等待cpu,接下来等到当前进程拿到cpu,然后sigsuspend也顺利完成了任务,但是总时间变长了)。

总结

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章