对于进程来说,信号就像它的客人。客人来了,进程没好好执行它,那么客人会很生气,后果很严重。
比如你发送信号 1 信号 2 给进程,直接导致进程退出。客人来你家了,如果你不理会,导致的就是你家被毁(^_^客人还是挺牛逼的)。
本篇学习一个ANSI C 规定的函数 signal,这个函数,可以帮助我们招待指定的客人。
1. 如何招待客人
- 按照规则编写信号处理函数
- 使用 signal 函数安装你刚刚编写的信号处理函数,同时指定该信号函数可以处理哪个信号
上面是编写信号处理函数的基本规则。
2. signal 函数
- 函数原型
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 参数 handler
这个函数可能是你见过有史以来最复杂的函数了,原因在于它的第二个参数是函数指针。一般来说,参数里有函数指针的函数,称之为“注册”函数,而指针指向的那个函数,称之为“回调函数”。
所以 signal 函数可以称它为信号注册函数。你注册了你指定的函数,你的函数就可以被回调了。回调的意思是指不需要你亲自调用,而是由别人(一般来说是操作系统)调用。这是一种常见的编程技巧。
你也可以编写一些工具库,提供注册函数给别人注册他们自己的函数,如此一来即使别人的函数还没写好(你根本不知道谁会使用,什么时候使用你的工具库),你就可以事先在你的工具库里调用这个还没写好的函数啦,是不是有点未卜先知的意思?
系统为我们事先提供好的两个宏,分别是 SIG_DFL (default) 和 SIG_IGN (ignore)。如果 handler 被指定为 SIG_DFL,系统将为该信号指定默认的信号处理函数,如果 handler 被指定为 SIG_IGN,系统将忽略该信号。实际上在程序启动时,所有信号的处理函数都被指定为默认或者忽略。
注意:如果你需要指定自己编写的信号处理函数,你的函数格式必须为 void func(int) 这种形式,函数的名字可以随便,但是参数和返回值不能随便改。
- 参数 signum
signal 的第一个参数指示了你需要捕捉哪个信号,这很简单。后面用例子讲解。
- 返回值
signal 的返回值表示旧的信号处理函数。如果返回值等于 SIG_ERR 说明注册失败。
3. 招待你的客人吧
下面这个例子的功能很简单,捕捉了SIGUSR1, SIGUSR2, SIGINT, SIGTSTP, SIGQUIT
以及 SIGSEGV
信号,sighandler 函数是自己编写的信号处理函数,这个函数必须以 void 返回,并且带一个 int 参数。
主函数主要做了三件事:
- 打印自己的进程 id 号
- 注册完所有你想捕捉的信号
- 每隔 10 秒打一个点。
- 代码
// catchsignal.c
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void sighandler(int sig) {
switch(sig) {
case SIGUSR1:
printf("hello SIGUSR1\n");break;
case SIGUSR2:
printf("hello SIGUSR2\n");break;
case SIGINT:
printf("休想干掉我!\n");break;
case SIGTSTP:
printf("不要停止我!\n");break;
case SIGQUIT:
printf("就是不退出!\n");break;
case SIGSEGV:
printf("呃!程序出 bug 了!\n");break;
default:
printf("hello, who are you %d?\n", sig);
}
sleep(2); // 删除这一行,再给程序发信号,看看 main 函数打点的情况。
}
int main() {
printf("I'm %d\n", getpid());
if (SIG_ERR == signal(SIGUSR1, sighandler)) {
perror("signal SIGUSR1");
}
if (SIG_ERR == signal(SIGUSR2, sighandler)) {
perror("signal SIGUSR2");
}
if (SIG_ERR == signal(SIGINT, sighandler)) {
perror("signal SIGINT");
}
if (SIG_ERR == signal(SIGTSTP, sighandler)) {
perror("signal SIGTSTP");
}
if (SIG_ERR == signal(SIGQUIT, sighandler)) {
perror("signal SIGQUTI");
}
if (SIG_ERR == signal(SIGSEGV, sighandler)) {
perror("signal SIGSEGV");
}
while(1) {
write(STDOUT_FILENO, ".", 1);
sleep(10);
}
return 0;
}
- 编译
$ gcc catchsignal.c -o catchsignal
- 运行
$ ./catchsignal
程序运行起来后,你可以试试快捷键 Ctrl + C
、Ctrl + Z
以及 Ctrl + \
,看看你的进程有何反应。
你也可以再打开一个终端,使用 kill
命令发送信号给你的进程。
代码我都给你写好了,请一定动手完成,看看发送不同的信号程序的反应。
提示:
kill -9 pid
可以终结你的进程。所有信号 9 被称为——终结者。
4. 总结
- 掌握信号注册函数 signal
- 理解什么是注册函数,什么是回调函数
- 1-31 号信号是不可靠的,可能会丢失(参考练习1)
- 信号会打断某些(不支持自动重启的函数,后面会讲)正在阻塞的函数(参考练习2)
- SIGKILL 和 SIGSTOP 信号无法被捕获(参考练习3)
练习:
- 在上面的实验中,如果你连续发送相同的信号,会有什么结果?请仔细观察!
- 把信号处理函数的 sleep 语句删除,再重新编译运行。当程序捕捉到信号后,是否还要等待 10 秒才会打点?
- 最后,请你尝试捕捉 SIGKILL 和 SIGSTOP 信号,看看能否成功。(提示:有些客人招待下就没事了,有些客人很霸道,比如城管)。