深入理解计算机系统笔记4:异常控制流

异常控制流

CPU会因为遇到内部异常或外部中断等原因而打断程序的正常控制流,转去执行操作系统提供的针对这些特殊事件的处理程序。

由于某些特殊情况引起用户程序的正常执行被打断所形成的意外控制流称为异常控制流(Exceptional Control of Flow,ECF)。

异常控制流的形成原因:

  • 内部异常(缺页、越权、越级、整除0、溢出等),外部中断(Ctrl-C、打印缺纸、DMA结束等)
  • 进程的上下文切换(发生在操作系统层)
  • 一个进程直接发送信号给另一个进程(发生在应用软件层)

异常控制流存在于系统的每个层级,最底层的机制称为异常(Exception),用以改变控制流以响应系统事件,通常是由硬件的操作系统共同实现的。更高层次的异常控制流包括进程切换(Process Context Switch)信号(Signal)非本地跳转(Nonlocal Jumps),也可以看做是一个从硬件过渡到操作系统,再从操作系统过渡到语言库的过程。进程切换是由硬件计时器和操作系统共同实现的,而信号则只是操作系统层面的概念了,到了非本地跳转就已经是在 C 运行时库中实现的了。

发生异常(exception)和中断(interrupt)事件后,系统将进入OS内核态对相应事件进行处理,即改变处理器状态(用户态→内核态)

同步异常(Synchronous Exception)是因为执行某条指令所导致的事件,分为故障、自陷和终止三类

故障(fault) :执行指令引起的异常事件,如溢出、非法指令、缺页、访问越权等。

自陷(Trap) :预先安排的事件(“埋地雷”),如单步跟踪、断点、系统调用 (执行访管指令) 等。是一种自愿中断。

终止(Abort) :硬故障事件,此时机器将“终止”,调出中断服务程序来重启操作系统。

缺页、TLB缺失等:补救后可继续,回到发生故障的指令重新执行。溢出、除数为0、非法指令、内存保护错等:终止当前进程。

外设通过中断请求信号线向CPU提出“中断”请求,不由指令引起,故中断也称为异步异常。

• 事件:Ctrl-C、DMA传送结束、网络数据到达、打印缺纸、……
• 每执行完一条指令,CPU就查看中断请求引脚,若引脚的信号有效,则进行中断响应:将当前PC(断点)和当前机器状态保存到栈中,并“关中断”,然后,从数据总线读取中断类型号,根据中断类型号跳转到对应的中断服务程序执行。中断检测及响应过程由硬件完成。
• 中断服务程序执行具体的中断处理工作,中断处理完成后,再回到被打断程序的“断点”处继续执行。

异常处理

为每个异常分配了一个非负的异常号(exception number),一些号码由处理器设计者分配 ,其他号码由操作系统内核的设计者分配。

系统启动时,操作系统分配和初始化一张称为异常表的跳转表。条目k包含异常k的处理程序的地址。异常表的地址放在叫异常表基址寄存器的特殊CPU寄存器中。

异常类似过程调用,不过有以下不同

  • 过程调用,跳转到处理程序前,处理器将返回地址压入栈中。对于异常,返回地址是当前,或下一跳指令。
  • 处理器会把额外的处理器状态压入栈中。
  • 如果控制一个用户程序到内核,那么所有这些项目会被压入内核栈中,而是用户栈。
  • 异常处理程序运行在内核模式下,这意味他们对所有系统资源有完整访问权限。

异常的类别

技术分享

陷阱也称自陷或陷入,执行陷阱指令(自陷指令)时,CPU调出特定程序进行相应处理,处理结束后返回到陷阱指令下一条指令执行。

陷阱的作用之一是在用户和内核之间提供一个像过程一样的接口,这个接口称为系统调用,用户程序利用这个接口可方便地使用操作系统内核提供的一些服务。操作系统给每个服务编一个号,称为系统调用号。例如,Linux系统调用fork、read和execve的调用号分别是1、3和11。

IA-32处理器中的 int 指令和 sysenter 指令、MIPS处理器中的 syscall指令等都属于陷阱指令(相当于“地雷”)。陷阱指令异常称为编程异常(programmed exception),这些指令包括 INT n、int 3、into(溢出检查)、bound(地址越界检查)等

利用陷阱机制可实现程序调试功能,包括设置断点和单步跟踪

  • IA-32中,当CPU处于单步跟踪状态(TF=1且IF=1)时,每条指令都被设置成了陷阱指令,执行每条指令后,都会发生中断类型号为1的“调试”异常,从而转去执行“单步跟踪处理程序”。注意: 当陷阱指令是转移指令时,不能返回到转移指令的下条指令执行,而是返回到转移目标指令执行。(在一定的条件下,每条指令都变成“地雷”)
  • IA-32中,用于程序调试的“断点设置”陷阱指令为int 3,对应机器码为CCH。若调试程序在被调试程序某处设置了断点,则调试程序就在该处“加”一条int 3指令(该首字节)。执行到该指令时,会暂停被调试程序的运行,并发出“EXCEPTION_BREAKPOINT”异常,以调出调试程序执行,执行结束后回到被调试程序执行。(int 3是一定爆炸的“地雷”)

终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM和SRAM被损坏。终止处理程序从不将控制返回给应用程序。返回一个abort例程。

进程

程序(program)指按某种方式组合形成的代码和数据集合,代码即是机器指令序列,因而程序是一种静态概念。

进程( process)指程序的一次运行过程。更确切说,进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义 。同一个程序处理不同的数据就是不同的进程

• 进程是OS对CPU执行的程序的运行过程的一种抽象。进程有自己的生命周期,它由于任务的启动而创建,随着任务的完成(或终止)而消亡,它所占用的资源也随着进程的终止而释放。
• 一个可执行目标文件(即程序)可被加载执行多次,也即,一个程序可能对应多个不同的进程。
例如,用word程序编辑一个文档时,相应的用户进程就是winword.exe,如果多次启动同一个word程序,就得到多个winword.exe进程,处理不同的数据。

操作系统(管理任务)以外的都属于“用户”的任务。 计算机处理的所有“用户”的任务由进程完成。 为强调进程完成的是用户的任务,通常将进程称为用户进程。

计算机系统中的任务通常就是指进程。例如, Linux内核中通常把进程称为任务,每个进程主要通过一个称为进程描述符(process descriptor)的结构来描述,其结构类型定义为task_struct,包含了一个进程的所有信息。
– 所有进程通过一个双向循环链表实现的任务列表(task list)来描述,任务列表中每个元素是一个进程描述符。

– IA-32中的任务状态段(TSS)、任务门(task gate)等概念中所称的任务,实际上也是指进程。

“进程”的引入为应用程序提供了以下两方面的抽象:

  • 一个独立的逻辑控制流: 每个进程拥有一个独立的逻辑控制流,使得程序员以为自己的程序在执行过程中独占使用处理器
  • 一个私有的虚拟地址空间:每个进程拥有一个私有的虚拟地址空间,使得程序员以为自己的程序在执行过程中独占使用存储器

进程”的引入简化了程序员的编程以及语言处理系统的处理,即简化了编程、编译、链接、共享和加载等整个过程。

进程控制

unix系统提供了大量从c程序库中操作进程的系统调用

获取进程ID,和终止进程,回收子进程,进程休眠,加载并运行程序

信号

信号是 Unix、类 Unix 以及其他 POSIX 兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。

信号术语

发送信号,接收信号,阻塞和解除阻塞信号

内核通过给目标进程发送信号,来更新目标进程的状态,发送信号有如以下两种原因:

  • 内核检测到了如除以零(SIGFPE)或子进程终止(SIGCHLD)的系统事件
  • 另一个进程调用了 kill 指令来请求内核发送信号给指定的进程

目标进程接收到信号后,内核会强制要求进程对于信号做出响应,可以有几种不同的操作:

  • 忽略这个型号
  • 终止进程
  • 捕获信号,执行信号处理器(signal handler),类似于异步中断中的异常处理器(exception handler)
安全处理信号

信号处理器的设计并不简单,因为它们和主程序并行且共享相同的全局数据结构,尤其要注意因为并行访问可能导致的数据损坏的问题,这里提供一些基本的指南(后面的课程会详细介绍)

  • 规则 1:信号处理器越简单越好
    • 例如:设置一个全局的标记,并返回
  • 规则 2:信号处理器中只调用异步且信号安全(async-signal-safe)的函数
    • 诸如 printf, sprintf, mallocexit 都是不安全的!
  • 规则 3:在进入和退出的时候保存和恢复 errno
    • 这样信号处理器就不会覆盖原有的 errno
  • 规则 4:临时阻塞所有的信号以保证对于共享数据结构的访问
    • 防止可能出现的数据损坏
  • 规则 5:用 volatile 关键字声明全局变量
    • 这样编译器就不会把它们保存在寄存器中,保证一致性
  • 规则 6:用 volatile sig_atomic_t 来声明全局标识符(flag)
    • 这样可以防止出现访问异常

这里提到的异步信号安全(async-signal-safety)指的是如下两类函数:

  1. 所有的变量都保存在栈帧中的函数
  2. 不会被信号中断的函数

Posix 标准指定了 117 个异步信号安全(async-signal-safe)的函数(可以通过 man 7 signal 查看)

阻塞和取消阻塞信号

同步流以避免并发错误

可移植的信号处理

显示地等待信号

非本地跳转

注:进程控制部分和信号部分主要介绍了相关函数的使用,可参阅其他书籍。

 

参考:csapp第八章,袁春风计算机系统基础课程及教材

http://wdxtub.com/2016/04/16/thin-csapp-5/

posted on 2018-05-20 17:39  flysong  阅读(816)  评论(0编辑  收藏  举报

导航