早期:程序员自己管理主存,通过分解程序并覆盖主存的方式执行程序。1961年,英国曼切斯特研究人员提出一种自动执行overlay的方式。
动机:把程序员从大量繁琐的存储管理工作中解放出来,使得程序员编程时不用管主存容量的大小
基本思想:把地址空间和主存容量的概念区分开来。程序员在一个虚拟地址空间里编写程序,而程序则在真正的物理内存中运行。由一个专门的机制实现地址空间和实际主存之间的映射,就是早期的分页管理机制。
当时的一种典型计算机,其指令中给出的主存地址为16位, 而主存容量只有4K字,程序员编写程序的空间(地址空间,可寻址空间)比执行程序的空间(主存容量)大得多。
分页的基本思想:
页表是内存中的一个数据结构,由内核维护,是每个进程上下文的一部分,每个进程都有自己的页表。
虚拟内存地址到物理地址之间的映射与主存到cache的映射原理相同(不完全相同,cache的映射方式是固定的,内存中的每一块只能映射到指定的行。如果所映射的行已满,那么即使cache中还有空间,也只能覆盖所映射的行),块内的地址不需要改变,页表描述的是虚拟页到物理页框之间的映射关系,只需要改变高位地址。
不需要将一个进程全部都装入内存,根据程序访问局部性可知:可把当前活跃的页面调入主存,其余留在磁盘上,采用 “按需调页 Demand Paging” 方式分配主存!这就是虚拟存储管理概念
通过 MMU(Memory management unit)把虚拟地址(Virtual Address, VA)转换为物理地址(Physical Address, PA),再由此进行实际的数据传输
虚拟存储技术的实质:
实际上可执行文件加载时不会真正从磁盘调入信息到主存, 只是生成一个初始的页表,将虚拟页和磁盘上的数据/代码建立对应关系,称为“映射”。而是采用按需调页的方法,将当前活跃的虚拟页调入内存。
虚拟存储器管理属于主存-磁盘层次,与“Cache–主存”层次相比:
因为缺页的开销比Cache缺失开销大的多!缺页时需要访问磁盘(约 几百万个时钟周期),而cache缺失时,访问主存仅需几十到几百个 时钟周期!因此,页命中率比cache命中率更重要!“大页面”和 “全相联”可提高页命中率。
作为内存管理工具,每个进程都有自己的虚拟地址空间,这样一来,对于进程来说,它们看到的就是简单的线性空间(但实际上在物理内存中可能是间隔、支离破碎的),每个虚拟页都可以被映射到任何的物理页上,如果两个进程间有共享的数据,那么直接指向同一个物理页即可(共享库的实现方式)。
简化链接:
简化加载:
Linux加载器只需要为可执行目标文件中的代码段和数据段分配虚拟页,然后在页表中将这些虚拟页设置为无效的(表示还未缓存),不需要将代码和数据复制到内存中,实际的加载工作会由操作系统自动地按需执行。
当访问某一虚拟地址时,发现其对应的PTE是无效的(该页还没有加载到内存中),则会发起缺页异常,通过缺页异常处理程序自动地将虚拟页加载到物理页中。这就是按需调用,所有的缓存机制都是这样的,一开始缓存为空,直到访问miss,才从上一级调用对应的块加载到缓存,只有我们访问的数据才会被加载。
**简化内存分配:**进行内存分配时,可以通过malloc
函数在物理内存中的任意位置进行创建,因为页表只需要让虚拟页指向该物理页,就能提供连续的虚拟地址抽象,让进程误以为是在连续的地址空间中进行操作的,由此简化了内存分配需要的工作。
**Access Rights (存取权限)**可能的取值有 R = Read-only, R/W = read/write, X = execute only,限制了对虚拟内存空间的访问权限
这里在每个PTE中引入四个字段:
SUP
:确定该虚拟内存空间的访问权限,确定是否需要内核模式才能访问READ
:确定该虚拟内存空间的读权限WRITE
:确定该虚拟内存空间的写权限EXEC
:确定该虚拟内存空间的执行权限MMU每次访问时都会检查这些位,如果有一条指令违背了这些许可条件,就会触发一个保护故障,Linux shell一般会将这种异常报告为段错误(Segment Fault)。
例如:
int sum(int a[ ], unsigned len){ int i,sum = 0; for (i = 0; i <= len–1; i++) sum += a[i]; return sum; }
当len=0时,此循环永远不会结束,程序不断地从内存中获取a[i],a[i]是一个虚拟地址,在虚拟地址转换成物理地址时,在虚拟内存空间中发生了访问违例。这种情况有可能是访问到了空洞的虚拟空间,也有可能访问到了内核。
在此例中,应该是访问到了虚拟内存空间中的内核区。因为a[i]是局部变量,分配在运行时栈中,而栈的上方就是内核区,数组不停地增长,一直访问到了内核区。
每个页表都有一个基地址寄存器CR3,描述页表的起始地址。基地址+页表的索引*页表表项的大小=该页表表项的首地址,在此处取V
虚拟页与主存页框之间采用全相联方式进行映射,高位地址是索引;全相联 Cache高位地址是Tag。实际上Tag和索引是相同的:
而二者的区别在于使用的方式:
造成这个差异的原因是:主存-cache的映射时,Tag在cache中,所以需要一个个比较。而逻辑地址-主存地址的映射时,有一个中间的页表,虚拟内存空间中的每一页都有一个页表项,从上到下顺序排列,描述了该进程的虚拟地址空间中所有的虚页到内存中的实页的映射,所以可以使用索引的方式。
MMU负责地址翻译和访问权限检查,加上MMU的完整过程:
可以发现,每次CPU将一个虚拟地址发送给MMU
时,MMU
都会将需要的PTE
物理地址发送给高速缓存/内存来获得PTE
,所以我们在MMU
中引入了一个保存最近使用的PTE
(页表项)缓存,称为翻译后备缓冲器(Translationi Lookaside Buffer,TLB),缓存中放页表的一部分,即当前正在访问的那几个页的页表表项。如果没有TLB,执行一条指令要访问多次主存中的页表进行地址转换(取指令、取操作数)
TLB通常是多路组相联,TLB与页表的关系相当于cache与内存的关系,TLB是页表的子集,每个表项上有Tag(就是虚页的索引,也就是虚拟地址去掉页内的偏移地址,和组号(如果有的话)剩下的高位)。
使用TLB将虚拟地址转换成物理主存地址:TLB对VPN进行分解,得到index和Tag,根据index确定所在的高速缓存组,然后在高速缓存组中依次比较各个高速缓存行的标记是否和Tag相同:
Tag相当于是HashCode,而索引相当于是数组索引。
在一个32位地址空间中,每个页面大小为4KB,则一共需要 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xkOKYuwA-1628237878286)(https://www.zhihu.com/equation?tex=2%5E%7B32%7D%2F2%5E%7B2%2B10%7D%3D2%5E%7B20%7D%3D1M)] 个页面,假设每个PTE大小为4字节,则页表总共为4MB。当使用一级页表时,需要始终在内存中保存着4MB大小的页表,我们这里可以使用多级页表来压缩内存中保存的页表内容。
如果使用一级页表,每个进程的虚拟地址空间中的每个虚拟页都需要一个页表条目,不管是否使用过这个页面。然而一个进程的虚存中的绝大多数页面都不会被使用。
首先,我们这里有1M个虚拟页,将连续的1024个虚拟页当成一个片(chunk),一级页表就负责指向每个片对应的二级页表,则一级页表需要 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WA8gSoQ0-1628237878306)(https://www.zhihu.com/equation?tex=2{20}页%2F2{10}片%3D1K)] 个PTE,每个PTE4字节,则一共需要4KB大小的一级页表。**注意:**这里只有当片中至少一个页被分配了才会指向对应的二级页表,否则为NULL。而二级页表就类似于我们之前的页表结构,这里只需要负责一个片的虚拟页,则每个二级页表为4KB。
当一级页表的某个PTE为NULL时,表示该片不存在被分配的虚拟页,所以就可以去掉对应的二级页表。并且在内存中只保存一级页表和较常使用的二级页表,极大减小了内存的压力,而其他的二级页表按需创建调入调出。
CPU拿到的是虚拟地址,根据虚页号(虚拟地址去掉页的大小后剩下的高位)到TLB中与每一个表项的Tag相匹配
慢表到TLB的映射是组相联映射(地址分为三部分:低位是页内地址,中间是组号,高位是Tag)。TLB的映射方式是直接相联映射(地址分为两部分:低位是页内地址,高位是索引(组号和Tag合在一起))。
为避免多道程序相互干扰,防止某程序出错而破坏其他程序的正确性或不合法地访问其他程序或数据区,应对每个程序进行存储保护
以下情况会发生存储保护错误:
最基本的保护措施:规定各道程序(用户进程)只能访问属于自己所在的存储区和共享区
运行模式:
管理模式(supervisor Mode):
执行系统程序(内核)时处理器所处的模式称为管理模式,或称为管理程序状态,简称为管态或管理态、核心态、内核态。
用户模式(User Mode):
CPU执行非操作系统的用户程序时,处理器所处的模式是用户模式,或称为用户状态、目标程序状态,简称为目态或用户态。