ejunjsh / csapp

my csapp homework

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

csapp(e3)

my csapp(e3) homework

exercise reference to https://github.com/DreamAndDead/CSAPP-3e-Solutions

summary

计算机系统漫游

这一章里面基本是对整本书的一个概括吧。

  • gcc 翻译源码的的四个阶段:预处理,编译,汇编,链接
  • 系统硬件组成:总线,I/O设备,主存,处理器
  • 存储设备的层次结构:L0(寄存器),L1,L2,L3(SRAM),L4(主存DRAM),L5(磁盘),L6(...)
  • 并发运行则是说一个进程的指令和另一个进程的指令交错执行的
  • 无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。
  • 操作系统实现这种交错执行的机制称为上下文切换
  • 虚拟内存是一个抽象概念,它为每个进程提供一个假象,即每个进程都是独占地使用主存。每个进程看到的内存都是一致的,称为虚拟内存空间
  • 每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩
  • 一个进程实际上可以由多个称为线程的执行单位组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。
  • 超线程是一项允许一个cpu执行多个控制流的技术
  • 利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。

信息的表示和处理

  • 大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,成为虚拟内存。内存的每个字节都由一个唯一的数字来表识,称为它的地址,所有可能的地址的集合就称为虚拟内存空间
  • c的指针包含值和类型,它的值表示某个对象的位置,它的类型表示那个位置上所存储对象的类型
  • 对于一个字长为w位的机器,虚拟地址范围为0~2^w-1,程序最多访问2^w个字节
  • 最低有效字节在最前面的方式,称为小端法,最高有效字节在最前面的方式,称为大端法。
  • 最常见的有符号数的计算机表示方式就是补码
  • 几乎所有的编译器/机器组合都对有符号数使用算数右移
  • 对于大多数c语言的实现,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,但是位模式不变
  • 两个数的w位补码之和与无符号之和有完全相同的位级表示,乘法也一样

程序的机器级表示

这篇是汇编相关,精华👿

  • 汇编代码是机器代码的文本表示
  • 目标代码是机器代码的一种形式,它包含所有指令的二进制表示,但是还没有填入全局值的地址。
  • x86-64的虚拟地址是由64位的字来表示的,目前的实现中,高16位都必须设置为0,所以一个地址实际上能够指定的是2^48或者64TB范围内的一个字节。
  • 传送指令的两个操作数不能都指向内存位置
  • 由于是从16位体系结构扩展成32位的,Intel用术语“字(word)”表示16位数据类型。因此,32位数为“双字(double words)”,64位数为“四字(quad words)”
  • 函数通过把值存储在寄存器%rax或寄存器的某个低位部分中返回
  • 栈向下增长,栈顶元素的地址是所有栈中元素地址中最低的,栈指针%rsp保存着栈顶元素的地址。
  • 跳转指令有几种不同的编码,但是最常用都是PC相对的(PC-relative)
  • 将栈指针减少一个适当的量可以为没有指定初始值的数据在栈上分配空间。类似的,可以通过增加栈指针来释放空间
  • 当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分成为过程的栈帧(stack frame)
  • 可以通过寄存器最多传递6个整型(例如整数和指针)的参数,超出6个的部分就要通过栈来传递
  • 根据惯例,寄存器%rbx,%rbp和%r12~%r15被划分为被调用者保存寄存器
  • 所有其他的寄存器,除了栈指针%rsp,都分类为调用者保存寄存器
  • 数组元素在内存中按照“行优先”的顺序排列
  • 一个联合的总大小等于它最大字段的大小
  • 对齐原则是任何k字节的基本对象的地址必须是k的倍数
  • 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值,指针也可以指向函数
  • 为了管理变长栈帧,x86-64代码使用寄存器%rbp作为帧指针(有时候称为基指针)

处理器体系结构

本书最难章节,但是看几次会有惊喜👿

  • 现代微处理器可以称得上是人类创造出的最复杂的系统之一
  • 一个处理器支持的指令和指令的字节级编码称为它的指令集(ISA)
  • Y86-64顺序实现(SEQ),处理一条指令可以分成5个阶段:取指,译码,执行,访存,写回,更新PC
  • 流水线化(PIPE)通过让不同的阶段并行操作,改进了系统的吞吐量性能。在任意一个给定的时刻,多条指令被不同的阶段处理

优化程序性能

  • 编写高效程序需要做到以下几点:第一,我们必须选择一组适当的算法和数据结构。第二,我们必须编写出编译器能够有效优化以转换成高效可执行代码的源代码。第三项技术针对处理运算量特别大的计算,将一个任务分成多个部分,这些部分可以在多核和多处理器的某种组合上并行地计算。
  • 千兆赫兹(Ghz)代表每秒十亿周期
  • 消除循环的低效率
  • 减少过程调用
  • 消除不必要的内存引用
  • 在实际的处理器中,是同时对多条指令求值的,这个现象称为指令级并行
  • 发射时间为1的功能单元被称为完全流水线化的(fully pipelined):每个时钟周期可以开始一个新的运算
  • 循环展开
  • 提高并行性(多个累积变量,重新结合变换)
  • 寄存器溢出:如果我们的并行度超过了可用的寄存器数量,那么编译器会将某些临时值存放到内存中,通常是在运行时堆栈上分配空间
  • 不要过分关心可预测的分支
  • 书写适合用条件传送实现的代码

存储器层次结构

理解这章,你会发现,什么技术都离不开缓存👿

  • CPU寄存器保存着最常用的数据。靠近CPU的小的,快速的高速缓存存储器作为一部分存储在相对慢速的主存储器中数据和指令的缓冲区域。主存缓存存储在容量较大的,慢速磁盘上的数据,而这些磁盘常常又作为存储在通过网络连接的其他机器的磁盘或磁带上的数据的缓冲区域。
  • 如果你的程序需要的数据是存储在CPU寄存器中的,那么在指令的执行期间,在0个周期内就能访问到它们。如果存储在高速缓存中,需要4~75个周期。如果存储在主存中,需要上百个周期。而如果存储在磁盘中,需要大约几千万个周期!
  • 随机访问存储器(RAM)分为两类:静态的和动态的。SRAM用来作为高速缓存,DRAM用来作为主存。
  • 每个扇区包含相等数量的数据位(通常是512字节)
  • 磁盘以扇区大小的块来读写数据
  • 对扇区的访问时间有三个主要的部分:寻道时间,旋转时间,传送时间
  • 磁盘封装中有一个小的硬件/固件设备,称为磁盘控制器,维护着逻辑块号和实际(物理)磁盘扇区之间的映射关系
  • 在磁盘控制器收到来自CPU的读命令之后,它就讲逻辑块号翻译成一个扇区地址,读该扇区的内容,然后将这些内容直接传送到主存,不需要CPU的干涉。
  • 设备可以自己执行读或者写总线事务而不需要CPU干涉的过程,称为直接内存访问(DMA)
  • 时间局部性:在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来再被多次引用
  • 空间局部性:在一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置
  • 高速缓存存储器:直接映射高速缓存,组相联高速缓存,全相联高速缓存
  • 写回高速缓存通常是写分配
  • 编写高速缓存友好的代码

链接

  • 静态链接:像Linux ld程序这样的静态链接器(static linker)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的,可以加载和运行的可执行目标文件作为输出
  • 目标文件有三种形式:可重定位目标文件,可执行目标文件,共享目标文件
  • ELF(Executable and Linkable Format)
  • 链接器如何解析多重定义的全局符号:函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号
    • 不允许有多个同名的强符号
    • 如果有一个强符号和多个弱符号同名,那么选择强符号
    • 如果有多个弱符号同名,那么从这些弱符号中任意选择一个
  • 将多有相关的目标模块打包成为一个单独的文件,称为静态库
  • linux程序都可以通过调用execve函数来调用加载器,加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行程序
  • 动态链接共享库在内存中的.text节的一个副本可以被不同的正在运行的进程共享
  • 可以加载而无需重定位的代码称为位置无关代码(PIC),共享库的编译必须总是使用该选项
  • 链接可以在编译时由静态编译器来完成,也可以在加载时和运行时由动态链接器来完成
  • 链接器的两个主要任务是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用

异常控制流

  • 一个硬件定时器定期产生信号,这个事件必须得到处理。包到达网络适配器后,必须存放在内存中。程序向磁盘请求数据,然后休眠,直到被通知说数据已就绪。当子进程终止时,创造这些子进程的父进程必须得到通知
  • 异常的类型:中断,陷阱和系统调用,故障,终止
  • 所有到Linux系统调用的参数都是通过寄存器而不是栈传递的,寄存器%rax包含系统调用号。
  • 当内核代表用户执行系统调用时,可能会发生上下文切换,例如read系统调用
  • 中断也可能引发上下文切换,例如系统的定时器到时间了,会把运行过久的进程调度出去
  • fork系统调用创建的子进程几乎但不完全与父进程相同。
  • 如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父

虚拟内存

这一章是全书最精华的地方,理解虚拟内存,很多内存问题的疑问都能够在这里找到👿

  • 虚拟内存提供了三个重要的能力,1. 它将主存看成时一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。2. 它为每个进程提供了一致的地址空间,从而简化了内存管理。3. 它保护了每个进程的地址空间不被其他进程破坏
  • 因为对磁盘的访问时间很长,DRAM缓存总是使用写会,而不是直写
  • DRAM缓存不命中称为缺页
  • 加载器从不从磁盘到内存实际复制任何数据,虚拟内存系统会按照需要自动地调入数据页
  • TLB是一个小的,虚拟寻址的缓存
  • c++中的new和delete操作符与c中的malloc和free相当
  • 首次适配从头开始搜索空闲链表,选择第一个合适的空闲块
  • 下一次适配和首次适配很相似,只不过不是从链表的起始处开始每次搜索,而是从上一次查询结束的地方开始。
  • 最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块
  • 内部碎片是在一个已分配块比有效载荷时发生的
  • 外部碎片是当空闲内存合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的

系统级I/O

  • 输入/输出(I/O)是在主存和外部设备(例如磁盘驱动器,终端和网络)之间复制数据的过程。输入操作是从I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。
  • 在某些情况下,read和write传送的字节比应用程序要求的要少。出现这样情况的原因有:
    • 读时遇到EOF
    • 从终端读文本行
    • 读和写网络套接字
  • 除了EOF,当你在读磁盘文件时,将不会遇到不足值,而且在写磁盘文件时,也不会遇到不足值。
  • 共享文件:每个进程都有它自己单独的描述符表,而所有的进程共享同一个文件表和v-node表

网络编程

  • 从网络上接收到到数据从适配器经过I/O和内存总线复制到内存,通常是通过DMA传送。相似地,数据也能从内存复制到网络
  • 集线器不加分辨地将从一个端口上收到地每个位复制到其他所有地端口上。因此,每台主机都能看到每个位.
  • 关于EOF:EOF是由内核检测到的一种条件。应用程序在它接收到一个由read函数返回地零返回码时,它就会发现出EOF条件。对于磁盘文件,当前文件位置超出文件长度时,会发生EOF。对于因特网连接,当一个进程关闭连接它的那一端时,会发生EOF。连接另一端地进程在试图读取流中最后一个字节之后的字节时,会检测到EOF。

并发编程

  • 进程,I/O多路复用和线程是三种不同的构建并发程序的机制

to be continue....

About

my csapp homework


Languages

Language:C 75.6%Language:HCL 14.3%Language:Tcl 5.3%Language:Perl 2.8%Language:Makefile 0.8%Language:Assembly 0.3%Language:Lex 0.3%Language:C++ 0.3%Language:Yacc 0.2%Language:HTML 0.1%