场景引入
在我们学习各种语言时,通常会听到内存会被划分成一下区域:
我们以下面这份代码做一个实验:刚开始时,我们打印val的值和地址,因为子进程没有修改数据,所以父子进程共享一份代码,所以刚开始时,我们打印的值时一样的。但当cnt==3时,子进程修改了g_val的值,因此会发送写时拷贝,此时父子进程打印g_val的值和地址应该是不一样的。但结果会是我们设想的那样吗?
运行结果后发现:g_val修改后,打印的值是不同的,但地址是相同的!
为什么父子进程的g_val的值相同,地址却不同呢?因为语言程序所使用的空间不是物理内存,而是进程地址空间!若是同一个物理内存,根本不可能存在同一块空间,却是不同的值。
进程地址空间
进程并不是直接使用物理内存的,而是使用进程地址空间,再通过页表映射到物理内存。我们创建进程时,除了创建pcb,还会创建一个结构体—进程地址空间变量(mm_struct)。创建进程地址空间的目的是让进程以为自己独占操作系统。
因此进程使用空间的逻辑上是这样的:
所以在第一份代码中,父子进程的g_val的值不同而地址相同,是因为g_val在父子进程的进程地址空间的地址是相同的,但父子进程的g_val分别映射到物理空间的地址肯定是不同的。
理解进程地址空间是什么之后,那么为什么需要进程地址空间呢?
- 直接操作物理内存是不安全的因为不安全。程序要是非法访问或破坏数据,会严重威胁计算机安全。所以引入进程地址空间是为了将程序和物理内存隔离开,让操作物理内存的操作交给操作系统。
- 将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软件上面的分离。例如:当我们申请1000个字节的空间时,如果你不立即使用这个空间(即不使用读写操作),这个空间并不会立刻在物理内存上开辟给你。因为如果你申请了空间,但是不使用,别的进程也使用不了这段空间,这就造成了空间浪费。所以,当我们申请空间时,只会在进程地址空间上开辟一个“虚拟空间”,告诉你这段空间已经申请了;当你真正通过读写操作来使用这段空间时,才会在物理内存上开辟这段空间给你,并通过写时拷贝将数据写入这个物理空间。
- 有了地址空间,cpu就可以去地址空间的固定位置去读代码和数据,再通过页表映射,找到真正的代码和数据;有了地址空间和页表映射,这样磁盘加载数据到物理内存时,就可以加载到物理内存的任意位置,减小内存管理的压力(cpu读数据只要去固定位置读,找数据交给操作系统,通过地址空间和页表去找)。如果没有地址空间,cpu读各个进程的代码和数据,就要去不同的物理空间去找,增加cpu负担。