
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- [1 ~> 理解信号是什么,为什么要有?生活中的信号](#1 ~> 理解信号是什么,为什么要有?生活中的信号)
-
- [1.1 信号是什么?](#1.1 信号是什么?)
-
- [1.1.1 普通信号和实时信号](#1.1.1 普通信号和实时信号)
- [1.1.2 信号的本质](#1.1.2 信号的本质)
- [1.2 生活中有哪些信号?以及一些结论总结](#1.2 生活中有哪些信号?以及一些结论总结)
-
- [1.2.1 man 7 signal:查看信号部分的内容](#1.2.1 man 7 signal:查看信号部分的内容)
- [1.2.2 信号没有产生也知道怎么处理](#1.2.2 信号没有产生也知道怎么处理)
- [1.2.3 收到一个信号会立即处理它吗?](#1.2.3 收到一个信号会立即处理它吗?)
- [1.2.4 信号怎么处理?](#1.2.4 信号怎么处理?)
- [1.2.5 总结一些这些小结论](#1.2.5 总结一些这些小结论)
- [1.2.6 图示](#1.2.6 图示)
- [1.3 信号具体化](#1.3 信号具体化)
-
- [1.3.1 为什么是大写?这些大写的名字是什么?](#1.3.1 为什么是大写?这些大写的名字是什么?)
- [1.3.2 头文件(含内核查看)](#1.3.2 头文件(含内核查看))
- [1.3.3 保存信号](#1.3.3 保存信号)
-
- [1.3.3.1 进程一定要有临时保存信号的能力](#1.3.3.1 进程一定要有临时保存信号的能力)
- [1.3.3.2 我们可以用什么样的特定的数据类型来保存信号?位图](#1.3.3.2 我们可以用什么样的特定的数据类型来保存信号?位图)
- [1.3.4 信号的位图在进程PCB里面,谁有资格修改内核数据结构中的字段?](#1.3.4 信号的位图在进程PCB里面,谁有资格修改内核数据结构中的字段?)
- [1.4 为什么要有信号?](#1.4 为什么要有信号?)
- [1.5 信号是什么的思维导图](#1.5 信号是什么的思维导图)
- [1.6 如何自定义捕捉信号?](#1.6 如何自定义捕捉信号?)
- [2 ~> 信号的产生](#2 ~> 信号的产生)
-
- [2.1 信号产生的方式](#2.1 信号产生的方式)
-
- [2.1.1 使用系统命令产生,kill命令](#2.1.1 使用系统命令产生,kill命令)
- [2.1.2 通过键盘产生信号](#2.1.2 通过键盘产生信号)
- [2.1.3 产生信号的还有一种方式:系统调用](#2.1.3 产生信号的还有一种方式:系统调用)
- [2.1.4 第四种产生信号的方式:异常](#2.1.4 第四种产生信号的方式:异常)
- [2.1.5 第五种信号产生的方式:由软件条件产生信号](#2.1.5 第五种信号产生的方式:由软件条件产生信号)
- [2.2 三个系统调用层层递进](#2.2 三个系统调用层层递进)
-
- [2.2.1 三个系统调用层层递进](#2.2.1 三个系统调用层层递进)
- [2.2.2 OS先收到键盘输入,再由操作系统转换成信号发送给目标进程](#2.2.2 OS先收到键盘输入,再由操作系统转换成信号发送给目标进程)
- [2.3 键盘怎么能够目标进程发送信号呢?](#2.3 键盘怎么能够目标进程发送信号呢?)
-
- [2.3.1 如何理解键盘输入?](#2.3.1 如何理解键盘输入?)
- [2.3.2 OS如何解释快捷键?](#2.3.2 OS如何解释快捷键?)
- [2.3.3 OS怎么知道信号应该发送给哪一个进程呢?](#2.3.3 OS怎么知道信号应该发送给哪一个进程呢?)
-
- [2.3.3.1 进程组](#2.3.3.1 进程组)
- [2.3.3.2 会话ID:SID](#2.3.3.2 会话ID:SID)
- [2.3.3.3 前台进程和后台进程 + 前后台进程切换](#2.3.3.3 前台进程和后台进程 + 前后台进程切换)
-
- [2.3.3.3.1 前台进程](#2.3.3.3.1 前台进程)
- [2.3.3.3.2 后台进程](#2.3.3.3.2 后台进程)
- [2.3.3.3.3 作业控制命令](#2.3.3.3.3 作业控制命令)
-
- [1 jobs](#1 jobs)
- [2 fg(foreground)](#2 fg(foreground))
- [3 bg(background)](#3 bg(background))
- [4 Ctrl+Z](#4 Ctrl+Z)
- [5 Ctrl+C](#5 Ctrl+C)
- [2.3.3.4 一些结论](#2.3.3.4 一些结论)
-
- [结论1:Ctrl C只能用于终止前台进程!](#结论1:Ctrl C只能用于终止前台进程!)
- [作业 / 独立的进程组有什么关系?](#作业 / 独立的进程组有什么关系?)
- 会话与进程组(作业)的作业之间的关系?
- 在一个会话中,任何时刻,只允许一个进程(组)在前台!
- 后台不允许获取
- [2.4 alarm:设置一个若干秒之后的闹钟](#2.4 alarm:设置一个若干秒之后的闹钟)
-
- [2.4.1 闹钟的返回值问题](#2.4.1 闹钟的返回值问题)
- [2.4.2 闹钟的应用场景](#2.4.2 闹钟的应用场景)
- [2.4.3 理解闹钟](#2.4.3 理解闹钟)
- 结尾
1 ~> 理解信号是什么,为什么要有?生活中的信号
1.1 信号是什么?
1.1.1 普通信号和实时信号

- 用
kill -l可以查看

常用的只有1~31,34~64,没有0号------一共62个,不是64个哦!
前31个是普通信号,后面带RT(real-time)实时信号。我们只考虑1-31就好了。
1.1.2 信号的本质

什么是通知?什么是异步?
- 通知:这个通知需要我们理解一下------事件通知。
- 异步:通知的到来,跟我不同步。
举个例子,我点了个外卖,然后就打游戏,外卖小哥送货上门,敲门是一个通知,说明外卖到了,我和外卖小哥是不同步的。
异步关系就是没关系(你做你的,我做我的);同步关系就是有关系(我得等你做完再做)。

- 再比如,比如我是一个老师,我讲课,突然快递电话打过来了,我叫张三去帮我取快递,但是我等张三找完快递回来再继续讲,这就是个同步的关系,张三不回来,我就不往下讲。
- 如果我继续讲课,张三也在找快递,我讲课、他帮我办事,这个就是个异步的关系------我讲我的课,张三找他要帮我找的快递。
1.2 生活中有哪些信号?以及一些结论总结
大抵有如下这些:

上面这些所有的信号产生的几乎都是异步的。
- 人能够识别对应的、甚至还没有发生的这些信号
为什么能够识别这些信号?
人是经过教育的,早就知道信号对应处理动作的对应关系。
操作系统给目标进程发送信号,目标进程能不能识别呢?
- 答案是:进程天然能够识别------进程和信号都是程序员写的代码 (进程相当于我们人,已经被程序员教育过了,程序员设计好了)
进程能够识别信号,并且已经知道怎么处理信号了。

1.2.1 man 7 signal:查看信号部分的内容
man 7本身不是查看信号的指令,而是指定要查看的手册章节为第 7 章。在 Linux 系统中,信号相关的概述通常位于第 7 章,因此要查看信号的详细信息,可以使用命令:
bash
man 7 signal
会显示信号的概念、列表、默认行为等全局说明。此外,信号相关的系统调用(如 signal()、sigaction())一般在第 2 章,库函数(如 sigsetops())可能在第 3 章,而第 7 章主要提供更上层的概述和标准约定。

1.2.2 信号没有产生也知道怎么处理

1.2.3 收到一个信号会立即处理它吗?
当我们收到一个信号,准备处理这个信号,这里的我们即进程,会立即处理这个信号吗?有时候中断不了呢!处理不了信号。
- 对于信号的处理------不会立即处理------信号可能会立即处理,只有合适的时候会处理。
既然有一定概率不会立即处理,那么得要有把信号临时保存起来的能力------不会立即处理,就得有临时保存的能力。

比如外卖小哥打电话,答应去取外卖之后如果没有保存信号,是不是就打游戏打着打着就忘了取外卖了。
1.2.4 信号怎么处理?
- 默认:我后面打完游戏去拿外卖回来吃
- 忽略:我直接就不想拿了(但是并不是忘了,只是不想)
- 自定义:比如我拿完外卖直接丢了或者干别的事,自定义行为
我们就以红绿信号灯为例:
- 红灯------自动停了------默认信号。
- 收到信号,也处理了,但是处理的方式是忽略------忽略信号。
- 红灯亮了,别人要么忽略要么停下,而你在跳舞------自定义信号。
1.2.5 总结一些这些小结论
- 设别信号是内置的,进程能自己识别信号,是内核程序员写的内置特性。
- 信号的处理方法,在信号产生之前就已经准备好了。
- 处理信号不是立即处理的,因为我可能正在做优先级更高的事情,会选择合适的时候进行处理。比如我在打游戏,外卖员给我打电话叫拿外卖,那我肯定是先忙完手头的事再去拿。
- 信号不会被立即处理,所以就注定了进程要有临时保存信号的能力!
- 信号处理的动作有三种:默认、忽略、自定义 ,后续都叫信号捕捉。
1.2.6 图示

1.3 信号具体化
1.3.1 为什么是大写?这些大写的名字是什么?
没有0号信号,我们知道,信号是有编号的。

- 这些大写的名字是什么呢?这些大写的名字就是宏。
1.3.2 头文件(含内核查看)

我们查看内核------

1~31是普通信号 ,我们现在是分时操作系统;34~64是实时信号(有RT,就是real-time)。
1.3.3 保存信号
1.3.3.1 进程一定要有临时保存信号的能力
进程一定要有临时保存信号的能力------原因我们前面已经分析了。
- 信号就是个数字吗?还不够具体。
- 进程需要使用特定的数据类型来保存对应的信号!
我们要分析两个问题------
- 1、我们要保存什么信号?哪一个信号?
- 2、我们要保存信号的侧重点?哪一个信号是否产生了?
1.3.3.2 我们可以用什么样的特定的数据类型来保存信号?位图
- 1、比特位的位置表示信号编号
- 2、比特位的内容要么是1要么是0,1或者0------是否收到对应的信号!
用位图表示对应的1~31个编号。
关键是这个位图在哪里?
- 这个位图应该在哪里?设计在
进程的PCB里面。
1.3.4 信号的位图在进程PCB里面,谁有资格修改内核数据结构中的字段?

-
信号的位图在进程PCB里面,谁有资格修改内核数据结构中的字段?
-
谁有资格------这个世界上,能够给进程写入信号的家伙只有一个------就是操作系统OS。
操作系统不能决定全部的往进程写入信号(有能力,但是也只是一个办事的)------用户让你办的!
- 用户通过系统调用让操作系统向目标进程发送信号。

1.4 为什么要有信号?
- 我们一看到问为什么,一定要想到没有这个东西会出现怎么样的问题。
Linux之所以需要信号,是为了提供一种 异步且轻量级的进程间通信与事件通知机制,使操作系统能够立即打断进程的正常执行流,以通知其发生了诸如用户中断(Ctrl+C)、硬件异常(段错误)、定时器到期或子进程退出等突发状况。信号机制弥补了管道或共享内存等同步通信方式的不足,让进程能够以一种统一、即时的方式响应外部与内部的高优先级事件,是实现进程控制、异常处理和系统紧急交互的核心基础。
1.5 信号是什么的思维导图
如下图所示:

1.6 如何自定义捕捉信号?
一个函数,我们写一段代码------
- 对任何信号进行一下捕捉!
bash
man signal

信号是发给进程------处理信号是进程自己去处理的。
- 回调函数

signal函数其实是对指定信号未来进行自定义处理的一种延时设定。
我们挑一个信号------这里挑选2号信号:SIGINT

运行一下:

bash
kill -2 [pid]

- 查看信号手册:
我们在手册(man 7 signal)里面查看2号信号------

要让2号信号不要终止进程,调用我自定义的方法------

这就叫做信号捕捉。
Ctrl C(就是给前台发送2号信号)也终止不了了

我们修改一下代码:

- 信号最终是由进程自己去处理的,通过上面的代码可以验证------两个pid一样

对某个信号进行设定,那这个参数sig是什么?

我们继续验证一下:

本来就是设定了是2号信号,为什么还要把2号信号传进来呢?
- Linux内部,不同的的信号捕捉的处理方法可以是同一个!
所以我们得知道是哪一个信号------我们多个信号设置处理方法,可以是同一个处理方法。

我们再来验证一下:

可以通过传入的参数来区分是哪个信号触发了自定义捕捉。

IGN的参数是1:

我们验证一下:
- 所有的2号信号都被忽略了!

- 还有一种是恢复默认:
SIG_DFL

我们再运行一下:

- 符合预期,就是进程终止,说明恢复默认了!
代码跑完对还是不对由退出码决定。

- 运行一下我们发现,进程果然就杀不掉了!

虽然1~31都能够设定自定义捕捉,但是有两个信号是不能够设定的:9号信号和19 / 20号新号(看系统,具体是哪个)
- 9号信号很重要:9号信号不能被自定义捕捉 、不能被忽略。

2 ~> 信号的产生
- 让OS给目标进程写信号。
操作系统给目标进程写信号------不管怎么样,必须经过操作系统------因为只有操作系统能够有资格修改内核数据结构。
产生信号这个话题内容有点多。信号的产生是异步产生的,所以不会立即处理。
2.1 信号产生的方式

2.1.1 使用系统命令产生,kill命令
在【自定义捕捉信号】那里我们已经见识过了。
2.1.2 通过键盘产生信号
放开循环:

我们运行一下:

我们查看手册,来一一对应一下,看看它对于当前进程的处理动作:
- term和core都是进程终止
Ctrl C证明我们可以通过键盘产生信号(组合键的方式)。
-
为什么你如果启动一个进程之后,
Ctrl C可以终止这个进程!
Ctrl C:发送了2号信号,2号信号的处理动作是终止! -
我们保留一个问题:键盘怎么能够向目标进程发送信号呢?
键盘是个硬件啊,进程是个软件啊,怎么发送信号?
2.1.3 产生信号的还有一种方式:系统调用
kill:发送信号给进程。

第一个参数:发送给哪个进程;第二个参数:发送哪个信号。
系统底层肯定要调用这个kill。
- 管道那里有一个对应的命令:
mkfifo

Stat:获取函数的属性

printf:底层也是调用了同名系统调用

很多命令底层用到了它的同名系统调用!
我们来测试一下------
- 输出参数不匹配,就给你打印一个使用手册,类似于平常编译时候的报错
- 类型要转成整数

然后就可以使用kill系统调用,向目标进程写入信号------

运行一下,我们发现多了个\n(左边黄色框框):

我们再修改一下源代码:
- 把
\r都去掉

我们再运行,还是前面的那种运行的图,这次我们看本次运行的结果:

kill命令底层调用了kill系统调用------让操作系统向目标进程发送指定信号。
- 如下图,验证了9号信号不能被捕捉

-
raise:自己给自己可以发任何信号 -
修改代码,让进程自己给自己发送信号

- 自己给自己发,1秒打一次------

一个系统调用可以给自己发送任意信号。
raise底层封装了kill

abort

我们修改一下代码,加入abort函数:

- 运行:当前进程立即终止了

我们发现信号是6号信号!
abort:也是Core(终止进程)

6号信号通常用来进行异常终止。
既把信号捕捉了,进程也终止了。
- 6号信号还很特殊:可以理解为不可以被捕捉
可以设置捕捉动作,但是最后还是会终止进程!------异常。
- 6号信号也可以被算做是不能被捕捉、忽略的

2.1.4 第四种产生信号的方式:异常

获取到哪个信号呢?

- 我们放开信号捕捉

- 运行
信号捕捉函数一直被触发(不是因为while循环)------死循环。

- 杀掉进程

11号信号------

- 证明不是因为while(true)循环

照样还是死循环:

我们故意来一个/0错误:

- 8号信号:SIGFPE(浮点异常)
尽管名字包含"浮点",但它实际上涵盖了更广泛的算术错误,包括整数运算中的错误。

观察一下:

除零错误会被当做信号发出来。
进程自己就会收到对应的信号!

为什么会把进程异常当成信号?进程异常是如何被OS识别、并且解释成为信号的?!
2.1.5 第五种信号产生的方式:由软件条件产生信号
这里就有点抽象了。
- 管道:r端关闭,W端打开,W端一直写 --> OS就会把写进程直接就杀掉了
操作系统认为管道不具备写条件------OS就把写进程杀掉了。
你得罪了软件或者硬件,操作系统都要把你杀掉。
进程退出一定是异常了,收到信号了。
关于闹钟(alarm)的相关话题我们在2.4细说。
2.2 三个系统调用层层递进
2.2.1 三个系统调用层层递进
前面的kill、raise、abort三个系统调用我们发现是层层递进的!

2.2.2 OS先收到键盘输入,再由操作系统转换成信号发送给目标进程
接下来,先不往后面看,先复盘一个问题:

我们已经说过啦,必须只能是OS!
只有操作系统可以在内核操作系统里面对PCB内部的位图进行修改。
换句话说,通过键盘产生信号也并不是键盘直接产生的信号,必然是OS先收到键盘
的输入的,再由操作系统转换成信号发送给目标进程!
2.3 键盘怎么能够目标进程发送信号呢?
- 1、如何理解键盘输入?
键盘是基于硬件中断来进行工作的! - 2、OS如何解释快捷键?(组合键)
ctrl c、ctrl \,......可能会直接解释成为信号! - 3、OS怎么知道信号应该发送给哪一个进程呢?操作系统怎么知道?要补充一点网络时要用的知识!
2.3.1 如何理解键盘输入?
如何理解键盘输入?
我们通过图来理解一下------

我的操作系统怎么知道用户把键盘按下了、按下了哪个(键)字符?------驱动去处理的。
我们关心的是,OS怎么知道键盘上有数据了?!
计算机的外设很多,操作系统要做各种工作,你能够轮询吗?这种做法在技术上可以实现,但是这会让操作系统变得很慢!
硬件中断

作为CPU,只要被动等待就行。
- 中断:是计算机组成原理的最重要的重点,没有之一!没有学懂中断,就是没有学懂操作系统。
操作系统有些东西没有学懂,问题可能不在操作系统上面,可能是在理解计算机组成原理上。
- 跨学科学习是一个比较重要的思路
除了冯诺依曼告诉我们的外设,还有......
键盘和CPU是一根线上连着的。
- 针脚如下所示(英特尔的):会和主板相连。

这些针脚会和主板连上,所以主板走线会和外设勾连。
网卡的例子。
告诉CPU,数据已经到来了,触发中断。
操作系统并不知道键盘上有数据了,而是中断告诉CPU了。
2.3.2 OS如何解释快捷键?
键盘的字符既有"ABCD......"这样的正常的字符,也有"Ctrl C""Ctrl V"......这样的组合键,操作系统会把组合键解释成信号(会判断,ctrl c是特殊字符,在ASCII码表里存在)------操作系统会有系统调用:给目标进程发送信号 ------kill(pid,signum)。
2.3.3 OS怎么知道信号应该发送给哪一个进程呢?
- 说到这个,我们先要谈一个进程组和作业的话题。
如下图,我们这里创建了三个进程:

三个进程的父进程都是同一个:bash;即这三个进程是兄弟进程。
2.3.3.1 进程组
- 进程是有组的概念

2.3.3.2 会话ID:SID
这三个兄弟进程的SID一样,属于同一个会话,以bash进程命名:

一般情况下,同一个组里面大家会使用同一个终端文件(因为我们是远程登录的Xshell,这里的终端文件是虚拟网络文件)。
每一个用户登录,都要建立会话,打开终端文件。
如果在bash命令行里再启动一个进程
- 我们把代码改一下,把abort注释掉
同一个终端下,在创建一个进程------

我们发现这个进程和那三个进程不在一个进程组里面,但是在一个会话组里。
本质是一个会话里面创建一个新的进程组(哪怕进程组只有我一个进程)。
- 我们把for循环也注释掉,方便
ctrl c等操作
2.3.3.3 前台进程和后台进程 + 前后台进程切换
进程分前台进程和后台进程:

我们取地址&,就变成后台进程了------

- 我们今天要把这个说法变一变。
我们发现Ctrl C不能干掉进程了------

再看一下:

观察运行的打印结果:

- 我们在代码里面把休眠时间改长一点
前台进程:

后台进程:

我们只能这样删除:
bash
kill -9 [pid]
- 我们把打印也注释掉,只看到时候的任务号

再创建一大批的任务:

每一个进程都有自己的进程组,都属于同一个会话:

- 启动后台任务,
Ctrl C是杀不掉的!

2.3.3.3.1 前台进程
定义:占用当前终端,必须等该进程执行完毕才能输入其他命令。
示例:直接执行 sleep 100,终端会被占用 100 秒,无法输入新命令。
2.3.3.3.2 后台进程
定义:在后台运行,不占用终端,用户可继续执行其他命令。
启动方式:在命令末尾加 &。
bash
sleep 100 &
输出会显示作业号 [1] 和进程 PID。
2.3.3.3.3 作业控制命令
1 jobs
bash
jobs
查看当前终端中的后台作业列表(带作业号)。
输出示例:
bash
[1]+ Running sleep 100 &
2 fg(foreground)
将后台作业调回前台继续运行。
bash
fg %1 # 将作业号 1 的作业调到前台
fg # 默认将最近一个后台作业调到前台
3 bg(background)
将暂停(挂起)的作业放到后台继续运行。
-
先用
Ctrl+Z暂停前台进程,此时作业变为"已停止"状态。 -
执行
bg %1让它在后台继续运行。
bash
sleep 200 # 执行,然后按 Ctrl+Z 暂停
bg %1 # 在后台继续运行
jobs # 可看到状态变为 Running
4 Ctrl+Z
暂停当前前台进程,将其放入后台(停止状态)。
5 Ctrl+C
终止前台进程。
进程可以在前台或后台运行,并通过 fg 和 bg 命令进行切换

2.3.3.4 一些结论
结论1:Ctrl C只能用于终止前台进程!
Ctrl C只能用于终止前台进程!
作业 / 独立的进程组有什么关系?

进程组是用来完成作业的!
进程组允许是一个进程。
作业是由进程组完成的!
在命令行./进程(启动一个进程)属于上面所说的特殊情况。
task(作业 / 任务)------进程组和作业就像是硬币,一体两面。
如下图所示:

会话与进程组(作业)的作业之间的关系?
会话内部至少包含一个进程组(就是bash)。
会话: 是由一个或者多个进程组构成的 进程组的集合,通常会有属于自己的终端文件!
通过 **fg [任务号]**把后台任务变成前台之后,就可以Ctrl C删掉了。
在一个会话中,任何时刻,只允许一个进程(组)在前台!
只要一个进程在前台,我的命令就没法被其它进程执行了。
这里存在两个问题:
- 1、为什么其他命令就无法执行了?
因为在一个会话中,任何时刻,只允许一个进程(组)在前台,能够接受、执行命令的bash在哪里?自动切换成为后台进程组了(为什么bash变成后台了,命令就无法进行了)!获取不到命令,就无法执行命令了!
键盘只有一个,在会话里有这么多进程,谁拥有键盘,谁就是前台进程。
-
2、什么叫做前后台?
-
能够直接获取用户输入的基础,叫做前台进程;
-
否则叫做后台进程
因为键盘只有一个,任何时刻只允许一个人输入(只有一个人,一个输入),谁拥有终端文件(主要是键盘),谁就是前台!获取不到键盘,所以就无法输入了。
- 终端文件:键盘和显示器的集合体
后台进程可以写数据(向显示器打印),但是后台进程无法从键盘读取数据(读取的时候会被系统直接暂停掉)。
这就叫做 【会话管理】!
任何时刻,前台进程(组)只有一个!
- 键盘输入:操作系统怎么知道要把信号发送给谁?
前台进程只有一个,所以操作系统知道未来要把信号发给哪个进程!
只能发送给前台进程(组),而且前台进程只有一个!

前后台切换就像是 "孔融让梨"。
- Ctrl C严格意义上不是杀掉一个进程,而是杀掉一个进程组。
父进程子进程都被Ctrl C杀掉了。
- 登录两次、三次、......系统是不是每次都要创建这些东西?
剩下的,在网络讲【守护进程】的时候再介绍。

后台不允许获取

代码再重编一下:

把前台进程被暂停了,前台进程就会自动切换到后台进程。
- 这里证明了,后台进程能够打印、但是不能获取,一旦获取就会暂停。

- 谁拥有键盘,谁就是前台;谁能够获取键盘的输入,谁就是前台。
当你按键盘一定是和某个会话或者某个终端相关联。
2.4 alarm:设置一个若干秒之后的闹钟
学习一个新的函数:alarm。
再认识一个信号:14号信号。

操作系统收到这个信号默认的处理动作是:Term。
2.4.1 闹钟的返回值问题
我想看到的现象是收到闹钟前,进程会疯狂打印cnt计数器:

效果:
- 计数器才加了1万多次

为了对比为什么CPU计算速度这么快还是只打印1万多,证明是收到了闹钟信号:

一万多次肯定对于当前的CPU还是太少了,我们纯在内存当中做++:

真实情况下是把一个整数加到4亿多次:

至于有没有回绕呢?应该是没有的。
为什么会有这么大的数量级上的差别的呢?
输出的本质是IO,IO比较慢,这个IO是要经过网络的,要访问外设。
4亿多次是因为没有访问外设,直接访问了内存,内存级别的操作。
这样我们对CPU访问外设速度非常慢有了一个量化的认识。
我们验证了闹钟本身的一些操作。
如果我们设置一个20秒的闹钟,在第五秒的时候就把闹钟提前唤醒了,这种闹钟给一个进程只能设置一个,已经设置了,过了一段时间后悔了,要重设闹钟,下一次返回值就是返回上一个闹钟的剩余时间。
验证一下:

如果后悔了,设置闹钟为0秒:

就是返回上一个闹钟的剩余时间,0就是取消闹钟。
- 如下图,我们先设置
alarm(10);,再设置成alarm(0);之后,因为第一个闹钟是10秒,使用返回了10 - 0 = 10秒。

我们把闹钟取消了,收不到闹钟,就会死循环:

我们改一下代码:

再过n个2秒我们就再也收不到闹钟了。

这就说明,alarm是一个一次性闹钟,而且这个闹钟只对这个进程只会有同一个闹钟(从头到尾调用alarm就一个闹钟)。

我们可能要对闹钟进行重复设置,时间得卡好!
- 下面这样写是不行的

当我调用下一个的时候闹钟就收不到闹钟了------上一个闹钟根本没有响!
重复设置一定要保证上一个闹钟已经被触发了。
一定要写在handlersig函数里:

此时我们就可以每隔2秒使用一次闹钟:

这就是闹钟的返回值问题。

2.4.2 闹钟的应用场景
闹钟功能具有延时触发的能力。
Windows中的关机程序:
bash
shutdown -t 10 -s(带选项)
实现 延迟关机 的功能。

其实严格意义上这个工作不是闹钟完成的,而是定时器完成的。
有点类似于"看门狗"------
- 使用 alarm 实现看门狗(B进程)
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#define MAX_COUNT 100
#define SHM_KEY 0x1234
int *counter; // 共享内存指针
void watchdog_handler(int signum) {
// 每次闹钟触发,计数器减1
(*counter)--;
printf("watchdog: counter = %d\n", *counter);
if (*counter <= 0) {
printf("watchdog: no feed, shutting down...\n");
// 执行关机命令,如 system("shutdown -h now");
exit(0);
}
// 重新设置1秒后的闹钟
alarm(1);
}
int main() {
// 创建/获取共享内存
int shmid = shmget(SHM_KEY, sizeof(int), IPC_CREAT | 0666);
if (shmid < 0) {
perror("shmget");
exit(1);
}
counter = (int *)shmat(shmid, NULL, 0);
*counter = MAX_COUNT; // 初始值
// 注册信号处理函数
signal(SIGALRM, watchdog_handler);
// 启动第一个1秒闹钟
alarm(1);
// 进入无限循环,等待信号
while (1) {
pause(); // 等待闹钟信号触发
}
return 0;
}
- A进程喂狗
A进程只需要通过共享内存访问计数器,并定期重置:
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/shm.h>
#define MAX_COUNT 100
#define SHM_KEY 0x1234
int *counter;
int main() {
// 获取共享内存
int shmid = shmget(SHM_KEY, sizeof(int), 0666);
if (shmid < 0) {
perror("shmget");
exit(1);
}
counter = (int *)shmat(shmid, NULL, 0);
while (1) {
// 模拟正常工作
printf("working...\n");
sleep(80); // 每隔80秒喂狗一次(小于100秒即可)
// 喂狗:重置计数器
*counter = MAX_COUNT;
printf("feed watchdog, counter reset to %d\n", *counter);
}
return 0;
}


2.4.3 理解闹钟
理解闹钟,看看操作系统内同时存在多种闹钟(定时器)?操作系统要不要管理?怎么管理?先描述,再组织。
我们是先描述了:

怎么再组织呢?

- 如果未来5秒的闹钟没有响,那么后面的15秒的50秒的、150秒的一定没响。
只要关注未来超时时间最近的闹钟即可:
我们可以用什么数据结构来管理?
OS给目标进程发送 SIGALARM。
未来自己使用定时器应该会使用堆结构,这就是为了方便理解。
堆结构有缺陷,不能遍历,不能很好地支持取消。
- 闹钟,是单独硬件来计时吗?如果是CPU来计时的话,它不得很忙,一边处理进程,还一边计时。
问题1:闹钟是软件实现的!
问题2:闹钟计时取决于操作系统是如何运行的,操作系统也是个软件,谁运行操作系统?原因是,键盘可以产生中断,操作系统内部也存在一种硬件单元,晶振,也会触发中断,中断向量表,操作系统是基于中断的一种集合。

一般触发时钟中断的硬件周期是固定的,比如每隔一纳秒,因此在OS内部就可以定义一个全局变量记录中断次数,如果是100,等于说从开机到现在已经过了100纳秒了,次数 = 时间。时钟中断,token中断,操作系统能够算出来从开机到现在过了多少秒,5秒会变成5^9次!如果次数超过了就是超时。计算机里把时间转化成次数,计时不是由CPU计时的,由外部晶振的振动周期决定的。中断越强,CPU响应能力越强。
操作系统并不连续,而是一卡一卡的。
- 除0死循环,标志位没清,再次上下文时,要先检查
core dumped标志位,才运行吗
CPU已经识别出来了,也会走中断,走中断也会走中断向量表,也属于异常。
只要core dump一次就可以了。
自定义捕捉了就没有core dump了,没有走自定义捕捉就没有走系统那一套,就没有core dump了。
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux进程间通信:共享内存】为什么共享内存的 key 值由用户设置
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
