Linux时序竞态问题(sleep函数的实现)
- 2020 年 3 月 9 日
- 筆記
时序竞态是指同样的程序,多次调用运行的结果不同,这是由于争夺系统资源所造成的。比如说我们要使用alarm和pause函数来实现一个sleep的功能,那么由于alarm函数的实现过程并不是一个原子操作,那么随时可能被中断。比如说alarm了1秒,在这个过程中,进程失去了CPU,然后当该进程再次获得CPU的时候可能这个时间已经大于1秒了,那么对于alarm来说就已经发出了SIGALRM信号。此时往下继续调用pause函数的话,它会一直都收不到alarm发来的信号,所以导致进程的永久挂起。
为了解决这个问题,引用了sigsuspend函数。sigsuspend用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止。也就是说,进程执行到sigsuspend时,sigsuspend并不会立刻返回,进程处于TASK_INTERRUPTIBLE状态并立刻放弃CPU,等待UNBLOCK(mask之外的)信号的唤醒。进程在接收到UNBLOCK(mask之外)信号后,调用处理函数,然后还原信号集,sigsuspend返回,进程恢复执行。下面通过使用alarm和sigsuspend函数来实现sleep函数,代码中有详细的注释来解释说明:
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> void sig_alarm(int num){ // 空函数仅用来捕捉信号 } unsigned int mysleep(unsigned int n_seconds){ struct sigaction n_sig, o_sig; // 一个用来设定,一个用来备份 sigset_t nsigmask, osigmask, tmpsigmask; // 用来设定,备份,替换 /*初始化*/ n_sig.sa_handler = sig_alarm; // 设定信号的捕捉动作 n_sig.sa_flags = 0; sigemptyset(&n_sig.sa_mask); sigaction(SIGALRM, &n_sig, &o_sig); // 设定SIGALRM的处理行为 /*对SIGALRM信号设置阻塞,防止在挂起前出现递达态*/ sigemptyset(&nsigmask); sigaddset(&nsigmask, SIGALRM); sigprocmask(SIG_BLOCK, &nsigmask, &osigmask); // 设置新mask,备份旧mask alarm(n_seconds); // 设置n秒的闹钟 /*对原有的mask取消SIGALRM的阻塞*/ tmpsigmask = osigmask; sigdelset(&tmpsigmask, SIGALRM); sigsuspend(&tmpsigmask); // 挂起等待没有被阻塞的信号 sigprocmask(SIG_SETMASK, &osigmask, NULL); // 还原mask sigaction(SIGALRM, &o_sig, NULL); // 还原处理行为 unsigned int unsletp = alarm(0); // 获取剩余的睡眠时间 /*因为如果没有睡眠够n_seconds就收到了一个唤醒信号,那么再次调用alarm会返回剩余的时间*/ return unsletp; } int main(void) { int ans = mysleep(5); printf("%dn", ans); return 0; }