概述
这本书在CMU作为计算机系统的入门书籍,每章还对应有课后实验,花了1个多月看完这本书,并且挑选了4个感兴趣的实验完成,收获还是不错的。看完这本书最大的感受就是对整个计算机系统有了一个整体的认识,一个简单的helloworld的后面发生了什么?认真读完这本书,相信你心中一定会有一个答案了。
======================================================================================================
以下是我在读书时,记录的一些重要的笔记:
CSAPP
ch1、ch2计算机系统漫游、信息的表示与处理
1. 编译系统:
a) 预处理:.c文件预
b) 处理成为.i
c) 编译:将.i生成汇编程序.s
d) 汇编:将汇编语言.s转换成机器语言指令生成.o
e) 链接生成可执行程序
2. 高速缓存存放处理器近期可能用到的信息,上层存储器作下一层的高速缓存。
3. 进程是操作系统对一个正在运行的程序的一种抽象,通过上下文切换机制实现。线程共享同样代码和全局数据,更易共享数据,比进程更高效。
4. 进程的虚拟地址空间从低地址到高地址依次为:程序代码和数据,运行时堆,共享库的存储器映射区域,用户栈,内核虚拟存储器:
5. 文件是对I/O的抽象,虚拟存储器是对程序存储器的抽象,进程是对正在运行程序的抽象,虚拟机是对整个计算机的抽象。
6. 字长w决定虚拟地址空间的最大大小,其范围:0~2^w-1
7. 大端法:起始端为高位 小端法:起始端为低位
8. 利用sizeof(类型)提高可移植性。文本数据比二进制数据平台独立性更强。
9. 位运算:
a) 异或:利用异或交换两个数值,异或本身为0,任何一个数异或0还是本身,异或1相当于取反。
b) 移位:逻辑右移(无符号)补0,算数(有符号)右移补符号位;左移都补0
10. 原码、反码、补码、无符号数的二进制表示转数值表示。
11. 有符号数与无符号数的转变其数值可能改变,位模式都不变。
12. 有符号x到无符号数(U结尾)映射:非负数不变为x,负数转换成大的正数(x+2^w,最靠近0的负数映射最大,最小的负数被映射为刚好在补码表示之外的无符号数)。比如w=4,补码能表示的有符号数范围是-8[1000]~7[0111]。
13. 算数运算中,其中1个为有符号,会隐式的将有符号转为无符号运算。如-1<0U为假;-2147483647-1 (转换为无符号相当于加2^32)== 2147483648U为真。这种隐式的转换带来的错误有时很难发现,避免的方法就是不使用无符号数。
14. 无符号数扩展(移位):零扩展;
补码扩展:符号扩展。
15. 假设要求数x十进制表示的补码非-x:
a) 求位级补码非,对每一位求补,再多结果加1:~x+1
b) 还要注意,当x为下界时,取非就是本身
取反运算!结果不是0就是1;而逻辑非运算与操作数有关,按位取补。
16. 在数值的运算中,要考虑边界输入TMAX,TMIN带来的溢出问题。
a) 补码相加判断是否溢出的C程序P58。思想是判断a,b,sum运算后的符号。
b) 补码相乘是否溢出的C程序判断。P62。思想是判断 !x|| p/x == y
17. 除法中除以2的幂可通过逻辑与算数右移实现。算数右移k位会自动加上一个偏置量2^k -1加以修正。现在处理器相对来说,除法相对于加法乘法所消耗指令周期更多。
18. IEEE浮点标准:V=(-1)^s * M *2^E s符号,M尾数,E阶码
浮点数由符号s,阶码k=e,尾数n=f组成,单精度对应1,8,23位,双精度1,11,52位。
对非规范化数,阶码全为0:E=1-(2^k-1 -1) M=f
规范化数,阶码有1有0:E=e-(2^k-1 -1) M=f+1
无穷大,阶码全1,尾数全0
NaN,阶码全1,尾数不全为0。
19. 浮点数二进制表示转成十进制表示P72:
a) 求e(阶码部分转10进制),f(尾数表示的小数),Bias=2^(k-1)-1
b) 非规范数:E=1-Bias,M=f 规范化数:E=e-Bias,M=f+1
c) V=2^E*M
20. 将整数值转换为浮点形式的步骤P74:
a) 得到小数字段:改成二进制,移动小数点,将小数点前的1舍去,不足后面补0
b) 得到阶码字段:求e=E+bias,改为二进制表示即为阶码
c) 合并加上符号位。浮点表示的小数部分的高位和整数低位最高有效位1之前匹配。
21. 向偶数舍入:当上下界不等时向近的舍入,相等就向偶数舍入。对二进制而言舍入的后面为100……才考虑向偶数舍入。
22. 浮点运算要注意的地方:
a) 浮点加法不具有结合性,如很小的小数加上浮点数-浮点数为0,因为很小的小数被舍去了。
b) int转float,不会溢出,但可能会舍入;转double保持精度。
c) double转float,可能溢出为无穷,还可能被舍入。
d) float,double转int,可能溢出,返回整数不确定值。
ch3程序的机器级表示
1. 学习汇编:理解编译器的优化能力,并分析代码中隐含的低效率。
2. linux采用了平坦寻址方式,使程序员将整个存储空间看做一个很大的字节数组。
3. 机器级编程二种重要的抽象:
a) 指令集体系结构,定义了处理器状态、指令的格式,及每条指令对状态的影响。
b) 机器级程序使用的存储器地址是虚拟地址。操作系统负责管理虚拟地址空间,并将虚拟地址翻译成实际处理器存储器的物理地址。
4. gcc-S生成汇编代码 objdump -d反汇编程序
生成可执行文件后,文件变大了,因为包括了启动、终止程序信息,以及操作系统交互信息。
5.
可以看出,64位系统与32位系统的字长只有long int,char* (指针)变为8字节,其他均不变。
6. IA32包含8个存储32位值的寄存器。其中:
a) %eax、%edx、%ecx称为调用者保存寄存器,过程P调用Q,Q可以覆盖他们。
b) %ebx、%esi、%edi称为被调用者寄存器,Q须在覆盖前保存,并在返回前恢复。
c) 栈指针%esp始终保存着栈顶元素的地址。压栈减小栈指针,栈向下增长。
帧指针%ebp,大多数信息的访问都是相对于帧指针的。
7. MOV类中指令将源操作数复制到目的操作数:源操作数指定一个立即数,存储在寄存器或存储器中。目的操作数指定一个位置,寄存器或存储器地址。
a)有效地址:立即数偏移Imm+基址寄存器R[Eb]+变址寄存器R[Ei]*比例因子s(1、2、4)
b)限制是两个操作数不能同时指向存储器位置。
c)MOVS和MOVZ将较小的源数据复制到较大的数据位置,高位符号扩展或零扩展。
8. 间接引用指针就是将该指针放在一个寄存器中,然后在存储器引用中使用这个寄存器。
9. 加载有效地址(leal)指令通常用来执行简单的算数操作。
10. 存在特殊的算数操作指令,支持两个32位数字的全64位乘积以及整数除法。一对%edx(高位)和%eax(低位)组成一个64位的四字。
11. 条件码:CF,ZF,SF,OF;通常用setxx D,根据条件码组合设置D。
a) CMP S2,S1 相当于 S1-S2 b) TEST S2,S1 相当于S1&S2
12. 间接跳转jmp *%eax 用寄存器%eax的值作为跳转目标
jmp *(%eax) 以%eax的值作为读地址,从存储器中读出跳转目标
直接跳转给出一个标号做目的地址
13. 用一个32位int表示n!,最大的n是多少。
a) 计算看数字什么时候停止增大。
b) n!/n== (n-1)!判断
14. cmvl:条件传送指令,只有在前一条指令满足才进行本条。
a) 条件跳转:13-57个周期,条件传送:14个周期。
b) 分支传送及时条件不满足,还是会引用后面的空指针(如果有)。
15. 栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息(保存的返回位置、栈指针和被调用者保存寄存器的内容)。因此每次递归其实就是一个栈深度不断增加的过程。
16. 访问数组元素:movl (%edx,%ecx,4),%eax 通过首地址+偏移量得到实际地址访问
T D[R][C];它的数组元素D[i][j]的存储器地址为:xd+L(C*i+j)
17. 读存储器比写存储器容易得多,因此将只读变量溢出到存储器是合适的。(寄存器数量有限,因此才会溢出到存储器)
18. 当所用元素互斥可用联合,且联合的所有元素引用的都是数据结构的起始位置(相同),一个联合总大小等于它最大字段的大小。枚举占一个int大小。
19. 数据对齐的优点:
a) 对齐限制简化了形成处理器和存储器系统之间的硬件设计。
b) 严格对齐后,可用一个存储器用作读或写。
c) 对齐数据以提高系统的性能。
回忆对齐的步骤。
20. 指针的强制转换,只是改变在内存中的解读方式。
21. 对抗缓冲区溢出攻击的几种方式:
a) 栈随机化。
b) 栈破坏检测:利用一个金丝雀值。
c) 限制可执行代码区域:只保存编译器产生的代码的那部分存储器是可执行的,其他部分可以被限制为只读、只写。
22. x86用64位寻址,指针和长整数是64位长。通用寄存器增加到16个。条件传送指令。
ch4 处理器体系结构
1. 程序员可见的状态:
CC为条件码, PC为程序计数器存放正在执行的指令地址。Stat存放状态码,表明程序执行总体状态,正常或者异常。
2. PIPE处理一条指令包括的操作:
a) 预测PC和取指:将PC内容作地址,从指令存储器读指令,预测下一跳PC地址。
b) 译码:读两个操作数的寄存器值。
c) 执行:根据指令类型,将算数/逻辑单元ALU用于不同操作,包括设置条件码。
d) 访存:数据存储器写入或读出一个存储器字。
e) 写回:写从ALU计算出来的值或者从存储器读出的值。
f) 更新PC
3. 处理器从来不需要为了完成一条指令的执行而去读该指令更新了的状态。
4. 流水线化:每个时钟周期,一条指令离开系统,一条新的进入,特点:
a) 流水线化增加了系统的吞吐量,但会轻微的增加延迟(增加了流水线寄存器)。
b) 不一致的阶段延迟造成的流水线技术的局限性:系统吞吐量受最慢阶段速度限制。
c) 流水线过深,收益反而下降,流水线寄存器成为吞吐量的一个制约因素。
5. 分支预测:猜测分支方向并根据猜测开始取值的技术。
栈的返回地址预测:利用过程调用和返回总是成对出现的。通常有一个硬件栈(对程序员不可见),保存过程调用指令产生的返回地址。
6. 流水线冒险:
a) 数据冒险:下一个指令会用到这一条计算出的结果。
b) 控制冒险:一条指令要确定下一条指令的地址,如JUMP.CALL等
c) 避免冒险的几种方式:
i. 暂停,停止一条流水线中一条或多条指令,知道不满足冒险条件。
ii. 转发,直接将结果值传到较早的阶段。
iii. 加载/使用冒险,当存储器读在流水线发生的比较晚,不能用单纯的转发解决,而是将暂停与转发结合起来。
7. 我们的指令集体系结构包括三种不同的异常:halt,非法指令和功能码结合,试图访问非法地址。
8. 我们要单独处理的控制情况(通过在不同阶段插入不同数量气泡实现):
a) ret。必须暂停到ret指令到达写回阶段。
b) 加载/使用冒险。
c) 预测错误的分支:必须要去掉预测错误时进入的几条指令。
d) 异常:发生时,要禁止后面的指令更新程序员可见的状态,并在写回时停止。
9. 流水线寄存器有2个输入:暂停和气泡。实现流水线控制逻辑:暂停,气泡。
ch5 优化程序性能
1. gcc大多数情况选择优化等级-O2。至少使用-O1会很大程度的优化程序。
2. 使用内联函数减少了函数调用的开销。
3. 我们使用度量标准每元素的周期数CPE作为表示程序性能的方法。
4. 现代处理器结构:
Fetch Control包括分支预测。任何对程序状态的更新都只在指令退役时发生。Operatingresult的作用是为加速指令传送,使信息在执行单元间传送
5. 延迟:完成运算需要的总时间
发射时间:连续两个同类型运算之间需要的最小时钟周期数。吞吐量为发射时间倒数。
关键路径:在循环的反复执行过程中形成的数据相关链。指明了执行该程序所需时间的一个下界。
6. 消除循环的低效率:代码移动(比如移除循环),这类优化识别要执行多次但是计算结果不会改变的值。
7. 减少过程调用:例子中没有通过函数调用获取每个向量元素,而是直接访问数组。
8. 消除不必要的存储器引用:使用一个临时变量,减少了每次读存储的开销。
9. 循环展开:增加每次迭代计算的元素的数量,减少循环的迭代次数。
10. 提高并行性,即循环展开+两路并行。
11. 重新结合变换P357例子:减少了关键路径上的操作数。
12. 限制性能的因素:
a) 寄存器溢出。
b) 分支预测错误:书写适合条件传送的代码。
13. 加载操作不会成为限制性能的关键路径的一部分。但是当加载操作的结果决定下一条操作的地址时,会产生延迟。
14. 一个存储器读的结果依赖于一个最近的存储器写,称为读/写相关,也会导致速度下降。
15. 提高技术性能最重要的还是选择适当的算法和数据结构。
16. gcc-O1 -pg prog.c prog然后再执行程序,会产生一个gmon.out文件
然后可以调用GPROF来分析其中的数据:gprof prog。
会产生一个剖析报告:包括执行各个函数所需要的时间,函数的调用历史等信息。
17. Amdahl定律:
当我们加快系统一个部分的速度时,对系统整体性能的影响依赖于这个部分有多重要和速度提高了多少。
即使我们大幅度改进了系统的一个主要部分,获得的净加速比还是很小。因此要想大幅度提高整个系统的速度,我们必须提高整个系统很大一部分的速度。
ch6 存储器层次结构
1. 访问速度,高速缓存1-30周期>主存50-200个周期>磁盘几千万个周期。
2. 随机访问存储器RAM分为SRAM(双稳态),DRAM(需要刷新),均断电易失。
3. DRAM结构:通过超单元(i,j)来访问。设计为二位阵列相比于一维阵列的好处是降低了芯片地址引脚数目。DRAM响应的都是一整行。一个模块通常含有多个DRAM,比如8个,假设有一个64位双字,则每个芯片对应超单元储存一位。
4. 非易失性存储器,通常都是只读存储器ROM。可擦写可编程ROM(EPROM),1000次;闪存基于电子可擦除PROM(EEPROM),10^5次。存储在ROM的数据通常称为固件。
5.
注意系统总线和存储器总线通过I/O桥连接。
a) 加载操作movl A, %eax读事务
i. CPU放地址A到存储器总线
ii. 主存从总线读出A,取出其中字x放到总线
iii. CPU读出字x,拷贝到寄存器
b) 存储操作movl %eax, A写事务
i. CPU放地址A到总线,主存读地址,等待数据字
ii. CPU放数据字y到总线
iii. 主存读出y,存储到地址A
6. 磁盘构造:
a) 间隙存储用来标识扇区的格式化位。
b) 磁盘用读/写头读写存储在磁性表面的位,读写头连接到一个传动臂,所有头垂直排列一致行动。
7. 完整的对扇区的访问时间包括:
a) 寻道时间Tseek
b) 旋转时间Tmax rotation,平均旋转时间是其的一半
c) 传送时间
访问一个字节主要的时间是寻道时间和旋转延迟,而这两者大小差不多,故寻道时间乘以2作为磁盘访问时间的估计。
磁盘控制器:负责翻译逻辑块号->三元组(盘面,磁道,扇区)
清理磁盘碎片使得寻道时间和旋转时间大大减小。使数据存储在磁盘连续区域。
8. 连接到I/O设备:USB,显卡等等,PCI的I/O总线被设计成与CPU无关,访问磁盘的步骤:
a) CPU发起一个磁盘读,包括命令字和逻辑块号
b) 磁盘控制器读扇区,执行到主存的DMA传送
c) DMA传送完成,磁盘控制器用中断方式通知CPU
9. 固态硬盘的结构:
数据以页为单位读写
10. 局部性原理:一个编写良好的程序倾向于引用邻近于其最近引用过的数据项的数据项,或其本身(优化范围几种在内循环上):
a) 时间局部性,重复引用同一个变量。
b) 空间局部性,一个存储器被引用,在将来其邻近的位置很可能被引用。内循环的步长越小(1最好),空间局部性越好。
11. 存储器层次:寄存器->SRAM L1->SRAM L2->DRAM L3->磁盘
每一层缓存读来自于缓存较低一层的数据的对象。层次越下面,块越大。
12. 编译器管理寄存器文件,硬件逻辑管理L1,L2,L3层缓存,操作系统软件和CPU上的地址翻译硬件管理DRAM。
13. 高速缓存存储器SRAM的结构:
组、行、块标记,
有效位,块偏移
高速缓存请求字的过程:组选择,行匹配,字抽取(有效位和标记匹配)。
14. 抖动:高速缓存反复地加载和驱逐相同的高速缓存块的组。
15. 高速缓存用中间的位作为组索引,而不是高位:若用高位,几个相邻的数组元素都映射到同一个高速缓存行;而用中间位,相邻的块总是映射到不同的高速缓存行。
16. 高速缓存行替换策略包括:LRU最近最少访问(时间),LFU最近最少引用(空间)。
17. 高速缓存参数的性能影响:
a) 高速缓存越大,命中率越高。
b) 块越大,意味着行越少,会损害时间局部性。
c) 相连度E(行数)越高,降低了抖动的可能,但会增加不命中处罚带来的开销。
d) 写策略,写回(替换算法驱逐更新过的块,才写回低一层)高速缓存引起的传送较少,越下层,越可能写回。
18. 缓存的命中率计算。P426。主要是根据块大小和高速缓存大小确定。
19. 重新排列循环提高空间局部性的例子P432。
ch7 链接
1. 编译的过程:
cpp为C预处理器生成.i,cc1为C编译器生成.s,as为汇编器生成.o,最后调用链接器ld生成可执行目标文件(ELF)。
2. 目标文件包括:可重定位目标文件,可执行目标文件,共享目标文件。
3. 注意.text .data .bss .symtab .debug .line
4. 符号表不包含局部变量信息。带C static属性的变量不在栈管理,编译器在.data .bss为每个定义分配空间,并在符号表创建唯一符号(即使不同函数中名字重复)。
5. 链接器的主要任务是:符号解析和重定位。
6. 符号解析:将每个引用与.o文件中符号表的一个确定符号定义联系起来。
a) C++和java中重载的函数名字对链接器来说唯一。
b) 链接器处理多重定义的符号(P456注意多重定义覆盖的一个问题):
i. 只允许一个强定义。
ii. 若有一个强定义和多个弱符号,则选择强定义。
iii. 若有多个弱符号,则任选一个。
c) GCC-fno-common遇到多重定义警告。
7. 重定位:
a) 重定位节和符号定义:将输入模块所有相同类型的节合并为同一类型的聚合节。之后链接器将运行时存储器地址赋给新的聚合节。
b) 重定位节中的符号引用:只要不知道这个模块引用的任何变量或函数的位置,通过一个重定位条目修改代码节和数据节的每个数据引用,指向正确运行时地址。包括重定位PC相对引用和重定位绝对引用。
8. 可执行目标文件通过调用加载器运行。步骤如下:
a) 加载器创建存储器映像,将可执行文件相关内容拷贝到代码和数据段。
b) 加载器跳转到程序入口点,即_start的地址(ctrl.o定义的启动代码)。汇编代码包含一个call main 和call _exit解释了为什么需要main函数和主函数是如何返回的。
9. 静态链接库:若每个可执行文件包含标准函数集合的完全拷贝,对磁盘空间极大浪费。故采用静态链接库只拷贝静态库里被应用程序引用的目标模块。注意:
a) 关于库的一般准则将它们放在命令行的末尾(考虑不这样做的后果)
b) 为了满足库的依赖需求,必须满足一定顺序:命令行先出现的依赖后面出现的库,且可以重复库。
10. 动态链接库:为解决每个C程序重复复制一些函数代码到每个程序运行的文本段,共享库运行时重定位到某个存储器段,并且重定位某个程序对加载的动态库的符号引用。
a) 创建一个与位置无关动态库:
unix> gcc -shared -fPIC -o libvector.so addvec.c multvec.c
b) 运行时动态加载和链接应用:分发软件,高性能Web服务器(当某些内容到达,服务器动态的加载某些函数):
#include<dlfcn.h>
void*dlopen(const char *filename, int flag); //打开一个动态共享库
void *dlsym(void*handle, char *symbol); //返回一个符号地址
int dlclose(void *handle); //没有其他人使用,卸载这个共享库
const char*dlerror(void); //对前面函数调用失败,就返回最近的错误
11. 如何使多个进程共享程序的一个拷贝:采用与位置无关的代码PIC,通过一个全局偏移量表GOT,每个被目标模块引用的全局数据对象对应一个重定位记录条目。通过一些代码就可以引用PIC数据和函数调用。
12. 延迟绑定:第一次需要过程链接表PLT压入相关信息后,动态链接器覆盖GOT相关值进入相关函数调用。
ch8 异常控制流
1. 异常:控制流的突变,响应处理器的某些变化。通过一张由处理器设计者和操作系统定义的异常表跳转到异常处理程序。IA32异常表包括256种异常,通过基址寄存器和异常号*4得到条目地址。
2. 异常种类:中断,陷阱,故障,终止
a) 陷阱是执行一条指令的结果,最重要的用途就是提供系统调用。
b) 故障示例:缺页异常。发生该异常,缺页处理程序处理后,重新执行产生故障的指令。
c) C程序用syscall执行系统调用(汇编代码int $0x80),也可以直接使用C标准库的包装函数。
3. 进程:一个执行中程序的实例。每个程序运行在某个进程的上下文(程序正确运行所需要的状态,包括用户栈,内核栈,PC,PATH,内核数据结构等)中,进程提供了2个重要的抽象:逻辑控制流和私有的地址空间。
4. 并发流:一个逻辑流的执行在时间上与另一个重叠。并行表示并发地运行在不同处理器。
5. 处理器通过某个控制寄存器的模式位提供用户模式和内核模式的切换。
6. pid_twaitpid(pid_t pid, int *status, int options);
a) pid大于0,等待集合为单独子进程。pid小于0,等待集合为父进程所有子进程。
b) 最后一个参数设置为0,表明阻塞到有子进程终止。
c) 成功返回子进程PID,若为WNOHANG | WUNTRACED非阻塞,若等待集合中没有任何子进程被停止或终止,立即返回0,或返回被停止或已终止子进程PID
7. unsignedint sleep(unsigned int secs); //请求时间到返回0,若因信号中断过 早返回,返回剩余的秒数。
8. intpause(void); //总是返回-1,让进程休眠,直到遇到一个信号。
9. intexecve(const char *filename, const char *argv[],
const char *envp[]); //在当前进程上下文中加载运行一个新程序。
10. kill-9 -15213 为负的PID导致信号被发到进程组PID每个进程。
11. ls| sort创建一个两个进程组成的前台作业(同属一个进程组),通过管道连接起来。
12. intkill(pid_t pid, int sig); //发送信号sig给进程pid,pid<0发送给abs(pid)中每个进程。
13. unsignedint alarm(unsigned int secs); //对alarm的调用将取消任何待处理闹钟,并返回剩余的秒数
14. typedefvoid (*sighandler_t)(int);
sighandler_tsignal(int signum, sighandler_t handler);
a) Returns:ptr to previous handler if OK, SIG_ERR on error (does not set errno)
b) handler可以设置为忽略signum的信号SIG_IGN,或者默认行为SIG_DFL,或者为自定义行为。
c) 同类型的一个待处理信号被阻塞,直到前一个处理程序返回。不会排队等待,只是简单的丢弃。且慢速系统调用可被中断,有时不会主动重启而返回EINTR错误。
15. 可以通过诸如intsigprocmask(int how, const sigset_t *set, sigset_t *oldset);的函数改变已阻塞信号的集合,可以用作同步解决竞争问题。
16. 暴露代码竞争的方法:用随机的方法确定子进程或者父进程任一个休眠一会儿。
17. int setjmp(jmp_buf env); int sigsetjmp(sigjmp_buf env, intsavesigs);
非本地跳转其中一个应用就是深层嵌套中发现错误,返回到错误处理处。
18. void longjmp(jmp_buf env,int retval); voidsiglongjmp(sigjmp_buf env, int retval); 这2个函数的版本是可以被信号处理函数使用的版本,使得信号处理程序分支能到达特殊的位置。
ch9 虚拟存储器
1. 虚拟存储器是对主存DRAM的抽象,提供了三个重要的功能:
a) 在主存中自动缓存最近使用过的虚拟地址空间对应磁盘的内容。
b) 简化了存储器管理,每个进程都有独立的页面,即独立的虚拟地址空间。
i. 简化了链接。如文本节总是从虚拟地址0x0848000(32位)开始等一致性。
ii. 简化加载。加载器行为一致。
iii. 简化共享。可将不同虚拟页面映射到相同物理页面。
iv. 简化存储器分配。分配连续虚拟内存地址,对应的物理地址可以不连续。
c) 通过在页表加保护位,简化了存储器保护。
2. VM系统分割虚拟存储器为许多虚拟页,对应大小一致的DRAM物理页。
3. DRAM中存在的页表将虚拟页映射到物理页DRAM:包括未分配,未缓存和已缓存。
4. DRAM缓存不命中称为缺页:
a) 判断异常,虚拟地址A合法么?(段错误)文本是否受保护?(保护异常)
b) 调用缺页异常处理程序,通过页面调度算法(最常用的按需页面调度)选择一个牺牲页,若牺牲页已经被修改就写回磁盘,从磁盘拷贝响应内容到牺牲页。重启导致缺页的指令。
5. malloc的影响就是现在磁盘创建一个空间,再增加一个页表项指向这个物理地址。
6. 程序往往在一个较小的工作集工作,若工作集大小超出物理内存大小,产生颠簸。
7. 地址翻译过程:
8. 使MMU在L1之前,我们的地址PTE条目可以缓存在L1高速缓存中。且在MMU中包含一个关于PTE的小的缓冲,称为翻译后备缓冲器TLB。利用他来加速地址翻译。
9. 通常采用多级页表来压缩页表。好处是:
a) 大大节约了虚拟地址。
b) 只有一级页面总是在主存中,按需创建,调入,调出二级页表,减少了主存压力。
c) 带多级页表的地址翻译不比单击页表慢很多,TLB在其中也有作用。
10. 一个linux进程的虚拟存储器:
11. 存储器映射表示虚拟存储器到磁盘上文件片的映射。可以映射为普通文件和请求二进制零的页。
12. 私有对象采用一种写时拷贝的技术。私有对象开始声明周期的方式与共享对象的一样。
13. void*mmap(void *start, size_t length, int prot, int flags,
int fd, off_toffset);
Returns: pointer to mapped area if OK,MAP_FAILED (−1) on error
14. intmunmap(void *start, size_t length);
Returns: 0 ifOK, −1 onerror
15. calloc将分配的内存初始化为0,realloc改变一个以前分配块的大小。
16. 分配器要求能够对齐块。内部碎片是已分配块大小和他们有效载荷大小之差。
a) 块分配算法:首次适配,下一次适配,最佳适配。
17. 隐式空闲链表,分配器的设计:重点P570-P575。
18. 双向空闲链表结构,按照地址顺序维护链表,比LIFO存储器利用率更高。
19. 分离的空闲链表:伙伴系统原理。优点在于快速搜索和快速合并。
20. 垃圾收集器Mark&Sweep算法维护一张有向可达图(当存在一条从任意根节出发并到达p的有向路径时表可达),通过定期释放不可达结点返回给空闲链表。
因C语言不会用类型信息标记存储器位置,故可能会不正确的标记。
21. *size--两符号优先级相同,右到左结合。不要返回局部变量。
ch10 系统级I/O
1. size_t表unsigned int
ssize_t表int
2. intopen(char *filename, int flags, mode_t mode);
Returns: new file descriptorif OK, −1on error
int close(intfd);
Returns: zero if OK, −1 on error
3. ssize_tread(int fd, void *buf, size_t n);
Returns: number of bytesread if OK, 0 on EOF, −1 on error
ssize_twrite(int fd, const void *buf, size_t n);
Returns: number of byteswritten if OK, −1 on error
以上两个函数有时会返回一个不足值,如EOF,从终端读文本行,读写socket。
4. 常常采用自己封装的带缓冲区的输入输出函数。C标准I/O库将一个打开的文件模型化为一个流,使得开销较高的UnixI/O系统调用的数量尽可能小。主要完成:
a) 确保指定的n个数据全部接收发送完成。
b) 将接收到的数据存入自己定义的一个缓冲区
5. 获取文件状态 int stat(const char*filename, struct stat *buf);
int fstat(int fd, struct stat *buf);
Returns: 0 if OK, −1 on error
6. 子进程如何继承父进程的打开文件:
描述符不同,打开文件表、v-node表就不同。
父子进程共享同一个描述符,故后面两个表都相同。
7. 将newfd重定向到oldfd:
intdup2(int oldfd, int newfd);
Returns:nonnegative descriptor if OK, −1 on error
8. 对网络套接字,不要用标准I/O进行输入输出。使用sprintf格式化字符,再发送。
使用sscanf对文本提取不同的字段。
ch12 并发编程
1. 进程或线程同步互斥的控制方法:
a) 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。仅线程可以使用。
b) 互斥量:为协调共同对一个共享资源的单独访问而设计的。 进程线程都可使用。
c) 事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。可用于不同进程间线程的同步。
d) 信号量:为控制一个具有有限数量用户资源而设计。 进程线程都可使用(linux仅线程间)
e) 互斥锁:线程间保护一段资源。
2. 信号量和互斥锁的实现原理:通过Futex(快速用户空间互斥体)系统调用实现,Futex 由一块能够被多个进程共享的内存空间(一个对齐后的整型变量)组成;这个整型变量的值能够通过汇编语言调用CPU提供的原子操作指令来增加或减少,并且一 个进程可以等待直到那个值变成正数。Futex的操作几乎全部在应用程序空间完成;只有当操作结果不一致从而需要仲裁时,才需要进入操作系统内核空间执行。这种机制允许使用 futex 的锁定原语有非常高的执行效率:由于绝大多数的操作并不需要在多个进程之间进行仲裁,所以绝大多数操作都可以在应用程序空间执行,而不需要使用(相对高代价的)内核系统调用。
最后
以上就是优秀太阳为你收集整理的【CSAPP】《深入理解计算机系统》读书笔记的全部内容,希望文章能够帮你解决【CSAPP】《深入理解计算机系统》读书笔记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复