目录
3.1 Dynamic Relocation (base and bounds)
一、概述
OSTEP memory virtualization相关章节笔记,主要为了理清虚拟内存的为什么是现在这样,备忘。
二、虚拟内存的出现
- 起初,OS和一个程序分别放置在物理内存中,OS提供访问资源的接口。
- 由于机器价格昂贵,人们希望共享机器资源以节约成本,一个要求是计算机能运行多个程序,多道程序(multiprogramming)在这个时期出现,我们知道,进程等待I/O很耗时,如果在进程等待I/O时机将cpu切换出去运行另一个进程,避免了等待的同时也提高了cpu的使用率。
- 之后,随着对计算机更多的要求出现了time sharing,其针对多个交互式用户产生,基本原理是,每个用户运行一段时间,在这个时间内拥有计算机的全部资源,运行一段时间后进行switch,使其他进程运行。
- time sharing要实现对资源的完全控制,如在进程运行时是拥有整个物理内存的。实现time sharing的一种方法是运行一段时间后,将内存内容全部保存到磁盘上,当再次轮到自己,再将内存的内容从磁盘恢复到内存中。 这个实现方法简单,但是效率很低——保存整个内存的内容是非常耗时的,远远不如保存通用寄存器和PC那样快——这是不可接受的。因此希望再进程切换时不要将内存全部都保存到磁盘上而是保留再内存中。
- 当然4中的方法就做不到完全进程在其运行的过程中完全拥有内存了。做不到资源隔离的后果很严重,试想一个进程运行中修改了其他进程的数据,会造成很严重的后果!
- 当然,5中的保护问题可以通过标识访问权限的机制解决,但多个进程同时在物理空间中还会有加载和重定位等难题。对这个问题一个比较好的解决方法是引入地址空间(address space)的概念,地址空间是以进程的角度来看系统中的内存。在地址空间有code,heap,stack。在地址空间的基础上引入内存的虚拟化,如果进程对系统内存的角度(进程地址空间)是一致的,我们只需要建立物理地址空间和虚拟地址空间的映射就可以了,每个进程看到的地址都是虚拟的,运行中看到的是整个虚拟地址空间。进程对地址的视图一致,也使重定位得以简化。
三、虚拟内存发展和演进
首先要明确,程序的地址都是虚拟地址,我们要做的就是在保证在上述约束条件下,将虚拟地址转化为物理地址,需要考虑以下指标:
- 透明:进程不感知其看到的内存是假的。
- 效率:引入该机制对系统负担尽可能小,在可接受的范围内。
- 保护:protection,进程间的资源隔
为了使虚拟地址转换物理地址更有效率,一般需要硬件的支持,这称为hardware-based address translation,简称address translation。当然,只靠硬件支持还是不够的,还需要OS的配合。
先来看一条指令有关于内存存取的操作:
128: movl 0x0(%ebx), %eax ;load 0+ebx into eax
流水线中相关的步骤是fetch和execution,针对上面的指令,fetch时要先将128这个虚拟地址翻译成对应的物理地址,excution时要将%ebx中的虚拟地址翻译成物理地址。
硬件提供地址翻译的部件称为MMU,位于CPU中。
3.1 Dynamic Relocation (base and bounds)
先来看一种最简单的硬件翻译的方式Dynamic Relocation ,该方法引入base和bound寄存器,分别代表物理地址的基址和允许访问物理地址的范围。
硬件通过下面的方式进行翻译:
- PhyAddr = base + VirtAddr
如果PhyAddr的值超过了bound,那么硬件raise exception,产生protection fault
通过上面的流程总结一下硬件提供的功能:
MMU {
base
bound //提供base和bound寄存器
SetBase/SetBound //提供设置base和bound寄存器的方法。必须在privileged mode下
RaiseExceptioin //提供产生异常的能力,如protection fault
AddressTranlation //提供将虚拟地址转换为物理地址的能力
}
OS需要配合硬件实现地址转换而实现的功能有:
- 物理内存管理(Memory management):当一个进程运行时,要为其分配物理地址,OS要对系统的物理内存进行管理(freelist),如在进程初始化时分配一个物理地址。在进程结束时要回收分配的物理地址
- 在进行context switch时要增加额外的流程:在目前的情况下要将base/bound save到PCB中,新进程根据PCB restore base/bound寄存器——每个进程的base/bound寄存器都是不同的,这样才能隔离资源
- Exception handling:必须能够处理MMU RaiseException
下面的时序图很好的说明了这个过程:
3.2 segmentation
Dynamic Relocation的方法可以满足2.1中7对应的要求,但是也有它的问题:程序可能只使用很少的空间,但是Dynamic Relocation每次需要分配的物理地址大小是整个进程地址空间大小,这显然会造成地址浪费——Dynamic Relocation会导致内部碎片(internal fragmentation)。
我们可以使用分段来解决这个问题。假设使用三个段(code, heap, stack)对应一个进程,那么可以根据实际使用的情况,大大减少实际分配的内存,实现的方法可以从base-and-bounds改进——可以实现多个base/bound。这时候需要区分地址属于哪个段,所以地址使用逻辑地址即段:偏移的形式使用下面的逻辑操作:
1 // get top 2 bits of 14-bit VA
2 Segment = (VirtualAddress & SEG_MASK) >> SEG_SHIFT
3 // now get offset
4 Offset = VirtualAddress & OFFSET_MASK
5 if (Offset >= Bounds[Segment])
6 RaiseException(PROTECTION_FAULT)
7 else
8 PhysAddr = Base[Segment] + Offset
9 Register = AccessMemory(PhysAddr)
分段的特性:、
- 内存增长的方向不一样(upwards/downwards)
- 可以为每个段增加R/W权限
- code sharing (通过设置段属性read-only)、
由此可以推广增加段的个数(fine-grained),使用多个段管理内存需要更多的寄存器。
分段解决了Dynamic Relocation内存管理内部碎片的问题,但同时也引入的新的问题
- 在context switch时,需要save/store段寄存器,段越多开销越大。
- 每个进程若干段大小不一致,一个是管理起来很复杂,另一个是可能会产生外部碎片。
3.3 paging
分段仍是一种粗粒度的内存管理方式,除了会产生外部碎片,当一个段很大但是很稀疏的时候,也会产生分配物理空间浪费的情况。那么我们考虑一种更细粒度的管理方式:管理固定大小的小块内存,称为分页。
将物理内存看做固定大小的小块(称为页)的数组,数组的下标称为PFN(page frame number),数组元素大小为一个page,每个物理地址向下面这样:
- 虚拟地址转换为物理地址就是查找VPN->PFN的映射关系!
存储VPN<->PFN对应关系的表称为页表(page table),页表是以VPN为索引的页表项(page table entry)的数组。一个页表项包含PFN和指示标志。
PTE中一般包含几个关键位:
- present bit 表项不在内存中
- protect bit 没有置为产生保护异常,不允许访问
- dirty bit 表项对应的页被修改
- reference bit (a.k.a. accessed bit) 用于page replacement,页是否被访问过
启用分页访问内存的过程
VPN = (VirtualAddress & VPN_MASK) >> SHIFT // Extract the VPN from the virtual address
PTEAddr = PTBR + (VPN * sizeof(PTE)) // Form the address of the page-table entry (PTE)
PTE = AccessMemory(PTEAddr) // Fetch the PTE
if (PTE.Valid == False) // Check if process can access the page
RaiseException(SEGMENTATION_FAULT)
else if (CanAccess(PTE.ProtectBits) == False)
RaiseException(PROTECTION_FAULT)
else // Access is OK: form physical address and fetch it
offset = VirtualAddress & OFFSET_MASK
PhysAddr = (PTE.PFN << PFN_SHIFT) | offset
Register = AccessMemory(PhysAddr)
3.3.1 第一种优化——TLB
paging机制也面临自己的问题,第一个问题是访问访问慢,如上面的例子仍需要两次访问内存,为了解决这个问题引入TLB进行加速,TLB是页表的缓存,相当是全相连的cache,其结构示意如下:
VPN | PFN | other bits
TLB流程如下:
1 VPN = (VirtualAddress & VPN_MASK) >> SHIFT
2 (Success, TlbEntry) = TLB_Lookup(VPN)
3 if (Success == True) // TLB Hit
4 if (CanAccess(TlbEntry.ProtectBits) == True)
5 Offset = VirtualAddress & OFFSET_MASK
6 PhysAddr = (TlbEntry.PFN << SHIFT) | Offset
7 Register = AccessMemory(PhysAddr)
8 else
9 RaiseException(PROTECTION_FAULT)
10 else // TLB Miss
11 RaiseException(TLB_MISS)
产生tlb miss后,可能会由硬件或者软件处理,软件处理要注意:
- trap返回要重新执行产生tlb miss的指令而不是下一条。
- 注意不要引入无限tlb miss
TLB加速了页表的访问,但同时TLB也引入了新问题:
- context switch 时要进行flush操作,因为它存储的映射关系只对本进程有效。这样开销很大,即下一个进程会产生tlb miss,解决办法是引入ASID避免刷新整个tlb
- cache replacement
3.3.2 第二种优化——多级页表
paging的第二个问题是页表占据的空间太大了,由于页表是基于进程的,假设在32-bit系统下,页大小为4K,那么有1M个页表项,每个页表项4byte,即每个页表占用4M内存,随着进程的增多占据空间增加,如果是64-bit,会占据更大的空间,这是不可接受的。
解决的方法包括以下几种:
- 大页,增加page的大小,那么PTE的数量自然会降低,从而减少页表项的大小。(大页的目的主要还是为了减少TLB miss)大页的使用场景还是收到限制,因为大页会产生内部碎片——很多程序只用物理内存中很少的一部分,会造成大量的浪费。
- Hybrid Approach: Paging and Segments
- 多级页表(multi-level page table) 多级页表的概念很简单:如果一个PTE不可用,就先不用为其分配内存,这样就节省了空间
- 只分配要使用的页表
- 每一级的页表放在一个页面中,那么内存管理变得更简单,也可以使表项的分配变得更灵活。(32-bit 系统10|10|12, 64-bit系统 9|9|9|9|12,假设页面大小是4K,想想为什么是这样?)
- Inverted Page Tables 不再是每个进程一个page table,只有一个page table管理所有的页面,每个entry指示使用这个page的进程以及和该物理页面进行映射的虚拟页面。
多级页表访问流程:
1 VPN = (VirtualAddress & VPN_MASK) >> SHIFT
2 (Success, TlbEntry) = TLB_Lookup(VPN)
3 if (Success == True) // TLB Hit
4 if (CanAccess(TlbEntry.ProtectBits) == True)
5 Offset = VirtualAddress & OFFSET_MASK
6 PhysAddr = (TlbEntry.PFN << SHIFT) | Offset
7 Register = AccessMemory(PhysAddr)
8 else
9 RaiseException(PROTECTION_FAULT)
10 else // TLB Miss
11 // first, get page directory entry
12 PDIndex = (VPN & PD_MASK) >> PD_SHIFT
13 PDEAddr = PDBR + (PDIndex * sizeof(PDE))
14 PDE = AccessMemory(PDEAddr)
15 if (PDE.Valid == False)
16 RaiseException(SEGMENTATION_FAULT)
17 else
18 // PDE is valid: now fetch PTE from page table
19 PTIndex = (VPN & PT_MASK) >> PT_SHIFT
20 PTEAddr = (PDE.PFN << SHIFT) + (PTIndex * sizeof(PTE))
21 PTE = AccessMemory(PTEAddr)
22 if (PTE.Valid == False)
23 RaiseException(SEGMENTATION_FAULT)
24 else if (CanAccess(PTE.ProtectBits) == False)
25 RaiseException(PROTECTION_FAULT)
26 else
27 TLB_Insert(VPN, PTE.PFN, PTE.ProtectBits)
28 RetryInstruction()
四、参考
【1】OSTEP