Linux支持30种不同类型的信号(输入man 7 signal查看),每种信号都对应于某种系统事件。底层的硬件异常由内核异常处理程序处理,在软件层面,系统提供信号作为一种用来通知进程发生了异常的机制,使一个进程可以显式的发送信号,并让预先设置的信号处理程序修改和信号相关联的默认行为,按我们的需求处理特定信号。

传送一个信号到目的进程由两个不同步骤组成:

  1. 发送信号: 内核通过更新目的进程的上下文中的某个状态,发送一个信号给目的进程。发送信号可以有如下两个原因:
    • 内核检测到一个系统事件,比如被零除错误(内核发送SIGFPE给试图除以0的进程)或者子进程终止(内核发送SIGCHLD给父进程)。
    • 一个进程调用kill函数,显式的要求内核发送一个信号给目的进程(一个进程也可以给自己发送信号)。
  2. 接收信号: 当一个进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或通过执行一个被称为信号处理程序(signal handler)的用户层函数捕获这个信号。

接收信号时的情况:

一个只发出而没有被接收的信号称为待处理信号(pending signal)(比如发送信号时处理程序正在处理另一个信号,那么新发送到的信号将成为待处理信号)。在任何时刻,一种类型的待处理信号至多只有一个,如果一个进程有一个类型为k的待处理信号,那么任何接下来发送到这个进程的类型为k的待处理信号都不会排队等待,而是被简单的丢弃。

一个进程可以有选择的阻塞接收某种信号。当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收(处理),直到进程取消对这种信号的阻塞。

1.进程组

每个进程都只属于一个进程组,进程组都是由一个正整数进程组ID来标识的。getpgrp返回当前进程的的进程组ID。

#include <unistd.h>
pid_t getpgrp(void);

默认地,一个进程和它的父进程同属于一个进程组。一个进程可以通过使用setpgid来改变自己或其他进程的进程组:

#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);

setpgid将进程pid的进程组改为pgid。如果pid是0,那么就使用当前进程的PID。如果pgid为0,那么就用pid指定的进程PID作为进程组ID。

2.用kill函数发送信号

进程通过kill发送信号给其他进程(包括它们自己)。

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
  1. 如果pid>0,那么kill发送信号sig给进程pid
  2. 如果pid<0, 那么kill发送信号sig给进程组abs(pid)

进程可以通过alarm向它自己发送SIGALRM信号

#include <unistd.h>
unsigned int alarm(unsigned int secs);

alarm会安排内核在secs秒内发送一个SIGALRM信号给调用进程。如果secs是0,那么不会调度新的闹钟。在任何情况下,对alarm的调用都将会取消任何待处理的(pending)闹钟,并且返回任何待处理的闹钟在被发送前还剩下的秒数(如果这次对alarm的调用还没有取消它的话),如果没有任何待处理的闹钟,就返回0。

3.接收信号

当内核从一个异常处理程序返回,准备将控制传递给进程p时,它会检查进程p的未阻塞的待处理信号的集合(pending && ~blocked)。如果这个集合为空(通常情况下),那么内核将控制传递到p的逻辑控制流中的下一条指令。

如果这个集合是非空的,那么内核会选择集合中的某个型号k(通常是最小的k),并且强制p接收信号k。收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回p的逻辑控制流中的下一条指令。每个信号类型都有一个预定义的默认行为,这些默认行为包括:

  • 进程终止(如SIGKILL)
  • 进程终止并转到存储器(dump core)
  • 进程停止直到被SIGCONT信号重启
  • 进程忽略该信号(如SIGCHLD)

进程可以通过signal修改和信号相关联的默认行为。唯一例外的是SIGSTOPSIGKILL,它们的默认行为是不能被修改的。

#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);

// 简化1
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

// 简化2
typedef void Sigfunc(int);
Sigfunc *signal(int signum, Sigfunc *handler);
  • 如果handlerSIG_IGN,那么忽略类型为signum的信号
  • 如果handlerSIG_DFL,那么类型为signum的信号恢复为默认行为
  • 如果handler为用户自定义的函数的地址,这个函数即为信号处理程序(signal handler),只要进程接收到一个类型为signal的信号,就会调用这个程序。

当处理程序执行它的return语句时,控制(通常)传递会控制流中进程信号接收中断位置处的指令。(在某些系统中被中断的系统调用会立即返回一个错误,比如在read执行时接受到信号并中断处理,read会返回错误)

4.处理被中断的系统调用

当阻塞于某个慢系统调用的一个进程(如acceptread…)捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR(被中断)错误。有些内核可能会自动重启某些被中断的系统调用,但是为了便于移植,当我们编写捕获信号的程序时,必须对慢系统调用返回EINTR有所准备。

for ( ; ; ) {
    clilen = sizeof(cliaddr);
    if ( (connfd = accpt(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
        if (errno == EINTR)
            continue;       // 如果被中断的话,可以自动重启
        else
            err_sys("accept error");
    }
}

上面的代码所做的事情是自动重启被中断的系统调用(accept)。


未解决的问题:

可以自定义一个信号吗?还是必须使用系统提供的信号?