虚拟存储器
现代系统提供的一种对主存的抽象概念。虚拟存储器是异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。其拥有三种重要的能力:
- 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动的区域,并根据需要在磁盘的主存之间来回传送数据,通过这种方式,高效地使用了主存。
- 它为每个进程提供了一致的地址空间,从而简化了存储器的管理。
- 它保护了每个进程的存储空间不被其他进程破坏。
虚拟寻址
CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。将物理地址装换成虚拟地址的任务叫做地址翻译。CPU芯片上交错存储器管理单元的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表内容由操作系统管理。
页面的调度
这部分在操作系统课程中老师已经做了详细的讲解,这里就不多加赘述了。
Linux的虚拟存储器
Linux将虚拟存储器组织成一些区域(也叫段)的集合。一个区域就是已存在着的(已分配的)虚拟存储器的连续片,这些页是以某种方式相关联的。
Linux缺页异常处理
当MMU试图翻译某个虚拟地址A时,触发一个缺页时。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行以下步骤:
- 虚拟地址A合法吗?
- 试图进行的存储器访问是否合法?
- 处理完以上两个问题后,内核知道了这个缺页是由于合法的虚拟地址进行合法的操作造成的。然后进行处理:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。然后A就能正常翻译A了。
存储器映射
Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射。
虚拟存储器区域可以映射到两种类型的对象:
- Unix文件系统中的普通文件。
- 匿名文件。
存储器映射给我们提供了一种清晰的映射机制,用来控制多个进程共享对象。
- 如果要将一个进程将一个存储对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到它的虚拟地址空间的其他进程而言是可见的,这些变化会反应在磁盘上的原始文件对象中。
- 而对于一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反应在磁盘的对象中。
动态存储器分配
动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护的。每个块就是一个连续的虚拟存储器片,要么是已分配的,要么是空闲的。
- 已分配的块显式地保留为供应用程序使用。
- 空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。
分配器有两种基本风格
-
显式分配器:要求应用显式地释放任何已分配的块。
- 工作条件:处理任意请求序列,立即响应请求,只使用堆,对齐块,不修改已分配的块。
- 目标:最大化吞吐率,最大化存储器利用率。
- 隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。因此隐式分配器叫做垃圾收集器,自动释放未使用的已分配块的过程叫做垃圾收集。
使用动态存储器分配的重要原因:这种分配方式直到程序实际运行时,它们才知道某些数据结构的大小,可以有效地利用存储器资源。
碎片
有未使用的存储器但不能满足分配请求的现象称为碎片。碎片由两种形式:
- 内部碎片。
- 外部碎片。
分配器常见放置策略
- 首次适配:从头开始搜索链表,选择第一个合适的空闲块。
- 下一次适配:从上次结束的地方搜索链表,选择第一个合适的空闲块。
- 最佳适配:检查每个空闲块,选择合适所需请求大小的最小空闲块。
分配器的数据结构
- 隐式空闲链表。
- 显式空闲链表。
分离存储
一种减少分配时间的方法,就是维护多个空闲链表,其中每个链表中的块都有大致相等的大小。
基本分离存储的方法:
- 简单分离存储。
- 分离适配。
- 伙伴系统。
垃圾收集
垃圾收集器是一种动态存储分配器,它自动释放程序不在需要的已分配块。这些块称为垃圾。自动回收堆存储的过程叫做垃圾收集。垃圾收集器将存储器视为一张有向的可达图。该图的节点被分成一组根节点和一组堆节点。每个堆节点对应于堆中的一个已分配块。
C语言中常见的与存储器有关的错误
- 间接引用坏指针。
- 读取未初始化的存储器。
- 允许栈缓冲区溢出。
- 假设指针和它们指向的对象大小相同的。
- 造成错位错误。
- 引用指针,而不是它所指向的对象。
- 误解指针运算。
- 引用不存在的变量。
- 引起存储器泄露。
参考资料
《深入理解计算机系统》第9章虚拟存储器