虚拟存储器

一个系统中的进程是与其他进程共享CPU和主存资源的。

为了更加有效的管理存储器并且少出错,现在系统提供了一种对主存的抽象概念,叫做虚拟存储(VM)。虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。虚拟存储提供三个重要的能力:

  • 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存
  • 它为每个进程提供了一致的地址空间,从而简化了存储器管理
  • 它保护了每个进程的地址空间不被其他进程破坏

物理和虚拟寻址

  • 计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。第一个字节的地址为0,接下来1,2,依次类推。给定这种简单的结构,CPU访问存储器的最自然的方式就是使用物理地址–物理寻址
  • 当CPU执行这条加载指令时,它会生成一个有效物理地址,通过存储总线,把它传递给主存。主存取出从物理地址4处开始的4字节的字,并将它返回给CPU,CPU会将它存放在一个寄存器里
  • 虚拟寻址:使用虚拟寻址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译,地址翻译需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做存储器管理单元(MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操作系统管理
  • 地址空间:是一个非负整数地址的有续集合:{0,1,2,…},如果地址空间中的整数是连续地,那么我们说它是一个线性地址空间。在一个带有虚拟存储器的系统中,CPU从一个有N=2(\n)个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间
    • 一个地址空间的大小是由表示最大地址所需要的位数描述的。例如,一个包含N=2(\n)个地址的虚拟地址空间就叫做一个n位地址空间。现代系统支持32位或62位虚拟地址空间
    • 主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址

虚拟存储器作为缓存的工具

虚拟存储器(VM)被组织为一个由存放在磁盘上的N个连续地字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,这个唯一的虚拟地址是作为到数组的索引的这里写图片描述

VM系统通过将虚拟存储器分割为虚拟页的大小的固定的块。每个虚拟页的大小为P=2(\p)字节,类似的物理存储器被分割为物理页,大小也为P字节。

在任意时刻,虚拟页面的集合都分为三个不想交的子集

  • 未分配的:VM系统还未分配(创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间
  • 缓存的:当前缓存在物理存储器中的已分配页
  • 未缓存的:没有缓存在物理存储器中的已分配页

SRAM缓存:表示位于CPU和主存之间的L1、L2和L3高速缓存

DRAM缓存:表示虚拟存储器系统的缓存,它在主存中缓存虚拟页

在存储层次结构中,DRAM缓存的位置对它的组织结构有很大的影响。SRAM比DRAM要快大约10倍,而DRAM要比磁盘快大约100000多倍。因此,DRAM缓存中的不命中比起SRAM缓存中的不命中要昂贵得多,因为,DRAM缓存不命中要由磁盘来服务,而SRAM缓存不命中通常是由基于DRAM的主存来服务的

页表:存放在物理存储器中的数据结构

物理存储器(DRAM)
虚拟存储器(磁盘)

同任何缓存一样,虚拟存储器系统必须有某种方法判定一个虚拟页是否存放在DRAM中的某个地方。

页表将虚拟页映射到物理页。每个地址翻译硬件将一个虚拟地址转换为物理地址时都会读取页表。操作系统负责维护页表的内容,以及磁盘与DRAM之间来回传送页

页命中

这里写图片描述

当CPU读包含在VP2中的虚拟存储器的一个字时,VP2是被缓存在DRAM中。

缺页

DRAM缓存不命中称为缺页,CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从存储器中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。

磁盘和存储器之间传送页的活动叫做交换或者页面调度。页从磁盘换入DRAM和从DRAM换出磁盘。一直等待,直到最后时刻,也就是当有不命中发生时,才换入页面的这种策略称为按需页面调度

当操作系统分配一个新的虚拟存储器页对我们示例页表的影响,例如,调用malloc的结果。这个示例中,通过在磁盘上创建空间并更新PTE 5,使它指向磁盘上这个新创建的页,从而分配了VP 5

局部性

  • 尽管在整个运行过程中程序引用的不同页面的总数可能超出物理存储器总的大小,但是局部性原则保证了在任意时刻,程序往往在一个较小的活动页面集合上工作(工作集)
  • 只要我们的程序有好的时间局部性,虚拟存储器系统就能工作得相当好。如果工作集超出了物理存储器的大小,那么程序将颠簸,这时页面将不断地换进换出

虚拟存储器作为存储管理的工具

  • 虚拟存储器机制:利用DRAM缓存来自通常更大的虚拟地址空间的页面
  • 实际上,操作系统为每个进程提供了一个独立的页表,因而也就是一个独立的虚拟地址空间。注意:多个虚拟页面可以映射到同一个共享物理页面这里写图片描述

按需页面调度和独立的虚拟地址空间的结合,对系统存储器的使用和管理造成了深远的影响。VM简化了链接和加载、代码和数据共享,以及应用程序的存储器分配

  • 独立的地址空间允许每个进程的存储器映像使用相同的基本格式,而不管代码和数据实际存放在物理存储器的何处。Linux系统上的每个进程都使用类似的存储器格式。文本节总是从虚拟地址0x08048000处开始(32位地址空间),或从0x400000(64位地址空间)处开始。数据和bss节紧随在文本节后面,栈占据进程地址空间最高的部分,并向下生长
  • 在一些情况下,进程需要共享代码和数据。例如,每个进程必须调用相同的操作系统内核代码,而每个C程序都会调用C标准中的程序,如printf。操作系统通过将不同进程中适当的虚拟页面映射到想用的物理页面,从而安排多个进程共享这部分代码的一个拷贝,而不是每个进程中都包括单独的内核和C标准库的拷贝

虚拟存储器作为存储器保护的工具

计算机系统必须为操作系统提供手段来控制对存储器系统的访问这里写图片描述

  • 提供独立的地址空间使得分离不同进程的私有存储器变得容易。但是地址翻译机制可以以一种自然的方式扩展到提供更好的访问控制。因为CPU每次生成一个地址时,地址翻译硬件都会读一个PTE,所以通过在PTE上添加一些额外的许可位来控制对一个虚拟页面内容的访问十分简单
  • 如上图,每个PTE中已经添加三个许可位。SUP位表示进程是否必须运行在内核模式(超级用户)下才能访问该页。READ和WRITE位控制对页面的读和写访问

地址翻译

形式上来说,地址翻译是一个N元素的虚拟地址空间和一个M元素的物理地址空间中元素之间的映射

MMU如何利用页表来实现这种映射

这里写图片描述

MMU利用虚拟页号(VPN)来选择适当的PTE(页表条目),例如,VPN 0 选择PTE 0, VPN 1选择PTE 1,依次类推。将页表条目中物理页号和虚拟地址中的VPO(虚拟页面偏移)串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移和VPO是相同的

页面命中,CPU硬件执行步骤

  • 第一步:处理器生成一个虚拟地址,并把它传送给MMU
  • 第二步:MMU生成一个PTE地址,并从高速缓存/主存请求得到它
  • 第三步:高速缓存/主存向MMU返回PTE
  • 第四步:MMU构造物理地址,并把它传送给高速缓存/主存
  • 第五步:高速缓存/主存返回所请求的数据字给处理器

处理缺页请求要求硬件和操作系统内核协作完成

  • 第一步到第三步上同
  • 第四步:PTE的有效位为0,所以MMU出发了一次异常,传递CPU控制到操作系统内核中的缺页异常处理程序
  • 第五步:缺页异常处理程序确定出物理存储器中的牺牲页,如果这个页面已经被修改了,则把它换出磁盘。
  • 第六步:缺页处理程序页面调入新的页面,并更新存储器中的PTE
  • 第七步:缺页处理程序返回原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理存储器中,所以就命中。

Linux虚拟存储器系统

  • Linux为每个进程保持了一个单独的虚拟地址空间这里写图片描述

内核虚拟存储器包含内核中的代码和数据结构。内核虚拟存储器的某些区域被映射到所有进程共享的物理页面。

Linux将虚拟存储器组织成一些区域的集合。一个区域就是已经存在着的虚拟存储器的连续片,这些页是以某种方式相关联的。例如,代码段,数据段,堆,共享库段以及用户栈都是不同的区域。每个存在的虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程引用。

内核为系统中的每个进程维护一个单独的任务结构。任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(例如,PID,指向用户栈的指针,可执行目标文件的名字以及程序计数器)

LInux缺页异常处理

  • 假设MMU在视图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
    • 虚拟地址A是合法的吗?换句话说,A在某个区域结构定义的区域内吗?
    • 视图进行的存储器访问是否合法?换句话说,进程是否有毒、写或执行这个区域内页面的权限呢?如果视图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。
    • 此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送A到MMU。这次MMU就能正常的翻译A,而不会产生缺页中断了

存储器映射

  • Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射。虚拟存储器区域可以映射到两种类型的对象中的一种:
    • Unix文件系统中的普通文件
    • 匿名文件

无论在那种情况下,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫做交换空间或者交换区域。在任何时刻,交换空间限制着当前运行着的进程能够分配的虚拟页面的总数

共享对象

  • 用来控制多个进程如何共享对象的机制

一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象。如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到它们虚拟存储器的其他进程而言也是可见的。而且,这些变化也会反映在磁盘上的原始对象中。

对一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反映在磁盘上的对象中。

  • 私有对象是使用一种叫做写时拷贝的巧妙技术被映射到虚拟存储器中的。一个私有对象开始生命周期的方式基本上与共享对象一样,在物理存储器只保存有私有对象的一份拷贝。下图,其中两个进程将一个私有对象映射到它们虚拟存储器的不同区域,但是共享这个对象同一个物理拷贝。对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时拷贝。只要没有进程试图写它自己的私有区域,它们就可以继续共享物理存储器中对象的一个单独拷贝。然而,只要有一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障。
    • 当故障处理程序注意到保护异常是由于进程试图写私有的写时拷贝区域中的一个页面而引起的,它会在物理存储器中创建这个页面的一个新拷贝,更新页表条目指向这个新的拷贝,然后恢复这个页面的可写权限。当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上这个写操作就可以正常执行了。

这里写图片描述

再看fork函数

  • 当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟存储器,它创建了当前进程的mm_struct、区域结构和页表的原样拷贝。它将两个进程中的每个页面都标记为只读。并将两个进程中的每个区域结构都标记为私有的写时拷贝。

再看execve函数

execve("a.out",NULL,NULL);

execve函数在当前进程中加载并运行包含在可执行文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要一下步骤

  • 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构
  • 映射私有区域。为新程序的文本、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时拷贝的。文本和数据区域被映射为a.out文件中的文本和数据区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。
  • 映射共享区域。如果a.out程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  • 设置程序计数器。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。

动态内存分配

动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。对于每个进程,内核维护着一个变量brk(break),它指向堆的顶部。

分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟存储器片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是存储器分配器自身隐式执行的。

  • 显式分配器
  • 隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。像Java之类的高级语言就依赖垃圾收集来释放已分配的块。

malloc和free

C标准库提供了一个称为malloc程序包的显式分配器。程序通过调用malloc函数来从堆中分配块。

如果malloc遇到问题(例如,程序要求的存储块比可用的虚拟存储器还要大),那么它就返回NULL,并设置errno。malloc不初始化它返回的存储器。那些想要已初始化的动态存储器应用程序可以使用calloc,calloc是一个基于malloc的瘦包装函数,它将分配的存储器初始化为零。想要改变一个以前已分配的大小,可以使用realloc函数。

为什么要使用动态存储器分配?

  • 程序使用动态存储器分配的最重要的原因是经常直到程序实际运行时,它们才知道某些数据结构的大小。

显式分配器必须在一些相当的约束条件下工作:

  • 处理任意请求序列。
  • 立即响应请求
  • 只使用堆
  • 对齐块
  • 不修改已分配的块

碎片

  • 造成堆利用率很低的主要原因是一种称为碎片的现象。当虽然有未使用的存储器但不能用来满足分配请求时,就会发生这种现象。
    • 内部碎片是在一个已分配块比有效载荷大时发生。例如,一个分配器的实现可能对已分配块强加一个最小的大小值,而这个大小要比某个请求的有效载荷大
    • 外部碎片是当空闲存储器合计起来足够一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生。

垃圾收集器

  • 垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配的块。在一个支持垃圾收集的系统中,应用显式分配堆块,但是从不显式地释放它们。

垃圾收集器将存储器视为一张有向可达图。这里写图片描述
该图的节点被分成一组根节点和一组堆节点。每个堆节点对应于堆中的一个已分配块。根节点对应于这样一种不再堆中的位置,它们中包含指向堆中的指针。

当存在一条从任意根节点出发并到达p的有向路径时,我们说节点p是可达的。在任何时刻,不可达节点对应于垃圾,是不能被应用再次使用的。垃圾收集器的角色是维护可达图的某种表示,并通过释放不可达节点并将它们返回给空闲链表,来定期的回收它们。
这里写图片描述
无论何时需要堆空间,应用都会用通常的方式调用malloc。如果malloc找不到合适的空闲块,那么它就调用垃圾收集器,希望能够回收一些垃圾到空闲链表。收集器识别出垃圾块,并通过调用free函数将它们返回给堆。关键思想是收集器代替应用区调用free。当对收集器的调用返回时,malloc重试,试图发现一个合适的空闲块。如果还是失败了,那么它就会向操作系统要求额外的存储器。最后,malloc返回一个指向请求块的指针(成功)或者返回一个空指针(不成功)。

Mark&Sweep垃圾收集器

  • Mark&Sweep垃圾收集器由标记阶段和清除阶段组成,标记阶段标记出根节点的所有可达的和已分配的后继,而后面的清除阶段释放每个未被标记已分配块。典型的,块头部中空闲的低位中的一位用来表示这个块是否被标记了。
  • 我们对Mark&Sweep的描述假设使用下列函数,其中ptr定义为typedef void* ptr;
    • ptr isPtr(ptr p):如果p指向一个已分配块中的某个字,那么就返回一个指向这个块的起始位置的指针b,否则返回NULL。
    • int blockMarked(ptr b): 如果已经标记了块b,那么就返回true
    • int blockAllocated(ptr b): 如果块b是已分配的,那么就返回true
    • void markBlock(ptr b): 标记块b
    • int length(ptr b): 返回块b以字为单位的长度
    • void unmarkBlock(ptr b): 将块b的状态由已标记的改为未标记的
    • ptr nextBlock(ptr b): 返回堆中块b的后继

标记阶段:

void mark(ptr p)
{
    if((b=isPtr(p))==NULL)
    {
        return;
    }
    if(blockMarked(b))
    {
        return;
    }
    markBlock(b);
    len = length(b);
    for(i=0;i<len;i++)
    {
        mark(b[i]);
    }
    return;
}
如果p不指向一个已分配并且未标记的堆块,mark函数就立即返回。否则,它就标记这个块,并对块中的每个字递归地调用它自己。每次对mark函数的调用都标记某个根节点的所有未标记并且可达的后继节点。在标记阶段的末尾,任何未标记的已分配块都认定是不可达的,是垃圾,可以在清除阶段回收。

void sweep(ptr b,ptr end)
{
    while(b < end)
    {
        if(blockMarked(b))
        {
            unmarkBlock(b);
        }
        else if(blockAllocated(b))
        {
            free(b);
        }
        b = nextBlock(b);
    }
    return;
}

C语言的保守Mark&Sweep

  • C语言为isPtr函数的实现造成了一些有趣的挑战
    • C不会用任何类型信息来标记存储器位置。因此,对isPtr没有一种明显的方式来判断它的输入参数p是不是一个指针。第二,即使我们知道p是一个指针,对isPtr也没有明显的方式来判断p是否指向一个已分配块的有效载荷中的某个位置。
    • 对后一个问题的解决方法是将已分配块集合维护成一颗平衡二叉树,这棵树保持着这样一个属性:左子树中的所有块都在放在较小的地址处,而右子树中的所有块都放在较大的地址处。这就要求每个已分配块的头部里有两个附加字段(left和right)。每个字段指向某个已分配块的头部这里写图片描述

isPtr(ptr p)函数用树来执行对已分配块的二分查找。每一步中,它依赖于块头部中的大小字段来判断p是否落在这个块的范围之内。

平衡树方法保证会标记所有从根节点可达的节点,从这个意义上来说它是正确的。这是一个必要的保证,因为应用程序的用户当然不会喜欢把他们的已分配块过早地返回给空闲链表。然而,这种方法从某种意义上而言又是保守的,因为它可能不正确的标记实际上不可达的块,因此它可能不会释放这些垃圾。虽然这并不影响应用程序的正确性,但是这可能导致不必要的外部碎片。

C程序的Mark&Sweep收集器必须是保守的,其根本原因是C语言不会用类型信息来标记存储器位置。因此,像int或者float这样的标量可以伪装成指针。例如,假设某个可达的已分配块在它的有效载荷中包含一个int,其值碰巧对应于某个其他已分配块b的有效载荷的一个地址。对收集器而言,是没有办法推断出这个数据实际上是int而不是指针的。因此,分配器必须保守地将块b标记为可达,尽管事实上它可能是不可达的。

C程序中常见的与存储器有关的错误

  • 间接引用坏指针
    • scanf(“%d”,&val);正确方法是传递给scanf一个格式串和变量的地址
    • scanf(“%d”,val); scanf将把val的内容解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序立即以异常终止。在最糟糕的情况下,val的内容对应于虚拟存储器的某个合法的读/写区域,于是就覆盖了存储器,这通常会在相当长的一段时间以后造成灾难性的后果。
  • 读未初始化的存储器

    • 虽然bss存储器位置(诸如未初始化的全局C变量)总是被加载器初始化为零,但是对于堆存储器却不是这样的。一个常见的错误就是假设堆存储器被初始化为零

      int * matvec(int **A,int *x,int n)
      {
          int i,j;
          int* y=(int * )(malloc(n*sizeof(int));
          for(i=0;i<n;i++)
          {
              for(j=0;j<n;j++)
              {
                  y[i] += A[i][j] * x[j];
              }   
          }
          return y;
      }
      正确的实现方式是显式地将y[i]设置为零,或者使用calloc。
      
  • 允许栈缓冲区溢出

    • 如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误。

      void bufoverflow()
      {
          char buf[64];
          gets(buf);
          return ;
      } 
      gets函数拷贝一个任意长度的串到缓冲区,缓冲区溢出错误。为了纠正这个错误,我们必须使用fgets函数,这个函数限制了输入串的大小。
      
  • 假设指针和它们指向的对象是相同大小的

    int **makeArray1(int n, int m)
    {
        int i;
        int ** A= (int ** )malloc(n * sizeof(int));
        for(i=0;i<n;i++)
        {
            A[i] =(int * )malloc(m * sizeof(int));
        }
        return A;
    }
    目的是创建一个由n个指针组成的数组,每个指针都指向一个包含m个int的数组。然而,在第五行将sizeof(int *)写成了sizeof(int),代码实际上创建的是一个int的数组。
    
  • 造成错位错误:一种常见的覆盖错误来源

    int ** makeArray2(int n, int m)
    {
        int i;
        int ** A=(int ** )malloc(n * sizeof(int * ));
        for(i=0;i<=n;i++)
        {
            A[i] = (int*)malloc(m * sizeof(int));
        }
        return A;
    }
    
  • 引用指针,而不是它所指向的对象

    • 不太注意C操作符的优先级和结合性,我们就会错误的操作指针,而不是指针所指向的对象。下面函数,其目的是删除一个有* size项的二叉堆的第一项,然后剩下的*size-1项重新建堆。

      int * binheapDelete(int * binheap, int size)
      {
      int * packet = binheap[0];
      binheap[0]=binheap[*size-1];
      *size–;
      heapfy(binheap,*size,0);
      return (packet);
      }
      第6行,目的是减少size指针指向的整数的值。然而,因为一元运算符–和*优先级相同,从右向左结合,所以实际减少的是指针自己的值,而不是它所指向的整数的值。

  • 误解指针运算

    • 忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的,这种大小单位并不一定是字节。

      int *search(int * p,int val)
      {
          while(*p && *p != val)
          {
              p += sizeof(int);  //should be p++
          }
          return p;
      } 
      
  • 引用不存在的变量

    • 不理解栈的规则,引用不再合法的本地变量

      int *stackref()
      {
      int val;
      return & val;
      }
      函数返回一个指针(如p),指向栈里的一个局部变量,然后弹出它的栈帧。尽管p仍然指向一个合法的存储器地址,但它已经不再指向一个合法的变量了。当以后在程序中调用其他函数时,存储器将重用它们的栈帧。再后来,如果程序分配某个值给*p,那么它可能实际上正在修改另一个函数的栈帧中的一个条目。

  • 引用空闲堆块中的数据

    • 引用已经释放了的堆块中的数据。

      int *heapref(int n,int m)
      {
          int i;
          int * x ,* y;
          x=(int* )malloc(n * sizeof(int));
          free(x);
          y = (int * )malloc(m * sizeof(int));
          for(i=0;i<m;i++)
          {
              y[i] = x[i]++;
          }
          return y;
      } 
      
  • 引起存储器泄露

    void leak(int n)
    {
        int *x = (int *)malloc(n * sizeof(int));
        return;
    }
    

小结

  • 虚拟存储器是对主存的一个抽象。支持虚拟存储器的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。专门的硬件通过使用页表来翻译虚拟地址,而页表的内容是由操作系统提供的。

虚拟存储器提供三个重要功能

  • 它在主存中自动缓存最近使用的存放在磁盘上的虚拟地址空间的内容。虚拟存储器缓存中的块叫做页。对磁盘上页的引用会触发缺页,缺页将控制转移到操作系统中的一个缺页处理程序。缺页处理程序将页面从磁盘拷贝到主存缓存。
  • 虚拟存储器简化了存储器管理,进而简化了链接,在进程间共享数据,进程的存储器分配以及程序加载。
  • 虚拟存储器通过在每条页表条目中加入保护位,从而简化了存储器保护。
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值