高地址 |
stack(栈区) |
共享区(静态储存区) |
heap(堆区) |
未初始化 |
已初始化 |
代码区 |
低地址 |
思考:地址空间是内存吗?如果不是内存,那又是什么呢?下面我们创建一个父子进程验证一下。
1 #include<stdio.h>
2 #include<unistd.h>
3 int global_value=100;
4 int main()
5 {
6 pid_t id=fork();
7 if(id<0)
8 {
9 printf("fork error\n");
10 return 1;
11 }
12 else if(id==0)
13 {
14 int cnt=0;
15 while(1)
16 {
17 printf("我是子进程,pid:%d, ppid:%d|golobal_value:%d,&global_value:%p\n",getpid(),getppid(),global_value,&global_value);
18 sleep(1);
19 cnt++;
20 if(cnt==10)
21 {
22 global_value=300;
23 printf("子进程已经更改了全局的变量了-----\n");
24
25
26 }
27 }
28 }
29 else
30 {
31 while(1)
32 { printf("我是父进程,pid:%d, ppid:%d|golobal_value:%d,&global_value:%p\n",getpid(),getppid(),global_value,&global_value);
33 sleep(2);
34 }
35
36 }
37
38 }
运行结果:
根据运行结果,我们创造了一对父子进程,并创建了全局变量,并打印全局变量的值和地址,我们知道父子进程具有独立运行的特性,所以不难证明当子进程全局变量golobal_value 改变为300,而我们的父进程全局变量golobal_value 依旧为100,但是让我们疑惑的是子进程全局变量改变后,父子对象的地址保持不变,这是为什么呢?
答:该地址并不是物理地址,是虚拟地址,又叫做进程地址空间。是操作系统给进程的划定的一个“大饼”,
那我们来聊一聊操作系统怎么“画饼的”:
操作系统内部的创造一个地址空间结构体(类似pcb),结构体内包括地址空间的所有类型,优先级等。进程地址在32位系统下,所以拥有2的32次方的地址,4gb。每一个都是虚拟地址。heap和stack的调整大小本质上就是修改各个区域地址的开始(start)和结束(end)。
如图,当我们引入一个变量(例如char c=a;),赋予的地址为虚拟地址,我们拿着虚拟地址通过数据结构中的特定页码结构来映射出对应的物理地址。这一套流程全部为操作系统自动完成。
了解上面内容后,我们来揭晓上面父子进程地址空间相同而数值不同的谜底吧!
如图,父进程进行时,会创造一个地址空间(0x60105c),通过页码访问真正的物理内存存放100数据。子进程为父进程的拷贝数据,所以原理一样。当我们改变子进程数值为300时,因为进程具有独立性,一个进程对被共享的数据做修改 ,如果影响了其他进程,不能称之为独立性。所以操作系统为了保证进程的独立性,任何一方尝试写入,0s先进行数据拷贝,更改页表的映射,有映射到不同的物理内存处,这种方式叫做写时拷贝。进程等于内核数据结构+进程对应的代码与数据。
思考:为什么存在地址空间(虚拟地址)?
1、如果让进程之间直接访问物理内存,万一进程越界非法操作,非常不安全。即当你使用非法地址访问时,会被操作系统拦截,继而保护了我们的物理内存。
2、地址空间的存在,可以更方便的进行进程和进程的数据代码的解耦,保证了进程独立性这样的特怔。
3、让进程以统一的视角,来看待进程对应的代码和数据等各个区,
思考:可执行程序里面有没有地址呢?(有没有加载到内存?)
编码器编译你的代码的时候,就谁按照虚拟地址空间的方式进行我们的代码和数据进行编址的。
当程序加载到物理内存中的时候,该程序对应的指令和数据,天然的物理地址了。