在另一篇文章Linux信号中,介绍了信号的产生与处理方式,以及一系列信号集函数的使用。
本文使用信号机制,模拟实现sleep函数并了解竞态条件。
在此之前先介绍一波需要用到的函数。
sigaction函数
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
可以读取和修改于指定信号相关联的处理动作。
参数 signnum 为指定信号的编号。
若act指针非空,则根据 act 修改信号的处理动作,oldact可以为空,或者传出原来的处理动作。act和oldact都指向下面的结构体:
struct sigaction
{
void (*sa_handler) (int) // 信号处理函数 SIG_IGN 表示忽略 SIG_DFL 表示默认动作
sigset_t sa_mask; // 额外要屏蔽信号集
int sa_flags; // 一般为0
void (*sa_sigaction)(int, siginfo_t *, void *); // 实时信号处理函数
}
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字。保证在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调动信号处理函数时,还需要屏蔽别的信号,则可以通过sa_mask 指定。
pause 函数
#include
int pause(void);
挂起调用进程直到有信号递达。 如果信号的处理动作是终止进程,则进程终止。如果信号的处理动作是忽略,则进程继续处于挂起状态。只有信号的处理动作是捕捉pause函数才会返回。
首先来实现 sleep 版本1:
1. 调用sigaction()
捕捉信号SIGALRM
2. 调用alarm()
设定闹钟
3. 调用pause()
挂起等待
4. 取消闹钟
5. 恢复捕捉动作
#include
#include
// 什么事情也不做
void handler(int signum)
{}
unsigned mysleep(unsigned seconds)
{
// 1. 捕捉信号 SIGALRM
struct sigaction act, oldact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, &oldact); // 设置act 并传出原来的act
// 2. 设定闹钟
alarm(seconds);
// 3. 挂起等待信号递达
pause();
// 4. 取消闹钟
unsigned ret = alarm(0);
// 恢复捕捉
sigaction(SIGALRM, &oldact, NULL);
return ret;
}
int main()
{
while(1)
{
printf(" 我正在睡觉 zzz \n");
mysleep(1);
}
return 0;
}
关于mysleep1有几个问题:
Q1:信号处理函数handler函数什么都不干,为什么还要注册它作为SIGALRM的处理函数?不注册信号处理函数可以吗*?
答:不可以。因为pause() 函数使进程挂起等待,直到有信号递达并且要执行自定义的信号处理函数才有机会返回。
Q2:为什么在mysleep函数返回前要恢复SIGALRM信号原来的sigaction?
答:main函数作为调用者只想睡一下觉,没叫你在它睡觉的时候把它打的鼻青脸肿的,所以调用之前什么样就给恢复成什么样子。
Q3:mysleep函数的返回值表示什么含义? 什么情况下返回非0值?
答:mysleep 返回值表示闹钟剩余时间。 在取消闹钟时,上一个闹钟返回为0时。
重新审视上面代码,会发现有一个bug,假如在设定闹钟后,出现大量优先级较高的进程需要执行,测试该进程就会被切出去,即CPU资源被分配给了别的进程,如果时间很长的话,当该进程再次或者CPU资源的时候,闹钟时间已过,而pause永远不会返回。
出现这个问题的原因是系统执行代码的时序是不确定的。如果在写程序时考虑不周密,可能由于时序问题而导致错误,这叫做竞态条件(Race Condition)。
我们可以在设定闹钟之前,屏蔽信号 SIGALRM, 在让进程挂起时,解除对该进程的屏蔽,然后在让进程挂起等待信号递达。可以利用 sigsuspend函数 帮我们圆梦。
#include
int sigsuspend(const sigset_t *sigmask);
和pause 函数一样,该函数没有成返回值,只有执行一个信号处理函数之后,才会返回。
调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某个信号的屏蔽,然后挂起等待,返回时,进程的信号屏蔽字恢复为原来的值。
下面的从新实现的mysleep2:
1. 捕捉SIGALRM信号
2. 屏蔽SIGALRM信号,让该信号处于未决状态,直接解除对该信号的屏蔽
3. 调用alarm()
设定闹钟
4. 调用sigsuspend( )
临时解除型号,并挂起等待执行完信号处理函数返回
5. 取消闹钟
6. 恢复信号捕捉
7. 恢复信号屏蔽
#include
#include
// 什么事情也不做
void handler(int signum)
{}
unsigned mysleep(unsigned seconds)
{
struct sigaction act, oldact;
sigset_t newmask, oldmask, suspmask;
// 1. 信号捕捉
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, &oldact);
// 2. 信号屏蔽
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
// 3. 设定闹钟
alarm(seconds);
// 4. 临时解除屏蔽并挂起等待
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM); // 确保SIGALRM 不会被阻塞
sigsuspend(&suspmask);
// 5. 取消闹钟
unsigned ret = alarm(0);
// 6. 恢复捕捉动作
sigaction(SIGALRM, &oldact, NULL);
// 7. 恢复信号屏蔽
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return ret;
}
int main()
{
while(1)
{
printf("我正在睡觉。。\n");
mysleep(1);
}
return 0;
}