跳转至

信号

是什么 / 解决什么问题

信号是 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 — 将信号转为文件描述符事件