在开始之前,我干脆建议你大体上忽略这一整章,改用你的操作系统(很可能)更高级的信号处理函数。类 Unix 系统有 sigaction() 一族函数,而 Windows 有的是……它自己那套东西171。
把这个放一边,我们先说说信号是什么?
在各种外部事件发生时,会引发一个信号。你可以把程序配置成被中断以处理该信号,并且(可选地)在处理完信号之后从刚才中断的地方继续。
可以把它想成一个在这些外部事件之一发生时被自动调用的函数。
这些事件是什么?在你的系统上,它们可能有很多,但在 C 规范里只有寥寥几个:
| 信号 | 说明 |
|---|---|
SIGABRT |
异常终止——调用 abort() 时发生的情况。 |
SIGFPE |
浮点异常。 |
SIGILL |
非法指令。 |
SIGINT |
中断——通常是按下 CTRL-C 的结果。 |
SIGSEGV |
“段错误”:非法的内存访问。 |
SIGTERM |
请求终止。 |
你可以用 signal() 函数把程序设置为忽略、处理或允许每种信号的默认动作。
signal() 处理信号signal() 调用接受两个形参:所讨论的信号,以及该信号被引发时要采取的动作。
这个动作可以是三种东西之一:
SIG_IGN 用于忽略该信号。SIG_DFL 用于恢复该信号的默认处理函数。我们来写一个没法用 CTRL-C 退出的程序。(别担心——在下面这个程序里,你还可以按 RETURN,它就会退出。)
#include <stdio.h>
#include <signal.h>
int main(void)
{
char s[1024];
signal(SIGINT, SIG_IGN); // 忽略由 ^C 引发的 SIGINT
printf("Try hitting ^C... (hit RETURN to exit)\n");
// 等待一行输入,免得程序直接就退出了
fgets(s, sizeof s, stdin);
}看看第 8 行——我们让程序忽略 SIGINT,也就是按下 CTRL-C 时引发的中断信号。无论你按多少次,这个信号都会被忽略。如果把第 8 行注释掉,你就会发现自己可以肆无忌惮地 CTRL-C,当场退出程序。
我提到过,你也可以写一个在信号被引发时被调用的处理函数。
这些函数相当直接,但就规范而言,它们的能力也非常有限。
在开始之前,我们来看看 signal() 调用的函数原型:
void (*signal(int sig, void (*func)(int)))(int);很容易读懂,对吧?
错了! :)
我们花点时间把它拆开来练练手。
signal() 接受两个实参:一个表示信号的整数 sig,以及一个指向处理函数的指针 func(该处理函数返回 void,并接受一个 int 作为实参),下面高亮显示:
sig func
|-----| |---------------|
void (*signal(int sig, void (*func)(int)))(int);基本上,我们要传入自己关心、想要捕获的信号编号,还要传入一个指向如下形式函数的指针:
void f(int x);由它来完成实际的捕获工作。
现在——原型剩下的部分呢?那基本上全是返回类型。你看,成功时 signal() 会返回你作为 func 传入的那个东西……也就是说,它返回的是一个指针,指向“返回 void 并接受一个 int 实参”的函数。
returned
function indicates we're and
returns returning a that function
void pointer to function takes an int
|--| | |---|
void (*signal(int sig, void (*func)(int)))(int);另外,出错时它还可能返回 SIG_ERR。
我们做一个例子,让程序必须按两次 CTRL-C 才能退出。
我要说明一点:这个程序在几处地方涉及未定义行为。但它多半在你那儿能正常工作,而且要想出可移植又不平凡的演示实在很难。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int count = 0;
void sigint_handler(int signum)
{
// 编译器有权运行:
//
// 即 signal(signum, SIG_DFL)
//
// 此时处理函数被调用。所以我们在这里重置处理函数:
signal(SIGINT, sigint_handler);
(void)signum; // 消除未使用变量的警告
count++; // 未定义行为
printf("Count: %d\n", count); // 未定义行为
if (count == 2) {
printf("Exiting!\n"); // 未定义行为
exit(0);
}
}
int main(void)
{
signal(SIGINT, sigint_handler);
printf("Try hitting ^C...\n");
for(;;); // 在此永远等待
}你会注意到的一件事是,我们在第 14 行重置了信号处理函数。这是因为 C 允许在运行你的自定义处理函数之前,先把信号处理函数重置为其 SIG_DFL 行为。换句话说,它可能是一次性的。所以我们第一件事就是重置它,这样下一个信号我们还能继续处理。
在这个例子里,我们忽略了 signal() 的返回值。如果我们之前把它设成了另一个处理函数,它就会返回指向那个处理函数的指针,我们可以这样取到它:
// old_handler 的类型是 "指向函数的指针,该函数接受一个
// int 形参,并返回 void":
void (*old_handler)(int);
old_handler = signal(SIGINT, sigint_handler);话虽如此,我并不确定这有什么常见用途。但如果你出于某种原因需要旧的处理函数,就可以这样拿到它。
关于第 16 行再快速说一句——那只是告诉编译器,不要因为我们没使用这个变量而发出警告。这就好比说:“我知道我没用它,你不用提醒我。”
最后你会看到,我在几处标出了未定义行为。下一节再细说。
事实证明,在信号处理函数里能做什么、不能做什么,限制相当多。这也是我说你根本不该费这个劲、而应该改用操作系统的信号处理功能(例如类 Unix 系统上的 sigaction())的原因之一。
维基百科甚至说,你唯一真正可移植的做法就是用 SIG_IGN 或 SIG_DFL 调用 signal(),仅此而已。
下面这些是我们不能以可移植方式做到的:
printf() 这种。static 变量、文件作用域变量或线程局部变量中读取或写入值。
volatile sig_atomic_t 类型的变量赋值。最后那一小点——sig_atomic_t——就是你从信号处理函数里往外送数据的通行证。(除非你想用无锁原子对象,那不在本节讨论范围内172。)它是一种整数类型,可能是有符号的,也可能是无符号的。而且它的范围受限于你能往里塞什么。
你可以在 SIG_ATOMIC_MIN 和 SIG_ATOMIC_MAX 这两个宏里查看允许的最小值和最大值173。
令人困惑的是,规范还说,你不得引用“具有静态或线程存储期、且不是无锁原子对象的任何对象,除非是给声明为 volatile sig_atomic_t 的对象赋值 […]”
我的理解是:凡不是无锁原子对象的东西,你既不能读也不能写。另外,你可以给 volatile sig_atomic_t 的对象赋值。
但你能从它读取吗?说实话,我看不出为什么不行,只是规范在提到赋值时措辞非常明确。可如果你必须读取它,并据此做出任何判断,那就可能给某种竞争条件留下可乘之机。
考虑到这一点,我们可以把“连按两次 CTRL-C 才退出”的代码改写得稍微可移植一些,尽管输出没那么啰嗦。
我们把 SIGINT 的处理函数改成:除了递增一个类型为 volatile sig_atomic_t 的值之外,什么都不做。这样它就能统计 CTRL-C 被按下的次数。
然后在主循环里,我们检查那个计数器是否超过 2,如果超过就退出。
#include <stdio.h>
#include <signal.h>
volatile sig_atomic_t count = 0;
void sigint_handler(int signum)
{
(void)signum; // 未使用变量的警告
signal(SIGINT, sigint_handler); // 重置信号处理函数
count++; // 未定义行为
}
int main(void)
{
signal(SIGINT, sigint_handler);
printf("Hit ^C twice to exit.\n");
while(count < 2);
}又是未定义行为?据我理解是的,因为要递增并存储这个值,就必须先读取它。另一个线程可能会来乱动 count,把我们搞疯。但在这种简单情形下,并没有别的线程这么做,所以我们可以对这种越界行为睁一只眼闭一只眼,开开心心地看演示。
如果我们只想把退出推迟一次 CTRL-C 的时间,那还不算太麻烦。但若想再推迟更多次,就得搞一些荒唐的函数链式调用了。
我们要做的是只处理一次,而处理函数会把该信号重置为默认行为(也就是退出):
#include <stdio.h>
#include <signal.h>
void sigint_handler(int signum)
{
(void)signum; // 未使用变量的警告
signal(SIGINT, SIG_DFL); // 重置信号处理函数
}
int main(void)
{
signal(SIGINT, sigint_handler);
printf("Hit ^C twice to exit.\n");
while(1);
}等后面讲到无锁原子变量时,我们会看到修复 count 版本的办法(前提是你的系统确实支持无锁原子变量)。
这就是为什么我在开头建议你去了解操作系统内置的信号系统,它很可能是更好的替代方案。
signal()再说一遍,请使用操作系统内置的信号处理机制或类似的东西。它不在规范里,可移植性也没那么好,但功能很可能强大得多。况且你的操作系统多半还定义了一批 C 规范里没有的信号。再说,用 signal() 本来就很难写出可移植的代码。