Linux学习笔记:
https://blog.csdn.net/2301_80220607/category_12805278.html?spm=1001.2014.3001.5482
前言:
前面我们已经将进程通信部分讲完了,现在我们来讲一个进程部分也非常重要的知识点——信号,信号也是进程间通信的一种,本篇主要讲解信号的概念和信号的几种产生方法及对应的场景
在 Linux 操作系统中,信号(Signal)是一种进程间通信(IPC,Inter - Process Communication)的机制,它用于通知进程发生了某种异步事件。信号可以来自内核,也可以来自其他进程。进程接收到信号后,会根据信号的类型以及自身的处理方式做出相应的反应。理解信号对于编写健壮的 Linux 程序以及深入理解 Linux 操作系统的运行机制至关重要。
信号是一种软中断,它是一种异步通知机制。当某个特定事件发生时,如用户按下特定组合键、系统资源耗尽、进程异常终止等,系统会向相关进程发送一个信号。每个信号都有一个对应的编号和名称,例如信号 1 表示 SIGHUP(挂起信号),信号 9 表示 SIGKILL(强制终止信号)。
信号的主要作用是让进程能够对异步事件做出响应。例如,当用户在终端中按下 Ctrl + C 组合键时,系统会向当前前台进程发送 SIGINT 信号,通常进程会接收到这个信号后停止当前正在执行的任务并退出。信号还可以用于进程间的通信,一个进程可以向另一个进程发送信号来通知其执行某些操作。
结合2.1和2.2我们来讲解一个概念:信号是一种软中断,是什么意思呢?当我们往键盘中输入内容时是如何告诉给内核的?ctrl+c又是如何被解释为指令的呢?
我们先来看下面这张图:
键盘实际上是通过中断来让操作系统知道自己要写入内容的,键盘被按下时,就会触发硬件中断,不同的硬件对应着不同的中断号,中断单元就可以通过它们的中断号将它们与CPU中不同的键位相连,从而使CPU中这个方向的寄存器(32位)特定位置产生电信号,操作系统中有一个叫中断向量表的类似于函数指针结构体的结构,里面保存着访问各种外设的方法,操作系统通过CPU产生的电信号就辨别出要获取哪种硬件的信息,从而通过中断向量表中的方法,将硬件中的信息拷贝到操作系统的文件缓冲区中(操作系统下一切皆文件,且每一个文件都有自己的文件缓冲中区),然后再拷贝到用户缓冲区 同时比如键盘等外键,操作系统在获取键盘上的信息时会先进行识别,会对数据进行判断,如果是控制进程的比如ctrl+c等组合键就不会往缓冲区中拷贝,我们可以发现我们学习的信号与上面的中断过程很像,其实信号,就是用软件方式,模拟的对讲程的硬件中断,所以信号也被叫做软中断
信号编号 | 信号名称 | 含义 | 默认处理方式 |
---|---|---|---|
1 | SIGHUP | 挂起信号,通常在终端关闭时发送给相关进程 | 终止进程 |
2 | SIGINT | 中断信号,由用户按下 Ctrl + C 组合键产生 | 终止进程 |
3 | SIGQUIT | 退出信号,由用户按下 Ctrl + \ 组合键产生 | 终止进程并生成核心转储文件 |
9 | SIGKILL | 强制终止信号,不能被捕获、阻塞或忽略 | 立即终止进程 |
15 | SIGTERM | 终止信号,通常用于正常终止进程 | 终止进程 |
18 | SIGCONT | 继续信号,用于恢复被暂停的进程 | 继续执行进程 |
19 | SIGSTOP | 停止信号,用于暂停进程,不能被捕获、阻塞或忽略 | 暂停进程 |
可以通过kill -l指令查看所有信号
kill -l
先来科普一个小知识点:前台进程和后台进程,来看下面一个程序
#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
#include <stdio.h>
int main() {
int *ptr = NULL;
*ptr = 10; // 试图向空指针指向的地址写入数据,会引发段错误
return 0;
}
编译运行这段代码,程序会崩溃,并提示 “Segmentation fault”,这是因为进程接收到了 SIGSEGV 信号。
#include <stdio.h>
int main()
{
int a = 10;
int b = 0;
int c = a / b; // 除零操作,会引发除零错误
return 0;
}
运行这段代码,程序会崩溃,并提示 “Floating point exception”,这是因为进程接收到了 SIGFPE 信号。
2. 系统资源相关:当系统资源达到一定阈值时,也可能产生信号。例如,当进程使用的内存超过了系统限制时,系统可能会发送 SIGKILL 信号来终止该进程,以防止系统内存耗尽。不过,这种情况通常需要系统进行相关的配置和监控。
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 信号并终止。
每个信号都有一个默认的处理方式,常见的默认处理方式包括:
进程可以通过调用 signal 函数或 sigaction 函数来设置自定义的信号处理函数。
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 系统中一种重要的进程间通信和异步事件通知机制。通过本文,我们详细了解了信号的概念,信号的产生和部分信号的处理工作,后面我们还会讲解信号的捕捉等处理工作,学习信号可以帮助我们更好的实现进程通信和异步处理等诸多操作
本篇笔记: