16、信号处理:signal函数、sigaction、SIGINT/SIGTERM处理、优雅退出
信号处理这个话题,说白了就是让程序学会「听人劝」。
你写了一个后台服务,跑得好好的。突然运维说:「停一下,我要升级。」你总不能直接 kill -9 把进程干掉吧?那样数据库连接没关、文件没刷盘、日志写到一半……后果很严重。
所以我们需要信号处理。让程序在收到「请退出」的信号时,先把屁股擦干净,再体面地离开。
16.1 信号是什么?
信号是操作系统发给进程的一种异步通知。你可以把它理解成「软中断」。
常见的信号有:
| 信号 | 默认行为 | 典型触发场景 |
|---|---|---|
| SIGINT | 终止进程 | Ctrl+C |
| SIGTERM | 终止进程 | kill 命令默认发送 |
| SIGKILL | 强制终止(不可捕获) | kill -9 |
| SIGSEGV | 终止 + 产生 core dump | 段错误(野指针) |
| SIGPIPE | 终止进程 | 向已关闭的管道写数据 |
嗯,这里要注意:SIGKILL 和 SIGSTOP 是不能被捕获、阻塞或忽略的。这是内核的底线——总得留一个「核按钮」给管理员。
16.2 signal 函数:最基础的信号处理
signal 函数是 POSIX 标准最早提供的信号处理接口。用法很简单:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
这个声明看着有点吓人。说白了就是:给信号 signum 注册一个处理函数 handler。handler 接收一个 int 参数(信号编号),返回 void。
来个例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("\n收到 SIGINT,但我偏不退出!\n");
}
int main() {
signal(SIGINT, sigint_handler);
while(1) {
printf("工作中...\n");
sleep(1);
}
return 0;
}
运行后按 Ctrl+C,程序不会退出,只会打印一条消息。想退出?只能开另一个终端 kill -9 了。
16.3 sigaction:更可靠的信号处理
我个人习惯用 sigaction 而不是 signal。为什么?因为 sigaction 更可控、更可移植、功能更强。
sigaction 的结构体长这样:
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 带更多信息的处理函数
sigset_t sa_mask; // 处理期间要阻塞的信号集
int sa_flags; // 控制标志
void (*sa_restorer)(void); // 已废弃,不用管
};
使用步骤:
- 定义 struct sigaction 变量
- 设置 sa_handler 或 sa_sigaction
- 设置 sa_mask(可选)
- 设置 sa_flags
- 调用 sigaction 注册
看个例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
volatile sig_atomic_t keep_running = 1;
void handle_signal(int sig) {
if (sig == SIGINT) {
printf("\n收到 SIGINT,准备退出...\n");
keep_running = 0;
} else if (sig == SIGTERM) {
printf("\n收到 SIGTERM,准备退出...\n");
keep_running = 0;
}
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handle_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
while(keep_running) {
printf("服务运行中...\n");
sleep(1);
}
printf("执行清理工作...\n");
// 关闭文件、释放内存、断开数据库连接...
printf("优雅退出完成。\n");
return 0;
}
16.4 SIGINT vs SIGTERM:该怎么处理?
这两个信号经常被混用,但语义不同:
- SIGINT:通常是用户主动中断(Ctrl+C)。可以理解为「嘿,停一下」。
- SIGTERM:系统或管理员发出的终止请求。可以理解为「请正常退出」。
我在项目中遇到过这样的情况:某个守护进程对 SIGINT 和 SIGTERM 做了完全相同的处理——设置退出标志、清理资源、退出。结果有一次运维发 SIGTERM 时,程序正在执行一个耗时操作,清理过程卡住了,导致进程无法退出。最后只能 kill -9。
后来我学乖了:
void handle_sigint(int sig) {
// 用户主动中断,尽快退出
keep_running = 0;
// 不执行耗时清理,留给 SIGTERM 处理
}
void handle_sigterm(int sig) {
// 系统要求退出,执行完整清理
keep_running = 0;
// 触发完整清理流程
do_cleanup = 1;
}
16.5 优雅退出的实践
什么叫优雅退出?就是程序在收到退出信号后:
- 停止接受新请求
- 处理完正在进行的任务(或设置超时)
- 关闭文件描述符、释放内存、刷写缓冲区
- 断开数据库连接、通知其他服务
- 记录退出日志
- 返回正确的退出码
一个典型的优雅退出框架:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
volatile sig_atomic_t shutdown_flag = 0;
volatile sig_atomic_t cleanup_done = 0;
void shutdown_handler(int sig) {
shutdown_flag = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = shutdown_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
// 忽略 SIGPIPE,防止写管道时意外退出
signal(SIGPIPE, SIG_IGN);
while (!shutdown_flag) {
// 主循环:处理任务
// 每次循环检查 shutdown_flag
// 如果设置了,跳出循环
}
// 执行清理
printf("正在清理资源...\n");
// 关闭文件、释放内存等
cleanup_done = 1;
printf("清理完成,退出。\n");
return 0;
}
16.6 信号处理的常见陷阱
- 不要在信号处理函数中调用非安全函数:malloc、free、printf、fprintf 都不安全。用 write 代替。
- 信号处理函数要尽量简单:设置一个标志变量就够了。复杂的逻辑放到主循环中处理。
- 注意信号屏蔽:处理一个信号时,同类型的信号默认会被阻塞。但其他信号可能乱入。用 sa_mask 控制。
- 不要试图捕获 SIGKILL:捕获不了,别白费力气。
- 多线程中的信号:信号处理在多线程中更复杂。建议指定一个线程专门处理信号。
16.7 知识体系图
下面这张图总结了信号处理的核心脉络:
16.8 小结
信号处理是命令行工具开发中绕不开的一环。你想想看,一个不能优雅退出的服务,谁敢在生产环境用?
记住几个要点:
- 能用 sigaction 就别用 signal
- 信号处理函数里只做最轻量的事
- 用 volatile sig_atomic_t 保护共享标志
- 区分 SIGINT 和 SIGTERM 的语义
- 优雅退出不是一句空话,是实实在在的代码
嗯,信号处理就聊到这里。代码写好了,跑起来试试看——按 Ctrl+C 的时候,你的程序会不会「有礼貌地」说再见?
公众号:蓝海资料掘金营,微信deep3321