目录
简单理解的空间布局图及验证:
早期的内存管理机制
分段
分页
简单理解的空间布局图及验证:
用两段代码测试一下:
1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 int val = 100;5 int main()6 {7pid_t pid = fork();8if(pid < 0)9{10 perror("fork");return -1;11}12if(pid == 0)13{14 printf("child:[%d],%d,%p\n",getpid(),val,&val);15}16else17{18 printf("parent:[%d],%d,%p\n",getpid(),val,&val);19}20sleep(1);21return 0;22 }
输出来的变量地址一模一样,因为子进程按照父进程为模板,父子进程并没有对变量进行修改
1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 int val = 100;5 int main()6 {7pid_t pid = fork();8if(pid < 0)9{10 perror("fork");11 return -1;12}13if(pid == 0)14{15 val = 0;16 printf("child:[%d],%d,%p\n",getpid(),val,&val);17}18else19{20 sleep(3);21 printf("parent:[%d],%d,%p\n",getpid(),val,&val);22}23return 0;24 }
父子进程,输出地址一样,但是变量内容不一样
变量的内容不一样,所以父子进程输出的变量不是同一个但是地址一样,说明这绝对不是物理地址这是虚拟地址,我们所看到的地址全是虚拟地址,物理地址用户一概看不到,由OS统一管OS也必须负责将虚拟地址转化为物理地址
早期的内存管理机制
早期的计算机中要运行一个程序,会把这写程序直接运行在内存上,也就是说程序访问的内存地址都是实际的物理内存地址。
当计算机要同时运行多个程序时,必须保证这些程序用到的内存总量要小于实际的物理内存。
内存分配是实例:
要运行一个程序时,会把这些程序全部装入库内当计算机要同时运行多个程序时,必须保证这些程序用到的内存总量要小于实际的物理内存进程地址空间不隔离,由于程序都是直接访问物理内存,所以恶意程序可以随意修改别的进程内存数据效率低,如果剩下的空间不够,系统只能将一个运行的程序数据暂时拷贝的硬盘上,释放出空间才能使用运行地址不确定,当剩余空间满足程序要求后,系统会在剩余空间中随机分配一段连续的空间给程序使用,因为是随机的,所以不确定地址(程序的起始地址都是物理地址,而物理地址都是在加载后才能确定)
分段
为 了解决上述问题,人们想到了一种方法,就是增加一个中间层,利用一种间接的地址访问方法访问物理内存。按照这种方法,程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映 射,就可以保证不同的程序最终访问的内存地址位于不同的区域,没有重叠,达到内存地址空间隔离的效果。
当创建一个进程时,操作系统会为该进程分配一个4GB的虚拟进程空间地址
(在32位操作系统中,一个指针长度4字节,寻址能力为:0x00000000~0xFFFFFFFF,最大值位4GB的容量)
虚拟地址空间对应的物理地址空间,当创建进程时,每个进程都会有一个自己的4GB虚拟地址空间,(每个进程只能访问自己的虚拟地址空间中的数据,从而实现了地址隔离)
在windows中,这个虚拟地址被分成了四部分:NULL指针区,用户区,64KB禁入区,内核区(应用程序只能使用用户区,2GB左右(最大可以调整成3GB),内核区为2GB,内核区保存的是系统线程调度,内存管理,设备驱动等数据,这部分数据供所有的进程共享,但是应用程序不能直接访问)
分段的思想就是在虚拟地址空间和物理地址空间一一映射(比如说虚拟地址空间中某个10M 大小的空间映射到物理地址空间中某个10M 大小的空间,可以多对一,就是说可虚拟地址的多个10M大小的空间可以映射到相同的物理地址空间的某个10M)操作系统保证不同的进程地址空间被映射到物理地址空间中不同的区域,这样每个进程最终访问到的物理地址空间都是彼此分开的,通过这种方式实现了进程间的地址隔离。
假设有两个进程 A 和 B ,进程 A 所需内存大小为 10M ,其虚拟地址空间分布在 0x00000000 到 0x00A00000 ,进程 B 所需内存为 100M ,其虚拟地址空间分布为 0x00000000 到 0x06400000 。那么按照分段的映射方法,进程 A 在物理内存上映射区域为 0x00100000 到 0x00B00000 ,,进程 B 在物理内存上映射区域为 0x00C00000 到 0x07000000 。于是进程 A 和进程 B 分别被映射到了不同的内存区间,彼此互不重叠,实现了地址隔离。从应用程序的角度看来,进程 A 的地址空间就是分布在 0x00000000 到 0x00A00000 ,在做开发时,开发人员只需访问这段区间上的地址即可。应用程序并不关心进程 A 究竟被映射到物理内存的那块区域上了,所以程序的运行地址也就是相当于说是确定的了。(也即是说虚拟地址虽然重复,但是其映射的物理地址是唯一的)
分段的映射方式虽然解决的部分问题,但是还存在内存使用的效率问题,再分段映射中,每次换入换出的内存都是整个程序,会造成大量的磁盘访问操作,导致效率低下。
实际上程序运行有局部的特性,在某个时间内,只访问程序的一小部分数据,也就是说大部分在一段时间内不会被用到,所以人们想到了粒度更小的内存分隔和映射的方法----分页
分页
分页的基本方法是将地址空间分成许多页,每页的大小有cpu决定,然后由操作系统选择页的大小。4GB 虚拟地址空间共可以分成 1048576 个页, 512M 的物理内存可以分为 131072 个页。显然虚拟空间的页数要比物理空间的页数多得多。
在分段的方法中,每次程序运行时总是把程序全部装入内存,而分页的方法则有所不同。分页的思想是程序运行时用到哪页就为哪页分配内存,没用到的页暂时保留在硬盘上。当用到这些页时再在物理地址空间中为这些页分配内存,然后建立虚拟地址空间中的页和刚分配的物理内存页间的映射。
可执行文件 (PE 文件 ) 其实就是一些编译链接好的数据和指令的集合,它也会被分成很多页,在 PE 文件执行的过程中,它往内存中装载的单位就是页。当一个 PE 文件被执行时,操作系统会先为该程序创建一个 4GB 的进程虚拟地址空间。前面介绍过,虚拟地址空间只是一个中间层而已,它的功能是利用一种映射机制将虚拟地址空间映射到物理地址空间,所以,创建 4GB 虚拟地址空间其实并不是要真的创建空间,只是要创建那种映射机制所需要的数据结构而已,这种数据结构就是页目和页表。
创建完虚拟地址空间所需要的数据结构后,进程开始读取 PE 文件的第一页。在 PE 文件的第一页包含了 PE 文件头和段表等信息,进程根据文件头和段表等信息,将 PE 文件中所有的段一一映射到虚拟地址空间中相应的页 (PE 文件中的段的长度都是页长的整数倍 ) 。这时 PE 文件的真正指令和数据还没有被装入内存中,操作系统只是根据 PE 文件的头部等信息建立了 PE 文件和进程虚拟地址空间中页的映射关系而已。当 CPU 要访问程序中用到的某个虚拟地址时,当 CPU 发现该地址并没有相相关联的物理地址时, CPU 认为该虚拟地址所在的页面是个空页面, CPU 会认为这是个页错误 (Page Fault) , CPU 也就知道了操作系统还未给该 PE 页面分配内存, CPU 会将控制权交还给操作系统。操作系统于是为该 PE 页面在物理空间中分配一个页面,然后再将这个物理页面与虚拟空间中的虚拟页面映射起来,然后将控制权再还给进程,进程从刚才发生页错误的位置重新开始执行。由于此时已为 PE 文件的那个页面分配了内存,所以就不会发生页错误了。随着程序的执行,页错误会不断地产生,操作系统也会为进程分配相应的物理页面来满足进程执行的需求。
分页方法的核心思想就是当可执行文件执行到第 x 页时,就为第 x 页分配一个内存页 y ,然后再将这个内存页添加到进程虚拟地址空间的映射表中 , 这个映射表就相当于一个 y=f(x) 函数。应用程序通过这个映射表就可以访问到 x 页关联的 y 页了。