关于进程虚拟内存
内存和系统
大家都知道,内存在计算机中是有限资源,它大概是一个这样的东西:
在计算机中,根据内存条容量,从而转换成了一个以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的内存映射
页表
内存页 (Memory Page):操作系统定义的进程申请内存的最小单位,根据页进行管理内存映射,一般默认为 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个字节,可能是页大小为16b)
如果在申请b1之前增加free,则会看到b1的内存地址跟b一样,因为b的内存地址已经被释放了,可以继续存储b1:
- 点赞
- 收藏
- 关注作者
评论(0)