先看一个程序和现象
预期现象是,子进程和父进程相互独立,子进程的gval是100,101,102....而父进程一直都是100.
结果我们并不意外,只是我们发现,父子进程的gval的地址是一样的,这有点颠覆我们的认知,同一个地址,怎么会有两个不同的值,我们只能认为,这个地址不可能是物理意义上的地址,我们叫虚拟地址。
一、进程地址空间是什么
我们确定,虚拟地址不是物理地址,但它和物理上的内存也有一定关系,假设我们现在有许多进程运行,每一个进程都会占用一部分内存空间,这是物理意义上的,但是,我们让每一个进程都认为自己独占整个物理内存大小,即各进程间不知道彼此的存在。那么进程地址空间就是操作系统告诉每一个进程自以为独占的那块“空间"(实际并不一定存在故是虚拟)
管理进程地址空间依旧是先描述再组织,进程地址空间,本质上是一个内核数据结构对象(PCB),名为struct mm_struct。每次进程访问物理内存时,都通过这个空间进行访问的。
我们的物理内存空间分为栈堆等空间,本质是讲内存划分为若干个区域,实现方法类似于定义变量然后通过下标来划定范围。在整个内存空间内,每一个字节代表一个地址,一共有2的32次方个,地址本质其实也是数字,可以被保存在unsigned long中。对于不同区域的地址,只需要记住每个区域的头和尾就可以了,中间随便用。我们 用图片再解释一下进程地址空间到底是怎样的。
首先,我们的进程的每一行代码都是有地址的,因为需要占空间,这里是指物理地址。这里出现一个页表,是操作系统为了维护每一个进程构建的,里面存放着虚拟地址和物理地址。对于一个进程来说,假设我们有一个全局变量,那么就会在内存有一个物理地址,同时,会在虚拟地址的初始化区也有一个地址(不一定相同),这两个地址就构建了映射关系。我们进程加载时,会把其中的虚拟地址返回给用户,我们是看不到真是的物理地址的。反过来也一样,写入数据时,先占虚拟地址,然后通过虚拟地址的映射关系找到物理地址进而就可以成功写入了。这也就是Linux的虚拟内存管理方案。
当创建子进程时,会拷贝父进程的代码和虚拟空间,实际上此时指向的物理空间也是一样的,也就说如果我们不做数据的修改,父子的数据也是共享的,但当我们修改时,子进程的物理内存讲改变(重新找一个地方),但此时的虚拟地址不变,也就导致了我们一个虚拟地址指向了两个物理地址。
二、关于虚拟空间mm_struct
1.初始化
我们知道,只要是结构体变量就必须要进行初始化,但虚拟空间是如何进行区域划分的初始化呢?我们通过查看某个进程发现,在其编译时,代码已经被分程好几段了,包括代码大小,地址等信息,而我们的mm_struct就是根据可执行程序初始化而划分每个区域的大小的。
三、虚拟地址存在的意义
虚拟地址+页表的方式可以有效的保护内存,其实,这个页表不仅保存着虚拟地址和物理地址以及映射关系,每个地址还存在两个标记,一个是标记着这个地址(物理)是否有某种权限(rwx),还有一个是检测该虚拟地址是否对应着虚拟地址,如果我们进行访问且该地址没有某种权限就会报错,以及访问不存在的内存地址等,这些风险都在访问页表的地址中已经得到解决。防止非法访问等问题。 我们的野指针就是属于访问虚拟地址所对应的不存在的地址而报错。
其次,我们要让进程以统一的视角看待物理内存,代码和数据加载到物理内存的任意地方,就可以用虚拟地址把特定信息加载到内存的特定位置(映射)。至于全局变量,其实就是其地址一直持续到进程结束,所以才能全局使用。