系统中的进程共享CPU和主存资源。
虚拟存储器(VM):是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每一个进程提供了一个大的、一致的和私有的地址空间。
通过一个很清晰的机制,VM提供三个重要的能力:
l 它将主存看成是一个存储在磁盘上地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。
l 它为每个进程提供一致的地址空间,从而简化了存储器管理。
l 它保护了每个进程的地址空间不被其他进程破坏。
9.1、物理和虚拟寻址
主存被组织成一个由M个连续的字节大小的单元组成的数组。
每字节都有一个唯一的物理地址(Physical Address,PA)。第一个字节的地址为0,累加。
物理寻址(virtual addressing):CPU访问存储器的最自然的方式就是使用物理地址。
早期的PC、数字信号处理器、嵌入式微控制器、Cray超级计算机这样的系统仍然使用物理寻址。
虚拟寻址(virtual addressing)CPU通过生成一个虚拟地址来访问主存。
现在处理器使用虚拟寻址。
地址翻译(address translation):将一个虚拟地址转换为物理地址的任务。
地址翻译需要CPU和操作系统之间的紧密合作。
存储器管理单元(Memory Management Unit,MMU):CPU芯片上的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操作系统管理的。
9.2、地址空间
地址空间(address space):是一个非负整数地址的有序集合
如果地址空间中的整数是连续的,称为线性地址空间(linear address space)。
(1)虚拟地址空间:在一个带有虚拟存储器的系统中,CPU从一个又N=2n 个地址空间中生成的虚拟地址。
(2)物理地址空间:与系统中物理存储器的M个字节相对应。
主存中每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。
9.3、虚拟存储器作为缓存工具
虚拟存储器被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。
每字节都有一个唯一的虚拟地址。这个唯一的虚拟地址是作为到数组的索引的,磁盘上数组的内容被缓存在主存中。
虚拟页(Virtual Page,VP):VM系统通过将虚拟存储器分割为虚拟页,大小固定的块,大小为P字节。
物理页(Physical Page,PP,也称为page frame页帧):物理存储器被分割为物理页,大小也为P字节。
(1)DRAM缓存:表示虚拟存储器系统的缓存,它在主存中缓存虚拟页。
DRAM缓存总是使用写回,而不是直写。
(2)页表
页表(page table):将虚拟页映射到物理页。
每次地址翻译都会读取页表。操作系统负责维护页表的内容。
页表条目(page table entry,PTE):页表就是一个页表条目的数组。
(3)页命中
(4)缺页(page fault):DRAM缓存不命中。
(5)分配页面:操作系统分配一个新的虚拟存储器页。
(6)局部性:程序往往在一个较小的活动页面集合上工作,这个集合叫做工作集(workset).
9.4、虚拟存储器作为存储器管理的工具
操作系统为每个进程提供了一个独立的页表,因而也就是一个独立的虚拟地址空间。
多个虚拟页面可以映射到同一个共享物理页面上。
9.5、虚拟存储器作为存储器保护的工具
控制对存储器系统的访问,访问权限。
9.6、地址翻译
地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素之间的映射。
MMU利用页表实现映射。
CPU中的一个控制寄存器---页表基址寄存器(Page Table Base Register,PTBR):指向当前页表。
9.7、案例研究:Intel Core i7/Linux 存储器系统
9.8、存储器映射
存储器映射(memory mapping):通过将一个虚拟存储器区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟存储器区域的内容,这个过程叫做存储器映射。
虚拟存储器区域可以映射到两种类型的对象之一:
1)Unix文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分。如果区域比文件区大,这用零填充。
2)匿名文件:匿名文件时由内核创建的,包含的全是二进制零。故映射到匿名文件的区域中的页面也叫请求二进制零的页。
一种简单而高效的把程序和数据加载到存储器中的方法。
(1)再看共享对象
一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象。
共享对象:进程对这个区域的任何写操作,会反映在磁盘上的原始对象中。
私有对象:进程对这个区域的任何写操作,不会反映咋磁盘上的独享中。
私有的写时拷贝(copy-on-write):只要没有进程试图写他自己的私有区域,它们就可以继续共享物理存储器中对象的一个单独拷贝。然而,只要有一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障。
(2)再看fork函数
创建一个带有自己独立虚拟地址空间的新进程。 写时拷贝。
(3)再看execve函数
execve函数加载和执行程序。
(4)使用mmap函数的用户级存储器映射
mmap函数创建新的虚拟存储器区域,并将对象映射到这些区域中。
void *mmap(void *start, size_t length, intprot, int flags, int fd, off_t offset);
munmap函数删除虚拟存储器的区域。
int munmap(void *start, size_t length);
9.9、动态存储器分配
虽然可以使用低级的mmap和munmap 函数来创建和删除虚拟存储器的区域,但是C程序员:当运行时需要额外虚拟存储器时,用动态存储器分配器(dynamicmemory allocator)更方便,也有更好的可移植性。
堆(heap):动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的存储器片(chunk)。分配器有两种基本风格:
l 显式分配器(explicit allocator):要求应用显示地释放任何已分配的块。
C程序:调用malloc函数来分配一个块,调用free函数来释放一个块。
C++程序:new 和 delete 操作符分配和释放块。
l 隐式分配器(implicit allocator):要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。
隐式分配器也叫垃圾收集器(garbagecollector)。
垃圾收集(garbage collection):自动释放未使用的已分配的块的过程。
(1)malloc和free函数
C标准库提供了一个mallo程序包的显示分配器。程序通过调用malloc函数来从堆中分配块。
#include <stdlib.h>
void *malloc(size_t size);
malloc函数返回一个指针,指向大小为至少size字节的存储器块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。malloc函数不初始化它返回的存储器。
calloc函数是一个基于malloc的瘦包装函数,它将分配的存储器初始化为零。
realloc函数改变一个以前分配块的大小。
动态存储器分配器,例如malloc函数,可以通过使用mmap、munmap和sbrk函数,显示的分配和释放堆存储器。
程序通过调用free函数来释放已分配的堆块。
#include <stdlib.h>
void free(void *ptr);
ptr参数必须指向一个从malloc calloc realloc函数获得的已分配块的起始位置。如果不是,那么free的行为就是未定义的。
(2)为什么要使用动态存储器分配
程序使用动态存储器分配器的最重要的原因是:经常到程序实际运行时,才知道某些数据结构的大小。
(1)硬编码数组界限:
#define MAXN 15213
int array[NAXN];
对于拥有百万行代码和大量使用者的大型软件产品而言,会变成一场维护的噩梦。
(2)在运行时,在已知了N值之后,动态地分配这个数组:
使用这种方法,数组大小的最大值 就只由可用的虚拟存储器数量来限制了。
int *array, i, n;
n=getn(); //scanf(“%d”,&n);
array=(int*)malloc(n*sizeof(int));
for(i=o;i!=n;i++)
scanf(“%d”,&array[i]);
exit(0);
(3)分配器的要求和目标
要求:
l 处理任意请求序列
l 立即响应请求
l 只使用堆
l 对齐块
l 不修改已分配的块
目标:
l 最大化吞吐率
吞吐率:每个单位时间里完成的请求数。
l 最大化存储器利用率
天真的程序员经常不正确的假设虚拟存储器是一个无限的资源。
好的程序员知道虚拟存储器是一个有限的空间,必须高效地利用。
实际上,一个系统中被所有进程分配的虚拟存储器的全部数量是受磁盘上交换空间的数量限制的。
(4)碎片
碎片(fragmentation):造成堆利用率低的主要原因。
l 内部碎片:
l 外部碎片:
(5)实现问题
(6)隐式空闲链表
(7)放置已分配的块
(8)分割空闲快
(9)获取额外的堆存储器
(10)合并空闲块
(11)带边界标记的合并
(12)综合:实现一个简单的分配器
(13)显示空闲链表
(14)分离的空闲链表
分离存储(segregated storage):维护多个空闲链表,其中每个链表中的块有大致相等的大小。---一种流行的减少分配时间的方法。
Ø 简单分离存储
Ø 分离适配
Ø 伙伴系统
9.10、垃圾收集
垃圾收集器(garbage collector):一种动态存储分配器,它自动释放程序不再需要的已分配块。
垃圾(garbage):程序不再需要的已分配块。
垃圾收集(garbage collection):自动回收堆存储的过程。
在一个支持垃圾收集的系统中,应用显式分配堆块,但是从不显式地释放。
(1)垃圾收集器的基本知识
垃圾收集器将存储器视为一张有向可达图(reachabilitygraph)。
节点可达的:存在一条从任意根节点出发并到达p的有向路径。
不可达节点对应于垃圾。
ML和JAVA的垃圾收集器,对如何创建和使用指针有严格的控制,能够维护可达图的一种精确表示,因此能够回收所有垃圾。
C和C++的垃圾收集器是保守的垃圾收集器,不能维护可达图的精确表示。每个可达图块都被正确的标记为可达,而一些不可达节点却可能被错误的标记为可达。
(2)Mark&Sweep垃圾收集器
Mark&Sweep垃圾收集器:
Ø 标记阶段:
Ø 清除阶段:
(3)C程序的保守Mark&Sweep
C程序的Mark&Sweep收集器必须是保守的,根本原因是C语言不会用类型信息来标记存储器位置。
9.11、C程序中常见的与存储器有关的错误
(1)间接引用坏指针
在进程的虚拟地址空间中有较大的洞,没有映射到任何有意义的数据。
坏指针:
读:段异常
写:保护异常---覆盖存储器
(2)读未初始化的存储器
一个常见的错误就是假设堆存储器 被初始化为零。
正确的做法是显示地初始化。
(3)允许栈缓冲区溢出
缓冲区溢出错误(buffer overflow bug):如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误。
例如:gets()函数拷贝一个任意长度的串到缓冲区,就有缓冲区溢出错误。用fgets()函数可以纠正这个错误,它限制了输入串的大小。
(4)假设指针和它们指向的对象是相同大小的
int int*
(5)造成错位错误
错位错误(off-by-one):另一种覆盖错误。
例如:试图初始化具有n个元素的第(n+1)个元素,会覆盖数组后面的某个存储器。
(6)引用指针,而不是它所指向的对象
注意C操作符的优先级和结合性。区别 :*size *size+1 *(size+1)
(7)误解指针运算
指针的算术操作:p++
(8)引用不存在的变量
函数不能返回局部变量的指针或引用。
(9)引用空闲堆块中的数据
int*x=new int (5); delete x; 后再次引用 int *y=x; ---错误。
(10)引起存储器泄露
忘记了释放已分配的块。 new 之后忘记调用 delete;
9.12、小结