目录
1.程序地址空间
1.1.程序地址空间的介绍
1.2.程序地址空间的本质
2.进程地址空间
3.Linux下的地址空间
1.程序地址空间
1.1.程序地址空间的介绍
我们在学习C/C++时,对于各组分的地址分配在程序地址空间的不同模块
如图我们能够验证各组分的对应的地址排布位置,但是我们可以看到堆是向上排布,栈是向下排布这个怎么验证呢?
所以我们验证了堆的开辟是有下到上,栈的开辟是由上到下,堆栈相对而生,并且我们可以发现堆栈之间有一块大的空白(这也是栈溢出发生的原因)
另外,在栈区中,我们知道地址是由高到低排布的,然而一个整型变量 int a; 由4个字节构成,我们知道一个地址对应着1个字节,那么可以推出int类型实际上由4块连续的地址来实现的,那么int类型在栈上是如何排布的呢?存在两种情况:&a是低地址 或者 高地址
接下来我们看看数组的地址分布
我们看到arr[1]在arr[0]上边,也就是只有地址向上排布时,才能够符合。
那么就有“栈空间的开辟是自上向下,而空间内变量的使用是自下向上增长”
回到对int a的分析
那么以最低地址为起始地址,也就是&a的本质就是这4个字节的最低的那个地址,对于double类型就是8个字节的最低地址,也就是通过 &变量名 + 变量类型 来确定对应变量的地址空间。
换一句话来说:地址空间通过“起始地址+偏移量”来访问不同的变量类型
讲了这么多,大家也只是知道了程序地址空间,用来存储变量地址的空间
1.2.程序地址空间的本质
#include <stdio.h> #include <stdlib.h> int global_val = 200; int main() {pid_t id = fork();if(id<0){printf("fork error\n");return 0;}else if(id == 0){printf("it is child process, pid is: %d , g_val is: %d , g_val_address is: %p \n", getpid(), global_val, &global_val);}else{printf("it is father process, pid is: %d , g_val is: %d , g_val_address is: %p \n", getpid(), global_val, &global_val);}return 0; }
这里符合我们之前学习的,父子进程在没有对数据写入时,共用一份代码和数据,那么同一个变量也是在同一块地址。
在上述代码的基础上,我们在子进程模块将g_val值修改再来看看!!!
#include <stdio.h> #include <stdlib.h> int global_val = 200; int main() {pid_t id = fork();if(id<0){printf("fork error\n");return 0;}else if(id == 0){global_val = 100;printf("it is child process, pid is: %d , g_val is: %d , g_val_address is: %p \n", getpid(), global_val, &global_val);}else{printf("it is father process, pid is: %d , g_val is: %d , g_val_address is: %p \n", getpid(), global_val, &global_val);}return 0; }
与上部分代码相对比,我们发现一个地址,两个变量
我们在学习地址时,每一个物理地址只能对应一个变量,而这里的“0x60104c”却对应着两个不同的g_val的值,所以这个地址一定不是物理地址,而是虚拟地址。
也就是程序地址空间实际上是抽象出来的虚拟地址。
我们在之前fork()函数初识时,知道fork()成功调用时的返回值可以为id==0,id>0两个值,本质上就是子进程可以继承父进程的虚拟地址,也就是实际上id就可以看做上面的全局变量global_val,共享一块虚拟地址,那么他们的物理地址呢?这里我们先不多讲剩下的留在下面讲解。
2.进程地址空间
每一个进程运行之后,都会有一个进程地址空间的存在
我们在上面知道,程序地址空间实际上就是抽象出来的虚拟地址,实际上进程地址空间也是抽象出来的虚拟地址。
因为实际上的数据是存储在物理内存中的,而地址空间是抽象出来的模块,并没有存储数据的能力,也就是地址空间最终需要通过映射关系到物理内存,每一个进程都有一个映射表。
实际上父进程在创建子进程后,会和子进程共用同一个物理内存,当父(子)进程数据发生改变时,物理内存中会进行数据的“写时拷贝”,进而产生一块新的地址,并通过页表再映射回去同一个虚拟地址。这就是父子进程的地址空间会出现一个地址对应两个变量的原因,实际上还是两块物理地址。
3.Linux下的地址空间
地址空间实际上就是一个抽象的模块,连接着进程和物理内存,并且是由操作系统所管理的,那么不就是一个“先描述再组织”结构体吗?
我们发现通过抽象出进程的地址空间这个模块,对于所有的进程均是同一在这个数据块上实现的,并且按照代码中的每一个模块把每一部分都有序地放置在地址空间内,在通过页表映射在物理内存中。那么我们在磁盘中加载程序进入物理内存中时,就可以随意加载在物理内存的任意位置,因为我们只需要通过页表就能进入一个有序的进程地址空间,也可以在维护好的进程地址空间的映射找到对应的物理内存。
这里我们可以看出“地址空间有助于我们将无序的地址变为有序的进程的地址空间的控制”
另外,页表除了存放着虚拟内存和物理内存两个字段外,还存放这一个“物理内存访问权限”字段
除了有访问权限字段,页表中有一个字段用于判断物理内存是否分配,当我们在运行程序时,进程可能处于挂起状态,也就是代码和数据从物理内存放回磁盘中,这时候我们把这个字段的进行修改,表示这个进程不在物理内存中。那么进程在查询页表时发现这个字段为挂起,就能够在进程管理模块中判断该进程为“挂起状态”(进程的状态是进程管理,而挂起实际上是内存层面上的),又因为我们把地址空间和物理内存独立成块了。
所以地址空间的引入,实现了“进程管理和内存管理的解耦!!!”