Linux 信号产生
Linux 信号(Signal)的简单定义
信号(Signal)是 Linux 系统用于通知进程发生某种事件的机制,它是一种异步通信方式,用于进程间通信(IPC)或由内核向进程发送通知。
- 信号就像是一个通知,告诉进程:“有事情发生了,你要处理一下!”
- 例如:
- 用户按下
Ctrl+C(发送SIGINT信号)终止进程。 - 系统强制杀死进程(
SIGKILL)。 - 进程出错(如段错误
SIGSEGV)。
- 用户按下
信号是 Linux 管理进程的重要机制,用于控制进程行为(终止、暂停、恢复等)。
Linux 信号分类:普通信号 vs 实时信号
1. 普通信号(Standard Signals,1~31)
- 范围:
1~31(如SIGINT(2)、SIGKILL(9)、SIGTERM(15))。 - 特点:
- 不排队:如果同一个信号连续发送多次,进程可能只收到一次。
- 无优先级:多个信号到达时,处理顺序不确定。
- 部分可被阻塞或捕获(如
SIGINT可捕获,SIGKILL不可捕获)。
- 典型用途:进程控制(终止、暂停、错误处理等)。
2. 实时信号(Real-Time Signals,34~64)
- 范围:
34(SIGRTMIN) ~64(SIGRTMAX)。 - 特点:
- 支持排队:连续发送多次,进程会按顺序处理所有信号。
- 有优先级:数值小的信号优先处理(如
34比35优先)。 - 可携带额外数据(通过
sigqueue()发送)。
- 典型用途:高可靠性通信(如多线程同步、自定义事件通知)。
区别总结
| 特性 | 普通信号 | 实时信号 |
|---|---|---|
| 编号范围 | 1~31 | 34~64 |
| 排队机制 | 不排队 | 排队(保证不丢失) |
| 优先级 | 无 | 数值小的优先 |
| 数据传递 | 不支持 | 支持(sigqueue) |
| 典型用途 | 进程控制 | 实时应用、线程通信 |
- 普通信号:适用于简单进程控制,但可能丢失。
- 实时信号:适用于高可靠性场景,支持排队和数据传递。
因为这里主要讲解的是进程控制,所以后面我们主要介绍的是普通信号。
Linux 信号处理方式
在 Linux 中,进程收到信号后,可以采取以下 3 种处理方式:
1. 执行默认动作(Default Action)
每个信号都有系统预定义的默认行为,常见的有:
- 终止进程(Terminate):
SIGTERM(15)、SIGKILL(9)(不可捕获)。 - 终止并生成核心转储(Core Dump):
SIGSEGV(11)(段错误)。 - 暂停进程(Stop):
SIGSTOP(19)(不可捕获)。 - 忽略(Ignore):
SIGCHLD(17)(子进程退出时默认忽略)。
示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main() {
while (1) {
cout << "My pid is: " << getpid() << endl;
sleep(1);
}
return 0;
}
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal.o
My pid is: 274247
My pid is: 274247
^C # Ctrl + C 发送 SIGINT 信号,进程终止
2. 忽略信号(Ignore Signal)
进程可以主动忽略某些信号(但部分信号如 SIGKILL 和 SIGSTOP 无法被忽略)。
为什么 SIGKILL 和 SIGSTOP 无法被忽略?
因为 SIGKILL 和 SIGSTOP 是用于强制终止和暂停进程的信号,如果允许进程忽略它们,就无法保证进程能够被正常终止或暂停,这可能会导致系统不稳定。
我们可以在 C 程序中使用 signal(SIGXXX, SIG_IGN) 忽略信号。
不可忽略的信号:
SIGKILL(9):强制终止。SIGSTOP(19):强制暂停。
示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main() {
// 忽略 SIGINT 信号
signal(SIGINT, SIG_IGN);
while (1) {
cout << "My pid is: " << getpid() << endl;
sleep(1);
}
return 0;
}
可以看到,使用 Ctrl + C 无法终止进程。
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal.o
My pid is: 273355
^CMy pid is: 273355 # Ctrl + C 无法终止进程
[1] 273355 killed ./signal.o # 通过 kill 命令发送 SIGKILL 信号
3. 自定义处理(捕获信号)
进程可以注册一个信号处理函数,在信号到达时执行自定义逻辑。
我们可以利用 C 语言signal() 或更安全的 sigaction() 来自定义信号处理函数。
不可自定义的信号:
SIGKILL(9)和SIGSTOP(19)无法被捕获或忽略,只能执行默认行为。
在示例讲解之前,我们先学习一个用于自定义信号处理方式的函数:
signal() 函数
函数原型:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:要处理的信号编号。handler:自定义的信号处理函数,类型为void (*)(int)。- 返回值:上一个信号处理函数的指针,用于链式调用。
示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void myhandler(int signal)
{
cout << "I get a signal: " << strsignal(signal) << endl;
// 执行完自定义逻辑后,退出进程
_exit(1);
}
int main() {
signal(SIGINT, myhandler);
while (1) {
cout << "My pid is: " << getpid() << endl;
sleep(1);
}
return 0;
}
总结
| 处理方式 | 适用场景 | 限制 |
|---|---|---|
| 默认动作 | 简单控制(如强制终止、错误处理) | 无 |
| 忽略信号 | 避免无关信号干扰(如 SIGCHLD) |
SIGKILL/SIGSTOP 不可忽略 |
| 自定义处理 | 执行清理、日志记录等自定义逻辑 | SIGKILL/SIGSTOP 不可捕获 |
信号产生方式
键盘发送信号
在 Linux 终端中,键盘快捷键 可以主动向当前前台进程发送特定信号,用于 进程控制(如终止、暂停、恢复等)。以下是常见的键盘信号及其扩展说明:
1. 常用键盘信号
| 快捷键 | 信号名 | 信号编号 | 默认行为 | 典型用途 |
|---|---|---|---|---|
| Ctrl + C | SIGINT |
2 | 终止进程 | 优雅终止当前运行的程序 |
| Ctrl + \ | SIGQUIT |
3 | 终止 + 核心转储 | 强制终止并生成调试文件(core dump) |
| Ctrl + Z | SIGTSTP |
20 | 暂停进程(放入后台) | 暂停当前任务,可用 fg/bg 恢复 |
| Ctrl + D | EOF |
- | 输入结束(非信号) | 结束终端输入或退出 Shell |
| Ctrl + S | - | - | 暂停屏幕输出 | 冻结终端显示(与信号无关) |
| Ctrl + Q | - | - | 恢复屏幕输出 | 解冻终端显示(与信号无关) |
注意:
Ctrl + D不是信号,而是发送 EOF(End-of-File),通常用于结束输入或退出 Shell。Ctrl + S和Ctrl + Q是 终端控制流,与信号无关。
2. 信号行为详解
(1) Ctrl + C(SIGINT)
- 默认行为:终止前台进程。
- 可被捕获:程序可以自定义处理(如清理资源后退出)。
- 示例(捕获
SIGINT):
// C 程序捕获 SIGINT
#include <signal.h>
void handler(int sig) { printf("收到 SIGINT,不退出\n"); }
signal(SIGINT, handler);
(2) Ctrl + \(SIGQUIT)
- 默认行为:终止进程并生成
core dump(需启用ulimit -c unlimited,我将在后面讲解core dump)。 - 用途:调试程序崩溃时的现场保存。
- 不可被忽略:即使捕获,仍会终止进程。
(3) Ctrl + Z(SIGTSTP)
- 默认行为:暂停进程并放入后台(状态变为
T)。 - 恢复方法:
fg # 恢复到前台
bg # 在后台继续运行
jobs # 查看被暂停的任务
需要注意的是,Ctrl + Z 发送的 SIGTSTP 可被捕获,而 SIGSTOP 是强制暂停(不可捕获)。
(4) Ctrl + D(EOF)
- 非信号,而是终端输入的结束符。
- 常见场景:
- 退出 Shell(输入
exit或直接按Ctrl + D)。 - 结束
cat、grep等命令的输入。
- 退出 Shell(输入
命令发送信号
1. kill 命令
该命令用于向进程发送信号,该命令只能向单个进程发送信号。
kill -信号编号 进程号
示例:
kill -9 12345 # 发送 SIGKILL(9) 终止进程 12345
2. killall 命令
该命令用于向所有指定名称的进程发送信号。
killall -信号编号 进程名
示例:
killall -15 firefox # 发送 SIGTERM(15) 终止所有 firefox 进程
函数调用发送信号
1. kill() 函数
该函数用于向单个进程发送信号。
语法如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void myhandler(int signal)
{
cout << "I get a signal: " << strsignal(signal) << endl;
_exit(1);
}
int main() {
signal(SIGINT, myhandler);
int cnt = 3;
while (cnt--) {
cout << "My pid is: " << getpid() << endl;
sleep(1);
}
// 发送 SIGINT 信号给当前进程
kill(getpid(), SIGINT);
return 0;
}
结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal.o
My pid is: 281270
My pid is: 281270
My pid is: 281270
I get a signal: Interrupt # kill 发送 SIGINT 信号
2. raise() 函数
该函数用于向当前进程发送信号,效果和 kill(getpid(), signal) 相同。
语法如下:
#include <signal.h>
int raise(int sig);
示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void myhandler(int signal)
{
cout << "I get a signal: " << strsignal(signal) << endl;
_exit(1);
}
int main() {
signal(SIGINT, myhandler);
int cnt = 3;
while (cnt--) {
cout << "My pid is: " << getpid() << endl;
sleep(1);
}
// 发送 SIGINT 信号给当前进程
raise(SIGINT);
return 0;
}
效果和 kill 函数的示例完全相同:
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal.o
My pid is: 282049
My pid is: 282049
My pid is: 282049
I get a signal: Interrupt
3. abort() 函数
该函数用于向当前进程发送 SIGABRT 信号,用于异常终止程序,效果和 raise(SIGABRT) 或 kill(getpid(), SIGABRT) 相同。
语法如下:
#include <stdlib.h>
void abort(void);
示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void myhandler(int signal)
{
cout << "I get a signal: " << strsignal(signal) << endl;
_exit(1);
}
int main() {
signal(SIGABRT, myhandler);
int cnt = 3;
while (cnt--) {
cout << "My pid is: " << getpid() << endl;
sleep(1);
}
// 发送 SIGABRT 信号给当前进程
abort();
return 0;
}
结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal.o
My pid is: 282738
My pid is: 282738
My pid is: 282738
I get a signal: Aborted
4. alarm() 函数
该函数用于设置定时器,当定时器到期时,向当前进程发送 SIGALRM 信号,而这种信号发送方式和前面几种硬件方式不同,alarm 是软件方式发送的信号。
语法如下:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void myhandler(int signal)
{
cout << "I get a signal: " << strsignal(signal) << endl;
_exit(1);
}
int main() {
signal(SIGALRM, myhandler);
alarm(3);
while (true) {
cout << "My pid is: " << getpid() << endl;
sleep(1);
}
// 设置定时器,3 秒后发送 SIGALRM 信号给当前进程
return 0;
}
结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal.o
My pid is: 284189
My pid is: 284189
My pid is: 284189
I get a signal: Alarm clock
利用闹钟,我们可以做出一些很好玩的机制,下面我们用闹钟做一个循环计数器:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void myhandler(int signal)
{
cout << "I get a signal: " << strsignal(signal) << endl;
alarm(3);
}
int main() {
signal(SIGALRM, myhandler);
// 设置定时器,3 秒后发送 SIGALRM 信号给当前进程
alarm(3);
while (true) {
cout << "My pid is: " << getpid() << endl;
sleep(1);
}
return 0;
}
没错,我们只将自定义函数当中的 _exit(1) 去掉,然后在自定义函数中重新设置定时器,这样就可以实现循环计数器了。
现象如下:
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal.o
My pid is: 284711
My pid is: 284711
My pid is: 284711
I get a signal: Alarm clock
My pid is: 284711
My pid is: 284711
My pid is: 284711
I get a signal: Alarm clock
My pid is: 284711
My pid is: 284711
My pid is: 284711
I get a signal: Alarm clock
异常
我们来实现一个除零异常的场景:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void myhandler(int signal)
{
sleep(1);
cout << "I get a signal: " << strsignal(signal) << endl;
}
int main() {
// 自定义除零异常执行函数
signal(SIGFPE, myhandler);
int a = 10 / 0;
return 0;
}
结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal.o
I get a signal: Floating point exception
I get a signal: Floating point exception
I get a signal: Floating point exception
I get a signal: Floating point exception
... # 该自定义函数会被反复执行
我们再试一试段错误(修改空指针):
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void myhandler(int signal)
{
sleep(1);
cout << "I get a signal: " << strsignal(signal) << endl;
}
int main() {
// 自定义段错误执行函数
signal(SIGSEGV, myhandler);
int *p = nullptr;
*p = 10;
return 0;
}
结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal.o
I get a signal: Segmentation fault
I get a signal: Segmentation fault
I get a signal: Segmentation fault
... # 该自定义函数会被反复执行
为什么会出现这种现象呢?
深入解析:异常信号(如除零、段错误)为何会持续触发
核心结论
当进程因 硬件异常(如除零、段错误)收到信号(如 SIGFPE、SIGSEGV)时,若在自定义处理函数中 不终止进程,操作系统会 反复触发该信号,导致处理函数被无限调用。
根本原因:异常的 硬件状态标志(如 CPU 状态寄存器、MMU 页表异常)未被清除,且随进程上下文保存,操作系统会持续检测并发送信号。
- 硬件异常与信号的关系
(1) 除零错误(SIGFPE)
- 硬件机制:
- CPU 的 ALU(算术逻辑单元) 在执行除法时,若除数为零,会置位状态寄存器的 溢出标志位(如 x86 的
#DE异常)。 - OS 的干预:
- 操作系统通过 中断处理程序 捕获该硬件异常,将其转换为
SIGFPE信号(编号 8)发送给进程。 - 进程上下文 保存了异常的 CPU 状态(包括标志位),因此异常状态会持续存在。
- 操作系统通过 中断处理程序 捕获该硬件异常,将其转换为
- CPU 的 ALU(算术逻辑单元) 在执行除法时,若除数为零,会置位状态寄存器的 溢出标志位(如 x86 的
(2) 段错误(SIGSEGV)
- 硬件机制:
- MMU(内存管理单元) 在访问非法地址(如空指针)时,触发 缺页异常 或 保护错误(如 x86 的
#PF异常)。 - OS 的干预:
- 操作系统检查异常地址合法性,若确认非法,则发送
SIGSEGV信号(编号 11)给进程。 - 页表错误状态会保留在进程的 内存上下文 中。
- 操作系统检查异常地址合法性,若确认非法,则发送
- MMU(内存管理单元) 在访问非法地址(如空指针)时,触发 缺页异常 或 保护错误(如 x86 的
- 为何信号会持续触发?
关键点:异常标志未被清除
- 若信号处理函数 不终止进程,操作系统在 恢复进程执行 时,会重新检测到 未解决的硬件异常状态,再次发送相同信号。
- 类比:
- 像一个人不断踩到钉子,医生(OS)每次包扎(信号处理)后,他仍踩在同一颗钉子上,伤口永远不会愈合。
代码示例(无限循环的陷阱)
#include <signal.h>
#include <stdio.h>
void handler(int sig) {
printf("捕获到信号 %d,但拒绝退出!\n", sig);
// 未终止进程,信号会再次触发
}
int main() {
signal(SIGFPE, handler); // 捕获除零错误
int a = 1 / 0; // 触发 SIGFPE
return 0;
}
输出:
捕获到信号 8,但拒绝退出!
捕获到信号 8,但拒绝退出!
...(无限循环)
- 操作系统的设计哲学
(1) 异常信号的目的
- “死得明白”:通知进程发生了不可恢复的错误(如内存损坏、算法错误)。
- “而非垂死挣扎”:不建议 在信号处理中尝试修复硬件异常(如跳过除零指令),因程序状态已不可信。
(2) 正确做法
- 立即终止进程:在信号处理中调用
_exit()或abort(),避免不可预测行为。 - 记录错误信息:如打印堆栈或写入日志,便于调试。
这样一来,我们就应该理解为什么需要使用 _exit() 而不是 exit() 了。
原因如下:
(1) 避免递归触发信号
若在 SIGSEGV 处理函数中调用 exit,可能会因 缓冲区刷新 或 atexit 钩子 再次访问非法内存,导致 信号递归触发(无限循环)。
(2) 跳过不可靠的清理
- 异常发生时,程序状态可能已 损坏(如堆栈溢出、内存泄漏),此时
exit的清理操作(如关闭文件、调用atexit)可能 失败或引发新问题。 _exit直接通知内核终止进程,跳过所有用户态清理,确保快速退出。
(3) 保持数据一致性
若程序因 段错误 崩溃,部分数据可能处于 不一致状态(如半写入的文件)。_exit 避免进一步写入,减少损坏风险。
(4) 避免僵尸进程_exit 不会执行 exit 的父进程等待逻辑,确保子进程立即变为僵尸,便于父进程清理。
修正后的代码:
void handler(int sig) {
fprintf(stderr, "错误:发生不可恢复异常(信号 %d)!\n", sig);
_exit(1); // 立即终止,避免循环
}
为何部分信号可安全处理?
并非所有信号都关联硬件异常。例如:
SIGINT(Ctrl+C):来自终端的请求,无硬件状态残留,可安全捕获后继续运行。SIGCHLD:子进程状态变化,无持久化硬件状态。
- 硬件异常信号是“死刑判决”,处理函数应仅用于记录遗言,而非自救。
- 持续触发是因 异常状态未被清除,而非操作系统“刁难”。
term 和 core(核心转储) 的区别
我们来看一下下面这个表格:
Signal Standard Action Comment
────────────────────────────────────────────────────────────────────────
SIGABRT P1990 Core Abort signal from abort(3)
SIGALRM P1990 Term Timer signal from alarm(2)
SIGBUS P2001 Core Bus error (bad memory access)
SIGCHLD P1990 Ign Child stopped or terminated
SIGCLD - Ign A synonym for SIGCHLD
SIGCONT P1990 Cont Continue if stopped
SIGEMT - Term Emulator trap
SIGFPE P1990 Core Floating-point exception
SIGHUP P1990 Term Hangup detected on controlling terminal
or death of controlling process
SIGILL P1990 Core Illegal Instruction
SIGINFO - A synonym for SIGPWR
SIGINT P1990 Term Interrupt from keyboard
SIGIO - Term I/O now possible (4.2BSD)
SIGIOT - Core IOT trap. A synonym for SIGABRT
SIGKILL P1990 Term Kill signal
SIGLOST - Term File lock lost (unused)
SIGPIPE P1990 Term Broken pipe: write to pipe with no
readers; see pipe(7)
SIGPOLL P2001 Term Pollable event (Sys V);
synonym for SIGIO
SIGPROF P2001 Term Profiling timer expired
SIGPWR - Term Power failure (System V)
SIGQUIT P1990 Core Quit from keyboard
SIGSEGV P1990 Core Invalid memory reference
SIGSTKFLT - Term Stack fault on coprocessor (unused)
SIGSTOP P1990 Stop Stop process
SIGTSTP P1990 Stop Stop typed at terminal
SIGSYS P2001 Core Bad system call (SVr4);
see also seccomp(2)
SIGTERM P1990 Term Termination signal
SIGTRAP P2001 Core Trace/breakpoint trap
SIGTTIN P1990 Stop Terminal input for background process
SIGTTOU P1990 Stop Terminal output for background process
SIGUNUSED - Core Synonymous with SIGSYS
SIGURG P2001 Ign Urgent condition on socket (4.2BSD)
SIGUSR1 P1990 Term User-defined signal 1
SIGUSR2 P1990 Term User-defined signal 2
SIGVTALRM P2001 Term Virtual alarm clock (4.2BSD)
SIGXCPU P2001 Core CPU time limit exceeded (4.2BSD);
see setrlimit(2)
SIGXFSZ P2001 Core File size limit exceeded (4.2BSD);
see setrlimit(2)
SIGWINCH - Ign Window resize signal (4.3BSD, Sun)
这个表格显示了不同的信号对应的默认处理动作,我们只对比看一下 term 和 core 的区别:
term(Terminate):普通终止,不生成核心转储文件。core(Core Dump):终止并生成核心转储文件(用于调试)。
以下是详细对比:
核心区别
| 特性 | term(普通终止) |
core(核心转储终止) |
|---|---|---|
| 行为 | 进程直接退出,无额外操作 | 进程退出,并生成 core 文件 |
| 信号示例 | SIGTERM(15)、SIGINT(2) |
SIGSEGV(11)、SIGABRT(6) |
| 是否可捕获 | 是(可自定义处理) | 是(但通常不推荐继续运行) |
| 用途 | 正常终止进程(如 kill 默认行为) |
调试程序崩溃(如段错误、断言失败) |
| 生成核心转储 | ❌ 否 | ✅ 是(需 ulimit -c unlimited) |
常见信号的终止类型
| 信号 | 编号 | 默认行为 | 说明 |
|---|---|---|---|
SIGTERM |
15 | term |
优雅终止(kill 默认发送) |
SIGINT |
2 | term |
Ctrl+C 触发 |
SIGKILL |
9 | term |
强制终止(不可捕获) |
SIGQUIT |
3 | core |
Ctrl+\ 触发,生成核心转储 |
SIGSEGV |
11 | core |
段错误(非法内存访问) |
SIGABRT |
6 | core |
abort() 触发,断言失败时常用 |
核心转储(Core Dump)详解
(1)什么是核心转储?
- 核心转储是 进程崩溃时的内存快照,保存了崩溃时的 寄存器状态、堆栈、变量值 等信息。
- 文件通常命名为
core或core.<PID>,需用gdb分析
(2)如何启用核心转储?
ulimit -c unlimited # 解除核心转储文件大小限制
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern # 指定保存路径
默认情况下,某些系统可能禁用核心转储(通过
ulimit -c 0)。
(3)什么情况下生成核心转储?
- 信号默认行为为
core(如SIGSEGV、SIGABRT)。 - 程序未捕获该信号(或捕获后仍调用
abort())。 - 文件系统有足够空间,且权限允许。
总结如下:
场景 推荐方式 正常终止进程(如 kill)SIGTERM(term)调试程序崩溃(如段错误) SIGSEGV(core)防止生成核心转储(安全敏感场景) 捕获信号并调用 _exit主动生成核心转储(调试) abort()或SIGABRT
term是 静默退出,core是 崩溃快照。- 生产环境可禁用
core(避免磁盘占满),开发环境建议启用。- 在信号处理函数中,优先用
_exit而非exit(避免递归问题)。