前言

本文简单阐述逻辑地址、线性地址、物理地址和虚拟地址。


1. 符号名空间

编写程序时使用的地址空间叫符号名空间,在符号名空间中,通过符号(即变量名)引用内存。


2. 逻辑地址

编译后的程序使用的地址空间是逻辑地址空间,逻辑地址是相对于段基地址的偏移。

2.1. Linux 中的分段

以下内容来自《深入理解 Linux 内核》第三版

Linux 以非常有限的方式使用分段。实际上,分段和分页在某种程度上有点多余,因为它们都可以划分进程的物理地址空间:分段可以给每一个进程分配不同的线性地址空间,而分页把同一线性地址空间映射到不同的物理空间。与分段相比,Linux 更喜欢分页的方式。

运行在用户态的所有 Linux 进程都使用一对相同的段来对指令和数据寻址。这两个段就是所谓的用户代码段和用户数据段。类似地,运行在内核态的所有 Linux 进程都使用一对相同的段来对指令和数据寻址:它们分别叫做内核代码段和内核数据段。表 2-3 显示了这四个重要段的段描述符字段的值。

linux-segment-managent.png

相应的段选择符由宏 __USER_CS、__USER_DS、__KERNEL_CS 和 __KERNEL_DS 分别定义。例如,为了对内核代码段寻址,内核只需要把 __KERNEL_CS 宏产生的值装进 cs 段寄存器即可。

注意,与段相关的线性地址从 0 开始,达到 232 - 1的寻址限长。这就意味着用户态或内核态下的所有进程可以使用相同的逻辑地址。

所有段都从 0x00000000 开始,这可以得出另一个重要结论,那就是 Linux 下逻辑地址与线性地址是一致的,即逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的


3. 线性地址

线性地址是逻辑地址转换为物理地址的中间层,CPU 的 MMU(内存管理单元)负责将线性地址转换为物理地址。

为提升效率,CPU 进行内存管理的基本单位不是字节,而是,每个页 4KB。因此在 32 位机器上,线性地址被划分为 2^32 / 4K = 2^20 = 1MB 个页。

同样地,CPU 也将物理地址空间划分为页,称每个页为物理页

理论上,每个页都应该对应一个物理页。但是一个地址是 32 位,4 个字节。那么存储该对应关系需要 4MB 的内存。

为节省空间,引入页目录页表的概念,每个进程都有自己的页目录,它的地址存储在 CPU 的寄存器(比如 CR3)中。

以 32 位机器为例,一个 32 位的线性地址分为 3 部分:

整个转换过程如下所示:

  1. 从 CPU 寄存器 CR3 中获取页目录地址(操作系统在调度进程时,负责将页目录地址放入寄存器)

  2. 根据线性地址的页目录索引部分,去页目录中找到页表

  3. 根据线性地址的页表索引部分,找到页的起始地址

  4. 起始地址 + 偏移,即可得到物理地址

linear-address.jpg

说明:


4. 物理地址

可以形而上地将物理地址理解为:将物理内存的存储单元从 0 开始编号,每个编号就是对应存储单元的物理地址。

综上,整个转换过程是 - 符号名 -> 逻辑地址 -> 线性地址 -> 物理地址。


5. 虚拟存储器(Virtual Memory)

以下内容来自《深入理解计算机系统》第三版

5.1. 物理和虚拟寻址

虚拟存储器是一个抽象概念,它为每个进程提供了一个假象,好像每个进程都在独占地使用主存。每个进程看到的存储器都是一致的,称之为虚拟地址空间。图 1.13 所示的是 Linux 进程的虚拟地址空间(其它 Unix 系统的设计也与此类似)。在 Linux 中,最上面的四分之一的地址空间是预留给操作系统中的代码和数据使用的,这对所有进程都一样。底部的四分之三的地址空间用来存放用户进程定义的代码和数据。请注意,图中的地址是从下往上增大的。

virtual-memory-def.png

每个进程看到的虚拟地址空间由大量准确定义的区域(area)组成,每个区都有专门的功能。

虚拟存储器的运作需要硬件和操作系统软件间的精密复杂的互相合作,包括对处理器生成的每个地址的硬件翻译。基本思想是把一个进程虚拟存储器的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。

为通用计算设计的现代处理器使用的是虚拟寻址,参见图 10.2。

virtual-memory-def-2.png

根据虚拟寻址,CPU 通过生成一个虚拟地址(Virtual Address,VA)来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译(Address Translation)。就像异常处理一样,地址翻译需要 CPU 硬件和操作系统之间的紧密合作。CPU 上的芯片叫做 MMU(Memory Management Unit,存储器管理单元)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操作系统管理。

5.2. 地址空间

地址空间(Address Space)是一个非负整数的地址的有序集合:

如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(Linear Address Space)。为了简化我们的讨论,我们总是假设使用的是线性地址空间。在一个带虚拟存储器的系统中,CPU 从一个有 N = 2n 个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间:

一个地址空间的大小是由表示最大地址所需的位数来描述的。例如,一个包含 N = 2n 个地址的虚拟地址空间就叫做一个 n 位地址空间。现代操作系统典型地都支持 32 位或者 64 位地址空间。

一个系统还有一个物理地址空间(Physical Address Space),它与系统中物理存储器的 M 个字节相对应:

M 不要求是 2 的幂,但是为了简化讨论,我们假设 M = 2m

地址空间的概念是很重要的,因为它清楚地区分了数据对象(字节)和它们的属性(地址)。一旦我们认识到了这种区别,我们就可以概括总结,允许每个数据对象有多个独立的地址,其中每个地址都选自一个不同的地址空间。这就是虚拟存储器的基本思想。主存的中每个字节都有一个选自虚拟地址空间的虚拟地址,和一个选自物理地址空间的物理地址。


6. Linux 内存管理

以下内容来自《深入理解 Linux 内核》第三版

6.1. 虚拟内存

所有新近的 Unix 系统都提供了一种有用的抽象,叫虚拟内存(Virtual Memory)。虚拟内存作为一种逻辑层,处于应用程序的内存请求与硬件内存管理单元(MMU)之间。

虚拟内存子系统的主要成分是虚拟地址空间(Virtual Memory Space)的概念。进程所用的一组内存地址不同于物理内存地址。当进程使用一个虚拟地址时,内核和 MMU 协同定位其在内存中的实际物理位置。

6.2. 进程虚拟地址空间处理

进程的虚拟地址空间包括了进程可以引用的所有虚拟内存地址。内核通常用一组内存区域描述符描述进程虚拟地址空间。例如,当进程通过 exec() 类系统调用开始某个程序的执行时,内核分配给进程的虚拟地址空间由以下内存区组成:

所有现代 Unix 操作系统都采用了所谓请求调页(demand paging)的内存分配策略。有了请求调页,进程可以在它的页还没有在内存时就开始执行。当进程访问一个不存在的页时,MMU 产生一个异常,异常处理程序找到受影响的内存区,分配一个空闲的页,并用适当的数据把它初始化。同理,当进程通过调用 malloc() 或 brk()(有 malloc() 在内部调用)系统调用动态地请求内存时,内核仅仅修改进程的堆内存区的大小。只有试图引用进程的虚拟内存地址而产生异常时,才给进程分配页框(Page Frame)。

虚拟地址空间也采用其他更有效的策略,如前面提到的写时复制策略。例如,当一个新进程被创建时,内核仅仅把父进程的页框赋予给子进程的地址空间,但是把这些页框标记为只读。一旦父进程或子进程试图修改页中的内容时,一个异常就会产生。异常处理程序把新页框赋给受影响的进程,并用原来页中的内容初始化新页框。


7. 示例 1 - mmap

7.1. Cargo.toml

7.2. src/main.rs

7.3. libc::mmap

libc::mmap 用于进行内存映射(Memory Mapping)。它可以将文件或者匿名映射(anonymous mapping)映射到进程的内存空间,使得进程可以直接访问这段内存。

函数签名如下:

参数解释如下:

函数返回值是映射的起始地址,如果映射失败,那么返回 MAP_FAILED,即 -1。


8. 示例 2 - malloc

8.1. Cargo.toml

8.2. src/main.rs


参考资料