0x00
首先看本文之前,先思考几个问题: 1. 在单CPU的电脑上,同一时间智能运行一个进程,CPU通过时间片的方式,模拟进程的同时运行。那多个运行的进程所使用的内存是如何管理的?是怎么保证多进程之间不会干扰到对方的内存区域 2. 电脑在内存占用非常高的时候变得很卡,是为什么? 3. mmap为什么会更快 # 什么是虚拟内存
虚拟内存是指操作系统提供的一种管理存储资源的方式,屏蔽了cache、DRAM和硬盘的使用细节,为用户或者说是进程提供统一的、简洁的内存使用和管理方式,是计算机系统中非常重要的概念。
为什么要有虚拟内存
- 高效的使用主存
- 为进程提供一致的内存空间,简化内存管理
- 为进程提供独立的内存空间,保证安全性
主存是磁盘的缓存
虚拟内存把主存看做是硬盘存储的缓存,主存由于成本原因往往比硬盘小很多,虚拟内存通过页交换来保证当前使用的页面处于主存中。页面分为虚拟页和物理页,虚拟页是进程所看到的概念,物理页是实际存储的页面块。根据状态虚拟页可分为:未被分配的、已分配但未被缓存在主存中的和已分配并且缓存在主存中的。
页表
根据虚拟页的状态,当进程需要访问一个虚拟页面的时候,它需要知道当前虚拟页是否可用以及对应物理页的位置,虚拟内存为每个进程提供了一个叫做页表(page table)的数据结构,它存储了当前进程的所有页面的实时信息,由页表条目(PTE: page table entry)组成,每个PTE包含一个有效未和n位地址位(对应物理地址空间的位数):有效位为1时代表当前PTE页面在主存中,地址位对应其实际的物理地址;有效位未设置时代表当前页面未被分配(往往意味着当前访问的VA是不合法的);当有效位为0时,代表物理页面未被缓存,此时会触发缺页异常。 页表是存在task_struct中的,而task_struct是内核控制的,存在每个进程的内核地址空间。
缺页
当进程访问到一个有效位为0的PTE时,会发现物理页面不在主存中,n未地址位代表页面在磁盘上的位置。这时操作系统会触发缺页异常,内核的缺页处理程序捕捉到这个异常后,会根据一定的算法,从页表中选取一个牺牲页,写回(如果需要的话)这个页面的内容到磁盘中,并将实际之前尝试访问的页面根据其在磁盘上的位置,将其复制到牺牲页之前所在的物理地址处,并更新进程的页表,这个过程就叫做交换(swap)。处理完缺页异常后内核将进程执行到缺页异常之前的指令处,并将CPU交还给进程,进程此时再去访问之前的虚拟地址时,会发现PTE的有效位已经是1,可以根据物理地址取到对应的页面内容。
虚拟页为什么很大(4KB-?)
DRAM比SRAM慢10倍左右,而磁盘比DRAM慢10000倍左右,同时访问扇区的第一个字节比访问整个扇区的内容要慢10 000倍。DRAM未命中的处罚和访问第一个字节的巨大开销,决定了虚拟页往往很大。那页面很大会不会导致操作系统频繁的出现缺页和swao导致CPU的运行效率变低呢?这时候那个贯穿计算机系统的概念又出现了:局部性。根据局部性原理,每个程序都倾向于在它的一个工作集(work set)中运行,在进程刚开始运行的时候,缺页率会比较高,那之后会逐渐降低。当然这要求我们的程序要有良好的局部性,良好的局部性能大大提高主存缓存的命中率,提高程序的运行速度。当工作集大于主存的大小的时候,虚拟系统会频繁的发生页面的换进换出,造成虚拟内存的抖动,降低程序的运行速度。
虚拟内存作为内存管理的工具
简化链接
内存空间一致:栈、共享内存区、堆、data、text,一致的进程内存空间,统一了程序链接器的设计和实现,不用考虑实际的物理内存。
简化共享
通过映射到同一块物理页实现共享:除了进程的私有数据和代码,一些通用的数据和代码每个进程间都是一样的,比如系统库的函数,这种情况OS只需要把不同进程的页表中的虚拟地址映射到同一块物理地址,就可以实现内存的共享,同时对进程的使用是透明的。
简化加载
On-demand load:在加载可执行文件的时候,OS通过分配对应的无效状态虚拟页啦加载程序代码,当进程实际使用时,按需加载,触发page fault并加载磁盘中的页到主存中。
简化内存分配
提供连续的内存空间:OS可以给进程分配连续的虚拟页,通过页表,实现这些虚拟页映射的物理页可以在内存中的任意位置。
虚拟内存作为内存保护的工具
为了保证进程正常的运行,不同区域的内存,存储不同内容的内存,不应该有相同的访问权限,OS通过给PTE添加一些控制位(内核权限、读、写)来实现访问控制,在进程尝试访问一个虚拟地址的时候,MMU首先会读到对应的PTE,如果当前访问不能PTE上的控制位或者不存在对应的PTE,CPU就会触发一个异常给异常处理程序,一般这个异常就叫做段错误segment fault。
地址翻译
进程访问的都是虚拟地址,OS和CPU会把虚拟地址翻译成对应的物理地址,读取物理页的内容并返回给进程,虚拟地址到物理地址的映射就叫做地址翻译,MAP:VAS -> PAS ∪ ∅
MMU
MMU(Memory Management Unit)是CPU上的硬件,配合OS进行地址翻译,大致步骤如下: 1. MMU接受一个VA 2. MMU生成VA对应的PTE地址,并从cache或者主存中请求拿到PTE 3. 根据PTE构造对应的PA,并传给cache或者主存 4. cache或者主存从PA中读取数据并返回给CPU
有几点需要注意的是: 1. CPU有专门的寄存器存执页表的基地址,叫做页表基址寄存器(page table base register) 2. 虚拟地址被分为两部分:VPN和VPO,虚拟页号和虚拟页偏移,VPO和PPO总是相同的,这样可以简化数据查找的过程 3. PTE和PA都会使用cache,即请求PTE的时候会先访问cache,拿到PA后还是会先访问cache。 4. 访问cache时到底是使用VA还是PA,大部分OS选择PA
TLB
Translation Lookaside Buffer,MMU请求PTE的时候,需要访问cache或者主存,这需要消耗CPU时间周期,因为地址翻译本身是一个很频繁且有局部性特点的工作,所以设计了TLB作为PTE的缓存,MMU会首先根据VA到TLB找对应的PTE,节省开销。TLB属于硬件的一部分。
多级页表
对于之前提到的页表,假如一个32位的虚拟地址空间,4KB的页大小,那么就要有2^20个PTE,每个PTE32 bit(20位代表页号,12位代表页内偏移),总共页表的大小就是4MB,而根据局部性原理,大部分使用的PTE其实很集中,这就造成了内存的浪费,所以引入了多级页表。 多级页表把VA分为N个VPN部分,和一个VPO部分,VPN指向每级页表的PTE索引,1-n-1的页表PTE内容是下一级页表的基址,第n级的内容是该VA对应的PPN,使用PPN+VPO构建对应的PA。
总结一下TLB和多级页表:使用多级页表来减少内存消耗,使用TLB来减少寻址开销。
Linux 虚拟内存结构
Linux的每个进程有单独的虚拟地址空间,把内存组织成段(vm_area)的集合,一段就是已经分配的内存的集合,内核不用记录那些不在内存中的虚拟页。从高地址到低地址依次是内核虚拟内存、用户栈、共享内存区域、堆、bss、data、text。这些段就是由vm_area_struct管理的,多个vm_area_struct组成一个链表,构成所有的段。
有几点需要注意的: 1. 内核部分的虚拟内存,可以大致分为两部分:① 进程间不同的部分,包括当前进程相关的的数据结构,比如页表、段集合等;② 进程间相同的部分,一是内核的代码和数据,二是物理内存,Linux把DRAM中连续的物理页映射到了同样连续的虚拟页中,更方便内核对物理地址的访问。 2. 内核的缺页处理程序在收到某个VPA的异常时,会首先在vm_area_struct链表查找这个VPA,如果未找到就触发段错误;如果找到了但是违反访问保护,就触发保护异常;如果是合法的操作,就选择一个物理牺牲页,swap换进需要的物理页,并更新页表并返回,CPU会重新运行缺页的指令,正常运行。 3. 为什么Linux进程的虚拟地址空间链接不是从0开始使用的,而是从比如0x40000开始使用的? 原因之一是因为这样可以不需要实现对空指针访问的异常。
mmap
直接把磁盘对象映射到虚拟地址的方式,叫做内存映射(memory mapping)。这种可以映射的磁盘对象分为两种: 1. 普通文件 2. 匿名文件,即非磁盘上的普通文件,是内核创建出的一块全零的区域,使用场景主要是进程间通信,适用于父进程和子进程之间,父进程fork子进程之前,先mmap一块匿名文件,然后子进程因为继承父进程的资源,也拥有这块mmap的匿名文件,这样父子进程就可以利用这块匿名文件进行进程间通信。 两种情况都需要使用交换空间(swap space),只是匿名文件不存在磁盘到内存的数据传输,是将牺牲页换出(如果需要)到磁盘后直接写为全零。交换空间维护这当前进程能够分配的VP的总数。
共享对象和copy on write
对于一个被映射到虚拟内存空间某个区域的对象,要么是共享对象,要么是私有对象。共享对象被映射到的区域的任何修改操作对所有进程都是可见的;而私有对象被映射到的区域的修改只在当前修改进程有感知,这种情况下,两个进程可以将私有对象映射到各自内存空间的不同区域,并且共享同一份物理内存副本,只有当某个进程修改了自己内存空间对应的区域,会触发一个保护异常,这时内核会再复制一份物理内存副本,供当前进程进行写操作,这就是写时拷贝(copy on write, cow)。写时复制可以最大限度上的节省物理内存资源。
Linux最常见的fork函数,就是利用了这种特性,fork子进程时完全拷贝父进程的页表样本,并标记为私有的写时复制,这样只有在两个进程尝试修改内存页的时候,才会去复制新的物理内存副本。
总结
虚拟内存是现代操作系统重要的抽象概念之一,以及与之配合的MMU、多级页表、TLB、cache、DRAM等。虚拟内存简化了内存的使用和管理,同时节省了物理内存资源。在使用和实现过程中,又结合了计算机领域的多个重要概念,比如程序的局部性原理,存储器层次结构,写时复制,swap等等,能更好的帮助理解操作系统的整体运行机制。