一、打破误区
在C语言学习中,我们接触的物理内存这样的:
假定这就是物理内存, 那么下面的现象怎么解释。一个变量在父子进程中打印出来的地址是相同的,且修改val的值,子进程的val值没有改变,改变是父进程的。
因此证明这并不是物理内存,如果是物理内存,相同地址,一个进程修改,一个进程中的值还是原来的值。
C语言一直以来讲的内存布局有一个自己的名字—进程地址空间。
这里留一个问题:为什么解释一个进程修改,一个进程中的值还是原来的值,是什么原理?可以思考一下🤓🤓
二、Linux中的进程地址空间
2.1 进程地址空间是一段虚拟地址的范围,例如32位系统,从0x00000000到0xffffffff, 进行区域划分,从而分出栈、堆、代码段、共享区等等。由于这样是一种线性结构,因此也叫线性空间。在Linux中,是用mm_struct来表示进程地址空间。
页表
是一种key-value类型的表,进程地址空间和物理内存与页表建立映射,实现虚拟地址转换成物理地址。它的功能不仅如此,能够设定权限(读、写、执行),从而保护物理内存数据。
解答一下上面遗留的问题
是发生了写时拷贝,创建子进程,子进程会继承父进程大部分pcb以及代码段和数据段,还有页表的映射关系。指向的同一块物理内存,当发生数据修改的时候,OS会将修改一方的进程的数据在其他地方开辟空间,并拷贝下来并修改,然后改变页表的映射关系(并未改变虚拟地址)。从而达到父进程修改val的值,子进程的val值不变。
三、为什么存在地址空间
3.1 保护数据和物理内存
从反面来看,没有进程地址空间的话,访问物理内存采用的是物理寻址方法,进程是一个接一个加载到内存中,一个程序出现了越界这类的问题,很大概率会修改另一个进程的数据,这样无法保护进程的独立性。
3.2 进程管理与内存管理解耦合
有了进程地址空间,我不需要关注程序是加载到内存的什么位置,我只需要在进程空间所划分的区域内进行操作,这样分成进程管理与内存管理两部分。
3.3 统一视角看待代码
遵守虚拟地址的不仅仅是OS,编译器也需要。源码在编译过程中, 函数地址按照虚拟地址的方式进行编址。运行程序时,CPU从入口函数开始读取,不断运行,当需要call地址时,地址都是虚拟地址,从而虚拟地址空间与页表建立映射,再通过虚拟地址找到内存相应函数的位置,被CPU运行,不断重复这样的步骤,可以做到边加载边运行。