Linux 信号产生

Linux 信号产生

七月 22, 2025 次阅读

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)。
  • 特点
    • 支持排队:连续发送多次,进程会按顺序处理所有信号。
    • 有优先级:数值小的信号优先处理(如 3435 优先)。
    • 可携带额外数据(通过 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)

进程可以主动忽略某些信号(但部分信号如 SIGKILLSIGSTOP 无法被忽略)。

为什么 SIGKILLSIGSTOP 无法被忽略?

因为 SIGKILLSIGSTOP 是用于强制终止和暂停进程的信号,如果允许进程忽略它们,就无法保证进程能够被正常终止或暂停,这可能会导致系统不稳定。

我们可以在 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 + SCtrl + 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)。
    • 结束 catgrep 等命令的输入。

命令发送信号

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
...                                   # 该自定义函数会被反复执行

为什么会出现这种现象呢?

深入解析:异常信号(如除零、段错误)为何会持续触发


核心结论
当进程因 硬件异常(如除零、段错误)收到信号(如 SIGFPESIGSEGV)时,若在自定义处理函数中 不终止进程,操作系统会 反复触发该信号,导致处理函数被无限调用。
根本原因:异常的 硬件状态标志(如 CPU 状态寄存器、MMU 页表异常)未被清除,且随进程上下文保存,操作系统会持续检测并发送信号。


  1. 硬件异常与信号的关系

(1) 除零错误(SIGFPE)

  • 硬件机制
    • CPU 的 ALU(算术逻辑单元) 在执行除法时,若除数为零,会置位状态寄存器的 溢出标志位(如 x86 的 #DE 异常)。
    • OS 的干预
      • 操作系统通过 中断处理程序 捕获该硬件异常,将其转换为 SIGFPE 信号(编号 8)发送给进程。
      • 进程上下文 保存了异常的 CPU 状态(包括标志位),因此异常状态会持续存在。

(2) 段错误(SIGSEGV)

  • 硬件机制
    • MMU(内存管理单元) 在访问非法地址(如空指针)时,触发 缺页异常保护错误(如 x86 的 #PF 异常)。
    • OS 的干预
      • 操作系统检查异常地址合法性,若确认非法,则发送 SIGSEGV 信号(编号 11)给进程。
      • 页表错误状态会保留在进程的 内存上下文 中。

  1. 为何信号会持续触发?

关键点:异常标志未被清除

  • 若信号处理函数 不终止进程,操作系统在 恢复进程执行 时,会重新检测到 未解决的硬件异常状态,再次发送相同信号。
  • 类比
    • 像一个人不断踩到钉子,医生(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. 操作系统的设计哲学

(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(核心转储) 的区别

我们来看一下下面这个表格:

这个表格显示了不同的信号对应的默认处理动作,我们只对比看一下 termcore 的区别:

  1. term(Terminate):普通终止,不生成核心转储文件。
  2. 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)什么是核心转储?

  • 核心转储是 进程崩溃时的内存快照,保存了崩溃时的 寄存器状态、堆栈、变量值 等信息。
  • 文件通常命名为 corecore.<PID>,需用 gdb 分析

(2)如何启用核心转储?

ulimit -c unlimited  # 解除核心转储文件大小限制
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern  # 指定保存路径

默认情况下,某些系统可能禁用核心转储(通过 ulimit -c 0)。

(3)什么情况下生成核心转储?

  • 信号默认行为为 core(如 SIGSEGVSIGABRT)。
  • 程序未捕获该信号(或捕获后仍调用 abort())。
  • 文件系统有足够空间,且权限允许。

总结如下:

场景 推荐方式
正常终止进程(如 kill SIGTERMterm
调试程序崩溃(如段错误) SIGSEGVcore
防止生成核心转储(安全敏感场景) 捕获信号并调用 _exit
主动生成核心转储(调试) abort()SIGABRT
  • term静默退出core崩溃快照
  • 生产环境可禁用 core(避免磁盘占满),开发环境建议启用。
  • 在信号处理函数中,优先用 _exit 而非 exit(避免递归问题)。