先认识一下sleep函数
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
- 函数说明:sleep()会令目前进程暂停,直至达到参数seconds所指定的时间,或被信号中断。
- 返回值:若进程暂停到参数seconds所指定的时间则返回0,若有信号中断则返回剩余秒数。
这里再简答介绍一下模拟实现sleep函数需要的几个函数:
(1)sigaction:查询或设置信号处理方式
函数原型:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
(2)alarm:调用alarm设置一个闹钟,zaiseconds秒之后给当前进程发送SIGALRM信号,该信号的默认处理动作时终止当前进程。
函数原型:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
(3)pause:使调用进程挂起,直至有信号递达。
函数原型:
#include <unistd.h>
int pause(void);
通过以上的几个函数,就可以模拟实现sleep函数了
- 首先调用sigaction注册SIGALRM信号的处理函数sig_handler;
- 调用alarm(seconds)设定闹钟;
- 调用pause进程挂起等待;
- seconds秒之后闹钟超时,内核发SIGALRM给这个进程;
- 从内核态返回用户态之前处理未决信号,发现有SIGALRM信号,其处理函数是sig_handler;
- 切换到用户态执行捕捉函数,进入sig_handler函数时SIGALRM信号会被自动屏蔽,从sig_handler函数返回时SIGALRM自动解除屏蔽,然后自动执行sigreturn再次进入内核
- 再返回用户态继续执行进程的主控制流程;
- pause函数返回-1,调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号以前的处理动作。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void sig_handler(int signo)
{
}
unsigned int mysleep(unsigned int seconds)
{
struct sigaction new,old;
unsigned int unslept = 0;
new.sa_handler = sig_handler;
sigemptyset(&new.sa_mask);
new.sa_flags = 0;
sigaction(SIGALRM, &new, &old);
alarm(seconds);
pause();
unslept = alarm(0);
sigaction(SIGALRM, &old, NULL);
return unslept;
}
int main()
{
while(1)
{
printf("Hello, world\n");
mysleep(1);
}
return 0;
}
但是这样的流程是存在错误的,假设第二步调用alarm函数之后,内核调度优先级更高的进程取代了当前进程的执行,并且优先级更高的进程有很多,每个都执行很长的时间,那么此时会发生什么?
- seconds秒之后闹钟超时,内核发送SIGALRM信号给这个进程,处于未决状态;
- 优先级更高的进程执行完毕,内核调度回这个进程,SIGALRM信号递达,执行处理函数之后再次进入内核;
- 返回这个进程的主控制流程,alarm返回,调用pause()挂起等待;这样就会一直挂起等待,与我们的要求不符了
出现这种问题的根本原因是系统运行的时序并不像我们写程序时向的那样,由于异步事件在任何事胡都有可能发生,如果我们写程序时考虑不周,就有可能出现错误,这叫做竞态条件。
那么如何解决呢?有的同学可能想到了这样:
- 屏蔽SIGALRM信号
- alarm(seconds)
- 解除对SIGALRM信号的屏蔽
- pause()
从解除信号屏蔽到调用pause之间存在间隙,SIGALRM仍有可能在这个间隙递达;如果我们能把上面的三四步合并成一步,成一个原子操作就可以了,这里sigsuspend函数就是这个功能了,我们先了解一下这个函数,然后在对mysleep进行改造。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
此函数也没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。调用此函数时,进程的信号屏蔽字由sigmask参数指定,可以通过sigmask来临时解除对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来是对该信号屏蔽,返回后仍然是屏蔽的。
以下就是我们改造后的mysleep函数了
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void sig_handler(int signo)
{
}
unsigned int mysleep(unsigned int seconds)
{ // 设定处理函数,捕捉信号
struct sigaction new,old;
unsigned int unslept = 0;
new.sa_handler = sig_handler;
sigemptyset(&new.sa_mask);
new.sa_flags = 0;
sigaction(SIGALRM, &new, &old);
// 屏蔽SIGALRM信号
sigset_t newmask, oldmask, suspmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);//屏蔽SIGALRM
// 设定闹钟
alarm(seconds);
// 调用sigsuspend临时解除屏蔽并挂起等待
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
sigsuspend(&suspmask);//解除对SIGALRM的屏蔽,然后挂起等待,SIGALRM递达后sigsuspend返回,
//自动恢复原来的屏蔽字,也就是在此屏蔽SIGALRM
// 取消设定的闹钟,返回剩余的时间
unslept = alarm(0);
sigaction(SIGALRM, &old, NULL);
// 再次解除对SIGALRM的屏蔽
sigprocmask(SIG_UNBLOCK, &oldmask, NULL);
return unslept;
}
int main()
{
while(1)
{
printf("Hello, world\n");
mysleep(1);
}
return 0;
}
volatile 是一个类型修饰符保证每次对变量的访问都是从内存,而不是别的地方;对于程序中存在多个执行流访问同一全局变量的按情况,volatile限定符是必要的。
可重入函数:一个函数被不同的控制流程调用,有可能第一次调用还没返回时就再次进入该函数,这称之为重入;如果一个函数因为冲入而造成错乱,这样的函数称为不可重入函数;反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。
符合以下条件的函数都是不可重入的:
- 调用了malloc或free,因为malloc也是⽤全局链表来管理堆的。
- 调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。