第八章异常控制流

指令的地址的过渡称为控制转移

控制转移序列叫做处理机的控制流

过程中发生的突变叫做异常控制流

 

8.1 异常

异常就是控制流中的突变

一部分由硬件实现,一部分由操作系统实现

当处理器检测到有事情发生时,它就会通过一张叫做异常表的跳转表,进行一个间接程序调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)

异常处理程序完成处理后:

(1)       处理程序将控制返回给当事件发生时正在执行的指令

(2)       处理程序将控制返回给如果没有发生异常将会执行的下一条指令

(3)       处理程序终止被中断的程序

 

8.1.1 异常处理

每种类型的异常都分配了一个唯一的非负整数的异常号

在程序启动时,操作系统会分配和初始化一张称为异常表的跳转表

异常号是异常表的索引,异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器中

在处理程序处理完事件之后,它通过执行一条“从终端返回”指令,可选的返回到被中断的程序

如果异常中断的是一个用户程序,就将状态恢复为用户模式,然后将控制返回给被中断的程序

 

8.1.2 异常的类别

分为四类:中断、陷阱、故障、终止

1.中断

中断是异步发生的(其他都是同步发生的,叫做故障指令),是来自处理器外部的I/O设备的信号的结果

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

中断处理程序将控制返回给应用程序控制流中的下一条指令

2.陷阱和系统调用

陷阱是有意的异常,是执行一条指令的结果。

陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用

为了允许对这些内核服务的受控的访问,处理器提供了一条特殊的“syscall n”指令,当用户程序想要请求服务n时,可以执行这条指令

执行syscall会导致一个到异常处理程序的陷阱,这个处理程序对参数解码,并调用合适的内核程序

3.故障

当故障发生时,处理器将控制转移给故障处理程序

如果处理程序能够修正这个错误情况,它就将控制返回到引用故障的指令,从而重新执行它,否则处理abort例程,它会终止引起故障的应用程序

4.终止

终止通常是一些硬件错误

终止处理程序从不将控制返回给应用程序

终止处理程序将控制传递给一个内核abort例程,该例程会终止这个应用程序

 

8.1.3 Linux/IA32系统中的异常

有256种不同的异常类型,0~31的号码对应的是由Intel架构师定义的异常,因此对任何IA32系统都一样,32~255的号码对应的是操作系统定义的中断和陷阱

1.Linux/IA32故障和终止

除法错误:当试图除以0时,或结果过大时,就会发生除法错误(异常0)

一般保护措施:许多原因(异常13)

缺页(异常14)

机器检查(异常18)

2.Linux/IA32系统调用

各自调用见书p485图8-10

系统调用是通过一条称为int n的陷阱指令来提供的

Write函数的第一个参数将输出发送到stdout,第二个参数是要写的字节序列,第三个参数是要写的字节数

 

8.2 进程

关键抽象:一个独立的逻辑控制流,一个私有的地址空间

8.2.1 逻辑控制流

进程向每个程序提供一种假象,好像它在独占地使用处理器

产生的PC值得序列:逻辑控制流

每个进程执行到它流的一部分,然后被抢占,然后轮到其他进程

8.2.2 并发流

一个逻辑流在执行的时间上与另一个流重叠,叫做并发流,这两个流被称为并发的运行

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

一个进程和其他进程轮流运行的概念称为多任务

一个进程执行它的控制流的一部分的每一个时间叫做时间片,多任务叫做时间分片

如果两个流并发地运行在不同的处理器核或者计算机上,称为并行流

8.2.3 私有地址空间

在一台有n位地址的机器上,地址空间是2的n次方个可能地址的集合

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

8.2.4 用户模式和内核模式

处理器通常用某个控制寄存器中的一个模式位提供限制一个应用可以执行的指令以及它可以访问的地址空间范围的功能

当设置了一个模式位时,进程就运行在内核模式中

没有设置模式位时,进程就运行在用户模式中

用户模式中的进程不允许执行特权指令,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据

8.2.5 上下文切换

内核为每个进程维持一个上下文

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

在执行进程的某个时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这种决定叫做调度,由内核中称为调度器的代码处理

上下文切换:1)保存当前进程的上下文 2)恢复某个先前被抢占的进程所保存的上下文 3)将控制传递给这个新恢复的进程

 

8.3 系统调用错误处理

通过错误包装函数,可以在某种程度上简化这个代码

通过错误处理包装,可以进一步简化代码

 

8.4 进程控制

8.4.1 获取进程ID

每个进程都有一个唯一的正数进程ID(PID)

Getpid函数返回调用进程的PID

Getppid函数返回它的父进程的PID

8.4.2 创建和终止进程

三种状态:

  1. 运行:要么在CPU上执行,要么在等待被执行且最终会被内核调度
  2. 停止:进程的执行被挂起,且不会被调度
  3. 终止:三种原因:1)收到一个信号,该信号的默认行为是终止进程 2)从主程序返回

3)调用exit函数

Exit函数以status退出状态来终止进程

父程序通过调用fork函数创新一个新的运行子程序

Fork函数只能被调用一次,却会返回两次:一次是在父程中,一次是在新创建的子进程中

在父进程中,fork返回子进程的PID

在子进程中,fork返回0

8.4.3 回收子进程

当一个进程由于某种原因终止,它要保存一种已终止的状态直到被他的父进程回收

一个终止了但还未被回收的进程称为僵死进程

一个进程可以通过调用waitpid函数来等待它的子进程终止

1.判定等待集合的成员

由参数pid确定,如果大于0,那么等待集合的就是一个单独的子程序,它的进程ID等于pid

如果pid等于-1,那么等待集合是由父进程所有的子进程组成的

2.修改默认行为

WNOHANG,WUNTRACED,WNOHANG|WUNTRACED

3.检查已回收子程序的退出状态

WIFEXITED,WEXITSTATUS,WIFSIGNALED,WTERMSIG,WIFSTOPPED,WSTOPSIG

4.错误条件

如果没有子进程,那么waitpid返回-1,并且设置errno为ECHILD

5.Wait函数:调用wait函数等价于调用waitpid(-1,&status,0)

8.4.4 让程序休眠

Sleep函数将一个进程挂起一段指定时间

如果请求的时间量已经到了,sleep返回0,否则返回还剩下的要休眠的秒数

Pause函数:让调用函数休眠,直到该进程收起一个信号

8.4.5 加载并运行程序

Execve函数在当前进程的上下文中加载并运行一个新程序

Int execve(const char *filename,const char *argv[],const char *envp[]);

操作环境数组的函数:

Getenv,setenv,unsetenv

8.4.6 利用fork和execve运行程序

外壳是一个交互型的应用级程序,代表用户运行其他程序

外壳执行一系列的读/求值步骤,然后终止

Parseline函数:解析了以空格分隔的命令行参数

如果最后一个参数是&字符,那么返回1,表示应在后台执行该程序,否则返回0,表示应在前台执行该程序

 

8.5 信号

一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件

每个信号类型都有对应于某种系统事件

8.5.1 信号术语

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

两个原因:1)内核检测到一个系统事件 2)一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程

接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信息处理程序的用户层函数捕获这个信号

一个发出而未被接收的信号叫做待处理信号

一个进程可以有选择地阻塞接收各种信号,当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞

8.5.2 发送信号

Unix系统提供了大量向进程发送信号的机制。所有这些机制都是基于进程组

1.进程组

每个进程只属于一个进程组,进程组是由一个正整数进程组ID来标识的,getpgrp函数返回当前进程的进程组ID

一个子函数和一个父函数同属于一个进程组,一个进程可以通过setpgid函数来改变自己或者其他进程的进程组

2.用/bin/kill程序发送信号

/bin/kill可以向另外的进程发送任意的信号

一个为负的PID会导致信号被发送到进程组PID中的每个进程

3.从键盘发送信号

Unix外壳使用作业这个抽象概念来表示为对一个命令行求值而创建的进程,在任何时刻,至多只有一个前台作业和0个或多个后台作业

外壳为每个作业创建一个独立的进程组

4.用kill函数发送信号

进程通过调用kill函数发送信号给其他进程

如果pid大于0,kill函数发送信号sig给进程pid

如果pid小于0,kill发送信号sig给进程组abs中的每个进程

5.用alarm函数发送信号

进程可以通过调用alarm函数向自己发送信号

Alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程

如果secs为0那么不好调度新的闹钟

在任何情况下,对alarm的调用都将取消任何待处理的闹钟,并且返回任何待处理的闹钟在被发送前还剩下的秒数

如果没有待处理的闹钟,就返回0

8.5.3 接收信号

当内核从一个异常处理程序返回,准备将控制传递给进程p时,它会检查进程p的未被阻塞的待处理信号的集合

如果集合为空,内核将控制下一条指令

如果集合非空,需要选择信号k,并强制p接收信号k

每个信号类型都有一个预定义的默认行为:进程终止,进程终止并转储存储器,进程停止直到被SIGCONT信号重启,进程忽略该信号

Signal函数可以通过三种方法改变和信号signum相关联的行为:

如果handler是SIG_IGN,那么忽略类型为signum的信号

如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为

Handler是用户定义的函数的地址,这个函数称为信号处理函数,只有进程收到一个类型为signum的信号,就调用这个程序。通过把处理程序的地址传递到signal函数从而改变默认行为,叫做设置信号处理程序。调用信号处理程序捕获不同类型的信号

8.5.4 信号处理问题

待处理信号被阻塞

待处理信号不会排队等待

系统调用可以被中断

8.5.5 可移植的信号处理

缺陷:信号处理语义的差异

定义一个包装函数,称为Signal,它调用sigaction

Signal包装函数设置了有一个信号处理秩序:

只有这个处理程序当前正在处理的那种类型的信号被阻塞

和所有信号实现一样,信号不会排队等待

只要可能,被中断的系统调用会自动重启

一旦设置了信号处理程序,它就会一直保持,直到Signal带着handler参数为SIG_IGN或者SIG_DFL被调用。

8.5.6 显式地阻塞和取消阻塞信号

Sigprocmask函数改变当前已阻塞信号的集合,具体的行为依赖于how的值:

SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK

 

8.6 非本地跳转

控制直接从一个函数转移到另一个正在执行的函数,而不需要经过正常的调用-返回序列

Setjmp函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0

Longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回,然后setjmp‘返回,并带有非零的返回值retval

 

8.7 操作进程的工具

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

PS:列出当前系统中的进程

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

PMAP:显示进程的存储器映射

/proc:一个虚拟文件系列,以ASCII文本格式输出大量内核数据结构的内容,用户程序可以读取这些内容

 

参考资料:《深入理解计算机系统》

遇到的问题:

同步流避免并发错误的地方没太看懂

信号处理问题不太懂

心得体会:上周没写博客所以有点手生,这次学的也完全是全新的知识,还是蛮有趣的,但是理解起来还是多多少少有些困难。

posted on 2015-11-22 04:18  綺麗な嘔吐物  阅读(196)  评论(0编辑  收藏  举报