接上一篇: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。
以上就是本次的分享了,希望能对广大网友有所帮助。