代码样例:
#include
2 #include
3 #include
4
5 int get_val=100;
6 int main()
7 {
8 pid_t ret =fork();
9 if(ret<0)
10 {
11 perror("fork fail");
12 return 0;
13 }
14 else if(ret == 0)
15 {
16 int cnt = 0;
17 while(1)
18 {
19
20 printf("这是父进程 get_val:%d get_val地址:%p getpid:%d getppid:%d\n",get_val,&get_val,getpid(),getppid());
21 sleep(1);
22 cnt++;
23 if(cnt ==5)
24 {
25 get_val = 200;
26 printf("get_val 100 -> 200 success\n");
27 }
28 }
29
30 }
31 else
32 {
33 while(1)
34 {
35 printf("这是子进程 get_val:%d get_val地址:%p getpid:%d getppid:%d\n",get_val,&get_val,getpid(),getppid());
36 sleep(1);
我们来看一下运行结果
发现当在五秒钟之后get_val的值改变但是父子进程的地址不变。这就很疑惑了。父子两个进程变量不一样但是为什么地址一样呢?
这个问题之前有一个问题需要我们明白什么是地址空间?
上面的运行结果我们产生了一下的问题和结论:
同一个地址同时读取的时候读出来不同的值——这里的地址一定不是物理地址。而是线性虚拟地址。所有的语言地址都不是真实物理地址,而是虚拟地址。
例如显卡等有多个寄存器都有独立的物理地址模块。有物理隔断,通过虚拟地址来帮他们串联在一起,这样子用户在看的时候会看到他们是一个整体。
############################
对于进程本身来说,进程只能看到操作系统想让他看到的,进程之间彼此独立,每个进程都认为他能访问全部的地址空间。
内存本身是可以随时被读写的,很不安全,万一被恶意读写非常不安全。为了防止这种不安全的行为,我们不能直接使用物理地址,我们要使用虚拟地址。通过某种映射机制来访问物理内存。但是光有映射关系也解决不了,映射到非法的物理地址。这里在映射关系中加了一个过滤对于非法地址,会禁止映射。变相的保护物理内存。
将一段空间隔离开来,划分彼此的start和end。所谓的区域划分就是在一个范围内定义start和end。
地址空间是一种数据结构,他里面至少要有:各个区域的划分[可能会浮动],划分通过struct mm task_struct。每个区域的和物理内存之间的映射关系通过一种表结构来进行,这种结构叫做页表。页表和地址空间是每个进程都私有一份。只要保证每一个页表映射的是不同的物理地址。就可以保证进程的独立性。fork修改之后出现一个地址两个结果是因为虽然地址相同但是他们所映射的物理地址不相同。在修改的时候会发生写时拷贝,,重新开辟空间,数据拷贝,修改映射关系。
编译器在编译的时候,形成可执行程序没有被加载到内存中的时候,我们程序内部有地址吗?
有地址了已经。地址空间不止os需要遵守,编译器也需要遵守,在编译的时候就形成了各个代码区。并且采用了和linux内核一样的编址方法,给每一个变量,每一段代码都进行了编址。
编译器内部使用的时候使用虚拟地址,当加载到内存中的时候,每个变量有了一个物理地址。【外部地址】,当加载到内存中的时候cpu看到的是物理地址还是虚拟地址呢?也是虚拟地址。
地址空间和页表,最初的数据是哪里来的?
为什么要有地址空间?
1.泛式非法的访问或者映射,os都会识别到,并终止这个进程。有效的保护了物理内存。页表是地址空间和os维护的,凡是想要使用地址空间和页表进行映射,也一定要在os的监管之下
2.因为有地址空间的存在,可以对物理内存中任意位置进行加载。物理内存的分配和进程的管理就可以做到没有关系。内存管理模块和进程管理模块完成了解耦合。所以在malloc和calloc的时候都是虚拟地址。本质上只有你在真正对物理地址空间进行访问的时候,才执行相关的的物理内存管理算法,帮你申请,构建页表映射关系。然后让你进行内存的访问。延时分配提高效率。
3.在物理内存中可以任意加载位置,几乎所有的数据和代码在内存中都是乱序的。但是页表的存在可以虚拟地址和物理地址进行映射。所有的内存分布都可以是有序的。可以通过这种方式实现进程的独立性。
本质上就是创建进程,但是并不是把程序和代码立马加载到内存里面。最极端的情况只有pcb内核结构被创建。理论上可以实现对程序的分批加载。需要哪部分加载哪部分。