[Linux]信号

news/2025/2/28 15:12:24/文章来源:https://www.cnblogs.com/wzhiheng/p/18601204

信号

认识信号

什么是信号

信号本质上是一种软件中断,用于通知进程发生了特定的事件。进程接收到信号后,会根据信号的类型采取相应的操作。

拿生活中的红绿灯来举例,当你看到红灯的时候你不会过马路,当变为绿灯时才会通过。但是也有可能在等红灯的时候,此时绿灯亮了而你正在打游戏,游戏正处于决胜时刻,这时候你不会选择立即过马路,而是等这局结束再通过。也就是说绿灯亮了就过马路这个行为并不是立即就要执行,而是会在一个合适的时候去执行这个动作。在接收到绿灯这个信号,和执行过马路动作这个期间就有一个时间窗口,在这段时间内你并没有过马路,但是你知道你已经可以通过了。这本质就是你“记住了绿灯已经亮了”。当游戏结束后,你就可以处理绿灯这个信号,这时候我们可以有三种处理方式:1. 默认(通过马路)。2. 忽略(游戏输了很生气,继续开一把游戏)。3. 自定义(来一段舞蹈)。

在进程中,对于信号的处理方式是一样的。当信号来的时候,进程可能在执行更重要的代码,对这个信号不一定会立即处理,但是会在自己的pcb中保存这个信号,等到合适的时候处理这个信号。

信号的分类

信号分为普通信号和实时信号,我们主要研究的是普通信号。在Linux中[1,31]号信号是普通信号,[34,64]号是实时信号。在命令行中使用kill -l命令查看。

信号产生

在理解信号产生之前,我们先来看一个系统调用接口signal(),它用于捕捉信号,设置信号处理方式。

当然,并不是所有信号都能被捕捉。比如说9号信号(SIGKILL),SIGKILL 信号的主要设计目的是用于无条件地终止一个进程。若允许进程捕捉 SIGKILL 信号,一个恶意进程或者出现错误的进程可以在接收到 SIGKILL 信号后,通过自定义的信号处理程序来阻止自身被终止,从而继续占用系统资源或执行一些非法操作。不允许捕捉 SIGKILL 信号,可以确保系统始终保留对进程生死的最终控制权。

函数原型:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数:

  • signum:指定要设置处理方式的信号编号。
  • handler:指定信号的处理方式,它是一个函数指针,指向一个具有 void (*)(int) 类型的函数,即该函数接受一个 int 类型的参数(通常就是信号编号 signum),并且没有返回值。有以下几种取值方式:
    • 自定义处理函数:当传入一个自定义的函数指针时,进程在接收到指定的信号 signum 后,将调用这个自定义的函数来处理信号。
    • SIG_IGN:表示忽略指定的信号。不过,有一些信号是不能被忽略的,如 SIGKILLSIGSTOP
    • SIG_DFL:表示采用信号的默认处理方式。

返回值:成功返回一个函数指针,指向之前该信号的处理函数;失败返回SIG_ERR

信号通常可以通过以下四种方式产生。

键盘发送

在命令行中,我们通常使用ctrl + c这种快捷方式结束进程。操作系统将我们这个操作识别为2号信号,从而帮我们执行对应的操作。

void handler(int signo)
{std::cout << "获取到一个信号,编号是: " << signo << std::endl;
}int main()
{pid_t pid = getpid();signal(2, handler);//捕捉2号信号,并重新设置信号的处理方法while (true){std::cout << "我是一个进程,pid是: " << pid << std::endl;sleep(1);}return 0;
}

在这个例子中,我们对2号信号捕捉,当我们使用ctrl+c这个快捷键时,他就会执行我们设定的方法。

系统调用

  1. kill()

    函数原型:int kill(pid_t pid, int signum);,表示给任意进程发送任意信号,成功返回0,失败返回-1。

  2. raise()

    函数原型:int raise(int signum);,给自己发送任意信号,成功返回0,失败返回非零值。

  3. abort()

    函数原型:void abort();,给自己发送SIGABRT信号。

从上面不难发现,通过kill()这个函数就能实现raise()abort()

硬件异常

只是举例子,不代表只有这几个。

  1. 除0错误

    int main()
    {int a = 10;a /= 0;return 0;
    }
    

    当我们运行上面的程序的时候,会报出Floating point exception,这其实就是八号信号。如何证明呢?我们设置对应的捕捉方法,然后再运行,如下:

    void handler(int signo)
    {std::cout << "获取到一个信号,编号是: " << signo << std::endl;
    }int main()
    {signal(8, handler);int a = 10;a /= 0;return 0;
    }
    

    到这里又有一个疑问了,设置了自定义的处理方法后,为什么会疯狂的进行输出呢?明明我只执行了一次除零的动作啊。这是因为CPU中有一套寄存器(寄存器中的内容属于当前进程的上下文),其中有一个叫状态寄存器,当发生除零错误的时候,状态寄存器的溢出标志位将会改变(假设是由0变为1)。而我们并没有对这个改变做出修正,每当进程发生切换的时候,就有无数次状态寄存器被保存和回复的过程,所以每一次恢复的时候,就让操作系统识别到了CPU内部的状态寄存器中的溢出标志位是1,所以才会不断地输出。

  2. 野指针

    同样会被操作系统发送信号终止,对应的信号是11号信号。

软件条件

  1. 管道读端关闭

    当读端关闭的时候,操作系统会发信号(SIGPIPE)关闭写端。(管道链接)

  2. 定时器

    可以通过alarm()系统调用来给进程设置定时器,当时间到了之后会向调用进程发送SIGALRM信号。

    函数原型:unsigned int alarm(unsigned int seconds);

    int main()
    {pid_t pid = getpid();alarm(5);//5秒后发送信号while (true){std::cout << "我是一个进程,pid是: " << pid << std::endl;sleep(1);}
    }
    

信号保存

一个信号是发给进程的,那么在进程的pcbtask_struct{};结构体)中一定保存了该信号。那么它是如何保存的呢?在task_struct中有两个位图和一个函数指针数组,通过这三个结构就能很好的对信号保存和处理。如下图:

信号处理

相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到抵达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

进程如何执行内核级的代码

在进程地址空间(Linux]进程地址空间 - 羡鱼OvO - 博客园)中说过,每个进程都有一个虚拟的地址空间,这个空间被划分为不同的区域,其中3~4G就属于内核空间。用户空间通过用户级页表映射找到对应的物理内存,这张页表是独立的,每个进程都有属于自己的用户级页表;而内核空间同样是通过页表映射的方式找到对应的物理内存,但是这张内核级页表是共享的,只有一张。

在CPU的寄存器中有一个叫做CR3的寄存器,它表征的是当前进程的运行级别。当进程在执行自己的代码的时候,此时处于用户态;一旦进程遇到了系统调用接口,此时的状态就被切换为内核态。而又由于每个进程都有一个虚拟地址空间,当进程切换为内核态执行内核级别的代码的时候,其实只需要在自己的地址空间上进行跳转就可以了。

信号捕捉流程

  1. 在执行主控制流程的某条指令时因为中断,异常或者系统调用进入内核。
  2. 在内核处理完异常准备回到用户模式之前,会先处理当前进程中可以递达的信号。
  3. 如果处理信号的函数是自定义的,则回到用户态执行对应的信号处理函数。
  4. 信号处理函数返回时执行特殊的系统调用再次进入内核。
  5. 从内核态再次返回到用户态,这次是从主控流程被中断的地方开始继续向下执行。

在第三步中,执行完信号处理函数(sighandler)后,既然已经是用户态了,为什么不直接跳到主控制流程(main)中,继续向下运行,而是还要转到内核态,然后再从内核态返回用户态呢?因为sighandlermain使用不同的内核空间,他们之间不存在调用和被调用的关系,是两个独立的控制流程。当sighanlder函数执行完毕后,它不知道此时main函数执行到哪了,所以不能直接从sighanlder函数跳到main函数。

信号集

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

信号集处理函数

  1. int sigemptyset(sigset_t *set);

    用来初始化set所指向的信号集,使其中所有信号对应的比特位置0,表示该信号集不包含任何有效信号。

  2. int sigfillset(sigset_t *set);

    初始化set所指向的信号集,使其中所有信号对应的比特位置1,表示该信号集包括系统支持的所有信号。

  3. int sigaddset(sigset_t *set, int signo);

    添加某种信号到信号集中。

  4. int sigdelset(sigset_t *set, int signo);

    从信号集中删除指定的信号。

  5. int sigismember(const sigset_t *set, int signo);

    判断信号集中是否包含某种信号。

  6. int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

    用于检查和修改进程对应的信号屏蔽字(block信号集)。

    • how:指定信号集的操作方式
      • SIG_BLOCK:将set中的信号添加到当前被阻塞的信号集中。就是将set中的信号和当前阻塞信号集进行 “或” 操作,使这些信号也被阻塞。
      • SIG_UNBLOCK:将set中的信号从当前被阻塞的信号集中移除。
      • SIG_SETMASK:将当前被阻塞的信号集设置为set中的信号。
    • oldset:一个指向sigset_t类型的指针。如果oldsetNULL,函数会将进程当前的信号掩码存储到oldset所指向的信号集中,以便后续恢复或查看之前的信号掩码状态。

    如果调用sigprocmask()解除了对当前若干个未决信号的阻塞,则在sigprocmask()返回前至少将一个信号递达。

  7. int sigpending(sigset_t *set);

    获取当前处于未决状态的信号集合。

简单的使用示例:

#include <iostream>
#include <signal.h>
#include <vector>
#include <unistd.h>#define MAX_SIGNUM 31static std::vector<int> sigarr = {2, 3};static void show_pending(const sigset_t &pending)
{for (int signo = MAX_SIGNUM; signo > 0; signo--){//判断指定信号在不在信号集中if (sigismember(&pending, signo)) std::cout << "1";else std::cout << "0";}std::cout << std::endl;
}int main()
{sigset_t block, oblock, pending;//初始化sigemptyset(&block);sigemptyset(&oblock);sigemptyset(&pending);//添加要屏蔽的信号for(const auto &sig : sigarr) sigaddset(&block, sig);//将屏蔽的信号设置进信号屏蔽字中sigprocmask(SIG_SETMASK, &block, &oblock);while (true){//获取处于未决状态的信号集sigpending(&pending);//进行打印输出show_pending(pending);sleep(1);}
}

Core Dump

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。它们都是使一个进程退出,那有什么不一样的地方呢。

首先我们先来认识一下什么是Core Dump。

Core Dump(核心转储)是指在程序异常终止(如由于段错误、非法指令等原因)时,操作系统将进程当时的内存状态(包括程序代码、数据段、栈等)保存到磁盘文件中的操作。这个文件被称为核心转储文件,通常命名为 “core” 或类似的名称。这个文件可以帮助定位导致程序崩溃的原因,例如访问了非法内存地址、栈溢出、内存泄漏等问题。

在生成Core Dump文件之前,要确保Core Dump功能已打开。使用ulimit -a查看用户资源限制,若选项为0,使用ulimit -c [非0值或unlimited]打开这个选项。

下面是一个数组越界使用gdb调试后的结果。(形成的Core Dump文件的后缀是引起core问题的进程的pid

这就是两种退出方式不一样的地方,使用Term的是直接退出,而Core模式结束会生成Core Dump文件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/851119.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通过 sealos 部署 k8s 集群

sealos 官方文档 release 背景 sealos 是用于一键部署 k8s 集群的工具。对于不想被太多操作系统细节配置困扰,又想快速装好一套测试k8s集群的同学,无疑是很方便的 网上教程很多,这边自己简单写下在公司机器上安装的过程,绕了一部分坑,帮助大家更快地使用 官方文档还说明 s…

Elasticsearch 系列(八)- 使用NSSM将Kibana安装为Windows服务

本章将和大家分享如何使用NSSM将Kibana安装为Windows服务。本章将和大家分享如何使用NSSM将Kibana安装为Windows服务。废话不多说,下面我们直接进入主题。 一、下载并安装Node.js 我们的 Kibana 是用 Node.js 写的,所以在安装 Kibana 之前我们首先需要先安装一下 Node.js 。 …

SAP配置网络打印机

1、网络打印机 在项目中需要用打印机打印表单,如果不配置网络打印机,通常需要通过连接特定的计算机(如打印服务器)来进行打印,或者使用本地连接的打印机,这样每台计算机可能需要单独配置打印机或安装驱动,导致资源浪费,也不方便。 如果配置了网络打印机,无需本地连接特…

【Java开发】SLF4J 桥接器及其原理,让你的旧代码也可以起飞

前言 虽然在新的项目中,我们一般使用推荐的SLF4J + 日志实现框架(Logback等)组合方式,但是对于一些旧的项目,已经使用了SLF4J之外的日志框架(如Log4j 1.x等),而且这些旧的代码我们无法直接修改源码,如果我们想使用SLF4J的API,那么就需要使用各种SLF4J的桥接器来实现。…

41. css溢出、定位、z-index属性

1. 溢出属性 1.1 概念 内容超出了标签的最大范围 overflow的值与描述:visible 默认值。内容不会被修剪,会呈现在元素框之外。hidden 内容会被修剪,并且其余内容是不可见的。scroll 无论内容是否超出范围,都会显示滚动条。auto 内容没有超出范围,不会显示滚动条。 内容超出…

成员推理攻击的防御方法及简单实现

defense_methods 2024年12月14日更新 前面我们实现了多种模型的成员推理攻击,接下来我们将针对其防御方法及其原理进行一个简单的介绍,并进行简单的代码实现,给用户提供一个详细的帮助文档。 目录 基本介绍 常用方法介绍 简单实现 基本介绍 机器学习中的成员推理攻击指的是攻…

【Java开发】SLF4J 门面日志框架原理分析

SLF4J的门面设计模式 SLF4J(Simple Logging Facade for Java)是一套日志接口,它提供了一种一致的API来使用不同的日志框架,如java.util.logging(JUL)、Logback、Log4j、Log4j 2等。SLF4J的设计基于门面(Facade)设计模式,这种设计模式为子系统中的一组接口提供一个统一…

活动报名:Voice Agent 开发者分享会丨RTE Meetup

引入 voice agent 的口语学习应用 Speak 估值已达 10 亿美元 Voice Agent 开发者分享会 一同探索语音驱动的下一代人机交互界面,一场 voice agent builder 的小规模深度交流会。RTE Meetup 迎来第六期!12 月 15 日(周日)上午,线上举办。本次活动将 聚焦 voice agent 领域 …

智慧灌区系统平台建设方案

在现代农业发展中,水资源的高效利用是提升农业产量和质量的关键。智慧灌区系统平台的建设,正是为了实现这一目标。该平台通过集成测绘地理信息与遥感技术,对灌区进行全方位的监测和管理,以提高水资源的利用效率和农业的可持续发展。一、智慧灌区系统平台的建设背景随着全球…

Go支付中台方案:多平台兼容与多项目对接

Go支付中台方案:多平台兼容与多项目对接 原创 就业陪跑训练营 王中阳2024年12月10日 09:02 湖南 6人欢迎点击下方👇关注我,记得星标哟~ 文末会有重磅福利赠送王中阳 专注程序员的就业辅导、简历优化、学习路线规划。私信我666,免费发你价值999元的学习资料。 344篇原创内容…

转载:【AI系统】推理参数

本文将介绍 AI 模型网络参数方面的一些基本概念,以及硬件相关的性能指标,为后面让大家更了解模型轻量化做初步准备。值得让人思考的是,随着深度学习的发展,神经网络被广泛应用于各种领域,模型性能的提高同时也引入了巨大的参数量和计算量(如下图右所示),一般来说模型参…

转载:【AI系统】推理引擎示例:AscendCL

AscendCL 作为华为 Ascend 系列 AI 处理器的软件开发框架,为用户提供了强大的编程支持。通过 AscendCL,开发者可以更加高效地进行 AI 应用的开发和优化,从而加速 AI 技术在各个领域的应用和落地。AscendCL 的易用性和高效性,使得它成为开发 AI 应用的重要工具之一。 本文将…