C 语言信号处理:signal() 函数与信号捕获
在 Linux 或类 Unix 系统中运行 C 程序时,程序可能会收到来自操作系统的“信号”(Signal),比如用户按下 Ctrl + C 发送的中断信号。如果不做处理,程序会直接终止。使用 signal() 函数可以捕获这些信号,并指定自定义的处理方式,让程序优雅地响应或忽略它们。
1. 理解信号的基本概念
信号是操作系统通知进程发生了某种事件的一种机制。常见的信号包括:
SIGINT:由用户按下Ctrl + C触发,表示“中断”。SIGTERM:请求程序终止(比如执行kill命令)。SIGKILL:强制终止程序(无法被捕获或忽略)。SIGUSR1/SIGUSR2:用户自定义信号,可用于进程间通信。
注意:不是所有信号都能被捕获。例如 SIGKILL 和 SIGSTOP 是不能被 signal() 处理的。
2. 使用 signal() 函数注册信号处理器
signal() 函数的原型在 <signal.h> 头文件中定义:
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
其中:
signum是要捕获的信号编号(如SIGINT)。handler是一个函数指针,指向你自定义的处理函数。
编写步骤如下:
- 包含头文件:在代码开头添加
#include <signal.h>。 - 定义处理函数:该函数必须返回
void,且接受一个int参数(即信号编号)。 - 调用
signal():将信号和处理函数关联起来。 - 编写主程序逻辑:确保程序不会立即退出,以便测试信号处理。
下面是一个完整示例,捕获 SIGINT(即 Ctrl + C):
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handle_sigint(int sig) {
printf("\n收到 SIGINT 信号 (%d),程序不会立即退出。\n", sig);
printf("再按一次 Ctrl+C 才退出。\n");
signal(SIGINT, SIG_DFL); // 恢复默认行为
}
int main() {
signal(SIGINT, handle_sigint);
printf("程序运行中... 按 Ctrl+C 测试信号捕获。\n");
while (1) {
sleep(1);
}
return 0;
}
编译并运行:
gcc -o sigtest sigtest.c
./sigtest
第一次按 Ctrl + C 时,程序会打印提示信息但继续运行;第二次按 Ctrl + C 才会真正退出,因为此时已恢复默认行为(SIG_DFL)。
3. signal() 的局限性与注意事项
虽然 signal() 简单易用,但它存在一些问题:
- 不可靠:在某些系统上,信号处理函数执行后,信号的处理方式可能自动重置为默认(
SIG_DFL),导致后续相同信号无法被捕获。 - 可移植性差:不同 Unix 系统对
signal()的实现不一致。 - 功能有限:无法设置额外选项(如是否重启被中断的系统调用)。
因此,在生产代码中,更推荐使用 sigaction() 函数,它提供了更精确、可靠和可移植的信号处理机制。但对于学习或简单脚本,signal() 足够直观。
4. 常见信号及其默认行为
下表列出了常用信号及其默认动作,帮助你判断哪些信号适合捕获:
| 信号名 | 编号 | 默认行为 | 是否可捕获 |
|---|---|---|---|
SIGINT |
2 | 终止进程 | 是 |
SIGQUIT |
3 | 终止进程并生成 core 文件 | 是 |
SIGTERM |
15 | 终止进程 | 是 |
SIGKILL |
9 | 强制终止(无条件) | 否 |
SIGSTOP |
19 | 暂停进程 | 否 |
SIGUSR1 |
10 | 无默认行为 | 是 |
SIGUSR2 |
12 | 无默认行为 | 是 |
注意:信号编号在不同系统上可能略有差异,应始终使用符号常量(如 SIGINT)而非硬编码数字。
5. 实战:让程序安全退出
很多后台程序需要在收到终止信号时清理资源(如关闭文件、释放内存)。下面是一个安全退出的示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
volatile sig_atomic_t keep_running = 1;
void cleanup_handler(int sig) {
keep_running = 0; // 设置标志,主循环将退出
}
int main() {
signal(SIGINT, cleanup_handler);
signal(SIGTERM, cleanup_handler);
FILE *log = fopen("app.log", "w");
if (!log) {
perror("无法创建日志文件");
exit(1);
}
fprintf(log, "程序启动\n");
fflush(log);
while (keep_running) {
fprintf(log, "工作进行中...\n");
fflush(log);
sleep(2);
}
fprintf(log, "收到终止信号,正在清理...\n");
fclose(log);
printf("程序已安全退出。\n");
return 0;
}
关键点:
- 使用
volatile sig_atomic_t类型的全局变量作为退出标志,确保信号处理函数与主线程之间的可见性。 - 在信号处理函数中只做最简单的操作(如设置标志),避免调用
printf、malloc等非异步信号安全函数。 - 主循环定期检查标志,决定是否退出。
6. 避免在信号处理函数中做复杂操作
信号处理函数是在“中断上下文”中执行的,此时主程序可能处于任意状态。因此,只能调用“异步信号安全”的函数。根据 POSIX 标准,以下函数是安全的(部分):
_exit()write()read()kill()signal()sigaction()
而以下函数不安全,禁止在信号处理函数中使用:
printf()malloc()free()exit()- 大多数标准 I/O 函数
因此,最佳实践是:在信号处理函数中仅设置一个全局标志,由主程序在安全时机处理。
7. 忽略信号
除了自定义处理,你还可以选择忽略某个信号:
signal(SIGINT, SIG_IGN); // 忽略 SIGINT
此后,即使用户按下 Ctrl + C,程序也不会中断。这在某些守护进程或批处理任务中有用。
8. 恢复默认行为
如果之前修改了信号处理方式,可以通过以下方式恢复:
signal(SIGINT, SIG_DFL); // 恢复默认行为(通常是终止进程)
这在临时屏蔽信号后再恢复时很有用。

暂无评论,快来抢沙发吧!