7

之前面试的时候有一个比较有意思的问题: 在操作系统层面上,申请超过物理内存的虚拟内存,会发生什么。

比如说,是4G内存的机器,申请12G的虚拟内存会发生什么?

当时不太清楚相关方面的知识,于是后面就来仔细了解一下。

虚拟内存

可以先来回顾一下虚拟内存。

以前的机器,或者单片机,都说直接操作物理地址的。

image.png

这意味着,不同应用程序之间的不存在内存隔离,可以直接访问物理内存,如果第一个程序在 0x123456789 的位置写入一个新的值,可能会擦掉第二个程序存放在相同位置上的内存。

所以,需要把进程所使用的地址 隔离 开来,即让操作系统为每个进程分配独立的一套 虚拟地址 ,人人都有,互不干涉。

虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)。

所以,我们在程序所使用的内存地址叫实际上叫做 虚拟内存地址

虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存
image.png

所以,我们到底可以申请多少的虚拟内存呢?是不是说虚拟内存和物理内存是一一对应的呢?

操作系统虚拟内存

我们需要先来了解不同位的操作系统的虚拟内存大小。

32 位操作系统和 64 位操作系统的虚拟地址空间大小是不同的,

在 Linux 操作系统中,虚拟地址空间的内部又被分为 内核空间 和 用户空间两部分

image.png

用户空间: 大约是3GB,用于存放用户级应用程序的代码和数据。这是用户应用程序可以访问的部分地址空间。

内核空间: 剩余的约1GB,用于操作系统内核的代码和数据。只有操作系统内核可以访问内核空间,而用户应用程序不能直接访问它。

所以说,我们最大能申请的虚拟内存的大小需要根据操作系统位来判断。

mac 可以用 uname -m查看

"x86_64",则表示系统是 64 位的,如果看到 "i386",则表示系统是 32 位的。
image.png

32 位系统:最大能申请3G的虚拟内存
64位系统: 最大能申请128T的虚拟内存

所以,我们了解了第一个影响虚拟内存大小的点:操作系统的位数

现在我们可以回答了其中一种情况:

在 32 位操作系统、4GB 物理内存的机器上,申请 12GB 内存,会怎么样?

因为 32 位操作系统,进程最多只能申请 3 GB 大小的虚拟内存空间,所以进程申请 12GB 内存的话,在申请虚拟内存阶段就会失败。

我们往下

操作系统访问虚拟内存过程

我们用c语言分配内存的时候,都会用到malloc 函数。

应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。

当应用程序读写了这块虚拟内存,

  1. CPU 就会去访问这个虚拟内存,
  2. 如果发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的处理。
  3. 缺页中断处理函数会看是否有空闲的物理内存:
    3.1 . 如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
    3.2 如果没有空闲的物理内存,那么内核就会开始进行回收内存 的工作,如果回收内存工作结束后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会触发 OOM (Out of Memory)。

image.png

我们可以看到,如果访问了虚拟内存,就会开始分配物理内存。

那么假如我们分配了虚拟内存,不访问呢?

没错,只要不读写这个虚拟内存,操作系统就不会分配物理内存。

所以我们可以明白: 在 64 位操作系统、4GB 物理内存的机器上,申请 12G 内存,会怎么样?

64 位操作系统,进程可以使用 128 TB 大小的虚拟内存空间,所以进程申请 12GB 内存是没问题的,因为进程申请内存是申请虚拟内存,并没有读写。

尝试

下面尝试用c语言分配虚拟内存,使用malloc函数

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define MEM_SIZE 1024 * 1024 * 1024

int main() {
    char* addr[4];
    int i = 0;
    for(i = 0; i < 0; ++i) {
        addr[i] = (char*) malloc(MEM_SIZE);
        if(!addr[i]) {
            printf("执行 malloc 失败, 错误:%s\n",strerror(errno));
                return -1;
        }
        printf("主线程调用malloc后,申请1gb大小内存,此内存起始地址:0X%p\n", addr[i]);
    }
    
    //输入任意字符后,才结束
    getchar();
    return 0;
}

我一开始先不分配内存, 并且运行上面的c文件

image.png
(只用看下面那条)

可以看到,即使不主动malloc分配虚拟内存,也会有32GB的虚拟内存。

接下来malloc 12G

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define MEM_SIZE 1024 * 1024 * 1024

int main() {
    char* addr[4];
    int i = 0;
    for(i = 0; i < 12; ++i) {
        addr[i] = (char*) malloc(MEM_SIZE);
        if(!addr[i]) {
            printf("执行 malloc 失败, 错误:%s\n",strerror(errno));
                return -1;
        }
        printf("主线程调用malloc后,申请1gb大小内存,此内存起始地址:0X%p\n", addr[i]);
    }
    
    //输入任意字符后,才结束
    getchar();
    return 0;
}

可以看到 多分配了12G虚拟内存
image.png

Swap 机制

先总结一下上面的知识,

  • 在 32 位操作系统,因为进程最大只能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
  • 在 64 位操作系统,因为进程最大只能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 12G 内存也是没问题,因为申请的内存是虚拟内存。

程序申请的虚拟内存,如果没有被使用,它是不会占用物理空间的。当访问这块虚拟内存后,操作系统才会进行物理内存分配。

如果申请物理内存大小超过了空闲物理内存大小呢?

其实在 OOM 前还有还有一个检查: 是否开启 Swap 机制

什么是 Swap 机制?

当内存使用存在压力的时候,会开始触发内存回收行为,会把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

这种,将内存数据换出磁盘,又从磁盘中恢复数据到内存的过程,就是 Swap 机制负责的。

所以:

  • 如果没有开启 Swap 机制,程序就会直接 OOM;
  • 如果有开启 Swap 机制,程序可以正常运行。

总结:

分配虚拟内存空间,考虑三个条件:

  • 操作系统是 32 位的,还是 64 位的
  • 申请完 8G 内存后会不会被使用?
  • 操作系统有没有使用 Swap 机制?

参考

https://sylvanassun.github.io/2017/10/29/2017-10-29-virtual_m...

weiweiyi
1k 声望123 粉丝