谈异常控制流

引子

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

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

并发是怎样实现的?

Try catch 使怎样跳转的?

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

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

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

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

这就是一个控制流,从ak到ak+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

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

2、 创建或终止进程

进程分为如下几种状态:

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

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

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

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

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

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

这个程序很有趣。第8行创建子进程,子进程由于继承了父进程的存储空间等,因此也从第8行开始执行。由于它返回的pid为0,因此执行第9行的child块。对于父进程,执行第14行的parent块。并且两者存储是独立的,因此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信号重启,进程忽略该信号。除了SIGSTOP和SIGKILL,其它信号默认行为可以修改。

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

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

Unix中的信号处理

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

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

非本地跳转

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

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

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

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

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

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

字会发生什么呢?

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

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

见图:

总结

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

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

时间: 2024-08-04 09:34:55

谈异常控制流的相关文章

异常控制流 第十周11.15~11.22

第八章 异常控制流 控制流:控制转移序列. 控制转移:从一条指令到下一条指令. 异常控制流:现代操作系统通过使控制流发生突变来对系统状态做出反应,这些突变称为异常控制流. 作为程序员,理解ECF很重要,这有很多原因: 理解ECF将帮助你理解重要的系统概念.ECF是操作系统用来实现I/O.进程和虚拟存储器的基本机制,在能够真正理解这些重要概念之前,你必须须理解ECF. 理解ECF将帮助你理解应用程序是如何与操作系统交互的.应用程序通过一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务. 理解

8.0 异常控制流 第8章 《深入理解计算机系统 原书第2版》

异常控制流 定义:现代操作系统对于控制流发生突变所作出的反应 全称:Exception Control Flow 缩写:ECF 各层形态: 1.硬件层:硬件检测到的事件会触发控制突然转移到异常处理程序: 2.操作系统层:在操作系统层,内核通过上下文转换,将控制从一个用户进程转移到另外一个用户进程: 3.应用层:一个进程可以发信号到另外一个进程,而接收者会将控制突然转移到它的一个信号处理程序. 描述:一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误做出的反应. 工作

第8章 异常控制流

控制转移序列叫做处理器的控制流 现代系统通过使控制流发生突变来对这些情况做出反应,这些突变被称为异常控制流(ECF).异常控制流发生在操作系统的各个层次. 8.1异常 异常是异常控制流的一种形式,一部分由硬件实现,一部分由操作系统实现. 异常就是控制流中的突变,用来响应处理器状态中的某些变化.在处理器中,状态变化称为事件. 任何情况下,当处理器检测到有事件发生时,他就会通过一张叫做异常表的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序). 8.

进程—异常控制流之中断篇

从给处理器加电开始,直到断电为止,PC(程序计数器)都在不间断的读取并执行指令. 最简单的一种控制流是一个"平滑的"序列,其中每个instk和instk+1 在存储器中都是相邻的.典型地,这种平滑流的突变,也就是instk?和instk+1? 不相邻,是由诸如跳转(jump).调用(call)和返回(ret)这样一些熟悉的程序指令造成的.这样一些指令都是必要的机制,使得程序能够对由程序变量表示的内部程序状态中的变化做出反应. 但是系统也必须能够对系统状态的变化做出反应,这些系统状态不是

异常控制流

异常 异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现. 异常就是控制流中的突变,用来响应处理器状态中的某些变化. 处理器发现异常时会进行一个间接过程调用,到一个操作系统子程序,即异常处理程序. 异常处理程序完成处理后会依据异常类型,产生以下三种情况: 将控制返回当前指令,即正在执行的指令. 返回给下一条指令. 终止被中断的程序. 异常处理 异常号:系统为每种类型的异常分配的唯一的非负整数. 异常表:系统启动时操作系统就会初始化一张条转变,使得条目k包含异常k的处理程序的地址

计算机系统:第8章 异常控制流

参考材料:深入理解计算机系统,第8章 1控制流的种类 -->对由"程序内部"变量表示的内部状态的变化作出反应的.简单来说,就是我在运行这段程序,在"程序"中明确指明了下条运行的指令,如顺序下一条,while,跳转等 -->对一些系统状态的变化作出反应,这些系统状态"不是由这段程序内的变量"捕获的.简单来说,就是我在运行这段程序,运行着运行着,突然从本段程序外部来了一个刺激要我作出反应. -->对于第二种控制流,系统通过ECF(E

第八章 异常控制流

第八章 异常控制流 ECF:(异常控制流)突变集合 平滑:顺序结构的指令 突变:跳转.调用.和返回等指令,不在同一栈 基本机制:ECF是操作系统用来实现I/O.进程和虚拟存器的基本机制   ECF是计算机系统中实现并发的基本机制 异常 控制流的突变 异常号: 系统为每种类型的异常分配的唯一的非负整数 异常表: 系统启动时操作系统就会初始化一张条转变,使得条目k包含异常k的处理程序的地址 用法: 从异常号到异常表的索引 类别: 中断:来自处理器外部的I/O设备的信号的结果,返回下一条指令 陷阱:陷

CSAPP:异常控制流

在一般的情况下,处理器处理的指令序列是相邻的(顺序执行). 异常控制流提供了指令的跳转,它一部分是由硬件实现的,一部分是由操作系统实现的. 异常处理 在系统启动时,操作系统分配和初始化一张称为异常表的跳转表: 触发异常时将从跳转表中找到并执行相应的异常处理程序的代码(所谓的内核态代码?): 系统调用 每个系统调用都属于异常,当调用C库中的系统调用函数时将触发异常. IA32系统中,系统调用是通过一条称为int 0x80(异常号)的陷阱指令来提供的. 所有Linux的系统调用都是通过寄存器来传递的

进程—异常控制流之故障、终止篇

进程-异常控制流之故障.终止篇 一.Exceptions(异常) and System Call(系统调用) 1.1 故障 故障由错误情况引起,它可能能够被故障处理程序修正.当故障发生时,处理器将控制转移给故障处理程序.如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它.否则,处理程序返回到内核中的abort例程, abort 例程会终止引起故障的应用程序. 故障处理.根据故障是否能够被修复,故障处理程序要么重新执行引起故障的指令,要么终止. 一个经典的故障示例是缺