在之前学习C/C++的时候都会提到 地址这个概念,我们写代码时创建变量,定义函数等都会有其对应的地址空间。而地址空间也会有着各个区域的分布,例如:栈区、堆区、常量区等等。
那么我们之前所谈的这些地址空间是不是内存的空间呢,现在来写一段代码验证一下
#include<stdio.h>
#include<unistd.h>
int value = 100;
int main(){
pid_t id = fork();
if(id < 0)
return 1;
else if(id == 0){
int cnt = 0;
while(1){
printf("我是子进程,pid:%d,ppid:%d | value:%d,&value:%p\n", getpid(),getppid(),value, &value);
sleep(1);
cnt++;
if(cnt == 10){
value = 300;
printf("子进程已经更改了全局变量\n");
}
}
}
else{
while(1){
printf("我是父进程,pid:%d,ppid:%d | value:%d,&value:%p\n", getpid(),getppid(),value, &value);
sleep(2);
}
}
return 0;
}
以上代码,按照正常来看,如果子进程将全局变量value改变之后,那么接下来不管是子进程还是父进程打印value的值都应该是改变之后的值。接下来运行程序看看效果
问题出现了,为什么子进程改变了变量值之后,父进程打印该变量的值没有改变,并且他们打印出变量的地址都是一样的。这也就说明不同的进程在运行的时候指向的根本就不是同一块地址空间,也就是说他们指向的就不是内存的物理地址空间,那么他们指向的是什么呢?这就是我们接下来要学习的—虚拟地址空间(进程地址空间)
什么是进程地址
通俗的理解一下虚拟地址
每一个进程在运行的时候都是会以为自己独占了整个系统资源,但其实并不是这样的。进程所占的地址空间是操作系统给它们创建出来的空间,这就是虚拟地址空间。操作系统最后都会将进程所使用的虚拟地址空间通过页表映射到物理空间上从而找到对应的虚拟地址。
所以对于我们平常写程序而言,我们所使用的都是虚拟地址
有了这各认知之后,我们再去思考上面的问题,为什么两个进程打印出来的地址相同,值却不同
根据进程独立性的性质,可以知道子进程和父进程是两个相互独立的进程,也就是说它们之间做的任何事情都是互不干扰的。因为子进程是父进程创建出来的,所以子进程的地址空间是由父进程拷贝出来的,因此在子进程还未改变变量值之前我们看到的是同样的地址和同样的值
也就是说,父子两进程一开始通过页表映射到物理地址时所指向的是同一块的空间。随后当子进程去修改空间的值时,操作系统为了确保进程之间的独立性,会自动进行写时拷贝,重新开辟一块空间并把子进程修改后的变量值写入到该空间,随后子进程通过页表映射到物理地址中指向的就不再是原来的空间了,而是新开辟的这块空间。
因此为什么我们看到的是两个进程打印出来的值不一样。因为程序中打印地址打印的是虚拟地址空间,所以我们看到两个进程打印出来的地址是一样的。
下面来看看图解
未发生改变前:
发生改变后:
写时拷贝
对于写时拷贝是操作系统自动处理的一个流程
在数据第一次写入到某个存储位置时,首先将原有内容拷贝出来,写到另一位置处,然后再将数据写入到存储设备中,该技术只拷贝在拷贝初始化开始之后修改过的数据
为什么要有进程地址
主要分为三个方面来考虑
为了保护物理内存
如果让进程直接的去访问物理内存,那假如进程干一些“非法”的操作,例如越界访问等,那物理内存就直接崩溃了。就跟我们的shell命令外壳一样都是为了起到保护作用
更方便进行进程和进程数据的解耦
因为进程时具有独立性的,有了进程地址空间后,进程之间的数据就不会互相干扰确保了进程的独立性
统一视角
进程地址空间可以让进程以统一的视角去看待进程对应的代码和数据等各个区域,同时也方便使用的编译器也已统一的视角进行编译
操作系统怎么管理进程地址
谈到管理那肯定是统一的先描述,在组织。操作系统的本身会存在着大量的进程,为了管理这些进程,操作系统会使用内核数据结构去对每一个进程管理。进程的地址空间实际上是操作系统内核的一种mm_struct的数据结构,操作系统会为每一个进程创建一个mm_struct对象。
下面来看看关于mm_struct的代码
区域划分
对于空间里的区域划分,是结构体里的变量去决定的
struct mm_struct{
uint32_t code_start,code_end;
uint32_t data_start,data_end;
uint32_t heap_start,heap_end;
uint32_t stack_start,stack_end;
}
end - start就是这块区域的大小,所以例如我们利用new开辟一块在堆上的空间,其实就是在调整堆区域对应的end值