今天的博文可能会有点枯燥,由于博主的水平有限,希望大家指正博文的缺点
进程地址空间是什么?
大家在学习语言阶段应该都听到过内存的概念,那么大家脑子里的固态思维就有这样一张图:
堆用于存储动态内存开辟,栈用来存放临时变量,全局变量的区域存放临时变量,常量区存储常量字符串等具有常量性质的代码
现在我们再把它具体化一点:
我们用一段代码来尝试一下到底符不符合这个图的规则:
window系统会出现随机的状况,所以建议大家再linux上尝试
#include<stdio.h> int x1 = 2; int x2; int main() { int data = 1; int data2 = 2; char* p = (char*)malloc(5); char* p1 = (char*)malloc(5); char* s = "abcd"; printf("初始化全局变量地址:%p\n", &x1); printf("未初始化全局变量地址:%p\n", &x2); printf("局部变量data地址:%p\n", &data); printf("局部变量data2地址:%p\n", &data2); printf("动态开辟内存p地址:%p\n", p); printf("动态开辟内存p1地址:%p\n", p1); printf("常量地址:%p\n", s); 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 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; printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val); } else { //parent sleep(3); printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val); } sleep(1); return 0; }
大家先将两段代码进行比对发现,我在子进程将g-val的值进行了修改
输出结果就发现了一个神奇的现象:
同一个变量,地址相同,却有两个不同的值!
这是为什么呢?
这就引出了一个概念:
进程地址空间
上面两段代码的结果给出了结论:
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量!
虽然打印出来的地址是一样的,但是这绝对不是真的地址,只是一个假的地址,说明打印出来的地址并不是在物理内存上的地址!
在Linux地址下,这种地址叫做 虚拟地址。我们在用C/C++语言所看到的地址,全部都是虚拟地址!
大家要记住,物理地址用户是一定看不到的,是由操作系统统一进行管理的,并且通过一系列的操作将这个虚拟地址转换为真实的物理地址!
其实进程地址空间只是一个比较抽象的存在,它本质上是一种将各个区域划分的内核数据结构,分别与各自的特定进程关联起来,并且各个进程都有自己私有的一份进程地址空间
所以大家还得记住,大家印象当中的那张图不是真实的内存!
为什么要有进程地址空间?
大家可能会有疑惑,为什么我要平白无故地创造出一个进程地址空间呢?这不是在在误导我们吗?
我们知道进程有几个特性分别为:
竞争性,独立性,并发性和并行性
我们假设一种情况:
如果没有进程地址空间的存在,直接让给进程将地址然后传递命令给CPU,如果给出的这个地址是个错误的地址呢?那不就会出现程序异常的情况吗?那么我们前面也提到了操作系统也会将虚拟地址转化为内存物理地址,那么如何将其转化为物理地址呢?
通过我们的原理就知道了!
进程地址空间的原理
页表!
页表是一种数据结构,他就类似于哈希表,是采用一种映射的方式,将虚拟地址映射到物理内存上,然后获取真正的物理地址
给出一张图大家就可以明白了
当虚拟地址时非法地址时,我们的页表就禁止映射到物理内存上,这就保证效率和安全
由于每个进程之间都有私有的地址空间和页表,所以各个进程之间也都不知道互相的存在!
所以,进程地址空间和页表的出现真正意义上保证了一个进程的独立性!
同时由于页表的映射,也使得内存分布变得有序!
那么大家想一想,当我们在C语言或者C++中,申请一块空间时,是真正直接在物理内存上开辟一块空间吗?
答案是不是!
其实我们申请空间本质上是在虚拟地址空间上申请空间,等到真正需要对内存进行访问时,才在物理内存上申请空间,同时操作系统自动完成建立页表等任务。
我们把这种技术叫做延迟分配,这样提高了CPU和内存的使用效率。
大家都知道,父子进程的代码都是共享的,父子进程不写入的时候数据都是共享的,那么当父子进程的任意一方需要进行写入的时候给如何操作呢?
这个时候就有一个技术叫做写时拷贝
写时拷贝是将需要被访问的或者需要被写入的数据拷贝一份副本让该进程进行写入,从而实现了将父子进程的数据完全分离,进程之间实现互不干扰!
也就是说只需要不被写入的数据可以不用拷贝
大家请看下图:
大家可以看到只读的代码数据父子进程还是共享,但是需要写入的代码数据就有两份,父子进程实现了分离,各自在物理内存上有一块内存地址
所以说,写时拷贝支撑了进程地址空间的存在,保证了进程的正常运行!
好了,今天的分享到这里就结束了,感谢大家的支持!