目录
[(1)SIGINT (2) - 中断信号](#(1)SIGINT (2) - 中断信号)
[(2)SIGKILL (9) - 强制终止](#(2)SIGKILL (9) - 强制终止)
[(3)SIGTERM (15) - 正常终止](#(3)SIGTERM (15) - 正常终止)
[(4)SIGSEGV (11) - 段错误](#(4)SIGSEGV (11) - 段错误)
[(5)SIGCHLD (17) - 子进程状态改变](#(5)SIGCHLD (17) - 子进程状态改变)
[1. 终端按键产生](#1. 终端按键产生)
[2. 硬件异常产生](#2. 硬件异常产生)
[3. 系统调用产生](#3. 系统调用产生)
[(1)kill() 函数](#(1)kill() 函数)
[(2)raise() 函数](#(2)raise() 函数)
[(3)abort() 函数](#(3)abort() 函数)
[4. 软件条件产生](#4. 软件条件产生)
[(1)alarm() 函数](#(1)alarm() 函数)
[(2)setitimer() 函数](#(2)setitimer() 函数)
[2.signal() 函数(传统方式)](#2.signal() 函数(传统方式))
[3.sigaction() 函数(推荐方式)](#3.sigaction() 函数(推荐方式))
[(1)sigaction 使用示例](#(1)sigaction 使用示例)
[(2)使用 SA_SIGINFO 获取详细信息](#(2)使用 SA_SIGINFO 获取详细信息)
[1. 信号处理函数的安全要求](#1. 信号处理函数的安全要求)
[2. 使用自管道(Self-Pipe)技巧](#2. 使用自管道(Self-Pipe)技巧)
[3. 优雅的信号处理模式](#3. 优雅的信号处理模式)
[4. 子进程信号处理](#4. 子进程信号处理)
[5. 信号安全日志记录](#5. 信号安全日志记录)
前言
信号是Linux/Unix系统中的异步事件通知机制,用于进程间通信和异常处理。
本文系统介绍了信号的基本概念、常见信号类型(如SIGINT、SIGKILL、SIGTERM等)、信号产生方式(终端按键、硬件异常、系统调用等)以及信号处理方法(signal/sigaction)。
重点阐述了信号阻塞、实时信号特性,并提供了信号处理的最佳实践,包括安全函数使用、自管道技巧和子进程处理方案。
文章还包含常用命令参考,可帮助开发者们正确使用信号机制进行进程管理和异常处理。
一、什么是信号?
信号(Signal) 是 Linux/Unix 系统中一种异步事件通知机制 ,用于通知进程发生了某个特定事件。信号是软件中断的一种形式,它提供了一种处理异步事件的方法。
1.信号的特点
- 异步性:信号可以在任何时候发送给进程,进程无法预知信号何时到达
- 简单性:信号携带的信息量很少,仅表示某种事件的发生
- 优先级:信号的处理会打断进程的正常执行流程
- 可处理性:进程可以选择捕获、忽略或执行默认操作
二、信号的基本概念
1.信号的生命周期
信号产生 → 信号注册 → 信号处理
2.信号编号与名称
每个信号都有一个唯一的整数编号(1-31 为标准信号,34-64 为实时信号)和一个符号名称(如 SIGINT、SIGKILL)。
3.信号的来源
| 来源类型 | 说明 | 示例 |
|---|---|---|
| 终端输入 | 用户通过键盘发送 | Ctrl+C, Ctrl+\ |
| 硬件异常 | 硬件错误产生 | 除零错误、非法内存访问 |
| 系统调用 | 通过函数发送 | kill(), raise(), alarm() |
| 软件条件 | 特定软件条件 | 定时器到期、管道断开 |
三、常见信号列表
1.标准信号(1-31)
| 信号 | 编号 | 默认动作 | 说明 |
|---|---|---|---|
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 | 暂停 | 后台进程写终端 |
2.重要信号详解
(1)SIGINT (2) - 中断信号
# 按 Ctrl+C 发送
# 默认行为:终止进程
# 常用于:优雅地终止前台进程
(2)SIGKILL (9) - 强制终止
kill -9 <pid>
# 特点:
# - 无法被捕获、阻塞或忽略
# - 立即终止进程
# - 进程无法进行清理操作
# 建议:仅在无法正常终止时使用
(3)SIGTERM (15) - 正常终止
kill <pid> # 默认发送 SIGTERM
kill -15 <pid>
# 特点:
# - 可以被捕获和处理
# - 允许进程进行清理操作
# - 推荐的首选终止方式
(4)SIGSEGV (11) - 段错误
// 常见原因:
// - 访问未分配的内存
// - 访问已释放的内存
// - 数组越界访问
// - 解引用空指针
int *p = NULL;
*p = 10; // 触发 SIGSEGV
(5)SIGCHLD (17) - 子进程状态改变
// 子进程终止或停止时发送给父进程
// 默认行为:忽略
// 常用于:防止僵尸进程
四、信号的产生方式
1. 终端按键产生
bash
Ctrl+C → SIGINT (中断)
Ctrl+\ → SIGQUIT (退出)
Ctrl+Z → SIGTSTP (暂停)
2. 硬件异常产生
bash
// 除零错误 → SIGFPE
int a = 10 / 0;
// 非法内存访问 → SIGSEGV
int *p = (int *)0x12345;
*p = 100;
3. 系统调用产生
(1)kill() 函数
bash
#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid, int sig);
// 功能:向指定进程发送信号
// 参数:
// pid > 0: 发送给指定进程
// pid = 0: 发送给同组所有进程
// pid = -1: 发送给所有有权限的进程
// pid < -1: 发送给进程组 |pid| 中的所有进程
// 返回值:成功返回0,失败返回-1
// 示例
kill(1234, SIGTERM); // 向PID为1234的进程发送SIGTERM
kill(0, SIGUSR1); // 向同组所有进程发送SIGUSR1
(2)raise() 函数
bash
#include <signal.h>
int raise(int sig);
// 功能:向当前进程自身发送信号
// 等价于:kill(getpid(), sig)
// 示例
raise(SIGTERM); // 自我终止
(3)abort() 函数
bash
#include <stdlib.h>
void abort(void);
// 功能:异常终止程序,产生 SIGABRT 信号
// 特点:会生成核心转储文件(如果允许)
4. 软件条件产生
(1)alarm() 函数
bash
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 功能:设置定时器,seconds秒后发送 SIGALRM 信号
// 返回值:返回上次定时器剩余时间,无上次定时器返回0
// 注意:每个进程只能有一个alarm定时器
// 示例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void alarm_handler(int sig) {
printf("定时器到期!\n");
}
int main() {
signal(SIGALRM, alarm_handler);
alarm(5); // 5秒后发送SIGALRM
printf("等待定时器...\n");
pause(); // 等待信号
return 0;
}
(2)setitimer() 函数
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
// 功能:设置高精度定时器
// which参数:
// ITIMER_REAL: 真实时间,发送 SIGALRM
// ITIMER_VIRTUAL: 用户态CPU时间,发送 SIGVTALRM
// ITIMER_PROF: 用户态+内核态CPU时间,发送 SIGPROF
struct itimerval {
struct timeval it_interval; // 间隔时间(周期性定时)
struct timeval it_value; // 首次到期时间
};
struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};
五、信号的处理方式
1.三种处理方式
- 默认动作(Default):执行系统预设的操作
- 忽略信号(Ignore):丢弃信号,不执行任何操作
- 捕获信号(Catch):执行自定义的信号处理函数
2.signal() 函数(传统方式)
bash
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// 参数:
// signum: 信号编号
// handler: 处理方式
// SIG_DFL: 默认处理
// SIG_IGN: 忽略信号
// 自定义函数指针: 捕获信号
// 示例代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 自定义信号处理函数
void sigint_handler(int sig) {
printf("\n捕获到 SIGINT 信号(编号:%d)\n", sig);
printf("正在执行清理工作...\n");
// 执行清理操作
_exit(0); // 安全退出
}
int main() {
// 设置 SIGINT 的处理方式
if (signal(SIGINT, sigint_handler) == SIG_ERR) {
perror("signal");
return 1;
}
printf("按 Ctrl+C 测试信号处理(PID: %d)\n", getpid());
while (1) {
printf("工作中...\n");
sleep(2);
}
return 0;
}
3.sigaction() 函数(推荐方式)
bash
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
// 功能:更强大的信号处理设置函数
// 参数:
// signum: 信号编号
// act: 新的处理方式(NULL表示不设置)
// oldact: 保存旧的处理方式(NULL表示不保存)
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 高级处理函数
sigset_t sa_mask; // 信号屏蔽字
int sa_flags; // 标志位
void (*sa_restorer)(void); // 已废弃
};
// sa_flags 常用标志
#define SA_RESTART // 自动重启被中断的系统调用
#define SA_NODEFER // 不自动阻塞该信号
#define SA_RESETHAND // 执行后恢复默认处理
#define SA_SIGINFO // 使用 sa_sigaction 处理函数
(1)sigaction 使用示例
bash
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
void sigint_handler(int sig) {
write(STDOUT_FILENO, "\n收到 SIGINT,优雅退出\n", 25);
_exit(0);
}
int main() {
struct sigaction sa;
// 清空结构体
memset(&sa, 0, sizeof(sa));
// 设置处理函数
sa.sa_handler = sigint_handler;
// 设置标志:自动重启被中断的系统调用
sa.sa_flags = SA_RESTART;
// 清空信号屏蔽字
sigemptyset(&sa.sa_mask);
// 安装信号处理器
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("按 Ctrl+C 退出(PID: %d)\n", getpid());
while (1) {
printf("运行中...\n");
sleep(2);
}
return 0;
}
(2)使用 SA_SIGINFO 获取详细信息
bash
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void advanced_handler(int sig, siginfo_t *info, void *context) {
printf("\n=== 信号详细信息 ===\n");
printf("信号编号: %d\n", sig);
printf("发送进程PID: %d\n", info->si_pid);
printf("发送进程UID: %d\n", info->si_uid);
printf("信号值: %d\n", info->si_value.sival_int);
printf("==================\n");
}
int main() {
struct sigaction sa;
sa.sa_sigaction = advanced_handler;
sa.sa_flags = SA_SIGINFO; // 使用三参数处理函数
sigemptyset(&sa.sa_mask);
sigaction(SIGUSR1, &sa, NULL);
printf("等待 SIGUSR1 信号(PID: %d)\n", getpid());
while (1) {
pause();
}
return 0;
}
六、信号集操作
1.信号集类型
bash
#include <signal.h>
typedef struct {
unsigned long sig[(64 + 8 * sizeof(unsigned long) - 1) /
(8 * sizeof(unsigned long))];
} sigset_t;
// 用于表示一组信号的位图结构
2.信号集操作函数
bash
#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);
3.信号集操作示例
bash
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t set;
// 清空信号集
sigemptyset(&set);
// 添加 SIGINT 和 SIGTERM
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
// 检查 SIGINT 是否在集合中
if (sigismember(&set, SIGINT)) {
printf("SIGINT 在信号集中\n");
}
// 检查 SIGKILL 是否在集合中
if (!sigismember(&set, SIGKILL)) {
printf("SIGKILL 不在信号集中\n");
}
// 删除 SIGINT
sigdelset(&set, SIGINT);
// 填充所有信号
sigfillset(&set);
printf("信号集已填充所有信号\n");
return 0;
}
七、信号的阻塞与未决
1.基本概念
- 信号阻塞(Blocked) :信号被阻塞后,不会立即递达,而是保持未决状态
- 信号未决(Pending):信号已产生但尚未被处理的状态
- 信号递达(Delivered):信号被处理的时刻
2.信号阻塞相关函数
bash
#include <signal.h>
// 检查或修改进程的信号屏蔽字
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// how参数:
// SIG_BLOCK: 将set中的信号添加到屏蔽字
// SIG_UNBLOCK: 从屏蔽字中移除set中的信号
// SIG_SETMASK: 将屏蔽字设置为set
// 获取当前未决信号集
int sigpending(sigset_t *set);
3.信号阻塞示例
bash
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig) {
printf("处理信号: %d\n", sig);
}
int main() {
sigset_t block_set, pending_set;
// 设置 SIGINT 处理函数
signal(SIGINT, handler);
// 创建要阻塞的信号集
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
printf("阻塞 SIGINT 5秒,请在这5秒内按 Ctrl+C\n");
// 阻塞 SIGINT
sigprocmask(SIG_BLOCK, &block_set, NULL);
sleep(5);
// 检查未决信号
sigpending(&pending_set);
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT 处于未决状态\n");
}
printf("解除阻塞,信号将被处理\n");
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &block_set, NULL);
sleep(1);
printf("程序结束\n");
return 0;
}
4.信号处理流程图
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 信号产生 │ ──→ │ 信号未决 │ ──→ │ 信号递达 │
│ (Generated)│ │ (Pending) │ │ (Delivered) │
└─────────────┘ └─────────────┘ └─────────────┘
↑
│ 信号被阻塞
┌─────────────┐
│ 信号阻塞 │
│ (Blocked) │
└─────────────┘
八、实时信号
1.实时信号特点
| 特性 | 标准信号 | 实时信号 |
|---|---|---|
| 编号范围 | 1-31 | 34-64 |
| 排队 | 不支持(相同信号只保留一个) | 支持(多个相同信号排队) |
| 携带数据 | 不能 | 可以(通过 sigqueue) |
| 优先级 | 无 | 编号小的优先 |
| 用途 | 系统预定义 | 用户自定义 |
2.实时信号操作
bash
#include <signal.h>
#include <unistd.h>
// 发送带数据的实时信号
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
int sival_int; // 整型数据
void *sival_ptr; // 指针数据(仅同一进程内有效)
};
4.实时信号示例
bash
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void rt_handler(int sig, siginfo_t *info, void *context) {
printf("收到实时信号 %d,数据: %d\n", sig, info->si_value.sival_int);
}
int main() {
struct sigaction sa;
pid_t pid = getpid();
sa.sa_sigaction = rt_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
// 注册实时信号处理函数
sigaction(SIGRTMIN, &sa, NULL);
printf("进程 PID: %d\n", pid);
printf("向自己发送3个实时信号...\n");
// 发送带数据的实时信号
union sigval value;
for (int i = 1; i <= 3; i++) {
value.sival_int = i * 100;
sigqueue(pid, SIGRTMIN, value);
}
sleep(1);
return 0;
}
九、信号处理的最佳实践
1. 信号处理函数的安全要求
信号处理函数中只能调用异步信号安全函数:
bash
// ✅ 安全的函数
write(), _exit(), kill(), sigaction()
getpid(), getppid(), sleep()
// ❌ 不安全的函数(避免使用)
printf(), malloc(), free(), exit()
fopen(), fread(), fwrite()
pthread_mutex_lock(), 大部分库函数
2. 使用自管道(Self-Pipe)技巧
bash
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
static int pipefd[2];
void signal_handler(int sig) {
// 只写入一个字节到管道
write(pipefd[1], &sig, 1);
}
int main() {
fd_set readfds;
char sig;
// 创建管道
pipe(pipefd);
fcntl(pipefd[0], F_SETFL, O_NONBLOCK);
fcntl(pipefd[1], F_SETFL, O_NONBLOCK);
// 设置信号处理
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
printf("使用 select 等待信号(PID: %d)\n", getpid());
while (1) {
FD_ZERO(&readfds);
FD_SET(pipefd[0], &readfds);
select(pipefd[0] + 1, &readfds, NULL, NULL, NULL);
if (FD_ISSET(pipefd[0], &readfds)) {
read(pipefd[0], &sig, 1);
printf("主循环中处理信号: %d\n", (int)sig);
if (sig == SIGINT || sig == SIGTERM) {
printf("退出程序\n");
break;
}
}
}
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
3. 优雅的信号处理模式
bash
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdatomic.h>
// 使用原子变量作为信号标志
static atomic_int signal_received = 0;
void signal_handler(int sig) {
// 只设置标志,不做复杂操作
atomic_store(&signal_received, sig);
}
void process_work() {
// 模拟工作
static int counter = 0;
printf("工作进行中... %d\n", ++counter);
sleep(1);
}
void cleanup() {
printf("执行清理操作...\n");
// 关闭文件、释放资源等
}
int main() {
struct sigaction sa;
sa.sa_handler = signal_handler;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
printf("程序运行中(PID: %d)\n", getpid());
while (1) {
process_work();
// 在主循环中检查信号
int sig = atomic_load(&signal_received);
if (sig != 0) {
printf("\n收到信号 %d,准备退出\n", sig);
cleanup();
break;
}
}
return 0;
}
4. 子进程信号处理
bash
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
void sigchld_handler(int sig) {
int status;
pid_t pid;
// 使用 WNOHANG 非阻塞等待所有子进程
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("子进程 %d 已退出\n", pid);
}
}
int main() {
struct sigaction sa;
// 设置 SIGCHLD 处理函数,防止僵尸进程
sa.sa_handler = sigchld_handler;
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
// 创建子进程
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("子进程 %d 启动\n", getpid());
sleep(2 + i);
printf("子进程 %d 退出\n", getpid());
_exit(0);
}
}
printf("父进程等待子进程...\n");
// 父进程继续工作
for (int i = 0; i < 10; i++) {
printf("父进程工作中...\n");
sleep(1);
}
return 0;
}
5. 信号安全日志记录
bash
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
// 信号安全的日志函数
void safe_log(const char *msg) {
// 使用 write 而不是 printf
write(STDOUT_FILENO, msg, strlen(msg));
write(STDOUT_FILENO, "\n", 1);
}
void handler(int sig) {
const char *msg;
switch (sig) {
case SIGINT: msg = "收到 SIGINT"; break;
case SIGTERM: msg = "收到 SIGTERM"; break;
case SIGALRM: msg = "收到 SIGALRM"; break;
default: msg = "收到未知信号"; break;
}
safe_log(msg);
}
int main() {
signal(SIGINT, handler);
signal(SIGTERM, handler);
signal(SIGALRM, handler);
alarm(3);
safe_log("程序启动,3秒后收到SIGALRM");
while (1) {
pause();
}
return 0;
}
九、常用命令参考
1.发送信号
bash
# 发送 SIGTERM(默认)
kill <pid>
# 发送特定信号
kill -SIGINT <pid>
kill -2 <pid> # 数字形式
kill -9 <pid> # 强制终止
# 向进程组发送信号
kill -TERM -<pgid>
# 使用 pkill 按名称发送
pkill -INT process_name
pkill -f "pattern"
# 使用 killall
killall -TERM process_name
2.查看信号信息
bash
# 列出所有信号
kill -l
# 查看信号编号对应的名称
kill -l 9 # 输出: KILL
kill -l 15 # 输出: TERM
# 查看信号帮助
man 7 signal
man sigaction
3.信号调试
bash
# 使用 strace 跟踪信号
strace -e trace=signal ./program
# 查看进程挂起的信号
cat /proc/<pid>/status | grep Sig
# SigPnd: 线程组共享的未决信号
# SigBlk: 阻塞的信号
# SigIgn: 忽略的信号
# SigCgt: 捕获的信号
十、总结
| 要点 | 说明 |
|---|---|
| 信号本质 | 软件中断,异步事件通知机制 |
| 可靠信号 | SIGKILL (9) 和 SIGSTOP (19) 不可捕获/忽略 |
| 推荐终止 | 优先使用 SIGTERM (15),允许进程清理 |
| 推荐API | 使用 sigaction() 替代 signal() |
| 处理安全 | 信号处理器中只使用异步信号安全函数 |
| 实时信号 | 支持排队和携带数据,用于应用自定义 |