可以仔细看一下这张图片,同一个数同一个地址,却出现不一样的值,这是怎么回事,这里就要用到我们今天的主题地址空间,上面这个现象我们会在后面讲,因为如果不知道地址空间,就没法理解上面的现象。
首先我们要明白,上面的地址绝对不是物理内存的地址,当然这里我就直接说明是虚拟地址。
背景:32位的平台下
首先我们来讲一下地址空间是如何设计的。
这里就要用到这张图了,地址空间的设计就是这样的,当然我们接下来会写一段代码验证。
1 #include<stdio.h>
2 #include<stdlib.h>
3 int g_unval;
4 int g_val=100;
5
E> 6 int main(int argv,char* argv[],char* cnv[])
7 {
8 printf("code addr: %p\n",main);
9 printf("init global addr: %p\n",&g_val);
10 printf("uninit global addr: %p\n",&g_unval);
11 char* heap_mem=(char*)malloc(10);
12 printf("heap addr: %p\n",heap_mem);
13 printf("stack addr: %p\n",&heap_mem);
14
E> 15 for(int i=0;i<argc ;i++)
16 {
E> 17 printf("argv[%d] %p\n",i.argv[i]);
18 }
E> 19 for(int i =0 ;env[i];i++)
20 {
21 printf("env[%d]: %p\n",i.env[i]);
22 }
23 return 0;
24 }
这个便是结果,从这个结果我们不难看出,完全符合上面那张图。
至于图中那里那个共享区那边有两个方向,其实就是堆和栈申请使用地址的方向。
接下来就要说一下什么是地址空间。
其实这里我们可以想如果使用物理地址空间的话,会出现一种什么情况呢。
首先我们将可执行程序加载到物理内存中,形成进程假设出现了野指针问题,该怎么办,这就非常的不安全,所以我们就要对他进行改进,当然这里不是只有一个野指针问题,还有其他的,我就不一一列举了,反正你只要知道她并不安全就是了。
所以就要用到我们的虚拟地址空间。
看上面这张图,虚拟地址空间是如何与物理地址空间联系起来的,就是应为他们多了一个页表,cpu通过访问虚拟地址空间,然后从页表通过映射到物理地址空间来获取数据,当然这里就要有人说了最终还是访问到物理地址空间了呀,这不没什么区别吗,其实你想如果你映射过去是越界的,我可以不让你访问就是了,这样不就保护了物理地址空间。
地址空间是一种内核数据结构,他里面有区域的划分,这个大家可以去看源码。
地址空间和页表是每个进程私有一份的,,只要保证每个进程的页表,影射的是物理内存的不同区域,就能保证进程之间不会相互干扰,保证进程之间的独立性。
最后我们来回答一下最开始的现象。
为什么同个地址出现不同的值,这是因为创建的子进程大部分的数据是和父进程一样的,所以他们的虚拟地址虽然是一样的但是当内容改变时,就会发生写时拷贝,就是说物理内存不一样。
地址空间深层次理解
当我们的程序,在编译的时候,形成可执行程序的时候,没有被加载到内存中的时候,我们程序内部,有地址吗,其实已经有地址了,可执行程序其实编译的时候,内部已经有地址了。
地址空间不要仅仅理解成为是OS内部要遵守的,其实编译器也要遵守,即编译器编译代码的时候,就已经给我们形成了各个区域代码区,数据区,.....并且,采用和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址,故,程序在编译的时候,每一个字段早已经具有了一个虚拟地址。
程序在运行前就已经有地址了(程序内部的地址)当程序的每一行代码,加载到物理内存中,就有了物理地址,这里就相当有了两套地址,一套是程序自己用的,另一套是操作系统通过页表查找我们代码的并且这个东西在编译期时就已经定好了。
为什么要有地址空间
这里我们讲三个理由,但是其实还有很多,这里就不一一列举了。
1.凡是非法的访问或者映射,0S都会识别到,并终止你这个进程,有效的保护了物理内存吗,因为地址空间和页表是OS创建并维护的,是不是也就意味着凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来进行访问,也便保护了物理内存中的所有的合法数据包括各个进程,以及内核的相关有效数据。
2.因为有地址空间的存在,因为有页表的映射的存在,我们的物理内存中,是不是可以对未来的数据进行任意位置的加载,当然可以。物理内存的分配就可以和进程的管理,可以做到没有关系,内存管理模块vs进程管理模块就完成了解耦合。
所以,我们在C、C++语言上new,malloc空间的时候,本质是在哪里申请的呢,物理内存还是虚拟地址空间,如果我申请了物理空间,但是如果我不立马使用?是不是空间的浪费呢,是的,本质上,(因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,物理内存可以甚至一个字节都不给你而当你真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系),然后,在让你进行内存的访问是由操作系统,自动完成,用户,包括进程,完全0感知。延迟分配的策略,来提高整机的效率,几乎内存的有效使用是100%的。
3.因为在物理内存中理论上可以任意位置加载,那么是不是物理内存中的几乎所有的数据和代码在内存中是乱序的!但是,因为页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,那么是不是在进程视角地址空间+页表的存在可以将内存分布,有序化!地址空间是OS给进程画的大饼。
结合第2条:进程要访问的物理内存中的数据和代码,可能目前并没有在物理内存中,同样的,也可以让不同的进程映射到不同的物理内存,是不是边很容易做到,进程独立性的实现。进程的独立性,可以通过地址空间+页表的方式实现!因为有地址空间的存在,每一个进程都认为自己拥有4GB空间(32),并且各个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性!!每一个进程不知道,也不需要知道其他进程的存在。
理解挂起
理解什么是挂起加载本质就是创建进程,那么是不是必须非得立马把所有的程序的代码和数据加载到内存中,并创建内核数据结构,建立映射关系,在最极端的情况下,甚至只有内核结构被创建出来了。新建状态,理论上,可以实现对程序的分批加载,既然可以分批加载,可以分批换出吗,当然可以喽,甚至,这个进程短时间不会再被执行了,比如阻塞了,进程的数据和代码被换出了,就叫做挂起了。