在实现mysleep函数之前,先介绍一下一会儿程序中要用到的函数。
信号捕捉函数:sigaction函数
sigaction 函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)。
他是POSIX的信号接口,而signal()是标准C的信号接口(如果程序必须在非POSIX系统上运行,那么就应该使用这个接口)给信号signum设置新的信号处理函数act, 同时保留该信号原有的信号处理函数oldact。调用成功返回0,出错返回-1.
#include <signal.h>
int sigaction (int signo,const struct sigaction *restrict act,
struct sigaction *restrict oact);
结构sigaction 定义如下:
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flag;
void (sa_sigaction)(int,siginfo_t ,void *);
};
sa_handler字段:包含一个信号捕捉函数的地址。当SIG_IGN传给sigaction表示忽略信号;赋值为常数SIG_DFL表示执行系统默认动作;赋值为函数指针表示用自定义函数捕捉信号,或者说由内核注册了一个信号处理函数,该函数的返回值为void,可以带int参数,通过参数可以得知当前信号的编号这样就可以用同一个函数处理多个信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统调用。
sa_mask字段:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
sa_flag字段:是一个选项,因为下面的程序我们不需要这个字段,所以默认给0 SA_INTERRUPT 由此信号中断的系统调用不会自动重启
SA_RESTART 由此信号中断的系统调用会自动重启 SA_SIGINFO
提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针
最后一个参数是一个替代的信号处理程序,当设置SA_SIGINFO时才会用他。
计数器:alarm函数
include <unistd.h>
unsigned int alarm(unsigned int seconds);/*指定秒数*/
alarm函数是设置一个计时器, 在计时器超时的时候, 产生SIGALRM信号. alarm也称为闹钟函数,一个进程只能有一个闹钟时间。
如果alarm函数计时完毕发出SIGALRM信号后,系统不忽略或捕捉此信号,
那么就执行该信号的默认处理操作—-终止调用该alarm函数的进程。
它的主要功能是设置信号传送闹钟。其主要功能用来设置信号SIGALRM在经过seconds指定的秒数后传送给目前的进程。返回值:
alarm函数的作用和闹钟的作用一样,设置信号SIGALRM在经过seconds指定的秒数后传送给目前的进程。
返回值为0,说明经过seconds秒后闹钟准时响了,就是说操作系统在senconds秒之后准时的给当前进程发送了SIGALRM信号。
返回值大于0,则说明由于某些原因闹钟提前响了,即操作系统由于某些原因在没有到senconds秒的时候给当前进程发送了SIGALRM信号,而返回值就是之前闹钟剩余的秒数(就是离senconds秒还差多少秒)。就好比,我要休息一会儿,设闹钟30分钟后响,但是由于我刚睡了20分钟就被吵醒了,还想再多睡一会,于是就重新设置闹钟为15分钟后响,那么之前闹钟剩余的时间就是10分钟。
如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍是以前设定的闹钟时间还余下的秒数。
pause函数
pause函数使调用进程挂起直到有信号递达。
如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;
如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;
如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR(错误码EINTR表示被信号中断),
所以pause只有出错的返回值。
普通版本的Sleep函数的实现
#include<stdio.h>
#include<signal.h>
void myhandler(int sig)
{}
int mysleep(int timeout)
{
struct sigaction act,oact;
act.sa_handler = myhandler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM,&act,&oact);
alarm(timeout);
pause();
int ret = alarm(0);
sigaction(SIGALRM,&oact,NULL);
return ret;
}
int main()
{
while(1)
{
printf("mysleep start...\n");
mysleep(3);
printf("mysleep success\n");
}
return 0;
}
程序执行过程:
1)main函数调用mysleep函数。mysleep函数调用sigaction注册了SIGALRM信号的处理函数myhandler。这里的myhandler函数并没有做什么工作,但不能少,因为SIGALRM信号的默认处理是终止进程。
2)调用alarm(timeout)设定闹钟。
3)调用pause等待,内核切换到别的进程运行。
4)timeout秒之后,闹钟超时,内核发SIGALRM给这个进程。
5)从内核态返回这个进程的用户态之前处理未决信号,发现有SIGALRM信号,其处理函数是myhandler。
6)于是切换到用户态执行myhandler函数,进入myhandler函数时SIGALRM信号被自动屏蔽, 从myhandler函数返回时SIGALRM信号自动解除屏蔽。然后自动执行系统调用sigreturn再次进入内核,再返回用户态继续执行进程的主控制流程,即为main函数调用mysleep函数处。
7) pause函数返回-1,然后调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号以前的处理动作。
运行结果:
正常情况下,我们都会这么写,而且从结果看与我们的预期相符,But~~~~~~~~这里面有个很大的问题
现在重新审视mysleep函数,设想这样的时序:
1. 注册SIGALRM信号的处理函数。
2. 调用alarm(timeout)设定闹钟。
3. 内核调度优先级更高的进程取代当前进程执行,并且优先级更高的进程有很多个,每个都要执行很长时间
4. timeout秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,该信号处于未决状态。
5. 优先级更高的进程执行完了,内核要调度回这个进程执行。SIGALRM信号递达,执行处理函数myhandler之后再次进入内核。
6. 返回这个进程的主控制流程,alarm(timeout)返回,调用pause()挂起等待。
7. 可是SIGALRM信号已经处理完了,还等待什么呢?
出现这个问题的根本原因是系统运行的时序(Timing)并不像我们写程序时所设想的那样。虽然alarm(timeout)紧接着的下一行就是pause(),但是无法保证pause()一定会在调用alarm(timeout)之后的timeout秒之内被调用。由于异步事件在任何时候都有可能发生(这里的异步事件指出现更高优先级的进程),如果我们写程序时考虑不周密,就可能由于时序问题而导致错误,这叫做竞态条件(Race Condition)。
如何解决上述问题呢?读者可能会想到,在调用pause之前屏蔽SIGALRM信号使它不能提前递达就可以了。看看以下方法可行吗?
1. 屏蔽SIGALRM信号;
2. alarm(timeout);
3. 解除对SIGALRM信号的屏蔽;
4. pause();
从解除信号屏蔽到调用pause之间存在间隙,SIGALRM仍有可能在这个间隙递达。要消除这个间隙,我们把解除屏蔽移到pause后面可以吗?
1. 屏蔽SIGALRM信号;
2. alarm(timeout);
3. pause();
4. 解除对SIGALRM信号的屏蔽;
这样更不行了,还没有解除屏蔽就调用pause,pause根本不可能等到SIGALRM信号。要是“解除信号屏蔽”和“挂起等待信号”这两步能合并成一个原子操作就好了,这正是sigsuspend函数的功能。sigsuspend包含了pause的挂起等待功能,同时解决了竞态条件的问题,在对时序要求严格的场合下都应该调用sigsuspend而不是pause。
sigsuspend函数
include <signal.h>
int sigsuspend(const sigset_t *sigmask);
和pause一样,sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。
调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。
以下用sigsuspend重新实现mysleep函数:
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void myhandler(int sig)
{}
int mysleep(int timeout)
{
struct sigaction act, oact;
sigset_t newmask, oldmask, suspmask;
act.sa_handler = myhandler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM, &act, &oact);
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);//添加SIGALRM信号
sigprocmask(SIG_BLOCK, &newmask, &oldmask); //添加SIGALRM信号屏蔽字
alarm(timeout);
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);//删除SIGALRM信号
sigsuspend(&suspmask); //解除对SIGALRM信号的屏蔽同时挂起等待
int ret = alarm(0);
sigaction(SIGALRM, &oact, NULL);
sigprocmask(SIG_SETMASK, &oldmask, NULL); //对信号屏蔽字复位
return ret;
}
int main(void)
{
while(1)
{
printf("mysleep start\n");
mysleep(3);
printf("mysleep success\n");
}
return 0;
}
如果在调用mysleep函数时SIGALRM信号没有屏蔽:
1. 调用sigprocmask(SIG_BLOCK, &newmask, &oldmask);时屏蔽SIGALRM。
2. 调用sigsuspend(&suspmask);时解除对SIGALRM的屏蔽,然后挂起等待待。
3. SIGALRM递达后suspend返回,自动恢复原来的屏蔽字,也就是再次屏蔽SIGALRM。
4. 调用sigprocmask(SIG_SETMASK, &oldmask, NULL);时再次解除对SIGALRM的屏蔽。