Linux信号四要素详解:从理论到实践
- 一、信号的基本概念
- 二、信号的四大核心要素
-
-
- 信号编号(Signal Number)
-
- 常见信号分类:
-
- 信号产生(Signal Generation)
-
- 硬件异常产生
- 终端相关信号
- 软件条件触发
- 命令发送
- 系统调用发送
-
- 信号处理(Signal Handling)
-
- 三种默认处理方式
- 信号处理函数注册
- sigaction结构体详解
-
- 信号屏蔽(Signal Masking)
-
- 相关系统调用
- 操作类型(how):
- 信号集操作函数
-
- 三、信号处理的高级话题
-
-
- 可重入函数与异步信号安全
-
- 信号处理与多线程
-
- 实时信号与信号队列
-
- 四、最佳实践与常见问题
-
-
- 信号处理的最佳实践
-
- 常见问题与解决方案
-
- 五、实际应用示例
-
-
- 优雅地终止进程
-
- 父子进程信号同步
-
- 六、总结
一、信号的基本概念
在Linux系统中,信号(Signal)是进程间通信的一种基本机制,用于通知进程发生了某种事件或异常情况。信号可以被看作是一种软件中断,它打断了进程的正常执行流程,迫使进程去处理这个事件。
信号的主要特点包括:
- 异步性:信号可以在任何时候发送给进程
- 简单性:信号携带的信息量有限(只有一个编号)
- 优先级:某些信号会优先于其他信号被处理
二、信号的四大核心要素
1. 信号编号(Signal Number)
每个信号都有一个唯一的整数编号,通常用符号常量表示(如SIGINT、SIGTERM等)。
常见信号分类:
标准信号(1-31):
- SIGINT (2):终端中断信号(Ctrl+C)
- SIGQUIT (3):终端退出信号(Ctrl+)
- SIGKILL (9):强制终止信号(不可捕获或忽略)
- SIGTERM (15):终止信号(可被捕获)
- SIGSEGV (11):段错误信号
- SIGALRM (14):定时器信号
实时信号(34-64):
- SIGRTMIN 到 SIGRTMAX
- 支持排队,不会丢失
- 携带附加信息
查看所有信号:
bash
kill -l
2. 信号产生(Signal Generation)
信号可以由多种方式产生:
硬件异常产生
- 除零错误(SIGFPE)
- 非法内存访问(SIGSEGV)
- 总线错误(SIGBUS)
终端相关信号
- Ctrl+C(SIGINT)
- Ctrl+Z(SIGTSTP)
- Ctrl+\(SIGQUIT)
软件条件触发
- 定时器到期(SIGALRM)
- 子进程终止(SIGCHLD)
- 管道破裂(SIGPIPE)
命令发送
bash
kill -SIGTERM pid
killall -9 process_name
系统调用发送
c
#include <signal.h>
int kill(pid_t pid, int sig);
int raise(int sig); // 向自身发送信号
3. 信号处理(Signal Handling)
进程可以自定义信号的处理方式:
三种默认处理方式
- 忽略(IGN) :SIG_IGN
- 但SIGKILL和SIGSTOP不能被忽略
- 默认(DFL) :SIG_DFL
- 通常是终止进程
- 捕获(Catch) :自定义处理函数
信号处理函数注册
c
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction结构体详解
c
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_RESTART:被信号中断的系统调用自动重启
- SA_SIGINFO:使用sa_sigaction而非sa_handler
- SA_NOCLDSTOP:子进程停止时不产生SIGCHLD
- SA_NODEFER:不自动阻塞当前信号
4. 信号屏蔽(Signal Masking)
进程可以阻塞(屏蔽)某些信号,使其暂时不被处理:
相关系统调用
c
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set); // 获取被挂起的信号集
int sigsuspend(const sigset_t *mask); // 临时替换信号掩码并挂起进程
操作类型(how):
- SIG_BLOCK:将set中的信号添加到阻塞集合
- SIG_UNBLOCK:从阻塞集合中移除set中的信号
- SIG_SETMASK:将阻塞集合设置为set
信号集操作函数
c
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); // 测试信号是否在集合中
三、信号处理的高级话题
1. 可重入函数与异步信号安全
在信号处理函数中只能调用异步信号安全的函数(如write、kill等),因为:
- 信号可能在任何时候中断主程序
- 使用不可重入函数可能导致死锁或数据损坏
常见不安全函数:
- malloc/free
- printf/scanf
- 大部分标准I/O函数
2. 信号处理与多线程
在多线程环境中:
- 信号处理是进程范围内共享的
- 每个线程有自己的信号掩码
- 信号可以发送给特定线程(pthread_kill)
c
#include <pthread.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
int pthread_kill(pthread_t thread, int sig);
3. 实时信号与信号队列
实时信号(SIGRTMIN-SIGRTMAX)的特点:
- 支持排队,不会丢失
- 按顺序传递
- 可以携带附加信息(通过siginfo_t)
c
union sigval {
int sival_int;
void *sival_ptr;
};
int sigqueue(pid_t pid, int sig, const union sigval value);
四、最佳实践与常见问题
1. 信号处理的最佳实践
- 保持处理函数简单:只做最基本的操作,如设置标志位
- 使用sigaction而非signal:signal在不同Unix系统行为不一致
- 正确处理EINTR:系统调用可能被信号中断
- 避免竞态条件:使用sigprocmask保护关键代码段
2. 常见问题与解决方案
问题1:信号处理函数中调用不可重入函数
c
// 错误示例
void handler(int sig) {
printf("Received signal %d\n", sig); // 不安全!
}
// 正确做法
volatile sig_atomic_t flag = 0;
void handler(int sig) {
flag = 1; // 只设置标志位
}
问题2:信号丢失
c
// 使用sigaction并设置SA_SIGINFO
struct sigaction sa;
sa.sa_sigaction = handler; // 使用三参数版本
sa.sa_flags = SA_SIGINFO;
sigaction(SIGRTMIN, &sa, NULL);
问题3:系统调用被中断
c
// 手动重启被中断的系统调用
while ((n = read(fd, buf, size)) == -1 && errno == EINTR)
continue;
五、实际应用示例
1. 优雅地终止进程
c
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
volatile sig_atomic_t shutdown_flag = 0;
void handle_shutdown(int sig) {
shutdown_flag = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = handle_shutdown;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
while (!shutdown_flag) {
printf("Working...\n");
sleep(1);
}
printf("Cleaning up...\n");
// 执行清理操作
printf("Exiting gracefully\n");
return 0;
}
2. 父子进程信号同步
c
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void child_handler(int sig) {
int status;
pid_t pid = wait(&status);
printf("Child %d exited with status %d\n", pid, WEXITSTATUS(status));
}
int main() {
struct sigaction sa;
sa.sa_handler = child_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
pid_t pid = fork();
if (pid == 0) {
// 子进程
sleep(2);
exit(42);
} else {
// 父进程
printf("Parent waiting...\n");
pause(); // 等待信号
}
return 0;
}
六、总结
Linux信号机制是系统编程中的重要组成部分,理解信号的四大要素(编号、产生、处理、屏蔽)是掌握信号编程的基础。在实际开发中:
- 优先使用sigaction而非signal
- 注意信号处理函数的可重入性
- 正确处理被中断的系统调用
- 在多线程环境中谨慎使用信号
- 考虑使用实时信号避免信号丢失
通过合理使用信号机制,可以构建更健壮、响应更快的Linux应用程序。