当前位置: 首页 > news >正文

Linux 操作系统 --- 信号

序言

 在本篇内容中,将为大家介绍在操作系统中的一个重要的机制 — 信号。大家可能感到疑惑,好像我在使用 Linux 的过程中并没有接触过信号,这是啥呀?其实我们经常遇到过,当我们运行的进程当进程尝试访问非法内存地址时,我们的进程会被中断,这是因为操作系统向该进程发送了中断信号。
Linux 操作系统离不开信号机制,在这篇文章中,让我们走进信号,了解信号的从哪里来,又到哪里去。


1. 信号的概念

1.1 定义

 信号是操作系统向进程发送的一种通知,表示某个特定事件已经发生。在Unix、类Unix 以及其他系统中,信号被广泛使用。

1.2 特点

 信号具有如下的特点:

  • 异步性:信号的产生对进程来说是异步的,即 进程无法预知信号何时到来
  • 通知机制:信号是一种 软件中断(由软件程序触发的中断方式),用于中断进程的正常执行流程,使其处理特定事件。
  • 进程间通信:虽然信号主要用于异常处理和系统调试,但也可以用于进程间的基本通信。

1.3 种类

 在 Linux 系统下,使用指令:kill -l 即可查看所有的信号:
在这里插入图片描述

信号是使用宏定义的,每个信号前面的数字,就是该信号宏对应的值。

前 31 个信号为常规信号,其余为实时信号。在本篇文章中,我们主要讨论前 31 个常规信号。

 你也可以使用指令: man 7 signal 查看每一个详细信息:
在这里插入图片描述

补充知识点:Core && Term

 在描述信号的字段中,有一个叫做 Action 的特征,他的值大多都是 Core Term 这是什么呢?

Term

termterminate 的缩写,表示默认动作是终止进程。当进程接收到一个默认动作为 term 的信号时,进程会被立即终止。

Core

core 表示 默认动作是终止进程并生成一个核心转储(core dump)文件。核心转储是一个包含进程在终止时的内存映像的文件,它对于调试程序非常有用,因为它 提供了进程终止时的状态信息
 咦?就比如,SIGSEGV 段错误 当我的程序非法访问被终结时,被没有产生传说中的核心转储文件呀?这是因为你的服务器默认关闭了该功能,使用指令 ulimit -a 查看:
在这里插入图片描述
现在我们使用指令 ulimit -c 4096 开启该功能:
在这里插入图片描述

现在我们运行下一段程序:

   8 int main()9 {10 11     int *ptr = NULL;12     *ptr = 1;13     14     return 0;15 }

程序不负众望地报错并退出了,产生了一个文件:
在这里插入图片描述
这个文件可以干嘛呢?当我们的程序出现异常时,相当该文件保存了案发现场,具体的用法是:

  • 首先使用 gdb 调试你的程序:
    在这里插入图片描述
  • 之后输入指令 core-file your_core
    在这里插入图片描述

可以看到,直接就复原了事故现场。

区别
  • 进程终止:term 信号会终止进程,但 不生成核心转储文件term 信号通常是用于请求进程正常终止的情况。
  • 调试信息:core 信号不仅会终止进程,还会 生成核心转储文件,这包含了进程的内存映像、寄存器状态、堆栈跟踪等信息,用于调试目的。

2. 信号的产生

 信号是从哪里产生的呢?虽然最后都是操作系统来执行对一个进程发送信号,但是是谁告诉操作系统这样做的呢?

2.1 用户操作 — kill 指令

 当我们运行一个程序时,可以通过指令 kill 来让操作系统对该进程发送相应的信号,就比如,我们可以手动发送 SIGKILL 9号 信号将该进程终结,这里有一个程序:

 1 TestSig1.cc                                                                                                                                                                                                  X 1 #include <iostream>2 #include <unistd.h>3 4 5 int main()6 {7     while(true)8     {9         std::cout << "I am Running, my pid is " << getpid() << std::endl;10         sleep(1);11     }12     return 0;13 }

该程序会每秒打印相应的内容,现在我们可以使用相关的指令 kill -9 [pid] 来杀掉该进程:
在这里插入图片描述
可以看到该进程被杀掉了!!!

2.2 用户操作 — 按键操作

 我们也可通过按键来让操作系统发送相关的信号,就比如我们平时终止一个进程的方式更多的是通过键盘按键,就比如 ctrl + c
在这里插入图片描述
其实这个按键对应的就是 3 号信号 SIGQUIT

2.3 用户操作 — 系统调用

 操作系统提供一个系统调用 int kill(pid_t pid, int sig); 该函数你可以想指定进程发送信号:

  • pid: 表示要发送信号的进程 ID
  • sig: 表示要发送的信号
  • 返回值:成功返回 0 ,失败返回 -1 ,错误码被设置

还有一个函数是 int raise(int sig);,该函数是向当前进程发送指定信号,简单来说,相当于简化的 kill => kill(getpid(), int sig);

2.4 触发软件条件

 在之前管道的学习中,我们了解到如果 读端被关闭了,写端一直再写,那么操作系统就会认为这是一个坏掉的管道,就会发送 13 号信号 SIGPIPE 终止该进程,这就是触发了某种软件条件。
 现在,在这里先向大家介绍几个非常重要的函数:

signal 函数

 该函数允许程序员定义当特定信号发生时,程序应该如何响应, 简单说,这个函数用于捕获特定信号,然后执行指定操作的sighandler_t signal(int signum, sighandler_t handler);:

  • signum:指定要处理的信号类型。注意,SIGKILLSIGSTOP 这两个信号不能被捕获、阻塞或忽略。
  • handler:指定信号的处理方式。它可以是一个函数指针,指向一个用户定义的信号处理函数;也可以是 SIG_IGN,表示忽略该信号;或者是 SIG_DFL,表示采用信号的默认处理方式。
  • 返回值:成功时,signal 函数返回之前为该信号设置的信号处理函数的指针。如果之前没有为该信号设置过处理函数,则返回 SIG_DFL。失败时,返回 SIG_ERR,并设置 errno 以指示错误原因。

看着描述这么多,其实用起来不复杂,比如,现在我要捕获 2 号信号 SIGINT,他的默认操作是退出,现在我不想要推出,想要执行我的逻辑:

 1 TestSig1.cc                                                                                       X 1 #include <iostream>2 3 #include <unistd.h>4 #include <sys/types.h>5 #include <signal.h>6 7 void signal_handle(int signum)8 {9     std::cout << "I got you signal: " << signum << std::endl;10 }11 12 int main()13 {14 15     // 2号信号的捕获16     signal(2, signal_handle);17 18     while(true)19     {20         std::cout << "I am Running, my pid is " << getpid() << std::endl;21         sleep(1);22     }23     return 0;24 }

现在我们使用 ctrl + c 已经不能终止该进程了:
在这里插入图片描述

你也可将 signal(2, signal_handle); 中的函数换成 SIG_IGN 这样就会忽略该信号。

现在我有个想法就是将全部信号都捕获,在写个死循环,是不是就没有人把我停下来了!!!我们能想到的,人家肯定也想到了,规定 9 号信号 SIGKILL 和 19 号信号 SIGSTOP 这两个信号不能被捕获、阻塞或忽略。保证系统的稳定性和管理员的控制权。

alarm 函数

 大家为了早起都设置过闹钟吧,闹钟的作用就是时间一到就提醒我们执行某件任务。在 Linux 中的闹钟 alarm 也是一样的,我们设置一个定时,当时间一到执行某项任务,unsigned int alarm(unsigned int seconds);

  • seconds:定时器应该等待的秒数。如果 seconds 是 0,则任何当前设置的定时器都会被取消(你可以同时设置多个闹钟),但不会发送 SIGALRM 信号。
  • 返回值:如果之前已经设置了定时器,alarm 函数 返回之前设置的剩余时间(秒),直到定时器到期。如果之前没有设置定时器,则返回 0。

alarm 定时器到期时,会向进程发送 SIGALRM 信号,终止进程:

12 int main()13 {14 15     // 设置一个闹钟,执行默认操作16     alarm(2);17 18     while(true)19     {20         std::cout << "I am Running, my pid is " << getpid() << std::endl;21         sleep(1);22     }23     return 0;24 }

两秒之后,进程自动终止:

 但更多情况下,我们想要闹钟解释后执行我们的逻辑,而不是终止进程,那咋办呢? 捕获该信号,自定义处理信号,这就需要我们上面说的 signal 函数了:

   7 void signal_handle(int signum)8 {9     std::cout << "Your alarm clock is ringing." << std::endl;10 }11 12 int main()13 {14 15     // 设置一个闹钟,执行默认操作16     alarm(2);17     // 捕获闹钟信号18     signal(SIGALRM, signal_handle);19 20     while(true)21     {22         std::cout << "I am Running, my pid is " << getpid() << std::endl;23         sleep(1);24     }25     return 0;26 }

现在闹钟时间到了,就不会终止进程啦!但是,我还有一个疑问,你这个闹钟只能执行一次呀,之后就失效了,我想要一个一直生效的定时任务,怎么做到呢?当捕获并执行自定义函数时再设置一个闹钟不就好啦:

   7 void signal_handle(int signum)8 {9     std::cout << "Your alarm clock is ringing." << std::endl;10     alarm(2);11 }

这样就得到一个持续的定时任务啦:
在这里插入图片描述

 在这里的闹钟就是一个触发了软件条件(倒计时),从而产生信号发送给进程!

2.4 硬件异常

段错误

 在我们的程序中,很可能涉及到 段错误(非法内存访问),具体触发错误的细节如下:

  • 现代计算机使用内存管理单元(MMU)来管理内存。MMU 负责 将虚拟地址(程序使用的地址)映射到物理地址(实际内存地址)
  • 当我们尝试访问一个地址时,MMU 尝试将虚拟地址翻译为物理地址,并检查该虚拟地址对应的页表项,以确定是否有权限访问该地址,以及地址是否有效
  • CPU 发现该块地址是 无效的,或者是不具有写权限的,或者是无权限访问的,将触发异常
  • 操作系统向该进程发送 SIGSEG 的信号

这就是简单的硬件异常触发流程。


3. 信号的保存

 现在我们已经基本了解了信号是从哪里来的,那么信号被一个进程接受过后,是以什么形式存在于进程当中呢?

 在介绍信号的保存之前,希望大家记住这几个概念:

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。

 信号的信息被保存在一个进程的 task_struct 中:
在这里插入图片描述
我们来好好的介绍这 3 个结构:

3.1 block 位图

Block 位图用于指示哪些信号当前被进程 阻塞。如果一个信号在 Block 位图中对应的位被设置(为 1 ),那么即使该信号已经到达,它也不会被立即处理,而是会 保持在未决状态,直到进程解除对该信号的阻塞。

3.2 pending 位图

Pending 位图(通常不是直接暴露给用户的,而是作为进程控制块 task_struct 的一部分)用于 跟踪哪些信号已经到达进程但尚未被处理。每个位代表一个信号,如果该位被设置(通常为 1 ),则表示对应的信号已经到达且处于 未决状态

3.3 handler 函数指针数组

 用户可以通过系统调用来设置特定信号的处理函数。当用户为某个信号注册了一个自定义的处理函数时,操作系统就会将该函数的地址存储在 handler 表中对应信号编号的位置。如果用户没有为某个信号设置自定义处理函数,那么当该信号发生时,操作系统就会 调用默认的处理函数

 所以,我们总结一下,当一个信号传递给进程时,操作系统会将 pending 表中该信号对应的值置 1,如果 block表中的对应的值 也是 1,代表该信号被阻塞,不会被立即处理,直至解除阻塞;当解除阻塞或者一开始就不是阻塞状态的话,就会执行 handle
表中该信号对应的函数操作。

3.4 验证结论

 现在我们准备验证我们的想法,我们先阻塞一个信号,然后发送该信号,查看是否执行相关操作,在解除对该信号的阻塞,再次观察现象:

在这里会涉及到对信号集的操作,大家可以简单理解为 对信号集进行的对某个信号的阻塞操作最终会保存到阻塞表中 ,在这里就不具体说明操作了,感兴趣的小伙伴,我找了一篇比较好的文章 👉信号集操作指南。

6 // 自定义函数7 void signal_handler(int signum)8 {9     std::cout << "Recived signal SIGINT!!!" << std::endl;10 }11 12 int main()13 {14     // 捕获信号15     signal(SIGINT, signal_handler);16 17     sigset_t sigset;18     // 初始化信号集19     sigemptyset(&sigset);20     // 添加指定信号到信号集21     sigaddset(&sigset, SIGINT);22 23     // 阻塞该信号24     if(sigprocmask(SIG_BLOCK, &sigset, NULL) == -1)25     {26         perror("sigprocmask");27     }28 29     std::cout << "SIGINT is blocked. Try pressing Ctrl+C after 5s!!!\n" << std::endl;30     sleep(5);31 32     // 解除阻塞33     if(sigprocmask(SIG_UNBLOCK, &sigset, NULL) == -1)34     {35         perror("sigprocmask");36     }37 38     std::cout << "SIGINT is unblocked. Try pressing Ctrl+C!!!\n" << std::endl;39 40     while(true)41     {42         sleep(1);43     }44 }

在这里插入图片描述

第一次我们按 ctrl + c 没什么反应,过了 5s 后,函数自动被执行,可以看出我们的结论是正确的。


4. 信号的处理

 现在我们知道信号哪里来的了,也知道保存在哪里了,现在我们来看看信号的处理方式。

4.1 执行默认方式

 对于没有为其注册信号处理函数的信号,进程会执行该信号的默认操作。就比如,SIGTERM 信号的默认操作是请求进程终止,而 SIGSEGV(段错误)信号的默认操作是生成core文件并终止进程。

4.2 调用信号处理函数

 如果进程为某个信号注册了信号处理函数(也称为信号处理器,上面代码中的 signal_handler 函数),那么当该信号到达时,内核会暂停进程的正常执行流程,转而调用该处理函数。

4.3 忽略信号

  进程可以选择忽略某些信号。这意味着当这些信号到达时,进程不会执行任何特别的操作,而是继续执行其当前的代码路径。然而,需要注意的是,并非所有信号都可以被忽略。例如,SIGKILL和SIGSTOP等信号是不能被忽略的

4.4 阻塞信号

 进程可以选择屏蔽某些信号,以 避免在关键操作期间接收到这些信号。通过调用sigprocmask 等系统调用,进程可以设置其信号屏蔽字,以决定哪些信号能够传递到进程中。被屏蔽的信号将保持在未决状态,直到屏蔽被解除后才会被处理。

4.5 阻塞和忽略的区别

 这两个概念相当容易混淆,从定义上来说:

  • 阻塞是指 进程选择性地阻止某些信号的传递。当这些被阻塞的信号发生时,它们会被内核记录下来(处于未决状态),但不会立即执行信号的处理函数或执行默认操作。
  • 忽略是指进程对收到的某些信号 不执行任何操作,即不调用处理函数也不执行默认操作,而是简单地丢弃这些信号。

大家可以这样理解:信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。 前者是处于未决的状态,后者是被递达后选择了忽略,不做其他处理。


总结

 在这篇文章中介绍了信号的概念,也介绍了信号从哪里来,到哪里去,被接受处理的过程,希望大家有所收获😁。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【项目】基于Vue2+Router+Vant 前端面经项目
  • 你知道Windows下的linux的神器WSL吗?
  • 【VRPCB】Python+Gurobi求解运输问题建模实践三
  • 景联文科技:图像标注的类型有哪些?
  • 【网络安全】SSO登录过程实现账户接管
  • 【网络通信】关于TCP和HTTP
  • 【MybatisPlus】创建Mapper接口继承BaseMapper接口
  • Linux 与 Windows 服务器操作系统 | 全面对比
  • 什么是DevUI?
  • 美股收涨,半导体板块领涨;苹果iPhone出货预测上调
  • 无人机之螺旋桨的安装与维护
  • 卫星图像检测,分割,跟踪,超分辨率,数据集调研
  • 今日(2024 年 8 月 13 日)科技新闻
  • 【网络】传输层TCP协议的报头和传输机制
  • 掌握NPM版本测试行为:策略、实践与示例
  • (三)从jvm层面了解线程的启动和停止
  • 「前端」从UglifyJSPlugin强制开启css压缩探究webpack插件运行机制
  • Android路由框架AnnoRouter:使用Java接口来定义路由跳转
  • ES6之路之模块详解
  • JavaScript创建对象的四种方式
  • Java基本数据类型之Number
  • JS专题之继承
  • k8s 面向应用开发者的基础命令
  • NSTimer学习笔记
  • PHP 7 修改了什么呢 -- 2
  • python学习笔记-类对象的信息
  • spring security oauth2 password授权模式
  • STAR法则
  • vue 个人积累(使用工具,组件)
  • 阿里云应用高可用服务公测发布
  • 读懂package.json -- 依赖管理
  • 微信开放平台全网发布【失败】的几点排查方法
  • 我从编程教室毕业
  • Hibernate主键生成策略及选择
  • 阿里云ACE认证学习知识点梳理
  • 阿里云ACE认证之理解CDN技术
  • #传输# #传输数据判断#
  • $$$$GB2312-80区位编码表$$$$
  • $.ajax,axios,fetch三种ajax请求的区别
  • (003)SlickEdit Unity的补全
  • (2022 CVPR) Unbiased Teacher v2
  • (CPU/GPU)粒子继承贴图颜色发射
  • (C语言)球球大作战
  • (TipsTricks)用客户端模板精简JavaScript代码
  • (保姆级教程)Mysql中索引、触发器、存储过程、存储函数的概念、作用,以及如何使用索引、存储过程,代码操作演示
  • (差分)胡桃爱原石
  • (二)Eureka服务搭建,服务注册,服务发现
  • (二十三)Flask之高频面试点
  • (二十一)devops持续集成开发——使用jenkins的Docker Pipeline插件完成docker项目的pipeline流水线发布
  • (附源码)spring boot校园拼车微信小程序 毕业设计 091617
  • (附源码)springboot 基于HTML5的个人网页的网站设计与实现 毕业设计 031623
  • (九)信息融合方式简介
  • (七)Flink Watermark
  • (转)MVC3 类型“System.Web.Mvc.ModelClientValidationRule”同时存在
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation