1.程序地址空间的引入
fork()函数在调用的时候子如果是子进程则返回0,如果是父进程则返回子进程的pid,在代码中我们分别在子进程和父进程读取全局变量g_val的时候居然出现了俩个不同的值。如下:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int g_val = 0; int main() { pid_t id = fork(); if(id == 0) { g_val = 100 int i = 5; while(i--) { printf("我是子进程,g_val值为:%d,g_val的地址:%p\n",g_val,&g_val); sleep(1); } } else { int i=5; while(i--) { printf("我是父进程,g_val值为:%d,g_val的地址为:%p\n",g_val,&g_val); sleep(1); } } return 0; }
执行结果:
[xvjiyi@hecs-399428 20240712_pragramAddressSpace]$ ./mytest
我是父进程,g_val值为:0,g_val的地址为:0x7ffd52f7b964
我是子进程,g_val值为:100,g_val的地址为:0x7ffd52f7b964
我是父进程,g_val值为:0,g_val的地址为:0x7ffd52f7b964
我是子进程,g_val值为:100,g_val的地址为:0x7ffd52f7b964
我是父进程,g_val值为:0,g_val的地址为:0x7ffd52f7b964
我是子进程,g_val值为:100,g_val的地址为:0x7ffd52f7b964
我是父进程,g_val值为:0,g_val的地址为:0x7ffd52f7b964
我是子进程,g_val值为:100,g_val的地址为:0x7ffd52f7b964
我是父进程,g_val值为:0,g_val的地址为:0x7ffd52f7b964
我是子进程,g_val值为:100,g_val的地址为:0x7ffd52f7b964
这个时候我们发现明明读取的地址相同为什么会读出俩个不同的值呢?既然读出了俩个值,说明这俩个值存在不同空间里,定然不可能地址相同,唯一的解释就是这个地址不是真实的地址。事实也正是这样,这里读取到的地址是虚拟地址,虚拟地址空间也就是程序地址空间的产物,接下来我们来看程序地址空间,以及虚拟地址的实现机制。
2.程序地址空间
2.1虚拟地址空间介绍
程序地址空间本质是一个结构体(我们认为是 struct mm_struct),这个结构体记录着不同内存区间的在这个程序地址空间的相对地址(不是真正的物理地址),映射关系如下:
2.2页表
当进程访问地址的时候访问的是虚拟地址,那我们最终是怎样访问到物理地址的呢?当进程地址空间创立时,操作系统会建立一张页表完成虚拟地址到物理地址的映射,如下:
2.3 子进程父进程访问全局变量出现俩个不同值
接下来我们回到开头的问题,为什么我们会在同一个地址上访问到俩个不同的值。这是因为子进程继承了父进程的地址空间,父子进程在访问这个全局变量时访问的虚拟地址是一致的,而这俩个虚拟地址映射的物理地址不同,而我们的数据是存放在物理内存中的,也就造成了同一个地址出现了俩个不同值的假象。(至于为什么会存放在不同的物理内存中在下一节进程控制中给大家讲解,为了确保进程的独立性相互之间不影响,发生了写时拷贝)。
3.扩展
3.1为什么要有地址空间
如果没有进程地址空间,物理内存就可以被随意访问,可以随意改动不属于自己的空间,系统安全性大大降低。我们有了地址空间有以下优点:
- 1.当进程访问到不属于该进程的空间时,操作系统可在页表映射时拒绝进程的访问,有效杜绝了物理地址被随意访问,大大保证了物理内存和其他进程的安全。
- 2.将进程管理与内存管理解耦合。
- 3.让进程以统一的视角看待自己的代码和数据。
3.2 malloc申请空间的实质
当进程使用malloc申请空间时,在访问页表的时候操作系统不会给你立马分配物理地址,而会发生缺页中断,在进程需要访问这块空间时才给你分配空间。
3.3程序的加载
程序编译在加载的时候并不是一股脑全部加载到物理内存中,而是执行多少加载多少,一边执行一边加载。(源代码在被编译时早就将源代码和数据按照程序地址空间的方式完成了编址)