研究背景:kernel 2.6.32 32位平台
进程地址空间
在之前进程地址空间我们学习的是用户空间:
可以通过下面的代码验证空间分布:
#include<stdio.h>
#include<stdlib.h>
int g_val=100;
int g_unval;
int main(int argc,char*argv[],char*env[])
{
printf("代码区:%p\n",main);
printf("全局初始化区:%p\n",&g_val);
printf("全局未初始化区:%p\n",&g_unval);
char*mem=(char*)malloc(10);
printf("堆区:%p\n",mem);
printf("栈区:%p\n",&mem);
printf("命令行参数首地址:%p\n",argv[0]);
printf("命令行参数尾地址:%p\n",argv[argc-1]);
printf("环境变量地址:%p\n",env[0]);
}
对于父子进程,它们的变量地址是相同的:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
但是如果此时通过子进程对变量进行修改:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
g_val=100;
while(1)
{
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
sleep(1);
}
}
else{ //parent
while(1)
{
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
sleep(1);
}
}
return 0;
}
可以看到这里的全局变量只有子进程被修改了,而父进程的全局变量并没有被修改,这也就说明了虽然父子进程的全局变量地址是相同的,但并不是同一块物理地址。
综上,我们可以得到下面的结论:
- 进程地址空间并不是真实的内存,如果是内存那子进程就可以修改父进程的全局变量。
- 既然不是真实的物理内存地址,那么我们在语言层面上打印出来的地址,都叫做“虚拟地址”。
而真实的物理地址,用户是看不到的,由操作系统统一管理 - 子进程继承父进程的虚拟地址,所以父子进程访问的数据虽然虚拟地址的地址号相同,但是都被保存到了不同的物理内存中。
- 只有物理内存有保存数据的能力。
虚拟地址
操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
如果程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。
所以对于上面的程序,一开始父子进程的虚拟地址映射的是同一块物理内存地址,但是当子进程尝试写入的时候,操作系统就会将子进程映射到另一块物理内存地址上,这样即使子进程对变量进行了修改也无法影响父进程。在这个过程中,子进程的虚拟地址是没有发生改变的,变的只是子进程虚拟地址和物理内存地址的映射关系。
为什么要有虚拟地址
如果没有虚拟地址,那么我们进行访问的地址都是物理地址。
- 由于进程地址空间不隔离,程序都是直接访问物理内存,所以恶意程序可以随意修改别的进程的内存数据,以达到破坏的目的。
- 存在很多情况,一个进程的数据存放的内存位置是不连续的,所以访问会不方便,并且增加了异常越界的概率。
- 程序运行的地址不确定。当内存中的剩余空间可以满足程序的要求后,操作系统会在剩余空间中随机分配一段的空间给程序使用,因为是随机分配的,所以程序运行的地址是不确定的。
至于如何映射,则是通过页表来完成:
一开始父子进程映射的都是同一块物理内存:
当父进程或子进程需要修改数据时,会将父进程的数据在内存当中拷贝一份,然后再进行修改,再映射到另一块物理内存,这种方式叫做写时拷贝:
通过这种方式,一个程序就没法越界访问另一个程序的地址空间了,因为页表中没有这种映射关系。
另外,之所以有的数据是只读的,有的数据是可读可写的,是因为页表当中对每个地址都有相关的权限管理。
补充内容
虚拟地址空间存的价值:1.通过虚拟内存将空间连续化处理,2.保护内存
为什么要使用写时拷贝:进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。
为什么不在创建子进程时就进行拷贝:子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。
什么是地址空间:地址空间的本质是一个数据结构,它里面包含的是区域信息,从而实现区域划分。
申请空间的本质:向内存索要空间,得到物理地址,然后在特定区域申请没有被使用的虚拟地址,建立映射关系,返回虚拟地址。