进程间通信之:信号
扫描二维码
随时随地手机看文章
信号是UNIX中所使用的进程通信的一种最古老的方法。它是在软件层次上对中断机制的一种模拟,是一种异步通信方式。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
在第2章kill命令中曾讲解到“−l”选项,这个选项可以列出该系统所支持的所有信号的列表。在笔者的系统中,信号值在32之前的则有不同的名称,而信号值在32以后的都是用“SIGRTMIN”或“SIGRTMAX”开头的,这就是两类典型的信号。前者是从UNIX系统中继承下来的信号,为不可靠信号(也称为非实时信号);后者是为了解决前面“不可靠信号”的问题而进行了更改和扩充的信号,称为“可靠信号”(也称为实时信号)。那么为什么之前的信号不可靠呢?这里首先要介绍一下信号的生命周期。
一个完整的信号生命周期可以分为3个重要阶段,这3个阶段由4个重要事件来刻画的:信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数,如图8.6所示。相邻两个事件的时间间隔构成信号生命周期的一个阶段。要注意这里的信号处理有多种方式,一般是由内核完成的,当然也可以由用户进程来完成,故在此没有明确画出。
图8.6信号生命周期
一个不可靠信号的处理过程是这样的:如果发现该信号已经在进程中注册,那么就忽略该信号。因此,若前一个信号还未注销又产生了相同的信号就会产生信号丢失。而当可靠信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此信号就不会丢失。所有可靠信号都支持排队,而所有不可靠信号都不支持排队。
注意
这里信号的产生、注册和注销等是指信号的内部实现机制,而不是调用信号的函数实现。因此,信号注册与否,与本节后面讲到的发送信号函数(如kill()等)以及信号安装函数(如signal()等)无关,只与信号值有关。
用户进程对信号的响应可以有3种方式。
n 忽略信号,即对信号不做任何处理,但是有两个信号不能忽略,即SIGKILL及SIGSTOP。
n 捕捉信号,定义信号处理函数,当信号发生时,执行相应的自定义处理函数。
n 执行缺省操作,Linux对每种信号都规定了默认操作。
Linux中的大多数信号是提供给内核的,表8.6列出了Linux中最为常见信号的含义及其默认操作。
表8.6 常见信号的含义及其默认操作
信号名
含义
默认操作
SIGHUP
该信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一会话内的各个作业与控制终端不再关联
终止
SIGINT
该信号在用户键入INTR字符(通常是Ctrl-C)时发出,终端驱动程序发送此信号并送到前台进程中的每一个进程
终止
SIGQUIT
该信号和SIGINT类似,但由QUIT字符(通常是Ctrl-)来控制
终止
SIGILL
该信号在一个进程企图执行一条非法指令时(可执行文件本身出现错误,或者试图执行数据段、堆栈溢出时)发出
终止
SIGFPE
该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出及除数为0等其他所有的算术错误
终止
SIGKILL
该信号用来立即结束程序的运行,并且不能被阻塞、处理或忽略
终止
SIGALRM
该信号当一个定时器到时的时候发出
终止
SIGSTOP
该信号用于暂停一个进程,且不能被阻塞、处理或忽略
暂停进程
SIGTSTP
该信号用于交互停止进程,用户键入SUSP字符时(通常是Ctrl+Z)发出这个信号
停止进程
SIGCHLD
子进程改变状态时,父进程会收到这个信号
忽略
SIGABORT
进程异常终止时发出
8.3.2信号发送与捕捉发送信号的函数主要有kill()、raise()、alarm()以及pause(),下面就依次对其进行介绍。
1.kill()和raise()(1)函数说明。
kill()函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组(实际上,kill系统命令只是kill()函数的一个用户接口)。这里需要注意的是,它不仅可以中止进程(实际上发出SIGKILL信号),也可以向进程发送其他信号。
与kill()函数所不同的是,raise()函数允许进程向自身发送信号。
(2)函数格式。
表8.7列出了kill()函数的语法要点。
表8.7 kill()函数语法要点
所需头文件
#include<signal.h>
#include<sys/types.h>
函数原型
intkill(pid_tpid,intsig)
函数传入值
pid:
正数:要发送信号的进程号
0:信号被发送到所有和当前进程在同一个进程组的进程
-1:信号发给所有的进程表中的进程(除了进程号最大的进程外)
<-1:信号发送给进程组号为-pid的每一个进程
sig:信号
函数返回值
成功:0
出错:-1
表8.8列出了raise()函数的语法要点。
表8.8 raise()函数语法要点
所需头文件
#include<signal.h>
#include<sys/types.h>
函数原型
intraise(intsig)
函数传入值
sig:信号
函数返回值
成功:0
出错:-1
(3)函数实例。
下面这个示例首先使用fork()创建了一个子进程,接着为了保证子进程不在父进程调用kill()之前退出,在子进程中使用raise()函数向自身发送SIGSTOP信号,使子进程暂停。接下来再在父进程中调用kill()向子进程发送信号,在该示例中使用的是SIGKILL,读者可以使用其他信号进行练习。
/*kill_raise.c*/
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include<sys/types.h>
#include<sys/wait.h>
intmain()
{
pid_tpid;
intret;
/*创建一子进程*/
if((pid=fork())<0)
{
printf("Forkerrorn");
exit(1);
}
if(pid==0)
{
/*在子进程中使用raise()函数发出SIGSTOP信号,使子进程暂停*/
printf("Child(pid:%d)iswaitingforanysignaln",getpid());
raise(SIGSTOP);
exit(0);
}
else
{
/*在父进程中收集子进程发出的信号,并调用kill()函数进行相应的操作*/
if((waitpid(pid,NULL,WNOHANG))==0)
{
if((ret=kill(pid,SIGKILL))==0)
{
printf("Parentkill%dn",pid);
}
}
waitpid(pid,NULL,0);
exit(0);
}
}
该程序运行结果如下所示:
$./kill_raise
Child(pid:4877)iswaitingforanysignal
Parentkill4877
2.alarm()和pause()(1)函数说明。
alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送SIGALARM信号。要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm()之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。
pause()函数是用于将调用进程挂起直至捕捉到信号为止。这个函数很常用,通常可以用于判断信号是否已到。
(2)函数格式。
表8.9列出了alarm()函数的语法要点。
表8.9 alarm()函数语法要点
所需头文件
#include<unistd.h>
函数原型
unsignedintalarm(unsignedintseconds)
函数传入值
seconds:指定秒数,系统经过seconds秒之后向该进程发送SIGALRM信号
函数返回值
成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0
出错:-1
表8.10列出了pause()函数的语法要点。
表8.10 pause()函数语法要点
所需头文件
#include<unistd.h>
函数原型
intpause(void)
函数返回值
-1,并且把error值设为EINTR
(3)函数实例。
该实例实际上已完成了一个简单的sleep()函数的功能,由于SIGALARM默认的系统动作为终止该进程,因此程序在打印信息之前,就会被结束了。代码如下所示:
/*alarm_pause.c*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
intmain()
{
/*调用alarm定时器函数*/
intret=alarm(5);
pause();
printf("Ihavebeenwakenup.n",ret);/*此语句不会被执行*/
}
$./alarm_pause
Alarmclock
想一想
用这种形式实现的sleep()功能有什么问题?
8.3.3信号的处理在了解了信号的产生与捕获之后,接下来就要对信号进行具体的操作了。从前面的信号概述中读者也可以看到,特定的信号是与一定的进程相联系的。也就是说,一个进程可以决定在该进程中需要对哪些信号进行什么样的处理。例如,一个进程可以选择忽略某些信号而只处理其他一些信号,另外,一个进程还可以选择如何处理信号。总之,这些都是与特定的进程相联系的。因此,首先就要建立进程与其信号之间的对应关系,这就是信号的处理。
注意
请读者注意信号的注册与信号的处理之间的区别,前者信号是主动方,而后者进程是主动方。信号的注册是在进程选择了特定信号处理之后特定信号的主动行为。
信号处理的主要方法有两种,一种是使用简单的signal()函数,另一种是使用信号集函数组。下面分别介绍这两种处理方式。
1.信号处理函数(1)函数说明。
使用signal()函数处理时,只需要指出要处理的信号和处理函数即可。它主要是用于前32种非实时信号的处理,不支持信号传递信息,但是由于使用简单、易于理解,因此也受到很多程序员的欢迎。
Linux还支持一个更健壮、更新的信号处理函数sigaction(),推荐使用该函数。
(2)函数格式。
signal()函数的语法要点如表8.11所示。
表8.11 signal()函数语法要点
所需头文件
#include<signal.h>
函数原型
void(*signal(intsignum,void(*handler)(int)))(int)
函数传入值
signum:指定信号代码
handler:
SIG_IGN:忽略该信号
SIG_DFL:采用系统默认方式处理信号
自定义的信号处理函数指针
函数返回值
成功:以前的信号处理配置
出错:-1
这里需要对这个函数原型进行说明。这个函数原型有点复杂。可先用如下的typedef进行替换说明:
typedefvoidsign(int);
sign*signal(int,handler*);
可见,首先该函数原型整体指向一个无返回值并且带一个整型参数的函数指针,也就是信号的原始配置函数。接着该原型又带有两个参数,其中的第二个参数可以是用户自定义的信号处理函数的函数指针。
表8.12列举了sigaction()的语法要点。
表8.12 sigaction()函数语法要点
所需头文件
#include<signal.h>
函数原型
intsigaction(intsignum,conststructsigaction*act,structsigaction*oldact)
函数传入值
signum:信号代码,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号
act:指向结构sigaction的一个实例的指针,指定对特定信号的处理
oldact:保存原来对相应信号的处理
函数返回值
成功:0
出错:-1
这里要说明的是sigaction()函数中第2个和第3个参数用到的sigaction结构。这是一个看似非常复杂的结构,希望读者能够慢慢阅读此段内容。
首先给出了sigaction的定义,如下所示:
structsigaction
{
void(*sa_handler)(intsigno);
sigset_tsa_mask;
intsa_flags;
void(*sa_restore)(void);
}
sa_handler是一个函数指针,指定信号处理函数,这里除可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式)或SIG_IGN(忽略信号)。它的处理函数只有一个参数,即信号值。
sa_mask是一个信号集,它可以指定在信号处理程序执行过程中哪些信号应当被屏蔽,在调用信号捕获函数之前,该信号集要加入到信号的信号屏蔽字中。
sa_flags中包含了许多标志位,是对信号进行处理的各个选择项。它的常见可选值如表8.13所示。
表8.13 常见信号的含义及其默认操作
选项
含义
SA_NODEFERSA_NOMASK
当捕捉到此信号时,在执行其信号捕捉函数时,系统不会自动屏蔽此信号
SA_NOCLDSTOP
进程忽略子进程产生的任何SIGSTOP、SIGTSTP、SIGTTIN和SIGTTOU信号
SA_RESTART
令重启的系统调用起作用
SA_ONESHOTSA_RESETHAND
自定义信号只执行一次,在执行完毕后恢复信号的系统默认动作
(3)使用实例。
第一个实例表明了如何使用signal()函数捕捉相应信号,并做出给定的处理。这里,my_func就是信号处理的函数指针。读者还可以将其改为SIG_IGN或SIG_DFL查看运行结果。第二个实例是用sigaction()函数实现同样的功能。
以下是使用signal()函数的示例:
/*signal.c*/
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
/*自定义信号处理函数*/
voidmy_func(intsign_no)
{
if(sign_no==SIGINT)
{
printf("IhavegetSIGINTn");
}
elseif(sign_no==SIGQUIT)
{
printf("IhavegetSIGQUITn");
}
}
intmain()
{
printf("WaitingforsignalSIGINTorSIGQUIT...n");
/*发出相应的信号,并跳转到信号处理函数处*/
signal(SIGINT,my_func);
signal(SIGQUIT,my_func);
pause();
exit(0);
}
运行结果如下所示。
$./signal
WaitingforsignalSIGINTorSIGQUIT...
IhavegetSIGINT(按ctrl-c组合键)
$./signal
WaitingforsignalSIGINTorSIGQUIT...
IhavegetSIGQUIT(按ctrl-组合键)
以下是用sigaction()函数实现同样的功能,下面只列出更新的main()函数部分。
/*sigaction.c*/
/*前部分省略*/
intmain()
{
structsigactionaction;
printf("WaitingforsignalSIGINTorSIGQUIT...n");
/*sigaction结构初始化*/
action.sa_handler=my_func;
sigemptyset(&action.sa_mask);
action.sa_flags=0;
/*发出相应的信号,并跳转到信号处理函数处*/
sigaction(SIGINT,&action,0);
sigaction(SIGQUIT,&action,0);
pause();
exit(0);
}
2.信号集函数组(1)函数说明。
使用信号集函数组处理信号时涉及一系列的函数,这些函数按照调用的先后次序可分为以下几大功能模块:创建信号集合、注册信号处理函数以及检测信号。
其中,创建信号集合主要用于处理用户感兴趣的一些信号,其函数包括以下几个。
n sigemptyset():将信号集合初始化为空。
n sigfillset():将信号集合初始化为包含所有已定义的信号的集合。
n sigaddset():将指定信号加入到信号集合中去。
n sigdelset():将指定信号从信号集合中删除。
n sigismember():查询指定信号是否在信号集合之中。
注册信号处理函数主要用于决定进程如何处理信号。这里要注意的是,信号集里的信号并不是真正可以处理的信号,只有当信号的状态处于非阻塞状态时才会真正起作用。因此,首先使用sigprocmask()函数检测并更改信号屏蔽字(信号屏蔽字是用来指定当前被阻塞的一组信号,它们不会被进程接收),然后使用sigaction()函数来定义进程接收到特定信号之后的行为。检测信号是信号处理的后续步骤,因为被阻塞的信号不会传递给进程,所以这些信号就处于“未处理”状态(也就是进程不清楚它的存在)。sigpending()函数允许进程检测“未处理”信号,并进一步决定对它们作何处理。
(2)函数格式。
首先介绍创建信号集合的函数格式,表8.14列举了这一组函数的语法要点。
表8.14 创建信号集合函数语法要点
所需头文件
#include<signal.h>
函数原型
intsigemptyset(sigset_t*set)
intsigfillset(sigset_t*set)
intsigaddset(sigset_t*set,intsignum)
intsigdelset(sigset_t*set,intsignum)
intsigismember(sigset_t*set,intsignum)
函数传入值
set:信号集
signum:指定信号代码
函数返回值
成功:0(sigismember成功返回1,失败返回0)
出错:-1
表8.15列举了sigprocmask的语法要点。
表8.15 sigprocmask函数语法要点
所需头文件
#include<signal.h>
函数原型
intsigprocmask(inthow,constsigset_t*set,sigset_t*oset)
函数传入值
how:决定函数的操作方式
SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中
SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合
SIG_SETMASK:将当前的信号集合设置为信号阻塞集合
set:指定信号集
oset:信号屏蔽字
函数返回值
成功:0
出错:-1
此处,若set是一个非空指针,则参数how表示函数的操作方式;若how为空,则表示忽略此操作。
最后,表8.16列举了sigpending函数的语法要点。
表8.16 sigpending函数语法要点
所需头文件
#include<signal.h>
函数原型
intsigpending(sigset_t*set)
函数传入值
set:要检测的信号集
函数返回值
成功:0
出错:-1
总之,在处理信号时,一般遵循如图8.7所示的操作流程。
图8.7一般的信号操作处理流程
(3)使用实例。
该实例首先把SIGQUIT、SIGINT两个信号加入信号集,然后将该信号集合设为阻塞状态,并进入用户输入状态。用户只需按任意键,就可以立刻将信号集合设置为非阻塞状态,再对这两个信号分别操作,其中SIGQUIT执行默认操作,而SIGINT执行用户自定义函数的操作。源代码如下所示:
/*sigset.c*/
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
/*自定义的信号处理函数*/
voidmy_func(intsignum)
{
printf("Ifyouwanttoquit,pleasetrySIGQUITn");
}
intmain()
{
sigset_tset,pendset;
structsigactionaction1,action2;
/*初始化信号集为空*/
if(sigemptyset(&set)<0)
{
perror("sigemptyset");
exit(1);
}
/*将相应的信号加入信号集*/
if(sigaddset(&set,SIGQUIT)<0)
{
perror("sigaddset");
exit(1);
}
if(sigaddset(&set,SIGINT)<0)
{
perror("sigaddset");
exit(1);
}
if(sigismember(&set,SIGINT))
{
sigemptyset(&action1.sa_mask);
action1.sa_handler=my_func;
action1.sa_flags=0;
sigaction(SIGINT,&action1,NULL);
}
if(sigismember(&set,SIGQUIT))
{
sigemptyset(&action2.sa_mask);
action2.sa_handler=SIG_DFL;
action2.sa_flags=0;
sigaction(SIGQUIT,&action2,NULL);
}
/*设置信号集屏蔽字,此时set中的信号不会被传递给进程,暂时进入待处理状态*/
if(sigprocmask(SIG_BLOCK,&set,NULL)<0)
{
perror("sigprocmask");
exit(1);
}
else
{
printf("Signalsetwasblocked,Pressanykey!");
getchar();
}
/*在信号屏蔽字中删除set中的信号*/
if(sigprocmask(SIG_UNBLOCK,&set,NULL)<0)
{
perror("sigprocmask");
exit(1);
}
else
{
printf("Signalsetisinunblockstaten");
}
while(1);
exit(0);
}
该程序的运行结果如下所示,可以看见,在信号处于阻塞状态时,所发出的信号对进程不起作用,并且该信号进入待处理状态。读者输入任意键,并且信号脱离了阻塞状态之后,用户发出的信号才能正常运行。这里SIGINT已按照用户自定义的函数运行,请读者注意阻塞状态下SIGINT的处理和非阻塞状态下SIGINT的处理有何不同。
$./sigset
Signalsetwasblocked,Pressanykey!/*此时按任何键可以解除阻塞屏蔽字*/
Ifyouwanttoquit,pleasetrySIGQUIT/*阻塞状态下SIGINT的处理*/
Signalsetisinunblockstate/*从信号屏蔽字中删除set中的信号*/
Ifyouwanttoquit,pleasetrySIGQUIT/*非阻塞状态下SIGINT的处理*/
Ifyouwanttoquit,pleasetrySIGQUIT
Quit/*非阻塞状态下SIGQUIT处理*/