空间结构
这样的空间分配图我想很多人都见过,那么可能有的人对这个不同区域空间增长的概念还没有清除的认识,下面我用代码来演示各种类型的变量地址的分布情况。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int g_unval;
int g_val=100;
int main()
{
const char* s="hello world";
printf("code add: %p\n",main);
printf("string add:%p\n",s);
printf("uninit add:%p\n",&g_unval);
printf("init add:%p\n",&g_val);
char* heap=(char*)malloc(10);
char* heap1=(char*)malloc(10);
char* heap2=(char*)malloc(10);
char* heap3=(char*)malloc(10);
printf("heap add: %p\n",heap);
printf("heap1 add: %p\n",heap1);
printf("heap2 add: %p\n",heap2);
printf("heap3 add: %p\n",heap3);
printf("stack add: %p\n",&s);
这段代码里面有未初始化全局变量,已初始化全局变量,常量,堆上开辟的空间,以及栈上的空间,让我们看看运行结果如何
运行结果正如图中所描述的,代码区地址最低,栈区地址最高,堆上空间开辟是由低到高的,而栈是相反方向开辟空间的。
进程虚拟地址空间
上面我们演示出来显示的地址是不是就是在内存上的地址分布呢?下面我们通过一段代码来测试一下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 0;
}
else if(id == 0)
{ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
从运行结果我们可以看到这里确实父子进程全局变量打印出来的值和地址都是一样的,这也能证明子进程能够继承父进程的数据和代码,那么如果做出如下调整呢?
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_val=100;
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
return 0;
}
else if(id==0)
{
//child
g_val=200;
printf("child[%d]:%d:%p\n",getpid(),g_val,&g_val);
}
else
{
printf("parent[%d]:%d:%p\n",getpid(),g_val,&g_val);
}
return 0;
}
这里我们在子进程中,将全局变量的值进行改变,运行结果如下所示:
这里我们看到,在子进程中g_val的值已经改变,但是其地址仍然指向原来的地址,显然一个实际的物理内存地址不可能对应两个不同的值,那么这里我们看到的地址是什么呢?
在Linux地址下,这种地址叫做虚拟地址,我们在使用C/C++语言所看到的地址,全部都是虚拟地址,物理地址用户无法直接看到,由操作系统进行管理。操作系统会将虚拟地址和物理地址进行相互映射。
虚拟地址空间
地址空间的本质是内核中的一种数据结构在Linux系统下叫做mm_struct。每个进程都会认为自己独占整个内存空间。虚拟地址是地址空间上进行区域划分时,对应的线性位置。
页表+MMU(内存管理单元,用来查页表):映射表,将虚拟地址和物理地址相对应。
地址空间存在意义
- 通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了,保护物理内存和各个进程的数据安全
- 将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和操作系统进行内存管理操作,进行软件上的分离
- 站在CPU和应用层角度,进程统一可以看作统一使用4GB空间,而且每个空间区域的相对位置,是比较确定的
- 程序的代码和数据可以被加载到物理内存的任意位置
当一个进程想要申请内存的时候,操作系统可能并不一定会直接在物理内存上申请需要的内存空间,而会进行判断当前进程是否需要使用这些空间(判断空间是否被使用是依据是否对空间进行读写操作),真正需要使用的时候操作系统才会到物理内存上申请空间,否则就只是在虚拟地址上开辟了空间并没有产生真正的物理映射。