CSAPP 第八、九章

来源:互联网 发布:ubuntu运行lua文件 编辑:程序博客网 时间:2024/05/21 13:10

Chapter8异常控制流:发生在计算机系统的各个层次。

8.1 Exception:异常是异常控制流的一种形式。一部分由硬件实现,一部分由操作系统实现。任何情况下,处理器检测有事件发生时,会通过一张exception table的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序),异常处理完成,根据引起异常的事件类型,发生的情况:1.处理程序将控制返回给当前指令2.控制程序将控制返回给下一条指令3.处理程序终止被中断的程序。

8.1.1异常处理:处理异常需要硬件和软件合作。系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号码(exception number),其中一些由处理器设计者分配,其他由操作系统内核(常驻内存部分)的设计者分配。系统启动时,操作系统分配和初始化一张异常表的跳转表,使得表目k包含异常k的处理程序地址。(清华汇编语言教材)运行时,处理器检测到发生一个事件,并确定了相应的处理异常号k,之后处理器触发异常,方法是执行间接过程调用,通过异常表的表目k,转到相应的处理程序

8.1.1异常类别:可以分成四类:interrypt,trap, fault and abort.

中断是异步发生的,来自处理器外部的I/O设备的信号结果,总返回下一条指令。陷阱是有意的,是执行一条指令的结果,总返回下一条指令。故障由错误情况引起,可以被故障处理程序修正。终止时不可恢复的致命错误造成的结果。

         8.2进程:异常是允许操作系统内核提供进程概念的基本构造块,是计算机科学中最深刻成功的概念之一。进程提供给应用程序关键的抽象:1.一个独立的逻辑控制流2.一个私有地址空间。一系列的程序计数器PC的值唯一地对应于包含在程序的可执行目标文件中的指令,或者包含在运行时动态链接到程序的共享对象中的指令。该PC值序列叫做逻辑控制流。一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流并发地运行。

8.2.4用户模式和内核模式:处理器提供机制,限制一个应用可以执行的指令以及可以访问的地址空间范围。通常用某个控制寄存器中的一个mode bit来提供这种功能,该寄存器描述了进程当前享有的特权,设置了mode bit时,进程就运行在内核模式,这个进程可以执行指令集中任何指令,访问系统中任何内存位置。未设置时,进程运行在用户模式中,不允许执行特权指令不允许直接引用地址空间中内核区内的代码和数据。运行应用程序代码的进程初始时处于用户模式,变成内核模式的唯一方法是通过中断,故障或者trap这样的异常。

8.2.5上下文切换:操作系统使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文(context)。它是内核重新启动一个被抢占进程需要的状态。Scheduler执行调度一个新的进程时,它抢占当前进程,使用上下文切换机制来讲控制转移到新的进程,上下文切换机制:1.保存当前进程的上下文2.恢复某个先前被抢占的进程被保存的上下文3.将控制传递给这个新恢复的进程。

         8.4进程控制:描述使用Unix大量操作系统的系统调用函数。

         8.5信号:一种更高层的软件形式异常,Linux信号,它允许进程和内核中断其他进程。一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。每种信号类型都对应于某种系统事件。信号提供一种可以通知用户进程发生了这些异常的机制。

发送信号:内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。Unix系统提供大量向进程发送信号的机制,它们都基于进程组的概念。其中每个进程都只属于一个进程组。

接受信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号,进程可以忽略这个信号,终止或者通过执行一个信号处理程序的用户层函数捕获这个信号。当内核把进程p从内核模式切换到用户模式时,它会检查进程p的未被阻塞的待处理信号的集合,若集合为空,则内核将控制传递到p的逻辑控制流中下一条指令,若是非空,则选择集合中的某个信号k,强制p接收信号k。每个信号类型都有一个预定义默认行为:1.进程终止2.进程终止并转储内存3.进程停止直到被SIGCONT信号重启4.进程忽略该信号。

Linux提供阻塞信号的隐式和显式的机制。对于隐式,内核默认阻塞任何当前处理程序正在处理信号类型的待处理信号。应用程序可以使用sigprocmask以及它的辅助函数明确阻塞或者解除阻塞选定的信号。

8.5.5编写信号处理程序。信号处理难点:1.处理程序与主程序并发运行,共享同样的全局变量,可能与主程序或者其他程序互相干扰2。如何及合适接收信号3.不同系统有不同的信号处理语义。安全的信号处理原则;正确的信号处理;可移植的信号处理

         8.6非本地跳转:C语言提供一种用户级异常控制流形式,称为非本地跳转(nonlocal jump).它将控制直接从一个函数转移到另一个当前正在执行的函数,无需经过正常的调用-返回序列,通过setjmp和longjmp函数来实现。

 

Chapter9虚拟内存:一个系统中进程与其他进程共享CPU和主存资源。现代系统提供堆主存的抽象,称为Virtual MemoryVM为每个进程提供一个大的,一致的私有地址空间。虚拟内存提供三种能力:1.将主存看做一个存储在磁盘上的地址空间的缓存2.为每个进程提供一致的地址空间3.保护每个进程的地址空间不被其他进程破坏。

9.1物理和虚拟寻址:计算机系统的主存被组织成一个由M个连续字节大小单元组成的数组,每个字节都有一个唯一地物理地址PA,其第一个字节地址为0,CPU访问内存最自然方式是使用物理地址,称此方式为物理寻址(physical addressing)。现代处理器使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存。该虚拟地址先转化为物理地址再送至存储器(称为地址翻译,需要CPU和OS配合),CPU有MMU(memory management unit)硬件利用存放在主存中的查询表来动态翻译虚拟地址,表的内容由操作系统管理。

9.2地址空间:是一个有序的集合(非负整数地址)。若地址空间的整数是连续的,我们称之为线性地址空间。n位地址空间含有2的n次方个地址。物理地址空间。

9.3虚拟存储器作为缓存工具:概念上,虚拟内存被组织成一个由放在磁盘上N个连续字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址(作为数组下标)。磁盘上数组的内容被缓存到主存中。虚拟内存系统通过将虚拟内存分割为虚拟页的大小固定的block来进行缓存,类似的,物理内存同样被分割称为物理页(也称为page frame)。

任何时间上,虚拟页集合分为三个不相交的子集:1.未分配的:VM系统未分配的页,没有数据与之关联,不占用磁盘空间2.缓存的:当前缓存在物理内存中已分配的页3.未缓存:未缓存在VM的已经分配的页。

9.3.1 DRAM Cache: DRAM表示虚拟内存系统的缓存,在主存中缓存虚拟页。DRAM比SRAM慢10倍,磁盘比DRAM慢约100000多倍,DRAM缓存不命中需要磁盘来服务,因此很昂贵。虚拟页一般为4KB到2MB。DRAM缓存是全相联的,任何虚拟页可以放在任何物理页中

9.3.2页表:判定一个虚拟页是否缓存在DRAM中的一个地方,同时确定虚拟页放在哪个物理页中,而不命中时,需要判断虚拟页存放在磁盘哪个位置,这些功能由软件硬件联合提供,包括操作系统,MMU中地址翻译硬件和一个存放在物理内存的页表(page table)数据结构。

页表是一个数组,包含page table entry.每个PTE由一个有效位和一个n位地址字段组成,有效位设置了(1),表示虚拟页被缓存在DRAM中,则地址字段页表示DRAM中相应物理页的起始位置。(指向物理内存地址)未设置则指向虚拟内存地址。

9.3.3页命中(hit):CPU需要虚拟内存的数据时,若该页面有效位设置了,则可以通过PTE获得该虚拟页相关联的物理地址。

9.3.4缺页:(page fault)即DRAM缓存不命中。当CPU需要引用的虚拟页未缓存在DRAM中,地址翻译硬件从内存中读取PTE3,从有效位推断VP3没被缓存,触发一个page fault。该异常调用内核中page fault处理程序,程序选择一个victim page(当物理内存空间不够时),该有不再缓存在主存中,VP3代替之(内核从磁盘复制VP3到内存中victim page原来的位置),之后更新PTE3,返回。

局部性原则保证在任意时刻,程序趋向于在一个较小的活动页面(active page)集合工作,集合称为working set or resident set。当工作集大小超出了物理内存大小,则会发生thrashing,使得页面不断换进换出。

         9.4虚拟内存作为内存管理的工具:VM简化了链接和加载,代码和数据共享以及应用程序的内存分配。

         9.5虚拟内存作为内存保护的工具:控制对内存系统的访问,独立的地址空间使得区分不同进程的私有内存变得容易,地址翻译机制以一种自然的方式扩展到提供更好的访问控制。通过在PTE添加一些额外的许可位来控制对一个虚拟页面内容的访问十分简单。若一条指令违反了许可条件,则CPU触发一般保护故障,将控制传递给一个内核中的异常处理程序,Linux shell称该异常报告为segmentation fault。

         9.6地址翻译:地址翻译是一个N元素的虚拟地址空间中的元素和一个M元素的物理地址空间中元素间的映射,MMU利用页表来实现这种映射。首先,是CPU一个控制寄存器:页表基址寄存器(page table base register PTBR)指向当前页表。页表由有效位和物理页号PPN组成。n位虚拟地址由两部分组成:一个p位虚拟页面偏移(virtual page offset VPO)和一个(n-p)位的虚拟页号(virtualpage number VPN)。其中VPN作为页表的索引,MMU也利用它来选择适当的PTE。物理地址类似:一个物理页号(physical page number PPN)和一个物理页偏移量PPO。其中,利用页表中的PPN和指向当前PTE的虚拟地址的VPO作为PPO组成一个物理地址。

CPU硬件执行的步骤:1.处理器生成一个虚拟地址,把它传送给MMU 2.MMU生成PTE地址,从cache/主存请求得到它 3.cache/主存向MMU返回PTE 4.MMU构造物理地址,将它传送给cache/主存 5.cache/主存返回所请求的数据字给处理器。Figure 9.12 9.13

Page hit完全由硬件来处理,处理缺页则要硬件和OS一起完成:1到3步同上,4.PTE中有效位归零,MMU触发一次异常,传递CPU的控制到操作系统内核的缺页异常处理程序5.程序确定出物理内存的victim页,若它已被修改,则把它换出磁盘 6.缺页处理程序调入新的页面,并更新内存中的PTE 7.程序返回到原来的进程,再次执行导致缺页的指令,CPU将引起缺页的虚拟地址重新发送给MMU。

9.6.1大多数系统使用物理寻址来访问SRAM高速缓存,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块变得简单,无需处理保护(地址翻译过程来完成)。

9.6.2 MMU中设置一个关于PTE的缓存,翻译后备缓冲器(TranslationLookaside Buffer)TLB

这是一个小的,虚拟寻址的缓存,每一行保存一个PTE。此时,虚拟地址组成改变了figure9.15

VPN由TLB标记和TLB索引组成。

TLB命中时步骤:(所有翻译在MMU完成)1.CPU产生一个虚拟地址2,3 MMU从TLB中取出相应的PTE4.MMU将虚拟地址翻译成物理地址,并发送到cache/主存 5.cache/主存的字返回给CPU

9.6.3多级页表:减少了内存要求

         9.8内存映射:Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,该过程称为内存映射。对象种类:1.Linux文件系统中的普通文件2.匿名文件,内核创建的,全是二进制零。一旦一个虚拟页面被初始化,它就在一个由内核维护的专门的交换文件(交换空间)间换来换去。一个对象可以被映射到虚拟内存的一个区域,要么作为共享对象,要么作为私有对象

         9.9动态内存分配:分配器维护一个进程的虚拟内存区域,称为heap。分配器将堆视为一组不同大小的block的集合,每个block是一个连续的虚拟内存片(chunk),它们是分配的,或者空闲的。空闲的保持该状态直到它显式地被应用分配,已分配的直到被释放前保持该状态,其中释放分为应用程序显式执行或者分配器隐式执行。分配器分为两种基本风格:1.显式分配器,应用显式的释放任何已分配的块。2.隐式分配器:分配器检测一个已分配的块何时不再被程序使用,则释放之。也称为垃圾收集

9.9.1 malloc and free: C标准库提供一个称为malloc程序包的显式分配器,程序通过调用malloc来从堆中分配块。一个字四个字节。函数返回一个指针,指向大小至少为size字节的内存块,若无法分配,返回NULL。需要已初始化的动态内存可以使用calloc,函数会将内存初始化为零,改变已分配块大小,可以使用realloc。Free参数需要时已分配的块的起始位置。

9.9.2程序使用动态内存分配最重要原因是一些数据大小要到程序实际运行时才知道。

9.9.3分配器要求和目标:

显式分配器的约束条件:1.处理任意请求序列2.立即响应请求3.只使用堆4.对其块保证它们可以存放任何类型的数据对象5.不修改已分配的块

设计目标:最大化吞吐率和最大化内存利用率

9.9.4碎片:fragmentation。存在未使用的内存但不能用来满足分配请求。

内部碎片:在一个已分配块比有效pay-load大时发生。其数目是已分配块大小和它们有效载荷大小之差的和。

外部碎片:空闲内存合计可以满足一个分配请求,但没有一个单独的空闲块足够大来处理这个请求。分配器通常用启发式策略来试图维持少量的大空闲块。

9.9.6隐式空闲链表:任何实际的分配器都需要一些数据结构,允许它区别块便捷,以及区别已分配块和空闲块。大多数分配器将这些信息放入块本身。Figure9.35方法。这个块由一个字的头部,有效载荷和额外的填充组成。头部编码了块的大小以及块是已分配或未分配。用这种格式,可以将堆组织为一个连续的已分配块和空闲块的序列。我们称这种结构为隐式空闲链表,因为空闲块是通过头部中的大小字段隐含地着的。

9.9.7放置已分配的块:一个应用请求一个k字节的块时,分配器搜索空闲链表,找到一个足够大可以放置所请求块的空闲块。其搜索方式由放置策略确定,常见有first,next,best fit.

9.9.10当释放已分配块时,可能有其他空闲块与新释放的空闲块相邻,相邻的空闲块引起fault fragmentation.任何实际的分配器都必须合并相邻的空闲块,该过程称为coalescing。何时执行合并,可以选择立即合并或者推迟合并。

9.9.11带边界标记的合并:已释放的块称为当前块,对于它后面的空闲块,只需要将当前块的头部加上下一个空闲块的大小即可。对于前面的,Knuth提出使用boundary tag.允许常数时间内进行对前面块的合并。在每个块结尾添加一个footer(是头部的一个副本,里当前块开始位置一个字的距离).分配器可以检查footer判断前一个块的起始位置和状态。

9.9.13显式空闲链表:因为块分配与堆中块总数呈线性关系,因此对通用的分配器,隐式空闲链表是不合适的。1.更好的方法是将空闲块组织为某种形式的显式数据结构,指针可以存放在空闲块的主体里(如双向空闲链表)。2.使用后进先出LIFO顺序维护链表,将新释放的块放置在链表的开始处。3.按地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。显式链表缺点是空闲块需要包含所有需要的指针,导致最小块变大。

9.9.14分离的空闲链表:一种流行的减少分配时间的方法,称为分离存储,即维护多个空闲链表。每个链表中的块有大致相等的大小。分配器维护的是一个空闲链表数组,每一个大小类一个空闲链表,按照大小升序排列。有很多种分离存储的方法,书中描述两种:简单分离寸纯和分离适配。

         9.10垃圾收集:一种动态内存分配器,自动释放程序不再需要的已分配块,这些块叫做垃圾,自动回收堆存储的过程给你叫做垃圾收集。收集器将内存视为一张有向可达图,图中节点对应堆中一个已分配块,根节点包含指向堆中的指针。不可达节点对应于垃圾,收集器将释放它们并返回给空闲链表。一些语言对应用如何创建和使用指针有很严格的控制,可以维护可达图的一种精确表示,因此可以回收所有垃圾。一些则不能维持可达图的精确表示,称它们为保守的垃圾收集器(一些不可达的节点被标记成为可达的)。

9.10.2 Mark&Sweep垃圾收集器:由mark阶段和sweep阶段组成。标记阶段标记出根节点的所有可达的和已分配的后继,清除阶段释放每个未被标记的已分配块。块头部中空闲的低位中的一位常用来表示这个块是否被标记。

原创粉丝点击