信号的阻塞

来源:互联网 发布:冷血动物 感情 知乎 编辑:程序博客网 时间:2024/06/11 02:01
8.3.1 信号的处理方式
        在Linux系统下,信号的处理方式有3种:一是忽略信号;而是按照系统提供的缺省处理规则进行处理;三是捕捉信号,在程序中定义自己的信号处理函数,在信号处理函数中完成相应的功能。Linux系统分别为前两种方式提供了相应的宏定义:SIG_IGN和SIG_DFN。下面列出几个重要信号及其处理方式。
-----------------------------------------------------------------------------------------------------------------
信号名称          缺省处理方式                                信号产生原因/说明
-----------------------------------------------------------------------------------------------------------------
SIGHUP          终止进程                                        控制终端挂起或者退出
SIGINT            终止进程                                        <Ctrl>+<C>
SIGQUIT         终止进程并进行内核映像转储         <Ctrl>+<\>
SIGKILL          终止进程                                        不能阻塞、忽略、捕捉
SIGALRM        终止进程                                        定时器超时信号 
SIGTERM        终止进程                                        可以阻塞、捕捉
SIGCHLD        混略信号                                        子进程退出时向父进程发送该信号
SIGSTOP        暂停进程执行                                 不能阻塞、忽略、捕捉
-----------------------------------------------------------------------------------------------------------------

8.3.2 信号的阻塞处理
        信号的阻塞就是通知系统内核暂时停止向进程发送指定的信号,而是由内核对进程接收到的相应信号进行缓存排队,直到进程解除对相应信号的阻塞为止。一旦进程解除对该信号的阻塞,则缓存的信号将被发送到相应的进程。
        信号在几种情况下会进入阻塞状态。
1)系统自动阻塞:在信号的处理函数执行过程中,该信号将被阻塞,直到信号处理函数执行完毕,该阻塞将会解除。这种机制的作用主要是避免信号的嵌套。
2)通过sigaction实现人为阻塞:在使用sigaction安装信号时,如果设置了sa_mask阻塞信号集,则该信号集中的信号在信号处理函数执行期间将会阻塞。这种情况下进行信号阻塞的主要原因是:一个信号处理函数在执行过程中,可能会有其他信号到来。此时,当前的信号处理函数就会被中断。而这往往是不希望发生的。此时,可以通过sigaction系统调用的信号阻塞掩码对相关信号进行阻塞。通过这种方式阻塞的信号,在信号处理函数执行结束后就会解除。
3)通过sigprocmask实现人为阻塞:可以通过sigprocmask系统调用指定阻塞某个或者某几个信号。这种情况下进行信号阻塞的原因较多,一个典型的情况是:某个信号的处理函数与进程某段代码都要某个共享数据区进行读写。如果当进程正在读写共享数据区的过程中,一个信号过来,则进程的读写过程将被中断转而执行信号处理函数,而信号处理函数也要对该共享数据区进行读写,这样共享数据区就会发生混乱。这种情况下,需要在进程读写共享数据区前阻塞该信号,在读写完成后再解除该信号的阻塞。
        提示:在信号的接收过程中可能存在这样的情况:若干个相同的信号同时到达。通过上面的介绍可以知道,当信号处理函数正在执行时,同类信号将被阻塞处理。但是,如果此时信号处理函数还没有来得及执行,那么该同类信号就不会阻塞,在这种情况下,将会发生一种成为“信号合并”的现象。同时到达的同类信号将被合并处理,就像只有一个信号到达一样。
        被阻塞的信号的集合成为当前进程的信号掩码。每个进程都有惟一的信号掩码。为了对信号进行阻塞或者解除阻塞,Linux提供了专门的系统调用sigprocmask完成这一任务。该函数的原型为:
#include <signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
参数说明:
1)how:输入参数,设置信号阻塞掩码的方式。可以包括3种方式对信号的掩码进行设置,分别是阻塞信号的SIG_BLOCK、解除阻塞的SIG_UNBLOCK和设置阻塞掩码的SIG_SETMASK。
2)set:输入参数,阻塞信号集。当参数how为SIG_BLOCK时,该参数表明要阻塞的信号集;当how参数为SIG_UNBLOCK时,该参数表明要解除阻塞的信号集;当how参数为SIG_SETMASK时,该参数表明要阻塞的信号集。
3)oset:输出参数,原阻塞信号集。
返回值:
若成功,返回0;若失败,返回-1。

例8-3:编程实现下面功能:为进程安装SIGINT信号,先阻塞该信号,休眠10秒,再解除该信号的阻塞。
代码如下:
#include <stdio.h>
#include <signal.h>
//SIGINT信号处理函数
void CbSigInt(int signo)
{
//输出信号的值
printf("receive signal %d\n",signo);
}
void main()
{
//信号掩码结构变量,用于指定新的信号掩码
sigset_t mask;
//信号掩码结构变量,用于保存原来的信号处理掩码
sigset_t omask;
//安装SIGINT信号
signal(SIGINT,CbSigInt);
//清空信号掩码变量
sigemptyset(&mask);
//向掩码结构中增加信号SIGINT
sigaddset(&mask,SIGINT);
//阻塞SIGINT信号
sigprocmask(SIG_BLOCK,&mask,&omask);
//休眠10秒
sleep(10);
//解除SIGINT信号的阻塞
sigprocmask(SIG_SETMASK,&omask,NULL);
}
编译运行该程序,在进程休眠期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,注意观看信号是否被阻塞。在休眠结束后,验证刚才被阻塞的SIGINT信号是否被重新发送。

提示:在创建新的子进程时,子进程将继承父进程的信号掩码。

8.3.3 信号集的操作
        通过上节对信号阻塞的介绍可以知道,信号的阻塞实际上是对一个集合的操作。这个集合中可能包含多种信号,这就是信号集。信号集的数据类型为sigset_t,实际上是个结构体,它的定义如下所示。
typedef struct
{
unsigned long sig[_NSIG_WORDS];
}sigset_t;
        Linux系统提供了一系列函数对信号集进行操作。这些函数的原型如下所示。
#include <signal.h>
sigemptyset(sigset_t *set); //初始化由set指定的信号集,信号集里面的所有信号被清空;
sigfillset(sigset_t *set); //调用该函数后,set指向的信号集中将包含linux支持的64种信号;
sigaddset(sigset_t *set,int signo);                 //在set指向的信号集中加入signo信号;
sigdelset(sigset_t *set,int signo);                  //在set指向的信号集中删除signo信号;
sigismember(const sigset_t *set,int signo);     //判定信号signo是否在set指向的信号集中。
参数说明:
1)set:输入参数,信号集。
2)signo:输入参数,要增加或删除或判断的信号。
返回值:
1)对于sigismember函数:返回1表示信号属于信号集;返回0表示信号不属于信号集。
2)对于其他函数:若成功,返回0;若失败,返回-1。

8.3.4 未决信号的处理
        信号的未决是信号产生后的一种状态,是指从信号产生后,到信号被接收进程处理之前的一种过渡状态。由于信号的未决状态时间非常短,所以通常情况下,处于未决状态的信号非常少。如果程序中使用了sigprocmask阻塞了某种信号,则向进程发送的这种信号将处于未决状态。Linux提供了专门的函数sigpending获取当前进程中处于未决状态的信号。该函数的原型为:
#include <signal.h>
int sigpending(sigset_t *set);
参数说明:
1)set:输出参数,处于未决状态的信号集。
返回值:
若成功,返回0;若失败,返回-1。

例8-4:编程实现下面功能:为进程安装SIGINT信号,先阻塞该信号,休眠10秒,最后查看当前进程未决的信号。
代码如下:
#include <stdio.h>
#include <signal.h>
void main()
{
//信号掩码结构变量,用于指定新的信号掩码
sigset_t mask;
//信号掩码结构变量,用于保存原来的信号处理掩码
sigset_t omask;
//信号掩码结构变量,用于保存未决的信号集
sigset_t pendmask;
//清空信号掩码变量
sigemptyset(&mask);
//向掩码结构中增加信号SIGINT
sigaddset(&mask,SIGINT);
//阻塞SIGINT信号
sigprocmask(SIG_BLOCK,&mask,&omask);
//休眠10秒
sleep(10);
//获取当前未决的信号集
if(sigpending(&pendmask)<0)
{
perror("sigpending");
//解除SIGINT信号的阻塞
sigprocmask(SIG_SETMASK,&omask,NULL);
return;
}
//判断SIGINT是否在未决信号集中
if(sigismember(&pendmask,SIGINT))
printf("SIGINT signal is pending.\n");
else
printf("SIGINT signal is not pending.\n");
//解除SIGINT信号的阻塞
sigprocmask(SIG_SETMASK,&omask,NULL);
}
编译运行该程序,在进程休眠期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证该信号是否处于未决状态。

8.3.5 等待信号

8.3.5.1 pause
        在有些情况下,程序需要暂停执行,进入休眠状态,以等待信号的到来。这时可以使用pause系统调用。pause一旦被调用,则进程将进入休眠状态。之后,只有在进程接收到信号后,pause才会返回。pause的原型为:
#include <unistd.h>
int pause();
返回值:
pause的返回值永远是-1,错误码errno为EINTR。

例8-5:用pause编程实现等待SIGINT信号到来的功能。
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
//SIGINT信号处理函数
void CbSigInt(int signo)
{
//输出信号的值
printf("receive signal %d\n",signo);
}
void main()
{
//安装SIGINT信号
signal(SIGINT,CbSigInt);
//等待信号
pause();
}
编译运行该程序,在进程pause期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证该信号是否被处理。
注意:由于此测试程序没有屏蔽其他信号,因此任何一个信号的到来都能唤醒pause。

8.3.5.2 sigsuspend
        pause系统调用可以实现暂停进程的执行等待某个信号的到来,但是,如果在pause被调用之前,指定的信号到达进程,那么,在随后的pause调用中,假定不再有信号到来,则进程将进入无限期的等待中。为此Linux提供了功能更强大的sigsuspend以满足这种需求。sigsuspend的工作过程如下:
1)设置进程的信号掩码并阻塞进程。
2)收到信号,恢复原来的信号掩码。
3)调用进程设置的信号处理函数。
4)等待信号处理函数返回后,sigsuspend返回。
        上述四个步骤是一次性完成的,操作系统保证操作过程的原子性。特别需要注意的是第三步调用信号处理函数是由sigsuspend完成的。sigsuspend的原型为:
#include <signal.h>
int sigsuspend(const sigset_t *set);
参数说明:
1)set:输入参数,执行sigsuspend过程中需要被阻塞的信号集。
返回值:
sigsuspend的返回值永远是-1,错误码errno为EINTR。

例8-6:用sigsuspend编程实现等待SIGINT信号到来的功能。
代码如下:
#include <stdio.h>
#include <signal.h>
//SIGINT信号处理函数
void CbSigInt(int signo)
{
//输出信号的值
printf("receive signal %d\n",signo);
}
void main()
{
//信号掩码结构变量,用于指定新的信号掩码
sigset_t mask;
//安装SIGINT信号
signal(SIGINT,CbSigInt);
//设置信号集为所有信号,准备阻塞所有信号
sigfillset(&mask);
//从信号集中删除SIGINT信号,该信号为目标信号,不能被阻塞。
sigdelset(&mask,SIGINT);
//等待SIGINT信号
sigsuspend(&mask);
}
编译运行该程序,在进程suspend期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证该信号是否被处理。
注意:由于此测试程序屏蔽了除SIGINT外的所有其他信号,因此只有在收到SIGINT信号后进程才会退出。

提示:
        一个阻塞式系统调用在执行过程中如果没有符合条件的数据,将进入休眠状态,直到有符合条件的数据到来。比较典型的例子是从网络连接上读取数据,如果没有数据到来,那么这个读操作将会阻塞。此时有两种情况可以中断该读操作的执行:一是网络上有数据到来,则读操作将获取到所需要的数据后返回;二是当前进程收到了某个信号,此时,读操作将被中断并返回失败,错误码errno为EINTR。

8.3.6 信号处理函数的实现
        信号处理函数是进程接收到信号后要执行的函数,该函数应该尽量简洁,一般不要执行过多的代码。最好只是改变一个外部标志变量的值,而在另外的程序中不断的检测该变量,繁杂的工作都留给那些程序去做。在定义信号处理函数时,应该特别注意以下几点。
1)如果信号处理程序中需要存取某个全局变量,则应该在程序中使用关键字volatile声明此变量。通知编译器,在编译过程中不要对该变量进行优化。
2)如果在信号处理函数中调用某个函数,那么那么该函数必须是可重入的,或者保证在信号处理函数执行期间不会有信号到达进程。Linux系统下存在许多不可重入的函数,如malloc、gethostbyname等。
        在信号处理函数里,有时需要用到长跳转的操作。所谓长跳转,就是从信号处理函数直接跳转到函数体外指定的代码位置继续运行。Linux系统提供了两个函数实现该功能:设置跳转点的sigsetjmp和执行跳转的siglongjmp。sigsetjmp用来设置跳转点,在成功调用后,sigsetjmp语句所在的位置就是跳转点,这个位置指针将被保存到sigsetjmp的第一个参数中。这个两个函数的原型为:
#include <setjmp.h>
int sigsetjmp (struct __jmp_buf_tag env[1], int savemask);
void siglongjmp(sigjmp_buf env,int val);
参数说明:
1)env[1]:输出参数,该参数实际上是一个结构体的指针。该结构体中包含了长跳转指针,是否保存信号掩码及保存的信号掩码值等信息。对于应用人员来说,该结构是透明的。
2)env:输入参数,等效于env[1]。
3)savemask:是否保存信号掩码。如果该参数非零,则在调用sigsetjmp后,当前进程的信号掩码将被保存;在调用siglongjmp时,将恢复由sigsetjmp保存的信号掩码。
4)val:当由siglongjmp调用sigsetjmp时,该参数将会被隐含传给sigsetjmp作为返回值。如果val等于0,那么sigsetjmp函数将忽略该参数而返回其他非零值。
返回值:
1)sigsetjmp函数:若返回0,表明sigsetjmp不是由siglongjmp调用的;若返回非零值,则是由siglongjmp调用而返回。

例8-7:编程实现捕捉SIGINT信号,在信号处理函数中用长跳转跳转至主程序。
代码如下:
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
//全局变量,用于保存跳转点及其现场
static sigjmp_buf jmpbuff;
//SIGINT信号处理函数
void CbSigInt(int signo)
{
//输出信号的值
printf("\nreceive signal %d\n",signo);
//长跳转到jmpbuff(即sigsetjmp函数入口处),并平衡堆栈
siglongjmp(jmpbuff,88);
}
void main()
{
int res;
//安装SIGINT信号
signal(SIGINT,CbSigInt);
//设置跳转点
res=sigsetjmp(jmpbuff,1);
//第一次调用sigsetjmp时将返回0
if(res==0)
printf("First call sigsetjmp!\n");
//从信号处理函数中跳转过来时,sigsetjmp将返回非零值
else
{
//输出提示信息后退出进程
printf("res=%d\n",res);
printf("sigsetjmp is called by siglongjmp!\n");
return;
}
//暂停执行等待信号
pause();
}

编译运行该程序,在进程pause期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证信号处理函数是否跳转到指定的位置。
0 0