Linux操作系统 -> 进程信号(上)

前言

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

一、从不同角度理解信号

1、生活角度的信号

信号本身可以被观察,但不必立即处理,可以暂存并延后响应,这就是"异步"的本质。

电梯呼叫:

  1. 信号触发:你按下电梯按钮(发出请求)。

  2. 信号识别:按钮亮起,你知道电梯已被呼叫,但不用一直盯着电梯门。

  3. 延迟处理:你可以继续做自己的事(如看手机)。

  4. 响应时机:电梯到达并"叮"一声,你才进入电梯(处理信号)。

  5. 处理方式

    • 默认:直接进去。

    • 自定义:让老人先进。

    • 忽略:电梯到了也不进

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

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

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

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

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

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

2、技术应用角度的信号

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

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

将生活例子和 Ctrl+C 信号处理过程相结合,解释一下信号处理过程:进程就是你(在等待ing),操作系统就是电梯系统 + 电梯员(负责通知你),信号就是电梯到达的"叮"声(相当于 Ctrl+C 产生的 SIGINT)。

3、注意

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

4、信号概念

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

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

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

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

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

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

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

1、忽略此信号

2、执行该信号的默认处理动作

3、提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为自定义捕捉一个信号

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

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

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

SIGCONT和SIGSTOP

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

(1)产生信号

  • kill 命令产生
  • 键盘产生

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

(2)信号识别

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

什么是合适的时候 呢?下面会详细说明,不是立即处理指的是,我们电梯的例子

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

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

(3)信号处理

默认方式(部分是终止进程,部分有特定的功能)

忽略信号。比如说发送了一个信号,但却什么都没有做,这就是忽略信号,它当然也是处理信号

自定义信号。比如你想自己处理这个信号,就叫做自定义信号,也叫做捕捉信号

eg:

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

7、信号的本质

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

信号再哪里保存?

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

信号是谁发的?

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

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

二、产生信号

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

SIGINT的默认处理动作是终止进程,SIGOUIT的默认处理动作是终止进程并且Core Dump,下面开始验证一下

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

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

永远也不会被调用

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

当我们在键盘Ctrl+C就是向前台进程发送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 信号。

3、由软件条件产生信号

软件条件不是错误,当某种条件被触发时,OS 会向目标进程发送信号。就好比你拿了你妈妈的 100 块钱,你妈妈发现是你拿的,相当于你发了信号给你妈妈,然后你妈妈检测到异常把你揍了一顿,这就叫作进程出问题被 OS 检测到,然后发信号终止进程。又好比,你叫你妈妈明早叫你起床,然后你妈妈明早就准时叫你起床,此时你和你妈妈之间的交互没有任何硬件单元存在,这叫做软件条件产生信号。
在学习进程间通信的时候就验证过:读端不光不读且把读端关闭,写端一直在写,那么 OS 会自动终止对应的写端进程,通过发信号的方式,写端就会收到 13)SIGPIPE 信号,进而导致写进程退出。(验证方法:创建匿名管道,让父进程进行读取,子进程进行写入。让父进程关闭读端 && waitpid(),子进程一直写入就行,子进程退出,父进程的 waitpid 拿到子进程的退出 status,提取退出信号)在底层 OS 一定会提供支持,所以在写入的时候,OS 一定是设置了我们能成功写入的条件,比如读端的文件是打开的写端就可以写,否则写端再写就会被操作系统发送信号。所以,在 OS 层面上这是一种软件条件产生的信号。
SIGPIPE 是一种由软件条件产生的信号,在 "管道" 中已经介绍过了,下面介绍 alarm 接口和 14)SIGALRM 信号(alarm 其实并不常用,只是想通过 alarm 来演示软件条件产生信号)。

(1)alarm

A. 接口介绍

这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。

调用 alarm 函数可以设定一个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发送 SIGALRM 信号,该信号的默认处理动作是终止当前进程。打个比方,我要小睡一觉,设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为 15 分钟之后响,"以前设定的闹钟时间还余下的时间" 就是 10 分钟。如果 seconds 值为 0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。比如 alarm 30 秒,但 20 秒时进程结束了,于是重新 alarm 15,此时就会返回以前设定闹钟时还余下的时间 10 秒。

为什么 1 秒内打出来的值才累加到 五万多?

如果只是单纯累加能加到很高,是亿级别的。

但是涉及到了 I/O 数据传输,而且这里是云服务器,还涉及到了网络数据传输,效率自然就低了。

B. 闹钟定时性功能

如何理解软件条件给进程发送信号?

OS 先识别到某种软件条件触发或者不满足,然后 OS 构建信号发送给指定的进程。

4、硬件异常产生信号

到此,我们就可以理解一个进程能够收到信号,收到信号后它会捕捉,忽略。比如忽略处理完后,进程就要退出了,然后再释放资源,这都能理解。但是像除 0,野指针 / 越界这些错误,OS 是如何具备识别异常的能力?

OS 是软硬件资源的管理者,好的坏的情况都知道,对于除 0,野指针 / 越界:在语言上都叫作报错,但实际上它们对应不同的软硬件。

理解除 0:对应 CPU 内部的状态寄存器(除 0 就是溢出,而状态寄存器有对应的溢出标志位用来检测每次计算有无溢出)。

理解野指针 / 越界:都必须通过地址找到目标地址,语言上对应的地址都是虚拟地址,对应内存、页表、内存管理单元 MMU(Memory Manager Unit 是负责的是将虚拟地址转换成物理地址的一种硬件,MMU 转化的时候一定会报错,并且提供硬件机制的内存访问授权。如果出现野指针,就会被检测你的这个地址没有权限去访问)。

坏的情况下,操作系统当然知道是哪一个进程做的:如果是 CPU 除 0,那么当前是哪个进程在执行代码就是哪个进程干的;如果是内存野指针/越界,当前用的是哪个进程的页表,完成是哪个进程的转换,那么也就是哪个进程干的。换而言之,OS 知道是哪个进程出错了,哪个进程干的,所以 OS 就可以向这个进程发送信号。

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

一旦出现硬件异常,进程一定会退出吗?

不一定。一般默认是退出,但是即便不退出,我们也做不了什么。

上述内容为什么会死循环呢?

寄存器中的异常一直没有被解决。

捕捉玩信号之后退出就不会出现死循环了。

5、总结

上面所说的所有信号产生,最终都要有 OS 来进行执行,这是为什么呢?

因为OS 是进程的管理者。

信号的处理是否是立即处理的?

不是,而是在合适的时候。

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

需要,记录在 PCB 对应的信号位图当中。

一个进程在没有收到信号的时候,能否知道自己应该对合法信号作何处理呢?

能知道,因为这是程序员写好的。

如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

简单来说,OS 去修改恢复,根据信号编号去修改特定比特位,由 0 至 1 就完成了信号的发送过程。

相关推荐
信创DevOps先锋2 小时前
Gitee:中国DevOps生态的数字化转型引擎
运维·gitee·devops
一只专注api接口开发的技术猿2 小时前
商品详情API的SLA保障体系:监控告警、异常检测与自动化修复
运维·数据库·架构·自动化
WarPigs2 小时前
SQL Server笔记
服务器·数据库·sqlserver
嵌入式学不会不改名3 小时前
香橙派环境
linux·ubuntu
BS_Li3 小时前
【Linux网络编程】Socket编程UDP
linux·网络·udp
时光之源3 小时前
程序猿常用命令行(Linux、Windows、Powershell、CMD、conda、pip、apt)
linux·conda·pip
qing222222223 小时前
Linux:/var/log/journal 路径下文件不断增加导致根目录磁盘爆满
linux·运维·前端
lwx9148523 小时前
Linux-后台运行操作符&
linux·运维·服务器
mounter6253 小时前
深度解析 Linux 内核 devlink:从硬件控制到跨功能速率调度的演进
linux·运维·服务器·网络·内核