在我们使用Linux系统的时候我们经常会使用ctrl + c的方式来终止进程,也
会使用kill命令来杀掉进程,评判进程退出的健康程度中也有信号的身影。那
么Linux中的信号到底是什么?今天就由我来介绍Linux中的信号。
1. 信号的概念
要了解计算机中的信号,首先要认识生活中的信号。在我们的生活中,信号时时刻刻在我们的身旁:早起响的闹钟、上下课铃声,起跑时的枪令、过马路时的红绿灯等等,这些都是我们所能够接触到的信号。
那我们对这些信号的认知应该是什么样子的呢?
首先就是我们应该认识它们,知道它做出行为之后知道它的意思是什么,我们知道我们的行为将是什么。
那就意味着,我们在收到这个信号前,我们就已经知道这个信号的处理方式了(比如我们幼儿园时常教的红灯停绿灯行)。
信号的到来,我们并不清楚是什么时候,信号到来相对于我现在的工作是异步产生的(比如起跑时的枪令)。
信号到来时,我们并不一定要立即处理它,我们会在合适的时候处理(比如正在打游戏的时候,我们的外卖来了)。
那这也意味着我们要有一种存储这个已经到来的信号的能力。
以上就是对信号的认识,而计算机中的信号也同样是遵循着以上规律:
我们的Linux中就存在着这样的一张列表存储着Linux系统中所有的信号。而上面规律中的“我们”在计算机中指向的就是进程。稍后我会介绍Linux中是如何体现信号的规律的。
现在我们就能得出Linux中信号的定义:
信号是向目标进程发送消息的一种机制。
2. 信号的产生
a. 关于进程的一些指令
当我们运行这样一段代码时:
我们会发现,当我们向命令行中输入命令时,shell并没有做出任何反应。这是因为Linux中只能有一个前台进程,对于前台进程我们可以使用ctrl + c终止前台进程来让我们的命令输入再次有效:
我们也可以将前台进程切换成后台进程,让我们的命令可以使用:
其中让进程后台运行的方式就是在后面加个取地址符号。
我们可以使用jobs查看后台进程:
可以使用fg + 后台进程编号,将该进程切换为前台进程:
可以看到我们的命令行中输入的命令又没有作用了。
我们也可以使用ctrl + 暂停前台进程,但是由于Linux中前台进程只能有一个,所以我们需要将暂停的进程切换到后台然后使用bg + 编号继续运行:
那这段时间我们的shell去了哪里?其实操作系统会在我们执行前台进程,终止前台进程的过程中,将shell不停的从前后台中进行切换。而判断一个进程是否是前台进程的依据之一就是这个进程能否有能力接收用户输入。操作系统它本质上也是一个软件,那么操作系统运行软件管理进程,操作系统又由谁来运行呢?这个我们稍后再解释。
b. Linux中信号的灵感
不知道大家有没有想过这么个问题,在我们使用键盘鼠标的时候,计算机怎么就知道我们的键盘中有了数据需要读取呢?难道是采用轮询的方式一直扫描机器的外设看看外设中是否有数据可读?这种方式未免太消耗计算机的时间了。
我们知道计算机中的CPU主要是由两部分组成:运算器和控制器。运算器主要是进行系统的运算,而控制器有一部分作用就是控制外设,CPU中存在着许多带着编号的针脚,这些针脚可以理解为其中一部分直接连接着我们的外设(其中肯定不是直接连接,它们中间还有8259)。这些针脚另一端就是寄存器,当我们的外设有数据输入时,它们会发送信息然后转化为光电信号传给我们的针脚另一端的寄存器中。而这些带有编号的针脚的编号也是有意义的,计算机在启动的时候会生成一张表,这张表中记录了外设的读方法,所以当我们的外设有数据输入时也就是寄存器接收到光电信号时,操作系统会让CPU执行那张表中的对应外设的读方法。而那张表其实也就是一个函数指针数组,而访问对应的外设读方法也就是通过编号作为下标映射来访问。
这种通过外设发送信号给cpu然后操作系统再调用对应的读方法,这种方法叫做中断。
针脚的编号我们叫做中断号,而那张记录对应特定外设的表叫做中断向量表。
我们再次观察信号列表,我们会发现它没有32和33号信号。其中1到31号信号成为普通信号,后面的信号称为实时信号。我们主要研究普通信号。
对于普通信号而言,当一个进程收到信号之后,这个进程起码要表示它是否收到了这个信号。那么如何表示呢?它会在进程pcb结构体中维护一个位图:位图的1/0表示信号的有无,位图的位数表示信号的编号,那么有了信号我们还应该知道对应信号的处理方式,而我们的每一个进程也都会有一个函数指针数组,里面记录了收到对应信号的处理方式,而信号的编号也跟这个数组的下表是对应的。
了解以上流程之后,我们发现键盘读数据的方式好像和信号的处理方式有些相似。其实Linux中的信号就是根据中断技术模拟实现的,只不过信号的中断更多是软件层面的。
c. 信号的产生方式
1). 键盘产生
我们所使用的ctrl + c等等我们发现它不会被操作系统识别为简单的输入,而是以组合键的形式输入。其实它们会被处理成信号,例如ctrl + c终止前台进程(对应二号信号),ctrl + z暂停前台进程(19号进程),还有一个ctrl + \终止前台进程(三号信号)。
我们可以验证以上说法:
我们对于信号的处理有三种方式,默认处理方式,忽略,和自定义处理,而这个signal函数就可以实现自定义处理,它的第一个参数是对应自定义的信号编号,第二个参数是一个函数指针,记录着用户自定义处理的方式。我们可以使用这个函数来验证以上说法:
我们先用对应的信号分别终止进程:
然后我们使用组合键终止进程:
ctrl + c
ctrl +
ctrl + z
我们发现组合键终止或暂停程序确实是对进程发送了信号,但是我们也发现19号信号好像没有被修改自定义,这是因为信号中有一些信号是不可被修改的,这样的信号还有9号信号等等。
我们可以使用man 7 signal查看信号的默认行为:
其中Core和Term一般都是终止进程的意思
2). 系统调用
有一些系统调用也可以产生信号。
abort:
这个就是直接终止自己了。对应六号信号
raise:
它可以指定信号让操作系统发送给自己。
c. 异常
当我们的代码中出现除零或者访问空指针的时候,就会出现错误,操作系统就会给我们发送信号,终止进程:
对应我们八号信号。
对应11号信号。
对于空指针错误来说,我们访问了0地址,那里本来就是不可被访问的,就算能访问,也会由于页表中没有相应的映射从而触发中断引起异常。我们知道cpu中会读取我们地址空间的地址进而将地址空间转化为物理地址再去内存中读取,那么这个转化的过程就由CPU中一个叫MMU的东西来进行,它其中会有一个标志位来记录是否转化异常,如果异常的话,它会置为1,操作系统就会检测到这个标志位然后向该进程发送信号终止该进程。
对于除零错误,为什么会除零错误,原因是数学层面不允许除零,除零无意义,但是计算机是怎么处理的呢?我们的CPU中会有一个寄存器status,它表示了该次运算的健康程度,当除零时它的身上也会有一个标志位,叫做溢出标志位,会被置为1,操作系统得知后它就会给对应进程发送信号终止该信号,那么此时我们就会有一种有意思的现象:
我们这个程序中可是没有循环的,但是其中却出现了循环的现象。这是因为我们修改了八号信号的处理方式改为自定义捕的方式,而自定义方式中,并没有直接退出进程。那么本来操作系统当看到status寄存器中的溢出标志位为1之后,就会向该进程发送八号信号终止该进程,然后继续调度其他进程,我们知道进程的调度就是将寄存器中的内容也就是硬件上下文切换为其他进程的。我们并没有退出这个进程,所以当我们再次调度这个进程的时候status寄存器中的溢出标志位仍是1,那么操作系统仍旧会发送八号信号给该进程。这就是为什么会造成循环的原因。
这是硬件方面的异常。还有软件方面的那就是管道,当读端管道关闭之后,写端进程会直接收到操作系统的13号信号而直接关闭。
d. 软件条件
我们上面所谈论的都是程序出了问题之后再终止程序的,但是也有些是不是因为出现问题需要终止进程的,而是特定的做某些事,比如在一个时间段做某些事,那么这时候就有了一个信号:
14号信号,也就是闹钟,它也有一个接口:
这个接口的意思就是在进程启动seconds秒之后终止进程(默认)。
我们的众多进程中可能会有多个进程使用闹钟,那就意味着闹钟也是需要被管理的,管理的方式肯定还是先描述再组织,我们可以大概想想这个结构体中应该会有什么:进程的pid、闹钟的目标时间、等等。我们的时间总要有个参照物,那就是时间戳。那如何管理呢?用链表的形式每秒遍历吗?操作系统不会做这种低效率的事情。所以我们可以采用小根堆的形式,每次只要查看堆顶的时间到了没就可以管理好所有的闹钟了。
而我们利用闹钟也可以发现一些现象:
稍加修改:
从这其中我们也可以看到与硬件大量交互的速度相比于CPU有多慢了。
以上就是信号产生的几种方式,我们可以看到无论信号以何种方式产生,最终都是由操作系统发送信号给对应进程,这是因为操作系统是进程的管理者。
3. 操作系统运行的原理
我们操作系统可以调度所有进程,运行程序等等一系列工作,那操作系统本身也是一个软件,它也是一个进程啊,它又由谁来运行呢?
其实操作系统它就是一个死循环,它会在做好所有前置准备之后进入死循环,然后开始调度运行其他进程。
我们前面说了,CPU中有针脚会连接外设利用中断的技术进程外设数据的读取。而还有一个针脚它连接着一个装置,这个装置它又连接着我们的时钟芯片。
我们的电脑在关机几个星期你打开之后它的时间可能还是正确的,那如果你关机几年呢?就不一定了,那为什么我们的电脑关机之后,我们再打开我们的电脑的时间还能是正确的呢?这就是因为我们的计算机中有些设备是有自己的小电源的,类似于时钟芯片它就会有自己的供电方式(一个小的纽扣电池)。
我们上面的装置叫做CMOS,它会根据时钟芯片来以周期的非常高频的时钟中断的方式来一直刺激CPU,它既然连接了针脚,那它就会有对应中断向量表中的读方式,但是这里并不是读的方式,而是操作系统的调度其他进程的方式也就是操作系统的代码,而其中的高频也叫主频也是衡量计算机性能的一个标志。这样我们的的操作系统就会一直被CPU所运行,它是由硬件来催动着它运行的。我们所说的操作系统的前置准备其中有一个就是中断向量表的构建。而这个前置准备也叫做各种中断的陷阱的初始化工作。所以,操作系统的执行是基于硬件中断的。