1.虚拟地址空间

虚拟地址空间是一个非常抽象的概念,先根据字面意思进行解释:

  • 它可以用来加载程序数据(数据可能被加载到物理内存上,空间不够就加载到虚拟内存中)
  • 它对应着一段连续的内存地址,起始位置为 0。
  • 之所以说虚拟是因为这个起始的 0 地址是被虚拟出来的, 不是物理内存的 0 地址

虚拟地址空间的大小也由操作系统决定,32位的操作系统虚拟地址空间的大小为 232 字节,也就是 4G,64 位的操作系统虚拟地址空间大小为 264 字节,这是一个非常大的数,感兴趣可以自己计算一下。当我们运行磁盘上一个可执行程序, 就会得到一个进程,内核会给每一个运行的进程创建一块属于自己的虚拟地址空间,并将应用程序数据装载到虚拟地址空间对应的地址上。

进程在运行过程中,程序内部所有的指令都是通过 CPU 处理完成的,CPU 只进行数据运算并不具备数据存储的能力,其处理的数据都加载自物理内存,那么进程中的数据是如何进出入到物理内存中的呢?其实是通过 CPU 中的内存管理单元 MMU(Memory Management Unit)从进程的虚拟地址空间中映射过去的。

意义

通过上边的介绍大家会感觉到一头雾水, 为什么操作系统不直接将数据加载到物理内存中而是将数据加载到虚拟地址空间中,在通过 CPU 的 MMU 映射到物理内存中呢?

先来看一下如果直接将数据加载到物理内存会发生什么事情:

假设计算机的物理内存大小为 1G, 进程 A 需要 100M 内存因此直接在物理内存上从 0 地址开始分配 100M, 进程 B 启动需要 250M 内存,因此继续在物理内存上为其分配 250M 内存,并且进程 A 和进程 B 占用的内存是连续的。之后再启动其他进程继续按照这种方法进行物理内存的分配。

使用这种方式分配内存会有如下几个问题:

1.每个进程的地址不隔离,有安全风险。

由于程序都是直接访问物理内存,所以恶意程序可以通过内存寻址随意修改别的进程对应的内存数据,以达到破坏的目的。虽然有些时候是非恶意的,但是有些存在 bug 的程序可能不小心修改了其它程序的内存数据,就会导致其它程序的运行出现异常。

2.内存效率低。

如果直接使用物理内存的话,一个进程对应的内存块就是作为一个整体操作的,如果出现物理内存不够用的时候,我们一般的办法是将不常用的进程拷贝到磁盘的交换分区(虚拟内存)中,以便腾出内存,因此就需要将整个进程一起拷走,如果数据量大,在内存和磁盘之间拷贝时间就会很长,效率低下。

3.进程中数据的地址不确定,每次都会发生变化。

由于物理内存的使用情况一直在动态的变化,我们无法确定内存现在使用到哪里了,如果直接将程序数据加载到物理内存,内存中每次存储数据的起始地址都是不一样的,这样数据的加载都需要使用相对地址,加载效率低(静态库是使用绝对地址加载的)。

总结:有了虚拟地址空间之后就可以完美的解决上边提到的所有问题了,**虚拟地址空间就是一个中间层,相当于在程序和物理内存之间设置了一个屏障,将二者隔离开来。程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。**这样,只要操作系统处理好虚拟地址到物理内存地址的映射,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠,就可以达到内存地址空间隔离的效果。

分区

从操作系统层级上看,虚拟地址空间主要分为两个部分内核区用户区

  • 内核区:
    • 内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。
    • 内核总是驻留在内存中,是操作系统的一部分。
    • 系统中所有进程对应的虚拟地址空间的内核区都会映射到同一块物理内存上(系统内核只有一个)。

img

每个进程的虚拟地址空间都是从 0 地址开始的,我们在程序中打印的变量地址也其在虚拟地址空间中的地址,程序是无法直接访问物理内存的。虚拟地址空间中用户区地址范围是 0~3G,里边分为多个区块:

  • 保留区: 位于虚拟地址空间的最底部,未赋予物理地址。任何对它的引用都是非法的,程序中的空指针(NULL)指向的就是这块内存地址。

  • .text段: 代码段也称正文段或文本段,通常用于存放程序的执行代码 (即 CPU 执行的机器指令),代码段一般情况下是只读的,这是对执行代码的一种保护机制。

  • .data段: 数据段通常用于存放程序中已初始化且初值不为 0 的全局变量和静态变量。数据段属于静态内存分配 (静态存储区),可读可写。

  • .bss段: 未初始化以及初始为 0 的全局变量和静态变量,操作系统会将这些未初始化变量初始化为 0

  • 堆(heap):用于存放进程运行时动态分配的内存。

    • 堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。

    • 堆向高地址扩展 (即 “向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。

  • 内存映射区(mmap):作为内存映射区加载磁盘文件,或者加载程序运作过程中需要调用的动态库。

  • 栈(stack): 存储函数内部声明的非静态局部变量,函数参数,函数返回地址等信息,栈内存由编译器自动分配释放。栈和堆相反地址 “向下生长”,分配的内存是连续的。

  • 命令行参数:存储进程执行的时候传递给 main() 函数的参数,argc,argv []

  • 环境变量: 存储和进程相关的环境变量,比如:工作路径,进程所有者等信息