[Linux]:信号(上)

news/2024/9/24 11:25:34/

img

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:Linux学习
贝蒂的主页:Betty’s blog

1. 信号的引入

1.1 信号的概念

Linux系统中,信号(Signal)是一种软件中断机制,用于通知进程发生了特定的事件。信号可以由系统内核、其他进程或者进程自身发送。

我们可以通过指令kill -l参考所有信号

信号的本质就是一个define定义的宏,其中131号信号是普通信号,3464号信号是实时信号,普通信号和实时信号各自都有31个。每一个信号与一个数字相对应,每个信号也都有特定的含义和默认的处理动作。例如,信号SIGINT(通常由用户按下ctrl + c产生)表示中断信号,默认情况下会导致进程终止。

其中需要注意的是:在Linux中,前台进程只能有一个,而后台进程可以为多个。一般而言,我们的bash进程作为我们的前台进程,而一旦我们执行一个可执行程序,这个可执行程序就会成为前台进程,而bash进程就会转为后台进程。但是我们如果在执行一个可执行程序时,在之后加一个&,此时的可执行程序就会由前台进程转换为后台进程。而前台进程与后台进程本质间区别就是前台进程可以从键盘获取数据,后台进程则不能。

比如我们运行一个后台进程,就无法通过ctrl + c终止进程,因为其无法从键盘读取数据。此时就只能通过kill指令直接杀死对应的进程。

1.2 信号的获取

我们可以通过指令man 7 signal查看信号的详细说明:

其中 TermCore 表示终止;Ign 标记忽略;Cont 表示继续;Stop 表示暂停。我们早在进程等待时就知道,waitwaitpid的参数status本质就是一个位图结构,其低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

画板

其中 core dump 标志就是用来区分 TermCore 的。云服务器的 Core dump 功能默认是关闭的,但我们可以通过指令ulimit -a 指令来查看当前系统的所有资源限制。

我们可以通过指令ulimit -c size,去设置它的大小,如果 size > 0 就表示开启 Core dump 功能。

其中Term对应的core dump标志位是 0,表示正常终止;Core对应的core dump标志位是 1,表示异常终止。我们可以在程序中,通过位运算status>>7&1来获取对应的core dump标志。

打开系统的core dump功能后,一旦进程出现异常,操作系统会将进程在内存中的运行信息转储到进程的当前目录中,形成core.pid文件,这一过程被称作核心转储。core.pid文件中详细记录了程序的异常原因,可以直接帮我们定位到出错行。

比如如下这段代码:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{int a = 10;int b = 0;a /= b;return 0;
}

既然core dump可以帮助我们定位错误信息,那么我们为什么要将其关闭呢?那是因为如果每次产生错误信息都形成core.pid文件的话,系统可能产生大量文件,而迫使操作系统挂掉,为了避免这种情况,一般而言我们并不建议开启该功能。

signal_73">1.3 signal函数

当一个信号被发送给一个进程时,进程可以采取以下几种方式来处理信号

  1. 忽略信号:进程可以选择忽略某些信号,即不对信号做出任何反应。但并不是所有信号都可以被忽略,例如 SIGKILL SIGSTOP 信号不能被忽略。
  2. 捕获信号:进程可以注册一个信号处理函数,当接收到特定信号时,就会执行这个函数。通过这种方式,进程可以在接收到信号时执行自定义的处理逻辑。
  3. 执行默认动作:如果进程没有显式地忽略或捕获信号,那么它将执行信号的默认动作。默认动作通常是终止进程、停止进程、继续进程等。

接下来我们介绍一个函数signal,其可以设置进程对某个信号的自定义捕捉方法:即当进程收到 signum 信号的时候,去执行 handler 方法。

  1. 函数原型:
  • typedef void (*sighandler_t)(int);
  • sighandler_t signal(int signum, sighandler_t handler);
  1. 参数:
  • signum:是一个整数,表示要处理的信号编号。
  • handler:是一个函数指针,指向一个信号处理函数。这个信号处理函数接受一个整数参数(即接收到的信号编号),并且没有返回值(void)。可以是以下几种值:
    • SIG_DFL:表示默认的信号处理动作。
    • SIG_IGN:表示忽略该信号
    • 自定义的信号处理函数指针,用于处理特定信号

例如,下面的代码中将2号信号进行了捕捉,当该进程运行起来后,若该进程收到了2号信号就会打印出对应的信号编码。

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handler(int sign)
{printf("get a signal :%d\n",sign);
}
int main()
{signal(2,handler);while(1){printf("hello world!\n");sleep(1);}return 0;
}

其中前台进程在运行过程中,用户随时可能按下Ctrl+C而产生一个信号,也就是说该进程的代码执行到任何地方都可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。

2. 信号的产生

在我们操作系统中,信号的产生方式有许多,总体归纳来说有四种。

2.1 终端按键

其中我们通过键盘快捷键直接向我们的进程发出信号的方式非常常见,其中较为我们常用的有:

组合键功能
Ctrl+C向进程发出SIGINT信号,终止进程。
Ctrl+\向进程发出SIGQUIT信号,终止进程。
Ctrl+Z向进程发送SIGTSTP信号,暂停进程的执行。

2.2 系统接口

我们也可以通过操作系统为我们提供的接口对进程发送对应的信号

其中较为常用的一个接口为kill,其具体用法如下:

  1. 函数原型:int kill(pid_t pid, int sig);
  2. 参数:pid对应要发送信号进程的pidsig表示发送的信号种类。
  3. 返回值:如果成功,返回值为 0。否则,返回值为 -1

例如:下面这段代码,我们可以对指定进程发送一个SIGKILL信号,正常终止该进程。

int main()
{int cnt = 0;while(1){printf("hello world!\n");sleep(1);++cnt;if(cnt == 5){kill(getpid(),SIGKILL);}}return 0;
}

接下来我们再来介绍两个接口:raiseabort

int raise(int sig);
void abort(void);

raise函数用于给当前进程发送sig信号,而abort函数相当于给当前进程发送SIGABRT信号,使当前进程异常终止。

abortexit函数同样是终止进程,它们之间有什么区别吗?

首先明确abort函数和exit函数的不同作用。abort函数的作用是异常终止进程,它本质上是通过向当前进程发送SIGABRT信号来实现这一目的。而exit函数的作用是正常终止进程。

需要注意的是,使用exit函数终止进程可能会失败,因为在某些复杂的程序运行环境中,可能存在一些因素干扰正常的进程终止流程。然而,使用abort函数终止进程通常被认为总是成功的,这是由于其通过发送特定信号强制终止进程,一般情况下进程很难忽略该信号而继续运行。

2.3 软件条件

在我们前面学习管道通信时,就知道如果进程将读端关闭,而写端进程还一直向管道写入数据,那么此时写端进程就会收到SIGPIPE信号进而被操作系统终止。SIGPIPE就是一种典型的因为软件异常而产生的信号

例如,下面代码,创建匿名管道进行父子进程之间的通信,其中父进程去读取数据,子进程去写入数据,但是一开始将父进程的读端关闭了,那么此时子进程在向管道写入数据时就会收到SIGPIPE信号,进而被终止。

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{int fd[2]={0};if(pipe(fd)<0){perror("pipe:");return 1;}pid_t id = fork();if(id ==0 ){//child -> writeclose(fd[0]);char*msg = "hello father, i am child...";while(1){write(fd[1],msg,strlen(msg));sleep(1);}close(fd[1]);exit(0);}// father -> readclose(fd[1]);close(fd[0]);int status = 0;waitpid(id,&status,0);printf("child get a signal :%d\n",status&0x7f);return 0;
}

我们能够通过alarm函数,设定一个闹钟,倒计时完毕向我们的进程发送SLGALRM信号,其具体用法如下:

  1. 函数原型:unsigned int alarm(unsigned int seconds);
  2. 参数:seconds表示倒计时的秒数。
  3. 返回值:如果调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置。如果调用alarm函数前,进程没有设置闹钟,则返回值为0。

例如下面这段代码,我们首先对SLGALRM信号进行捕捉,并给出我们的自定义方法,然后5秒后调用alarm函数。

#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
void handler(int sign)
{printf("get a signal:%d\n",sign);exit(1);
}
int main()
{signal(SIGALRM,handler);alarm(5);while(1){printf("hello wrold!\n");sleep(1);}return 0;
}

2.4 硬件异常

当程序出现除 0、野指针、越界等错误时,程序会崩溃,本质是进程在运行中收到操作系统发来的信号而被终止。 这些发送的信号都是由硬件异常产生的。

比如下面这段代码,进行了对空指针的解引用,那么其到底是如何被操作系统识别的呢?

#include<stdio.h>
int main()
{int *p = NULL;*p = 3;//对空指针解引用。return 0;
}

首先我们知道,当我们要访问一个变量时,进程控制块task_struct一定要会经过页表的映射,将虚拟地址转换成物理地址,然后才能进行相应的访问操作。

画板

而页表属于一种软件映射关系,在从虚拟地址到物理地址映射过程中,有一个硬件单元叫做 MMU(内存管理单元),它是负责处理 CPU 的内存访问请求的计算机硬件。如今,MMU 已集成到 CPU 当中。虽然映射工作原本不是由 CPU 做而是由 MMU做,但现在其与 CPU 的紧密结合使得整个内存访问过程更加高效。

当进行虚拟地址到物理地址的映射时,先将页表左侧的虚拟地址提供给 MMUMMU会计算出对应的物理地址,随后通过这个物理地址进行相应的访问。

由于 MMU 是硬件单元,所以它有相应的状态信息。当要访问不属于我们的虚拟地址时,MMU 在进行虚拟地址到物理地址的转换时会出现错误,并将对应的错误写入到自己的状态信息当中。此时,硬件异常,硬件上的信息会立马被操作系统识别到,进而向对应进程发送 SIGSEGV 信号


http://www.ppmy.cn/news/1529793.html

相关文章

文件上传js代码

大家好&#xff0c;很久没更新了&#xff0c;今天空了&#xff0c;记录一下文件上传js代码。(自己搭建的网站&#xff0c;演示学习一下这种漏洞&#xff0c;不要做违法的事情&#xff01;&#xff01;&#xff01;) 一般文件上传的话都是奔着getshell去的&#xff0c;但是一般…

02【Matlab系统辨识】白噪声

1.白噪声与有色噪声 1.1 白噪声(white noise) 系统辨识中所用到的数据通常都含有噪声。从工程实际出发&#xff0c;这种噪声往往可以视为具有有理谱密度的平稳随机过程。白噪声是一种最简单的随机过程&#xff0c;是由一系列不相关的随机变量组成的理想化随机过程。白噪声的数…

ubuntu中通过源码安装pointnet2_ops_lib

注&#xff1a;本帖所用环境为&#xff1a;ubuntu 24.04、 cuda 12.04 文章目录 1. 克隆 PointNet 源码库2. 安装依赖3. 编译 pointnet2_ops_lib4. 测试安装 1. 克隆 PointNet 源码库 首先&#xff0c;克隆 PointNet 的 GitHub 仓库&#xff1a; git clone https://github.co…

设计原则模式概览

核心 分清楚哪些是稳定的&#xff0c;哪些是变化的&#xff08;一定有稳定跟变化的成分&#xff09;&#xff1b; 捋清楚哪些是类设计者的责任&#xff0c;哪些是使用者的责任。管理变化&#xff0c;提高复用&#xff01; 违背原则的代价 重新编译&#xff0c;重新测试&#xf…

高维空间的维数灾难问题

高维空间的维数灾难问题是指在处理高维数据时&#xff0c;随着维度的增加&#xff0c;数据的性质发生了显著变化&#xff0c;从而导致许多传统的机器学习和统计方法失效的现象。 主要问题 数据稀疏性&#xff1a; 在高维空间中&#xff0c;数据点之间的距离会变得相对较远&…

51单片机 - DS18B20实验1-读取温度

上来一张图&#xff0c;明确思路&#xff0c;程序整体裤架如下&#xff0c;通过单总线&#xff0c;单独封装一个.c文件用于单总线的操作&#xff0c;其实&#xff0c;我们可以把点c文件看成一个类操作&#xff0c;其属性就是我们面向对象的函数&#xff0c;也叫方法&#xff0c…

滚雪球学SpringCloud[9.1讲]:Docker与容器化详解

全文目录&#xff1a; 前言9.1 Docker与容器化Docker的基本概念与Spring Boot应用的容器化1. Docker的核心概念2. 将Spring Boot应用容器化 Docker Compose与微服务编排1. Docker Compose的核心概念2. 使用Docker Compose编排微服务 使用Kubernetes部署Spring Cloud应用1. Kube…

01.前端面试题之ts:说说如何在Vue项目中应用TypeScript?

文章目录 一、前言二、使用Componentcomputed、data、methodspropswatchemit 三 、总结 一、前言 与link类似 在VUE项目中应用typescript&#xff0c;我们需要引入一个库vue-property-decorator&#xff0c; 其是基于vue-class-component库而来&#xff0c;这个库vue官方推出…