上篇博客的最后我们说到了进程在接收到信号,信号处于未决的状态是,不是立刻就递达,去处理这个信号的,而是在合适的时候去处理,那这个合适的时候在什么时候呢?
由上图可以知道,其实我们前面所说的合适的时候就是在在内核中处理完某些异常或执行完某些系统调用之后,在即将返回用户模式之前,这个时间点去处理能够递达的信号,如果是默认处理,那么则终止该进程,如果是忽略此信号,那么不管它,如果是自定义动作,那么就返回用户模式执行完自定义动作之后再经过某些特殊的系统调用返回到内核,最终返回到上次由于某些异常或执行某些系统调用的地方继续向下执行。
我们发现,其实在处理默认动作与忽略信号的时候是非常简单的。而执行自定义动作的时候,是要返回用户模式处理完毕后,再次返回到内核模式下。其实处理自定义动作的这个过程就叫做信号的捕捉。
信号捕捉函数
signal函数
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
其中第一个参数为几号信号,第二个参数为自定义的处理动作,这是一个函数指针。这个函数在上篇博客中使用到过,这里不做赘述。
sigaction函数
#include <signal.h>
int sigaction(int signo, const struct sigaction* act, struct sigaction* ocat);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,失败返回-1.
参数:
signo:是指定信号的编号
act:如果act非空,那么根据act的内容作为该信号的处理动作,即自定义动作
oact:如果oact非空,则通过oact来传出该信号原来的处理动作。act与oact都指向sigaction结构体
当某个信号处理函数被调用的时候,内核将自动将当前信号加入进程的信号屏蔽字当中,当信号处理函数调用结束返回时,自动恢复到原来的信号屏蔽字。这是为了防止在处理某个信号的时候,如果这种信号再次产生,那么它就会被阻塞到当前调用结束。
pause函数
#include <unistd.h>
int pause(void);
作用:pause函数调用时,会让进程一直维持挂起状态,直到有信号递达。如果信号的处理动作为默认动作,那么则终止进程,pause函数没有机会返回。如果为忽略动作,则进程继续挂起,pause不返回。如果为自定义动作,那么在调用了信号处理函数后,pause返回-1。所有pause只有出错的返回值。
这样,我们就可以用pause函数与之前所说的alarm函数来写一个我们的sleep函数。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sig_alarm(int signo)
{
}
int my_sleep(int t)
{
struct sigaction o;
struct sigaction n;
int unslept = 0;
n.sa_handler = sig_alarm;
sigemptyset(&n.sa_mask);
n.sa_flags = 0;
sigaction(SIGALRM, &n, &o);
alarm(t);
pause();
unslept = alarm(0);
sigaction(SIGALRM, &o, NULL);
return unslept;
}
int main()
{
while(1)
{
my_sleep(5);
printf("5 seconds passed\n");
}
return 0;
}
我们知道进程的运行不是一直的,它是调度器不断的切换,不断的调整,各个进程都能够持久的运行。而如果在设置完闹钟以后,内核恰好到了切换进程的时候,这个时候切换了一个优先级更高的别的进程,而这个进程执行时间又很长,而新的进程执行完毕之后,闹钟已经响了,而内核要调度回这个进程执行,这时候SIGALRM已经处于未决状态,那么这个进程将直接进行信号处理函数,处理完毕后,再执行后面的代码,但是这个时候pause才挂起等待,那么它还能等待什么信号呢?
sigsuspend函数
我们上面由于内核的调度,使得别的进程开始了执行,t秒过后,闹钟响了,发送信号SIGALRM,使其处于未决状态。别的进程执行完毕以后,切换至该进程,此时处于内核态,而发现了SIGALRM信号处于未决状态,接着就会处理这个信号,所以导致了最后pause的永久挂起。
那么怎么解决呢?我们如果能够在前面先屏蔽掉SIGALRM信号,这样的话,在回到内核以后发现未决后,由于阻塞的原因,并不会递达这个信号。而在返回用户模式后,在进行解除屏蔽并且挂起进程,这样的话就不会存在刚才的情况。而我们的sigsuspend函数恰好就能实现这个。
#include <signal.h>
int sigsuspend(const sigset_t* sigmask);
这个函数就是在调用的时候,临时解除某个信号的屏蔽,然后挂起进程。在这个函数调用返回后,在将信号屏蔽字恢复到原来的值。
也就是在我们之前的sleep函数内,在开始先屏蔽掉SIGALRM信号,接着在后面调用sigsuspend函数即可。这样就可以让我们解除信号屏蔽与挂起进程同步执行。避免了其他问题的出现。
可重入函数
举一个我们数据结构的例子,在我们模拟实现单链表的时候,有一个操作为头插,就是将一个新节点插入到头部,然后改变头指针的指向。这分为两个动作,倘若,在我们执行时,我们首先将一个新节点插入到头部,在准备进行改变头指针的指向的时候,这时候由于某些异常或是硬件中断,进程不得不到内核中去处理这些问题,处理完问题后,发现有信号可以被递达且该信号执行动作为自定义动作,这时候返回到用户模式执行自定义动作,而自定义动作恰好是一个给链表头插的函数,完成自定义动作后,经过特殊的系统调用返回到内核,再经内核返回当刚才中断处,继续向下执行,改变头指针的指向。这个时候是不是相当于我们信号的处理函数所执行的头插并没有插进去呢?到最后,实际上只有一个节点插入了这个链表当中去。
像上面这样,有可能在第一次调用还没返回时就再次进入该函数称作重入,即重复进入。而由于重入导致错乱,这样的函数称为不可重入函数。如果一个函数值访问自己的局部变量或参数,而重入后不会导致错乱,这种函数就称为可重入函数。
如果一个函数满足一下条件之一,那么就是不可重入函数:
- 调用了malloc或free函数,因为malloc也是全局链表来管理堆的。
- 调用了标准I/O库函数。标准库的很多实现都不可以重入的。
欢迎大家共同讨论,如有错误及时联系作者指出,并改正。谢谢大家!