信号
是什么 / 解决什么问题
信号是 Linux 进程间通信的异步通知机制。内核或其他进程可以向目标进程发送信号,触发预定义的动作(终止、忽略、执行处理函数)。
使用模式
发送信号
#include <signal.h>
kill(pid, SIGTERM); // 向指定进程发送信号
kill(0, SIGTERM); // 向同进程组所有进程发送
kill(-pgid, SIGTERM); // 向指定进程组发送
raise(SIGTERM); // 向自己发送
// 向线程发送
#include <pthread.h>
pthread_kill(tid, SIGUSR1);
// 内核级
tgkill(tgid, tid, sig); // 精确到线程
注册信号处理函数
#include <signal.h>
// sigaction(推荐,行为确定)
struct sigaction sa = {
.sa_handler = handler_fn,
.sa_flags = SA_RESTART, // 被信号中断的 syscall 自动重启
};
sigemptyset(&sa.sa_mask);
sigaction(SIGTERM, &sa, NULL);
// handler 函数
void handler_fn(int signo) {
// 注意:handler 中只能调用 async-signal-safe 函数!
write(STDOUT_FILENO, "caught\n", 7); // write 是安全的
// printf 不安全!malloc 不安全!
}
带信息的信号处理 (SA_SIGINFO)
struct sigaction sa = {
.sa_sigaction = handler_info,
.sa_flags = SA_SIGINFO | SA_RESTART,
};
sigaction(SIGUSR1, &sa, NULL);
void handler_info(int signo, siginfo_t *info, void *context) {
printf("signal %d from pid %d\n", signo, info->si_pid);
}
信号屏蔽
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
// 屏蔽信号(阻塞,不递送)
sigprocmask(SIG_BLOCK, &mask, NULL);
// 在此期间收到的 SIGINT/SIGTERM 被挂起
// 解除屏蔽(挂起的信号立即递送)
sigprocmask(SIG_UNBLOCK, &mask, NULL);
同步等待信号(推荐方式)
// 屏蔽信号后,用 sigwait 同步等待
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGINT);
pthread_sigmask(SIG_BLOCK, &mask, NULL);
int sig;
sigwait(&mask, &sig); // 阻塞直到收到信号
printf("received signal %d\n", sig);
优雅退出模式
// 方法1: volatile 标志位
volatile sig_atomic_t should_exit = 0;
void handler(int sig) {
should_exit = 1;
}
int main() {
signal(SIGTERM, handler);
signal(SIGINT, handler);
while (!should_exit) {
do_work();
}
cleanup();
return 0;
}
// 方法2: self-pipe trick(配合 epoll)
int pipe_fds[2];
pipe2(pipe_fds, O_NONBLOCK | O_CLOEXEC);
void handler(int sig) {
write(pipe_fds[1], &sig, 1); // 写一个字节到 pipe
}
// 在 epoll 中监听 pipe_fds[0],收到事件时处理退出
行为与语义
常用信号
| 信号 |
编号 |
默认动作 |
说明 |
| SIGKILL |
9 |
终止 |
不可捕获、不可忽略 |
| SIGSTOP |
19 |
暂停 |
不可捕获、不可忽略 |
| SIGTERM |
15 |
终止 |
请求优雅退出(可捕获) |
| SIGINT |
2 |
终止 |
Ctrl+C |
| SIGQUIT |
3 |
终止+core |
Ctrl+\ |
| SIGHUP |
1 |
终止 |
终端关闭/常用于重载配置 |
| SIGPIPE |
13 |
终止 |
写已关闭的 pipe/socket |
| SIGCHLD |
17 |
忽略 |
子进程状态变化 |
| SIGUSR1/2 |
10/12 |
终止 |
用户自定义 |
| SIGSEGV |
11 |
终止+core |
非法内存访问 |
| SIGBUS |
7 |
终止+core |
总线错误(如 mmap 越界) |
| SIGALRM |
14 |
终止 |
定时器到期 |
可靠信号 vs 不可靠信号
标准信号 (1-31): "不可靠"
- 不排队。相同信号连续到来时只保留一个
- kill(pid, SIGUSR1) 发送 5 次,目标可能只收到 1 次
实时信号 (SIGRTMIN~SIGRTMAX, 32-64): "可靠"
- 排队。不会丢失
- 可携带额外数据 (sigqueue)
- 按信号编号优先级递送(编号小的优先)
// 实时信号 + 携带数据
union sigval val = { .sival_int = 42 };
sigqueue(pid, SIGRTMIN, val);
信号与系统调用
// 被信号中断的 syscall 返回 -1, errno = EINTR
// 除非设置了 SA_RESTART
// SA_RESTART 对以下 syscall 有效:
// read, write, open, wait, ioctl (大部分 I/O 类)
// SA_RESTART 对以下 syscall 无效(总是返回 EINTR):
// poll, epoll_wait, select, sleep, nanosleep, connect
多线程中的信号递送
信号递送规则:
- 发给进程的信号 (kill):选择一个未屏蔽该信号的线程
- 发给线程的信号 (pthread_kill):只递送给目标线程
- 由异常产生的信号 (SIGSEGV):递送给引起异常的线程
推荐模式:
- 所有工作线程屏蔽信号
- 专用信号线程用 sigwait 处理
性能考量
信号的开销
- 信号递送需要内核介入:修改用户栈,设置信号帧
- 频繁信号递送会影响性能(每次需要保存/恢复上下文)
- 不要用信号做高频通信,用 eventfd/pipe/futex 代替
替代方案
| 需求 |
用信号 |
更好的方案 |
| 通知事件 |
SIGUSR1 |
eventfd + epoll |
| 定时器 |
SIGALRM |
timerfd + epoll |
| 子进程退出 |
SIGCHLD handler |
signalfd + epoll |
| 优雅退出 |
SIGTERM handler |
signalfd 或 self-pipe |
常见陷阱与 FAQ
1. 信号处理函数中的安全问题
// 错误:handler 中调用非 async-signal-safe 函数
void handler(int sig) {
printf("got signal\n"); // 死锁风险!printf 内部有锁
malloc(100); // 死锁风险!malloc 内部有锁
exit(0); // 可能不安全(刷新 stdio 缓冲区)
}
// 安全函数列表见 man 7 signal-safety
// 安全的: write, _exit, atomic 操作, signal
// 不安全的: printf, malloc, free, exit, any function that takes a lock
2. SIGPIPE 导致服务器崩溃
// 对方关闭连接后 write → SIGPIPE → 默认终止进程
// 服务器启动时必须忽略:
signal(SIGPIPE, SIG_IGN);
// 之后 write 到已关闭 fd 返回 -1, errno = EPIPE
3. signal() vs sigaction()
// signal() 行为在不同系统不一致(handler 可能一次性)
// 始终使用 sigaction()
// 另外:signal(SIGCHLD, SIG_IGN) 在 Linux 上有特殊语义
// → 子进程退出时自动回收,不产生 zombie
4. fork 后的信号处理
// fork 后子进程继承信号处理函数
// exec 后所有信号处理恢复默认(SIG_DFL)
// 但被忽略的信号 (SIG_IGN) 在 exec 后仍然被忽略
观测与调试
# 查看进程的信号屏蔽和挂起
cat /proc/<pid>/status | grep -i sig
# SigPnd: 进程级挂起信号
# ShdPnd: 线程级挂起信号
# SigBlk: 屏蔽信号
# SigIgn: 忽略的信号
# SigCgt: 捕获的信号
# strace 看信号递送
strace -e signal=all -p <pid>
# 发送信号并观察
kill -SIGUSR1 <pid>
# 查看信号导致的崩溃
coredumpctl list # systemd 系统
dmesg | grep segfault
延伸阅读
man 7 signal — 信号概述和完整列表
man 2 sigaction — 信号处理
man 7 signal-safety — async-signal-safe 函数列表
man 2 signalfd — 将信号转为文件描述符事件