Unix信号

信号

常见信号

  • SIGABRT:异常终止,默认动作:终止+core.
  • SIGALRM:定时器超时,终止
  • SIGCHLD:子进程状态改变,忽略
  • SIGINT:终端中断符,终止
  • SIGKILL:终止,终止
  • SIGPIPE:写至无读进程的管道,终止
  • SIGQUIT:中断推出符,终止+core
  • SIGSTOP:停止,停止进程
  • SIGTERM:终止,终止

值得注意的是,我们可以为很多信号编写自己的信号处理函数,但是对于SIGSTOP和SIGKILL,它们既不能被捕捉也不能被忽略。

几种终止信号的比较

SIGINT:当用户按中断键,终端驱动程序产生此信号并发送到前台进程组中每一个进程。

SIGABRT:调用abort产生此信号,进程异常终止。

SIGKILL:不能被忽略或捕捉的信号,杀死一个进程可靠的方法。

SIGQUIT:相比于SIGINT,同时产生一个core文件。

SIGTERM:kill命令的默认终止信号。SIGTERM让程序有机会在推出前做好清理工作,从而优雅的终止。

SIGSTOP:作业控制信号,停止一个进程,类似于交互停止信号。不能被忽略或捕捉。

常用Api

1
2
3
#include<signal.h>
void (*signal(int signo, void (*func)(int)))(int);
//返回值:若成功,返回以前的信号处理配置;若出错,返回SIG_ERR.
1
2
3
4
#include<signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
//返回值:若成功,返回0;若出错,返回-1.

raise(signo)等价于调用kill(getpid(),signo)

进程将信号发送给其他进程需要权限,超级用户可以将信号发送给任一进程。对于非超级用户,基本规则是发送者的实际用户ID或有效用户ID必须等于接收者的实际用户ID或有效用户ID。(谁登录,执行进程的实际ID就是谁)

1
2
3
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
//返回:0或以前设置过的闹钟时间的余留时间。

每个进程只能有一个闹钟时间,以前注册的时间将作为新闹钟的返回值返回。如果seconds为0,相当于取消以前的闹钟。alarm的默认动作是终止进程,我们必须在alarm之前安装处理程序,不然可能出现意外情况。

1
2
3
#include<unistd.h>
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 sigdelset(sigset_t *set, int signo);
//返回:若成功,返回0;若出错,返回-1
int sigismember(const sigset_t *set, int signo);
//返回:若真,返回1;若假,返回0
1
2
3
#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功,返回0,若出错,返回-1.

how是可选的选项,包括SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK。注意:不能阻塞SIGKILL和SIGSTOP信号。

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

sigpending返回当前未决的信号。

1
2
3
4
5
6
7
8
9
10
#include<signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
//返回值:若成功,返回0;若出错,返回-1.

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

在信号处理期间将sa_mask加入阻塞集,在调用完后恢复。若同一个信号发生多次,通常不会将它们加入队列,被阻塞的信号发生多次,在恢复后通常只会被调用一次。sa_flags能决定对于信号中断的系统调用是否重启,通过设置SA_INTERRUPT或SA_RESTART.

1
2
3
4
#include<setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
//返回值:若直接调用,返回0;若从siglongjmp调用返回,则返回非0.
void siglongjmp(sigjmp_buf env, int val);

之所以增加对于信号的跳转函数的特殊处理,是因为在信号处理函数里的屏蔽信号跟进程环境是有区别的。当savemask非0,则调用sigsetjmp时在env中保存当前信号屏蔽字。在siglongjmp后恢复。

1
2
3
#include<signal.h>
int sigsuspend(const sigset_t *sigmask);
//返回值:-1,并将errno设置为EINTR

这个函数是为了表达进入休眠,期待被期望的信号唤醒的语义。假想在没有该函数时,我们该如何实现。为期待的信号设置阻塞,然后解除阻塞后紧接着调用pause()?那要是确实有期待的信号被阻塞了,但是在接触阻塞后,调用pause前解除了,可能就存在永远唤不醒的情况。

进程的信号屏蔽字设置为由sigmask指向的值。如果捕捉到一个信号而且从信号处理程序返回,则sigsuspend返回,并恢复原来的屏蔽字。

1
2
#include<stdlib.h>
void abort(void);

调用此函数将给进程发送一个SIGABRT信号。让进程捕捉SIGABRT意图是:在进程终止之前由其执行所需的清理操作。如果进程不在信号处理程序中终止自己,POSIX对此的要求是当信号处理程序返回时,abort终止该进程。POSIX对终止进程提出的要求是,所有打开的标准流应当与对每个流调用fclose相同。

1
2
3
#include<unistd.h>
unsigned int sleep(unsigned int seconds);
//返回值:0或未休眠完的秒数。

函数使进程被挂起直到满足下列两个条件之一:

  • 过了seconds指定的时间。
  • 调用进程捕捉到了一个信号并从信号处理程序返回。

有关信号的一些小内容

可重入函数

​ 我对信号处理函数的理解是像线程一样,它和主线程是共享全局环境的。因此在信号处理函数中调用可重入函数就显得很重要。函数的不可重入性往往是因为如下原因a)使用静态数据结构;b)调用malloc或free;c)它们是标准I/O函数,标准I/O库很多都使用全局数据结构。

​ 信号处理函数虽然像线程,但是线程保存着errno的副本,互不干扰。但信号处理函数不一样,因此,我们在调用一些系统函数时,需要先保存errno值,在调用完后恢复。

SIGCLD语义

​ SIGCLD和SIGCHLD不一样,前者时System V的信号名,与POSIX采用的SIGCHLD不同。System V的该信号有如下特点。

  1. 如果进程明确将信号配置为SIG_IGN,则调用进程的子进程不产生将死进程。
  2. 如果SIGCLD被设置为捕捉,则内核会立即检查是否有子进程准备好。如果是,调用SIGCLD。

可靠信号语义

​ 当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。在信号产生和递送之间的时间间隔里,信号是未决的。内核在将信号递给进程时,才决定处理方式,在此之前可以任意修改对此信号的处理动作。在进程解除对某个信号的阻塞前,这种信号发生多次,Posix允许递送信号一次或多次。除非支持Posix实时扩展,否则大多数UNIX不对信号排队,只递送信号一次。