异常控制流知识归纳

#异常

  1. 异常控制流

现代计算机系统通过控制流突变(即异常控制流)来响应很多系统运行事件。比如:

硬件层:硬件检测到的事件会触发控制突然转移到异常处理程序

系统层:内核通过上下文转换将控制从一个用户进程转移到另一个用户进程

应用层:一个进程可以向另一个进程发送信号,接收者会将控制突然转移到它的一个信号处理程序。一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误作出反应(这里就是程序员定义的软件异常

2.异常

这里异常是异常控制流的一种形式,两者概念可以等价。

异常可以分为四类:中断(interrupt),陷阱(trap),故障(fault),和终止(abort)。

中断:中断是异步发生的,是来自处理器外部I/O设备的信号的结果。I/O设备指网卡,磁盘控制器和定时器等。处理结束后返回下一条指令。

陷阱:陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途就是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。向内核请求服务,从用户态转到内核态就要用系统调用,用指令syscall n请求OS提供的服务n(在IA32结构貌似就是int n指令)。 处理结束后返回下一条指令。

故障:由潜在的可恢复的错误引起,典型的就是缺页故障。处理结束后返回当前指令。

终止:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序从不将控制再返回给应用程序。

  1. Linux/IA32系统的异常

共有256种。其中0~31由处理器的设计者分配的,32~255由操作系统内核的设计者分配的。处理器的设计者负责的异常有除数为零,缺页,内存访问违规,算术溢出等。操作系统内核的设计者负责的异常有系统调用,和来自外部I/O设备的信号。

在IA32系统上,系统调用通过一条称为int n的陷阱指令来提供,这里的n是异常表中256条目中的任何一个。(疑惑是这里的n为什么是异常编号而不是系统调用编号?而且int n不一定只是陷阱指令,因为异常表中的条目有四种类型,并不全是陷阱)。

历史上系统调用通过第128(0x80)号异常提供,它属于陷阱异常,实现系统调用的过程为:将系统调用编号(注意与异常编号区分)放在寄存器eax中,然后将系统调用(也是函数)的参数依次压栈,最后执行指令int 0x80。

4.用户模式和内核模式

为了更安全吧,处理器提供一种机制,限制一个应用程序可以执行的指令以及它可以访问的地址空间范围。

处理器通常是用某个控制寄存器中的一个模式位(mode bit)来提供这种功能的,当设置了模式位的时候,进程就可以在内核模式中运行。一个运行在内核模式的进程可以执行任何指令集合中的指令,并且可以访问系统中任何存储器的位置。当没有设置模式位的时候,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,比如停止处理器,改变模式位等操作,也不能发起一个I/O操作。更不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。

那么进程从用户模式变成内核模式的唯一途径就是通过诸如中断,故障,陷阱来陷入系统调用。当这些异常发生的时候,控制就传递到了异常处理函数中,处理器将模式位从用户模式变为内核模式。异常处理程序是运行在内核模式中的,当他返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式

  1. 进程
    异常是允许系统提供进程的概念的基本构造块,我们运行一个程序时,会得到一个假象,就像我们的程序是系统当中运行的唯一的程序。这些假象都是通过进程的概念提供的。

进程的经典定义是一个执行中的程序的实例,系统中每个程序都运行在进程上下文。上下文由程序正常运行所需的实例组成。这个状态包括存放在存储器中的程序代码和数据,它的栈,寄存器,环境变量等。进程提供给应用程序的关键抽象:a)一个独立的逻辑控制流 ;b)一个私有的地址空间

逻辑控制流

程序计数器(PC)值的序列叫做逻辑控制流,简称逻辑流。如下图所示,处理器的一个物理控制流分成了三个逻辑流,每个进程一个。

一些概念:并发流:并发流一个逻辑流的执行在时间上与另一个流重叠,叫做~

并发:多个流并发执行的一般现象称为并发。

多任务:多个进程并发叫做多任务。

并行:并发流在不同的cpu或计算机

私有地址空间

一个进程为每个程序提供它自己的私有地址空间。运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常。 linux提供了/proc文件系统,它允许用户模式进程访问内核数据结构的内容。

上下文切换,调度

上下文切换:操作系统内核使用叫上下文切换的异常控制流来实现多任务。

上下文切换:a)保存当前进程的上下文;b)恢复某个先前被抢占的进程被保存的上下文; c)将控制传递给这个新恢复的进程

调度:内核中的调度器实现调度。

当内核代表用户执行上下文切换时,可能会发生上下文切换。如果系统调用发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,如read系统调用,或者sleep会显示地请求让调用进程休眠。一般,即使系统调用没有阻塞,内核亦可以决定上下文切换,而不是将控制返回给调用进程。中断也可能引起上下文切换。如,定时器中断。

6.进程控制与fork
1、 获取进程ID

每个进程都有一个唯一的非零进程id,getpid返回调用进程的id,getppid返回父进程的pid,

2、 创建或终止进程

进程分为如下几种状态:

运行:进程要么在cpu上执行,要么等待执行,最终会被内核调度。

停止:进程的执行被挂起,不会被调度。当收到SIGSTOP,SIGTSTP等,进程会停止,当收到一个SIGCONT时,进程再次运行。(信号是一种软件中断的形式)

终止:进程永远停止。一般三个情况,收到终止信号,从主程序返回,调用exit函数。

父进程通过fork创建子进程,新创建的进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟进程相同(但独立)的一份拷贝,还有相同的文件描述符。这意味着当父进程调用fork时,子进程可以读写父进程打开的任何文件,父进程和子进程最大的区别是不同的pid。

Fork很有趣,调用一次,返回两次:一次是在调用进程(返回子进程pid)中,一次是在

创建的子进程中(返回0)。返回值提供判断是哪个进程的依据。
它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;

在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。
看一份代码

#include <unistd.h>  
#include <stdio.h>  
int main(void)  
{  
   int i=0;  
   printf("i son/pa ppid pid  fpid/n");  
   //ppid指当前进程的父进程pid  
   //pid指当前进程的pid,  
   //fpid指fork返回给当前进程的值  
   for(i=0;i<2;i++){  
       pid_t fpid=fork();  
       if(fpid==0)  
           printf("%d child  %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
       else  
           printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
   }  
   return 0;  
}  

运行结果是:
i son/pa ppid pid fpid
0 parent 2043 3224 3225
0 child 3224 3225 0
1 parent 2043 3224 3226
1 parent 3224 3225 3227
1 child 1 3227 0
1 child 1 3226 0
这份代码比较有意思,我们来认真分析一下:
第一步:在父进程中,指令执行到for循环中,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是p3224和p3225(后面我都用pxxxx表示进程id为xxxx的进程)。可以看到父进程p3224的父进程是p2043,子进程p3225的父进程正好是p3224。我们用一个链表来表示这个关系:
p2043->p3224->p3225
第二步:假设父进程p3224先执行,当进入下一个循环时,i=1,接着执行fork,系统中又新增一个进程p3226,对于此时的父进程,p2043->p3224(当前进程)->p3226(被创建的子进程)。
对于子进程p3225,执行完第一次循环后,i=1,接着执行fork,系统中新增一个进程p3227,对于此进程,p3224->p3225(当前进程)->p3227(被创建的子进程)。从输出可以看到p3225原来是p3224的子进程,现在变成p3227的父进程。父子是相对的,这个大家应该容易理解。只要当前进程执行了fork,该进程就变成了父进程了,就打印出了parent。
所以打印出结果是:
1 parent 2043 3224 3226
1 parent 3224 3225 3227
第三步:第二步创建了两个进程p3226,p3227,这两个进程执行完printf函数后就结束了,因为这两个进程无法进入第三次循环,无法fork,该执行return 0;了,其他进程也是如此。
以下是p3226,p3227打印出的结果:
1 child 1 3227 0
1 child 1 3226 0
这个程序最终产生了3个子进程,执行过6次printf()函数。

另一份

#include <stdio.h>  
#include <unistd.h>  
int main(int argc, char* argv[])  
{  
   fork();  
   fork() && fork() || fork();  
   fork();  
   return 0;  
}  

问题是不算main这个进程自身,程序到底创建了多少个进程。
答案是总共20个进程,除去main进程,还有19个进程。
我们再来仔细分析一下,为什么是还有19个进程。
第一个fork和最后一个fork肯定是会执行的。
主要在中间3个fork上,可以画一个图进行描述。
这里就需要注意&&和||运算符。
A&&B,如果A=0,就没有必要继续执行&&B了;A非0,就需要继续执行&&B。
A||B,如果A非0,就没有必要继续执行||B了,A=0,就需要继续执行||B。
fork()对于父进程和子进程的返回值是不同的,按照上面的A&&B和A||B的分支进行画图,可以得出5个分支。加上前面的fork和最后的fork,总共4*5=20个进程,除去main主进程,就是19个进程了。

当一个进程某种原因终止时,内核并不是立刻把他们清除,相反进程被保持在一个已终止的状态,直到被父进程回收,当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,一个终止了但未被回收的进程称为僵尸进程(如父进程未调用waitpid)。而在父进程某种原因退出,而它的子进程还在运行,这些子进程会变成孤儿进程,会被init进程收养(进程号为1)

由于僵尸进程占据着进程号,进程号是有限的,大量僵尸进程可能会耗尽进程号。

任何一个子进程(init除外)在exit后并非马上挂掉,而是留下一个僵尸进程的结构,等待父进程处理。僵尸进程危害这么严重,怎么解决呢?方法很简单,kill掉他们的父亲,他们就成为了孤儿,可以被init进程收养,然后清除。

  1. 加载并运行程序

Execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表

Execve加载filename后,调用启动代码,启动代码设置栈,并将控制权转移给新程序的main函数。

8.信号
一个信号就是一条消息,能打断其它进程。每种信号类型对应着某种系统事件,底层的硬件异常由内核异常处理程序处理的,正常情况下对用户进程不可见,信号提供了一种机制,通知用户进程发现了传送一个信号到目的进程是两个不同的步骤组成:

发送信号。内核通过更新目的进程上下文某个状态,来通知进程。发送信号一般有如下原因:内核检查到系统事件,如零除错误或子进程终止。或者一个进程调用了kill函数。

接收信号。当目的进程被内核强迫以某种方式对信号反应时,目的进程就接收了信号。进程可以忽略,也可以执行一个信号处理程序的函数捕获信号。

一个只发出没被接受的信号叫做待处理信号(pending singnal)。任何时刻,一种类型只有一种待处理信号,比如进程有信号类型为k,其它类型为k的都会被丢弃。进程也可以阻塞某种类型的信号。一个待处理信号最多被接收一次。内核有pending 位向量和block位向量来维护信号集合。

Unix中的发送信号

1、 每个进程属于一个进程组,函数getpgrp()获得当前进程的进程组

2、 子进程和父进程属于同一个进程组。Setpgid()设置自己或其它进程的进程组。

常见的信号有kill,如kill -9 12345。

还有alarm,这个略复杂,uint alarm(uint secs),这个函数安排内核在secs后发送一个SIGALRM信号给调用进程。如果secs是零,不会调用新的闹钟。

Unix中的接收信号

当内核从一个异常处理程序返回,准备将控制传递给进程p时,会检查p未被阻塞的待处理进程集合。如果集合为空,将控制传给p逻辑控制流的下一条指令。集合非空,内核选择集合中的某个信号k(通常最小),强迫p接收信号k,触发进程的某种行为,然后控制流交给p的下一条指令。预定义行为是下面默认的一种:

进程终止,进程终止并转储存储器,进程停止直到被SIGCONT信号重启,进程忽略该信号。除了SIGSTOP和SIGKILL,其它信号默认行为可以修改。
在这里插入图片描述
Handler为SIG_IGN,则忽略相关信号行为。SIGDFL,则类型为signum恢复默认行为。

否则,handler为用户定义的函数地址。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值