08异常控制流

控制流

给处理器加电开始,直到断电,程序计数器假设一个值的序列 a1,a2,...an1, a 1 , a 2 , . . . a n − 1 , ,其中每个 ak a k 是某个相应指令 Ik I k 的地址,叫做控制流

每次从 ak a k a+k1 a + k 1 的过渡称为控制转移

平滑控制流

如果其中每个 Ik I k Ik+1 I k + 1 在存储器中都是相邻的,那么控制流是平滑的

平滑流的突变是由跳转、调用、返回这样的程序指令造成的

异常控制流

现代系统通过使控制流发生突变来对系统状态的变化做出反应,这些突变称为异常控制流(ECF)

异常控制流(ECF)可以发生在计算机系统的各个层次

  • 硬件层

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

  • 操作系统层

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

  • 应用层

    一个进程可以发送信号到另一个进程,接受者会将控制突然转移到它的一个信号处理程序

    一个程序可以通过回避通常的栈规则,并执行到其它函数中任意位置的非本地跳转来对错误作出反应

异常控制流(ECF)使计算机系统中提供并发的基本机制


异常

异常是异常控制流的一种形式,一部分由硬件实现,一部分由操作系统实现

处理器中,状态被编码成不同的位和信号,状态的变化叫做事件

任何情况下,当处理器检测到有事件发生时,就会通过一张异常表,进行一个简洁过程调用(异常)到一个专门设计用来处理这类事件的操作系统子系统(异常处理程序

当异常处理程序完成处理后,根据异常的事件的类型,会发生以下三种情况

  • 处理程序将控制返回给当前指令 Icurr I c u r r ,即当事件发生时正在执行的指令
  • 指出程序将控制返回给 Inext I n e x t ,即如果没有发生异常将会发生将会执行的下一条指令
  • 处理程序终止被中断的程序

异常处理

系统中为每种类型的异常分配了一个唯一个一个非负整数异常号,一些是由处理器设计者分配,一些由操作系统内核设计者分配

系统启动时,操作系统分配和初始化一张异常表,使条目 k k 包含异常k的处理程序的地址。异常号是到异常表中的索引,异常表的起始地址放在一个叫做异常表基址寄存器

k+4× 异 常 k 的 条 目 的 地 址 = 异 常 表 基 址 寄 存 器 + 4 × 异 常 号

异常与过程调用的不同之处

  • 过程调用时,在跳转到处理程序之前,处理器将返回地址压入栈

    根据异常的类型,返回地址要么是当前指令,要么是下一条指令

  • 异常还将处理器的一些额外状态压入栈里

  • 过程调用将数据压入到用户栈

    异常将数据压入到内核栈

  • 过程在用户模式

    异常处理程序在内核模式

异常种类

类别原因同步/异步返回行为
中断(interrupt)I/O设备信号异步返回下一条指令
陷阱(trap)有意的异常同步返回下一条指令
故障(fault)潜在可恢复的错误同步可能返回当前指令
终止(abort)不可恢复的错误同步不返回

IA32异常

中断

硬件中断不是由任何一条专门指令造成的,所以是异步的

硬件中断的异常处理程序称为中断处理程序

陷阱和系统调用

陷阱是执行一条指令的结果,最重要的用途是在用户程序和内核之间提供一个像过程一样的接口——系统调用

Linux系统调用

系统调用运行在内核模式,允许系统调用执行指令,并访问定义在内核的栈

处理器提供了一条特殊的syscall指令,当用户想要请求服务,执行这条指令,会导致一个到异常处理程序的陷阱,这个处理程序对参数解码,并调用适当的内核程序

故障

故障由错误引起,可能被故障处理程序修正

如果故障处理能修正错误,就将控制返回到引起故障的指令,从新执行,否则返回到内核的abort例程,终止引起故障的应用程序

终止

终止是不可恢复的致命错误造成的结果,通常是硬件错误

终止处理程序从不将控制返回给应用程序,而是返回abort例程,终止这个引用程序

进程

经典定义:进程是一个执行中的程序的实例

系统中每个程序都是运行在某个程序上下文中的,上下文由程序正确运行所需的状态组成,包括存放在存储器中程序的代码、数据、栈、通用目的寄存器的内容、程序计数器、环境变量、打开文件描述符的集合

用户通过向Shell输入一个可执行文件的名字,并运行一个进程时,Shell就会创建一个新的进程,然后在这个新的进程的上下文运行这个可执行目标文件

进程提供给用户程序关键抽象

  • 独立的逻辑控制流

    好像程序独占使用处理器

  • 私有的地址空间

    好像程序独占使用存储器系统

逻辑控制流

PC值的序列叫做逻辑控制流,简称控制流

进程轮流使用处理器,每个进程执行它的流的一部分,然后被抢占(prempted)然后轮到其他进程

一个逻辑流的执行在时间上与另一个流重叠,称为并发流,多个流并发地执行的一般现象成为并发,一个进程与其他进程轮流运行的概念称为多任务,一个进程执行它的控制流的一部分的每个时间段叫做时间片

如果两个流并发地运行在不同处理器核或者计算机上,称为并行流。并行流是并发流的真子集。并行流中的程序并行地运行,并行地执行

私有地址空间

一个进程为每个程序提供自己的私有地址空间,和这个空间中某个地址相关联的存储器字节不能被其他进程读/写

用户模式和内核模式

处理器使用某个控制寄存器中的一个模式位来描述进程的特权状态

当设置了模式位,进程就在内核模式,可以执行指令集中的任何指令,访问系统中任何存储器位置

没有设置模式位,进程就在用户模式,不允许执行特权指令,不允许直接引用地址空间中内核区的代码和数据,必须通过系统调用接口间接地访问内核代码和数据

进程从用户模式变为内核模式的唯一方法是通过终端、故障、陷阱这样的异常,当处理程序返回到应用程序时,模式从内核模式改为用户模式

上下文切换

系统内核使用一种称为上下文切换的较高层次的异常控制流来实现多任务

内核为每个进程维持一个上下文,就是内核重新启动一个被抢占的进程所需的状态

在进程执行的时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个决定叫做调度,由内核中的调度器的代码处理。调度通过上下文切换的机制将控制转移到新的进程

  1. 保存当前进程的上下文
  2. 恢复某个先前被抢占的进程被保存的上下文
  3. 将控制传递给这个新恢复的进程
进程控制
  • 获取进程ID
  • 创建和终止进程
  • 回收子进程
  • 休眠
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

// 返回pid
pid_t getpid();

// 返回父进程pid
pid_t getppid();


// 以状status退出状态来终止进程
void exit(int status);

/* 创建新的子进程
 * 父进程返回得到子进程pid
 * 子进程返回得到0
 */
pid_t fork();

/* 等待子进程终止
 * 如果成功,得到子进程pid
 * 如果WNOHANG,得到0
 * 如果错误,得到-1
 * pid:     >0 单独的子进程,进程id等于pid
 *          = -1 所有子进程的集合
 * status:  WIFEXITED 如果子进程调用exit或者return正常终止,返回真
 *          WEXITSTATUS 返回正常终止的子进程的退出状态
 *          WIFSIGNALED 如果子进程因为未被捕获的信号终止,返回真
 *          WTERMSIG 返回导致子进程终止的信号的编号
 *          WIFSTOPPED 如果引起返回的子进程当前是被停止的,返回真
 *          WSTOPSIG 返回引起子进程停滞的信号的数量
 * opition: WNOHANG 如果等待集合中任何子进程都没有终止,立即返回pid,否则返回0
 *          WUNTRACED 直到等待集合中一个进程终止,才返回
 *          WNOHANG | WUNTRACED 立即返回,如果有子进程终止,返回pid,否则0
 * errno:   ECHILD 如果进程没有子进程
 *          EINTR 如果waitpid函数被信号中断
 */
pid_t waitpid(pid_t pid, int *status, int options);

/* 等待子进程终止
 * 如果成功,得到子进程pid
 * 如果错误,得到-1
 * 等价于 waitpid(-1, &status, 0);
 */
pid_t wait(int *status);

/* 进程挂起指定时间
 * 如果时间量到了,返回0
 * 否则,返回剩余休眠的秒数
 */
unsigned int sleep(unsigned int secs);

/* 调用函数休眠,直到收到一个信号
 * 总是返回-1
 */
int pause();

/* 加载并运行可执行目标文件,带有参数列表和环境变量列表
 * 如果成功,不返回
 * 如果出错,返回-1
 */
int execve(const char *filename, const char *argv[], const char *envp[]);

/* 在环境数组中搜索name=value
 * 如果找到,返回指向value的索引
 * 如果没找到,返回NULL
 */
char *getenv(const char *name);

/* 设置环境变量
 * 如果成功,返回0
 * 如果错误,返回-1
 * overwrite:   !0 如果环境数组包含name=oldvalue,则用newvalue替换
 *              0 不替换 
 */
int setenv(const char *name, const char *newvalue, int overwrite);

// 删除name=value的环境变量
void unsetenv(const char *name);

进程组

每个进程只属于一个进程组,由进程组ID标识

默认,一个子进程和父进程属于一个进程组

#include <unistd.h>

// 返回调用进程的进程组ID
pid_t getpgrp();

/* 设置进程组ID
 * 如果成功,返回0
 * 如果出错,返回-1
 * pid:     0 使用当前进程pid
 *          !0 指定进程pid
 * pgid:    0 pid做为进程组ID
 *          !0 指定进程组ID
 */
int setpgid(pid_t pid, pid_t pgid);

信号

信号通知进程系统发生了一个某种类型的事件,每种信号类型对应于某种系统事件

Linux信号

发送信号

内核通过更新目的进程上下文中某个状态,发送一个信号给目的进程

发送信号的机制基于进程组

原因

  • 内核检测到系统事件
  • 一个进程调用了kill函数

方式

  • 使用/bin/kill程序
unix> /bin/kill -9 15213
  • 使用键盘

    Shell为每个作业创建一个独立的进程组,任何时刻至多只有1个前台作业和0个或多个后台作业

    输入ctrl-c会导致内核想每个前台进程组中的成员发送一个SIGINT信号,ctrl-zSIGTSTP信号类似

    前台和后台进程组

  • 使用kill函数

#include <sys/types.h>
#include <signal.h>

/* 发送信号给进程
* 如果成功,返回0
* 如果出错,返回-1
*/
int kill(pid_t pid, int sig);
  • 使用alarm函数
#include <sys/types.h>
#include <signal.h>

/* 向自己发送SIGALRM信号
* 如果设置了闹钟,返回剩余秒数
* 如果没有设置闹钟,返回0
*/
unsigned int alarm(unsigned int secs);
接收信号

原因

  • 目的进程被内核强迫以某种方式对信号的发送做出反应

    在任何类型,一种类型至多只会有一个待处理信号,其它相同类型的信号会被简单丢弃

当内核从一个异常处理程序返回,准备将控制传递给进程时,它会检查进程的未被阻塞的待处理信号集合。如果集合为空,内核将控制传递到进程的逻辑控制流的下一条指令,否则选择集合中某个信号 k k (通常是最小的k),强制进程接受信号 k k ,触发某种行为。一旦进程完成了这个行为,控制就传递回进程的逻辑控制流的下一条指令

处理信号的程序叫做信号处理程序

每种信号类型都有一个预定义的默认行为

  • 进程终止
  • 进程终止并转储存储器
  • 进程停止直到被SIGCONT信号重启
  • 进程忽略该信号

可以使用signal函数修改信号的默认行为,但是SIGSTOPSIGKILL不能被修改

修改信号的程序叫做设置信号处理程序

#include <signal.h>

typedef void (*sighandler_t)(int);

/* 修改和信号相关联的默认行为
 * 如果成功,返回指向前次处理程序的指针
 * 如果出错,返回SIG_ERR,不设置errno
 * handler: SIG_IGN 忽略类型为signum的信号
 * SIG_DFL 类型为signum的信号行为恢复成默认信号
 * 其它 handler就是用户定义的函数的地址
 */
sighandler_t signal(int signum, sighandler_t handler);

调用信号处理程序叫做捕获信号

执行信号处理程序叫做处理信号

信号处理的问题

  • 待处理信号被阻塞

    当进程处理一个类型k的信号时,另一个相同类型的信号会变成待处理,不会被接收,直到处理程序返回

    • 待处理信号不会排队等待

      任意类型至多只有一个待处理信号,第二个相同类型的信号会被丢弃

    • 系统调用可以被中断

      在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误条件,并将error设置为EINTR


    • 非本地跳转

      C语言提供了一种用户级异常控制流形式,称为非本地跳转,将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要正常的调用-返回序列

      非本地跳转的实现

      非本地跳转是通过setjmplongjmp函数提供的

      • setjmp只被调用一次,返回多次
        • 第一次调用setjmp,而调用环境保存在缓冲区env中
        • 被每个相应的longjmp调用
      • longjmp从不返回
      #include <setjmp.h>
      
      /* 在env缓冲区保存当前调用环境,供后面longjmp使用
       * 返回0
       */
      int setjmp(jmp_buf env);
      
      // 是setjmp可以被信号处理程序使用的版本
      int sigsetjmp(sigjmp_buf env, int savesigs);
      
      /* 从env缓冲区恢复调用环境
       * 然后触发一个从最近一次初始化env的setjmp调用的返回
       * 然后setjmp返回,并带有非零的retval
       */
      void longjmp(jmp_buf env, int retval);
      
      // 是longjmp可以被信号处理程序使用的版本
      void siglongjmp(sigjmp_buf env, int retval);

      调用环境包括程序计数器、栈指针、通用目的寄存器

      非本地跳转的应用

      • 允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的

        如果在一个深层嵌套的函数调用中发现了一个错误,可以使用非本地跳转至节返回到一个普通的本地化的错误处理程序,而不是费力地揭开调用栈

      • 使一个信号处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令的位置


      操作进程的工具

      • STRACE

        打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹

      • PS

        列出当前系统中的进程(包括僵死进程)

      • TOP

        打印出关于当前进程资源的使用的信息

      • PMAP

        显示进程的存储器映射

      • /proc

        虚拟文件系统,以ASCII文本格式输出大量内核数据结构的内容,用户可以读取这些内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值