强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

POSIX 标准详解教程 / 第五章:信号

第五章:信号

深入理解 POSIX 信号机制:信号类型、信号处理、sigaction、可靠信号、实时信号。


5.1 信号概述

5.1.1 什么是信号

信号(Signal) 是 POSIX 中进程间异步通信的一种机制。信号可以:

  • 由内核发送(如除零错误、非法内存访问)
  • 由其他进程发送(如 kill()
  • 由用户从终端发送(如 Ctrl+C

信号是软件中断——它打断进程的正常执行流,强制执行信号处理函数。

5.1.2 标准信号一览

信号编号默认行为说明
SIGHUP1终止终端挂断/守护进程重载配置
SIGINT2终止键盘中断(Ctrl+C)
SIGQUIT3核心转储键盘退出(Ctrl+\)
SIGILL4核心转储非法指令
SIGTRAP5核心转储断点/跟踪陷阱
SIGABRT6核心转储abort() 调用
SIGBUS7核心转储总线错误(内存对齐)
SIGFPE8核心转储浮点异常(除零)
SIGKILL9终止强制终止(不可捕获
SIGUSR110终止用户自定义信号 1
SIGSEGV11核心转储段错误(非法内存访问)
SIGUSR212终止用户自定义信号 2
SIGPIPE13终止管道破裂(读端已关闭)
SIGALRM14终止alarm() 定时器到期
SIGTERM15终止终止请求(优雅退出)
SIGCHLD17忽略子进程状态改变
SIGCONT18继续继续已停止的进程
SIGSTOP19停止停止进程(不可捕获
SIGTSTP20停止终端停止(Ctrl+Z)
SIGTTIN21停止后台进程读终端
SIGTTOU22停止后台进程写终端

不可捕获/忽略的信号SIGKILL(9)和 SIGSTOP(19)无法被捕获、忽略或阻塞。


5.2 signal() vs sigaction()

5.2.1 signal() 的历史问题

signal() 是最早期的信号处理接口,存在以下问题:

问题说明
信号重置某些系统上处理完信号后,处理函数会被重置为默认
不阻塞其他信号处理信号期间不自动阻塞其他信号
行为不一致不同 Unix 系统的 signal() 行为不同

5.2.2 sigaction()——推荐接口

sigaction() 是 POSIX 推荐的信号处理接口,行为明确且可移植:

/*
 * sigaction_basic.c - 使用 sigaction() 处理信号
 * 编译: gcc -Wall -o sigaction_basic sigaction_basic.c
 * 运行: ./sigaction_basic  (然后按 Ctrl+C)
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static volatile sig_atomic_t g_sigint_count = 0;
static volatile sig_atomic_t g_running = 1;

static void sigint_handler(int sig)
{
    (void)sig;
    g_sigint_count++;
    /* 异步信号安全函数非常有限,write() 是安全的 */
    const char msg[] = "\n[信号] 收到 SIGINT (Ctrl+C)\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);

    if (g_sigint_count >= 3) {
        g_running = 0;
    }
}

static void sigterm_handler(int sig)
{
    (void)sig;
    const char msg[] = "\n[信号] 收到 SIGTERM,正在退出...\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);
    g_running = 0;
}

int main(void)
{
    struct sigaction sa_int, sa_term;

    /* 处理 SIGINT (Ctrl+C) */
    sa_int.sa_handler = sigint_handler;
    sigemptyset(&sa_int.sa_mask);     /* 处理期间不额外阻塞信号 */
    sa_int.sa_flags = SA_RESTART;     /* 被中断的系统调用自动重启 */
    sigaction(SIGINT, &sa_int, NULL);

    /* 处理 SIGTERM */
    sa_term.sa_handler = sigterm_handler;
    sigemptyset(&sa_term.sa_mask);
    sa_term.sa_flags = 0;
    sigaction(SIGTERM, &sa_term, NULL);

    printf("PID=%d: 按 Ctrl+C 3 次或 kill %d 退出\n",
           getpid(), getpid());

    while (g_running) {
        pause();  /* 挂起等待信号 */
        printf("  SIGINT 计数: %d\n", g_sigint_count);
    }

    printf("程序正常退出\n");
    return EXIT_SUCCESS;
}

5.2.3 sigaction 结构体详解

struct sigaction {
    union {
        void (*sa_handler)(int);           /* 简单处理函数 */
        void (*sa_sigaction)(int, siginfo_t *, void *);  /* 详细处理函数 */
    };
    sigset_t    sa_mask;    /* 处理期间额外阻塞的信号集 */
    int         sa_flags;   /* 行为标志 */
    void        (*sa_restorer)(void);  /* 内部使用,勿设置 */
};
sa_flags 标志说明
SA_RESTART被信号中断的系统调用自动重启
SA_SIGINFO使用 sa_sigaction 而非 sa_handler
SA_NOCLDSTOP子进程停止时不发送 SIGCHLD
SA_NOCLDWAIT子进程不产生僵尸
SA_NODEFER处理信号期间不自动阻塞该信号
SA_RESETHAND处理后重置为默认处理

5.3 信号集 (Signal Set)

5.3.1 信号集操作

POSIX 使用 sigset_t 类型表示信号集,用于信号阻塞和检测:

/*
 * sigset_demo.c - 信号集操作演示
 * 编译: gcc -Wall -o sigset_demo sigset_demo.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <signal.h>

static void print_sigset(const sigset_t *set)
{
    for (int sig = 1; sig < NSIG; sig++) {
        if (sigismember(set, sig)) {
            printf("  SIG %d", sig);
            /* 获取信号名称 */
            const char *name = "unknown";
            switch (sig) {
                case SIGHUP:  name = "SIGHUP"; break;
                case SIGINT:  name = "SIGINT"; break;
                case SIGQUIT: name = "SIGQUIT"; break;
                case SIGKILL: name = "SIGKILL"; break;
                case SIGTERM: name = "SIGTERM"; break;
                case SIGUSR1: name = "SIGUSR1"; break;
                case SIGCHLD: name = "SIGCHLD"; break;
            }
            printf(" (%s)\n", name);
        }
    }
}

int main(void)
{
    sigset_t set, old_set;

    /* 获取当前信号掩码 */
    sigprocmask(SIG_BLOCK, NULL, &old_set);
    printf("当前阻塞的信号:\n");
    print_sigset(&old_set);

    /* 创建自定义信号集 */
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGUSR1);
    sigaddset(&set, SIGTERM);

    printf("\n自定义信号集:\n");
    print_sigset(&set);

    /* 检查信号是否在集合中 */
    printf("\nSIGINT 在集合中? %s\n",
           sigismember(&set, SIGINT) ? "是" : "否");
    printf("SIGHUP 在集合中? %s\n",
           sigismember(&set, SIGHUP) ? "是" : "否");

    return 0;
}

5.3.2 信号阻塞 (Blocking)

/* 临时阻塞 SIGINT */
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_BLOCK, &block_set, &old_set);

/* 此期间 SIGINT 被挂起 */
/* ... 临界区代码 ... */

/* 恢复原来的信号掩码(SIGINT 可能被递送) */
sigprocmask(SIG_SETMASK, &old_set, NULL);
how 参数说明
SIG_BLOCK将新信号集添加到当前阻塞集(并集)
SIG_UNBLOCK从当前阻塞集中移除指定信号
SIG_SETMASK用新信号集替换当前阻塞集

5.4 可靠信号与不可靠信号

5.4.1 传统信号 vs 可靠信号

特性传统信号(1-31)实时信号(SIGRTMIN ~ SIGRTMAX)
排队❌ 不排队(丢失)✅ 排队(不丢失)
携带数据❌ 不可以✅ 可以(sigval
语义确定⚠️ 某些不确定✅ 确定
编号范围1-31通常 34-64

关键区别:如果同一个信号连续发送多次,标准信号只会递送一次(丢失),而实时信号会排队,全部递送。

5.4.2 发送带数据的信号

/*
 * sigqueue_demo.c - 使用 sigqueue() 发送带数据的信号
 * 编译: gcc -Wall -o sigqueue_demo sigqueue_demo.c
 * 运行: 在终端 A 运行 ./sigqueue_demo
 *        在终端 B 发送: kill -USR1 <PID>
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static volatile sig_atomic_t g_running = 1;

static void sigusr1_handler(int sig, siginfo_t *info, void *context)
{
    (void)sig;
    (void)context;

    const char msg[] = "[SA_SIGINFO] 收到信号\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);

    /* 获取信号发送者 PID */
    char buf[128];
    int len = snprintf(buf, sizeof(buf),
        "  发送者 PID: %d\n"
        "  发送者 UID: %d\n"
        "  附加数据 (int): %d\n",
        info->si_pid,
        info->si_uid,
        info->si_value.sival_int);
    write(STDOUT_FILENO, buf, len);
}

int main(void)
{
    struct sigaction sa;
    sa.sa_sigaction = sigusr1_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;  /* 使用三参数处理函数 */
    sigaction(SIGUSR1, &sa, NULL);

    printf("PID=%d: 等待 SIGUSR1 信号(带数据)...\n", getpid());
    printf("  使用: kill -USR1 %d\n", getpid());
    printf("  或:   kill -10 %d\n", getpid());

    while (g_running)
        pause();

    return 0;
}

5.4.3 使用 sigqueue() 发送数据

/* 向目标进程发送带整数数据的信号 */
pid_t target_pid = 12345;
union sigval value;
value.sival_int = 42;
sigqueue(target_pid, SIGUSR1, value);

5.5 信号与系统调用的交互

5.5.1 慢速系统调用

POSIX 将某些可能无限期阻塞的系统调用称为慢速系统调用(Slow System Call)

慢速系统调用说明
read() 无数据时终端、管道、套接字等
write() 管道满时写入管道/套接字
accept()等待连接
pause()等待信号
sleep() / nanosleep()定时休眠
wait() / waitpid()等待子进程

5.5.2 EINTR 与 SA_RESTART

信号打断系统调用
        │
        ├── sa_flags 不含 SA_RESTART
        │   └── 系统调用返回 -1,errno = EINTR
        │       → 程序需要检查并重新调用
        │
        └── sa_flags 包含 SA_RESTART
            └── 系统调用自动重启
                → 程序无感知
/* 推荐模式:使用 SA_RESTART 处理慢速系统调用 */
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;  /* 关键:自动重启被中断的系统调用 */
sigaction(SIGINT, &sa, NULL);

/* 即使 read() 被信号中断,也会自动重试 */
ssize_t n = read(fd, buf, sizeof(buf));
/* 不需要手动检查 EINTR */

当不使用 SA_RESTART 时,需要手动处理 EINTR

/* 手动重试模式 */
ssize_t safe_read(int fd, void *buf, size_t count)
{
    ssize_t n;
    do {
        n = read(fd, buf, count);
    } while (n == -1 && errno == EINTR);
    return n;
}

5.6 SIGCHLD 与异步等待

5.6.1 信号驱动的子进程回收

/*
 * sigchld_handler.c - 使用 SIGCHLD 信号回收子进程
 * 编译: gcc -Wall -o sigchld_handler sigchld_handler.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>

static volatile sig_atomic_t g_child_count = 0;

static void sigchld_handler(int sig)
{
    (void)sig;

    /* 使用 waitpid(-1, WNOHANG) 回收所有已终止的子进程 */
    /* 必须循环:多个子进程可能同时终止 */
    int saved_errno = errno;
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        g_child_count++;
        /* 异步信号安全函数:write() */
        char buf[128];
        int len = snprintf(buf, sizeof(buf),
            "[SIGCHLD] 回收子进程 PID=%d, 退出码=%d\n",
            pid, WIFEXITED(status) ? WEXITSTATUS(status) : -1);
        write(STDOUT_FILENO, buf, len);
    }
    errno = saved_errno;
}

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sa, NULL);

    /* 创建多个子进程 */
    for (int i = 0; i < 3; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            /* 子进程 */
            sleep(1 + i);
            _exit(i);
        }
        printf("创建子进程 PID=%d\n", pid);
    }

    /* 父进程继续工作,不阻塞等待 */
    while (g_child_count < 3) {
        printf("父进程工作中... (已回收 %d 个子进程)\n", g_child_count);
        sleep(1);
    }

    printf("所有子进程已回收\n");
    return EXIT_SUCCESS;
}

5.7 信号安全函数

在信号处理函数中,只能调用异步信号安全函数(Async-Signal-Safe Functions)

5.7.1 安全函数列表

类别安全函数
I/Owrite(), read(), close()
进程_exit(), execve(), fork(), getpid()
信号signal(), sigaction(), kill(), raise()
内存无(malloc/free 不安全)

5.7.2 不安全函数

❌ 不安全函数原因安全替代
printf()使用全局缓冲区和锁write()
malloc() / free()使用全局堆锁预分配缓冲区
syslog()使用全局状态无直接替代
exit()调用 atexit 处理器_exit()
sprintf()使用全局 localesnprintf()(某些实现安全)

最佳实践:信号处理函数中只设置 volatile sig_atomic_t 标志变量,主循环中检查并处理。如果必须在处理函数中输出,使用 write()


5.8 业务场景

5.8.1 优雅关闭服务器

/*
 * graceful_shutdown.c - 使用信号实现优雅关闭
 * 编译: gcc -Wall -o graceful_shutdown graceful_shutdown.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static volatile sig_atomic_t g_shutdown = 0;
static volatile sig_atomic_t g_reload = 0;

static void handle_signal(int sig)
{
    switch (sig) {
    case SIGTERM:
    case SIGINT:
        g_shutdown = 1;
        break;
    case SIGHUP:
        g_reload = 1;
        break;
    }
}

static void setup_signals(void)
{
    struct sigaction sa;
    sa.sa_handler = handle_signal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;

    sigaction(SIGTERM, &sa, NULL);  /* kill <PID> */
    sigaction(SIGINT,  &sa, NULL);  /* Ctrl+C */
    sigaction(SIGHUP,  &sa, NULL);  /* kill -HUP <PID> */
}

int main(void)
{
    setup_signals();
    printf("服务启动, PID=%d\n", getpid());
    printf("  kill %d       → 优雅关闭\n", getpid());
    printf("  kill -HUP %d  → 重载配置\n", getpid());

    while (!g_shutdown) {
        if (g_reload) {
            g_reload = 0;
            printf("[信号] 收到 SIGHUP,重新加载配置...\n");
            /* 模拟重载 */
        }

        /* 模拟处理请求 */
        printf("处理请求...\n");
        sleep(2);
    }

    printf("\n正在优雅关闭...\n");
    /* 清理资源:关闭连接、写入状态等 */
    printf("资源已清理,服务关闭\n");
    return EXIT_SUCCESS;
}

5.9 注意事项

⚠️ sig_atomic_t:信号处理函数中访问的全局变量必须使用 volatile sig_atomic_t 类型。这是唯一保证原子访问的类型。

⚠️ sigprocmask 与多线程:在多线程程序中,使用 pthread_sigmask() 替代 sigprocmask()sigprocmask() 在多线程程序中的行为是未定义的。

⚠️ 信号与线程:信号可以发送到进程或特定线程。kill() 发送到进程,pthread_kill() 发送到特定线程。使用 sigwait()signalfd() 在专门线程中同步处理信号。

⚠️ 避免在信号处理函数中分配内存malloc() 不是异步信号安全的。在处理函数中使用栈上缓冲区或预分配的静态缓冲区。


5.10 扩展阅读

  1. man 7 signal — Linux 信号概述
  2. man 2 sigaction — sigaction 系统调用
  3. man 2 sigprocmask — 信号掩码操作
  4. man 7 signal-safety — 异步信号安全函数列表
  5. APUE 第 10 章:Signals
  6. TLPI 第 20-22 章:Signals 系列

5.11 本章小结

要点说明
sigaction()推荐的信号处理接口,行为确定且可移植
sig_atomic_t信号处理函数中应使用的原子类型
SA_RESTART自动重启被信号中断的系统调用
sigprocmask()阻塞/解除阻塞信号(多线程用 pthread_sigmask)
SA_SIGINFO三参数处理函数,获取发送者信息
SIGCHLD子进程终止通知,配合 waitpid(WNOHANG)
异步信号安全处理函数中只调用 write() 等安全函数