虚拟地址空间 Virtual Address Space

读《Computer System》,遇到 Virtual Address Space 这个概念,即虚拟地址空间,以前也看过,不过都是知其然不知其所以然,故本文做一下详细的概念解析:起源和发展和现状,起到什么作用,为何要创造这个概念。

参考链接:

浅谈进程地址空间与虚拟存储空间

Virtual Address Space 的背景应该从内存分配机制的发展说起,它是一个衍生概念,服务于内存分配机制。

以下从3个阶段进行说明:

  1. 原始的内存分配机制
  2. 分段内存分配机制
  3. 分页内存分配机制

1. 原始的内存分配机制

原始计算机中,运行一个程序,会把程序全部装入内存,并直接运行在内存上。

在这个阶段,程序中访问的内存地址就是实际的物理内存地址

当系统同时运行多个程序时,如何为这些程序分配内存?

举例:

Total Memory Size 128M,同时运行 A 和 B 两个程序,A 需 10M,B 需 110M。

原始内存分配策略:

先将内存中的前 10M 分给 A,接着再将余下的 118M 分 110M 给 B。如下图所示:

局限性

  1. 进程地址空间未隔离

由于AB都是直接访问物理内存,存在互相修改对方内存数据风险,违反了独立进程的原则。

  1. 程序运行的地址不确定。

当内存中的剩余空间可以满足程序 C 的要求后,操作系统会在剩余空间中随机分配一段连续的 20M 大小的空间给程序 C 使用,因为是随机分配的,所以程序运行的地址是不确定的。

注意:这是相对于下面要将的Segmentation 分段技术,即虚拟内存空间,对于程序来说,地址的起点都是0,而非随机的,实现了一种对程序员透明的内存映射方式。

2. 分段内存分配机制

分段的目的:解决地址空间隔离问题。

原理:增加中间层(虚拟地址空间)。

既然直接访问物理地址有风险,那就不让直接访问,交给一个中间人来控制,即程序无权直接访问物理内存,只能通过这个中间人来传达访问诉求,而这种传达方式遵从系统规则,规避了内存串扰的问题,间接起到地址空间隔离的作用。

现在,程序访问的内存地址不再是实际的物理地址,而是虚拟地址,然后由系统将这个虚拟地址映射到物理内存地址,完成内存访问。

当创建一个进程时,系统为它分配 4G 的虚拟地址空间。

为什么是4G?

因为在32位系统中,一个指针长度是4个字节,而4个字节指针的寻址能力是:

From
0000 0000 0000 0000 0000 0000 0000 0000
to
1111 1111 1111 1111 1111 1111 1111 1111
复制代码

换成16进制是 0x00000000 ~ 0xFFFFFFFF,最大值 0xFFFFFFFF 即 4G 大小的容量。

什么是内存寻址,什么是寻址能力?

寻址:Addressing,去寻找、定位一个地址。

寻址能力

指针长度是4个字节,我们知道指针的内容是内存地址,这个4个字节里面存放的就是N种可能的地址。 N是多少呢?这4个字节 32 bits 大小的内存空间可以表示多少种可能的地址呢?

32 个 bit 连在一起能表示的多少个 0101 的可能性,就能表示多少个地址。32位能表示多达 4G 种 0101 的可能性,即 4G 个地址。

也就是说我这个指针能访问的内存地址,只能局限在这个 4G 个不同地址中,超出这 4G 个地址范围就是我力所不能及之处。

好比我手上只拿了四个房间的钥匙,那我只能开这对应的四个房间的门,这一层剩下的另外4间房子对我来说,就是我的能力范围之外了。对应32位系统使用8G的内存条,我最多只能用到其中的4G。剩余的4G对于我这个指针来说,根本就没有钥匙去开这些门,访问也就无从谈起了。

这里的 4G 就是我这个指针的寻址能力。

通过映射机制,当程序访问虚拟地址空间上的某个地址时,就相当于访问了某一个物理地址对应的那实际内存。

这种映射机制的原理是什么?原理将在分段技术中分析。

分段 Segmentation (内存分块技术)

思想:在虚拟地址空间和物理地址空间之间做一一映射。系统保证不同进程的地址空间被映射到物理地址空间中不同的区域上,而物理地址空间都是彼此分开的,这样就实现了进程间的地址空间隔离。

例子:

A、B 两个进程

A需要10M,对应的虚拟地址空间分布是: 0x00000000 到 0x00A00000 

B需要100M,对应的虚拟内存地址空间分布是:0x00000000 到 0x06400000。

按照 Segmentation 的映射方法,进程 A和B 在物理内存上映射区域分别为:

进程 A:0x00100000 到 0x00B00000(分配一块 10M 大小的内存片段)

进程 B:0x00C00000 到 0x07000000(分配一块 100M 大小的内存片段)

复制代码

于是,进程 A 和进程 B 分别被映射到不同的内存区,彼此互相不重叠,实现了地址隔离。

从程序员的角度来讲,进程 A 的地址空间就是分布在 0x00000000 到 0x00A00000,这就是面向程序员的虚拟地址空间,我们只需要面对这个虚拟地址空间来编程即可,背后的真实物理地址映射交给系统来处理,对程序员来说是透明的。

分段技术的局限性

Segmentation 分段的映射方式可以解决原始内存分配机制的内存空间隔离和程序内存地址不确定的问题,但效率还是比较低下。

Segmentation 分段的映射方法中,每次切换内存都是整个程序,粒度太大,造成大量的磁盘访问操作,性能差。

实际上,程序的运行有局部性的特点,在某个时间段内,大部分程序只是用到当前程序的一小部分数据而已,剩余的大部分数据在一个时间段内都不会被用到(占着茅坑不拉屎,浪费宝贵的内存坑位)。

基于此,计算机科学家想到了粒度更小的内存分割和映射方法 -- 分页(Paging)

3.分页 Paging

分页:将地址空间分割成许多的 page,每个 page 的大小由 CPU 决定。目前 PC 都选择使用 4KB 的分页大小。比起按进程分割内存大小,分页的方式粒度要小得多。

分段和分页的区别

分段:每次程序运行时总是把当前程序的全部数据加载到内存块。

分页:程序运行时,用到哪页就为哪页分配内存,没用到的页暂时保留在硬盘,而不一次性load到主内存。

当用到这些 page 时,分页流程分析如下:

--> 1.在物理地址空间中分配内存给这些 page

--> 2.在虚拟地址空间中创建这些 page

--> 3.映射:搭建桥梁,将物理地址和虚拟地址空间对接起来

可执行文件的装载过程

下面通过介绍一个可执行文件的装载过程,来说明分页机制的具体实现方法。

一个可执行文件,本质是一些编译好的数据和指令的集合,它会被分成很多 page。它在执行过程中,往内存加载的单位是 page。

当一个程序被执行时,系统首先为该程序创建一个 4G 的进程虚拟地址空间。

虚拟地址空间只不过是一个中间层,本质是一个数据结构,创建 4G 虚拟地址空间其实不是真的创建空间,只是创建那种映射机制所需要的额数据结构而已,这种数据结构就是页目(page index)和页表(page array)。

当创建完虚拟地址空间这一层所需的数据结构之后,这个新创建的进程开始读取可执行文件的第一页。

在可执行文件的第一页包含了文件头和段表等信息,进程根据这些头信息,将可执行文件中所有的段,一 一 映射到虚拟地址空间中响应的页。

此时,可执行文件的真正指令和数据(相对头信息而言)还没被加载进内存中,系统知识根据可执行文件的头信息,预先建立了可执行文件和进程虚拟地址空间中 page 的映射关系而已。

其实就是提前先建好目录和未来实际内容的对应关系而已,好比我是老师,提前给一个班级的学生一人分配一个钥匙,但先不让你进房间。只有当我念到你名字的时候,你才能拿着钥匙进入你自己的房间。

当 CPU 要访问程序中用到的某个虚拟地址时,发现改地址并没有相关联的物理地址,PUC把该虚拟地址所在的页面视为空页面 -- Page Fault (CPU 知道系统还没给该程序页分配内存),进而 CPU 将控制权交回给系统。

操作系统于是为该程序页在物理地址空间分配一个页面,然后再将这个物理内存 page 和虚拟地址空间中的 page 建立起映射,最后将控制权交还给 CPU,CPU 从刚才发生页面错误的位置重新开始执行。

我们可以将这理解为一种内存分页通信机制 -- 随着程序的继续执行,page fault 会不断发生,操作系统也会为进程分配相应的物理内存 pag,满足进程执行所需内存。

分页方法的核心思想

当程序执行到第N页,就为这一页分配一个内存页,然后再将这个内存页添加到虚拟地址空间的映射表中。进程通过这个映射表,就可以访问到对应页的内存了。

逻辑地址(Logical Address)

在 C 语言里,能读取指针变量本身值(& 操作),这个值就是逻辑地址,它是相对于当前进程数据段的地址,不和绝对物理地址相关。

只有在 Intel 实模式下,逻辑地址才和物理地址相等。

程序员只需要和逻辑地址打交道,分段和分页机制对你来说是完全透明的。

逻辑地址可以理解为虚拟地址。

转载于:https://juejin.im/post/5d0ae862f265da1b897ad552

猜你喜欢

转载自blog.csdn.net/weixin_34337381/article/details/93183002