Unix/Linux信号处理机制


信号是什么

信号是一种软中断,用于通知进程某个事件的发生。

Unix/Linux系统一般定义了30多种信号,编号从1开始,编号0是空信号。这些信号在头文件<signal.h>被定义成以SIG开头的信号名,如SIGINTSIGCHLD等。


信号如何产生

产生信号的条件比如当系统的发生异常时、在终端按下Ctrl+C会发送SIGINT给前台进程组、子进程结束会发送SIGCLD给父进程等。

而用户想自己发送特定的信号给特定进程可以通过kill(2)系统调用发送信号:

1
int kill(pid_t pid, int sig);

也可以通过kill(1)命令,比如强制终止一个进程可以用如下命令:

1
2
kill -s SIGKILL pid # 向进程ID为pid的进程发送SIGKILL信号
kill 9 pid # 同上,9是SIGKILL信号的编号


信号中断进程

进程在CPU的内核态运行时不会被信号中断,在用户态执行或者被阻塞才会被信号中断。

通过调用系统调用函数可以让进程运行在内核态,不过并不是所有的系统调用函数从调用开始到结束都不会被信号中断,比如一些低速系统调用read()write()sleep()wait()等函数)可能会被阻塞,此时就会被信号中断。

被通知信号的进程可以选择:

  1. 忽略该信号
  2. 执行一般是终止进程的默认操作
  3. 捕捉信号,执行用户自定义的信号处理函数

其中SIGKILLSIGSTOP这两个信号不能被忽略或捕捉。

利用系统调用signal()函数可以进行对特定信号的处理行为进行设置:

1
2
3
4
5
6
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc *);
#define SIG_ERR (void (*)())-1
#define SIG_DFL (void (*)())0
#define SIG_IGN (void (*)())1


信号屏蔽

内核为各个进程维护了pendingblocked这两个位向量,类似bitsetbitmap),向量的每一个二进制位表示对应编号的信号是否被标记。

pending就是标记待处理的信号,而blocked就是标记所要屏蔽的信号。进程要处理信号的时候就根据这两个向量从中获得某一个还未处理并且没有被屏蔽的信号。

每当一个信号发生,就会在pending中对应的位中标记为 1。如果这个信号在同一时段发生多次也只是同样标记为 1,发生次数的信息被无视了,这就是相同信号会被合并成一个的道理。

在某一个信号发生后进程执行处理函数前会将该信号屏蔽,即设置blocked中对应的位为 1,执行完处理函数才取消其屏蔽。于是,在信号处理函数里如果发生相同的信号则不会中断去再次处理该信号,而是推延到信号处理函数返回后。

除此之外,可以利用系统调用函数sigprocmask()手动屏蔽信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
信号集sigset_t
*/
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(sigset_t *set, int signo);
/*
sigprocmask(2)
how: SIG_BLOCK/SIG_UNBLOCK/SIG_SETMASK
*set: sigset_t
*oldset: sigset_t
*/
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

写个简单的小例子:

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
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
void fun(int sig)
{
write(STDOUT_FILENO, "*\n", 2);
}
int main()
{
signal(SIGUSR1, fun);
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGUSR1);
sigprocmask(SIG_SETMASK, &mask, &prev);
kill(getpid(), SIGUSR1);
sleep(2);
sigprocmask(SIG_SETMASK, &prev, NULL);
return 0;
}

这个程序里,进程屏蔽了SIGUSR1后向自己发送SIGUSR1信号,并在休眠2秒后取消屏蔽SIGUSR1信号。

其执行的结果是,隔了差不多2秒的时间,进程向控制台输出了*。这是因为,在产生SIGUSR1信号后,pending对应位被设置为 1,但是由于该信号已经被屏蔽即blocked对应位为 1,所以这个信号不会被处理,直到2秒后取消屏蔽,发现这个信号待处理且没被屏蔽,随后才被处理。


最后

在一些早期系统里signal()的实现是不可靠的,所以POSIX定义了一个语义精确的函数sigaction(),用它可以实现各种可靠、不可靠的signal(),达到可移植的目的。用法有点复杂搞不太清楚,所以这儿先略过了。

信号机制还需要注意很多细节,比如:

  • 信号机制会合并相同的信号
  • 多线程并发情况的信号处理
  • 考虑到信号机制的存在操作可能被中断这时如何保证正确性

目前只是了解到用while而不是if、利用信号屏蔽等手段等来解决上述问题,我还需体会体会。