信号
Unix信号允许进程中断其他进程,一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。Linux系统上支持30种不同类型的信号,在shell命令行上输入“man 7 signal”,在Mac系统上输入“ man signal”就能得到这个列表。
每种信号类型都对应于某种系统事件。底层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。如果一个进程试图除以0,那么内核就发送给它一个SIGFPE信号(序号8),如果当进程在前台运行时,你键入ctrl-c,那么内核就会发送一个SIGINT信号(序号2,来自键盘的中断),一个进程可以通过另一个进程发送一个SIGKILL信号(序号9)强行终止它,当一个子进程终止或者停止时,内核会发送一个SIGCHLD(序号17)给父进程。
信号术语
发送一个信号到目的进程是由两个不同步骤组成的:
(1)发送信号:内核通过更新目的进程上下文的某个状态,发送(递送)一个信号给目的进程。发送信号可以有如下两个原因:内核检测到一个系统事件,比如被零除错误或者子进程终止;一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程,一个进程可以发送信号给它自己。
a.进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。getpgrp函数返回当前进程的进程组ID:
#include<unistd.h>
pid_t getpgrp(void);
b.用kill程序发送信号
unix> kill -9 15213
在shell中发送信号9(SIGKILL)给进程15213。
c.从键盘发送信号
Unix 外壳使用作业(job)这个抽象概念来表示为对一个命令行求值而创建的过程。在任何时刻,至多只有一个前台作业和和0个或多个后台作业。进程和作业的概念也有区别。一个正在执行的进程称为一个作业,而且作业可以包含一个或多个进程,尤其是当使用了管道和重定向命令。
在bash环境下:
前台作业:你可以控制的作业
后台作业:在内存可以自行运行的作业,你无法直接控制它,除非以bg/fg等命令将该作业调用出来。
比如键入:
unix>ls | sort
就创建了一个由两个进程组成的前台作业,这两个作业是通过Unix管道连接起来的:一个进程运行ls程序,一个进程运行sort程序。
外壳为每个作业创建一个独立的进程组。典型地,进程组ID是取自作业中父进程中的一个。
图中展示了有一个前台作业和两个后台作业的外壳。前台作业中的父进程PID为20,进程组ID也为20。父进程创建两个子进程,每个子进程也都是进程组20的成员。
输入ctrl-c会导致内核向每个前台进程组中的成员发送一个SIGINT(2)信号(而不是向shell),ctrl-z和SIGSTP(20)信号也是类似。
SIGINT:来自键盘的中断
SIGSTP:来自终端的停止信号
d.用kill函数发送信号
如果pid大于零,那么kill函数发送信号给sig给进程pid,如果小于0,那么kill发送信号sig给进程组中的每个进程。
#include "csapp.h"
int main(){
pid_t pid;
if(pid==Fork()==0){
Pause();
printf("control should never reach here!\n");
exit(0);
}
kill(pid,SIGKILL);
exit(0);
}
(2)接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应后时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。一个只发出而没有接收的信号叫待处理信号(pending signal)。
当内核从一个异常处理程序返回,准备将控制传递给进程p时,它会检查进程p的未被阻塞的待处理信号的集合(pending&~blocked)。如果这个集合为空,那么内核将控制传递到p的逻辑控制流中的下一条指令(
Inext
)。
然而,如果集合是非空的,那么内核选择集合中的某个信号k(通常是最小的k),并且强制p接收信号k。收到这个信号会出发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回p的逻辑控制流的下一条指令(
Inext
)。每个信号类型都有一个预定义的默认行为,是下面的一种:
进程终止;
进程终止并转储存储器(dump core);
进程停止直到被SIGCONT信号重启;
进程忽略该信号;
进程可以通过使用signal函数修改和信号关联的默认行为,唯一的例外是SIGSTOP和SIGKILL,它们的默认行为是不能被修改的,下面给出signal的宏定义:
#include <signal.h>
typedef void (*sighandler_t)(int) /*void 类型的指针,形参是int */
sighandler_t signal(int num,sighandler_t handler);
/*若成功则为指向前次处理程序的指针,若出错则为SIG—ERR(不设置errno) */
如果handler是SIG_IGN,那么忽略类型为signum的信号;
如果handler是SIG_DFL,那么类型是signum的信号行为恢复为默认行为;
否则,handler就是你自己定义的默认行为的函数的地址,这个函数称为信号处理程序(signal handler)。调用信号处理程序称为捕获信号(动词),执行信号处理程序称为处理程序(动词)。
下面展示一段程序,它捕获用户在键盘上输入ctrl-c时外壳发送的SIGINT信号,SIGINT的默认行为是立即终止该程序。在这个示例中,我们将默认行为修改为捕获信号,输出一条信息,然后终止该进程。
#include "csapp.h"
void handler(int sig) /*SIGINT handler*/
{
printf("Caught SIGINT\n");
exit(0);
}
int main(){
/*Install the SIGINT handler*/
if(signal(SIGINT,handler)==SIG_ERR)
unix.error("signal error");
pause(); /*进入休眠,直到收到一个信号*/
exit(0);
}
信号处理问题
(1)待处理信号被阻塞:假设一个进程捕获一个SIGINT信号,并且当前正在运行它的SIGINT处理程序(handler),如果另一个SIGINT信号传递到这个进程,那么这个SIGINT将变成待处理的,但是不会被接收,直到程序返回。
(2)待处理信号不会排队等候:任意类型至多只有一个待处理信号。也就是说,如果有两个类型为k的信号传送到一个目的进程,而由于目的进程正在执行信号k的处理程序,所以信号k是阻塞的,那个第二个k信号就会丢失,它是不会排队等候的。
(3)系统调用可以被中断:像read,wait,accept这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获一个信号时,这个慢速系统调用会被中断。在信号处理程序返回时,慢速系统调用不再继续,而是立即返回给用户一个错误条件,并将error设置为EINTR.
可以看出,信号处理程序是计算机系统中并发的又一示例。信号处理处理程序的执行,中断了main 函数的执行。因为信号处理程序的逻辑控制流与主函数的逻辑控制流重叠,信号处理程序和主函数并发地运行。下面展示一个示例函数,它能够正确解决信号会阻塞和不会排队等待的情况,也可以正确解决系统调用可能被中断的情况。
#include <csapp.h>
void handler2(int sig)
{
pid_t pid;
while(pid=waitpid(-1,NULL,0)>0)
printf("Handler reaped child %d\n",int(pid));
if(error!=ECHILD)
unix_error("Waitpid error");
Sleep(2);
return;
}
int main(){
int i,n;
char buf[MAXBUF];
pid_t pid;
if(signal(SIGCHLD,handler2)==SIG_ERR) /* 中断main 函数的执行*/
unix_error("signal error");
for(i=0;i<3;i++){
pid=Fork();
if(pid==0){
printf("Hello from child %d\n",(int)getpid());
sleep(1);
exit(0);
}
}
/* 手动重启被终止的read()调用,因为从键盘进行输入之前,被阻塞的read系统调用就提前返回一个错误 */
while((n=read(STDIN_FILENO,buf,sizeof(buf)))<0)
if(error!=EINTR)
unix_error("read error");
print("Parent processing input\n");
while(1)
;
exit(0);
}