Computer_Systems_A_Programmer's_Perspective_读书笔记
- 9 mins书中存在的错误
- P150页(书本:114页)错误:
- 应该是256TB
- P165页(书本:129页)错误:
- 这里的操作顺序和ATT格式的汇编代码相反?应该是相同的吧
- P176页(书本:140页)错误:
- 红线标注部分应该是32位机器的内容,x86-64使用的应该是48位长的虚拟地址长度
- 所以应该改为,1,2,4或8字节,然后使用8个字节直接指定目标(???我也不确定正确应该是怎样)
- P200页(书本:164页)错误:
- 应该是stack frame
简介
我读的是《Computer Systems:A programmer’s Perspective》的中文翻译版《深入理解计算机系统》
- 密码:
3hrt
Chapter 1: 计算机系统漫游
-
本书的目的就是帮助程序员理解当执行hello world程序时,操作系统到底发生了什么以及为什么会这么会如此运作
-
在unix系统上,从源文件到可执行目标文件(executable object file)是由编译器驱动程序(compiler driver) 完成的
unix> gcc -o hello hello.c
上述过程是gcc读取源程序文件hello.c,并翻译成可执行目标文件hello。这个翻译过程是分四个阶段完成的。
预处理器(cpp)-> 编译器(ccl)-> 汇编器(as)-> 链接器(ld)
.s
也称汇编文件-
.o
也称目标代码文件 - shell是一个命令行解释器(一个应用程序)
1.4.1 系统硬件组成
- 总线
- 字(word)
- 字长(字中的字节数)
- 字节(8 bit)
- I/O设备
- 主存
- 物理上:主存是由一组DRAM(动态随机存取存储器)芯片组成的
- 逻辑上:存储器是由一个线性的字节数组组成的,每个字节都有自己唯一的地址(数组索引)
- 处理器(CPU)
- 核心:一个被称为程序计数器(PC)的字长大小的存储设备(或寄存器)
- 可能会执行的操作:
- 加载
- 存储
- 更新
- I/O读
- I/O写
- 转移
1.4.2 - 1.10
-
DMA(直接存储器存取):数据可以不通过CPU而直接从磁盘到达主存
-
加快处理器的运行速度比加快主存的处理速度要容易和便宜得多
-
为了解决处理器和主存之间读取速度的差异,采用了高速缓存存储器(cache memories,简称高速缓存)
-
L1和L2高速缓存是采用一种叫做静态随机访问存储器(SRAM)的硬件技术实现的
- 操作系统的两大功能:
- 防止硬件被失控的应用程序滥用
- 管理复杂而又广泛不同的低级硬件设备,给应用程序提供简单统一的接口
-
unix和posix标准
-
进程
- 进程是计算机科学中最重要和最成功的概念之一
- 进程是操作系统对运行程序的一种抽象
- 操作系统可以运行多个进程
- 一个进程的指令和另一个进程的指令是交错进行的,我们称之为上下文切换(context switching)
- 在任何时刻,都有且仅有一个进程正在运行
- 实现线程需要低级硬件和操作系统软件的紧密合作
- 线程
- 虚拟存储器
- 虚拟存储器是一个抽象的概念
- 它为每个进程提供一个假象,每个进程好像都在独占主存
- 每个进程看到的存储器都是一致的,称之为虚拟地址空间
- 先简单看看每一区
- 程序代码和数据
- 堆
- 共享库
- 栈
- 内核虚拟存储器
-
文件:文件只不过就是字节序列
-
操作系统内核是硬件和系统软件之间的媒介
Chapter 2 信息的表示和处理
- 字长(word size)
- 指明整数和指针数据的标示大小(nominol size)
- 对32位计算机来说,程序最多访问2^32字节,这就限制了虚拟地址空间为4GB
2.1.3 寻址和字节顺序
-
小端法(little endian)
-
大端法(big endian)
2.1.4 表示字符串
-
C中的字符串为一个以null(其值为零)字符结尾的字符数组
- 文本数据比二进制数据具有更强的平台独立性
- 原因:对使用同一编码格式(例:ASCII码)的任意系统得到的数据结果都是一样的,与字节顺序和字大小规则无关
-
生成异常ascii码表
unix> man ascii
- java使用unicode来表示字符串
2.1.5 表示代码
-
二进制代码很少能在不同的机器和操作系统之间移植
-
程序仅仅只是二进制序列
2.1.6 布尔代数和环(!!!)
-
二进制值是计算机编码、存储和操作信息的核心
-
布尔代数(bool algebra)
- 起源:1850年左右乔治布尔(George Boole)的工作
- 整数环:
<Z, +, *, -, 0, 1>
- 布尔代数:
<{0,1}, |, &, ~, 0, 1>
- 整数环和布尔代数这两种数据结构大体上相同,但也有一些关键点不同,尤其是在
-
和~
之间 - 布尔环:
<{0,1}, ^, &, /,>
/
:同一运算(identity operation)
练习题2.8 ~a = 10010110 ~b = 10101010 a & b = 01000001 a | b = 01111101 a ^ b = 00111100
- 位向量:
- 可以用向量表示任何子集
练习题2.9 A. 黑 <-> 白 蓝 <-> 黄 绿 <-> 红紫 蓝绿 <-> 红 B. ??? C. 101 001 101
2.1.7 C中的位级运算
练习题2.10 ??? 第一步:x = a^b , *y = b 第二步:x = a^b , y = a 第三步:x = b , *y = a
练习题2.12 x|m x&m
2.1.8 C中的逻辑运算
&&, ||, !
练习题2.14 !(x^y)
2.1.9 C中的移位运算
<<, >>
>>
:右移运算- 一般而言系统支持两种方式的右移运算
- 逻辑右移:往左边补0
- 算术右移:在左边补最高有效位的拷贝
- 算术右移对有符号整数数据的运算非常有用
- 一般而言:无符号(unsigned)整数必须使用逻辑右移,有符号整数使用算术右移
- 一般而言系统支持两种方式的右移运算
2.2 整数表示
- C支持多种整形数据类型——表示有限范围的整数
- char
- short
- int
- long
-
C/C++中支持无符号数和有符号数,Java只支持有符号数
-
有符号数的计算机表示:二进制补码(two’s-complement)
- 二进制反码(ones’ complement):P61
-
符号数值(sign-magnitude):P61
-
无符号二进制
-
二进制到二进制补码
- 历史上存在过的有符号编码
- 二进制反码
- 符号数值
- 强制类型转换没有改变位的表示,改变的只是解释位的方式
练习题2.18 * -8 = 8 * -6 = 10 * -4 = 12 * -1 = 15 * 0 = 0
* 3 = 3
2.2.3 补码编码
2.2.4 有符号和无符号数之间的转换(!!!)
- T2U中的T为Two’s-complement,U为Unsigned
2.2.5 C中的有符号与无符号数
- 显示强制转换
-
隐示强制转换
- 由于C同时包含有符号和无符号数的表达式的处理方式,会出现一些奇特的行为
- 一个运算,有一个运算数是无符号的,另一个运算数是有符号的,那么C会隐示地将有符号参数强制转换为无符号数
练习题:2.20 * 无符号 1 * 有符号 0 (-2147483648在32位机器上会溢出,但会被机器识别成什么数字呢?) * 无符号 0 * 有符号 1 * 无符号 1
2.2.6 拓展一个数字的位表示
- 零拓展(zero extension)
- 将一个无符号整数拓展成一个更大的数据类型,高位补0
- 符号拓展(sign extension)
- 将一个二进制补码数拓展成一个更大的数据类型,高位补符号位
练习题:2.21 * 127, 127 * 128, -128 * 255, -1 * 0, 0
2.2.7 截断数字
B2U_k([X_k-1, X_k-2, ..., X_0]) = B2U_w([X_w-1, X_w-2, ..., X_0]) mod 2^k
B2T_k([X_k-1, X_k-2, ..., X_0]) = U2T_k(B2U_w([X_w-1, X_w-2, ..., X_0])) mod 2^k
2.2.8 关于有符号数和无符号数的建议
练习题 2.25 * 因为length - 1,当无符号数和有符号数进行运算时,机器自动将有符号数强制类型转换为无符号数, 这就会造成length - 1 != 0,而是UMax_w * 修改为 (int)length - 1
练习题 2.26 * 当s<t时 ???
- 事实上除了C以外很少有语言支持无符号数
- Java中
>>
表示算术右移,>>>
表示逻辑右移
- Java中
2.3 整数运算
2.3.1 无符号加法
- 大部分编程语言支持固定精度的运算,少部分编程语言支持无精度运算
练习题2.27
- 模数加法形成了一种数学结构,称为阿贝尔群(Abelian group)
- 单位元0
- 每个元素有一个加法逆元
- 元素和它的加法逆元相加会等于单位元0
练习题2.28 * 0, 0, 0 * 5, 11, B * 8, 8, 8 * 13, 3, 3 * 15, 1, 1
2.3.2 补码加法
2.3.3 补码的非
练习题 2.33 * 0, 0, 0 * 5, 11, B * 8, 8, 8 * 13, 3, 3 * 15, 1, 1
- 补码非的位级表示!!!
- 执行位级补码非的第一种方法是对每一位求补,再对结果加1
- 在C中,-x和~x+1结果是一样的
- 执行位级补码非的第二种方法是按从低位向高位的方向,将出现的第一个1之后的所有位求补
- 执行位级补码非的第一种方法是对每一位求补,再对结果加1
2.3.4 无符号数乘法
2.3.5 补码的乘法
2.4.2 IEEE浮点表示
标准:`V = (-1)^s * M * 2^E
- 符号(sign)
- 尾数(significand)
-
阶码(exponent)
- 单精度格式
Chapter3 程序的机器级表达
-
每个后继处理器的设计都是向后兼容的
-
摩尔定律
- 半导体工业一直能使晶体管的数量每18个月翻一倍
3.2.1 机器级代码
- 机器级编程有两种抽象特别重要
- 第一种抽象:指令集架构(Instruction Set Architecture)
- 第二种抽象:机器级程序使用的内存地址是虚拟地址
- x86-64的虚拟地址是64位的字
- 在目前的实现中,高16位必须设为0。所以一个地址实际上能够指定的是2^48或256TB范围内的一个字节
- 程序计数器(通常称”PC”,在x86-64中用
%rip
表示)
3.2 程序编码
linux> gcc -Og -o p p1.c p2.c
使用Og等级的优化将p1.c
和p2.c
翻译成可执行文件pOg
优化等级是GCC版本4.8之后引入的Og
优化等级是指编译器会生成符合原始C代码整体结构的机器代码的优化等级- 较高级别的优化产生的代码会严重变形
-
linux> gcc -Og -S mstore.c
使用Og等级的优化将mstore.c
翻译成名为mstore.s
的汇编文件 -
linux> gcc -Og -c mstore.c
使用Og等级的优化将mstore.c
翻译成名为mstore.o
的目标代码文件 -
linux> objdump -d mstore.o
反汇编器objdump
将mstore.o
目标代码文件反汇编成mstore.s
汇编文件 - 旁注:ATT与Intel汇编代码格式
- ATT是GCC、OBJDUMP和其他一些我们使用的工具的默认格式
3.3 数据格式
- 在64为机器中,指针长8字节!!!
3.4 访问信息
-
整数寄存器
-
对于生成小于8字节结果的指令有相对应的两条规则
- 生成1字节和2字节数字的指令会保持剩下的字节不变
- 生成4字节数字的指令会把高位4个字节置为零
3.4.1 操作数指示符
-
大多数指令有一个或多个操作数(operand)
-
x86-64支持多种操作数格式
- 立即数(immediate):用来表示常数值
- 寄存器(register):表示某个寄存器的内容
- 内存引用:它会根据计算出来的有效地址访问某个内存位置
3.4.2 数据传递指令
- x86-64加了一条限制,传送指令的两个操作数不能都指向内存位置
- 将一个值从一个内存位置复制到另一个内存位置需要两条指令
3.4.3 数据传送示例
3.4.4 压入和弹出栈数据
3.5 算术和逻辑操作
3.5.1 加载有效地址(load effective address)
leaq
:加载有效地址(load effective address)- 实际上是
movq
指令的变形 - 它的第一个操作数看上去是一个内存引用,但实际上它并没从指定的位置读入数据,而只是将有效地址写入到目的操作数
- 疑问?:那为什么当
%rdx=x
时执行leaq 7(%rdx,%rdx,4),%rax
指令后,%rax
表示7+x+4x
- 解释:假设是执行
movq 7(%rdx,%rdx,4),%rax
表示的是第一个操作数计算得到的数是7+x+4x
,然后将它作为 有效地址,到虚拟内存中读取地址为7+x+4x
对应的值,然后赋予%rax
- 解释:假设是执行
- 疑问?:那为什么当
- 实际上是
3.5.2 一元和二元操作
3.5.3 移位操作
-
移位量可以是一个立即数(immediate),或者放在单字节寄存器%c1中。(这些指令很特别, 因为只允许以这个特殊的寄存器%c1作为操作数)
-
原则上来说一个字节的偏移量范围应该是 0 ~ 255 = 2^8 - 1。但在x86-64中,移位操作对w位长的数据值进行操作, 只会对低m位有效(2^m = w)。例如:指令salb会移7位,salw会移15位,sall会移31位,salq会移63位。
3.5.5 特殊的算术操作
3.6 控制
用jump指令可以改变程序运行的顺序,指令可能依赖于某一次测试的值。编译器必须产生构建在这种低级机制的指令序列, 来控制C语言的控制结构
3.6.1 条件码
-
除了整数寄存器,CPU还维护着一组单个位的条件码(condition code)
- 最常用的条件码有
- CF:进位标志
- ZF:零标志
- SF:符号标志
- OF:溢出标志
- 两类指令:
CMP
和TEST
指令,只会设置条件码(condition code)的值,不会更新寄存器中的值
3.6.2 访问条件码
条件码(condition code)一般不会直接读取,它一般有三种使用方式
- 根据条件码的某种组合,将一个字节设为0或1
- 可以条件跳转到程序的某个其他的部分
- 可以有条件的传输数据
3.6.3 跳转(jump)指令
jmp
指令是无条件跳转- 直接跳转:即跳转目标是作为指令的一部分编码的
- 例:
jmp .L1
- 例:
- 间接跳转:从寄存器或内存位置中读出的
jmp *%rax
: 用寄存器中的值作为跳转目标jmp *(%rax)
: 根据寄存器的值作为地址,从内存中读出跳转目标
- 直接跳转:即跳转目标是作为指令的一部分编码的
3.6.4 跳转指令的编码
我们不需要理解机器代码格式的相关细节,但了解跳转指令的目标如何编码十分重要
- 跳转指令有很多种编码方式
- 最常见的是PC相对(PC-relative)寻址
- 也就是会将目标指令的地址与紧跟在跳转指令后面的那条指令的地址之间的差作为编码 !!!, 编码可以是1,2,4字节
- 第二种编码方法:给出”绝对”地址
- 用四字节直接给出目标地址
- 最常见的是PC相对(PC-relative)寻址
- 旁注:rep和repz是同义词
- 作用:在AMD给编译器编写者的指导意见书中,建议用rep后面跟ret的组合来避免是ret指令成为条件跳转指令的目标
3.6.5 用条件控制来实现条件分支(传统方式,低效)
将条件表达式翻译称机器代码,最常用的方式是有条件跳转和无条件跳转组合使用实现跳转
3.6.6 用条件传送来实现条件分支(高效但非常局限)
但不是所有条件表达式都可以用条件传送来编译
GCC只有当分支中是两条加法的时候会使用条件传送,大部分情况下都会使用条件控制,虽然预测错误的开销很大
3.6.7 循环
- 1.do-while循环
练习题3.23
A. x -> %rdi, y -> %rcx, n -> %rdx
B. 通过
leaq 1(%rcx,%rax),%rax
指令消除了对指针变量p
和表达式(*p)++
间接引用的需求
C. 第4行:
compute y = x * x
第5行:compute n = 2 * x
第7行:compute x += y ; (*p)++
第9行:compare n:n
第10行:If n>0, goto .L2
- 2.while循环
第一种翻译方法:jump to middle
第二种翻译方法:guarded-do
- 3.for循环
C语言标准说明,for循环与while循环的代码行为一样(有一个例外,使用continue时)
3.6.8 switch语句
-
多重分支(multiway switch)
-
跳转表(jump table)
-
标号(case label)
- C语言中
&
运算符创建一个指向数据值的指针 - C语言中
&&
运算符创建一个指向代码位置的指针
3.7 过程
对于不同的编程语言,过程的形式多样
- 函数(function)
- 方法(method)
- 子例程(subroutine)
- 处理函数(handler)
机器级语言,过程P调用过程Q,Q执行后返回到P,这些动作包含下面的一个或多个机制:
- 传递控制
- 传递数据
- 分配和释放内存
3.7.1 运行时栈
一种内存管理机制
- 栈帧(stack frame)
- 返回地址
- 被保存的寄存器
- 局部变量
-
参数构造区
- 通过寄存器过程P传递最多6个整数值(也就是指针或整数),但如果过程Q需要更多的参数,P可以在调用Q之前在自己的栈帧里 存储好字节参数
3.7.2 传递控制
3.7.3 数据传送
在x86-64中,过程间的数据传送大部分是通过寄存器实现的。(%rdi, %rsi和其他寄存器)
通过寄存器只可以传最多6个参数,如果一个函数有大于6个的参数,那可以通过栈传递。 前6个参数通过寄存器传递,7~n个参数放入栈中,而第7个参数会在栈顶。看运行时栈对应的部分
通过栈传递参数时,所有的数据大小都向8的倍数看齐。
3.7.4 栈上的局部变量
局部数据必须存放在内存中的情况包括:
- 寄存器不足够存放所有的本地数据。
- 对一个局部变量使用地址运算符
&
,因此必须能够为它产生一个地址。 - 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。
3.7.5 寄存器中的局部存储空间
寄存器组是唯一被所有过程共享的资源
为了避免被调用者不会覆盖调用者稍后会使用的寄存器值。x86-64采用了一组统一的寄存器使用惯例,所有的过程(包括程序库)都必须遵守
根据惯例,寄存器%rbx、%rbp和%r12~%r15被划分位被调用者保存寄存器。当过程P调用过程Q时,Q必须保存这些寄存器的值。
所有其他寄存器,除了%rsp都被称为调用者保存寄存器
3.7.6 递归调用
3.8 数组的分配和访问
3.8.1 基本原理
3.8.2 指针计算
3.8.3 嵌套的数组
3.8.4 定长数组
3.8.5 变长数组
3.9 异质的数据结构
3.9.1 结构
C语言的struct
声明创建一个数据类型
3.9.3 数据对齐
3.10 浮点代码
AVX浮点体系结构允许数据存储在16个YMM寄存器中
- YMM寄存器(媒体寄存器)
- 给个寄存器保存32个字节
- 低16字节可以作为XMM寄存器来访问
- 当对标量数据进行操作时,这些寄存器值保存浮点数,而且值使用低32位(float)或64位(double)
3.11.1 浮点传送和转换操作
Chapter 7:链接
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接可以执行于编译时(compile time),加载时(load time),运行时(run time)
分离编译(separate compilation) * 我们不用将一个大型的应用程序组织成一个巨大的源文件,而是可以将其分解成一个个更小、更好管理的模块。
学习链接知识的好处: 1. 理解链接器将帮助你构造大型程序:缺少库、缺少模块或不兼容库版本引起的链接器错误。 2. 理解链接器将帮助你避免一些危险的编程 3. 理解链接器将帮助你理解语言的作用域是如何实现的:全局变量和局部变量的差别是什么?static一个变量或函数到底意味着什么? 4. 理解链接器将帮助你理解系统的其他重要概念:加载和运行程序、虚拟内存、分页、内存映射。 5. 理解链接器你将能使用共享库
本章的链接知识的讨论基于这样一的环境:一个运行linux的x86-64系统,使用标准的ELF-64(此后简称ELF)目标文件格式(.o
)
不同的操作系统、ISA、目标文件格式。细节可能不尽相同,但概念是相通的。
7.1 编译器驱动程序
7.2 静态链接
像Linux LD这样的静态链接器(static linker)以一组可重定位的目标文件和命令行参数作为输入,生成一个完全链接的、可加载 和运行的可执行的目标文件作为输出。
为了构造可执行文件,链接器必须完成下面两个重要的任务:
- 符号解析(symbol resolution)
- 重定位(relocation)
7.3 目标文件
目标文件有三种形式:
- 可重定位目标文件
- 可执行目标文件
- 共享目标文件
从技术上来说,一个目标模块(object module)就是一个字节序列,而一个目标文件(object file)就是一个以文件形式存放 在磁盘中的目标模块
7.4 可重定位目标文件
7.5 符号和符号表
- 全局符号:由模块m定义并能被其他模块引用,对应模块m中的非静态的C函数和全局变量
- 外部符号:由其他模块定义被模块m引用的全局变量,对应其他模块中的非静态的C函数和全局变量
- 局部符号:由模块m定义,在模块m中任何位置都可见,但不能被其他模块引用,对应模块m中的带
static
属性的C函数和全局变量
链接器不关注本地非静态变量
练习题7.1 ???
7.6 符号解析
- 旁注:对C++和Java中链接器符号的重整!!!
7.6.1 链接器如何解析多重定义的全局符号
强符号:函数和已定义的全局变量 弱符号:未定义的全局变量
- 规则1:不允许有多个同名的强符号
- 规则2:如果有一个强符号和多个弱符号,那么选择强符号
- 规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个
7.6.2 与静态库链接
静态库(static library)
7.6.3 链接器如何使用静态库来解析引用
7.7 重定位
重定位由两步组成:
- 重定位节和符号定义
- 重定位节中的符号引用
7.7.1 重定位条目
ELF定义了32种不同的重定位类型,我们只关注最基本的两种
R_X86_64_PC32
:重定位一个使用32位PC相对地址的引用。R_X86_64_32
:重定位一个使用32位绝对地址的引用。
7.7.2 重定位符号引用
7.8 可执行目标文件
7.9 加载可执行目标文件
- 加载器(loader)
7.10 动态链接共享库
静态库缺陷:
- 静态库和所有软件一样需要定期维护和更新
- 几乎每个C语言程序都使用标准的I/O函数,比如
printf
和scanf
,在运行时,这些函数的代码会被复制到每个 运行进程的文本段中,在一个运行上百个进程的典型系统上,对稀缺的内存资源来说是极大的浪费。
共享库(shared library)是解决静态库缺陷的一个现代创新产物
共享库是一个目标模块,在运行和加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程叫做 动态链接(dynamic linking),是由一个动态链接器(dynamic linker)的程序来执行的。共享库也称共享目标, 在linux中一般使用.so
后缀表示,在windows中成为DLL
(动态链接库)
动态链接器通过执行下面的重定位完成链接任务
- 重定位libc.so的文本和数据到某个内存段
- 重定位libvector.so的文本和数据到另一个内存段
- 重定位prog21中所有对由libc.so和libvector.so定义的符号的引用 最后,动态链接器将控制传递给应用程序。
7.11 从应用程序中加载和链接共享库
7.12 位置无关代码
多个进程如何共享程序的一个副本?
可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code,PIC)
- PIC数据引用
- 全局偏移量表(Global Offset Table,GOF)
- PIC函数调用
- 延迟绑定(lazy binding),将过程地址绑定推迟到第一次调用该过程时
- 延迟绑定是通过两个数据结构之间简洁但又有些复杂的交互来实现的
- GOT:数据段的一部分
- 过程链接表(Procedure Linkage Table,PLT):代码段的一部分
![用PLT和GOF调用外部函数][P527]
7.13 库打桩机制(library interpositioning)
Chapter 8:异常控制流
- 控制转移(control transfer)
- 控制流(control flow)
- 异常控制流(Exceptional Control Flow,ECF)
- 异常控制流会发生在计算机系统的各个层次。
- 在硬件层,硬件检测到的事件会触发控制突然转移到异常处理流。
- 在操作系统层,内核通过上下文切换将控制一个用户进程转移到另一个用户进程。
- 在应用层,一个进程可以发送信号到另一个进程,而接受者会将控制突然转移到它的一个信号处理程序。
- 异常控制流会发生在计算机系统的各个层次。
作为程序员理解ECF(Exceptional Control Flow)非常重要,原因如下:
- 理解ECF将帮助你理解重要的系统概念
- 理解ECF将帮助你理解应用程序是如何与操作系统交互的
- 理解ECF将帮助你编写有趣的新应用程序
- 理解ECF将帮助你理解并发
- 理解ECF将帮助你理解软件异常是如何工作。软件异常允许程序进行非本地跳转来响应错误情况
8.1 异常
异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。
状态变化称为事件(event)
8.1.1 异常处理
异常表的起始地址放在一个叫做异常表基址寄存器(exception table base register)的特殊CPU寄存器里
8.1.2 异常的类别
异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)。
除了interrupt其他属于faulting instruction(故障指令)
1.中断(interrupt)
interrupt是异步的,是来自处理器外部的I/O设备的信号的结果。硬件终端不是由任何一条专门的指令造成的,从这个意义上 来书interrupt是异步的。硬件interrupt的异常处理程序常常成为中断处理程序(interrupt handler)
2.陷阱(trap)和系统调用(system call)
从程序员的角度看,系统调用和普通函数调用是一样的。但并不完全相同,普通函数运行在用户模式中,系统调用 运行在内核模式中
3.故障(fault)
故障是由错误引起的。
一个典型的fault示例是缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中 取出时,就会发生fault。
4.终止(abort)
8.1.3 Linux/x86-64系统中的异常
1.Linux/x86-64故障和终止
- 除法错误
- 一般保护故障
- 缺页
- 机器检查
2.Linux/x86-64系统调用
8.2 进程
异常是允许操作系统内核提供进程(process)概念的基本构造块。process是计算机科学中最深刻、最成功的概念之一。
进程的经典定义就是一个执行中程序的实例。系统的每个程序都运行在某个进程的上下文(context)中。
我们关注进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占地使用处理器
- 一个私有的地址空间,它提供一个假象,好想我们的程序独占地使用内存系统
8.2.1 逻辑控制流
抢占(preempted)(暂时挂起)
8.2.2 并发流
一个逻辑流的执行在时间上与另一个流重叠,称为concurrent flow(并发流),这两个流称为并发地运行。
- 并发(concurrent)
- 多任务(multitasking):一个进程和其他进程轮流运行的概念
- 时间片(time slice):一个进程执行它的控制流的一部分的每一时间段
注意:并发流的思想与流运行的处理器核数或者计算机数无关
- parallel flow(并行流):是并发流的一个真子集。两个流并发地运行在不同的处理器核或者计算机上。
8.2.3 私有地址空间
8.2.4 用户模式和内核模式
模式位(mode bit)
当设置了模式位以后,进程运行在内核模式中(有时叫超级用户模式)
没有设置模式位,进程就运行在用户模式中,用户模式中的进程不允许执行特权指令(privileged instruction)
8.2.5 上下文切换
context switch
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程
scheduling(调度):在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。scheduling是 由内核中称为调度器(scheduler)的代码处理的。
中断也可能引发上下文切换。比如,所有的系统都有某种产生周期性定时器中断的机制,通常为每1毫秒或每10毫秒。 每次发生定时器中断时,内核就能判定当前进程已经运行了足够长的时间,并切换到一个新的进程。
8.3 系统调用错误处理
8.4 进程控制
进程的三种状态
- 运行
- 停止(suspended)
- 终止
9.6.3 多级页表
- 一个32位地址空间、4KB页面和一个4字节的PTE
如果你在中国大陆地区,则需要连接外网才能进行评论
comments powered by Disqus