文章目录
- 前言
- 一、程序地址空间
- 二、感受虚拟地址的存在
- 三、进程地址空间
- 四、程序从磁盘加载到内存的过程
- 4.1 物理地址和虚拟地址的区别
- 五、写时拷贝
- 5.1 解释fork()函数有两个返回值
前言
- 我们在学习C/C++的时候用到的地址是什么地址呢?虚拟地址?物理地址?
- 本文就来寻找一下答案~
一、程序地址空间
- 程序地址空间的空间布局图
- 从上面的图我们可以看出,程序地址空间中存在一些相关的区域:正文代码,初始化数据,未初始化数据,堆,共享区,栈,命令行和环境变量,内核空间,除了内核空间,其他空间都属于用户空间,所占的空间大小是3G
二、感受虚拟地址的存在
- 我们可以用fork进程创建一个子进程,然后再定义一个全局变量,然后父进程和子进程同时访问全局变量然后进行同时观察地址
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
int g_val = 100;
int main(){
pid_t id = fork();
if(id < 0){
perror("fork");
exit(-1);
}
else if(id == 0){
// child
printf("This is child[%d],%d:%p",getpid(), g_val, &g_val);
}
else{
// parent
printf("This is parent[%d],%d:%p",getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
- 可以观察到父进程和子进程的访问的地址和值是一样的,所以子进程和父进程共享同一个数据
- 再来观察一个现象
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
int g_val = 50;
int main(){
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){
g_val = 100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else{
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
- 我们发现,父子进程,输出地址是一致的,但是变量内容不一样!
- 我们可以得出一个结论,这里所指的地址不可能是物理地址,因为同一个地址只能是同一个内容,不可能出现同一个地址存放两个不同的值
- 其实这里的地址是虚拟地址,不是真正的物理地址
三、进程地址空间
- 其实在每一个进程建立的时候,操作系统不仅会为进程创建一个PCB,同时还会为每一个进程创建一个进程地址空间。
- 每一个进程都有自己独立的进程地址空间,那么这样系统中的进程地址空间就会非常多,操作系统就需要对这些进程地址空间进行管理和控制,而管理的本质就是先描述再组织,描述的意思就是为进程地址空间创建一个结构体。
- 在Linux系统中,有一个结构体叫做:mm_struct,每一个进程都是相对独立,互不影响的,每一个进程中的PCB和mm_struct都是相互独立的,这就是进程的独立性。
-
进程地址空间中的结构和前面讲的程序地址空间的结构一样,其中都包含正文代码,初始化数据,未初始化数据,堆区,共享区和栈区,还有命令行参数和环境变量
-
在实际中,每个区域都每一个区域都是由对应的
start
和end
来维护的,如果我们想改变对应区域的大小,我们可以通过设置对应区域的start和end进行修改即可,在每一个区域的start和end中会包含很多的地址,这个地址就是所谓的虚拟地址,不是物理地址,物理地址是存在于内存中的,不是存在进程地址空间的。
四、程序从磁盘加载到内存的过程
程序被编译但还没有被加载到内存时程序内部是否存在地址?
- 代码被编译形成可执行程序之后是存在对应的地址的,也就是说程序中的每一段代码在程序中的位置已经确定,这个地址是代码在程序中的地址,与内存中的虚拟地址是没有任何关系的
程序被编译但还没有被加载到内存时程序内部是否存在区域?
- 代码被编译成可执行程序之后,在可执行程序中是存在相关区域的,存在的区域有:正文代码,初始化数据区,未初始化数据区,命令行参数和环境变量,这时需要注意:并不存在栈区和堆区,栈区和堆区是要等程序加载到内存中才存在的
4.1 物理地址和虚拟地址的区别
- 物理地址是在代码在真正的内存中存在的地址(位置)
- 虚拟地址是指CPU直接能够访问到的地址,并不是相关代码在内存中的真实地址,这个虚拟地址的作用就是能够通过页表相关的映射关系转化成代码在内存中的物理地址
- 因此,我们一旦有一个代码的虚拟地址还有页表的映射关系,其实就相当于我们有了代码在内存中的物理地址,虚拟地址和物理地址是通过页表建立联系的
- 当一个进程运行起来的时候,每个进程都会分别创建PCB和mm_struct,每一个进程都独自拥有一个进程地址空间。
- 而页表是进程地址空间和物理内存之间存在的一个工具,主要作用就是负责利用其中虚拟地址和物理地址的映射关系实现虚拟地址和物理地址之间的相互转化,也就是说有了虚拟地址和页表,我就可以找到对应的物理地址,也就是相当于对应映射。
- 上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址
五、写时拷贝
-
写时拷贝是指当数据被修改的时候,系统会在内存中重新为该数据开辟一块新空间,将该数据原来的内存拷贝放到新空间,然后再在新空间对该数据进行修改
-
在我们前面的
感受虚拟地址的存在
的时候知道,父子进程访问同一个数据出现两个结果是因为有虚拟地址的存在,那么我们可以近一步讨论一下这个问题 -
当系统识别到子进程想要修改该数据的时候,系统会为子进程在内存的另一个地方开辟一块新的空间,然后将该数据原来的值拷贝放到新空间,然后再在新空间对数据进行修改,这个新空间就是该变量在内存中实际存在的物理地址空间,此时操作系统会更新子进程中的页表映射关系,其中改变的是页表中原先映射关系的物理地址,让原先的物理地址更新为更改后的物理地址,
-
因此,我们会发现,父子进程的页表中对该变量的虚拟地址是一样的,但是在子进程对该数据进行修改之后,子进程的页表被重新更新,更新之后映射出的物理地址就是不一样的,此时父子进程访问的其实是两个不同的物理空间中的内容,所以结果就会出现父子进程访问同一个虚拟地址出现不同的结果
在只读的情况下:
在写入的时候,进行写时拷贝
5.1 解释fork()函数有两个返回值
pid_d id
是属于父进程栈空间的变量,fork()
函数内部return会被执行两次,return
的本质就是将保存在寄存器上的值写入到接收返回值的变量中, 当id = fork();的时候,谁先返回,谁就要发生写时拷贝,所以,同一个变量,会有不同的内容,本质是因为这个变量的虚拟地址是一样的,但是会有不同的物理地址。
好了,本文就到这里,感谢大家的收看🌹🌹🌹