Linux 进程信号的产生

目录

0.前言

1. 通过终端按键产生信号

1.1 Ctrl+C:发送 SIGINT 信号

1.2 Ctrl+\:发送 SIGQUIT 信号

1.3 Ctrl+Z:发送 SIGTSTP 信号

2.调用系统命令向进程发信号

3.使用函数产生信号

3.1 kill 函数

3.2 raise 函数

3.3 abort 函数

4.由软件条件产生信号

4.1 SIGPIPE 信号

4.2 SIGALRM 信号

5.硬件异常产生信号

5.1 常见硬件异常信号

5.1.1 除 0 操作:SIGFPE 信号

5.1.2 访问野指针:SIGSEGV 信号

5.2 核心转储(Core Dump)

6.小结


(图像由AI生成) 

0.前言

在 Linux 系统中,信号是用于进程间通信的重要机制,通过信号可以实现对进程的控制、状态传递以及异常处理。在上一篇博客中,我们简单介绍了进程信号的概念、常见类型以及信号的基本作用。本篇博客将继续深入,从信号的产生开始,探讨信号如何被触发并送达进程。这是理解信号处理的第一步,也是后续学习信号保存和信号处理机制的基础。通过本篇内容,你将了解信号的几种常见触发方式,包括用户手动产生、程序主动触发,以及系统自动生成等场景。

1. 通过终端按键产生信号

Linux 系统中,终端不仅是与操作系统交互的重要工具,也是信号产生的主要入口之一。在终端中按下特定的键盘组合,可以向当前前台进程发送信号,这些信号可以中断、暂停或终止程序运行。

1.1 Ctrl+C:发送 SIGINT 信号

  • 概念
    按下 Ctrl+C 会向当前前台进程发送 SIGINT 信号(Signal Interrupt)。这是用户最常用的中断方式,用于优雅地终止正在运行的程序。

  • 默认行为
    程序收到 SIGINT 信号后,默认会立即终止执行,但不会生成核心转储文件。

  • 实际场景
    例如,当你运行一个死循环脚本时,可以通过 Ctrl+C 终止它:

    while true; do echo "Running..."; sleep 1; done

    按下 Ctrl+C 后,循环将中止。

  • 自定义处理
    程序也可以捕获 SIGINT 信号并定义自定义逻辑。例如,以下代码展示了如何拦截并处理 SIGINT 信号:

    #include <stdio.h>
    #include <signal.h>
    
    void handle_sigint(int sig) {
        printf("Caught SIGINT signal: %d\n", sig);
    }
    
    int main() {
        signal(SIGINT, handle_sigint);
        while (1) {
            printf("Running... Press Ctrl+C to interrupt\n");
            sleep(1);
        }
        return 0;
    }
    

1.2 Ctrl+\:发送 SIGQUIT 信号

  • 概念
    按下 Ctrl+\ 会触发 SIGQUIT 信号(Signal Quit)。与 SIGINT 类似,SIGQUIT 的默认行为也是终止进程,但它会额外生成一个核心转储文件(core dump)。

  • 默认行为
    程序收到 SIGQUIT 信号后,系统会记录程序运行时的内存状态,生成 core dump 文件,供开发者调试使用。

  • 实际场景
    在程序运行时按下 Ctrl+\,程序不仅会终止,还会留下 core dump 文件。如果 core dump 未启用,可以通过以下命令启用:

    ulimit -c unlimited # 设置核心文件大小为无限

1.3 Ctrl+Z:发送 SIGTSTP 信号

  • 概念
    按下 Ctrl+Z 会向前台进程发送 SIGTSTP 信号(Signal Terminal Stop)。该信号用于将进程挂起(暂停运行),并将控制权还给终端。

  • 默认行为
    收到 SIGTSTP 信号后,进程会进入“暂停”状态,直到用户手动恢复它。恢复命令包括:

    • fg:恢复进程到前台继续执行。
    • bg:将进程移至后台继续执行。
  • 实际场景
    当运行一个耗时较长的任务时,可以按 Ctrl+Z 暂时挂起它,进行其他操作。例如:

    sleep 100

    按下 Ctrl+Z 后,任务将暂停。输入 fg 恢复它,或者使用 bg 将其移至后台。

2.调用系统命令向进程发信号

在 Linux 系统中,除了通过终端按键,用户还可以使用系统命令向进程发送信号。其中最常用的命令是 kill,它允许用户直接向目标进程发送指定信号,以控制或终止进程。kill 命令的基本语法如下:

kill [选项] <进程ID>

常用选项

  • -SIGNAL:指定发送的信号名称或编号。例如,-SIGKILL-9 表示发送 SIGKILL 信号。
  • -l:列出所有信号名称及其编号。
  • -s SIGNAL:显式指定信号名称。
  • -pid:目标进程 ID,可以是一个或多个。

举例说明

  • 发送默认信号(SIGTERM)

    kill 1234(某个进程的PID)

    默认发送 SIGTERM 信号,通常用于优雅地终止进程。

  • 发送特定信号
    强制终止进程可以使用 SIGKILL(信号编号 9):

    kill -9 1234
  • 列出所有信号
    查看系统支持的信号列表:

    kill -l

3.使用函数产生信号

3.1 kill 函数

kill 是用于向任意进程发送信号的系统调用,其语法如下:

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
  • 参数说明

    • pid:目标进程的 PID。
      • > 0:向指定的单个进程发送信号。
      • = 0:向调用进程所在的进程组发送信号。
      • < -1:向特定进程组发送信号。
    • sig:要发送的信号类型(如 SIGINTSIGKILL)。
  • 示例: 向当前进程发送 SIGTERM 信号:

    #include <signal.h>
    #include <unistd.h>
    
    int main() {
        kill(getpid(), SIGTERM);
        return 0;
    }
    

3.2 raise 函数

raise 是一个标准库函数,用于向自身发送信号,其本质是对 kill 函数的封装。语法如下:

#include <signal.h>

int raise(int sig);
  • 参数说明

    • sig:要发送的信号。
  • 特点

    • 只能向当前进程发送信号。
    • 使用方便,不需要获取 PID。
  • 示例: 触发 SIGINT 信号:

    #include <signal.h>
    #include <stdio.h>
    
    void signal_handler(int sig) {
        printf("Caught signal: %d\n", sig);
    }
    
    int main() {
        signal(SIGINT, signal_handler); // 注册信号处理函数
        raise(SIGINT); // 发送信号
        return 0;
    }
    

3.3 abort 函数

abort 是一个标准库函数,用于向自身发送 SIGABRT 信号,强制使程序异常终止,并生成核心转储文件(core dump)。语法如下:

#include <stdlib.h>

void abort(void);
  • 特点

    • 触发 SIGABRT 信号。
    • 通常用于在程序遇到严重错误时调用。
    • 如果未捕获 SIGABRT,程序将直接退出。
  • 示例: 强制终止程序并生成 core dump:

    #include <stdlib.h>
    
    int main() {
        abort();
        return 0;
    }
    

4.由软件条件产生信号

除了用户手动触发信号和程序显式调用信号函数外,信号也可以在程序运行时,由特定的软件条件自动触发。例如,管道写入异常会触发 SIGPIPE 信号,定时器到期会触发 SIGALRM 信号。

4.1 SIGPIPE 信号

  • 触发条件
    当一个进程尝试向已经关闭的管道(pipe)或套接字(socket)写入数据时,系统会自动发送 SIGPIPE 信号。默认行为是终止进程。

  • 常见场景
    例如,父进程向关闭的管道写入数据:

    #include <unistd.h>
    #include <signal.h>
    #include <stdio.h>
    
    int main() {
        int fds[2];
        pipe(fds);           // 创建管道
        close(fds[0]);       // 关闭读端
    
        if (write(fds[1], "data", 4) == -1) {  // 写入数据
            perror("write error");
        }
        return 0;
    }
    

    输出类似:

    write error: Broken pipe
  • 解决方式
    捕获 SIGPIPE 信号并进行自定义处理,或者忽略该信号:

    signal(SIGPIPE, SIG_IGN); // 忽略SIGPIPE

4.2 SIGALRM 信号

  • 触发条件
    SIGALRM 信号由 alarm 函数触发,用于在指定的时间间隔后通知进程执行某些任务。

  • alarm 函数
    alarm 函数设置一个秒级定时器,到期时发送 SIGALRM 信号:

    #include <unistd.h>
    
    unsigned int alarm(unsigned int seconds);
    
  • 示例
    设置一个 3 秒定时器,触发后执行信号处理函数:

    #include <signal.h>
    #include <unistd.h>
    #include <stdio.h>
    
    void handle_alarm(int sig) {
        printf("Alarm triggered!\n");
    }
    
    int main() {
        signal(SIGALRM, handle_alarm); // 注册处理函数
        alarm(3);                      // 设置定时器
        pause();                       // 等待信号
        return 0;
    }
    

    输出类似:

    Alarm triggered!
  • 取消定时器
    再次调用 alarm 函数,传入 0 即可取消定时器:

    alarm(0);

5.硬件异常产生信号

在程序运行过程中,某些硬件异常(例如非法操作或内存访问)会触发系统向进程发送特定信号。这些信号通常表示程序存在错误,默认行为是终止程序运行,并可能生成 核心转储文件(core dump)

5.1 常见硬件异常信号

5.1.1 除 0 操作:SIGFPE 信号

  • 触发条件
    当程序执行非法的算术运算(如整数除以 0)时,会触发 SIGFPE 信号(Floating Point Exception)。默认行为是终止进程。

  • 示例代码
    以下代码试图执行除以 0 的操作:

    #include <stdio.h>
    
    int main() {
        int a = 1;
        int b = 0;
        printf("%d\n", a / b); // 除以 0
        return 0;
    }
    

    结果:程序崩溃,触发 SIGFPE

  • 捕获信号
    使用信号处理函数捕获 SIGFPE

    #include <signal.h>
    #include <stdio.h>
    
    void handle_sigfpe(int sig) {
        printf("Caught SIGFPE: Illegal operation\n");
    }
    
    int main() {
        signal(SIGFPE, handle_sigfpe);
        int a = 1, b = 0;
        printf("%d\n", a / b);
        return 0;
    }
    

5.1.2 访问野指针:SIGSEGV 信号

  • 触发条件
    当程序尝试访问非法内存地址(如空指针或已释放的内存)时,会触发 SIGSEGV 信号(Segmentation Fault)。默认行为是终止进程。

  • 示例代码
    以下代码访问了一个空指针:

    #include <stdio.h>
    
    int main() {
        int *ptr = NULL;
        *ptr = 42; // 访问空指针
        return 0;
    }
    

    结果:程序崩溃,触发 SIGSEGV

  • 捕获信号
    可以捕获 SIGSEGV 信号来处理异常:

    #include <signal.h>
    #include <stdio.h>
    
    void handle_sigsegv(int sig) {
        printf("Caught SIGSEGV: Invalid memory access\n");
    }
    
    int main() {
        signal(SIGSEGV, handle_sigsegv);
        int *ptr = NULL;
        *ptr = 42;
        return 0;
    }
    

5.2 核心转储(Core Dump)

核心转储是一种调试工具,用于记录程序崩溃时的内存状态,帮助开发者排查问题。

生成核心转储

  1. 启用核心转储
    默认情况下,核心转储可能被限制或禁用。可以使用以下命令启用:

    ulimit -c unlimited
  2. 运行出错程序
    当程序崩溃时,系统会生成核心转储文件,通常命名为 corecore.<pid>

  3. 调试核心转储
    使用调试工具 gdb 分析核心转储文件:

    gdb ./program core

    在调试器中,可以查看崩溃时的调用堆栈和内存状态。

示例调试

假设程序崩溃生成了核心转储文件,可以通过以下命令进入调试器:

gdb ./a.out core

进入调试器后,运行 bt(backtrace)命令查看调用堆栈:

bt (进入gdb调试后)

6.小结

本篇博客全面介绍了 Linux 系统中信号的产生方式,从终端按键、系统命令,到函数调用和软件条件触发,再到硬件异常的信号处理。信号作为进程间通信和异常管理的关键机制,贯穿了用户操作、程序设计和系统运行的各个层面。理解信号的来源和触发方式,是深入学习 Linux 信号处理与调试技巧的重要基础,为编写健壮的程序提供了强有力的支持。

上一篇:uniapp适配暗黑模式配置plus.nativeUI.setUIStyle适配DarkMode配置