请问C/C++地址空间是内存吗?
其实不是,是虚拟地址空间!
我们见识见识虚拟地址空间。
#include
#include
int global_val=100;
int main()
{
pid_t id=fork();
if(id == 0)
{
int cnt=0;
while(1)
{
printf("我是子进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_val, &global_val);
sleep(1);
cnt++;
if(cnt == 10)
{
global_val=200;
printf("子进程已经改变global_val的值了。。。。。\n");
}
}
}
else if(id > 0)
{
while(1)
{
printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_val, &global_val);
sleep(2);
}
}
else
{
printf("fork error\n");
}
return 0;
}
fork之后,父子进程谁先运行,不确定,由调度器决定。
我们发现多线程在读取同一个地址的时候,怎么可能出现不同的结果?
地址没变----> 这里的地址,绝对不可能是物理地址。
曾经我们学习的语言的地址(指针),不是对应的物理地址,是虚拟地址(线性地址/逻辑地址)
打印出来的地址空间排布,全部都是虚拟地址。
每个进程都认为自己是独占系统资源的(事实上并不是),这知识一种理念。
son1 工厂周转不开了,找peter要1w美金,5w美金,
son2 要2k美金,买个手表
son3 要3k美金,生活费
不可能全要,因为peter也不会给。
peter和他每个私生子都说,等他死后,就让儿子继承全部家产。给儿子画大饼
那如何画饼呢?
假设500个员工---->画饼---->员工要被管理----->饼要不要管理呢?
肯定是要的,不然许张三当总经理,把这个大饼当成许给李四了。
500个进程---->地址空间------>进程要被管理------>地址空间也要被管理
如何管理呢?
先描述,在组织
地址空间的本质:是内核的一种数据结构!mm_struct
区域划分
有了地址区间就要对地址区间进行划分;
有属于代码的起始和结束的地址,属于数据的起始和结束的地址。。。
区域调整
进程在运行时,会有一个PCB,task_struct,操作系统结合地址空间malloc一个struct mm_struct。
代码和数据区本质上不变的
heap和stack所谓的区域调整,本质上就是修改各个区域的start/end的值。
定义局部变量,malloc,new----->扩大栈区或者堆区
函数调用结束,free------>缩小栈区或者堆区
进程地址空间和内存的映射
进程地址空间和物理内存之间是通过页表来进行映射,建立映射关系的。
每个进程到要有对应的地址空间
进程1看不到物理内存,进程2也看不到物理内存。
每个进程都认为自己由2^32个地址空间或者内存大小,实际上操作系统不会给那么多,只会给一些,但是依旧让每个进程都认为自己独占系统空间。
1.如果让进程之间访问物理内存,万一进程越界非法操作,是非常不安全的,有了操作系统提供的虚拟地址和页表,在进程越界非法操作时,页表就会提醒。所以页表不只是映射,还要拦截,所有进程都要遵守。
还记得上面,写的一段代码,创建子进程,然后子进程把global_val变量改了变成200,但是父子进程的虚拟地址还是一样的,并且父进程global_val值还是100;这是为什么呢?
创建子进程,是把父进程的PCB,地址空间,页表都给子进程一份。
还记得之前说过,进程具有独立性,如果一个进程对共享的数据做修改,影响了其他进程,这样就不能称之为独立性。
所以不管是父进程还是子进程修改了数据,为了保证进程的独立性,操作系统会在内存中申请一块空间,把要被修改的数据拷贝下来,修改其页表对应的映射关系,这时候再修改数据,就和其他进程没关系了。而打印出来的地址是一样的,是因为只修改了页表,并没有对进程地址虚拟空间进行变动。
任何一份尝试写入,os先进行数据拷贝,更改页表映射,然后再让进程进行修改。这叫做写时拷贝。
操作系统,为了保证进程的独立性做了很多工作-------通过地址空间,页表让不同进程映射到不同的物理内存处。
2.地址空间的存在,可以更方便的进行进程与进程之间的数据和代码的解耦,保证了进程独立性这样的特征
重新再一次理解地址空间
可执行程序里面,有没有地址呢?(再没有被加载到内存的时候)
是有的,早在汇编阶段就有地址,这也是逻辑地址
虚拟地址空间,你不要认为只有操作系统会遵守对应的规则,编译器也要遵守!编译器在编译你的代码的时候,就是按照虚拟地址空间的方式,对我们的代码和数据进行编址的。
上面说的地址,是我们程序内部使用的地址,程序加载到内存中,天然就具有了一个外部的物理地址!
可执行程序加载到内存中,就具备了两套地址;1.标识物理内存中代码和数据的地址;2.在程序内部互相跳转的地址---->虚拟地址;
物理地址加载到页表的右侧,由于可执行程序在编译的时候,也是按照虚拟地址的方式给代码和数据编制的,所以mm_strucrt直接就可以使用这类地址,并且加载到页表左侧。
再看看CPU是如何执行这些指令的。
找到main函数虚拟地址,通过页表去找对应的物理地址,然后往下执行代码,当读到fun函数时,取到fun的虚拟地址,然后也根据页表去找对应的物理地址。
cpu在运行进程的时候,读进来的都是指令,指令内部就有了地址(这是虚拟地址);然后在页表找对应的物理地址,去执行进程内部的指令,当跳转到别的函数或者数据时,会再次拿到地址(也是虚拟地址),然后再去找页表对应的物理地址,然后再执行指令。
CPU没有见到物理地址。
3.让进程以统一的视角,来看待进程对应的代码和数据等各个区域,编译器也以统一的视角进行编译代码,因为规则是一样的,方便了使用。
关于进程地址空间就到这了,这里比较难以理解,多看几遍。
喜欢的小伙伴点赞,评论,收藏+关注!感谢!