CMU15-213学习笔记(五)virtual memory虚拟内存
分页存储管理
早期:程序员自己管理主存,通过分解程序并覆盖主存的方式执行程序。1961年,英国曼切斯特研究人员提出一种自动执行overlay的方式。
动机:把程序员从大量繁琐的存储管理工作中解放出来,使得程序员编程时不用管主存容量的大小
基本思想:把地址空间和主存容量的概念区分开来。程序员在一个虚拟地址空间里编写程序,而程序则在真正的物理内存中运行。由一个专门的机制实现地址空间和实际主存之间的映射,就是早期的分页管理机制。
当时的一种典型计算机,其指令中给出的主存地址为16位, 而主存容量只有4K字,程序员编写程序的空间(地址空间,可寻址空间)比执行程序的空间(主存容量)大得多。
分页的基本思想:
- 内存被分成固定长且比较小的存储块(页框、实页、物理页)
- 每个进程也被划分成与页框长度相同的固定长的程序块(页、虚页、逻辑页)
- 程序块可装到存储器中可用的存储块中
- 无需用连续页框来存放一个进程
- 操作系统为每个进程生成一个页表 ,通过**页表(page table)**实现逻辑地址向物理地址转换(Address Mapping )
页表是内存中的一个数据结构,由内核维护,是每个进程上下文的一部分,每个进程都有自己的页表。
虚拟内存地址到物理地址之间的映射与主存到cache的映射原理相同(不完全相同,cache的映射方式是固定的,内存中的每一块只能映射到指定的行。如果所映射的行已满,那么即使cache中还有空间,也只能覆盖所映射的行),块内的地址不需要改变,页表描述的是虚拟页到物理页框之间的映射关系,只需要改变高位地址。
不需要将一个进程全部都装入内存,根据程序访问局部性可知:可把当前活跃的页面调入主存,其余留在磁盘上,采用 “按需调页 Demand Paging” 方式分配主存!这就是虚拟存储管理概念
通过 MMU(Memory management unit)把虚拟地址(Virtual Address, VA)转换为物理地址(Physical Address, PA),再由此进行实际的数据传输
虚拟存储技术的实质:
- 程序员在比实际主存空间大得多的逻辑地址空间中编写程序 (程序中代码和数据的地址、引用的地址都是按照虚拟内存空间来安排的)
- CPU执行程序时,不会真正从磁盘调入信息到主存,只是生成一个初始的页表,将被执行程序的虚拟页和磁盘上的数据/代码建立对应关系。CPU按照该进程的逻辑地址来执行代码,通过该进程的页表,将逻辑地址转换成实际内存地址,把当前需要的程序段和相应的数据块调入主存,其他暂不用的部分存放在磁盘上
- 每个进程都有页表,反映了每一个虚页和物理内存之间的关系
- 指令执行时,通过硬件将逻辑地址(也称虚拟地址或虚地址)转化为物理地址(也称主存地址或实地址)
实际上可执行文件加载时不会真正从磁盘调入信息到主存, 只是生成一个初始的页表,将虚拟页和磁盘上的数据/代码建立对应关系,称为“映射”。而是采用按需调页的方法,将当前活跃的虚拟页调入内存。
虚拟存储器管理属于主存-磁盘层次,与“Cache–主存”层次相比:
- 页大小(2KB~64KB),比Cache中的Block大得多。
- cache-主存层次多采用组相联映射(内存中的每一块只能映射到指定的行。如果所映射的行已满,那么即使cache中还有空间,也只能覆盖所映射的行),而页表采用全相联映射(主存只要有空的位置就可以放page)
因为缺页的开销比Cache缺失开销大的多!缺页时需要访问磁盘(约 几百万个时钟周期),而cache缺失时,访问主存仅需几十到几百个 时钟周期!因此,页命中率比cache命中率更重要!“大页面”和 “全相联”可提高页命中率。
- 通过软件来处理“缺页”!缺页时需要访问磁盘(约几百万个时钟周期),慢!不能用硬件实现。
- 采用Write Back写策略!避免频繁的慢速磁盘访问操作。
- Write-through: 命中后更新缓存,同时写入到内存中
- Write-back: 直到这个缓存需要被置换出去,才写入到内存中
- 地址转换用硬件实现!加快指令执行
- 每个进程有一个页表,其中有装入位、修改(Dirt)位、替换控制位、访问权限位、禁止缓存位、实页号。
- 一个页表的项数(即有多少虚页)理论上取决于虚拟地址空间的大小。例如:32位机器的虚拟地址空间为232,一页的大小为4KB,所以一共有220个虚页。而页表中每一个表项的大小为4B,所以页表的大小为4MB。页表存在虚拟内存空间的内核区中。
- 各进程有相同的虚拟空间,故理论上每个进程的页表大小相同。实际大小看具体实现方式,如“空洞”页面如何处理等
- 未分配页:进程的虚拟地址空间中“空洞”对应的页(如VP0、VP4),存放位置为null,装入为0
- 已分配的缓存页:有内容对应的已装入主存的页(如VP1、VP2、VP5等),装入位为1,存放位置为主存实页号
- 已分配的未缓存页:有内容对应但未装入主存的页(如VP3、VP6),装入位为0,存放位置为磁盘的实页号
作为内存管理工具,每个进程都有自己的虚拟地址空间,这样一来,对于进程来说,它们看到的就是简单的线性空间(但实际上在物理内存中可能是间隔、支离破碎的),每个虚拟页都可以被映射到任何的物理页上,如果两个进程间有共享的数据,那么直接指向同一个物理页即可(共享库的实现方式)。
简化链接和加载
-
简化链接:
- 如果没有虚拟地址空间,链接器不知道程序将来会被加载到内存中的什么地方,无法确定符号引用的地址,也就无法重定位。
- 使用虚拟内存空间,每个程序都有相同的虚拟地址空间,所有的代码、数据、堆都起始于相同的地址,所以链接器生成可执行目标文件确定内存地址时,无需考虑当前物理内存的状态,可以根据我们预定义的内存模型来分配虚拟内存地址。CPU通过进程的页表来确定最终的物理地址。
-
简化加载:
-
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
- 如果V=1,那么该页在主存,把PF(page frame number)实页号取出,和页内偏移量拼起来,得到该页的物理地址。
- 当V=0时,发生缺页,即我要找的page还没有装入主存。页表条目包含指向该页面在磁盘上的位置的指针,将其视为逻辑块号,可在磁盘上找到该页面,从磁盘读到内存,若内存没有空间,则还要从内存选择一页替换到磁盘上,替换算法类似于Cache,采用回写法,淘汰时,根据“dirty” 位确定是否要写磁盘。如果主存中被淘汰的页被修改过,还需要将修改的结果写回磁盘。
- 如果V=1,但是对该页所进行的操作不符合存取权限(Access Right)。比如:这个页的访问权限是只读,但是我要进行写操作,此时就会发生保护违例,访问越权( protection_violation_fault )。
虚拟页与主存页框之间采用全相联方式进行映射,高位地址是索引;全相联 Cache高位地址是Tag。实际上Tag和索引是相同的:
- Tag是内存地址的高位(内存地址去掉缓存块的大小)
- 索引是逻辑地址的高位(逻辑地址去掉页的大小)
而二者的区别在于使用的方式:
- Tag:内存地址的高位(即Tag)与cache中每一块的Tag对比,如果有相同的Tag就是命中,如果没有相同的Tag就从内存中调出该块
- 索引:通过逻辑地址的高位(即索引)在页表中找到对应的表项,如果V=1就是命中,找到该逻辑地址在内存中的实页。如果V=0就是没命中,发生缺页,从磁盘中调出对应的页到内存中。
造成这个差异的原因是:主存-cache的映射时,Tag在cache中,所以需要一个个比较。而逻辑地址-主存地址的映射时,有一个中间的页表,虚拟内存空间中的每一页都有一个页表项,从上到下顺序排列,描述了该进程的虚拟地址空间中所有的虚页到内存中的实页的映射,所以可以使用索引的方式。
MMU负责地址翻译和访问权限检查,加上MMU的完整过程:
- CPU生成一个虚拟地址,将它发送给MMU
- MMU根据虚拟地址(VA)获得虚拟页号(VPN),然后通过页表基址寄存器(PTBR)确定相应的页表表项(PTE)所在的物理内存地址(PTEA)(因为页表保存在物理内存中),然后将物理内存地址发送给物理内存
- 物理内存根据页表表项的内存地址将对应的页表表项(PTE)发送给MMU,其中页表表项只包含物理页号(PPN)(因为数据在页内偏移量相同)
- MMU将物理页号和页内偏移量拼接起来,就可以得到虚拟地址对应的物理地址(PA)。然后MMU再将物理地址发送给物理内存
- 物理内存根据物理地址将数据发送给处理器
快表TLB
可以发现,每次CPU将一个虚拟地址发送给MMU
时,MMU
都会将需要的PTE
物理地址发送给高速缓存/内存来获得PTE
,所以我们在MMU
中引入了一个保存最近使用的PTE
(页表项)缓存,称为翻译后备缓冲器(Translationi Lookaside Buffer,TLB),缓存中放页表的一部分,即当前正在访问的那几个页的页表表项。如果没有TLB,执行一条指令要访问多次主存中的页表进行地址转换(取指令、取操作数)
TLB通常是多路组相联,TLB与页表的关系相当于cache与内存的关系,TLB是页表的子集,每个表项上有Tag(就是虚页的索引,也就是虚拟地址去掉页内的偏移地址,和组号(如果有的话)剩下的高位)。
- TLB全相联时,没有index,只有Tag,虚页号需与每个Tag比较;
- TLB组相联时,则虚页号高位为Tag,低位为index,用作组索引。
使用TLB将虚拟地址转换成物理主存地址:TLB对VPN进行分解,得到index和Tag,根据index确定所在的高速缓存组,然后在高速缓存组中依次比较各个高速缓存行的标记是否和Tag相同:
- 如果有相同的Tag,并且V=1,那么TLB返回对应的表项,MMU取出物理页号,与页内地址拼接,就可生成物理地址。
- 如果没有相同的Tag或者V=0,就要访问主存的慢表。此时VPN(Tag+index)变成了访问慢表的索引,MMU根据VPN和PTBR(页表基址寄存器)得到PTE对应的表项的地址PTEA,将其发送给缓存/内存
- 如果V=1且Access Right允许,内存/缓存将对应的PTE发送给MMU和TLB
- TLB会根据VPN将PTE保存在合适的位置
- MMU取出物理页号,与页内地址拼接,就可生成物理地址
- 如果V=0,则说明对应的页还没有调入主存,用户程序无法访问磁盘,此时需要靠操作系统从磁盘中调入
- 如果V=1且Access Right允许,内存/缓存将对应的PTE发送给MMU和TLB
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相匹配
- 如果匹配成功并且V=1,那么命中,即可获得虚拟地址对应的物理内存地址。拿着这个内存地址到cache中寻找,如果找到就将内容送回CPU。这是最好的情况,无需访问主存
- 如果没有相同的Tag,就要访问主存中的页表。使用索引在慢表中找到对应虚页的表项:
- 如果V=1,说明该虚页的代码和数据已经被加载到了内存中,这段虚拟内存空间与物理内存空间已经建立了映射。直接将页表中的这个表项调入TLB中,并且将虚拟地址转换成物理地址送给CPU。CPU再拿着这个内存地址到cache中寻找。
- 如果V=0,说明该虚页还没有被加载到内存中,操作系统到磁盘中读出一页到内存中,建立虚页到内存的映射关系,修改慢表,将这个表项调入TLB中。操作系统读取的过程在笔记六中描述。
慢表到TLB的映射是组相联映射(地址分为三部分:低位是页内地址,中间是组号,高位是Tag)。TLB的映射方式是直接相联映射(地址分为两部分:低位是页内地址,高位是索引(组号和Tag合在一起))。
存储保护
为避免多道程序相互干扰,防止某程序出错而破坏其他程序的正确性或不合法地访问其他程序或数据区,应对每个程序进行存储保护
以下情况会发生存储保护错误:
- 地址越界(转换得到的物理地址不属于可访问范围)
- 访问越权(访问操作与所拥有的访问权限不符) 。页表中设定访问(存取)权限
最基本的保护措施:规定各道程序(用户进程)只能访问属于自己所在的存储区和共享区
- 对于属自己存储区的信息:可读可写,只读/只可执行
- 对共享区或已获授权的其他用户信息:可读不可写
- 对未获授权的信息(如OS内核、页表等):不可访问
运行模式:
-
管理模式(supervisor Mode):
执行系统程序(内核)时处理器所处的模式称为管理模式,或称为管理程序状态,简称为管态或管理态、核心态、内核态。
-
用户模式(User Mode):
CPU执行非操作系统的用户程序时,处理器所处的模式是用户模式,或称为用户状态、目标程序状态,简称为目态或用户态。