什么是进程地址空间?
我们来看一下这个例子:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
int main()
{
int val=1000;
printf("I am father process,pid:%d, ppid:%d\n",getpid(),getppid());
sleep(5);
pid_t id =fork();
if(id==0)
{
while(1)
{
val=200;
printf("I am child process,pid:%d, ppid:%d, val:%d, &val:%p\n",getpid(),getppid(),val,&val);
sleep(1);
}
}
else
{
while(1)
{
printf("I am father process,pid:%d, ppid:%d, val:%d, &val:%p\n",getpid(),getppid(),val,&val);
sleep(1);
}
}
}
运行结果:
根据之前的学习我们已经知道进程是有独立性的 ,所以在我们可以理解父子进程中val的值可以不一样,但是我们发现父子进程的val变量的地址是一样的,这是为什么?
从上面的运行结果来看,我们得到的val的地址不是真实的地址 (物理地址),因为同一时间内一个物理地址中只能存储一个进程的数据,不同进程的不同数据不可能同时存在于同一个物理内存中;
实际上OS会给每个进程创建一个独立的虚拟地址空间,通过页表将虚拟地址空间与物理地址一一对应;用户只能得到虚拟地址空间中的虚拟地址,当我们修改虚拟地址中的数据时,操作系统会先通过页表找到对应的物理内存,然后修改物理内存中的数据。
所以父子进程的val变量的地址 其实就是虚拟地址;因为子进程完全的父进程的拷贝;在子进程的val没有改变时,父子进程中val的虚拟地址都是0x7ffc83547fa8,通过页表找到的物理内存也都是一样的0x1122334455,如果子进程对val进行修改,那么OS就会在物理内存中寻找一块新空间,然后将原空间的数据拷贝到新空间,再修改子进程的页表映射关系,(也就是将原来的0x112233445修改为现在的物理地址)最后再修改新空间中 g_val 的值,上述过程叫做 写时拷贝。在这个过程中,我们并没有修改val对应的虚拟地址,所以我们查询到的地址是一样的;
根据上述的例子,我们就可以知道进程地址空间就是虚拟地址空间;
注:在OS中,进程地址空间中的地址通常也被称为线性地址,因为它是按比特位从全0到全1依次顺序编址的;磁盘程序内部的地址通常被称为逻辑地址;在Linux中,三者的意思是一样的,都表示虚拟地址,不用过于区分。
为什么要有地址空间?
上面我们知道的什么是地址空间,了解的地址空间是怎么管理的,那为什么要有地址空间呢?为什么不能直接把数据存放到物理内存中,反而要通过页表,地址空间一一映射到物理内存中?
是为了保护物理内存不受到任何进程内地址的直接访问,保证了数据的安全性;在虚拟地址到物理地址的转化过程中方便进行合法性校验;
如果进程直接访问物理内存,那么看到的地址就是物理地址,而语言中有指针,如果指针越界了,一个进程的指针指向了另一个进程的代码和数据,那么进程的独立性,便无法保证,因为物理内存暴露,其中就有可能有恶意程序直接通过物理地址,进行内存数据的篡改,如果里面的数据有账号密码就可以改密码,即使OS不让改,也可以读取。
- 进程地址空间保证了数据的安全性
OS会给每个进程创建一个独立的虚拟地址空间,通过页表将虚拟地址空间与物理地址一一对应,当用户对某一进程的虚拟内存越界访问或者非法读取与写入时,页表或操作系统可以直接进行拦截,从而保证了内存中数据的安全
- 进程地址空间保证了进程的独立性
对于互不相关的两个进程来说,它们都拥有自己独立的地址空间以及页表,页表会映射到不同的物理内存上,磁盘代码和数据加载到内存中的位置也不同,一个进程数据的改变不会影响另一个进程;
以上是进程地址空间的一部分,后续内容会慢慢补充!