【Linux 系统】进程信号 -- 详解(上)

简介: 【Linux 系统】进程信号 -- 详解(上)

⚪前言

注意:进程间通信中的信号量跟下面要讲的信号没有任何关系。


一、从不同角度理解信号

1、生活角度的信号

  • 你在网上买了很多件商品,在等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递,也就是你能 “识别快递”。
  • 当快递员到了你楼下,你也收到快递到来的通知,但是此时你正在打游戏,需 5min 之后才能去取快递。那么在在这 5min 之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成 “在合适的时候去取”。
  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间内,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你 “记住了有一个快递要去取”。
  • 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1、执行默认动作(幸福的打开快递,使用商品);2、 执行自定义动作(快递是零食,你要送给你你的女朋友);3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)。
  • 快递到来的整个过程对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。

在生活中也存在着很多信号,比如闹钟、电话铃响、红绿灯等等,这里就有下面两个问题:

为什么我们能认识红绿灯或者闹钟呢?

因为曾经有人教过我们红绿灯或着闹钟是什么,然后我们记住的。

身边没有闹钟时,我们是否知道闹钟响了之后,该怎么办?

当然知道,因为曾经有人教过我们,教我们的是:它是什么,为什么,怎么办。这两个问题对应是什么和怎么办。而为什么,是我们需要被提醒,所以要认识闹钟。

所以对于是什么和怎么办这个话题称为人能够识别信号。OS 类似社会,人就是进程,社会中会有很多信号围绕着人去展开,而 OS 中也会有很多信号围绕着信号去展开,所以进程要能够识别非常多的信号。这里只想说明进程能够认识信号,以及信号不管到没到来进程都知道该怎么做。


2、技术应用角度的信号

(1)用户输入命令,在 Shell 下启动一个前台进程。

用户按下 Ctrl+C,这个键盘输入产生一个硬件中断,被 OS 获取,解释成信号,发送给目标前台进程。前台进程因为收到信号,进而引起进程退出。

将生活例子和 Ctrl+C 信号处理过程相结合,解释一下信号处理过程:进程就是你,操作系统就是快递员,信号就是快递。


3、注意

  1. Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个 & 可以放到后台运行,这样 Shell 不必等待进程,结束就可以接受新的命令,启动新的进程。
  2. Shell 可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

4、信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。


5、kill -l 命令可以察看系统定义的信号列表

我们前面也简单的接触过信号,kill -l 就可以查看信号,仔细观察可以发现这里不是 64 种信号,因为中间并不是连续的,一种有 62 种信号(其中,没有 32 和 33 信号)。其中,1~31 叫做普通信号,而 34~64 叫做实时信号,每个实时信号中都包含了 RT 两个字母。

下面将重点谈谈普通信号,实时信号不考虑,简单提一下即可,实时信号是一种响应特别强的信号,比如着火,而普通信号则对应我们每天早上的闹钟。

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在 signal.h 中找到,例如其中有定义 #define SIGINT 2


6、信号处理常见方式概览

(sigaction 函数后面会详细介绍),可选的处理动作有以下三种:

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为自定义捕捉(Catch)一个信号。

生活中的信号有三种生命周期,Linux 下的信号也是如此,所以下面就围绕着这三种生命周期进行研究。

这段代码就是一段简单的死循环,当我们在键盘 Ctrl+C 就是向前台进程发送 2) SIGINT 信号结束进程。当然可以新建 ssh 渠道验证一下,这里可以向目标进程发送 2 号信号或者它所对应的宏 SIGINT

对于相当一部分信号而言,当进程收到的时候,默认的处理动作就是终止当前进程。

SIGCONT 和 SIGSTOP

这两个信号,我们之前也接触过,19)SIGSTOP 用于暂停目标进程,18)SIGCONT 用于继续目标进程。此时发送 18 号信号后,Ctrl+C 也就是发送第 2 号信号不能结束目标进程,因为目标进程被发送 18 号信号后,已经变成了一个后台进程 S(ps ajx 可以看到),2 号信号无法结束,所以这里可以发送第 3,9 号信号来结束像这样的后台进程。


(1)产生信号

  • kill 命令产生。
  • 键盘产生。

第 1 点就是 kill -2 pid,第 2 点就是 Ctrl+C。


(2)信号识别

进程收到信号,其实并不是立即处理的,而是选择在合适的时候再处理。

什么是 “合适的时候” 呢?下面会详细说明,“不是立即处理” 指的是,我们前面取快递那个例子。

那么信号中为什么 “不是立即处理” 呢?

因为信号的产生可以在进程运行的任何时间点,那么进程可能正在做更重要的事情。


(3)信号处理

  1. 默认方式(部分是终止进程,部分有特定的功能)。
  2. 忽略信号。比如说发送了一个信号,但却什么都没做,这就是忽略信号,它当然也是处理信号。
  3. 自定义信号。如果你想自己处理这个信号,就叫做自定义信号,也叫做捕捉信号。

举例:早上的闹钟响了,然后你就起床了,这是默认; 闹钟响了,然后还选择继续睡,这就是忽略;闹钟响了,然后起来跳个舞,这是自定义捕捉。


7、信号的本质

从信号识别中,我们可以知道信号不是立即处理的,那么就意味着信号需要被保存起来。

信号在哪里保存?

信号不是给硬件,网络发的,它是给进程发的,所以这个信号一定是在进程的 PCB 下,也就是在进程控制块 task_struct 中保存。

信号如何保存?

由 kill -l,我们知道一共有 31 个普通信号,它们都是大写字母构成,其实也就是一个个的宏,我们可以在系统中查找到。

你买了一个快递,于我而言,我当然知道寄来的是什么,而快递员是男是女,多大年纪这并不重要,重要的是快递是否到了,里面的东西是否完整无损。所以对进程来说,最重要的无非就是 “是否有信号” + “是谁”。操作系统提供了 31 个普通信号,所以我们采用位图来保存信号,也就是说在 task_struct 结构中只要写上一个 unsigned int signals; (00000000 … 00000000) 这样一个字段即可。比特位的位置代表是哪一个信号,比特位的内容用 0 1 来代表是否。

信号是谁发的,如何发?

发送信号的本质就是写对应进程 task_struct 信号位图。因为 OS 是系统资源的管理者,所以把数据写到 task_struct 中只有 OS 有资格、有义务。所以,信号是操作系统发送的,通过修改对应进程的信号位图(0 -> 1)完成信号的发送,再朴素点说就是信号不是 OS 发送的,而是写的。

接下来再看信号的产生(kill,键盘),不管信号是如何产生的,最后都一定要经过 OS,再到进程。kill 当然是命令,是在 bash 上的,也就是在系统调用之上,所以 kill 的底层一定使用了操作系统某种接口来完成像目标进程写信号的过程。键盘是一种硬件,它所产生的各种组合键会产生各种不同的数据,OS 作为硬件的管理者,键盘上所获得的各种数据,一定是先被 OS 拿到。所以,虽然信号的产生五花八门,但归根结底所有信号的产生后都是间接或直接由 OS 拿到后向目标进程发信号。


二、产生信号

1、通过终端按键产生信号

SIGINT 的默认处理动作是终止进程,SIGQUIT 的默认处理动作是终止进程并且 Core Dump,下面就来验证一下。

在 Linux 下,C++ 文件的后缀可以是 .cpp,.cxx,.cc,可以看到这里的 makefile,这样写的好处是如果以后想修改依赖文件或者目标文件,那么只需要修改上面的一部分即可。

如果后续没有任何 SIGINT 信号产生,catchSig 会不会被调用?

永远也不会被调用。

signal 函数仅仅只是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作。

当我们在键盘 Ctrl+C 就是向前台进程发送 2)SIGINT信号结束进程相当于向目标进程发送它所对应的宏 SIGINT 或者是发送 2 号信号

可以看到没有信号产生时,它就不会执行 signal,因为它是回调函数。而一旦 Ctrl+C 收到信号,这里就调用了 catchSig 函数并获取到信号编号,同样命令也是如此。虽然捕捉了 2 号信号 SIGINT,但是其它信号并没有被捕捉,所以可以 Ctrl+/ 或者是其它信号。那么问题来了,如果将 31 个信号都捕捉完呢。


假设当我们把全部信号捕捉时,操作系统给进程写的任何信号,进程只是默认知道,然后给你一句话就完了,接着继续跑路,是不是就意味着写了一个 “金刚不坏” 的进程呢?Linux 操作系统当然需要考虑这种场景,如果允许所有的信号被捕捉,那么非法用户就很容易创建了一个非法进程,这个进程各种申请资源就是不还,并且还把所有的信号全部捕捉或忽略,这就导致操作系统知道是这个进程的问题,还拿它没办法,这就是系统设计上的 Bug。所以,Linux 系统中有若干个信号不能被捕捉或自定义,最典型的信号就是第 9 号信号 SIGKILL,快捷键 Ctrl+\,它叫做管理员信号,是所有信号中权力最大的。那么忽略信号的现象是什么呢?

可以看到 SIG_IGN 对应的就是把 1 强制成函数指针类型,它依旧是一个回调函数(这里 grep -ER 在筛选时后面可以 -n 以获取行号,在 vim /usr/include/bits/signum.h 时也可以在其后 +24 以定位所在行号)。此时系统发送信号给进程,它一句话也不说,继续跑路,直接忽略(不过这里 Ctrl+C 时有反应),我们知道它不能对所有信号进行忽略,所以发送第 9 号 SIGKILL 杀掉进程。

所以第 9 号进程 SIGKILL 既不能被捕捉,也不能被忽略。上面说过进程运行的任何时间点都可以产生信号,所以信号产生和进程运行是异步的(当然也有同步,这也就是在前面讲信号量时只谈了异步的原因,同步这个名词有不同的解释,场景不同表达的意思也就不同。同步和异步有时表示的是执行流的关系,有时是进程访问临界资源的问题。后者就好比老师在上课过程中烟瘾犯了,然后跟学习不好的张三说,你去帮我拿包烟,我们先休息会等你,你回来后我们再开始上课,此时课程的进度跟张三回来要同步,互相影响,这叫做同步;还是老师在上课过程中烟瘾犯了,然后跟学习好的李四说,你去帮我拿包烟,然后老师继续上课,而李四在跟老板吵着架,此时两件事是同时进行的,互不影响,这叫做异步)。换而言之,想说明的是如果两个进程是毫无关系,一个进程在执行时随时可能会收到信号,而信号是用户还没发,准备发,已经发,所以进程就不等信号了,这就是异步。


(1)Core Dump

先解释一下什么是 Core Dump,当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是 core,这叫做 Core Dump。

进程异常终止通常是因为有 Bug,比如非法内存访问导致段错误,事后可以用调试器检查 core 文件以查清错误原因,这叫做 Post-mortem Debug(事后调试)。一个进程允许产生多大的 core 文件取决于进程的 Resource Limit(这个信息保存在 PCB 中)。默认是不允许产生 core 文件的,因为 core 文件中可能包含用户密码等敏感信息,不安全。

在开发调试阶段可以用 ulimit 命令改变这个限制,允许产生 core 文件。 首先用 ulimit 命令改变 Shell 进程的 Resource Limit,允许 core 文件最大为 1024K: $ ulimit -c 1024

设置 core file size,kill -8/11 后,发现报错信息中多了一个(core dumped),且 ll 还发现多了一个 core 文件

ulimit 命令改变了 Shell 进程的 Resource Limit,test 进程的 PCB 由 Shell 进程复制而来,所以也具有和 Shell 进程相同的 Resource Limit 值,这样就可以产生 Core Dump 了。

前面讲进程等待的时候说过一个概念,父进程中 waitpid 可以获取子进程的退出信息,其中 status 中,低 7 位表示进程退出时的终止信号,次低 8 位表示进程退出时的退出码,而低 8 位中的最后 1 位还没有讲,它表示进程是否 core dump,core dump 是一个标志位。

当一个进程被异常退出时,退出码没有意义,我们不仅想知道它的退出信号,更想知道的是它在代码的哪一行触发的信号。因为云服务器默认看不到现象,如果是虚拟机的话就可以看到。所以为了让云服务器能够看到,我们就需要设置一下,ulimit -a 查看系统资源,其中 ulimit -c 1024 就设置好了 core file size。

在上面运行报错后,有一个(core dumped),它叫做核心转储。当一个进程崩溃时,OS 会将进程运行时的核心数据 dump 到磁盘上,方便用户进行调试,一旦发生核心转储,core dump 标志位就会被设置 1,否则就是 0。

一般而言,线上环境的核心转储是被关闭的。因为程序每崩溃一次就会 dump 一次,而这一个 core 文件有 56 万多个字节,还不说这个文件不大。如果线上环境的核心转储是打开的,那么在公司项目中有几千台机器,那肯定是自动运行的,此时如果存在大量错误,一运行就 dump,一 dump 就运行,那么过了一晚,服务器肯定都登不上了,原因就是磁盘已经被大量的 core 文件占用了。


A. 除 0 错误

此时,我们就可以利用核心转储生成的 core 文件来定位 bug,需要 makefile 中 -g 先生成 release 文件。gdb 中直接 core-file + core 文件即可。我们之前找 bug 是一行行调试,而现在是什么都不管,直接让你先崩掉,然后配合 gdb 定位 bug,这种调试方案叫做事后调试


B. 野指针异常

这里还有一个细节,除 0 异常和 kill -8 报的错误是一样的,野指针异常和 kill -11 报的错误也是一样的,这就说明的是信号产生的第三种方式是程序异常,这里更准确来说应该是硬件异常,因为除 0 和野指针都有对应的硬件资源,后面会解释。

  • 8)SIGFPE 是指进程在运行时发生了算术异常,比如除 0 或者浮点数溢出等。
  • 11)SIGSEGV 是段错误,指进程在运行时访问了不属于自己的内存地址或者访问已经被释放的内存地址,比如野指针。

站在语言的角度这叫做程序崩溃,本质应该是进程崩溃。因为站在系统的角度来说,这就叫做进程收到了信号。换而言之,一般程序崩溃是因为你的代码有非法操作被 OS 检测到了,然后向你的进程发送了信号。当然,在语言层也可以使用异常捕捉来进行语言层面上的检测。如果没有信号,那么出现野指针等内存问题时,OS 作为软硬件资源的管理者设计的健壮性就很差,所以信号存在的价值也是为了保护软硬件等资源。


验证进程等待中的 core dump 标志位:


2、调用系统函数向进程发信号

(1)kill 命令

A. 接口介绍

kill 命令是一个系统接口,它是调用 kill 函数实现的,可以给一个指定的进程发送指定的信号。


B. 手动写一个 kill 命 —— 用系统调用接口来向系统发送指定信号


(2)raise 函数

A. 接口介绍

可以给当前进程发送指定的信号(自己给自己发信号)。

kill 和 raise 两个函数都是成功返回 0,错误返回 -1


(3)abort 函数

A. 接口介绍

使当前进程接收到信号而异常终止,通常用来终止进程,就像 exit 函数一样,abort 函数总是会成功的,所以没有返回值。

注意:abort 和 raise 是立即发送,而 alarm 是延时 seconds 秒发,abort 只能向自己发第 6 号信号,raise 是向自己发第 sig 信号。


如何理解系统调用接口?

用户调用系统接口 -> 执行 OS 对应的系统调用代码 -> OS 提取参数,或者设定特定的数值 -> OS 向目标进程写信好 -> 修改对应进程的信号标记位 -> 进程后续会处理信号 -> 执行对应的处理动作


【Linux 系统】进程信号 -- 详解(下)https://developer.aliyun.com/article/1515695?spm=a2c6h.13148508.setting.15.11104f0e63xoTy

相关文章
|
4天前
|
Linux 应用服务中间件 Shell
linux系统服务二!
本文详细介绍了Linux系统的启动流程,包括CentOS 7的具体启动步骤,从BIOS自检到加载内核、启动systemd程序等。同时,文章还对比了CentOS 6和CentOS 7的启动流程,分析了启动过程中的耗时情况。接着,文章讲解了Linux的运行级别及其管理命令,systemd的基本概念、优势及常用命令,并提供了自定义systemd启动文件的示例。最后,文章介绍了单用户模式和救援模式的使用方法,包括如何找回忘记的密码和修复启动故障。
21 5
linux系统服务二!
|
4天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
26 4
linux进程管理万字详解!!!
|
4天前
|
Linux 应用服务中间件 Shell
linux系统服务!!!
本文详细介绍了Linux系统(以CentOS7为例)的启动流程,包括BIOS自检、读取MBR信息、加载Grub菜单、加载内核及驱动程序、启动systemd程序加载必要文件等五个主要步骤。同时,文章还对比了CentOS6和CentOS7的启动流程图,并分析了启动流程的耗时。此外,文中还讲解了Linux的运行级别、systemd的基本概念及其优势,以及如何使用systemd管理服务。最后,文章提供了单用户模式和救援模式的实战案例,帮助读者理解如何在系统启动出现问题时进行修复。
21 3
linux系统服务!!!
|
4天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
24 4
|
4天前
|
网络协议 Linux
linux系统重要文件目录
本文介绍了Linux系统中的重要目录及其历史背景,包括根目录、/usr、/etc、/var/log和/proc等目录的结构和功能。其中,/etc目录下包含了许多关键配置文件,如网卡配置、DNS解析、主机名设置等。文章还详细解释了各目录和文件的作用,帮助读者更好地理解和管理Linux系统。
18 2
|
5天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
6天前
|
Ubuntu Linux Shell
Linux 系统中的代码类型或脚本类型内容
在 Linux 系统中,代码类型多样,包括 Shell 脚本、配置文件、网络配置、命令行工具和 Cron 定时任务。这些代码类型广泛应用于系统管理、自动化操作、网络配置和定期任务,掌握它们能显著提高系统管理和开发的效率。
|
6天前
|
消息中间件 存储 Linux
|
4月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
4月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
148 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)