Linux概念——进程地址空间
<1> 进程地址空间
对于C语言学习,我们并不陌生这张图 我们编写一段C语言代码来验证上述图的准确性
#include<stdio.h> #include<stdlib.h> int g_val = 100; int g_unval; int main(int argc, char* argv[], char* env[]) { //环境变量 printf("env addr:%p ", env[0]); printf("env addr:%p ",env[1]); //命令行参数 printf("argv addr:%p ",argv[0]); printf("argv addr:%p ",argv[argc-1]); char* mem = (char *)malloc(sizeof(char)*4); //栈 printf("Stack addr:%p ",&mem); //堆 printf("Heap addr:%p ",&mem); //未初始化数据 printf("uninit addr:%p ",&g_unval); //初始化数据 printf("init addr:%p ",&g_val); //正文代码 printf("code addr:%p ",&main); return 0; }
结果
可以看到对于程序中定义的变量打印出来的地址全部都一一对应了
Q1:什么是地址空间?
地址空间是对物理内存的一种虚拟化表示,在Linux内核中以结构体的形式划分为不同的区域。
<2> 虚拟地址空间
既然前面说到了进程空间是一种虚拟化表示,我们运行下面代码来验证进程地址空间
#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 ", getpid(), g_val, &g_val); } else { //parent printf("parent[%d]: %d : %p ", 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; printf("child[%d]: %d : %p ", getpid(), g_val, &g_val); } else { //parent sleep(3); printf("parent[%d]: %d : %p ", getpid(), g_val, &g_val); } sleep(1); return 0; }
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!
能得出如下结论:
-
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量 但地址值是一样的,说明该地址绝对不是物理地址! 在Linux地址下,这种地址叫做虚拟地址。 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
Q2:为什么要有地址空间?
- 如果没有地址空间,那么进程访问的地址都是物理地址,当一个进程的指针去读写另一个进程的地址时,出现野指针现象,导致别人的空间遭到破坏,从而违背了进程的独立性原则。
- 进程数据存放的位置时不连续的,因此访问难度较大,也增加了异常越界的概率。
Q3: 地址空间时怎么工作的?
- 既然地址空间时虚拟的,那么OS必须负责将 虚拟地址 转化成 物理地址 , 这中间就需要建立映射关系——页表来进行转换。
- 由操作系统去访问物理内存,人为干扰的进程非法行为将被禁止,堆内存起到了保护作用。
<3> 页表
上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!