Linux 信号的产生与处理
1. 信号的概念
1.1 认识信号
生活中的类比:网购快递的过程类比信号的注册和处理,快递员的电话通知相当于信号,处理方式体现了异步性。
技术应用方面,在Linux上按下Ctrl+C触发硬件中断,操作系统发送信号给前台进程,实现退出。
1.2 信号的概念
信号是进程间异步通知的方式,属于软中断。
1.3 查看系统定义的信号
系统定义了一系列信号,分为普通和实时信号,每个信号都有编号和宏定义名称。
1~31为普通信号,34~64为实时信号。
使用 kill -l 命令查看
2. 信号的产生
2.1 键盘触发
键盘操作是产生信号的主要途径之一。以下是一些常见的键盘触发信号的例子:
Ctrl+C(SIGINT): 用户按下Ctrl+C组合键,产生SIGINT信号,通常用于中断当前正在执行的进程。
Ctrl+Z(SIGTSTP): 使用Ctrl+Z组合键,产生SIGTSTP信号,将前台进程置于后台并暂停它的执行。
Ctrl+\(SIGQUIT): 按下Ctrl+\组合键,产生SIGQUIT信号,通常用于结束进程并生成Core Dump文件。
2.2 其他触发源
除了键盘触发外,还有其他情况会产生信号:
定时器到期(SIGALRM): 使用alarm函数设置的定时器到期会产生SIGALRM信号。
子进程状态变化(SIGCHLD): 当一个子进程退出或停止时,父进程会收到SIGCHLD信号。
硬件错误(SIGBUS、SIGSEGV): 运行时发生硬件错误,例如内存访问越界,会产生相应的信号。
用户发送信号(kill命令): 用户可以使用kill命令向指定进程发送信号,例如 kill -2 <pid>
3. 信号的处理方法
在 Linux 中,有三种常见的信号处理方法:忽略信号、执行默认处理动作、以及利用 signal 系统调用捕捉信号并设置自定义处理函数。
signal 是一个用于设置信号处理方式的函数,其原型为:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
简介
signal 函数用于设置对特定信号的处理方式,其中 sighandler_t 是一个函数指针类型,表示信号处理函数的类型。
参数
signum:指定信号的编号,可以是标准信号(如 SIGINT)或用户自定义的信号。
handler:是一个函数指针,指向处理指定信号的函数。
- handler有三种特殊的处理方式:
SIG_DFL:默认处理方式,即系统提供的默认行为。
SIG_IGN:忽略信号,即不采取任何操作。
自定义处理函数(sighandler_t ):用户自己定义的函数,用于处理接收到的信号。
定义sighandler_t类型
typedef void (*sighandler_t)(int);
定义了 sighandler_t类型,表示信号处理函数的类型。该类型是一个函数指针类型,指向一个接受 一个整数参数(信号编号) 并返回 void 的函数。
返回值
如果成功,返回之前与该信号相关联的处理函数的指针。
如果设置失败,返回 SIG_ERR。
3.1 忽略信号
什么是忽略信号?
忽略信号意味着进程在接收到该信号时将不采取任何动作,即忽略信号的发生。
如何忽略信号?
使用 signal 系统调用,将信号处理函数设置为 SIG_IGN(忽略信号)。
#include <stdio.h>
#include <signal.h>
int main() {
// 忽略 SIGINT 信号
signal(SIGINT, SIG_IGN);
printf("Waiting for SIGINT (Ctrl+C) signal...\n");
pause(); // 暂停进程,等待信号发生
return 0;
}
在这个例子中,进程将忽略 SIGINT 信号,即按下 Ctrl+C 时不会终止程序。
3.2 执行默认处理动作
什么是执行默认处理动作?
执行默认处理动作表示进程在接收到信号时将按照系统默认的处理方式来处理该信号。
如何执行默认处理动作?
不调用默认处理函数
如果不调用 signal 设置处理函数,系统会默认执行默认处理动作。
使用 signal 系统调用,将信号处理函数设置为 SIG_DFL(默认处理方式)。
#include <stdio.h>
#include <signal.h>
int main() {
// 执行默认处理动作(调用 SIG_DFL 函数)
signal(SIGINT, SIG_DFL);
printf("Waiting for SIGINT (Ctrl+C) signal (default action)...\n");
pause(); // 暂停进程,等待信号发生
return 0;
}
在这个例子中,进程将恢复对 SIGINT 信号的默认处理方式,即按下 Ctrl+C 时将终止程序。
3.3 捕捉信号并设置自定义处理函数
什么是捕捉信号?
捕捉信号意味着进程在接收到信号时将执行用户自定义的信号处理函数。
如何捕捉信号?
#include <stdio.h>
#include <signal.h>
// 信号处理函数 - 捕捉信号并执行默认处理动作
void sigint_handler(int signum) {
printf("SIGINT signal received. Custom handler called for signal %d\n", signum);
}
int main() {
// 捕捉 SIGINT 信号并设置自定义处理函数
signal(SIGINT, sigint_handler);
printf("Waiting for SIGINT (Ctrl+C) signal (custom handler)...\n");
pause(); // 暂停进程,等待信号发生
return 0;
}
在这个例子中,signal(SIGINT, custom_handler) 将 SIGINT 信号的处理方式设置为 custom_handler 函数。当程序运行时,如果用户按下Ctrl+C,就会调用自定义处理函数。
3.4 SIGKILL和SIGSTOP信号
Unix/Linux操作系统中的信号处理是进程通信和控制的关键部分。然而,有两个特殊的信号,SIGKILL和SIGSTOP,它们具有不同寻常的特性,不能被捕获和处理。
1. SIGKILL信号
不可捕获: 任何尝试捕获SIGKILL信号的操作都会被忽略。
强制终止: 一旦进程接收到SIGKILL信号,它将立即被强制终止,无法改变这一行为。
用途: 主要用于紧急情况下迅速结束无响应或不受控制的进程。2. SIGSTOP信号
不可捕获: 与SIGKILL类似,SIGSTOP信号无法被捕获。
暂停执行: 进程接收到SIGSTOP信号后,会被暂停执行,成为非运行状态的进程。
用途: 主要用于调试和进程管理,为系统管理员提供了操控进程执行状态的有效手段。
4. 信号系统调用
4.1 kill系统调用
#include <signal.h>
int kill(pid_t pid, int sig);
功能: 向指定进程发送指定信号。
pid: 目标进程的进程ID。
sig: 要发送的信号,可以是预定义的信号(如SIGTERM)或用户自定义的信号。
返回值: 成功返回0,失败返回-1,同时设置errno以指示错误类型。
4.2 raise函数
#include <signal.h>
int raise(int sig);
功能: 向当前进程发送指定信号。
sig: 要发送的信号,可以是预定义的信号(如SIGUSR1)或用户自定义的信号。
返回值: 总是返回0。
4.3 abort函数
#include <stdlib.h>
void abort(void);
功能: 使当前进程异常终止,并生成核心转储文件(如果启用了核心转储)。
abort 函数会向当前进程发送 SIGABRT 信号,导致程序异常终止。
4.4 alarm 函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
功能: 设置一个定时器,经过指定的秒数后,向调用进程发送 SIGALRM 信号。
seconds: 指定的秒数,即定时器的时长。如果 seconds 为 0,表示取消先前设置的定时器。
返回值:
在时间到之前没有再调用alarm函数,则返回 0。
如果在时间到之前又再调用alarm,覆盖了之前的定时器,则返回之前定时器的剩余时间。
5. waitpid和Core Dump(核心转储)
pid_t waitpid(pid_t pid, int *status, int options);
在 waitpid 函数中,用于检测子进程是否产生了 core dump 的标志位是 WCOREDUMP。该标志位通过宏定义WCOREDUMP(status) 来判断,在 status 参数中,如果 WCOREDUMP(status) 返回非零值,表示子进程产生了核心转储文件,否则没有。这可以帮助程序在子进程发生崩溃时进行适当的处理。
if (WCOREDUMP(status)) {
printf("Core dump occurred.\n");
}
以上表格是关于不同Linux信号的详细信息,包括信号名称、数值、默认动作和注释。每一行都描述了一个特定的信号,使用户能够了解不同信号在操作系统中的行为以及处理方式。
Core(产生核心转储文件)
动作(Action): 生成包含进程内存映像的核心转储文件,有助于调试。
Term(终止)
动作(Action): 温和地终止进程,允许进行清理操作。
Kill(强制终止)
动作(Action):
强制立即终止进程,不能被阻塞、处理或忽略。
Cont(继续)
动作(Action): 使被停止的进程继续执行。
Ign(忽略)
动作(Action): 忽略信号,即不对其产生任何响应。
Stop(停止)
动作(Action): 使进程暂停执行,直到接收到 SIGCONT 信号。
Core Dump
是指在程序发生严重错误时,操作系统将程序的内存内容以文件形式保存下来。这有助于调试程序,分析错误原因。Core Dump 文件通常命名为 core。
使用 ulimit -a 命令查看是否有开起core dump
这会显示当前允许生成的核心转储文件的最大大小,表示可以生成。
如果是0,表示不允许生成核心转储文件(core dump)。
更改核心转储设置
ulimit -c 大小
关闭设置为 0 即可
代码例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t child_pid = fork();
if (child_pid == 0) {
// 子进程
printf("子进程ID: %d\n", getpid());
// 引发除以零的错误,产生核心转储文件
int result = 1 / 0;
// 由于除以零,这一行将不会被执行到
exit(EXIT_SUCCESS);
} else if (child_pid > 0) {
// 父进程
int status;
pid_t pid = waitpid(child_pid, &status, 0);
if (pid == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status)) {
printf("子进程以状态 %d 退出\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程被信号 %d 终止\n", WTERMSIG(status));
// 检查是否发生了核心转储
if (WCOREDUMP(status)) {
printf("发生了核心转储。\n");
}
}
} else {
perror("fork");
exit(EXIT_FAILURE);
}
return 0;
}
输出:
使用core dump
请注意,使用核心转储文件需要对程序进行调试,因此需要在编译时包含调试信息(使用 -g 编译选项)。