异常控制流

本文介绍了计算机系统中的异常处理机制,包括中断、陷阱、故障和终止等类型,并详细阐述了进程的概念及其控制方法,如创建、回收子进程及信号处理等。

异常

  • 异常
    最简单的处理器控制流是平滑的,即指令按顺序执行。但是系统必须对状态变化进行反应,这种变化不一定和程序执行相关,如一个硬件定时器定期产生信号。异常就是控制流中的突变,用来响应处理器状态中的某些变化。
    这里写图片描述

  • 异常处理
    当处理器检测到有事件发生时,就会通过异常表进行跳转,调用异常处理程序。异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里。
    这里写图片描述

  • 异常类别
    这里写图片描述

    • 中断
      中断是来自处理器外部的I/O设备的信号的结果。I/O设备通过向处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,在当前指令完成执行之后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。
      这里的异步,个人觉得意为与当前的指令无关,而同步的异常时执行当前指令的结果。
    • 陷阱
      陷阱通过系统调用,向内核请求服务。处理器提供的“syscall n”指令会导致一个到异常处理程序的陷阱。
    • 故障
      故障由错误引起,它可能能够被处理程序修正,如果处理程序能够修正这个错误,就会将控制返回到引起错误的指令并重新执行它。
    • 终止
      终止是不可恢复的错误,应用程序会被终止。

    系统调用错误处理:当Unix系统级函数遇到错误时,它们典型地会返回-1,并设置全局整数变量errno来表示什么出错了。

异常示例
这里写图片描述
其中,0~31的号码对应的是由Intel架构师定义的异常。32~255的号码对应的是操作系统定义的中断和陷阱。接下来我们来看下系统调用。
这里写图片描述
Linux提供上百种系统调用,每个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。

进程

进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的上下文(context)中的。上下文是程序运行的状态。

  • 私有地址空间
    一个进程为每个程序提供它自己的私有地址空间。

    进程和程序:程序是一堆代码和数据;进程是执行中程序的一个具体实例。程序总是运行在某个进程的上下文中。例:fork函数在新的子进程中运行相同的程序;execve函数在当前进程上下文中加载运行一个新的程序,它会覆盖当前进程的地址空间,但并没有创建新的进程。

  • 用户模式和内核模式
    处理器通过模式位来控制进程权限。内核模式下,进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器位置。用户模式中的进程不允许执行特权指令,如停止处理器、改变模式位,或者发起一个读操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据。运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷人系统调用这样的异常。

  • 上下文切换
    操作系统内核通过上下文切换来实现多任务。
    这里写代码片

进程控制

  • 获取进程ID
    每个进程都有一个唯一的正数进程ID(PID)。
    getpid()函数返回调用进程的PID。
    getppid()函数返回它的父进程的PID(创建调用进程的进程)。
  • 进程组
    每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。默认地,一个子进程和它的父进程同属于一个进程组。
    getpgrp()函数返回当前进程的进程组ID。
    setpgid(pid_t pid,pid_t pgid)函数改变进程的进程组。
  • 创建进程fork
    fork()函数会创建一个子进程,新创建的子进程与父进程几乎完全相同,最大的区别在于有不同的PID。
    父进程与子进程是并发运行的独立进程。
    fork函数会返回两次,在父进程中返回子进程的PID,在子进程中返回0。
  • 回收子进程waitpid
    当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。而是被保持在一种已终止的状态中,直到被它的父进程回收。一个终止了但还未被回收的进程称为僵死进程。如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排init进程来回收它们。
    我们使用waitpid(pid_t pid,int *status,int options)或wait(int *status)来回收子进程。
  • 进程休眠sleep
    sleep(unsigned int secs)函数将一个进程挂起一段指定的时间。
  • 加载并运行程序execve
    execve(const char *filename, const char *argv[], const char *envp[])函数在当前进程的上下文中加载并运行一个新程序。如果成功则不返回,失败返回-1。

信号

一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。
这里写图片描述
一个只发出而没有被接收的信号叫做待处理信号。在任何时刻,一种类型至多只会有一个待处理信号,多余的会被丢弃。一个进程可以有选择性地阻塞接收某种信号,当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。即,同一个信号最多保持两个,一个在处理中,一个待处理。
这里写图片描述

  • 信号产生
    内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。发送信号可以有如下两个原因:
    1)内核检测到一个系统事件,比如被零除错误或者子进程终止。
    2) 一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程。

    • 用kill程序发送信号
      /bin/kill程序可以向其它进程发送任意的信号。

      unix> /bin/kill -9 15123 //发送信号9给PID为15123的进程
      unix> /bin/kill -9 -1530 //发送信号9给PID为1530的进程组中每个子进程
    • 从键盘发送信号
      在键盘上输人ctrl-c会导致一个SIGDINT信号被发送到外壳,外壳捕获该信号后发送SIGDINT信号到这个前台进程组中的每个进程。类似地,输人ctrl-z会发送一个SIGTSTP信号到外壳。

    • 用kill函数发送信号
      同kill程序类似,kill(int pid,int sig)可以发送信号给其他进程(包括自己)。
    • 用alarm函数发送信号
      进程可以通过调用alarm(unsigned int secs)函数向它自己发送SIGALRM信号。
  • 信号处理
    当内核从一个异常处理程序返回,它会检查进程的未被阻塞的待处理信号的集合。如果集合非空,则强制接受一个信号(通常是最小的),收到这个信号会触发进程的某种行为。
    通过signal(int signum,sighandler_t handler)可以修改信号的默认行为。signum是信号的序号,handler是自定义的信号处理程序。

    因为信号处理的一些特性,比如待处理信号的阻塞,只能保留一个待处理信号,信号调用可以被中断,信号处理并发等原因,会导致一些问题,书中有做探讨,这里就不做记录了。

非本地跳转

非本地跳转将控制直接从一个函数转移到另一个函数,而不需要经过正常的调用一返回序列。
setjmp(jmp_buf env)函数保存当前的执行状态,并且返回0。
longjmp(sigjmp_buf env, int savesigs)函数返回到setjmp的位置,并且设置一个返回值。
setjmp相当于设置了一个锚点,比如错误处理程序,当发生错误的时候,就可以通过longjmp直接跳转到错误处理程序。

<script type="text/javascript"> $(function () { $('pre.prettyprint code').each(function () { var lines = $(this).text().split('\n').length; var $numbering = $('<ul/>').addClass('pre-numbering').hide(); $(this).addClass('has-numbering').parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($('<li/>').text(i)); }; $numbering.fadeIn(1700); }); }); </script>
<think>我们正在讨论程序控制指令的分类。用户询问是否可以分为正常控制流异常控制流。 根据计算机系统的一般概念: - 正常控制流(Normal Control Flow)是指程序按照指令顺序执行,或者通过跳转、调用、循环等指令改变执行流程,但这些改变是程序开发者有意设计的。 - 异常控制流(Exceptional Control Flow)是指由于某些异常事件(如硬件中断、系统调用、程序错误等)导致的非预期控制流转移。 在指令集架构层面,程序控制指令通常包括: 1. 跳转指令(JMP) 2. 条件分支指令(Jcc) 3. 子程序调用和返回(CALL/RET) 4. 循环控制指令(LOOP等) 这些都属于正常控制流指令,因为它们是由程序显式安排的流程改变。 而异常控制流通常由以下情况触发: - 硬件中断(如时钟中断、I/O中断) - 处理器异常(如除零异常、缺页异常) - 系统调用(通过软中断或专用指令,如x86的INT/SYSCALL) 在高级语言中,异常处理(如try-catch)也是异常控制流的一种表现,但在机器指令层面,它通常通过跳转指令和特定的异常处理机制实现。 因此,我们可以将程序控制指令分为两类: 1. 正常控制流指令:包括跳转、分支、调用、返回、循环等。 2. 异常控制流指令:包括中断指令(如INT)、系统调用指令(如SYSCALL/SYSENTER)以及处理器内部用于异常处理的机制(这些通常不是显式的指令,而是由硬件触发)。 注意:异常控制流指令通常不是由程序直接调用(除了系统调用),而是由硬件或操作系统在特定条件下触发。 参考用户提供的引用: 引用[1]和[2]讨论了Java中的异常处理,将异常分为受检异常和非受检异常(RuntimeException),这属于高级语言层面的异常控制流。 引用[3]和[4]则提到了异常捕获和处理,也属于高级语言层面。 在指令集层面,异常控制流通常通过中断和异常处理机制实现,而程序控制指令主要处理正常控制流。 因此,回答用户的问题:是的,程序控制指令可以分为正常控制流指令和异常控制流指令。 下面我们详细说明: ### 一、正常控制流指令 这类指令是程序主动改变控制流的指令,包括: 1. 跳转指令(JMP) 2. 条件分支指令(JE, JNE, JG等) 3. 子程序调用和返回(CALL/RET) 4. 循环指令(LOOP, LOOPZ等) 这些指令的特点是由程序显式使用,控制流转移的目标地址在指令中指定或由寄存器给出。 ### 二、异常控制流指令 这类指令用于处理非预期的或由外部事件触发的控制流转移,包括: 1. 中断指令(如x86的INT n):主动触发软件中断,通常用于系统调用。 2. 系统调用指令(如x86的SYSCALL/SYSENTER):专门用于快速系统调用。 3. 中断返回指令(如IRET):用于从中断处理程序返回。 4. 处理器异常:由硬件自动触发,例如除零异常(对应x86的DIV指令出错时)或页错误,这些不是指令,而是事件。 注意:异常控制流指令的执行通常伴随着处理器状态的保存和恢复(如压栈保存现场),以及控制权转移到操作系统内核。 在高级语言中,异常处理机制(如try-catch)在底层通常通过正常控制流指令(跳转)和操作系统/运行时的异常处理机制(异常控制流)共同实现。 因此,我们可以这样总结: 程序控制指令在指令集层面可以分为: - **正常控制流指令**:程序主动控制的跳转、分支、调用、返回、循环等。 - **异常控制流指令**:用于响应外部事件或错误的指令(如INT、SYSCALL、IRET),以及由硬件自动触发的异常处理机制。 需要说明的是,硬件自动触发的异常处理机制并不是一条指令,而是一个过程,但它的效果相当于控制流的改变。 下面我们根据用户提供的引用,看看高级语言层面的异常控制流与指令集层面的关系: 引用[1]中展示了Java的FileInputStream类,其中read方法抛出了IOException(受检异常)。在底层,当发生I/O错误时,操作系统会产生一个中断或异常信号,然后由JVM捕获并转换为Java异常抛出。 引用[2]提到,受检异常用于期望调用者能够恢复的情况,而非受检异常(RuntimeException)通常表示程序错误。在底层,非受检异常可能由硬件异常(如空指针访问)触发,而受检异常则可能由系统调用错误(如文件不存在)触发。 引用[3]和[4]则说明了异常捕获的语法和常见异常类型,这些异常在底层都对应着异常控制流的处理。 因此,从整个计算机系统的角度,控制流可以分为正常和异常,而指令集也提供了相应的指令支持。 最后,我们生成相关问题:</think>### 程序控制指令的分类:正常控制流异常控制流 程序控制指令可分为**正常控制流**和**异常控制流**两类,二者在触发机制、处理逻辑和设计目标上存在本质区别。以下是具体分析: --- #### 一、正常控制流指令 通过**显式指令**主动改变程序执行顺序,是开发者预期的流程控制。 1. **核心类型**: - 跳转指令(`JMP`/`Jcc`) - 子程序调用/返回(`CALL`/`RET`) - 循环控制(`LOOP`) 2. **特点**: - **主动触发**:由程序显式调用(如`CALL SUB_ROUTINE`)。 - **可预测性**:流程转移目标明确(如跳转至标号`LABEL`)。 - **堆栈管理**:依赖堆栈保存返回地址(`CALL`压栈,`RET`弹栈)[^1]。 --- #### 二、异常控制流指令 由**非预期事件**触发的控制流转移,用于处理错误或外部中断。 1. **触发条件**: - **硬件异常**:除零错误、缺页中断等(如`#DE`除零异常)。 - **软件异常**:显式抛出异常(如`throw new IOException()`)[^1]。 - **外部中断**:时钟中断、I/O设备信号等。 2. **处理机制**: - **中断向量表**:通过预定义入口跳转至处理程序。 - **上下文保存**:自动保存寄存器状态(如`EFLAGS`、`CS:IP`)。 - **异常分类**: - **受检异常**(Checked):需显式处理(如Java的`IOException`)[^2]。 - **非受检异常**(Unchecked):逻辑错误(如`NullPointerException`)[^4]。 3. **示例**: ```java // Java异常控制流(受检异常) public int read() throws IOException { return read0(); // 可能触发IOException[^1] } ``` --- ### 核心区别 | **特性** | 正常控制流 | 异常控制流 | |------------------|---------------------------|---------------------------| | **触发方式** | 显式指令(如`CALL`) | 事件驱动(错误/中断) | | **目标地址** | 固定标号或地址 | 通过中断向量表动态跳转 | | **恢复机制** | 通过`RET`返回原流程 | 需显式恢复上下文或终止程序 | | **设计目标** | 实现分支、循环、模块化 | 容错处理或响应外部事件 | > **关键洞察**: > - 正常控制流是**程序主动设计**的流程(如函数调用);异常控制流是**被动响应**机制(如错误处理)[^2][^4]。 > - 异常控制流通常需要操作系统/运行时环境支持(如中断处理程序)[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值