Linux支持30种不同类型的信号(输入man 7 signal
查看),每种信号都对应于某种系统事件。底层的硬件异常由内核异常处理程序处理,在软件层面,系统提供信号作为一种用来通知进程发生了异常的机制,使一个进程可以显式的发送信号,并让预先设置的信号处理程序修改和信号相关联的默认行为,按我们的需求处理特定信号。
传送一个信号到目的进程由两个不同步骤组成:
- 发送信号: 内核通过更新目的进程的上下文中的某个状态,发送一个信号给目的进程。发送信号可以有如下两个原因:
- 内核检测到一个系统事件,比如被零除错误(内核发送
SIGFPE
给试图除以0的进程)或者子进程终止(内核发送SIGCHLD
给父进程)。 - 一个进程调用
kill
函数,显式的要求内核发送一个信号给目的进程(一个进程也可以给自己发送信号)。
- 内核检测到一个系统事件,比如被零除错误(内核发送
- 接收信号: 当一个进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或通过执行一个被称为信号处理程序(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);
- 如果
pid>0
,那么kill
发送信号sig
给进程pid
。 - 如果
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
修改和信号相关联的默认行为。唯一例外的是SIGSTOP
和SIGKILL
,它们的默认行为是不能被修改的。
#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);
- 如果
handler
为SIG_IGN
,那么忽略类型为signum
的信号 - 如果
handler
为SIG_DFL
,那么类型为signum
的信号恢复为默认行为 - 如果
handler
为用户自定义的函数的地址,这个函数即为信号处理程序(signal handler),只要进程接收到一个类型为signal
的信号,就会调用这个程序。
当处理程序执行它的return
语句时,控制(通常)传递会控制流中进程信号接收中断位置处的指令。(在某些系统中被中断的系统调用会立即返回一个错误,比如在read
执行时接受到信号并中断处理,read
会返回错误)
4.处理被中断的系统调用
当阻塞于某个慢系统调用的一个进程(如accept
、read
…)捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR
(被中断)错误。有些内核可能会自动重启某些被中断的系统调用,但是为了便于移植,当我们编写捕获信号的程序时,必须对慢系统调用返回EINTR
有所准备。
for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accpt(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; // 如果被中断的话,可以自动重启
else
err_sys("accept error");
}
}
上面的代码所做的事情是自动重启被中断的系统调用(accept
)。
未解决的问题:
可以自定义一个信号吗?还是必须使用系统提供的信号?
- older
- Newer