时序竟态

一、pause函数

#include <unistd.h>
int pause(void);

/*
返回值(成功):-1, 并设置errno为EINTR
1、如果信号的默认处理动作是终止进程,则进程终止,pause函数没有机会返回。
2、如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
3、如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause返回-1。
4、pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。
*/

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

二、什么是时序竟态?

利用alarm+pause实现sleep功能:

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

void catch_sigalrm(int signo)
{
	
}

unsigned int my_sleep(unsigned int seconds)
{
	int ret;
	struct sigaction act, oldact;

	act.sa_handler = catch_sigalrm;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;

	// 注册SIGALRM处理函数
	ret = sigaction(SIGALRM, &act, &oldact);
	if (ret == -1)
	{
		perror("sigaction error");
		exit(EXIT_FAILURE);
	}

	// 使用alarm+pause实现sleep功能
	alarm(seconds);
    // -----------这里可能会失去CPU,导致SIGALRM已经发出,但下面的pause还没有调用
	ret = pause();
	if (ret == -1 && errno == EINTR)
	{
		printf("pause sucess\n");
	}

	// 如果在等待期间有其它信号被捕捉,则关闭闹钟,保证程序的健壮性。
	ret = alarm(0);

	// 恢复SIGALRM默认处理函数
	sigaction(SIGALRM, &oldact, NULL);
	
    // 返回闹钟等待的时长
	return(seconds-ret);
}


int main()
{
	while(1) printf("---------sleep %u seconds-------\n", my_sleep(1));
	
	return 0;
}

上面的代码中存在时序竟态的问题。设想如下场景:

想睡觉,定闹钟10分钟,希望10分钟后闹钟将自己唤醒。

正常:定时,睡觉,10分钟后被闹钟唤醒。

异常:闹钟定好后,睡了5分钟被人喊醒外出劳动(失去CPU),20分钟后劳动结束。回来继续睡觉计划,但劳动期间闹钟已经响过(alarm函数已经发送过了信号),不会再将我唤醒。

三、时序问题分析

同理,回顾借助pause和alarm实现的my_sleep函数。设想如下时序:

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

四、解决时序问题:sigsuspend函数

屏蔽SIGALRM    // 这样信号就能阻塞住
alarm(seconds);
// ------------------失去CPU
解除屏蔽
// ------------------这里也可能失去CPU
ret = pause(); //主动挂起,等信号

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

#include <signal.h>

int sigsuspend(const sigset_t *mask); // 挂起等待信号。

/*
RETURN VALUE
       sigsuspend() always returns -1, 
       with errno set to inndicate the error (normally, EINTR).
*/

sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。

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


改进版my_sleep:

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

void catch_sigalrm(int signo)
{
	
}

unsigned int my_sleep(unsigned int seconds)
{
	int ret;
	struct sigaction newact, oldact;
	sigset_t newmask, oldmask, suspendmask;
	unsigned int unslept;

	// 1、为SIGALRM设置捕捉函数
	newact.sa_handler = catch_sigalrm;
	sigemptyset(&newact.sa_mask);
	newact.sa_flags = 0;

	ret = sigaction(SIGALRM, &newact, &oldact);
	if (ret == -1)
	{
		perror("sigaction error");
		exit(EXIT_FAILURE);
	}

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

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

	// 4、构造一个调用sigsuspend临时有效的阻塞信号集,解除对SIGALRM的阻塞
	suspendmask = oldmask;
	sigdelset(&suspendmask, SIGALRM);	// 原子操作

	// 5、sigsuspend调用期间,采用临时阻塞信号集suspendmask替换原有的阻塞信号集
	// 这个信号集中不包含SIGALRMM信号,同时挂起等待,
	// 这样,即使在此之前失去CPU,闹钟已经发送了SIGALRM信号,信号会被阻塞住。
	// sigsuspend调用时解除了对SIGALRM的阻塞,会捕捉到信号,恢复程序运行。
	sigsuspend(&suspendmask);

	// 获得上次闹钟剩余时间
	unslept = alarm(0);

	// 6、恢复SIGALRM原有的处理动作,呼应前面注释1
	// 当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集。
	sigaction(SIGALRM, &oldact, NULL);

	// 7、解除对SIGALRM的阻塞,呼应前面注释2
	sigprocmask(SIG_SETMASK, &oldmask, NULL);

	return(seconds-unslept);
}


int main()
{
	while(1) printf("---------sleep %u seconds-------\n", my_sleep(1));

	return 0;
}

五、总结

竟态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。

  不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否处理某个信号,当系统负载过重时,会出现时序混乱。

  这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过GDB程序高度等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值