
(图像由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:要发送的信号类型(如- SIGINT或- SIGKILL)。
 
-  示例: 向当前进程发送 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)
核心转储是一种调试工具,用于记录程序崩溃时的内存状态,帮助开发者排查问题。
生成核心转储
-  启用核心转储 
 默认情况下,核心转储可能被限制或禁用。可以使用以下命令启用:ulimit -c unlimited
-  运行出错程序 
 当程序崩溃时,系统会生成核心转储文件,通常命名为core或core.<pid>。
-  调试核心转储 
 使用调试工具gdb分析核心转储文件:gdb ./program core在调试器中,可以查看崩溃时的调用堆栈和内存状态。 
示例调试
假设程序崩溃生成了核心转储文件,可以通过以下命令进入调试器:
gdb ./a.out core进入调试器后,运行 bt(backtrace)命令查看调用堆栈:
bt (进入gdb调试后)6.小结
本篇博客全面介绍了 Linux 系统中信号的产生方式,从终端按键、系统命令,到函数调用和软件条件触发,再到硬件异常的信号处理。信号作为进程间通信和异常管理的关键机制,贯穿了用户操作、程序设计和系统运行的各个层面。理解信号的来源和触发方式,是深入学习 Linux 信号处理与调试技巧的重要基础,为编写健壮的程序提供了强有力的支持。










