信号(signals)

信号(signal)是*nix以及POSIX兼容系统中的一种进程间通讯的方法。signal大致可以做如下划分:

  • Traditional Unix signal
  • POSIX standard signal
  • POSIX real time signal

每一个信号对应一个整数值,每个信号都定义了默认行为,即进程收到信号之后默认执行的动作,
这些以及signal编程所需的其他内容都定义在signal.h中,可以通过kill -l命令查看都有
哪些signal,其中SIGRTMINSIGRTMAX之间的signal用于实时信号(real time signal)。有
多种方式可以产生信号,当然进程也有多种方式来处理信号:

  • 使用默认行为
  • 忽略信号(ignore)
  • 捕获信号(catch),即用户自定义signal handler
  • 阻塞信号(block)

信号的产生

产生信号的方式有多种,

  • 通过终端按键产生信号,如Ctrl-C产生SIGINT信号,Ctrl-\产生SIGQUIT信号
  • 通过系统调用向进程发送信号
    • int kill(pid_t pid, int signo); // 向其他进程发送信号
    • int raise(int signo); // 向当前进程发送信号(自己发给自己)
    • void abort(void); // 向当前进程发送SIGABRT信号
  • 通过软件产生信号,如通过alart系统调用产生SIGALRM信号

传统的Unix信号

Unix信号使用方法比较简单,它只有一个接口,

1
2
3
4
5
6
7
8
9
10
11
12
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);

// 另一种比较容易理解的方式:
// signal接受一个整数信号和一个func类型的函数,然后返回一个func类型的函数
typedef void (*func)(int);
func signal(int sig, func f);

// 忽略信号sig
signal(sig, SIG_IGN);
// 重置sig信号的默认行为
signal(sig, SIG_DFL);

Unix信号接口可以忽略、捕获信号,但是无法阻塞信号。

一个简单的实例:

Tradational Unix Signal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <signal.h>

static void signal_handler(int signo) {
switch(signo) {
case SIGINT:
printf("You'v hit Ctrl+C.\n");
signal(SIGINT, SIG_DFL);
case SIGQUIT:
printf("You'v hit Ctrl+\.\n");
signal(SIGQUIT, SIG_DFL);
default:
printf("I don't know this signal.\n");
}
}

int main() {
// ...
signal(SIGTTOU, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
signal(SIGINT, signal_handler);
signal(SIGQUIT, signal_handler);
// ...
return 0;
}

程序将会忽略SIGTTOUSIGTTIN信号,捕获SIGINTSIGQUIT信号,并设置
signal_handler函数为新的handler。在handler中,收到信号之后先打印出相关信息,
然后重置信号的默认行为。

整个过程大致是这样的(以用户按下Ctrl+\为例):

  1. main函数中注册SIGQUIT信号的处理函数为signal_handler
  2. 程序执行过程中键盘中断到达,切换到内核态执行
  3. 中断处理完毕后,在返回用户态之前检查发现有SIGQUIT信号(键盘驱动程序把Ctrl+\翻译为SIGQUIT信号)
  4. 内核决定返回用户态执行signal_handler函数,它跟main函数使用不同的栈空间,这是两个独立的控制流程
  5. signal_handler执行完毕自动执行特殊的系统调用sigreturn再次进入内核态
  6. 内核再次检查信号,如果没有新的信号就恢复main函数的上下文继续执行

然而,Unix信号过于简单,很多场景下它会显得捉襟见肘,如,

  • 在信号处理函数执行的过程中,新的信号到达该如何处理(特殊地,同样的信号再次到达)

针对这些问题,POSIX提出来他们的解决方案。

POSIX信号

POSIX信号兼容Unix信号。首先需要明确以下概念:

  • 当信号出现时,我们用产生(generated)来表述
  • 我们可以为信号定义动作
  • 当信号对应的动作执行时,我们说信号被送达(delevered)了
  • 从产生到送达,这期间信号处于悬停(pending)状态
  • 一个进程的信号可以阻塞,如果该进程没有忽略该信号,那么该信号将处于pending状态
  • 处于阻塞状态的信号可能多次产生,如果内核多次送达该信号,则称该信号被入队(queued),如果只送达一次,则它没有被入队
  • 每个进程都有一个bit array称为signal mask,表示哪些信号被阻塞:一个bit代表一个信号的阻塞状态,如果该bit为on则表示阻塞
POSIX标准信号

在内核中信号大致是这样的,

Signal in Kernel

为此,POSIX定义了一套接口用来处理信号,

signal set operations

signal set数据结构sigset_t用来表示一个信号集合,它用一个bit来表示一个信号
的on或者off,对应的有一套操作函数,

1
2
3
4
5
6
#include <signal.h>
int sigemptyset(sigset_t *set); // clear set, set all bit off
int sigfillset(sigset_t *set); // fill set, set all bit on
int sigaddset(sigset_t *set, int signo); // add signo into set
int sigdelset(sigset_t *set, int signo); // delete signo from set
int sigismember(const sigset_t *set, int signo); // does signo be member of set
sigprocmask函数

该函数可以读取或者改变进程的block mask,

1
2
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数how可以是

  • SIG_BLOCK:效果相当于mask |= set
  • SIG_UNBLOCK:效果相当于mask &= ~set
  • SIG_SETMASK:效果相当于mask = set

如果oldset不为NULL则会保存之前的mask值,它可以用于恢复以前的状态。

sigpending函数

该函数用于读取当前pending状态的信号,

1
2
#include <signal.h>
int sigpending(sigset_t *set); // 处于pending状态的信号保存在set中
sigaction函数

该函数可以读取和修改与指定信号相关联的处理动作,

1
2
3
4
5
6
7
8
9
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
void (*sa_handler)(int); // signal handler or SIG_DFL or SIG_IGN
sigset_t sa_mask; // handler执行期间block mask设置为sa_mask,执行完之后自动恢复
int sa_flags; // TODO 待补充。。。
void (*sa_sigaction)(int, siginfo_t *, void *);
}
sigsuspend函数

该函数重置进程block mask并挂起当前进程,注意,这两步是原子操作,这很重要。
函数返回后,block mask恢复为原来的值。

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

写一个闹钟程序,每隔一段时间闹钟响一次,在闹钟响的间歇,进程挂起。

闹钟程序 v1
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
#include <unistd.h> // for pause()
#include <signal.h>
#include <stdio.h>

void alarm_handler(int signo) { /* nothing to do */ }

void mysleep(unsigned int t) {
struct sigaction newact, oldact;

newact.sa_handler = alarm_handler;
sigemptyset(&newact.sa_mask); // unblock all signals
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact); // set user defined handler

alarm(t); // t seconds之后内核将会给进程发送SIGALARM信号
pause(); // 设置好闹钟之后,进程挂起

alarm(0); // 取消闹钟
sigaction(SIGALRM, &oldact, NULL); // reset SIGALARM handler
}

int main() {
while(1) {
mysleep(1);
printf("ONE second passed\n");
}
return 0;
}

仔细分析mysleep函数,该函数有个致命之处,如果在alarm函数和pause函数之间程序
暂停超过1s(可能被调度了),那么就会导致调用pause之前,SIGALAM信号到达,然后执行
handler,之后才执行pause函数,如果在此之后没有其他信号送达,那么该进程就会永远
被挂起。为避免这种情况,我们必须要保证SIGALRM信号在pause之后到达,一个“解决”方法
是使用信号阻塞。其他不变,

闹钟程序,使用signal block
1
2
3
4
5
6
7
8
9
10
// ...

// 1. 屏蔽SIGALRM信号
// 2. 调用alarm
alarm(t);
// 3. 接触信号屏蔽
// 4. 挂起
pause();

// ...

但是,信号在3, 4之间到达呢,仍然无法解决问题,如果能保证3, 4是原子操作,问题就解决了,
这正是sigsuspend函数的作用,

闹钟程序,使用sigsuspend函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void mysleep(unsigned int t) {
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspendmask;

newact.sa_handler = alarm_handler;
sigemptyset(&newact.sa_mask); // unblock all signals
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact); // set user defined handler

// block SIGALRM
sigemptyset(&newmask);
sigaddset(&newmask, SIGALARM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);

alarm(t);

suspendmask = oldmask;
sigdelset(&suspendmask, SIGALARM); // unblock SIGALARM
sigsuspend(&suspendmask); // set block mast as suspendmask, and suspend

alarm(0);
sigaction(SIGALRM, &oldact, NULL);
sigprocmask(SIG_SETMASK, &oldmask, NULL);
}
POSIX实时信号

待续。。。


参考文档

  1. All about Linux signals
  2. Linux C编程一站式学习(pdf)