信号都以SIG开头,信号名都被定义为正整数常量,不存在编号为0的信号,kill函数对信号编号0有特殊的应用,这种信号称为空信号。

很多条件可以产生信号:

  • 当用户按某些终端键,引发终端产生的信号
  • 硬件产生的信号:除数为0,无效内存引用
  • 进程调用kill函数可将任意信号发送给另一个进程或进程组
  • 用户可调用kill命令将信号发送给其他进程
  • 当检测到某种软件条件已经发生,并将其通知有关的进程时也产生信号

在某个信号出现时,可以告诉内核按照下列三种方式之一处理

  • 忽略,大多数信号可以使用这种方式进行处理,但时SIGKILL和SIGSTOP不能被忽略,因为他们向内核和超级用户提供了使进程终止的可靠方法。
  • 捕捉,做到这一点的前提是要通知内核在某种信号发生时,调用一个用户函数,在用户函数中对这种事件进行处理
  • 执行系统默认动作,大多数信号的系统默认动作是终止进程

在系统默认动作中,有的默认动作会在进程当前工作目录中生成core文件(中间复制了该进程的内存映像)下面几种条件不产生core文件:

  • 进程是设置用户id的,而且当前用户并非程序文件的所有者
  • 进程是设置组id的
  • 用户没有写当前工作目录的权限的
  • 文件已经存在,而且用户对该文件有写权限
  • 文件太大
信号名 说明
SIGABRT 调用abort函数时产生此信号
SIGALRM 用alarm函数设置定时器超时时产生此信号,setitimer函数也产生此信号
SIGBUG 硬件故障产生
SIGCANCEL 线程库内部使用的信号
SIGCHLD 当一个进程终止或停止,SIGCHLD信号被送给父进程。父进程可以使用wait取得子进程id和终止状态,系统默认忽略此信号
SIGFPE 算术异常(除以0,浮点溢出)
SIGHUP 如果终端检测到一个连接断开,则将此信号送给与该终端相关的控制进程
SIGILL 非法硬件指令
SIGINFO 键盘状态建(Ctrl+T)请求
SIGINT 终端中断符(DELETE/Ctrl+C)
SIGIO 异步io事件,SIGPOLL
SIGIOT 实现定义的硬件故障
SIGKILL 这是两个不能被捕捉或忽略信号中的一个,直接杀死进程
SIGPIPE 如果管道的读进程已终止写管道,产生该信号
SIGQUIT 退出键时触发(Ctrl+)
SIGSEGV 进程进行了一次无效的内存引用
SIGTERM 这是kill命令发送的的系统默认终止信号。该信号由应用程序捕获,可以让程序有机会在退出前做好清理工作。
SIGUSR1(SIGUSR2) 用户定义的信号,可用于应用程序

信号函数

1
2
3
4
5
6
#include<signal.h>
void(*signal(int signo,void (*func)(int)))(int) ;
//成功返回以前的信号处理配置,出错返回SIG_ERR
#define SIG_ERR (void (*)()) -1
#define SIG_DFL (void (*)()) 0
#define SIG_IGN (void (*)()) 1

该函数的signal参数是信号名,func值是常量SIG_IGN,常量SIG_DFL或当接收到此信号后要调用的函数的地址。

  • 当指定SIG_IGN,则向内核表示忽略此信号
  • 当指定SIG_DFL,则表示接收到此信号后的动作是系统默认动作
  • 当指定函数地址时,则信号发生时,调用该函数。我们称这种函数为信号处理程序或信号捕捉函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "apue.h"

static void sig_usr(int);   /* one handler for both signals */

int main(void)
{
    if (signal(SIGUSR1, sig_usr) == SIG_ERR)
        err_sys("can't catch SIGUSR1");
    if (signal(SIGUSR2, sig_usr) == SIG_ERR)
        err_sys("can't catch SIGUSR2");
    for ( ; ; )
        pause();
}

static void sig_usr(int signo)      /* argument is signal number */
{
    if (signo == SIGUSR1)
        printf("received SIGUSR1\n");
    else if (signo == SIGUSR2)
        printf("received SIGUSR2\n");
    else
        err_dump("received signal %d\n", signo);
}

output:
$ ./a.out &                   start process in background
[1]      7216                 job-control shell prints job number and process ID
$ kill -USR1 7216             send it SIGUSR1
received SIGUSR1
$ kill -USR2 7216             send it SIGUSR2
received SIGUSR2
$ kill 7216                   now send it SIGTERM
[1]+  Terminated    ./a.out

程序启动时:

  • 当执行一个程序时,所有信号状态都是系统默认或忽略。
  • shell自动将后台进程对终端和退出信号的处理方式设置为忽略

进程创建时:

  • 当一个进程调用fork时,其子进程继承父进程的信号处理方式

不可靠信号

信号不可靠指的是信号可能会丢失:一个信号发生了,但进程可能不知道这一点。同时,进程对信号的控制能力也很差,他能捕获信号或或忽略它。有时用户希望通知内核阻塞某个信号:不要忽略该信号,在其发生时记住它,然后在进程做好了准备时再通知它。这种阻塞信号的能力当时并不具备。

中断的系统调用

早期unix系统的特性是:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不在继续执行。

当捕捉到某个信号时,被中断的是内核中执行的系统调用,而非函数。

为了支持该特性,系统调用分为两类:低速系统调用和其它系统调用,低速系统调用是可能会使进程永远阻塞的的一类系统调用。包括:

  • 某些类型文件不存在(读管道,终端和网络设备),则读操作坑会永远阻塞
  • 如果这些数据不能被相同类型文件立即接受,则可操作可能会使调用者永远阻塞
  • 在某种条件发生之前被相同类型文件立即接受,则可能会发生阻塞(例如打开一个终端设备,需要先等待与之连接的调制解调器应答)
  • pause函数(它使调用进程休眠直至捕捉到一个信号)和wait函数
  • 某些ioctl操作
  • 某些进程间通信函数
  • 其他系统调用

与被中断的系统调用相关的问题是必须显示的处理出错返回。某些操作系统为了使程序不必处理被中断的系统调用,引入了自动重启动的系统调用。

可重入函数

进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果在信号处理的过程中,由于出现了新的信号,则会导致信息的丢失或被覆盖,因而引入了可重入的函数。

可重入是一种异步信号安全的(async-signal safe)函数。在信号处理操作期间,它会阻塞任何会引起信号不一致的信号发送。

SIGCLD语义

SIGCHLD信号产生于子进程状态改变后,父进程需要调用一个wait函数以检测发生了什么。

SIGCLD处理方式:

  • 如果进程明确地将该信号的配置设置为SIG_IGN,则调用进程的子进程将不产生僵尸进程。子进程再终止时,将其状态丢弃。如果调用进程随后调用一个wait函数,那么它将阻塞直到所有子进程都终止,然后wait会返回-1,并将其errno设置为ECHILD。(此信号的默认配置是忽略,与SIG_IGN不同)
  • 如果将SIGCLD的配置设置为捕捉,则内核立即检查是否有子进程准备好被等待,如果是这样,则调用SIGCLD处理程序。

可靠信号术语和语义

当造成信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号)。事件可以是硬件异常(除0),软件条件(如alarm定时器超时),终端产生的信号或调用kill函数。

当一个信号产生的时候,内核通常在进程表中以某种形式设置一个标志,当对信号采取了这种的动作的时候,我们说向进程递送了一个信号。

在信号产生(generation)和递送(delivery)之间的时间间隔,信号是未决的

进程可以选用“阻塞信号递送”,如果为进程产生了一个阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程为对此信号解除了阻塞,或者将该信号的动作改为忽略。

内核在递送一个原来被阻塞的信号给进程时(而不是在产生该信号时)才决定对它的处理方式。于是进程在信号递送给它之前仍可改变对该信号的动作。进程调用sigpending函数来判断哪些信号是设置为阻塞并处于未决状态的。

如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,POSIX.1允许系统递送该该信号一次或多次。如果递送该信号多次,则称这些信号进行了排队。除非支持POSIX.1实时扩展,否则大多数UNIX并不对信号排队,而是只递送这种信号一次。

每个进程都有一个信号屏蔽字(signal mask),它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已设置,则它当前是被阻塞的。进程可以调用sigprocmask函数来检测和更改当前信号屏蔽字。

1
2
3
4
#include<signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
//成功返回0,出错返回-1

kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发送信号。raise(signo); == kill(getpid(), signo);

kill的pid参数有以下4种不同的情况:

  • pid > 0 将该信号发送给进程ID为pid的进程。
  • pid == 0 将该信号发送给与发送进程属于同一进程组的所有进程,而且发送进程具有权限向这些进程发送信号。
  • pid < 0 将该信号发送给其进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程。
  • pid == -1 将该信号发送给进程有权限向他们发送信号的所有进程。(不包括系统进程集种的进程)

进程将信号发送给其它进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户:发送者的实际用户ID或有效用户ID必须等于接收者的实际用户ID或有效用户ID。

使用alarm函数可以设置一个定时器,在将来某个时刻该定时器会超时。当定时器超时时,产生SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作时终止调用该alarm函数的进程。

1
2
3
4
5
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值为0或以前设置的闹钟时间的余留秒数
int pause(void);
//返回值-1,errno设置为EINTR

每个进程只能有一个闹钟时间。

pause函数使调用进程挂起直至捕捉一个信号。

信号集

1
2
3
4
5
6
7
8
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);    //信号集中添加一个信号
int sigdelset(sigset_t *set, int signo);    //信号集中删除一个信号
//以上4个函数成功返回0出错返回-1
int sigismember(const sigset_t *set, int signo);
//若真返回1,若假返回0

函数sigemptyset初始化由set指向的信号集,清除其中所有的信号。函数sigfillset初始化由set指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用sigemptysetsigfillset一次。

1
2
3
#include<signal.h>
//检测或更改信号屏蔽字,成功返回0,出错返回-1
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
  • 若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
  • 若set是非空指针,则参数how指示如何修改当前信号屏蔽字。
how 说明
SIG_BLOCK 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含期望阻塞的附加信号。
SIG_UNBLOCK 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集补集的交集。set包含了希望解除阻塞的信号。
SIG_SETMASK 该进程新的信号屏蔽字是set指向的值。

sigpending函数返回一信号集,对于调用进程而言,其中的信号是阻塞不能递送的,因而也一定是当前未决的。

1
2
3
#include<signal.h>
//成功返回0,出错返回-1
int sigpending(sigset_t *set);

sigaction函数的功能是检查或修改(或检查并修改)与指定信号相关联的处理动作。此函数取代unix早期版本使用的signal函数。参数signo是要检测或修改其具体动作的信号编号。若act指针非空,则要修改其动作。若oact指针非空,则系统经由oact指针返回该信号的上一个动作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include<signal.h>

struct sigaction {
    void      (*sa_handler)(int);   /* addr of signal handler, */
                                    /* or SIG_IGN, or SIG_DFL */
    sigset_t sa_mask;               /* additional signals to block */
    int      sa_flags;              /* signal options, Figure 10.16 */
    /* alternate handler */
    void     (*sa_sigaction)(int, siginfo_t *, void *);
};

int sigaction(int signo, const struct sigaction *restrict act,
    struct sigaction *restrict oact);

sigsuspend函数原子的恢复信号屏蔽字,然后使进程休眠。

1
2
#include<signal.h>
int sigsuspend(const sigset_t *sigmask);

abort函数使程序异常终止。此函数将SIGABRT信号发送给调用进程(经常不应忽略此信号)。

1
2
#include<stdlib.h>
void abort(void);
1
2
3
4
5
#include<unsitd.h>
unsigned int sleep(unsigned int seconds);
int nanosleep(const struct timespec *reqtp, struct timespec *remtp);
int clock_nanosleep(clockid_t clock_id, int flags,
        const struct timespec *reqtp, struct timespec *remtp);

sleep函数使调用进程被挂起直到满足:

  • 已经经过seconds所指定的墙上时钟时间。
  • 调用进程捕捉到一个信号并从信号处理程序返回。当由于捕获到某个信号sleep提早返回时,返回值是未休眠的描述。

nanosleep函数挂起调用进程,直到要求的时间已经超过或者某个信号中断了该函数。reqtp参数用秒和纳秒指定了需要休眠的时间长度。如果某个信号中断了休眠间隔,进程并没有终止,remtp参数指向的timespec结构就会被设置为未休眠的时间长度。

clock_nanosleep函数用于特定时钟的延迟时间调用。