进程如何使用操作系统内存

  众所周知,内存中存放着需要运行的代码和代码运行所需的一些数据。

一、计算机的缓存系统

  由于工艺水平的限制,同价位下存储器容量越大访问速度越慢。磁盘的容量很大,但是远远无法满足 CPU 对数据访问速度的要求。而内存以快于磁盘几万倍甚至十几万倍的速度与 CPU 直接交互数据。但随着 CPU 的日益发展,内存也无法满足其对数据访问速度的要求,于是缓存诞生了。缓存的数据访问速度远远超过内存,但存储容量大大减小,并且基于局部性原理,让 CPU 经常用到的数据可以被更快的再次访问。多级缓存和内存共同组成的缓存系统,是计算机存放运行时数据的主要部件。

二、虚拟内存机制

  现代操作系统会同时执行很多程序,如何从有限的内存中为这些程序分配它们所需的资源,并同时兼顾安全性和高效性,便成为了首要考虑的问题。于是,虚拟内存机制诞生了。

  虚拟内存对应于物理内存,前者是操作系统在后者的基础上抽象出的一个概念,用于帮助运行在操作系统上的程序合理的分配与管理内存。因此,我们在程序中打印出来的地址都是虚拟内存中的地址,这些地址被称为虚拟地址,所有程序可以使用的虚拟地址构成了虚拟地址空间。而 CPU 在访问内存上的数据时,会借助内存芯片上名为 MMU(内存管理单元) 的硬件,将虚拟地址转换为物理地址,再访问数据。虚拟内存机制的重要特征就是,为每一个应用程序抽象出了独立于物理内存的虚拟地址空间。从进程的角度来看,每个进程都独享所有的内存,通过这种方式,开发者不需要考虑数据被加载在内存的具体位置,所有进程均可使用统一的静态文件结构。比如在 64 位 Linux 系统中,与应用代码相关的 segment 会从 VAS 的固定地址 0x400000 处开始加载。通过这个命令,可以查看进程的 VAS 布局情况:cat /proc/<pid>/maps

1.VAS 中的数据布局

 

Linux 进程 VAS 中的数据,按照地址由低到高的顺序,可以分为以下几个主要部分:
——LOAD segment::这部分数据被加载到从地址 0x400000 开始的虚拟内存中。其主要内容为应用程序 ELF 二进制文件内定义的各种 LOAD Segment 结构。按照顺序,与代码相关的 Text Segment(包含 .text、.rodata 等多个 Section)位于最低地址处,紧接着为包含有已初始化和未初始化数据的 Data Segment(包含 .data,.bss 等多个 Section)。
——堆(Heap):通过 malloc 分配的空间,它将向高地址方向不断增长。
——共享库数据:包含了各种 .so 共享库相关的数据,程序会在运行时通过动态链接器完成对它们的加载和处理。
——栈(Stack):存放各种局部变量,向 VAS 的低地址方向不断增长。
——用于系统调用加速的内核数据:接下来的三个虚拟内存区域 [vvar]、[vdso],以及 [vsyscall] 中包含有操作系统内核的代码和数据结构,它们主要提供了用户进程可以直接与内核进行交互的接口。其中,[vvar] 中包含有只读的内核数据。而另外的 [vdso] 与 [vsyscall] 则包含有用于辅助操作系统,加速用户进程执行某些系统调用过程的信息。
——其他内核数据:除此之外,在进程 VAS 的高地址处,还可能包含有与当前进程相关的各种数据结构。甚至,该区域内的某段虚拟内存页还会被直接映射到某段被所有进程共享的物理内存页上(比如用于 MMIO)。

  由于虚拟内存的机制,进程可以使用完全统一、独立的内存数据布局,而不用考虑数据在物理内存分布的细节。虚拟内存机制通过以下几种方式,对物理内存进行管理。

2.用页表维护虚拟页状态

  为了保证效率,操作系统以页为单位,来在磁盘和内存之间传递数据。虚拟内存机制为每个进程提供独立的页表结构,来维护虚拟页与物理内存之间的映射关系。

  页表本身存在物理内存中,由页表项(Page Table Entry,PTE)组成。进程中每个虚拟页都对应页表中的某个 PTE ,PTE 中包含用于描述该虚拟页的众多字段。当 MMU 需要将虚拟地址翻译为物理地址时,都要先读取页表,查询相关的 PTE 信息。然后根据虚拟地址内隐含的偏移信息,计算出真实的物理地址。

  在简化的实现中,PTE 可以由有效位字段一个地址字段组成。其中有效位字段用于表示该虚拟页是否被缓存进物理内存中,若该位置位,地址表示该页在物理内存中的起始位置;若该字段复位,如果地址字段为空,说明该页还没有被分配,如果有地址,表示虚拟页在磁盘上的起始位置。

  当 CPU 访问虚拟地址的上的数据时,会发生以下两种情况:

——MMU 查找进程页表,发现目标数据已被缓存,则通过 PTE 存储的物理地址找到并返回所需数据。

——MMU 查找进程页表,发现目标数据未被缓存,此时出发缺页异常。此时,该异常会调用内核中的异常处理程序,在物理内存中选择一页与虚拟内存的页相关联。对于空闲页,内核将虚拟页的内容直接从磁盘拷贝到物理内存中;对于非空闲页,如果页已经被修改,内核将修改后的页的内容更新至磁盘中,再从磁盘拷贝到物理内存中。

  页表隔离了 VAS 和物理内存,承载着双方的映射关系。不同进程通过不同的页表维护着 VAS 中虚拟页的映射,多个进程因此能做到数据共享。同时,独立的 VAS 和页表使得进程的私有数据不会被相互访问

3.使用多级页表压缩虚拟页体积

  一级页表有时无法满足需求,以 64 位地址空间为例,假使内存页的大小为 2M,为保证完整映射,每个 PTE 的大小为 8 字节。如果是单一页表的话,64 的 VAS 需要 65535 GB 的空间存储页表信息。这显然是不现实的,因此,现代计算机会使用多级页表的方式来优化页表的大小。

  事实上,一个进程不可能用到所有的 VAS ,我们只需要将用到的页表信息存放在物理内存中。因此,多级页表的优势是通过顶级页表去为真正有用的页表提供索引

  以二级页表为例,假设在 64 位空间中,页大小为 8KB,每个 PTE 大小为 8 字节。此时,MMU 在查询物理地址时,先根据虚拟地址中隐含的虚拟页号信息来查找一级页表内的目标 PTE ,一级页表内的每个 PTE 负责映射 VAS 中一块 8 MB 的片。当一级页表被查询完成后,MMU 得到了二级页表的地址,通过该地址,再结合虚拟地址内的虚拟页号信息,便可以查到目标数据所在的物理内存页,最后再通过虚拟地址中的页偏移信息,计算出真正的物理地址。

  多级页表节省空间的两个重要因素是:

——只有一级页表常驻内存,二级页表在有需要的时候被创建或存储在磁盘中。

——当一级页表中的某个 PTE 没有实际映射时,二级页表便不会被创建。

4.使用 TLB 加速 PTE 查询

  多级页表可以压缩页表占用的内存量,但是 MMU 需要对页表进行逐级查询,这个过程需要一定的时间成本。现代计算机使用翻译后备缓冲器(Translation Lookaside Buffer,TLB )来加速查询页表的过程。TLB 属于 MMU 的一部分,可以理解成一个矩阵,MMU 从虚拟地址中取出用于查询页表项的 TLB 索引和 TLB 标记。这两个值可以定位到 TLB 矩阵中的某一个单元格,如果该单元格有值,可以直接从单元格中提取该值,与虚拟地址中的其他信息结合计算出目标页的物理地址,否则 MMU 需要通过逐级查询页表的方式获取目标页的物理地址。

posted @ 2022-03-10 22:43  一只吃水饺的胡桃夹子  阅读(178)  评论(0编辑  收藏  举报