第一种方式:mysleep的实现实际上是利用alarm(timeout)函数和利用pause函数将当前进程挂起
pause函数使调用进程挂起直到有信号递达。如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;如果信号的处理动作是忽略,则进程继续处于挂起状态,pause 不返回;如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为 EINTR, 所以pause只有出错的返回值
例如我们之前学过的进程间的程序替换也是只有出错了才会返回错误码 EINTR表 示“被信号中断”。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction函数的作用和signal函数的作用是一样的
sigaction函数可以读取和修改与指定信号相关联的处理动作。调⽤用成功则返回0,出错则 返回- 1。 signo是指定信号的编号。若act指针⾮非空,则根据act修改该信号的处理动作。
若oact指针⾮非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结 构体:
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL 表⽰示执行系统默认动作,赋值为一个函数指针表示⽤用⾃自定义函数捕捉信号,或者说向内核注册 了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信 号的编号,这样就可以⽤用同⼀一个函数处理多种信号。显然,这也是一个回调函数,不是被main 函数调用,而是被系统所调⽤用。
当某个信号的处理函数被调⽤用时,内核⾃自动将当前信号加⼊入进程的信号屏蔽字,当信号处理函 数返回时⾃自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产 ⽣生,那么 它会被阻塞到当前处理结束为⽌止。
如果在调⽤用信号处理函数时,除了当前信号被⾃自动屏蔽之外,还希望⾃自动屏蔽另外⼀一些信号,则 ⽤用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时⾃自动恢复原来的信号 屏蔽字。
sa_flags字段包含⼀一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号
在进行阻塞式系统调用时,为避免进程陷入无限期的等待,可以为这些阻塞式系统调用设置定时器。Linux提供了alarm系统调用和SIGALRM信号实现这个功能。要使用定时器,首先要安装SIGALRM信号。如果不安装SIGALRM信号,则进程收到SIGALRM信号后,缺省的动作就是终止当前进程。SIGALRM信号安装成功后,在什么情况下进程会收到该信号呢?这就要依赖于Linux提供的定时器功能。在Linux系统下,每个进程都有惟一的一个定时器,该定时器提供了以秒为单位的定时功能。在定时器设置的超时时间到达后,调用alarm的进程将收到SIGALRM信号。alarm系统调用的原型为:
unsigned int alarm(unsigned int seconds)
函数说明: alarm()用来设置信号SIGALRM在经过参数seconds指定的秒数后传送给目前的进程。如果参数seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。
返回值: 返回之前闹钟的剩余秒数,如果之前未设闹钟则返回0。
alarm()执行后,进程将继续执行,在后期(alarm以后)的执行过程中将会在seconds秒后收到信号SIGALRM并执行其处理函数。
mysleep的代码实现
#include<stdio.h>
#include <signal.h>
void myhandler(int signal)
{
printf("get a signal :%d\n",signal);
}
int mysleep(int timeout)
{
struct sigaction act,out;
act.sa_handler=myhandler;
sigemptyset(&act.sa_mask);//Init
act.sa_flags=0;
sigaction(SIGALRM,&act,&out);
alarm(timeout);
pause();
int ret=alarm(0);
sigaction(SIGALRM,&out,NULL);
return ret;
}
int main()
{
int ret= mysleep(5);
printf("%d\n",ret);
return 0;
}
但是这个简易版的mysleep是有bug的。这样就引入了竞态条件
现在重新审视“mysleep”程序,设想这样的时序:
1. 注册SIGALRM信号的处理函数。
调⽤用alarm(nsecs)设定闹钟。
内核调度优先级更⾼高的进程取代当前进程执⾏行,并且优先级更⾼高的进程有很多个,每个都要 执⾏行很长时间
nsecs秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,处于未决状态。
优先级更⾼高的进程执⾏行完了,内核要调度回这个进程执⾏行。SIGALRM信号递达,执⾏行处理函 数sig_alrm之后再次进⼊入内核。
返回这个进程的主控制流程,alarm(nsecs)返回,调⽤用pause()挂起等待。
可是SIGALRM信号已经处理完了,还等待什么呢?
alarm(timeout);
pause();
之前的问题就出在这里由于这是两步操作,这执行完alarm(timeout)之后很可能发生被切换出去去执行优先级更高的进程,因为我们设定的timeout秒之后闹钟超时了那么此时优先级较高的进程执行完之后再回到这个进程但调用pause()等待挂起,但是此时SIGALRM已经处理完了,根本就不用等待
为了解决这个问题必须保证在这两部操作之间不会去执行其他优先级更高的进程
如何解决上述问题呢?读者可能会想到,在调⽤用pause之前屏蔽SIGALRM信号使它不能提前递 达就可 以了。
看看以下方法可行吗?
1.屏蔽SIGALRM信号;
2. alarm(nsecs);
3. 解除对SIGALRM信号的屏蔽;
4. pause(); 从解除信号屏蔽到调⽤用pause之间存在间隙,SIGALRM仍有可能在这个间隙递达。
要消除这 个间隙, 我们把解除屏蔽移到pause后⾯面可以吗?
1.屏蔽SIGALRM信号;
2. alarm(nsecs);
3. pause();
4. 解除对SIGALRM信号的屏蔽; 这样更不⾏行了,还没有解除屏蔽就调⽤用pause,pause根本不可能等到SIGALRM信号。要是 “解除信号屏蔽”和“挂起等待信号”这两步能合并成⼀一个原子操作就好了,这正是sigsuspend 函数的功 能。sigsuspend包含了pause的挂起等待功能,同时解决了竞态条件的问题,在对 时序要求严格的场合下都应该调⽤用sigsuspend⽽而不是pause。
int mysleep1(int timeout)
{
struct sigaction act,out;
sigset_t newmask,oldmask,suspmask;
act.sa_handler=myhandler;//自定义信号处理函数
sigemptyset(&act.sa_mask);//Init
act.sa_flags=0;
sigaction(SIGALRM,&act,&out);
sigemptyset(&newmask);
sigaddset(&newmask,SIGALRM);//添加SIGALRM到newmask信号集中
sigprocmask(SIG_BLOCK,&newmask,&oldmask);//设置信号屏蔽字
alarm(timeout);
suspmask=oldmask;
sigdelset(&suspmask,SIGALRM);
sigsuspend(&suspmask);//解除信号屏蔽”和“挂起等待信号
int ret=alarm(0);
sigaction(SIGALRM,&oldmask,NULL);
sigprocmask(SIG_SETMASK,&oldmask,NULL);
return ret;
}