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

Linux 信号

目录

1. 基本概念

2. 几种产生方式 

3. Core Dump

4. 内核结构和操作接口

5.  捕捉的原理

6. SIGCHLD (17)信号 


1. 基本概念

  信号是进程之间 事件异步通知的一种方式。 

  在目标进程运行时,操作系统(OS)可以将特定信号 “传递” 给该进程,执行对应的动作

  查看Linux系统中有那些信号: kill -l  【宏】  

  发现:没有 0, 32, 33号信号 

(本文的介绍专注于 1-31 号信号类型的学习)

  执行对应的动作 即 怎么处理:

  1. 默认行为 (OS制定)详情可使用 man 7 signal 阅览(大多数为终止这个进程)

  2. 提供一个信号处理函数 --- 这种方式称为 捕捉(Catch)一个信号 【要求内核在处理该信号时切换到用户态执行这个处理函数(后面详细介绍)】

  3. 忽略此信号

  系统调用接口:

  或者使用 更加灵活的 sigaction ,但使用的细致和复杂度也随之增加

  注意:SIGKILL(9) 和 SIGSTOP(19) 不能捕捉!  

2. 几种产生方式 

  1. 键盘

  Ctrl + c 产生的信号【SIGINT(2)】只能发给前台进程。一般的程序代码不做特殊处理,在Shell启动的都是前台进程;一个命令后面加个&可以放到后台运行,这样Shell不必等待进程 结束就可以接受新的命令,启动新的进程。

  Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像这样的信号,比如还有 Ctrl + \  表示【SIGQUIT (3)】,Ctrl + z 表示【SIGSTOP(19)】

  前台进程在运行过程中用户随时可能按下这些快捷键 或者 后面提到的方式, 这个进程都能收到指定信号, 并在用户空间代码执行对应的动作所以信号相对于进程的控制流程来说是异步 (Asynchronous)的。 

  2. 调用函数接口 

  3. 软件条件 

  通常表示程序运行中的异常 或 需要特定响应的事件。 

  举例:

  1. 管道的同步机制:读端关闭,写端尝试写入 已经没有意义,OS发送 SIGPIPE(13)终止写端

  2. “闹钟” 

#include <unistd.h>

       unsigned int alarm(unsigned int seconds);

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

  它的返回值为0 或者 前一个闹钟剩余的秒数

  当设置参数 seconds == 0时 表示 取消该闹钟 

  4. 硬件异常产生信号 

  硬件异常以某种方式可以被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。

  例如,当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为 SIGFEP(8)信号发送给进程,默认终止进程。

  再比如,当前进程访问了非法内存地址(越界,空指针/野指针的解引用, ...), MMU会产生异常,内核将这个异常解释 成 SIGSEGV(11)号信号发送给进程,默认终止进程。

  . . . . . .

3. Core Dump

  当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上, 文件名通常是core (不同的系统可能不同), 这叫做Core Dump。

  进程异常终止通常是因为有Bug, 比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)

  一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。

  默认是不允许产生core文件的, 因为core文件中可能包含用户密码等敏感信息, 不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许 产生core文件。

  首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024KB:

  ulimit -c 查看 core file size 大小

  修改: ulimit -c 1024

  然后尝试写一个异常代码,或者给它发送信号比如 SIGFEP 

  运行失败后,就可以看到对应的 core文件 

  使用以下命令启动 GDB 并加载可执行文件和 core 文件:

db your_program(崩溃的可执行文件) core

   查看程序崩溃位置:

(gdb) bt

  更多gdb的调试操作可点击前往小编的另一篇文章《Linux基础开发工具》查看。

4. 内核结构和操作接口

  首先,对上面提到的内容做总结表述

  1. 实际执行信号的处理动作称为信号递达(Delivery) 

  2. 信号从产生到递达之间的状态称为信号未决(Pending) 

  3. 进程可以选择阻塞(Block)某个信号,即屏蔽:可以收到,但不 递达

  4. 被阻塞的信号将始终保持在未决状态,直到进程解除对此信号的阻塞, 才递达

  需要注意的是:忽略 也是递达的方式之一,和阻塞不是同一个概念!

  信号在内核中的表示示意图:

   开头所说,操作系统(OS)可以将特定信号 “传递” 给某个进程 ,其 “传递” 的意思是:OS修改该进程对应的内核数据结构(如上),记录信号状态。

  每个信号有两个标志位分别表示 阻塞(Block)和 未决(Pending),还有一个函数指针 指向 递达动作。

  收到信号时,内核在进程控制块中设置该信号的未决标志,直到信号递达才修改该标志。

  从上图来看, 每个信号只有一个bit的未决标志,非0即1, 不记录该信号产生了多少次, 阻塞标志也是这样表示的。

  因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储, sigset_t 称为信号集, 这个类型可以表示每个信号 的“有效”“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中 “有 效”和“无效”的含义是该信号是否处于未决状态。

  阻塞信号集也叫做当 前进程的信号屏蔽字(Signal Mask)。

  至于这个类型内部如何存储这些bit则依赖于系统实现, 从使用者的角度是不必关心的, 使用者只能调用以下函数来操作sigset_ t变量, 而不应该对它的内部数据做任何解释, 比如用printf直接打印sigset_t变量是没有意义的:

#include <signal.h>
 int sigemptyset(sigset_t *set);
 int sigfillset(sigset_t *set);
 int sigaddset (sigset_t *set, int signo);
 int sigdelset(sigset_t *set, int signo);
 int sigismember(const sigset_t *set, int signo);

  函数 sigemptyset 初始化set所指向的信号集, 使其中所有信号的对应bit清零, 表示该信号集不包含 任何有 效信号。

  函数 sigfillset 初始化set所指向的信号集, 使其中所有信号的对应bit为1,表示该信号集的有效信号包括系统支持的所有信号。

  注意, 在使用sigset_ t类型的变量之前, 一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

  这四个函数都是成功返回0,出错返回-1。

  sigismember是一个布尔函数, 用于判断一个信号集的有效信号中是否包含某种信号, 若包含则返回1, 不包含则返回0, 出错返回-1。

  对 sigset_t 类型的变量做好设置后,就可以用下面的两个函数接口,把 它数据 修改到进程内核结构中,影响进行的执行:

  1. sigprocmask

#include <signal.h>
 int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

  如果oset是非空指针, 则读取进程的当前信号屏蔽字通过oset参数传出。

  如果set是非空指针, 则更改进程的信号屏蔽字, 参数how指示如何更改:

  SIG_BLOCK :当前集合和set参数的并集

  SIG_UNBLOCK :将 set 信号集中的信号从当前进程的信号屏蔽字中移除  

  SIG_SETMASK :重新设置当前进程的信号屏蔽字为 set 

  2. sigpending 

#include <signal.h>

    int sigpending(sigset_t *set);

  读取当前进程的未决信号集,通过set参数传出; 调用成功则返回0,出错则返回-1。 

5.  捕捉的原理

  到这,我们还有最后一个问题没有解决:信号的处理(递达)时机?

  —— 通常是内核态 到 用户态的切换过程中

  关于什么是 内核态,什么是用户态,简单来说就是:

  用户态执行受限的操作,访问受限的资源

  内核态可执行所以操作,访问所有资源

  举个例子:

  比如涉及到:read(), write(), wait() 等操作;进程调度切换时,重新获得CPU,进行上下文切换时;...... 都会导致进程进入内核态

   因为硬件上有相关机制来标识和支持两种状态,通常是寄存器,比如序状态字寄存器(PSW)或状态寄存器(SR)等,其中的某些位可以用来表示当前处理器处于用户态还是内核态。

  至于为什么这样做的根本原因就是:OS作为软硬件资源的管理者,负责整个计算机的安全和稳定,用户的一切操作都不能跨过它,特别是涉及到底层文件数据的敏感性操作,软硬件需要统筹提供一些安全策略和机制。 

  下面是一张过程示意图:

  这里需要特别解释的是:信号的产生是异步的,它可以在进程执行的任何时刻发生,与进程当前正在执行的代码路径(如main函数中的代码)没有直接的顺序关系。 也就是说 sighandlermain 函数使用不同的堆栈空间, 它们之间不存在调用和被调用的关系, 是两个独立的控制流程,所以:递达动作结束后,还要返回内核态,读取内核保存的当前进程上下文数据,保证返回用户态后从正确的位置继续。

  至于是怎么做到的,你还记得下面的 地址空间图吗:

6. SIGCHLD (17)信号 

  用wait和waitpid函数清理僵尸进程, 父进程可以阻塞等待子进程结束, 也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。

  采用第一种方式, 父进程阻塞了就不能处理自己的工作了; 采用第二种方式, 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。

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

  但是事实上, 由于UNIX 的历史原因, 要想不产生僵尸进程还有另外一种办法: 父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN, 这样fork出来的子进程在终止时会自动清理掉, 不会产生僵尸进程,也不会通知父进程。

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

 

  本篇分享到这就结束了,如果对你有所帮助,就是对小编最大的鼓励,可以的话,点赞+收藏并分享给你的小伙伴一起学习吧!

  关注小编,持续更新中...... 

相关文章:

  • 什么样的物联网框架适合开展共享自助KTV唱歌项目?
  • 游戏引擎学习第144天
  • 《几何原本》命题I.20
  • Spring (八)AOP-切面编程的使用
  • 前端面试题 口语化复述解答(从2025.3.8 开始频繁更新中)
  • 动静态库链接生成和使用以及认识ELF文件
  • FPGA 实验报告:四位全加器与三八译码器仿真实现
  • 【NLP 31、文本匹配任务 —— 深度学习】
  • 跟着 Lua 5.1 官方参考文档学习 Lua (11)
  • 线性代数之矩阵特征值与特征向量的数值求解方法
  • 2022年《申论》第二题(河北A卷)
  • Java高频面试之集合-07
  • 剖析Manus:AI领域的创新先锋还是虚假泡沫?
  • 【leetcode hot 100 141】环形链表
  • 解锁AIGC新时代:通义万相2.1与蓝耘智算平台的完美结合引领AI内容生成革命
  • 掌握Linux基础:从Shell提示符到文件管理的全面指南
  • JavaWeb-Servlet6 入门
  • 低纹波高效率DCDC电源芯片ASP4644技术解析
  • 行为模式---责任链模式
  • window 11亲测NodeMCU-8266 开发板+Micropython点灯实验
  • 市建设局网站的综合业务管理平台/网络推广引流最快方法
  • wordpress 反馈插件/seo关键词排名优化手机
  • 江苏建设集团招聘信息网站/网络营销有本科吗
  • 烟台房地产网站建设/网站优化排名服务
  • 建设银行网站不能登录不了/50个市场营销经典案例
  • 太原网站建设维护/链接生成二维码