原创

关于进程虚拟内存

温馨提示:
本文最后更新于 2022年02月14日,已超过 774 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

内存和系统

大家都知道,内存在计算机中是有限资源,它大概是一个这样的东西:

仙士可博客

在计算机中,根据内存条容量,从而转换成了一个以8位为1字节的大数组:

仙士可博客

系统通过访问具体的内存地址,获取具体存储的二进制值,从而实现读写内存数据

为什么需要虚拟内存

由于内存数据是固定的一个大数组,而操作系统往往是运行多个程序,如果这些程序都直接访问内存数组的话,就出现了以下问题:

1:每个进程需要的内存都是变动的,可能需要1G,可能需要2G,也可能需要10k,如果预先申请的过多,就会导致其他进程无法申请内存,但是如果申请的过少,又会需要频繁申请

2:在频繁申请时,内存地址不固定,每个进程都得管理自身不连续的内存段,非常麻烦

3:如果所有进程在同一时间都需要申请内存,就会造成读写冲突

这个时候,就需要用到虚拟内存了

虚拟内存

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。目前,大多数操作系统都使用了虚拟内存,如Windows家族的“虚拟内存”;Linux的“交换空间”等。

虚拟内存做了以下事情:

1:每个进程拥有自己的独立虚拟内存空间,在进程看来,整个地址是连续的

2:在实际内存不足时,进程依旧可以申请内存(将使用磁盘空间存储)

3:在进程克隆后,将通过 "写时复制" 技术,只复制虚拟空间,不复制实际内存(只有写的时候复制一份),实现内存利用最大化

4:可以将共享对象映射到实际内存空间,多个进程读取自身的虚拟空间,映射相同的共享内存空间

5:进程在申请虚拟空间时,并没有实际分配内存空间,而是只有在实际使用时才会进行分配

内存管理单元 MMU(Memory Management Unit).

现在由于进程使用的是虚拟内存,所以操作系统需要将虚拟内存地址映射到物理内存中,通过MMU进行内存映射.

仙士可博客

MMU内存管理由cpu实现,cpu如果为32位,则只支持2^32=4GB内存的映射,而64位这是8TB的内存映射

页表


内存页 (MemoryPage):操作系统定义的进程申请内存的最小单位,根据页进行管理内存映射,一般默认为 4kb 大小

页表(Page Table):操作系统给每个进程存储了一个页表,用于存储虚拟内存和物理内存的关联,页表存储的对应关系叫:页表条目(Page Table Entry,简称PTE)

在创建进程后,操作系统将把页表存储进物理内存,使得MMU可以直接读取物理内存获取PTE
大页表: 操作系统可提供4kb,1Mb,1GB的页进行分配,而不是只能分配多个4k页

分级页表:当进程持续性申请4GB内存时,会发现4kb的页有100万条,这时候寻找起来会十分复杂,操作系统将页表分级存储,1级存储2级的页表范围,2级存储3级的页表范围,3级页表存储实际的页表,这样就加快了查询速度

虚拟内存转换过程

1:操作系统创建进程,初始化进程信息,分配进程虚拟地址页表

2:当进程需要存储变量数据时,虚拟空间分配虚拟地址

3:CPU获取虚拟地址访问

4:通过虚拟地址发送给MMU
5:MMU获取到一个PTE信息

6:如果PTE中存在地址,则从物理地址/CPU内存地址缓存Translation Lookaside Buffer (TLB)  读取数据

7:如果PTE中不存在地址,则触发  缺页异常

8:缺页异常后,cpu尝试给虚拟地址绑定一个物理地址,并且更新页表

9:如果内存空间占满,则确定一个不常使用的地址页,将其存储更新到物理硬盘中,该地址页重新绑定虚拟地址并更新

10:重新回到第6步,读取数据

进程虚拟内存空间分布

在64位系统中,虚拟内存可以达到好几TB,不好做演示,这边按32位系统来说

在32位4G内存中,linux内核默认会真实占用1G空间,剩余3GB用于存储用户进程数据

同样在虚拟内存中,1GB内核空间也会存在,不允许用户态访问:

仙士可博客

在创建运行进程后,高位->低位的1GB作为内核空间,

.text编译代码段 低位->高位固定

.data,.bss 静态代码段 低位->高位固定

启动成功后,环境变量 高位->低位 固定

命令行参数 高位->低位固定

同时 栈空间 高位->低位增长

堆空间 低位->高位增长

中间存放进程运行时候的共享库数据

c语言实际操作

#include<stdio.h>
#include <stdlib.h>
int main(){
    int a = 1;
    printf("栈内存变量地址a:%p \n",&a);

    int a1 = 1;
    printf("栈内存变量地址a1:%p \n",&a1);

    int \*b = (int \*)malloc(8);
    if (b==NULL){
        printf("申请内存失败\n");
        return  0;
    }

    printf("动态变量申请的堆地址b:%p\n",b);

    int \*b1 = (int \*)malloc(8);
    if (b1==NULL){
        printf("申请内存失败\n");
        return  0;
    }

    printf("动态变量申请的堆地址b1:%p\n",b1);
    return 0;
}

运行结果

/Users/tioncico/CLionProjects/cTest/cmake-build-debug/cTest
栈内存变量地址a:0x7ffee2096928 
栈内存变量地址a1:0x7ffee2096924 
动态变量申请的堆地址b:0x7fbaf1c059e0
动态变量申请的堆地址b1:0x7fbaf1c059f0

Process finished with exit code 0

可看到,在函数内一开始确定好类型,

声明的变量,将分配栈空间,栈的地址从高位到低位 (6928->6924,4个字节)

而动态分配的变量则是在堆空间,堆的地址从低位到高位 (59e0->59f0  ,16个字节,不知道为啥,可能是中间有存储其他数据)

如果在申请b1之前增加free,则会看到b1的内存地址跟b一样,因为b的内存地址已经被释放了,可以继续存储b1:

仙士可博客

正文到此结束
本文目录