5. 信号处理函数:从入门到避坑
信号处理,说白了就是让进程能响应外部事件的一种机制。我刚开始学Linux编程时,总觉得信号这东西挺玄乎——不就是发个通知嘛,能有多复杂?直到我在一个嵌入式项目中,因为信号处理函数写得不规范,导致程序莫名其妙地崩溃,排查了整整两天……嗯,从那以后我再也不敢小看信号处理了。
5.1 signal函数:最基础的信号处理
signal()函数是POSIX标准中最古老的信号处理接口。它的原型很简单:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
这个声明看着有点绕,我习惯把它拆开理解:
- signum:要捕获的信号编号,比如SIGINT(2)、SIGTERM(15)
- handler:处理函数的指针,可以是自定义函数,也可以是SIG_IGN(忽略)或SIG_DFL(默认行为)
- 返回值:返回之前的信号处理函数指针
来个最简单的例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
write(STDOUT_FILENO, "Caught SIGINT!\n", 15);
}
int main() {
signal(SIGINT, sigint_handler);
while(1) {
pause();
}
return 0;
}
5.2 sigaction函数:更可靠的替代方案
说实话,sigaction()才是现代Linux编程中推荐的方式。它比signal更强大,也更可控。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct 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); // 已废弃,不用管
};
我项目中常用的做法是:
#include <stdio.h>
#include <signal.h>
#include <string.h>
void handler(int sig) {
write(STDOUT_FILENO, "Signal caught\n", 14);
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask); // 不阻塞额外信号
sa.sa_flags = 0; // 使用默认行为
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
while(1) pause();
return 0;
}
5.3 信号处理函数的可重入性
这是信号处理中最容易踩坑的地方。什么叫可重入?简单说就是:一个函数在被调用执行的过程中,又被再次调用(比如被信号处理函数中断后再次调用),还能正确运行。
为什么会这样?你想想看,信号处理函数是异步执行的。当主程序正在执行某个函数时,信号来了,处理函数被调用。如果处理函数里又调用了同一个函数,或者访问了同一个全局变量,那就乱套了。
可重入函数 vs 不可重入函数
| 可重入函数 | 不可重入函数 |
|---|---|
| 只使用局部变量 | 使用全局或静态变量 |
| 不调用不可重入函数 | 调用malloc/free等 |
| 不操作errno(或先保存再恢复) | 使用标准I/O(printf等) |
| 原子操作或使用锁保护 | 依赖全局状态 |
我曾经在一个网络服务器项目中,信号处理函数里调用了printf()。结果在高并发下,程序偶尔会输出乱码甚至崩溃。排查后发现,printf内部使用了全局缓冲区,信号处理函数和主程序同时操作缓冲区,数据就乱了。
volatile sig_atomic_t g_flag = 0;
void handler(int sig) {
g_flag = 1; // 只做这件事
}
int main() {
signal(SIGINT, handler);
while(1) {
if (g_flag) {
g_flag = 0;
// 在这里做真正的处理
printf("Handling signal in main loop\n");
}
// 其他工作...
}
}
5.4 信号的发送:kill/raise/alarm/pause
信号不光能接收,还得会发。常用的发送函数有这几个:
- kill():向指定进程发送信号
- raise():向自己发送信号
- alarm():定时发送SIGALRM信号
- pause():挂起进程,等待信号
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig); // 向pid进程发送sig信号
int raise(int sig); // 给自己发信号
unsigned int alarm(unsigned int seconds); // seconds秒后发SIGALRM
int pause(void); // 等待信号
举个例子,父子进程通信:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
void child_handler(int sig) {
write(STDOUT_FILENO, "Child: got signal from parent\n", 30);
}
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
signal(SIGUSR1, child_handler);
pause(); // 等待信号
_exit(0);
} else {
// 父进程
sleep(1);
kill(pid, SIGUSR1); // 向子进程发信号
wait(NULL);
}
return 0;
}
5.5 信号集的操作
信号集说白了就是一个位图,用来表示一组信号。Linux提供了几个操作函数:
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空信号集
int sigfillset(sigset_t *set); // 填满信号集(包含所有信号)
int sigaddset(sigset_t *set, int signum); // 添加一个信号
int sigdelset(sigset_t *set, int signum); // 删除一个信号
int sigismember(const sigset_t *set, int signum); // 判断是否在集中
我习惯这样用:
sigset_t set;
sigemptyset(&set); // 先清空
sigaddset(&set, SIGINT); // 添加SIGINT
sigaddset(&set, SIGTERM); // 添加SIGTERM
if (sigismember(&set, SIGINT)) {
printf("SIGINT is in the set\n");
}
5.6 sigprocmask:阻塞信号
有时候我们需要临时屏蔽某些信号,等关键操作完成后再放开。这就是sigprocmask()的用武之地。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how参数有三种:
- SIG_BLOCK:将set中的信号添加到当前阻塞集
- SIG_UNBLOCK:从当前阻塞集中移除set中的信号
- SIG_SETMASK:直接用set替换当前阻塞集
实际场景:
sigset_t new_mask, old_mask;
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
// 阻塞SIGINT
sigprocmask(SIG_BLOCK, &new_mask, &old_mask);
// 执行关键操作,不会被SIGINT打断
do_critical_work();
// 恢复原来的信号掩码
sigprocmask(SIG_SETMASK, &old_mask, NULL);
5.7 sigpending:查看挂起的信号
被阻塞的信号处于"挂起"状态。用sigpending()可以查看当前有哪些信号被挂起:
#include <signal.h>
int sigpending(sigset_t *set);
举个例子:
sigset_t pending_set;
// 先阻塞SIGINT
sigset_t block;
sigemptyset(&block);
sigaddset(&block, SIGINT);
sigprocmask(SIG_BLOCK, &block, NULL);
// 给自己发SIGINT
raise(SIGINT);
// 查看挂起信号
sigpending(&pending_set);
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT is pending\n");
}
// 解除阻塞,SIGINT会立即递达
sigprocmask(SIG_UNBLOCK, &block, NULL);
知识体系总览
下面这张图把信号处理的整体脉络梳理清楚了:
信号处理这块,说白了就是三个核心问题:怎么发、怎么收、怎么控制。发信号用kill/raise/alarm,收信号用signal/sigaction,控制信号用sigprocmask和sigpending。而贯穿始终的,是信号处理函数的可重入性——这是最容易出问题的地方,也是我反复提醒学生要注意的点。
记住一句话:信号处理函数里,能不做的事就别做。实在要做,只设一个标志变量。剩下的,交给主循环去处理。
公众号:蓝海资料掘金营,微信deep3321