Linux:进程信号的产生以及操作系统理解

文章目录

  • 通过键盘产生信号
  • 通过系统调用产生信号
  • 通过异常产生信号
  • 通过软件条件产生信号
  • 总结
  • 操作系统中的时间
  • 对操作系统的理解

本节要总结的话题是关于进程信号的产生

关于进程信号的产生,可以通过键盘产生信号

通过键盘产生信号

Linux:进程信号的产生以及操作系统理解_第1张图片
上述展示的是信号列表,在Linux中无非就是上述的这些信号,而每一个信号都对应有一个特殊的数字,这个数字和信号之间的关系就是宏和数字之间的关系,在操作系统内部,这些信号本身被定义的时候其实就是用define定义宏的方式来进行定义的,而从上述的表中可以看到的一个直观点是,没有零号信号,为什么呢?

在之前关于进程等待的篇章中有过,对于进程,它退出无非有两种信息,一种是收到的信号编号,一种是退出的时候收到的退出码,而父进程也是会通过这样的信息来关注子进程的退出健康情况,如果子进程在运行的过程中没有收到任何的信号,表示代码是正常跑完的,但是结果是否正确,可以通过进程的退出码来查看,如果退出码是0,表示正确,如果不正确的原因是什么,这都是可以知晓的内容,从这个角度来看,信号就不应该有零号信号,如果有零号信号还如何去进行表示进程到底是否是正常的退出呢?所以在设计的初衷角度来讲,没有零号信号其实也是在意料之中的事

从上述的列表中看到,不光没有零号信号,在中间也会出现断层,而这之间的断层把信号列表分成了1-31为一个模块,其余的是另外一个模块,而对于从1-31这个模块有31个信号,这些信号也被叫做是普通信号,剩下的信号都被叫做是实时信号,这里对于实时信号不做过多讲解,主要是研究普通信号的各种机理和性质,而对于实时信号,和普通信号的特点是不会出现信号丢失,以及需要操作系统立即处理这两个方面,对于后续的更多描述内容,在研究结束普通信号后再进行解释

那么对于普通信号来说,有很多是认识的,例如有2号信号,9号信号,以及管道信号:如果对于一个管道来讲,把读端关闭了,写端还会一直写吗?肯定是不会的,这个时候操作系统就会向写端发送一个SIGPIPE信号,从而终止进程

那这些信号是如何进行后续的处理的?本质上是操作系统在内部会维护一张表,这个表就是一个函数指针数组,其中蕴含的是各种信号对应的处理方法,而信号本身的编号和数组的下标有强相关的联系,这些内容在前面一节的内容已经有所提及,这里为了保证信号产生的完整性,重新进行编写完善

那操作系统是如何向进程发送信号的?

操作系统可以向进程发送信号,这些信号并不是立刻处理,所以这就意味着进程本身是需要具备某种存储信号的能力,在合适的时候进行处理,所以如何理解这个过程呢?

在进程的PCB中,是需要对于信号做管理的,如何进行管理,本质就是维护了一张位图,位图中的0表示这个位置没有收到对应的信号,位图中的1表示这个位置收到信号了,那么这个位置其实就代表了信号的位置,根据这个信号的位置就能进行判断这是几号信号,对应的这个信号到底有没有收到,因此在进程的task_struct中也必然会有这样的字段

Linux:进程信号的产生以及操作系统理解_第2张图片
基于这样的结构,就实现了对于信号的临时存储,以便于处理当前信息后,对信息做出进一步的处理

而在实际的进程设计中,可能一个进程会收到很多的信号,那么与之对应的一个设计思路就是,设计一个节点中存储的是信号的编号,里面还包含有信号处理方法的函数指针,最后在PCB中只需要维护一个队列,此时对于进程发送信号就是把对应的节点信息存储到队列中,让进程去处理,这样的过程就是用链表实现队列从而进行信号相关信息的处理,这就是实时信号的处理基本模式

所以,归根结底来说,对于每一个进程都有一张函数指针数组表,数组下标就是信号的编号,其次,每一个进程对应的PCB中都会有一张位图,这个位图里面存储的信息就是当前是否收到了对应的信号,未来对于进程是否收到了某种信号,只需要借助位图就可以确定到底是不是收到了对应的内容,那么现在知道了收到了某种信号,又该如何进行处理对应的信号呢?换句话说,如何理解操作系统向目标进程发信号呢?

其实,所谓的发信号,应该叫做写信号,操作系统作为内存的管理者,为了管理进程所以创建了对应的PCB这样的结构,那么操作系统自然拥有向位图中写信息的能力,因此操作系统只需要在位图中进行对应的信息的写入,就可以完成一个信号发送这样的目的

通过系统调用产生信号

下面是通过系统调用来产生信号:

在这里插入图片描述
这个也比较好理解,用指定信号对于指定pid发送信号

// kill系统调用的使用
// ./process -9 [pid]
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cout << "用法错误" << endl;
        return 1;
    }
    int signnumber = stoi(argv[1] + 1);
    pid_t processid = stoi(argv[2]);
    kill(processid, signnumber);
    return 0;
}

在这里插入图片描述
本质上,对于bash中使用的kill -9命令,其实底层就是调用了这个系统调用,这个很好理解,不再说

在这里插入图片描述
下一个介绍的是对于raise的调用,这个调用是自己对自己发送指定的信号,测试代码如下:

void handler(int signo)
{
    cout << "收到了" << signo << "号信号" << endl;
}

int main()
{
    signal(2, handler);
    while(true)
    {
        raise(2);
        sleep(1);
    }
    return 0;
}

Linux:进程信号的产生以及操作系统理解_第3张图片
结果自然也是符合预期的,会不断的接受到2号信号,并进行对应的处理

在这里插入图片描述
下一个是对于abort函数,这个函数其实也不陌生,它的作用就是令对应的进程异常终止,在前面的列表中也有这个函数的踪迹

int main()
{
    cout << "准备终止" << endl;
    sleep(2);
    abort();
    return 0;
}

Linux:进程信号的产生以及操作系统理解_第4张图片
对于这个函数,一个特殊的一点是,它允许被自定义捕捉,但是依旧会进行强制终止,示例代码如下:

void handler(int signo)
{
    cout << "收到了" << signo << "号信号" << endl;
}

int main()
{
    signal(6, handler);
    cout << "准备终止" << endl;
    sleep(2);
    abort();
    return 0;
}

Linux:进程信号的产生以及操作系统理解_第5张图片

通过异常产生信号

在代码正常运行期间,可能会因为一些异常导致进程的终止,例如有空指针解引用,或是除零错误,这里以空指针为例,当要向零号地址写入信息时,对于当前的进程来讲,其实是没有建立从零地址到内存之间的映射的,所以在进行写入的时候要发生缺页中断,此时在缺页中断的过程中,进程就被终止了

那么该如何理解异常呢?报错的原因又是什么呢?这些该如何进行理解呢?

首先,要明确的观点是,除零会出现错误这是数学上进行规定的,不管什么数据都不能除零,这是约定俗成的规则,因此c语言本身自然也遵守这个规定,那么为什么进程会报异常的错误呢?这是由于CPU内部的原因,在CPU的内部存在很多的寄存器,比如有eax,ebx,ecx等等,这些叫做通用寄存器,比较简单,那么当进程需要做出除法的时候,本质上就是把对应的数据拿到计算机中,比如有10/0这样的代码语句,那么在寄存器中就会存储10,存储0,还有操作类型除法,这些都会存储在寄存器中,除了这些简单的寄存器外,还会有例如说状态寄存器这样的寄存器,它的作用就是用来衡量运算的结果是否满足一定的标准,这只是它众多标准中的一种,但是这样的寄存器的一个作用就是用来衡量当前的操作是否符合某种标准

在状态寄存器中,会存在一个比特位,这个比特位标记的就是本次在CPU的运算中,是否存在溢出的行为,这个标记位也被叫做溢出标记位,当CPU在进行运算的时候,如果出现除零的情况,那么结果自然会出现溢出的情况,所以在寄存器中的溢出标记位直接就置为1了,运算的结果也就没有意义了,所以在硬件的角度来讲,一旦出现除零的操作,那么在CPU内部的状态寄存器中的溢出标记位就会变成1,所以在硬件的角度来讲,就能体现出除零错误本身

有了这个概念,在实际的运算中,操作系统会调度这个进程时,把这个进程放到运行队列中,就会有对应的资源来进行数据的处理,但是当操作系统出现问题,此时硬件上也会有对应的标记出现,此时就发生了终止

对于CPU来讲,它可以接受来自外部硬件向针脚发送的信号,此时CPU就知道外部的设备已经准保好了,就可以执行操作系统内部的一些中断方法,同样的道理,在CPU内部的一些针脚或者是功能版块,其实并不需要知道外部的通知,如果在CPU的内部出现了错误,它可以直接告诉操作系统,当前的运行状态中出现了问题,同时把出现的问题的信息传递给了操作系统,操作系统就会执行中断对应的方法,一般而言,在CPU的内部一旦出现异常,它是可以想办法告诉操作系统当前对应的标记位出现了溢出的问题,由此操作系统就会把这个信息转换成kill命令,对于目标进程发送信号,所以在目标进程中,此时就收到了来自操作系统的信号,而对于信号的处理方式大多就是默认的处理方式,由此就把硬件问题转换成了信号问题,直接让进程终止

由此可以看出,表面上c语言写的代码如果除零会报错,但是实际的背后,会有这么一系列的内容执行,当程序运行除零之后,CPU内部的状态寄存器标志位就会标记为1,之后计算结果出现异常,CPU的内部出现异常,会直接通知给操作系统,操作系统就会向引发该错误的进程发送对应的信号,这个进程就会终止,所以说,键盘是硬件,CPU本身也是硬件,操作系统是硬件的管理者,它必须要知道对应的硬件健康状态,当CPU出现报错的时候,实际上就是这个CPU不再健康,那么操作系统必然要立刻对这样的行为进行处理,所以就会强制终止这个程序,这都是合乎常理的逻辑

在kill列表中,是存在这样的信号的,八号信号就是我们要找的信号,那么现在对八号信号做出自定义捕捉,会发现神奇的现象:

void handler(int signo)
{
    cout << "收到了" << signo << "号信号" << endl;
}

int main()
{
    signal(8, handler);
    int a = 10 / 0;
    return 0;
}

运行结果却是意料之外的,会在一瞬间立刻打印非常多的收到了8号信号,明明代码中只写了一次,为什么会一直处理这个问题呢?

其实原因在于,代码本身是没有问题的,操作系统是CPU的管理者,而寄存器中的内容是属于进程本身的,所以当出现了除零错误的时候,操作系统默认是终止这个进程的,因为终止进程本身就是处理异常的手段之一,这是可以理解的,而只要这个进程被杀掉了,那么在寄存器中就会把标识寄存器的标识位从1变成0,也就是说恢复成原来的字段,也就表示这个异常处理结束了,而在上面的代码中,对于自定义代码,并没有使用对应的退出进程,而是只是简单打印一句话,那么就意味着这个进程不会因为报错而退出了,所以只要CPU还在调度这个进程,那么CPU就会一直告诉操作系统,这里有一个错误进程,赶快处理一下,而操作系统收到这个消息后就会发送一个八号信号,当然这个信号自然不会被处理,只是会打印一句话,操作系统就认为自己的工作已经做完了,但是这个进程却没有被杀掉,还是会被一次次的调度,当上下文被寄存器恢复后,此时会发现标识寄存器中存储的还是1,那么就会继续向操作系统呼救,请求处理这个进程,操作系统就会发信号…

这样的循环往复,实际上就是完成了一个异常转换成信号发送给进程这样的一个过程,只是这个进程的处理动作是打一句话,而CPU希望的执行是直接杀死,所以就会一次次的调度,放下来再放上去,一次次的调度,每次都打印一句话,所以会满屏打印收到了信号

总结一下,根本原因在于,CPU识别到进程异常后就不再执行后续代码,会通知操作系统处理一下这个进程,操作系统却只是打一句话,CPU继续调度,识别到进程异常后就不再执行后续代码,会通知操作系统处理一下这个进程,操作系统却只是打一句话…

因此,引发进程异常的本质,肯定是因为进程引起了硬件的问题,而硬件的问题被操作系统知道了,操作系统就会把异常问题转换为信号问题,向目标进程的位图中发信号,进而来处理信息,这也是可以理解的内容

通过软件条件产生信号

这个条件比较奇怪,要引入的内容是闹钟问题

在引入这个问题前,先回顾之前对于匿名管道的问题,如果对于管道的处理,只是关闭读端,写端还在写,那么此时操作系统就会向写端系统发送信号,终止写端进程,为什么呢?因为操作系统不会做没有意义的事,本身已经没有读端了,只有一个写端是没有什么意义的,所以会杀掉,但是问题在于,操作系统是如何判断出需要杀掉写端呢?这中间是如何进行运转的

在之前的条件中知道,如果是在CPU运行或者是在页表的MMU硬件运行,的的确确是可以检测到的,因为这都隶属于硬件向操作系统申请,请求发送信号,但是对于管道来说,并不属于硬件的范畴,对于管道的写入,是一个纯软件的层面,因此就引入了软件条件

当判断管道的读端是不是被关掉了,其实就是判断管道文件的读文件描述符是否被打开了,它是一种软件的条件,没有检测CPU的寄存器和一些特定的结构,而是操作系统会去看,对应管道的内核数据结构已经不存在了,所以就把整个进程杀掉了,因此,这种因为软件问题而向目标进程发送信号的这个情况就被叫做是软件条件,前面对于异常的处理基本都是出现在硬件的层面上,而对于管道来说和硬件没有任何关系,操作系统识别到管道的引用计数有所改变,当读文件描述符不存在了,此时就会把对应的文件结构体对象释放,操作系统识别到都没有对应的内核数据结构了,那读端也就别读了,直接终止进程,由此得出初步结论是,操作系统发现软件条件并不满足自己的规定,不满足软件条件,此时就向特定的进程发送对应的终止信号

操作系统是软硬件资源的管理者,所以如果硬件出现问题,操作系统会处理,那么软件出现问题,操作系统也理所应当去处理

有了这一层的铺垫,下面引入的是闹钟的话题,在之前的内容中,虽然大多数信号都是异常产生的,但是也有一部分的场景是可以产生信号的,换句话说,当一个进程异常了,绝对是会收到信号,但是收到信号并不意味着是因为异常,也可能是单纯的发出了一个stop指令,先停下来,后续可能会继续执行,而对于除了异常情况下,收到的常规的信号,其中就有一个接口是alarm系统调用

这个系统调用可以在系统中设定闹钟
在这里插入图片描述
Linux操作系统设定闹钟的原理是什么?如何理解这个闹钟的存在呢?

在Linux中设定一个闹钟,相当于是在计算机中设定一个未来的时间,当到达预计的时间后就会提醒,有些类似于Windows下的定时开关机的功能,而有了这样的概念,对于操作系统来说,闹钟就是一个到了某个时间就提醒用户,而对于闹钟的管理也会使用一张链表管理起来,而使用的时间可以使用时间戳的概念

Linux:进程信号的产生以及操作系统理解_第6张图片
在这个模块中,可以看出有对应的节点信息和函数指针,函数指针中存储的就是到达时间后要执行的操作

如何鉴别超时

闹钟如何鉴别超时,只需要对于闹钟进行排序即可,头部的节点没有超时就都没有超时,如果头部的节点超时了就转到下一个继续判断

所以此时,就把闹钟这个结构体对象转换成了一张有序链表,用有序链表的方式链接起来,由于需要保证是有序,还可能会涉及到插入和删除,因此就需要引入的是一个数据结构—堆

demo代码如下:

int cnt = 0;

void handler(int signo)
{
    cout << "收到了" << signo << "号信号" << endl;
    cout << cnt << endl;
    exit(0);
}

int main()
{
    signal(14, handler);
    alarm(2);
    while(true)
        cnt++;
    return 0;
}

上述代码演示的就是alarm的作用

总结

软件条件也可以产生信号,对于信号来讲,不一定非要是异常才会产生信号,例如操作系统内部的某种软件触发了对于信号的设定,一旦软件条件满足,闹钟响了,进程依旧会收到信号,这就是信号产生的第四种方式,软件条件,并且由此看出,这个闹钟条件也是很正常的条件,没有出现任何问题,就是很正常的闹钟,而管道的触发也是软件条件,只不过那个是对应的异常条件

总结

如上就是对信号的四大产生方式,简单总结一下就是键盘产生,系统调用,异常,软件这四种方式,如果再加一个指令触发,其实可以归结到系统调用中,但是不管是什么方式触发,要明确的一个核心观点是,只有操作系统才能触发,操作系统才有资格向进程中写入信号

操作系统中的时间

下面要探讨的问题是一个有趣的问题,关于时间的问题

有这样的现象,当计算机开机后,即使不联网,也依旧可以正确的获取到现在的时间,这是为什么呢?如何解释这样的现象呢?

在电脑关机的时候,实际上电脑中还有一个小的纽扣电池,这个纽扣电池会一直给计算机中的某些硬件进行供电,而这些供电就是会给这些硬件发送特定的信号,这些硬件可以看成是一个计数器,而发送的这些信号就是在不断的进行计数,而当计算机重新开机之后,现在过了多长时间,就会可以通过计数器转换成时间窗,时间窗就可以转换成现在所看到的时间,这是可以做到的,换句话说就是,计算机关机,并不是真关机,在内部还有一个小的电池在给一些电路板供电,进行一些记录时间的工作,这说明计算机内部天然有一些用来时间处理相关的模块硬件上,更重要的是,可以周期性触发一些统计时间模块的信号,以用来进行相应的时间统计,这也就是一些远程开机的原理

最终想说的一点是,用户的行为最终都是用进程的形式来表现,会通过各种各样的手段转换成进程的形式来体现,操作系统的管理工作,也就是把这些进程都控制好,管理好,调度好,这就是操作系统的本职工作,要做好对于不同进程的资源分配的工作,有些核心的资源,要对于不同的进程进行不同的分配,这就是一个衡量操作系统的一套标准,那有没有一些合适的硬件来帮助处理呢?

CMOS

CMOS是一个硬件,它的主要工作是周期性,高频率的向CPU发送时钟中断,也就是说电脑中有一个硬件,它会非常快速的向CPU不停的发送信息,频率非常快,因此对于CPU的评判标准的其中一个指标就是CPU的主频是多少,这个就是衡量CPU执行指令的速度和同一时间内接受指令的数量,但是这并不是唯一的衡量标准,在实际的比对性能中还要参考很多其他内容

对操作系统的理解

对于操作系统来说,一种朴素的理解是,操作系统就是一个死循环,一旦启动后,就意味着前面的初始化工作都做好了,此时就把自己进行暂停状态,紧接着在计算机启动的时候,会创建一个中断向量表,CMOS是一个小的硬件,它会不停的给CPU发送中断,也就是时钟中断,发送后一旦触发,就会有对应的时钟中断的编号,比如编号是6,此时操作系统中会有一张中断向量表,那么这个中断向量表就会索引到编号为6的下标,此时就会执行编号为6对应的执行方法,这个就是从左系统的调度方法

所以只有在非常高频率的不断触发下,CPU在收到之后就会调用中断向量表中的方法,执行操作系统对应的调度方法,此时进程就在硬件的驱动下进行调度了,而被调度执行的调度方法是在操作系统写的,最后这个方法被CPU执行,进程的调度,以及基于时间片的轮转,以及开始执行CPU操作系统的代码,在外部看来是操作系统在运行,实际上是在CMOS的驱动下,操作系统才会运行的

与此同时,操作系统中含有各种各样其他的硬件,这些硬件都有唯一的针脚与之对应,有的是键盘的,有的是网卡的,都是一一对应的,而这些方法都可以在中断向量表中找到对应的处理方法,从此之后,计算机在执行用户进程的任务之后,就不需要进行过多关注了,硬件会推送着操作系统执行对应的调度方法,执行对应的方法,硬件上哪个资源就绪了,就可以根据这个硬件对应的中断信号去执行对应的方法,所以得出的一个观点是,操作系统是基于中断的

换句话说,再跳出前面的逻辑圈,简单说就是操作系统是一个死循环,启动之后就卡在那不动了,而只有外部的信号传来“刺激”,也就是中断,才会去执行对应的代码,而有一些是间歇性的,比如键盘显示器,有些是必须一直要有的,这个就叫做CMOS,在这样的刺激下,就会根据中断编号调度对应的执行方法,从而调度操作系统内部已经写好的代码,比如检测对应的时间片有没有到,就直接return,如果时间片到了,就把当前进程从CPU上剥离下来,再从运行队列中找一个进程放到CPU上执行,而在CPU内部存在有对应的定时器,定期进行时间中断以保证可以对进程时间片可以进行顺利检测,保证进程的调度机制正常运行

所以操作系统在启动之前的准备工作是很重要的,包括对于硬件的检查和准备工作,只有在这样的基础下,进行各种中断的陷阱的初始化工作,操作系统内部一定会有一张很大的表,而最终一定是把操作系统的各个模块打散到对应的中断向量表中来进行整个操作系统的工作

你可能感兴趣的:(Linux,知识总结,linux,服务器,运维)