Linux学习笔记:
https://blog.csdn.net/2301_80220607/category_12805278.html?spm=1001.2014.3001.5482
前言:
前面我们已经将进程通信部分讲完了,现在我们来讲一个进程部分也非常重要的知识点——信号,信号也是进程间通信的一种,本篇主要讲解信号的概念和信号的几种产生方法及对应的场景
目录
一、引言
在 Linux 操作系统中,信号(Signal)是一种进程间通信(IPC,Inter - Process Communication)的机制,它用于通知进程发生了某种异步事件。信号可以来自内核,也可以来自其他进程。进程接收到信号后,会根据信号的类型以及自身的处理方式做出相应的反应。理解信号对于编写健壮的 Linux 程序以及深入理解 Linux 操作系统的运行机制至关重要。
二、信号的概念
2.1 什么是信号
信号是一种软中断,它是一种异步通知机制。当某个特定事件发生时,如用户按下特定组合键、系统资源耗尽、进程异常终止等,系统会向相关进程发送一个信号。每个信号都有一个对应的编号和名称,例如信号 1 表示 SIGHUP(挂起信号),信号 9 表示 SIGKILL(强制终止信号)。
2.2 信号的作用
信号的主要作用是让进程能够对异步事件做出响应。例如,当用户在终端中按下 Ctrl + C 组合键时,系统会向当前前台进程发送 SIGINT 信号,通常进程会接收到这个信号后停止当前正在执行的任务并退出。信号还可以用于进程间的通信,一个进程可以向另一个进程发送信号来通知其执行某些操作。
结合2.1和2.2我们来讲解一个概念:信号是一种软中断,是什么意思呢?当我们往键盘中输入内容时是如何告诉给内核的?ctrl+c又是如何被解释为指令的呢?
我们先来看下面这张图:
键盘实际上是通过中断来让操作系统知道自己要写入内容的,键盘被按下时,就会触发硬件中断,不同的硬件对应着不同的中断号,中断单元就可以通过它们的中断号将它们与CPU中不同的键位相连,从而使CPU中这个方向的寄存器(32位)特定位置产生电信号,操作系统中有一个叫中断向量表的类似于函数指针结构体的结构,里面保存着访问各种外设的方法,操作系统通过CPU产生的电信号就辨别出要获取哪种硬件的信息,从而通过中断向量表中的方法,将硬件中的信息拷贝到操作系统的文件缓冲区中(操作系统下一切皆文件,且每一个文件都有自己的文件缓冲中区),然后再拷贝到用户缓冲区
同时比如键盘等外键,操作系统在获取键盘上的信息时会先进行识别,会对数据进行判断,如果是控制进程的比如ctrl+c等组合键就不会往缓冲区中拷贝,我们可以发现我们学习的信号与上面的中断过程很像,其实信号,就是用软件方式,模拟的对讲程的硬件中断,所以信号也被叫做软中断
2.3 信号的特点
- 异步性:信号的产生是异步的,与进程的执行顺序无关。进程在运行过程中可能随时收到信号。
- 简单性:信号机制相对简单,只需要一个信号编号就可以标识不同的信号。
- 有限性:Linux 系统中定义的信号数量是有限的,不同的系统可能略有差异,但通常在几十种左右。
2.4 常见信号列表
信号编号 | 信号名称 | 含义 | 默认处理方式 |
1 | SIGHUP | 挂起信号,通常在终端关闭时发送给相关进程 | 终止进程 |
2 | SIGINT | 中断信号,由用户按下 Ctrl + C 组合键产生 | 终止进程 |
3 | SIGQUIT | 退出信号,由用户按下 Ctrl + \ 组合键产生 | 终止进程并生成核心转储文件 |
9 | SIGKILL | 强制终止信号,不能被捕获、阻塞或忽略 | 立即终止进程 |
15 | SIGTERM | 终止信号,通常用于正常终止进程 | 终止进程 |
18 | SIGCONT | 继续信号,用于恢复被暂停的进程 | 继续执行进程 |
19 | SIGSTOP | 停止信号,用于暂停进程,不能被捕获、阻塞或忽略 | 暂停进程 |
可以通过kill -l指令查看所有信号
kill -l
三、信号的产生
3.1 前台进程和后台进程
先来科普一个小知识点:前台进程和后台进程,来看下面一个程序
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
while(true)
{
cout<<"I am a crazy process"<<endl;
sleep(1);
}
return 0;
}
我们进行编译后会得到一个可执行程序
./myfile
我们这样执行时我们会发现在程序运行的时候,我们输入其它指令比如Is,pwd等都不会有结果,进程还在继续运行,除非用ctrl+c终止掉进程,这样的进程称为前台进程
./myfile &
这种的后面加上地址符的叫做后台进程,后台进程可以被其它进程命令临时打断并执行这个命令,比如我们输入ls指令,进程就会暂停并且输出Is的结果,但是最后需要自己把进程结束掉
Linux中,一次登陆中, 一个终端,一般会配上一个bash,每一个登陆,只允许一个进程是前台进程,可以允许多个进程是后台进程
当./process运行时,输入指令之所以不能运行就是因为此时的前台进程由bash转变为了process
- 终端占用情况
- 前台进程:会独占终端,直到进程执行完成或者被挂起,在这期间终端无法接受其他命令输入,用户只能与该进程进行交互。
- 后台进程:不会占用终端,终端可以继续接受用户输入的其他命令,用户可以在同一个终端中同时启动多个后台进程,并随时切换到其他任务。
- 运行特性
- 前台进程:其执行过程会受到用户操作的直接影响,比如用户可以通过键盘输入来中断或暂停进程。如果终端关闭,前台进程通常会被终止,除非进行了特殊的设置。
- 后台进程:通常是长时间运行的,不受终端关闭的影响,除非明确地对其进行停止或重启操作。它按照自身的逻辑和任务需求在后台持续运行,不会因为用户的一些常规操作而中断。
3.2 用户产生信号
- 键盘输入:用户可以通过在终端中按下特定的组合键来产生信号。例如:
- Ctrl + C:产生 SIGINT 信号,用于中断当前正在运行的进程。比如,我们在终端中运行一个长时间运行的命令while true; do echo "Hello"; sleep 1; done,按下 Ctrl + C 后,该命令对应的进程会接收到 SIGINT 信号并终止。
- Ctrl + \:产生 SIGQUIT 信号,不仅会终止进程,还会生成核心转储文件(如果系统配置允许,一般在云服务器上是默认关闭的,虚拟机上可能是开启的)。例如,运行一个简单的 C 程序#include <stdio.h> int main() { while(1); return 0; },编译运行后,按下 Ctrl + \,进程会终止并生成核心转储文件(在当前目录下,文件名为 core,具体名称和位置可能因系统配置而异)。(了解即可,这个生成core文件的内容与进程退出部分也有联系,有想了解的可以单独去搜索一下)
- 使用 kill 命令:用户可以使用 kill 命令向指定进程发送信号。kill 命令的基本语法是kill [信号编号] 进程ID。例如,要向进程 ID 为 1234 的进程发送 SIGTERM 信号(信号编号为 15),可以在终端中输入kill -15 1234,也可以使用信号名称kill -SIGTERM 1234。如果省略信号编号或名称,默认发送 SIGTERM 信号。
3.3 系统产生信号
- 进程异常:当进程发生异常时,如段错误(访问非法内存地址)、除零错误等,系统会向该进程发送相应的信号。
-
- 段错误(Segmentation Fault):当进程访问了不属于它的内存区域时,会产生段错误,一般都是野指针问题,系统会向该进程发送 SIGSEGV 信号。例如,下面的 C 代码会导致段错误:
#include <stdio.h>
int main() {
int *ptr = NULL;
*ptr = 10; // 试图向空指针指向的地址写入数据,会引发段错误
return 0;
}
编译运行这段代码,程序会崩溃,并提示 “Segmentation fault”,这是因为进程接收到了 SIGSEGV 信号。
- 除零错误(Division by Zero):当进程执行除法运算时,如果除数为零,会产生除零错误,系统会向该进程发送 SIGFPE 信号。例如:
#include <stdio.h>
int main()
{
int a = 10;
int b = 0;
int c = a / b; // 除零操作,会引发除零错误
return 0;
}
运行这段代码,程序会崩溃,并提示 “Floating point exception”,这是因为进程接收到了 SIGFPE 信号。
2. 系统资源相关:当系统资源达到一定阈值时,也可能产生信号。例如,当进程使用的内存超过了系统限制时,系统可能会发送 SIGKILL 信号来终止该进程,以防止系统内存耗尽。不过,这种情况通常需要系统进行相关的配置和监控。
3.4 软件产生信号
- 使用 kill 函数:在 C 语言编程中,可以使用 kill 函数向指定进程发送信号。kill 函数的原型可以用man手册查看,如下:
man 2 kill
其中,pid 是目标进程的 ID,sig 是要发送的信号编号。例如,下面的代码演示了如何使用 kill 函数向另一个进程发送 SIGTERM 信号:
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main()
{
pid_t target_pid = 1234; // 假设目标进程ID为1234
int result = kill(target_pid, SIGTERM);
if (result == -1)
{
perror("kill failed");
}
else
{
printf("SIGTERM sent to process %d\n", target_pid);
}
return 0;
}
在实际使用中,需要将target_pid替换为真实的目标进程 ID。
2. 使用 raise 函数:进程可以使用 raise 函数向自身发送信号。raise 函数的原型也可以通过man手册来查看,如下:
man raise
其中,sig 是要发送的信号编号。例如,下面的代码演示了如何使用 raise 函数向自身发送 SIGINT 信号:
#include <stdio.h>
#include <signal.h>
int main()
{
int result = raise(SIGINT);
if (result != 0)
{
perror("raise failed");
}
else
{
printf("SIGINT sent to self\n");
}
return 0;
}
运行这段代码,进程会接收到自己发送的 SIGINT 信号并终止。
四、信号的处理
4.1 默认处理方式
每个信号都有一个默认的处理方式,常见的默认处理方式包括:
- 终止进程:如 SIGINT、SIGTERM 等信号的默认处理方式是终止进程。
- 生成核心转储文件并终止进程:例如 SIGQUIT 信号,在终止进程的同时会生成核心转储文件,该文件包含了进程在收到信号时的内存状态等信息,可用于调试程序。
- 忽略信号:有些信号(如 SIGCHLD,子进程状态改变时发送给父进程的信号)的默认处理方式是忽略。
4.2 自定义信号处理函数
进程可以通过调用 signal 函数或 sigaction 函数来设置自定义的信号处理函数。
- signal 函数:signal 函数的原型如下:
man signal
其中,signum 是信号编号,handler 是指向信号处理函数的指针。例如,下面的代码演示了如何使用 signal 函数设置 SIGINT 信号的自定义处理函数:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signum)
{
printf("Received SIGINT. Cleaning up...\n");
// 在这里进行一些清理工作,如关闭文件、释放资源等
_exit(0); // 退出进程
}
int main()
{
signal(SIGINT, signal_handler);
while (1)
{
printf("Running...\n");
sleep(1);
}
return 0;
}
在这个例子中,当进程接收到 SIGINT 信号时,会调用signal_handler函数,而不是默认的终止进程操作。
2. sigaction 函数:sigaction 函数比 signal 函数提供了更丰富的功能,它可以设置信号处理函数、处理信号时的掩码、信号的标志等。sigaction 函数的原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
其中,signum 是信号编号,act 是指向新的信号处理动作的结构体指针,oldact 是指向旧的信号处理动作的结构体指针(如果不需要获取旧的处理动作,可以设为 NULL)。例如,下面的代码演示了如何使用 sigaction 函数设置 SIGINT 信号的自定义处理函数:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signum)
{
printf("Received SIGINT. Cleaning up...\n");
// 在这里进行一些清理工作,如关闭文件、释放资源等
_exit(0); // 退出进程
}
int main()
{
struct sigaction new_action, old_action;
new_action.sa_handler = signal_handler;
sigemptyset(&new_action.sa_mask);
new_action.sa_flags = 0;
sigaction(SIGINT, &new_action, &old_action);
while (1)
{
printf("Running...\n");
sleep(1);
}
return 0;
}
这段代码与使用 signal 函数的例子功能类似,但使用 sigaction 函数可以更灵活地配置信号处理方式。
五、总结
信号是 Linux 系统中一种重要的进程间通信和异步事件通知机制。通过本文,我们详细了解了信号的概念,信号的产生和部分信号的处理工作,后面我们还会讲解信号的捕捉等处理工作,学习信号可以帮助我们更好的实现进程通信和异步处理等诸多操作
本篇笔记:
感谢各位大佬观看,创作不易,还请各位大佬点赞支持!!!