谈异常控制流

来源:互联网 发布:js让隐藏的div显示 编辑:程序博客网 时间:2024/05/17 22:15

引子

Cpu/内核是怎么处理各种异常的?

用户态程序怎样调用系统函数,与操作系统交互的?

并发是怎样实现的?

Try catch 使怎样跳转的?

.............

 

异常控制流是这些问题的根基,想更多的理解计算机系统,必须对这个问题有一定的了解。

首先,必须清楚什么是控制流?

cpu有一个处理序列a1,a2…ak,ak+1..

这就是一个控制流,从akak+1就是控制转移。但是很多时候不是按顺序处理的,比如突然插上一个u盘,就要对它进行处理,这种突变叫做异常控制流。

那么都有哪些异常呢?

异常

异常就是控制流中的突变,用来响应处理器的某些变化。图1展示了基本的思想。

 

1对异常的响应

 

 

异常难以理解,因为需要软硬件分工,设计思路如下:

1、 为每一种异常分配一个非负异常号,部分是cpu(零除,缺页,存储器访问违例等),部分是内核(系统调用,外部设备等)给定的

2、 系统加电时,操作系统初始化一张异常表,见图2。

3、 根据异常表的指针调用异常处理程序,在跳转前,要将当前状态压栈,先是返回地址(可能是当前地址,也可能是吓一跳地址,根据异常类型来确定)

 


 

2:异常表

而异常通常分为如下几种:

中断,陷阱,故障,终止。见图3.

 


3:中断类型

这里的中断一般指的是外中断,也就是各种io设备。以软件程序员的视角,我主要聊一聊陷阱。

陷阱是有意的异常,一般用来作为用户态和内核态的接口,也就是系统调用。系统调用常见的有读一个文件(read),创建新的进程(fork)。系统调用运行在内核模式中,并且可以访问内核中的栈。参数是通过通用寄存器而不是栈来传递的,如,%eax存储系统调用号,%ebx,%ecx,%edx,%esi,%edi,%ebp最多存储六个参数,%esp不能用,因为进入内核模式后,会覆盖掉它。

 

进程

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

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

逻辑控制流

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


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

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

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

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

私有地址空间

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

上下文切换,调度

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

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

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

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

进程控制

1、 获取进程ID

每个进程都有一个唯一的非零进程idgetpid返回调用进程的idgetppid返回父进程的pid,

2、 创建或终止进程

进程分为如下几种状态:

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

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

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

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

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

创建的子进程中(返回0)。返回值提供判断是哪个进程的依据。下图是一个简单的样例。

 


 

这个程序很有趣。第8创建子进程,子进程由于继承了父进程的存储空间等,因此也从第8开始执行。由于它返回的pid0因此执行第9child块。对于父进程,执行第14parent块。并且两者存储是独立的,因此x值互不影响。并且他们是并发执行,共享文件(在同一屏幕上打印)。下面是一个更有意思的代码。


 

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

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

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

 

3、 加载并运行程序

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

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

信号

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

发送信号。内核通过更新目的进程上下文某个状态,来通知进程。发送信号一般有如下原因:内核检查到系统事件,如零除错误或子进程终止。或者一个进程调用了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信号重启,进程忽略该信号。除了SIGSTOPSIGKILL,其它信号默认行为可以修改。

 

HandlerSIG_IGN,则忽略相关信号行为。SIGDFL,则类型为signum恢复默认行为。

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

 

Unix中的信号处理

捕获一个信号很简单,多个较复杂。

待处理信号被阻塞,待处理信号不会排队等待,系统调用可以被中断。前面两个问题本质上是因为待处理信号只能有一个(这种类型的信号正在被处理,所以下一个成了待处理信号,接下来的就被丢弃了)

 

非本地跳转

这是一种用户级的一场控制流形式,通过setjmplongjmp提供。

Setjmp函数在env中保存当前调用环境,包括计数器,栈指针,和通用目的寄存器。Setjmp调用一次,但返回多次。见图。

 

C++java中的异常机制是setjmplongjmp更结构化的版本,try catch相当于setjmp,而throw相当于longjmp

setjmplongjmp函数用于非局部跳转,在信号处理程序中经常调用longjmp函数以返回到程序的主循环中,而不是从该处理

程序返回。但是调用longjmp有一个问题,当捕捉到一个信号时,进入进行处理函数,此时当前信号被自动加到进程的信号

屏蔽字中。这阻止了后来产生的这种信号中断该信号处理程序。如果用longjmp跳出信号处理程序,那么对此进程的信号屏蔽

字会发生什么呢?

POSIX.1并没有说明setjmplongjmp对信号屏蔽字的作用,而是定义了两个新函数sigsetjmpsiglongjmp。在信号处理程序

进行非局部转移时应该使用这两个函数

见图:

 


 

总结

异常控制流发生在计算机系统各个层次,是计算机中提供并发的基本机制。

了解异常控制流,是探索系统函数,高级语言异常,并发等的基础。

0 0