目录
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
:要发送的信号类型(如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 信号处理与调试技巧的重要基础,为编写健壮的程序提供了强有力的支持。