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

异常控制流概念:

 通常情况下控制流是平滑的(即每条相邻指令的内存地址也是相邻的),但是也有突变的情况。而这种有突变的控制流就是异常控制流Exceptional Control Flow,ECF(如程序中的跳转,调用和返回;硬件产生的中断等)。

 ECF的好处:

8.1 异常

 

那到底异常的类型与这3种情况是怎么对应的?(8.1.2有描述)

 8.1.1 异常处理

异常表:

 8.1.2 异常的类型

 中断:中断的异常处理一般称为中断处理程序。收到中断时,会先把当前指令完成后,再将控制流突变到中断处理程序。

2.陷阱

陷阱是有意执行一条命令的异常,最重要的用途是给用户程序提供系统调用(如syscall,read,write,execve,exit等)。

 系统调用与普通的函数调用的区别:

普通的函数调用运行在用户模式下,限制了函数可以执行的指令的类型,且只能访问与调用函数相同的栈;而系统调用在内核模式中,允许系统调用执行特殊指令,并访问定义在内核的栈。       

执行内核代码(内核也是程序),访问内核空间,称为内核模式(Kernel Mode)。

当执行应用程序自己的代码时,称为用户模式(User Mode)。程序很多情况下,都需要使用系统调用进入内核模式来完成特定的功能(需要输入输出、申请内存等比较底层的操作时)。

系统调用:

C语言可以使用syscall函数调用任何的系统调用,但标准C库提供了一组方便的包装函数(下表),系统调用如果发生错误会返回-4095到-1之间的错误码,且系统调用返回的任何错误代码存储在errno,可以通过strerror(errno)获取错误码对应的错误信息(字符串英文信息)。

       #include <sys/syscall.h>                 /* For SYS_xxx definitions */

       int syscall(int number, ...);

如:     tid = syscall(SYS_gettid);

C语言中常见的两个执行shell命令的函数: 

int system(const char * string)

FILE *popen(const char * command, const char* type)

为何要有用户模式和内核模式?

一个计算机系统,会有大量的程序同时运行,而内核空间存放的是操作系统内核代码和数据,是被所有程序共享的,直接在程序中修改内核空间中的数据不仅会影响操作系统本身的稳定性,还会影响其他程序,这是非常危险的行为,所以操作系统禁止用户程序直接访问内核空间。因此用户程序想要获得系统资源,就必须通过系统调用,来进入到内核模式,统一由内核来进行管理,保证内核空间的数据不会被随意修改,才能保证操作系统本身和其他程序的稳定性。        

3.故障

故障是由错误情况引起的,当故障发生时,处理器把控制权交给故障处理程序,如果能被修正,则控制返回给引起故障的指令,从而重新执行它,否则返回到内核的abort例程,终止程序。 

比如读取数据时的缺页异常为最常见的故障;程序中的段错误。

4.终止

终止一般是由硬件错误所引起的,不可恢复的致命错误造成的结果。

 比如DRAM或者SRAM位被损坏时发生的奇偶错误。

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

0-31的异常号,是由Intel架构师定义的异常,32-255对应的是操作系统定义的中断和陷阱。

 8.2 进程

 context通俗的理解就是运行的环境。

进程提供给程序的关键抽象:

 8.2.1 逻辑控制流

用调试器单步执行程序时,我们会看到一系列的程序计数器PC的值,这些PC值唯一地对应于包含在程序的可执行目标文件的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值得序列就叫做逻辑控制流,或简称逻辑流。

下图为三个进程对应的三个逻辑流执行的情况。

 8.2.2 并发流

8.2.3 私有地址空间

进程为每个程序提供它自己的私有地址空间,该空间是不被其他进程读写的。每个这样的空间都有相同的通用结构。

一个可执行文件本质上都是由代码段、数据区和未初始化数据区三部分组成的

.bss段(未初始化数据区):通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。.bss段属于静态分配,程序结束后静态变量资源由系统自动释放。

.data段:存放程序中已初始化的全局变量的一块内存区域。数据段也属于静态内存分配。

代码段:存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量(指.rodata)。

栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息就会被压栈,然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的(Linux下通过ulimit -s命令可以查询,一般是4M或者8M),申请的栈空间超过这个界限时会提示溢出。

堆区:用于动态分配内存,由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free会造成内存空间的不连续,产生外部碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
 

共享库的内存映射区域:是运行程序时,需要加载的动态共享库的内存映射区域。关于是如何映射的,此处不讨论。

8.2.4 用户模式与内核模式 

 8.2.5 上下文切换

下图为上下文切换的例子:

 8.3 系统调用错误处理

当系统级函数遇到错误时,通常会返回-1,并设置全局整数变量errno来表示出了什么错。

 错误处理包装函数:

对需要被调用的函数,定义一个具有相同参数的函数,但函数名的第一个字母为大写,并检查错误。

 

 8.4 进程控制

获取进程ID:

创建与终止进程:

 C语言中能让进程停止的函数:

int pause(void);

unsigned int sleep(unsigned int seconds);

创建子进程

 

终止进程

 回收子进程:

 waitpid函数等待子进程结束:

 pid为所需等待的子进程PID号

options为修改该函数默认行为的参数

statusp是指向已回收子进程的退出状态值 

进程休眠:

加载并运行程序: 

fork和execve结合运行程序:

 8.5 信号

更高层的软件形式的异常,称为linux信号,它允许进程和内核中断其他进程。

信号提供了一种机制,把低层的异常通知到用户进程。

下图为linux支持的30中信号类型:(常见的SIGINT(Ctrl+C)和SIGKILL(kill -9命令))

发送信号与接收信号:

 进程组:每个进程都只属于一个进程组。

获取当前进程所在的进程组ID

改变指定进程的进程组ID 

  linux中使用ps命令查看进程

UID进程用户ID
PID进程ID 
PPID父进程ID
PGID进程组ID
TTY进程所属的控制台
STIME进程启动时间

不同系统的PS命令的信息可能不一样,还有top 命令也是用来看进程的信息的。

前台和后台进程:

发送信号:

使用bin/kill 发送信号:kill -9 pid

使用键盘发送信号:Ctrl + C,Ctrl + Z

使用C语言中kill函数给指定进程发送信号:kill(pid,SIGKILL)

使用C语言alarm函数给自己发送信号:alarm(1),一秒后给自己发送一个SIGALRM信号

接收信号:

信号处理的默认行为有以下几种:

 通过signal函数可以改变信号的处理方式

 下图为信号处理程序的信号处理流程:

阻塞和解除阻塞信号:

8.6 非本地跳转

 sig开头的函数是在信号处理函数中使用的。goto语句跳转到当前函数的一个标志。而jmp是可以直接跳转到其他函数。

setjmp函数会把当前调用的环境保存在env缓冲区中,若是直接调用setjmp返回值为0,若是通过longjmp跳转的,会返回longjmp中的retval值。

例1 setjmp和longjmp:输出结果为“Detected an error2 condition in foo”

 例2 sigsetjmp和siglongjmp 输出结果为

 8.7 操作进程的工具

习题:

 

 

答案:AB:否,AC:是,AD:是,BC:是,BD:是,CD:是。 

 

 

答案:5次。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值