几个重要的函数
实现代码之前,先来学习几个函数。
- sigaction : 该函数可以读取和修改与指定信号相关联的处理动作。
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
返回值 : 调⽤成功则返回0,出错则返回- 1。
sig : sig是指定信号的编号。
act : 若act指针非空,则根据act修改该信号的处理动作。
oact : 若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
- pause : 该函数使调⽤进程挂起直到有信号递达。
int pause(void);
如果信号的处理动作是终⽌进程,则进程终⽌,pause函数没有机会返回
如果信号的处理动作是忽略,则进程继续处于挂起状态,pause
不返回
如果信号的处理动作是捕捉,则调⽤了信号处理函数之后pause返回-1,errno设置为EINTR, 所以pause只有出错的返回值(错误码EINTR表示“被信号中断”)
- alarm : 调⽤alarm函数可以设定⼀个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终⽌当前进程。
unsigned int alarm(unsigned int seconds);
返回值 : 0或者是以前设定的闹钟时间还余下的秒数。
如果seconds值为0,表⽰取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
普通版本mysleep
下面,简单实现一个mysleep函数。
程序执行过程:
- mysleep调⽤sigaction注册了SIGALRM信号的处理函数myhandler。这里的myhandler函数什么都没做,但是不可缺少,否则SIGALRM信号默认终止进程,而我们并不期望程序就此终止。
- 调⽤alarm(timeout)设定闹钟。
- 调⽤pause等待,内核切换到别的进程运⾏。
- timeout秒之后,闹钟超时,内核发SIGALRM信号给这个进程。
- 从内核态返回这个进程的⽤户态之前处理未决信号,发现有SIGALRM信号,其处理函数是myhandler。
- 切换到⽤户态执⾏myhandler函数,进⼊myhandler函数时SIGALRM信号被⾃动屏蔽,从myhandler函数返回时SIGALRM信号⾃动解除屏蔽。然后⾃动执⾏系统调⽤sigreturn再次进⼊ 内核,再返回⽤户态继续执⾏进程的主控制流程即main函数调⽤的mysleep函数。
- pause函数返回-1,然后调⽤alarm(0)取消闹钟,调⽤sigaction恢复SIGALRM信号以前的处理动作。
我们的预期:该程序运行结果 : 每3秒打印一次“mysleep success!”。
那么,思考一下,有没有可能出现意料之外的结果呢?
假设一种情形:
- 闹钟设定之后,内核调度优先级更⾼的进程取代了当前进程执⾏,并且优先级更⾼的进程有很多个,每个都要执⾏很长时间。当定时时间到之后,闹钟超时,内核发送SIGALRM信号给这个进程,此时该信号处于未决状态。当优先级更⾼的进程执⾏完了,内核调度回这个进程执⾏。这时,SIGALRM信号递达,执行处理函数myhandler之后再次进⼊内核。返回这个进程的主控制流程,alarm(timeout)返回,调⽤pause()挂起等待。
- 这个时候就出现问题了,SIGALRM信号已经递达,pause将一直挂起等待。函数无法返回。
出现这个问题的根本原因是系统运⾏的时序(Timing)并不像我们写程序时所设想的那样。虽然alarm(timeout)紧接着的下⼀⾏就是pause(),但是⽆法保证pause()⼀定会在调⽤alarm(timeout)之 后的timeout秒之内被调⽤。
这种因为程序的时序问题,调度优先级或进程切换以及其他等原因导致进程运行结果与预期不相符的情况就叫做竞态条件 (Race Condition)
有人提出了以下两种解决方法。我们来看看:
在调⽤pause之前屏蔽SIGALRM信号使它不能提前递达。即:
仔细想想,其实这种是治标不治本的,因为解除信号屏蔽的瞬间,可能信号就将递达成功,然后pause挂起等待,仍然无效。
把解除屏蔽放到pause之后。即:
这种情景就更不靠谱了,pause挂起等待,而此时还没有解除对SIGALRM信号的屏蔽,也就是说,pause将一直等待下去,而SIGALRM信号一直未解除屏蔽。。
- 于是我们想,能不能把解除屏蔽和pause的挂等待功能原子性的一步实现呢?当然可以啦,发现这个问题的时候,各位大牛们就已经解决啦。
来看几个函数
- sigsuspend : sigsuspend函数包含了pause的挂起等待功能,同时解决了竞态条件的问题
int sigsuspend(const sigset_t *sigmask);
和pause⼀样,sigsuspend没有成功返回值,只有执⾏了⼀个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。
调⽤sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。
- sigprocmask : 调⽤函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1 。
如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
如果set是⾮空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
如果oset和set都是⾮空指针,则先将原来的信号屏蔽字备份到oset⾥,然后根据set和how参数更改信号屏蔽字。
假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
规避竞态条件mysleep
下面来看看使用sigsuspend函数如何实现mysleep。
如果在调⽤mysleep函数时SIGALRM信号没有屏蔽:
- 调⽤sigprocmask(SIG_BLOCK, &newmask, &oldmask)时屏蔽SIGALRM。
- 调⽤sigsuspend(&suspmask);时解除对SIGALRM的屏蔽,然后挂起等待待。
- SIGALRM递达后suspend返回,⾃动恢复原来的屏蔽字,也就是再次屏蔽SIGALRM。
- 调⽤sigprocmask(SIG_SETMASK, &oldmask, NULL);时再次解除对SIGALRM的屏蔽。
- 由此可见,使用sigsuspend函数可以很好地解决上述普通版本mysleep的问题——竞态条件。