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

【Linux】————信号

 9efbcbc3d25747719da38c01b3fa9b4f.gif

                                                      作者主页:     作者主页

                                                      本篇博客专栏:Linux

                                                      创作时间 :2024年11月12日

9efbcbc3d25747719da38c01b3fa9b4f.gif

信号和信号量

首先说明这两者之间没有任何关系

信号:信号是在软件层次对中断机制的一种模拟,是一种异步通知机制,用于通知进程发生了某个特定的事件,例如当用按下Ctrl+c时,会产生一个SIGINT信号发送给当前正在运行的进程,通知他用户想要进行中断操作。信号既可以由操作系统内核发送给进程,也可以由进程发送给进程(需要一定的权限)

信号量:信号量是一种用于进程同步和互斥的机制,用于协调多个进程或者线程对共享资源的访问。它本质上就是一个计数器,用于控制同时访问共享资源的进程或线程的数量。信号量表示当前可用资源的数量。

信号

我们可以通过kill -l来查看所有的信号。

这些信号中,1-31为普通信号,34及以上为实时信号,这些信号都在什么条件下产生,默认的处理动作是什么,这些都在signal(7)中有着详细的说明man 7 signal

基本结论:信号就是Linux系统提供给用户用于向指定进程发送特定事件的方式

信号的产生和进程是异步的,即进程也不知道自己什么时候会接收到信号

信号是可以随时产生的,如果进程坐着别的事,也可以先不处理信号,等待合适的时机再处理信号

信号的处理

信号处理有三种情况:

  1. 默认动作
  2. 忽略动作
  3. 自定义处理——信号的捕捉

信号捕捉

signal

运行上面代码,在另一个终端上输入kill -2 指令,数字也可换成对应的宏名称。发现输出了hander函数的内容。

signal是用来进行信号捕捉的。参数1是信号的编号,参数2是函数指针。如果进程收到参数1对应的信号,就会执行参数2对应的方法。

我们也可以同时对多个信号进行捕捉:

信号的产生

信号产生的方式:

  1. 通过kill命令,向指定进程发送命令
  2. 键盘可以产生信号,Ctrl+c(SIGINT),Ctrl+\(SIGQUIT)
  3. 系统调用
  4. 软件条件
  5. 异常

系统调用

kill

参数1是指定进程,参数2是指定信号,作用是向指定进程发送指定信号

通过运行下面这个代码,得到这个进程的pid,然后通过我们自己写的Mykill来执行对应的信号,实现对应的操作

raise

raise作用就是谁调用这个函数,他就给调用者发送对应的信号

kill是给任意进程发送任意信号,如果想给自己发送信号,相当于killI(getpid(),signal);

 

这个就是我们通过raise不断给自己发送3号信号,然后signal捕捉执行

abort

执行上述代码之后,一秒之后我们的进程就结束了,这是为什么呢?

其实abort就相当于我们的6号信号,6号信号(SIGABRT)就是终止我们的进程,即使我们自定义捕捉了这个信号,也会执行一次自定义的函数之后结束,所以这个是比较特殊的一个

所以这里我们就可以回答一个问题,那就是如果我们自定义捕捉了所有的信号,那是不是就无法终止进程了

上面这个就是例外了,同样9号信号也无法自定义捕捉

由软件产生信号

SIGPIPE是一种由软件条件产生的信号。下面介绍alarm函数和SIGALRM信号。

alarm

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。 

如果我们稍微改一下代码,像这样

我们就可以发现这个值大多了,这是因为IO很慢

硬件异常产生信号

运行这段代码会直接崩溃,因为这里涉及到了除零操作,这是不允许的,还有就是野指针也会出现出错,除以0会发送8号信号(SIGFPE),野指针会发送11号信号(SIGSEGV)

此时我们如果将8号或者11号信号捕捉,就会死循环打印对应的东西

Core、Term

Core、Term都是终止进程的意思,那他们有啥区别呢?

Term:异常终止

Core:异常终止,但是它会帮我们形成一个类似debug文件。

默认下这个功能是关闭的,我们可以这样打开

指令 ulimit -a 可以查看系统中对于普通用户能使用资源对应的限制。下面可以看到core file size 大小是0,所以云服务器默认不允许我们形成core文件。

通过ulimit -c 数字 指令,这样core file选项就打开了。此时再运行程序,就有core文件了。

通过ulimit -c 数字 指令,这样core file选项就打开了。此时再运行程序,就有core文件了

Core文件就是进程退出时候的镜像数据,这个功能叫核心转储。

核心转储其实是进程异常时,核心数据转而存储到磁盘上。

所以上面图中,core dump标志位为0时表示没有核心转储,为1表示有核心转储。 

如果进程是Term就没有核心转储。如果是Core并且打开了核心转储功能,就有核心转储。

 我们把Makefile文件里g++带上-g选项,允许被调试。

当程序里面有除0错误时,并且有了core文件。我们gdb进行调试。 输入 core-file core 给gdb加载core文件,我们就可以直接定位到程序出错的地方。

所以core是协助我们进行debug的文件,这种操作也叫事后调试

 阻塞信号

信号相关其他常见概念

  • 实际执行信号的处理动作叫做信号递达 (Delivery)
  • 信号从产生到递达之间的状态叫做信号未决,即Pending状态
  • 进程可以选择阻塞(Block)某个信号,阻塞和有没有未决没有关系
  • 被阻塞的信号产生时将处于未决状态,知道进程接触对此信号的阻塞,才进行递达动作
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略实在递达之后可以选择的一种处理动作

在内核中表示

每个进程pcb中会维护三张表。

pending表就是未决表,他有32个比特位,使用其中31位,假设最左边的一位不用,从右往左数,第几个比特位就代表着第几个信号,为1就是处于未决状态,为0则不处于

handler表就代表着函数指针数组。handler表里面写的就是该表如何被处理,信号的标号就是数组的下标,因此我们之前的handler函数调用,如signal(2,handler),其实就是用2号编号在handler数组里面索引,把自己写的handler函数地址传进表里,这里系统就知道你要如何处理信号了,处理对应的信号时就通过地址找到对应的函数执行即可。

block表也是一张位图,和pending表类型一样,也只使用其中31位。

这三张表要横着,对应着编号看。

因此,两张位图+一张函数指针数组就可以让进程识别信号。 

sigset_t 

每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

sigset_t就是Linux给用户提供的一个用户级的数据类型,禁止用户直接修改位图。

 信号集操作函数

 sigset_t类型内部如何存储这些bit依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量

  • int sigemptyset(sigset_t *set);  把位图全部清空
  • int sigfillset(sigset_t *set);    把整个位图全部置1
  • int sigaddset (sigset_t *set, int signo);  把特定信号设置进该集合里。比如信号是5,就是把第五个bit位置1。
  • int sigdelset(sigset_t *set, int signo);  把特定位置置0,如果是1就置0,如果是0就不动
  • int sigismember(const sigset_t *set, int signo);  判断一个信号是否在集合当中

 sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。 

返回值:若成功则为0,若出错则为-1

set是输入型参数,oldset是输出型参数。

如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

 sigpending

sigpending的作用是获取当前进程的pending位图,它的参数是输出型参数。

调用成功则返回0,出错则返回-1。

捕捉信号

如果一个信号不做任何处理,它默认就是SIG_DFL选项。

SIG_IGN选项就是忽略一个信号。

 内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。 

信号捕捉的过程:要经历4次状态的切换。

在内核态切换回用户态的时候,进行信号的检测和处理。

 再谈地址空间

开机时,操作系统是最先加载的软件,所以OS也要在内存中。内核级页表是将内核地址空间和OS之间进行映射的。因此OS本身就在我的进程地址空间中。

如果有多个进程,不会再创建一个新的内核级页表,而是共用一张。

sigaction

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signum 是指定信号的编号。act是输入型参数,是结构体类型,结构如上图,这里只了解结构体里的第一行,即函数指针。所以act传的是函数指针。oldact是输出型参数,用来保存旧的结构体。

sigaction本质就是修改信号的handler表。  

sigaction跟前面的signal本质作用是一样的,都是对特定信号进行捕捉。

 volatile

运行上面代码,按下ctrl+c后,信号被捕捉,gflag就被修改了,while循环条件为假,程序就结束了。 

 Linux系统中g++是有各种优化级别的。

 默认优化级别是-O0,即没有优化。

可以通过gcc main.cc -01对gcc进行优化

优化后,发现按ctrl+c 程序不会结束。因为main执行流判定代码里没有对gflag进行修改,觉得不用每次都从内存拿数据,直接在第一次拿的时候,把gflag的值优化到寄存器里,从此之后,每次while检测只检测寄存器里的值。当收到信号后修改gflag的值,修改的是内存里的gflag,就导致寄存器隐藏了内存中的真实值。这是编译器过度优化导致的问题。

为了保持内存的可见性,就有了volatile关键字。

有了volatile修饰,就没有被优化的问题了。

volatile的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作 

SIGCHLD信号 

子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。 

 如果有多个子进程同时退出,此时会同一时间向父进程发送多个SIGCHLD信号。普通信号是用pending位图接收信号的,收到了多个SIGCHLD信号,但pending位图只会记录一次,导致最后只会回收一个子进程。所以waitpid等待时,外面需要套一层while循环,不断回收。

如果有的子进程退出,有的永远不退出,此时就要用非阻塞等待。否则就会阻塞在信号捕捉里,父进程永远做不了别的事情。

wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进 程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。 

要想不产生僵尸进程还有另外一种办法:父进程调用signal将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

系统默认的忽略动作和用户用signal函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可用。

如果不关心子进程的退出信息,不想产生僵尸进程,就可以用这样做。

最后:

十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:

1.一个冷知识:
屏蔽力是一个人最顶级的能力,任何消耗你的人和事,多看一眼都是你的不对。

2.你不用变得很外向,内向挺好的,但需要你发言的时候,一定要勇敢。
正所谓:君子可内敛不可懦弱,面不公可起而论之。

3.成年人的世界,只筛选,不教育。

4.自律不是6点起床,7点准时学习,而是不管别人怎么说怎么看,你也会坚持去做,绝不打乱自己的节奏,是一种自我的恒心。

5.你开始炫耀自己,往往都是灾难的开始,就像老子在《道德经》里写到:光而不耀,静水流深。

最后如果觉得我写的还不错,请不要忘记点赞✌,收藏✌,加关注✌哦(。・ω・。)

愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚菜鸟逐渐成为大佬。加油,为自己点赞!


http://www.mrgr.cn/news/71915.html

相关文章:

  • Python常用高阶函数:map()、filter()、reduce()
  • ElasticSearch学习篇18_《检索技术核心20讲》LevelDB设计思想
  • React 组件生命周期
  • Redis 性能优化 18招
  • docker 部署 kvm 图形化管理工具 WebVirtMgr
  • 提升性能测试效率与准确性:深入解析JMeter中的各类定时器
  • java数据结构与算法:栈
  • 用户,组管理命令
  • 高情商的人都在用的处事细节和技巧
  • 人工智能助手是否让程序员技能退化?
  • Java多线程进阶(锁策略)
  • python 由于系统缓冲区空间不足或队列已满,不能执行套接字上的操作
  • 政务数据治理专栏开搞!
  • 时间空间频域融合的Corssformer时间序列预测项目
  • Fortran安装(vscode+gcc+Python)
  • Django Form
  • JVM——类加载器、类加载器的分类
  • 专题十八_动态规划_斐波那契数列模型_路径问题_算法专题详细总结
  • 2024134读书笔记|《花间集》——云解有情花解语,山月不知心里事, 水风空落眼前花
  • SpringBoot如何集成WebSocket
  • RT-DETR融合NeurIPS[2022]Ghost Module v2模块
  • C#-命名空间
  • 【FFmpeg】FFmpeg 函数简介 ③ ( 编解码相关函数 | FFmpeg 源码地址 | FFmpeg 解码器相关 结构体 和 函数 )
  • (一)- DRM架构
  • 【364】基于springboot的高校科研信息管理系统
  • WSL2 中大模型环境一步到位!(wsl --update 不好使/wsl2安装/python环境)