linux内存管理和虚拟内存


一、虚拟内存简介

在现代操作系统中,进程之间共享使用cpu和内存,但是内存资源有限,为了更加高效地使用内存,现代操作系统提供一个内存抽象—虚拟内存。

虚拟内存是操作系统内核为了对进程地址空间进行管理而精心设计的一个逻辑意义上的内存空间概念。我们编写程序中用到的内存都是在这个虚拟内存空间中进行分配。在程序运行中,操作系统通过地址管理单元(Memory Management Unit, MMU)和映射页表(page tables)来将需要访问的虚拟内存地址映射为物理内存地址。

虚拟内存提供三个重要的能力:

  1. 将内存当作磁盘的缓存,在内存中只保留常用数据,必要时从内存和磁盘之间交换数据。
  2. 简化内存管理,为每个进程提供统一的地址空间。
  3. 保护进程的内存空间不受其他进程影响。

二、虚拟内存机制

以下文字和图片中用到的简称说明:

VA: Virtual Address 虚拟地址
PA: Physical Address 物理地址
VP: Virtual Page 虚拟内存页
PP: Physical Page 物理内存页
PTE: Page Table Entry 页表项
PTEA:Page Table Entry Address 页表项地址


2.1 地址映射

在简介中我们提到了虚拟地址到物理地址的映射,下面是这种过程的示意图:

  • 没有虚拟内存,cpu通过物理地址读取4个字节示意图:

    当cpu执行加载操作,它将有效的物理地址通过地址总线传给内存,内存从地址4开始读取4个字节,将这4个字节内容返回给cpu。

  • 有虚拟内存,cpu通过虚拟地址读取4个字节示意图:

    cpu在加载虚拟地址(VA)之前,MMU(地址管理单元)会将VA转换成PA(物理地址),然后在执行正常的读取操作。

2.2 内存页

进程的虚拟内存在物理内存和磁盘之间交换时以内存页(4kB)为最小单位。进程运行中,虚拟内存页有三种状态:

  • 未分配:未在虚拟内存系统分配,只在页表存在一个记录,不占用内存和磁盘空间。
  • 未缓存:已分配页,没有缓存在内存中,存于磁盘。
  • 已缓存:已分配页,并缓存在内存中。

2.3 页表

页表是用于内存页管理的的结构。其中记录了每个虚拟内存页(VP)对应的状态、与内存和磁盘之间的映射。如下示意图:

2.4 内存缓存命中与MISS

当CPU访问某个虚拟内存地址时,如果页表中记录该地址所在的分页已缓存,则直接去对应物理内存中读取数据,这种情况称为缓存命中(cache hit);当页表中记录该地址所在分页为分配或未缓存时,则会触发”缺页”异常(page fault exception),然后内核会分配新的物理内存页并建立页表映射,这种情况称为缓存未命中(cache miss)。

缓存命中的情形:

缓存命中的流程:

  1. CPU将虚拟地址传给MMU,
  2. MMU生成PTE页表地址,去内存里拿页表。
  3. 内存返回页表给MMU,
  4. MMU根据页表查表后得到物理地址,去请求内存
  5. 根据物理地址,内存将数据返回给CPU

缓存miss的情形:

缓存miss的流程:

  1. CPU将虚拟地址传给MMU,
  2. MMU生成PTE页表地址,去内存里拿页表。
  3. 内存返回页表给MMU,
  4. MMU发现PTE的valid标志位是0,将CPU的执行权交给操作系统内核的page fault异常处理程序(page fault exception handler)。
  5. 异常处理程序选中一个物理内存页(PP)剔除出去,如果该页内容相比之前有变动,同时将其内容写回磁盘。
  6. 异常处理程序将需要的页从磁盘里读取到内存里,并且更新内存里的页表里的PTE。
  7. 异常处理程序执行结束返回,CPU继续之前的寻址操作,现在一切情况都和 page hit 情况一样了。


从上面的步骤可以看出,page hit 这种情况可以在硬件基础上完成,可是 page fault 就需要操作系统内核配合完成了。

三、虚拟内存与内存管理

3.1 进程的地址空间

有了虚拟内存的机制,操作系统可以为每个进程提供独立的页表的虚拟内存空间,并且每个进程都可以有相同的内存布局。

每个进程拥有4G的进程地址空间,该空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。

进程的用户内存空间又分为5个数据段:

  • 代码段(text段):存放可执行文件的操作指令(代码主体),只读。
  • 数据段(data段):存放初始化后的静态和全局变量,常量也存放在这个区域。
  • BSS段:存放未初始化的静态和全局变量,在程序载入时该段内存由内核清零。
  • 堆(heap):堆是“先进先出”的数据结构,用于存放进程运行中动态分配的内存。堆的内存地址向上增加;即堆上的数据越多,新分配的堆内存地址越大。
  • 栈(stack):栈是“后进先出”的数据结构,用于存放程序中的局部变量,包括函数调用时传入的参数和返回值。栈从内存空间的最高地址向下生长,和堆相对。

【注意,内存管理中栈的概念和作用与《数据结构》中类似, 而堆和数据结构中的堆不是一回事。】

进程内存分配如下图所示:

![](/images/2019/05/process_mem_plot.gif)

3.2 虚拟内存与内存管理

每个进程拥有独立的虚拟内存空间和相同的空间布局,这样就为程序的链接、加载、代码与数据共享和程序内存分配等环节带来了便利,使相应工作变得相对简单。

  • 简化链接:因为每个进程有独立的虚拟内存空间和相同的内存布局,这样的统一使得链接器的设计和实现变得简单,不管代码区或者数据区在物理内存的那个地方,链接器总能够根据统一规则生产一个可执行文件。
  • 简化加载:当加载可执行文件和共享对象文件时,加载器不需要立即从磁盘copy全部可执行文件内容到内存里;只需为可执行文件建立页表并分配页表项(text段和data段),并将页表项映射到磁盘上的可执行文件。CPU加载或者执行指令时候发现相应的VP没有在缓存中,这时才会触发数据从磁盘copy到内存。
  • 简化共享:单独的地址空间为操作系统提供了一个统一的机制用于管理用户进程和操作系统内核之间的内存共享。正常情况下,每个进程都有自己的代码、数据、堆和栈区域,进程之间不会共享,操作系统为每个进程创建的页表将进程虚拟页映射到不相交到物理内存中。但是有时候需要需要进程之间共享数据和代码。比如每个进程都要调用相同到操作系统内核代码,每个C程序都要调用标准C库函数printf函数。这时就要用到上图示意的共享PP的场景了。
  • 简化内存分配:VM为用户态进程提供了一个简单机制用于分配额外内存。比如说,进程想额外从堆上分配内存(通过malloc系统调用),操作系统会分配k个连续堆虚拟内存页(k of VPs),因为有页表的存在,在物理内存DRAM上没必要分配连续k页内存,k页可以分散开来。