信号(signal)是Linux进程间通信的一种机制,全称为软中断信号,也被称为软中断。信号本质上是在软件层次上对硬件中断机制的一种模拟。它提供了一种处理异步事件的方法,也是进程间惟一的异步通信方式。体现为操作系统修改了目标进程的PCB内容,即为对其发送了信号。
(1)硬件方式
a.当用户在终端上按下某键时,将产生信号。如按下
b.硬件异常产生信号:除数据、无效的存储访问等。这些事件通常由硬件(如:CPU)检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时正在进行的程序。
(2) 软件方式
c.用户在终端下调用kill命令向进程发送任务信号。
d.进程调用kill或sigqueue函数发送信号。
e.当检测到某种软件条件已经具备时发出信号,如由alarm或settimer设置的定时器超时时将生成SIGALRM信号。
在Linux系统下,信号的处理方式有3种:
一是 忽略信号; --------------------------------------如signal(信号,SIG_IGN);
二是 按照系统提供的缺省处理规则进行处理; ---------------如signal(信号,SIG_DFN); 或不作任何注册
三是 捕捉信号,在程序中定义自己的信号处理函数,在信号处理函数中完成相应的功能。-----注册一个信号函数,自己处理
Linux系统分别为前两种方式提供了相应的宏定义:SIG_IGN和SIG_DFN。
下面列出几个重要信号及其处理方式(用第二种方式缺省处理)。
-----------------------------------------------------------------------------------------------------------------
信号名称 缺省处理方式 信号产生原因/说明
-----------------------------------------------------------------------------------------------------------------
SIGHUP 终止进程 控制终端挂起或者退出
SIGINT 终止进程
SIGQUIT 终止进程并进行内核映像转储
SIGKILL 终止进程 不能阻塞、忽略、捕捉
SIGALRM 终止进程 定时器超时信号
SIGTERM 终止进程 可以阻塞、捕捉
SIGCHLD 混略信号 子进程退出时向父进程发送该信号
SIGSTOP 暂停进程执行 不能阻塞、忽略、捕捉
-----------------------------------------------------------------------------------------------------------------
3.1 信号处理函数的实现
信号处理函数是进程接收到信号后要执行的函数,该函数应该尽量简洁,一般不要执行过多的代码。最好只是改变一个外部标志变量的值,而在另外的程序中不断的检测该变量,繁杂的工作都留给那些程序去做。在定义信号处理函数时,应该特别注意以下几点。
1)如果信号处理程序中需要存取某个全局变量,则应该在程序中使用关键字volatile声明此变量。通知编译器,在编译过程中不要对该变量进行优化。
2)如果在信号处理函数中调用某个函数,那么那么该函数必须是可重入的,或者保证在信号处理函数执行期间不会有信号到达进程。Linux系统下存在许多不可重入的函数,如malloc、gethostbyname等。
在信号处理函数里,有时需要用到长跳转的操作。所谓长跳转,就是从信号处理函数直接跳转到函数体外指定的代码位置继续运行。Linux系统提供了两个函数实现该功能:设置跳转点的sigsetjmp和执行跳转的siglongjmp。sigsetjmp用来设置跳转点,在成功调用后,sigsetjmp语句所在的位置就是跳转点,这个位置指针将被保存到sigsetjmp的第一个参数中。这个两个函数的原型为:
#include
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调用而返回。
编程实现捕捉SIGINT信号,在信号处理函数中用长跳转跳转至主程序。
#include
#include
#include
//全局变量,用于保存跳转点及其现场
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期间,按下
3.2 信号的阻塞处理
信号的阻塞 就是通知系统内核暂时停止向进程发送指定的信号,而是由内核对进程接收到的相应信号进行缓存排队,直到进程解除对相应信号的阻塞为止。一旦进程解除对该信号的阻塞,则缓存的信号将被发送到相应的进程。
信号在几种情况下会进入阻塞状态。
1)系统自动阻塞:在信号的处理函数执行过程中,该信号将被阻塞,直到信号处理函数执行完毕,该阻塞将会解除。这种机制的作用主要是避免信号的嵌套。
2)通过sigaction实现人为阻塞:在使用sigaction安装信号时,如果设置了sa_mask阻塞信号集,则该信号集中的信号在信号处理函数执行期间将会阻塞。这种情况下进行信号阻塞的主要原因是:一个信号处理函数在执行过程中,可能会有其他信号到来。此时,当前的信号处理函数就会被中断。而这往往是不希望发生的。此时,可以通过sigaction系统调用的信号阻塞掩码对相关信号进行阻塞。通过这种方式阻塞的信号,在信号处理函数执行结束后就会解除。
3)通过sigprocmask实现人为阻塞:可以通过sigprocmask系统调用指定阻塞某个或者某几个信号。这种情况下进行信号阻塞的原因较多,一个典型的情况是:某个信号的处理函数与进程某段代码都要某个共享数据区进行读写。如果当进程正在读写共享数据区的过程中,一个信号过来,则进程的读写过程将被中断转而执行信号处理函数,而信号处理函数也要对该共享数据区进行读写,这样共享数据区就会发生混乱。这种情况下,需要在进程读写共享数据区前阻塞该信号,在读写完成后再解除该信号的阻塞。
提示:在信号的接收过程中可能存在这样的情况:若干个相同的信号同时到达。通过上面的介绍可以知道,当信号处理函数正在执行时,同类信号将被阻塞处理。但是,如果此时信号处理函数还没有来得及执行,那么该同类信号就不会阻塞,在这种情况下,将会发生一种成为“信号合并”的现象。同时到达的同类信号将被合并处理,就像只有一个信号到达一样。
被阻塞的信号的集合成为当前进程的信号掩码。每个进程都有惟一的信号掩码。为了对信号进行阻塞或者解除阻塞,Linux提供了专门的系统调用sigprocmask完成这一任务。该函数的原型为:
#include
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。
例:编程实现下面功能:为进程安装SIGINT信号,先阻塞该信号,休眠10秒,再解除该信号的阻塞。
代码如下:
#include
#include
//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);
}
编译运行该程序,在进程休眠期间,按下+键向进程发送SIGINT信号,注意观看信号是否被阻塞。在休眠结束后,验证刚才被阻塞的SIGINT信号是否被重新发送。
提示:在创建新的子进程时,子进程将继承父进程的信号掩码
信号集
我们已经知道,我们可以通过信号来终止进程,也可以通过信号来在进程间进行通信,程序也可以通过指定信号的关联处理函数来改变信号的默认处理方式,也可以屏蔽某些信号,使其不能传递给进程。那么我们应该如何设定我们需要处理的信号,我们不需要处理哪些信号等问题呢?信号集函数就是帮助我们解决这些问题的。
信号的递送、阻塞和未决
实际执行信号的处理动作称为信号递送(Delivery),信号从产生到递送之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号,SIGKILL 和 SIGSTOP 不能被阻塞。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递送的动作。
每个进程都有一个信号掩码,它实际上是一个信号集,位于该信号集中的信号一旦产生,并不会被递送给相应的进程,而是会被阻塞在未决状态。在信号处理函数执行期间,这个正在被处理的信号总是处于信号掩码中,如果又有该信号产生,则会被阻塞,直到上一个针对该信号的处理过程结束以后才会被递送。
信号集的操作
通过上节对信号阻塞的介绍可以知道,信号的阻塞实际上是对一个集合的操作。这个集合中可能包含多种信号,这就是信号集。信号集的数据类型为sigset_t,实际上是个结构体,它的定义如下所示。
typedef struct
{
unsigned long sig[_NSIG_WORDS];
}sigset_t;
Linux系统提供了一系列函数对信号集进行操作。这些函数的原型如下所示。
#include
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。
未决信号的处理
信号的未决是信号产生后的一种状态,是指从信号产生后,到信号被接收进程处理之前的一种过渡状态。由于信号的未决状态时间非常短,所以通常情况下,处于未决状态的信号非常少。如果程序中使用了sigprocmask阻塞了某种信号,则向进程发送的这种信号将处于未决状态。Linux提供了专门的函数sigpending获取当前进程中处于未决状态的信号。该函数的原型为:
#include
int sigpending(sigset_t *set);
参数说明:
1)set:输出参数,处于未决状态的信号集。
返回值:
若成功,返回0;若失败,返回-1。
编程实现下面功能:为进程安装SIGINT信号,先阻塞该信号,休眠10秒,最后查看当前进程未决的信号。
#include
#include
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);
}
//编译运行该程序,在进程休眠期间,按下+键向进程发送SIGINT信号,验证该信号是否处于未决状态。
等待信号
在有些情况下,程序需要暂停执行,进入休眠状态,以等待信号的到来。这时可以使用pause系统调用。pause一旦被调用,则进程将进入休眠状态。之后,只有在进程接收到信号后,pause才会返回。pause的原型为:
#include
int pause();
返回值:
pause的返回值永远是-1,错误码errno为EINTR。
用pause编程实现等待SIGINT信号到来的功能。
#include
#include
#include
//SIGINT信号处理函数
void CbSigInt(int signo)
{
//输出信号的值
printf("receive signal %d\n",signo);
}
void main()
{
//安装SIGINT信号
signal(SIGINT,CbSigInt);
//等待信号
pause();
}
//编译运行该程序,在进程pause期间,按下+键向进程发送SIGINT信号,验证该信号是否被处理。
//注意:由于此测试程序没有屏蔽其他信号,因此任何一个信号的到来都能唤醒pause。
sigsuspend
pause系统调用可以实现暂停进程的执行等待某个信号的到来,但是,如果在pause被调用之前,指定的信号到达进程,那么,在随后的pause调用中,假定不再有信号到来,则进程将进入无限期的等待中。为此Linux提供了功能更强大的sigsuspend以满足这种需求。sigsuspend的工作过程如下:
1)设置进程的信号掩码并阻塞进程。
2)收到信号,恢复原来的信号掩码。
3)调用进程设置的信号处理函数。
4)等待信号处理函数返回后,sigsuspend返回。
上述四个步骤是一次性完成的,操作系统保证操作过程的原子性。特别需要注意的是第三步调用信号处理函数是由sigsuspend完成的。
sigsuspend的原型为:
#include
int sigsuspend(const sigset_t *set);
参数说明:
1)set:输入参数,执行sigsuspend过程中需要被阻塞的信号集。
返回值:
sigsuspend的返回值永远是-1,错误码errno为EINTR。
用sigsuspend编程实现等待SIGINT信号到来的功能。
#include
#include
//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期间,按下+键向进程发送SIGINT信号,验证该信号是否被处理。
//注意:由于此测试程序屏蔽了除SIGINT外的所有其他信号,因此只有在收到SIGINT信号后进程才会退出。
提示:
一个阻塞式系统调用在执行过程中如果没有符合条件的数据,将进入休眠状态,直到有符合条件的数据到来。比较典型的例子是从网络连接上读取数据,如果没有数据到来,那么这个读操作将会阻塞。此时有两种情况可以中断该读操作的执行:一是网络上有数据到来,则读操作将获取到所需要的数据后返回;二是当前进程收到了某个信号,此时,读操作将被中断并返回失败,错误码errno为EINTR。
abort
abort()函数首先解除进程对SIGABRT信号的阻止,然后向调用进程发送该信号。
abort()函数会导致进程的异常终止除非SIGABRT信号被捕捉并且信号处理句柄没有返回。