深入理解计算机系统_第8章 异常控制流

异常控制流发生在计算机系统的各个层次:
1) 硬件层面, 硬件检测到事件会触发控制,跳转到异常处理程序;
2) 操作系统层面, 内核通过上下文切换,实现进程切换,也是异常控制流;
3) 应用程序层面, 信号的信号处理程序,也是异常控制流;
语言层面的try, catch, throw也是异常控制流;
非本地跳转setjmp, longjmp也是异常控制流;

8.1 异常

异常就是控制流中的突变,用来响应处理器状态的某些变化;
异常事件可能是与当前执行指令相关: 虚拟内存缺页、算术溢出、一条指令试图除以零;
异常事件与当前指令无关:定时器产生的信号、一个IO请求;
8.1.1 异常处理
系统为每种类型的异常分配了唯一一个异常号,有一些异常号由处理器设计者提供, 有一些异常号由操作系统内核的设计者分配;
处理器设计者提供的异常号:零除, 缺页, 内存访问违例,断点, 算术溢出;
操作系统内核提供的异常号: 系统调用, 外部的有关IO设备;
异常表是异常号与异常处理程序的表,通过索引异常号可以得到具体的异常处理程序;异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器中;

异常与过程调用的不同之处:
1) 异常的返回地址要么是当前中断的指令,要么是下一条指令;
2) 处理异常处理程序的时候iu处理器会将一些额外的处理器状态压到栈中。处理程序返回的时候, 重新开始处理被中断的程序可能需要这些状态;
3) 处理异常处理程序的时候,如果控制从用户转换到内核, 处理器的状态被压入到内核栈中,而不是用户栈中;
4) 异常处理程序运行在内核状态下,意味着一场处理程序对所有的系统资源都具有访问权限;

8.1.2 异常的类别
异常卡哇伊分为四类:中断、陷阱、故障、终止;
在这里插入图片描述
1) 中断
中断是异步发生的,来自处理器外部的IO设备的信号。硬件中断不是由任何一条专门的指令造成的, 所以是异步的。
陷阱、故障、终止都是当前指令执行的结果,所以是同步到;
2) 陷阱和系统调用
陷阱实际上就是系统调用,用户程序经常要向内核请求服务,为了允许用户对内核服务的受控访问, 操作系统提供系统调用接口;
3) 故障由错误引起,肯呢个会被错误处理程序修正。如果故障处理程序能够修正这个故障, 控制就返回到引起故障的指令(当前指令)再次执行;否则故障处理程序就执行内核的abort例程, abort例程会终止引起故障的应用程序;
例如缺页异常;
4) 终止
终止时不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序会直接调用一个abort例程, abort例程会终止这个应用程序;

8.1.3 linux/x86-64系统中的异常

1) Linux/x86-64 故障和终止

  1. 除法错误
    除法错误直接终止程序, linux shell将除法错误报告成浮点异常;(floating execotion)
  2. 一般保护故障
    一个程序引用了未定义的虚拟内存区域, 或者因为程序试图写一个只读的文本段;这些是段故障(segmentation fault);
  3. 缺页异常
    异常处理程序会将适当的磁盘上的一个页面映射到物理内存上,然后返回重新执行引起故障的指令;(实际上就是缓存的机制)
  4. 机器检查
    检测到致命的硬件错误时发生的。

2) Linux/86-64 系统调用
linux有几百个系统调用,每一个系统调用都有唯一一个整数号, 对应于内核中跳转表的偏移量(区别跳转表和异常表);
所有linux系统调用的参数都是通过通用寄存器而不是栈传递的。寄存器%rax 包含系统调用号, 其他6个寄存器包含最多6个参数, %rax 包含返回值;

8.2 进程

每一个进程都有一个上下文, 上下文是程序正确运行时需要的各种状态, 包括内存中的代码和数据,栈、通用目的寄存器, PC, 环境变量, 打开的文件描述符集合;
关于进程的两个抽象:
1) 一个独立的逻辑控制流,好像我们的程序独占处理器资源;
2) 一个私有的地址空间, 好像程序独占内存系统;
8.2.1 逻辑控制流
进程的PC值的序列就是逻辑控制流;

8.2.2 并发流
一个逻辑流动执行时间上与另外一个流重叠,就是并发流;
多个流并发地执行的现象称为并发;
一个进程和其他进程轮流运行的概念称为多任务;
一个进程执行他的控制流的一部分的每一时间段叫做时间片;
并行流是并发流的一个子集,并行流中,不同的逻辑控制流同时运行在不同的处理器上;

8.2.3 私有地址空间
进程为程序提供一种假象,好像进程独自占有系统的地址空间;
和这个地址空间相关联的物理内存中的字节不能被其他进程访问,从这个意义上讲, 这个地址空间是私有的;
不同进程的私有空间有着相同的通用结构:
在这里插入图片描述

8.2.4 用户模式和内核模式
处理器必须提供一种机制, 限制一个应用可以执行的指令以及他可以访问的地址空间范围;处理器使用一个控制寄存器中的模式位来提供这种功能,设置了该位, 当前进程就具有特权,运行在内核模式;
用户模式的进程不允许执行特权指令, 比如停止处理器、改变模式位,或者发起一个IO操作。 也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据;
linux提供一种机制, 叫做/proc文件系统,允许用户模式的进程访问内核数据结构的内容。/proc文件系统将许多的内核数据结构的内容输出为一个用户可以读到文本文件的层次结构。例如, 使用/proc文件系统找出一般的系统属性(CPU类型, /proc/cpuinfo); 某个特殊的进程使用的内存段(/proc//maps);

8.2.5 上下文切换
上下文包括对对象:
通用目的寄存器, 浮点寄存器, PC, 用户栈, 状态寄存器, 内核栈, 各种内核数据结构(页表, 进程表, 文件表);
发生上下文切换的情形:
1) 调度器决定切换进程的时候;
2) 当内核代表用户执行系统调用时候,可能会发生上下文切换。如果系统调用因为等待某个事件而阻塞, 内核会让当前进程休眠,切换到另一个进程,从而发生上下文切换;
3) 中断也会引起进程切换, 所有系统都有某种产生周期定时器中断的机制(1ms 或是10ms), 每次发生定时器中断, 内核就能够判定当前进程已经运行了足够长的时间,并切换到一个新的进程;

8.3 系统调用错误处理

unix系统级函数遇到错误时候, 通常会返回-1;
使用包装函数处理错误,例如将fork函数包装成Fork函数,在Fork函数中处理错误情况,这会使得主函数中代码简洁;

pid_t Fork(void)
{
   
	pid_t pid;
	if((pid = fork()) < 0)
		unix_error("Fork error");
	return pid;
}

8.4 进程控制

8.4.1 获取进程ID

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);

8.4.2 创建和终止进程
进程的3个状态:
1) 运行,进程要么被执行, 要么在等待被执行且被内核调度;
2) 停止,进程挂起,不会被调度
3) 终止, 进程永远地停止了。进程会停止的三个原因:
1. 收到一个信号,该信号终止进行
2. 从主函数返回
3. 调用exit

子进程与父进程共享代码、数据、对、共享库、用户栈,文件描述符;父子进程唯一不同的就是进程ID;fork函数调用一次返回两次,兵器父子进程并发地执行;
理解调用Fork()函数会产生什么样的进程拓扑结构;
在这里插入图片描述
8.4.3 回收子进程
当一个进程终止时候, 内核并不会马上将这个进程从系统中清除,而是保持这个进程为一种已终止的状态,知道父进程回收子进程资源。一个已经终止了但是没有被回收的进程是僵尸进程;
父进程使用waitpid回收子进程资源

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);

默认情况时(options =0), waitpid挂起当前进程, 阻塞等待子进程中一个子进程终止。 如果等待集合中的一个进程在刚调用的时刻之前就已经终止了, waitpid会立即返回。
1) 判定等待集合的成员
pid > 0, 等待一个单独的子进程;
pid = -1, 等待子进程中的一个进程, 默认情况;
2) 修改默认行为, options参数
WNOHANG:不阻塞,没有终止的子进程时候函数立即返回;
WUNTRACED: 挂起调用进程的执行, 直到等待集合中的一个进程变为已终止或者被停止
WCONTINUED:挂起调用进程的执行, 直到等待集合中的一个正在运行的进程终止或者一个被停止的进程收到SIGCONT信号重新开始;
3)检查一回收子进程的退出状态
statusp返回子进程状态信息
WIFEXITED(status):是否正常终止
WEXITSTATUS(status): 返回一个正常终止的子进程的终止状态;
WIFSIGNALED(status) :如果子进程因为一个没有被捕获的信号终止的,就返回真;
WTERMSIG(status):返回导致子进程终止的信号的编号;
WIFSTOPPED(status):如果返回的子进程状态是停止的,为真;
WIFCONTINUED(status):如果返回的子进程是重新启动的,为真;
wait函数是waitpid函数的默认版本
wait(&status) 等价于waitpid(-1, &status, 0);
8.4.4 让进程休眠

unsigned int sleep(unsigned int secs);
int pause(void);

当一个信号打断sleep函数的时候, sleep函数返回剩余的时间;
pause使得调用进程一直等待,直到接收到一个信号;

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

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

在这里插入图片描述
进程栈顶实际内容如下,栈顶顶部是系统启动函数libc_start_main的栈帧;
在这里插入图片描述
关于环境变量的三个函数,不建议直接操作环境变量environ;

#include <stdlib.c>
char *getenv(const char *name);
int setenv(const char *name, const char *newvalue, int overwrite);
void unsetenv(const char *name);

8.4.6 利用fork和execve运行程序

8.5 信号

linux信号, 允许进程和内核通过发送信号中断其他进程。
低层次的硬件异常是由内核的异常处理程序处理的,正常情况下, 对用户进程是不可见的。信号提供了一种机制,通知用户进程发生了这些硬件异常;
例如:
除零,硬件异常:SIGFPE信号;
一个进程执行非法指令: 内核发出SIGILL给这个进程;
非法内存引用: 内核向进程发出SIGSEGV信号;

8.5.1 信号术语

传送一个信号到目的进程由两个不同的步骤组成:
1) 发送信号, 发送信号肯呢个会有两种原因
1. 内核检测到一个系统事件;
2. 一个进程调用kill函数,显式地发送一个信号给目的进程;
2) 接收信号
进程对信号的处理方式有三种:
1. 接受这个信号,执行默认的动作
2. 忽略这个信号
3. 执行自定义的信号处理程序

一个发出但是还没有被接受的信号称为待处理信号。在任何时刻,一种类型的信号最多只有一个待处理信号。如果一个进程有一个类型为k的待处理信号,那么任何接下来发送到这个进程的类型为k的信号都不会排队,这些信号只是简单地被丢弃。
一个进程可以有选择地阻塞接收某种信号。当一种信号被阻塞的时候, 这个信号依旧可以被发送,但是待处理的信号不会被接受,知道进程取消对这种信号的阻塞;
内核为每一个进程在pending位向量中维护着待处理信号的集合;在blocked向量中维护着被阻塞的信号集合。只要传送了一个类型为k的信号,内核就会设置pending中的k位;只要进程接收了k信号,内核就会清除pending中的k位;

8.5.2 发送信号
1) 进程组
每个进程都只属于一个进程组,进程组由一个正整数进程组ID来标识。getpgrp()函数返回当前进程的进程组ID;
一个进程可以通过使用setpgid函数改变自己或是一个进程的进程组;

pid_t getpgrp(void);
int setpgid(pid_t pid, pid_t pgid);//将进程pid的进程组改为pgid;

2) 用/bin/kill 程序发送信号
使用kill向一个负的进程ID发生信号,这个信号会被发送到这个进程组中所有的进程中;

3) 从键盘发送信号
Ctrl+C:内核发送一个SIGI

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值