1. Linux进程信号基础概念
1.1 信号的本质与生活类比
信号在Linux系统中相当于快递通知的电子版。想象你正在等待多个快递包裹:
- 识别信号:就像你知道如何处理不同快递(顺丰、京东等),进程内置了识别各种信号的能力
- 异步通知:快递到达时间不确定,信号也可能在任何时刻到达
- 延迟处理:正在打游戏时,你会等合适时机取快递,进程也可能因执行更重要任务而延迟处理信号
- 处理方式:
- 默认动作(像拆开普通包裹使用商品)
- 自定义动作(把零食转送给女友)
- 忽略信号(直接扔掉包裹)
关键理解:信号处理方案在信号到达前就已确定,这就像你在下单时就想好了如何处理不同商品
1.2 信号的技术实现机制
当你在终端按下Ctrl+C时:
- 硬件中断触发 → 内核捕获 → 转换为SIGINT信号(2号)
- 内核将信号传递给前台进程
- 进程根据预设方案处理信号(默认终止)
cpp复制#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signo) {
std::cout << "进程" << getpid() << "捕获信号:" << signo << std::endl;
}
int main() {
signal(SIGINT, handler); // 注册信号处理器
while(true) {
std::cout << "运行中..." << std::endl;
sleep(1);
}
}
这段代码展示了:
signal()函数注册信号处理方式- 信号到来时不会立即终止进程
- 处理函数与主程序异步执行
2. 信号的产生方式
2.1 终端按键触发信号
| 快捷键 | 信号类型 | 默认行为 |
|---|---|---|
| Ctrl+C | SIGINT(2) | 终止进程 |
| Ctrl+\ | SIGQUIT(3) | 终止并core dump |
| Ctrl+Z | SIGTSTP(20) | 暂停进程 |
核心原理:
- 键盘输入产生硬件中断
- OS解释为特定信号
- 仅发送给前台进程(后台进程需用kill命令)
2.2 系统命令发送信号
bash复制kill -SIGSEGV 1234 # 发送段错误信号
kill -9 1234 # 强制终止(SIGKILL)
特殊现象:SIGKILL(9)和SIGSTOP(19)不能被捕获或忽略,是系统保留的终极控制手段
2.3 程序函数产生信号
cpp复制// 给指定进程发信号
kill(pid, SIGUSR1);
// 给自己发信号
raise(SIGTERM);
// 强制异常终止
abort(); // 产生SIGABRT
2.4 硬件异常信号
典型场景:
- 除零操作:触发SIGFPE(8)
- 野指针访问:触发SIGSEGV(11)
cpp复制int *p = NULL;
*p = 100; // 立即触发段错误
底层机制:
- CPU检测到异常(如MMU内存访问错误)
- 设置状态寄存器标志位
- 内核检查寄存器并发送对应信号
- 除非异常被处理,否则信号会持续产生
2.5 定时器信号
cpp复制alarm(5); // 5秒后发送SIGALRM
性能测试案例:
cpp复制int count = 0;
void handler(int) {
std::cout << "计数:" << count << std::endl;
exit(0);
}
int main() {
signal(SIGALRM, handler);
alarm(1); // 1秒计时
while(1) count++; // 无IO操作
}
对比有IO输出的版本,性能差异可达1000倍,说明:
- 系统调用(如printf)消耗大量CPU时间
- 纯计算任务能最大化CPU利用率
3. 信号的保存与阻塞
3.1 信号状态模型
| 状态 | 说明 |
|---|---|
| 产生(Pending) | 信号已生成但未递达 |
| 阻塞(Block) | 进程主动屏蔽该信号 |
| 递达(Delivery) | 信号被实际处理 |
内核数据结构:
c复制struct task_struct {
// 信号位图
sigset_t blocked; // 阻塞信号集
sigset_t pending; // 未决信号集
struct sigaction sa[32]; // 处理动作
};
3.2 信号集操作函数
cpp复制sigset_t set;
sigemptyset(&set); // 初始化空集
sigaddset(&set, SIGINT); // 添加信号
sigprocmask(SIG_BLOCK, &set, NULL); // 设置阻塞
// 检查未决信号
sigpending(&set);
if(sigismember(&set, SIGINT)) {
cout << "SIGINT被阻塞中" << endl;
}
重要特性:常规信号不排队,多次发送只记一次(实时信号支持排队)
4. 信号捕捉机制
4.1 信号处理流程
- 用户注册处理函数(通过signal/sigaction)
- 信号触发时,内核临时构建处理栈帧
- 切换到用户态执行处理函数
- 通过sigreturn系统调用恢复原上下文
cpp复制struct sigaction act;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, NULL);
4.2 关键注意事项
-
处理函数重入风险:
- 避免在信号处理中使用不可重入函数(如malloc)
- 静态变量可能引发竞态条件
-
自动阻塞机制:
- 执行处理函数时,同种信号会被自动阻塞
- 可通过sa_mask添加额外阻塞信号
-
volatile关键字:
cpp复制volatile sig_atomic_t flag; // 防止编译器优化确保信号处理函数修改的变量对主程序可见
5. 信号与系统调用的关系
5.1 中断处理体系
| 中断类型 | 触发方式 | 示例 |
|---|---|---|
| 硬件中断 | 外部设备 | 键盘输入 |
| 时钟中断 | 定时器 | 进程调度 |
| 软中断 | 特殊指令 | 系统调用 |
| 异常 | CPU错误 | 除零操作 |
操作系统运行本质:
c复制void kernel_main() {
while(1) {
if(中断发生) {
保存上下文;
调用中断处理程序;
恢复上下文;
}
}
}
5.2 从信号看CPU特权级
- 用户态:执行用户代码(CPL=3)
- 内核态:执行系统调用(CPL=0)
- 信号处理:
- 用户态被中断
- 进入内核态判断信号
- 返回用户态执行处理函数
- 再次陷入内核恢复现场
现代Linux使用vsyscall机制优化频繁系统调用
6. 高级信号处理技巧
6.1 SIGCHLD与僵尸进程
cpp复制void child_handler(int sig) {
while(waitpid(-1, NULL, WNOHANG) > 0); // 非阻塞回收
}
int main() {
signal(SIGCHLD, child_handler);
if(fork() == 0) exit(0); // 子进程立即退出
while(1) pause(); // 父进程不阻塞
}
两种避免僵尸进程的方案:
- 捕获SIGCHLD并调用wait
- 直接忽略SIGCHLD(Linux特有):
cpp复制signal(SIGCHLD, SIG_IGN); // 自动回收子进程
6.2 信号驱动IO
cpp复制struct sigaction act;
act.sa_handler = io_handler;
act.sa_flags = SA_RESTART | SA_SIGINFO;
sigaction(SIGIO, &act, NULL);
fcntl(fd, F_SETOWN, getpid());
fcntl(fd, F_SETFL, O_ASYNC);
当文件描述符就绪时,内核发送SIGIO信号通知进程
6.3 实时信号处理
相比常规信号(1-31),实时信号(34-64)特点:
- 支持排队不丢失
- 携带附加信息
- 优先级顺序处理
cpp复制union sigval value;
value.sival_int = 123;
sigqueue(pid, SIGRTMIN, value); // 发送实时信号
7. 信号应用实战案例
7.1 优雅的服务端重启
cpp复制void reload_config(int) {
read_config(); // 重载配置
signal(SIGHUP, reload_config); // 重新注册
}
int main() {
signal(SIGHUP, reload_config);
start_server();
}
通过kill -HUP通知服务端重载配置而不中断服务
7.2 多线程信号处理
黄金法则:
- 每个线程可以独立设置信号掩码
- 信号处理是进程级别的,由任意线程执行
- 推荐专门线程处理信号:
cpp复制void* signal_thread(void*) {
sigset_t set;
sigfillset(&set);
pthread_sigmask(SIG_BLOCK, &set, NULL);
int sig;
while(1) {
sigwait(&set, &sig); // 同步等待信号
handle_signal(sig);
}
}
7.3 定时任务实现
cpp复制void timer_handler(int) {
// 执行定时任务
alarm(60); // 重新设置
}
int main() {
signal(SIGALRM, timer_handler);
alarm(60); // 60秒触发
while(1) {
// 主业务逻辑
}
}
对比sleep的优势:不阻塞进程执行
8. 信号编程的常见陷阱
-
不可重入函数危险:
- printf/malloc等标准库函数非信号安全
- 推荐仅设置volatile标志位
-
竞态条件:
cpp复制if(!flag) { // 判断 // 此处可能被信号中断 pause(); // 等待信号 }应使用
sigsuspend原子操作 -
信号丢失:
- 常规信号多次发送只保留一次
- 关键场景应使用实时信号
-
死锁风险:
- 信号处理函数中加锁可能导致死锁
- 使用pthread_sigmask控制信号时机
最佳实践:信号处理应尽量简单,仅通过标志位与主程序通信