POSIX 标准详解教程 / 第五章:信号
第五章:信号
深入理解 POSIX 信号机制:信号类型、信号处理、sigaction、可靠信号、实时信号。
5.1 信号概述
5.1.1 什么是信号
信号(Signal) 是 POSIX 中进程间异步通信的一种机制。信号可以:
- 由内核发送(如除零错误、非法内存访问)
- 由其他进程发送(如
kill()) - 由用户从终端发送(如
Ctrl+C)
信号是软件中断——它打断进程的正常执行流,强制执行信号处理函数。
5.1.2 标准信号一览
| 信号 | 编号 | 默认行为 | 说明 |
|---|---|---|---|
SIGHUP | 1 | 终止 | 终端挂断/守护进程重载配置 |
SIGINT | 2 | 终止 | 键盘中断(Ctrl+C) |
SIGQUIT | 3 | 核心转储 | 键盘退出(Ctrl+\) |
SIGILL | 4 | 核心转储 | 非法指令 |
SIGTRAP | 5 | 核心转储 | 断点/跟踪陷阱 |
SIGABRT | 6 | 核心转储 | abort() 调用 |
SIGBUS | 7 | 核心转储 | 总线错误(内存对齐) |
SIGFPE | 8 | 核心转储 | 浮点异常(除零) |
SIGKILL | 9 | 终止 | 强制终止(不可捕获) |
SIGUSR1 | 10 | 终止 | 用户自定义信号 1 |
SIGSEGV | 11 | 核心转储 | 段错误(非法内存访问) |
SIGUSR2 | 12 | 终止 | 用户自定义信号 2 |
SIGPIPE | 13 | 终止 | 管道破裂(读端已关闭) |
SIGALRM | 14 | 终止 | alarm() 定时器到期 |
SIGTERM | 15 | 终止 | 终止请求(优雅退出) |
SIGCHLD | 17 | 忽略 | 子进程状态改变 |
SIGCONT | 18 | 继续 | 继续已停止的进程 |
SIGSTOP | 19 | 停止 | 停止进程(不可捕获) |
SIGTSTP | 20 | 停止 | 终端停止(Ctrl+Z) |
SIGTTIN | 21 | 停止 | 后台进程读终端 |
SIGTTOU | 22 | 停止 | 后台进程写终端 |
不可捕获/忽略的信号:
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/O | write(), read(), close() |
| 进程 | _exit(), execve(), fork(), getpid() |
| 信号 | signal(), sigaction(), kill(), raise() |
| 内存 | 无(malloc/free 不安全) |
5.7.2 不安全函数
| ❌ 不安全函数 | 原因 | 安全替代 |
|---|---|---|
printf() | 使用全局缓冲区和锁 | write() |
malloc() / free() | 使用全局堆锁 | 预分配缓冲区 |
syslog() | 使用全局状态 | 无直接替代 |
exit() | 调用 atexit 处理器 | _exit() |
sprintf() | 使用全局 locale | snprintf()(某些实现安全) |
最佳实践:信号处理函数中只设置
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 扩展阅读
man 7 signal— Linux 信号概述man 2 sigaction— sigaction 系统调用man 2 sigprocmask— 信号掩码操作man 7 signal-safety— 异步信号安全函数列表- APUE 第 10 章:Signals
- TLPI 第 20-22 章:Signals 系列
5.11 本章小结
| 要点 | 说明 |
|---|---|
| sigaction() | 推荐的信号处理接口,行为确定且可移植 |
| sig_atomic_t | 信号处理函数中应使用的原子类型 |
| SA_RESTART | 自动重启被信号中断的系统调用 |
| sigprocmask() | 阻塞/解除阻塞信号(多线程用 pthread_sigmask) |
| SA_SIGINFO | 三参数处理函数,获取发送者信息 |
| SIGCHLD | 子进程终止通知,配合 waitpid(WNOHANG) |
| 异步信号安全 | 处理函数中只调用 write() 等安全函数 |