linux信号系列文终结篇:信号的捕捉(含mysleep的实现)

来源:互联网 发布:易语言魔域登陆器源码 编辑:程序博客网 时间:2024/06/15 01:02

高能预警


本文主要介绍了信号捕捉的概念和方法,并用相关函数实现了与系统sleep函数功能类似的mysleep程序。

本文主要内容有:

1.信号捕捉的概念及其在内核中的实现机制

2.信号捕捉相关函数介绍

3.mysleep的实现(1.0版)

拓展知识:
竞态条件(mysleep 2.0版)

————>全文阅读大概需要8min(我真的发四)<——


信号捕捉


本系列文第一篇和第二篇中都讲到了进程对信号递达后的三种处理方式:

1.忽略

2.默认(终止)

3.自定义(捕捉)

小伙伴们可以点击这里阅读:linux:进程中信号的“3种状态 And 3张表”

我们知道,信号在递达给进程之后,后者并不是马上对它进行处理,而是要等待合适的时机。这个时机就是:内核态—>信号处理—>用户态。类似的过程还有线程切换。

很显然,在进程对信号的3种处理方式中,忽略、缺省这两种方式最简单,也最容易实现。而自定义处理方式不但要定义相应的信号处理函数。而且由于信号处理函数的代码是在用户空间的,所以处理过程比较复杂

信号捕捉:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

那么内核是怎么实现信号的捕捉呢?我们先来看一张图:

1

上图就是内核对信号捕捉方式的处理流程,一共4次切换。

解释如下:

1.用户程序注册了某信号的处理函数sighandler。
2.当前正在执行main函数,这时发生中断或异常切换到内核态。
3.在中断处理完毕后要返回用户态的main函数之前检查到有信号递达。
4.内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数(sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系, 是两个独⽴的控制流程)
5.sighandler函数返回后⾃动执⾏特殊的系统调用sigreturn再次进入内核态。
6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执⾏了。

到这里,你应该不难理解内核处理信号捕捉的流程了。其实,流程中的4次切换整体看起来像一个符号,你们有没有发现呢?,就是它:

2

4个交点就是4次切换,这样就可以更容易理解信号捕捉的处理流程了,怎么样,不是不很机智>.<


相关函数


1.sigaction函数:可以读取和修改与指定信号相关联的处理动作。

3

其中参数解释如下:

1.signo是指定信号的编号
2.若act指针非空,则根据act修改该信号的处理动作
3.若oact指针非空,则通过oact传出该信号原来的处理动作
4.act和oact指向sigaction结构体:

4

其中成员解释如下:

1.sa_handler是一个常数,将其赋值为SIG_IGN传给sigaction表示忽略信号;赋值为常数SIG_DFL 表示执行系统默认动作;赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册 了一个信号处理函数。

注:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。

2.如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号。则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

3.sa_flags字段包含⼀些选项,默认设置设为0。

下面编写一段代码用sigaction函数实现简单的信号捕捉:

5

执行效果:

6

其实博主在前两篇博客中也说了一个信号捕捉函数signal,但它仅仅只是实现信号捕捉,sigaction函数只需要修改结构体参数就可以实现它的功能。后者的功能更强大,可以实现3种处理动作。

2.pause函数:进程挂起直到有信号递达,只有出错返回值

7

pause函数使调用进程挂起直到有信号递达。

返回值说明:

1.如果信号的处理动作是终止进程,则进程终止。此时pause函数没有机会返回
2.如果信号的处理动作是忽略,则进程继续处于挂起状态。pause不返回
3.如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为 EINTR,。

所以pause只有出错的返回值(exec函数组也一样)。错误码 EINTR表示“被信号中断”


Code:实现mysleep(V 1.0)


代码:利用pause和alarm函数函数及信号捕捉实现sleep函数的机制。

8

其中,在pause()和alarm(0)之间,内核完成了这些事情:

1.timeout秒之后,闹钟超时,内核发SIGALRM信号给这个进程。
2.从内核态返回这个进程的用户态之前处理未决信号,发现有SIGALRM信号。其处理函数是handler。
3.切换到⽤用户态执行handler函数,进入handler函数时SIGALRM信号被自动屏蔽。从handler函数返回时SIGALRM信号自动解除屏蔽。然后自动执行系统调用sigreturn再次进入内核,再返回用户态继续执行进程的主控制流程(main函数调用的mysleep函数)。

9

执行结果:

10


竞态条件和mysleep(V 2.0)


前方高能预警,一大片文字即将来袭。。。

首先,上边的代码虽然运行无误,但它是有bug的。

现在让我们重新审视“mysleep”程序,上面的程序中,先注册了SIGALRM信号的处理函数,然后调用alarm(timeout)设定闹钟。

假设内核调度优先级更高的进程取代当前进程执行,并且优先级更高的进程有很多个,每个都要执行很长时间 。

timout秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,此时该信号处于未决状态。

优先级更⾼的进程执⾏完了,内核要调度回这个进程执行。SIGALRM信号递达,执行处理函数handler之后再次进入内核。然后返回这个进程的主控制流程,alarm(timeout)返回。调用pause()挂起等待。

那么问题来了:此时SIGALRM信号已经处理完了,还等待什么呢?

出现这个问题的根本原因是系统运行的时序(Timing)并不像我们写程序时所设想的那样。

虽然alarm(timeout)紧接着的下⼀行就是pause(),但是无法保证pause()一定会在调用 alarm(timeout)之 后的timeout秒之内被调用。

也就是说,如果在pause函数被调用之前被进程操作系统切换走,由于程序优先级问题,操作系统让它再次回来时此时设定的时间已过,信号已被处理。继续执行pause时就不会收到alarm信号,进程将永远被挂起。

由于异步事件在任何时候都有可能发生(这⾥的异步事件指出现更高优先级的进程),如果我们写程序时考虑不周密,就可能由于时序问题而导致错误。这叫做竞态条件 (Race Condition)。

如何解决上述问题呢?

这里有两个方案:

方案一,把程序执行流程改为:

1.屏蔽SIGALRM信号;
2. alarm(nsecs);
3. 解除对SIGALRM信号的屏蔽;
4. pause();

但这样做还有问题,从解除信号屏蔽到调用pause之间存在间隙,SIGALRM仍有可能在这个间隙递达

要消除这 个间隙, 我们把解除屏蔽移到pause后⾯,这就是方案二:

1.屏蔽SIGALRM信号;
2. alarm(nsecs);
3. pause();
4. 解除对SIGALRM信号的屏蔽;

但这样更不行了,还没有解除屏蔽就调用pause,pause根本不可能等到SIGALRM信号。

所以,要是 “解除信号屏蔽”和“挂起等待信号”这两步能合并成一个原子操作就好了,巧了,正好有一个函数可以实现这个功能,就是sigsuspend 函数

15

sigsuspend包含了pause的挂起等待功能,同时解决了竞态条件的问题。在对 序要求严格的场合下都应该调用sigsuspend而不是pause。

参数介绍:

1.调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某个信号的屏蔽,然后挂起等待。
2.当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。

下面就用sigsuspend重新实现mysleep函数(2.0版):

代码:

22

注:SIGALRM有两次阻塞和解除阻塞的过程。

当然,程序依旧运行成功:

20


The End


这个系列终于更新完了,好累呀,先吃口西瓜压压惊。。。

原创粉丝点击