目录
一.信号发送的概念
首先来讲几个发送术语:
它有三种情况:
注意:
二.信号在内核中的表示示意图
三.信号捕捉
所以总结一下:
此时,会出现这样一个疑问:操作系统是如何得知现在被执行的进程是用户态还是内核态?
问题2:CPU在执行某个进程时是如何跑到操作系统中执行代码的呢?
用户态的进程想要转换为内核态的情况大致有3种:
如下为进程的两种状态转换图:
关于第三步有人就会问了,为什么内核态的进程不能够去执行handler方法,而是还要变成用户态才去执行?
关于第三步完成后,调用handler表中的方法后,为什么不就在用户态中继续往下执行,还要再回一次内核态?
在上一篇博客中,我以列举生活中的例子为基础,介绍了对信号的理解,信号的4个重点知识,信号的4种产生方式,且信号只能由操作系统发送给进程......
接下来,我继续来详解一下操作系统是怎么将信号发送出去的!
一.信号发送的概念
首先来讲几个发送术语:
1.实际执行信号的处理动作称为信号递达(Delivery);
2.信号从产生到递达之间的状态,称为信号未决(Pending);
3.对于操作系统发送来的信号,进程可以选择阻塞(Block) 该信号。
对于信号递达的理解:——是指进程收到信号后将其处理时的动作!所以进程在执行递达时,有三种方式可供选择:
1.执行默认处理;
2.自定义处理(例如:采用signal函数,做想要做的行为);
3.忽略操作(默认不管)
信号的产生(可以是由键盘的手动产生:ctrl+c、可以是系统调用kill()函数产生、可以是硬件异常产生:除零错误和段错误等、也可以是软件条件产生的信号:读端关闭写端仍开),所以信号在被OS操作系统指定目标进程发送开始到进程接收信号的这段时间称之为信号未决。
它有三种情况:
1.操作系统向进程发送了信号,进程收到了信号还没来得及处理,该进程可能在做着更重要的事情;
2.操作系统向进程发送了信号,在此途中,进程并没收到信号;
3.操作系统向进程发送了信号,进程收到了信号,但提前进行了对该信号阻塞,导致进程无法递达处理该信号。
注意:
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,进程才会执行递达动作;
阻塞和忽略是不同的,只要信号被阻塞,那么意味着进程永远不能递达(处理)这个信号;而忽略是进程递达(处理)这个信号的一种方式。
上图就好比一块电路板,我们打开了电源,插座也插上了灯泡接口,但灯就是不亮,原因在于灯泡中的保险丝已经在通电前就被熔断了——保险丝的熔断阻塞了电流通向灯泡的道路。
进程进行默认处理信号的案例:
该进程的执行方式就是采用默认的动作,遇到错误就立即终止退出。
进程进行自定义处理信号的案例:
进程所作的递达动作是:自定义方式,该方式是通过调用signal函数,然后执行该函数的第二参数——回调函数,进而对该信号所想做的一些事情——Checksig();
忽略动作就不展示了......
所以对于信号递达、信号未决、阻塞等术语还不理解的朋友们来看下面这个例子:
小明是个初中生,当晚自习快结束时,班主任兼物理老师因为家中有急事,给同学们布置了一套卷子,他把卷子放讲台上并让同学们下了自习拿回家做,就急着走了。同学们都记下了这件事,等到下课,小明并没有拿上卷子,转身就骑车回了家。他这么做的原因在于:上次期中考试时,他的物理成绩考的不像样子,被班主任劈头盖脸的骂了一顿,他心生怨恨,根本不想写与物理有关的一切作业。
根据上面这个例子可知:小明相当于进程,班主任是OS操作系统,是小明的管理者,班主任让小明拿卷子回家做(OS发送信号给进程),小明故意没有拿(进程阻塞了信号的传递),小明只要一直怨恨着班主任,他就一直不做物理卷子(阻塞导致进程无法递达该信号)。
这种情况就属于阻塞——信号未决的其中一种情况;
信号未决的第二种表示情况: 小明拿到了物理卷子,但是回家路上出车祸了,住进医院导致没来得及做卷子;
信号未决的第三种表示情况:老师正要把卷子放到了讲台,还没告知同学们的时候,地震了,老师和同学们就都走了。
而递达表示:物理老师当天下午又叫小明谈话,这一次他俩冰释前嫌(没了阻塞),晚上发了卷子后,小明拿了物理卷子,回家认认真真做完(递达的默认动作);
递达的忽略:小明拿了物理卷子,回家后由于数学英语作业明天要讲,导致他没时间做物理卷子;
递达的自定义:小明拿了物理卷子,回家后开始做卷子,但由于上课没有好好听,导致很多题不会做,瞎写答案。
二.信号在内核中的表示示意图
根据上图,做出以下理解:
操作系统在指定目标进程后向该进程发送信号,紧接着该进程的PCB(进程控制块),它的底层内核数据结构:task_struct中的pending(位图结构)、block(位图结构)、handler(函数指针数组)三个属性就会被激活。
先来看前两个属性,pending未决位图和block阻塞位图,两个位图结构的底层都是由32比特位形成,从右往左由低到高。这32比特位默认情况下都为0;每个比特位都代表了一个信号,例如两个位图最右边的那个比特位是第一比特位,该比特位指的是1号信号,往左依次为2号信号、3号信号.....到第31号信号。
pending未决位图的作用是显示:OS发送的信号是否被进程所处理。举个例子:由于进程在执行过程中出现了非法错误,使得OS查出错误后向该进程发送了4号信号,该进程的pending位图中从右往左第4个比特位由0--->1,直到该进程收到4号信号并开始处理这4号信号时,pending位图的比特位才会由1-->0。
block阻塞位图的作用是显示:OS发送的信号是否已被进程所阻塞。举个例子:OS向指定进程发送4号信号,进程提前设置阻塞位图的第4比特位0--->1,该进程收到信号后无法处理4号信号。什么时候进程解除了阻塞,block阻塞位图的第4比特位什么时候才会由1--->0。
接下来就是第三个属性——handler表,handler是一个函数指针数组,在该数组中,数组的下标表示信号的编号,例如数组下标0标识为1号信号......;数组元素的内容代表着对应信号的处理方法,当我们访问数组中的[0]、[1]、[2]时,访问的是handler的函数指针,每一个函数指针都指向对应信号的处理方法。
当我们来到handler时,已经能够表明进程已经成功收到了操作系统发来的信号,并正在对其进行递达处理,上面说过,递达分为三种方式:默认、忽略、自定义。在下面这个内核图中:
第一行的SIGUP信号,它的处理方式为:SIG_DFL(默认处理——宏定义),意味着handler[0]的内容写的是SIG_DFL函数(立即终止进程,并退出);第二行SIGINT信号(2号信号),它的处理方式是SIG_IGN(忽略动作——宏定义),意味着handler[1]的内容写的是SIG_IGN的方式,对该信号的处理方式是不处理。而第三行SIGQUIT信号(3号信号),它的处理方式为用户自己写的自定义方法signal(signo,sighandler)函数,意味着handler[2]中填入的内容是用户采用sighandler函数的函数地址。
在这里考虑一种特殊情况:当一个进程收到了OS发送来的多个信号时,由于进程只记录一个,所以pending位图一次只能置一个比特位,剩下的信号就会被丢弃。
注:上面这种情况只针对普通信号的发送,而对于实时信号的发送(Linux中第34——第64号信号),进程会对这些信号组成一个队列,该队列会保存这些信号的属性信息,等待进程一个一个的处理。
三.信号捕捉
当进程收到信号时,进程不会立即处理,而是在“合适的时候”。这个合适的时候是指:从内核态返回用户态时,进程才会进行处理。
用户想要访问上图的两种资源,就必须采用系统调用函数,系统调用是操作系统给用户提供的接口。普通用户无法以自己用户态的身份去访问系统调用资源,得先让自己的状态变成内核态才能执行。
详解如下:
1.当CPU执行进程自己的代码时,进程当前处于用户态,直到CPU执行了系统调用函数(open、getpid、fork)等,进程就必须转换为内核态,因为这些接口是属于OS的,CPU想要访问别人的资源,就得提升到能和别人平起平坐的身份才有资格。所以状态的转换是必然的。
在转入内核态后,CPU就可以访问系统的资源了,这时代码中有open函数,于是CPU开始访问该函数,好比它在门外敲了敲open,请求open的帮骂,OS知道后将open的底层实现代码拷贝了一份给CPU。
即使你的身份是内核态,但操作系统不相信任何人,它只相信它自己!你的身份是用户态便没有访问需求的资格;当你是内核态,有了资格也无法亲自访问,操作系统怕你乱搞,于是它帮你拷贝相应函数的代码给你。
需要注意的是:这里只是做了状态转换,访问系统接口的人仍然是CPU,只不过身份变了而已。
2.CPU执行系统调用是费时间的,因为有状态的来回转换,会导致执行效率降低,所以尽量不要频繁的采用系统调用!而自己写的函数往往比函数调用要更快。
所以总结一下:
用户态:正在执行用户层的代码,此时进程的状态是用户态.
内核态:正在通过系统调用访问内核、硬件资源,此时进程的状态是内核态。
此时,会出现这样一个疑问:操作系统是如何得知现在被执行的进程是用户态还是内核态?
在CPU中有一套寄存器,寄存器中存放着进程执行代码的数据,之前说过多个磁盘文件被加载到内存后,会被放进运行队列中,计算机之所以能够并发执行多个程序,就是通过时间片轮转的方式,让cpu一段时间内执行进程A,一段时间执行进程B,而寄存器的作用不仅仅用来处理计算,还用来保存上一个进程A被换下后的上下文数据,以遍下次cpu再次执行进程A时,能够瞬间找到上次末尾的位置继续运行该进程。
而在上图中,某寄存器1用来存放当前被执行进程的PCB地址;某寄存器2用来存放该进程的页表(虚拟空间中数据的虚拟地址可通过页表映射内存的物理地址)地址;而CR3寄存器的作用是表证当前进程的运行级别:
若CR3寄存器为0,表示该进程当前是内核态(高级别);
若CR3寄存器为3,表示该进程当前是用户态(低级别);
有了CR3寄存器,OS就可以轻易的知道每个进程当前的运行级别是什么状态的!
问题2:CPU在执行某个进程时是如何跑到操作系统中执行代码的呢?
从上图看,进程被加载到内存后,其PCB(task struct)就会被CPU执行。之前学习进程的时候就解释过上图,当CPU执行进程的代码时,PCB(task struct)中的属性指针会指向进程虚拟的地址空间,里面会存放代码中数据的虎拟地址,当CPU访问到某个变量的虑拟地址后,需要通过页表找到物理内存中的该变量的物理地址,找到后将数据返回给CPU。这里说的虑拟地址空间中共有4GB大小,其中从低到高 第0到第3GB的空间为用户级空间,用来存放用户的代码,数据等。为了保证进程的独立性,每个进程都有一个进程地址空间,都有一个用户级页表。
第3-第4GB的空间为内核空间,那么相对应的也有一份内核级页表,该内核级页表是操作系统的,操作系统只有一个,所以是所有的进程共用这独一份内核级页表。
其次内核空间也是不允许用户访问的,因为这1GB空间中的数据是通过内核级页表和内存中的操作系统相映射,属于内核级别的。因为内存中只存在一份内核,那么所有进程的虚拟地址空间中这1GB的内核空间都通过同一份内核级页表和内存中的内核相映射。即每一个进程地址空间中的内核空间都是一样的,因为它们都通过同一个内核级页表和内存中的OS相互映射。
有了以上这些知识的铺垫,我们就可以理解CPU是如何跑到OS中访问其资源的:
首先用户写了一份代码,CPU将其运行,现状态为用户态,在CPU执行的代码中有了系统调用接口,在CPU调用系统接口时进程转换为内核态,CPU寄存器CR3的值自动转换为0,OS发现该进程的CR3为0,验证成功,允许CPU访问系统资源,且CPU自动从进程地址空间的用户空间跳转到内核空间,从而申请系统资源进行访问,因为此时访问的还是接口的虚拟地址,需要通过内核级页表去映射内存中相应的物理地址,最终找到后返回CPU。调用完系统接口后,CPU再回到用户空间继续执行下面的代码。
用户态的进程想要转换为内核态的情况大致有3种:
1.执行系统调用函数;
2.进程在执行过程种出现了中断、异常、缺陷;
3.进程之间的相互切换
解析:执行系统调用上面已经说的很清楚了;
对于第2点,进程出现了中断异常,即表明CPU在执行代码过程发现了代码上的错误(越界,除零...)然后报告给OS,OS根据错误向进程发送信号......当进程出现中断时,进程由用户态转换为内核态,然后操作系统成为执行者,之后才能做一系列的操作(查出错误、发送信号),发送完后进程再转换为用户态,然后根据发来的信号对出现中断,异常的地方进行处理,处理完后继续往下执行代码。
对于第三点进程切换,说自了比如有3个程序被加载到内存成为进程,这三个进程被OS放进运行队列中,CPU通过时间片轮转的方式依次执行这三个进程,当一个进程被CPU执行时,该进程转为用户态,执行用户写的代码,执行几秒后,该进程被转换为内核态,被操作系统从CPU中剥离到运行队列,等待下一次的运行。等到下一次再被CPU执行时,再转为运行态...
如下为进程的两种状态转换图:
顺序如下:
第一步:用户态进程的CPU需要访问open系统调用函数时,转入内核态访问。
第二步:从用户空间跳转到内核空间后,访问到open函数的虚拟地址,再通过内核级页表访问到该函数的物理地址。紧接着CPU通过进程的两个位图和handler数组表,查看有没有信号递达。
第三步:在检查的过程中第四行的信号正处于未决状态,handler表中采用的是自定义方式处理(这时进程仍是内核态),自定义方式为signal(signo, handler)函数调用,进程需要转换为用户态去执行signal方法。
关于第三步有人就会问了,为什么内核态的进程不能够去执行handler方法,而是还要变成用户态才去执行?
在理论上,内核态进程是完全可以执行signal函数的,但在实际操作上是不被允许的,原因在于:操作系统不相信任何人,万一用户写的代码是恶意的、非法代码,用内核态(被操作系统任何)的身份去执行,就会导致安全问题。所以必须经过特定的方式切换到用户态身份去执行自定义处理的方式才能保证系统的安全。
第四步:执行完signal方法后,需要带着数据再转换回内核态,此时,检查信号是否递达的工作已然做完。
第五步:进程返回用户态,继续执行open()函数之后的代码。
关于第三步完成后,调用handler表中的方法后,为什么不就在用户态中继续往下执行,还要再回一次内核态?
原因:因为进程的上下文信息是系统保存的,是紧密相关的,进程不能由一种状态直接跳回另一种状态,这时需要操作系统帮助才能的,需要操作系统就得让进程经过特定的方式回到内核态。
简化上图的过程:
绿色圆圈表示进程的两种身份的切换,共有4次。上图可以看成是一个无穷大的符号。线上的是用户态,线下的是内核态。
况且上面这种是进程状态转换最复杂的情况,因为是自定义的操作,第三步和第四步的过程就导致状态转换多了两次;
若是SIG_DFL(默认处理方式)和SIG_IGN(忽略方式),以内核态身份就可以处理,然后就可以直接返回到用户代码中系统调用的位置,少了两次身份的转变,如下图:
默认和忽略动作只有两次的身份切换。这两种方式是程序员写入到操作系统中的宏定义,是被操作系统所信任的方式,那在第三步的过程中就直接在内核态处理完毕,然后直接返回用户态了。