- 虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供一个大的、一致的私有的地址空间;
- 主存是磁盘的高速缓存、主存中只保留活动区域、并且根据需要,在磁盘和主存之间来回传送数据;
- 每个进程提供一致的地址空间,简化了存储器的管理;
- 保护每个进程的地址空间,保护不被破坏;
9.1 物理和虚拟寻址
- 内存中每个字节都有一个物理地址
- CPU利用物理地址进行访问,是物理寻址;
- CPU利用虚拟地址,在地址被传送到存储器之前,先进行地址翻译(存储器管理单元(MMU)和系统的合作),在访问物理主存;
9.2 地址空间
- 地址空间:非负整数地址的有序结合;整数是连续的,我们称为 线型地址空间
- 虚拟地址空间:这是CPU生成的地址空间;
- 物理地址空间:对应于物理存储器的M个字节;
- 物理地址可以对应多个虚拟地址(如共享内存);
9.3 虚拟存储器作为缓存工具
- 虚拟存储器:被组织为一个由存放在磁盘上N个连续字节大小的数组;
- 虚拟页:将虚拟存储器分割为虚拟页,以此为单位用来与物理内存交换;
- 未分配的:系统还未分配的页,在页表中体现为,既不指向物理存储器,也不指向虚拟页
- 缓存的:当前虚拟页已被加载到物理内存中,页表体现为指向物理内存;
- 未缓存的:当前虚拟页未被加载到物理内存中,页表体现为指向虚拟页(磁盘);
- 物理页:物理内存被分割为物理页;
- DRAM总是使用写回,而不是直写;
9.3.2 页表
- 页表将虚拟页映射到物理页,页表常驻在内存中;
- 每次地址翻译时,都将读取页表,这需要系统、内存管理单元中的地址翻译硬件和页表的共同作用;
- 系统负责维护页表,以数据页的相互传送;
- 页表是一个条目的数组,虚拟页页表中都有一个条目,具有固定的偏移;
9.3.3 页命中
当CPU通过虚拟地址翻译后,对应的虚拟页已缓存到内存中时,称为页命中;
9.3.4 缺页
- DMA缓存不命中称为缺页;
- 当CPU的虚拟地址访问页表发现虚拟页并未缓存时(有效为未标记),触发缺页异常;
- 缺页异常调用缺页异常处理程序;程序会选择一个牺牲页,如果牺牲页发生过修改,就写回;并修改牺牲页的条目,表明此虚拟页不在存储器中;
- 将需要缓存的虚拟页加载到存储器中,跟新对应条目,表明此虚拟页已缓存;之后重新启动导致缺页的指令;
9.3.5 分配页面
在磁盘上创建空间,并更新对应页表目,指向新创建的虚拟页(未被缓存);
9.3.6 页的局部性
- 局部性保证了代码往往在较小的活动页面集合上工作,这个集合叫做工作集;
- 当工作集的大小超过物理存储器时,会产生颠簸,频繁的发生页面交换;
9.4 虚拟存储器作为存储器的管理工具
- 操作系统为每个进程提供一个独立的页表,因此每个进程拥有独立的地址空间;
- 按需调度页面和独立的虚拟地址空间
- 简化链接:独立的地址空间允许每个进程的存储器映像使用相同的基本格式 ;这样简化了链接器的设计;可执行文件独立于最终的物理地址;
- 简化加载:加载时只需要将条目指向目标文件的对应位置;在实际中,加载器从不拷贝任何数据到存储器,而是按需交换数据页;
- 简化共享:只需要将页表指向相同的物理内存页;
- 简化存储器分配:当用户程序需要堆内存时,系统分配K个连续虚拟存储器页面,但由于页表的存在,实际的物理存储器中并不存在;
9.5虚拟存储器作为存储器的保护工具
一个用户进程不能:
- 修改其只读文本段
- 内核代码和数据结构
- 其他进程的存储器
共享的物理页面,除非显式允许(共享内存)
解决办法,在页表中添加标志位:
9.6 地址翻译
地址翻译是N元素的虚拟地址空间和M元素的物理地址空间之间的映射:
虚拟地址-》物理地址的大致翻译原理:
- 虚拟页偏移量(VPO)和物理页偏移量(PPO)是相同的,因为虚拟页和物理页大小相同,可以直接映射;
物理地址翻译过程:
9.6.1 结合高速缓存和虚拟存储器
- 当存在SRAM和虚拟存储器时,大多数系统使用物理地址访问SRAM(SRAM相当于主存,因此也可以采用虚拟地址的方式,但大多数系统没有采用);
- 高速缓存无需处理保护的问题,因为这是在地址翻译的过程中解决的;
9.6.2 利用TLB加速地址翻译
- 在MMU中包括了一个关于PTE的小缓存,翻译后备缓冲器(Translation Lookaside Buffer,TLB);
- TLB每一行都保存着一个PTE条目;
- 访问TLB的标记和索引都是从虚拟地址中的VPN提取的;
- 应为所有的操作都是在MMU中进行的,速度非常快;
9.6.3 多级页表
-当使用一级页表时,对于32位的虚拟地址,页表就占就有4MB的空间,64位占有跟多;
- 使用多级页表可以解决这样的问题;
- 一级页表条目指向二级页表,二级页表条目指向虚拟存储器;
- 当一级页表有些条目为空时,二级页表不需要存在,大大节省了空间
- 只有一个页表总是存储在存储器中,系统可以在需要时创建,调入,调出二级页表;只有经常使用的二级页表存在在主存中;
#### 9.7.2 Linux虚拟存储器系统
- Linux为每个进程维护了一个单独的虚拟地址空间
Linux也将一组连续的虚拟页面映射到一组连续的物理页面,大小为DRAM,方便系统访问任何位置;(因为用户空间中没有映射所有的物理页,所以不能通过用户空间访问所有物理页)
完整的虚拟地址空间:
- Linux将虚拟存储器区域
- Linux将虚拟存储器组织成一些区域,也叫段。
- 一个区域或者段,就是已经分配的虚拟存储器的连续片;
- 每个存在的虚拟页都是在某个区域中的,不属于某个区域的虚拟页是不存在的;
- 区域的存在,使得虚拟地址空间存在间隙,内核也不用记录不存在的虚拟页;(页表记录)
- Linux缺页处理
当MMU试图翻译一个虚拟地址时,触发缺页异常,处理程序会执行下面的步骤:
- 虚拟地址是否合法,地址是否在某个区域中;
- 访问是否合法,即是否有权限读、写或者执行的权限;
- 到此处就是合法的地址和访问权限,异常是由于缺页造成的;交换页之后,再次执行这个虚拟地址的翻译
- Linux将虚拟存储器区域
9.8 存储器映射
- 将虚拟存储器区域与磁盘上的一个对象关联起来,以初始化这个虚拟存储器区域的内容,此称为存储器映射
- 虚拟存储器可以映射两种文件:
- 普通文件:将文件分成页大小的片,将页表指向这些片,但不缓存这些片,采用按需调度的策略,直到CPU第一次引用这个页面;如果区域要比文件区大,就用零填充;
- 匿名文件:匿名文件由内核创建,包含的全是二进制零;过程是:CPU在物理存储器找一个合适的牺牲页面(修改过则写回),用零覆盖,更新页表,将此页标记为缓存在存储器中;磁盘和存储器没有数据交换;
9.8.1 在看共享对象
- 一个对象可以被映射到虚拟存储器的一个区域,要么作为一个共享对象,要么私有对象;
- 当作为共享对象时,任何修改对其他进程是可见的,也会反映到磁盘的文件上;
- 映射到私有区域的对象,修改对其他进程是不可见的,也不会影响磁盘上的文件;
- 私有对象使用了一种写时拷贝技术
- 写时拷贝只会拷贝要修改的页,使页表条目指向新拷贝的物理页
- 写时拷贝只会拷贝要修改的页,使页表条目指向新拷贝的物理页
9.8.2 再看fork函数
- 当调用fork时,内核为新进程创建各种数据结构,并分配一个唯一的PID;
- 给新进程创建虚拟存储器,复制当前进程的页表,mm_struct和区域结构;并将两个进程的页面都标记为只读,区域结构标记为写时拷贝;
9.8.3 再看execve函数
利用execve函数加载程序有以下几步:
- 删除已经存在的用户区域结构:删除当前进程中用户空间中的区域结构;
- 映射私有区域:为新程序的文本、数据、bss和栈区创建新的区域结构;所有的这些区域都是私有的,写时拷贝的;
- 映射共享区域:将共享库动态链接到此程序,然后映射到用户虚拟地址空间的共享区域;
- 设置程序计数器: 设置当前进程的上下文程序计数器指向文本区域的入口点;
9.8.4 使用mmap函数的用户级存储器映射
mmap函数创建新的虚拟存储器区域,并将对象映射到这些区域中;
9.9 动态存储器分配
- 动态存储器分配器维护虚拟存储器中的堆区域;
- 对于每一个进程,内核维护着一个变量brk,指向堆的顶部
- 分配器将堆视为一组不同大小的块(就是malloc分配的对齐的空间),而每个块就是连续的虚拟存储器片,要么已分配,要么空闲;
- 分配时,都要求显式执行;
- 已分配的块的释放方式:
- 显式分配器
- 要求引用显式的释放已分配的块
- 例如malloc&free new&delete
- 隐式分配器
- 当分配器检测到不再使用的已分配的块时,就释放这个块;
- 显式分配器
9.9.1 malloc和free函数
- 返回块大小至少为申请的大小,可能为了能够保存各种数据类型,保证内存对齐
- malloc从初始化分配的块,可以使用calloc函数,此函数将块初始化为0;
- 改变已分配块的小小,realloc函数;
- 可以使用mmap和mumap函数显式分配和释放堆存储器
- 也可以使用sbrk函数:
通过将内核的brk指针增加incr来扩展或者收缩;
程序通过调用free来释放以分配的块:
- ptr必须指向一个从malloc、calloc、realloc获得的已分配块的起始位置;
9.9.2 为何要使用动态存储器分配
- 程序经常在运行时才知道某些数据结构的实际地址;
9.9.3 分配器的要求和目标
- 处理任意的请求序列
- 一个应用可以有任意的分配请求和分配请求序列
- 每个释放请求必须对应一个通过分配请求的块;
- 立即响应请求
- 分配器必须立即响应请求,不可为提高性能而重新排列或者缓冲请求
- 只使用堆
- 任何非标量数据结构都必须都必须保存在堆中;
- 对齐块
- 对齐块,保证可以存储任何类型的数据对象;
不修改已经分配的块
- 分配器只能操作已分配的块,一旦块被分配,就不可以移动或者修改他;
9.9.4 碎片
虽然有未使用的存储器,但不能用来满足分配请求,这些存储空间被称为碎片;
碎片的两种形式:- 内部碎片
- 发生在分配的块比有效载荷大时发生的;比如为了对齐;
外部碎片
- 当空闲块合并时满足请求,但没有一个单独的块满足请求;
9.9.6隐式空闲链表
一个简单块的结构:
- 由于双字对齐,所以地址前3位总是0,用来存放标记;
隐式空闲链表结构:
- 空闲块是通过头部中的大小字段隐含的链接着,所以称为隐式空闲链表;
- 最后为一个结束块,大小0,已分配;
9.9.7 放置已分配的块
当请求一个k字节的块时,结果是由放置策略决定的,常用的放置策略:
- 首次适配
- 下一次适配:每一次搜索都是从上一次结束的地方开始
- 最佳适配:大小最合适
9.9.8 分割空闲块
- 匹配较好时,只会产生内部碎片;
- 匹配较差时,一部分变成分配块,一部分变成空闲块;
9.9.9 获取额外的堆存储器
当存储器不能找到合适的块时:
- 合并相邻的空闲块
- 当还不满足时,分配器调用sbrk函数,申请额外的堆存储器,插入链表中;
9.9.10 合并空闲块
- 假碎片:有两个相邻的空闲块,单独时不能满足分配请求,合并则可以,则这两个空闲块形成假空闲块
合并策略:
- 立即合并:释放块时,合并相邻的块;
- 推迟合并:推迟到某个时刻,例如没有合适的空闲块时;
9.9.11 带边界标记的合并
- 在块的尾部添加一个标记,那么释放的当前块就可以用常数时间去查询前面一个块是否空闲,而链表又需要重新遍历到当前块的前一个块;
9.9.13 显式空闲链表
显式空闲链表显式的将指针记录在空闲块中:
- 使用双向链表而不是隐式空闲链表,使得空闲块的寻找时间从总块数的线型时间,下降到空闲块的线性时间;
- 显式链表的缺点是空闲块的空间必须足够大,用以存放