之前对c语言中的各个变量在内存中的位置大概有了了解。但我们可能对它并不理解。我们不知道这个地址空间究竟是什么。
这个地址空间是内存吗?
不是内存,那是什么呢?
系统中,只要是一个进程就要被操作系统管理,只要被操作系统管理,那么创建子进程时,就要拷贝父进程的内核数据结构,比如子进程需要创建PCB,否则无法对子进程管理。
父子进程谁先跑不一定,由系统调度器决定
- 见现象
#include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<stdlib.h>
5 int global_value=0;
6 int main()
7 {
8 pid_t id=fork();
9 if(id<0)
10 {
11 printf("fork error\n");
12 return ;
13 }
14 else if(id==0)
15 {
16 int n=0;
17 while(1)
18 {
19 printf("子进程, pid:
%d,ppid:%d|global_value:%d,&global_value:%p\n",getpid(),getppid(),global_value,&global_value);
20 sleep(1);
21 n++;
22 if(n==10)
23 {
24 global_value=300;
25 printf("子进程全局变量已经更新了\n");
26 }
27 }
28 }
29 else if(id>0)
30 {
31 while(1)
32 {
W> 33 printf("父进
程,pid:%d,ppid:%d|global_value:%d&global_value:\n",getpid(),getppid(),global_value,&global_value);
34 sleep(2);
35 }
36 }
37 sleep(1);
38 return 0;
39 }
父子进程global_value值不同,地址相同。
多进程读取同一个地址的时候,怎么可能出现不同的结果?(继续往下看,答案在后面)
地址没变,打出来的值不同,说明地址一定不是物理地址,物理地址相同打出来的值一定相同,所以之前语言阶段学习的地址(指针)不是物理地址。而是虚拟地址(线性地址)[逻辑地址]
- 感性理解虚拟地址空间
进程认为自己独占系统资源,实际并不是,(设计时的理念)
漂亮国有个大富翁有10亿美金,有三个私生子,(私生子彼此不知道对方存在),大儿子是工厂老板,二儿子是金融机构的CEO,三儿子在MIT读书,大富翁告诉大儿子,要好好工作,等到自己老了不行后将10亿美金都给他,大富翁又告诉二儿子,要经营好告诉,等自己老了不行时把10亿美金给他,大富翁又告诉小儿子,要好好读书,等自己老了不行时把10亿美金都给他。当大富翁老了,三个儿子都想要这笔钱,但他们只能找各种理由每次要一点,尽力去索要那10亿美金。
这里,大富翁就是操作系统,三个儿子就相当于三个进程,三个儿子每次要的钱相当于是这个进程申请的内存或对象空间。 大富翁画的三个大饼(要好好工作,等到自己老了不行后将10亿美金都给他)相当于进程地址空间。
计算机的很多理念不是凭空产生的,而是来源于生活。
- 系统如何画饼
画饼的本质是在大脑中绘制蓝图,相当于一个结构体对象,
所以地址空间本质是内核的一种数据结构--mm_struct
struct mm_struct中应该有哪些成员呢?
1.地址空间描述的基本空间大小是字节
2.32位下,2的32次方个地址
3.2的32次方个字节约为4GB
4.每个字节都有一个地址
- 如何理解区域划分
小时候,我们上学时,为了和同桌有相同的桌子面积,我们用尺子在桌子上画出一道线,这个过程相当于一个区域划分。
- 如何 理解区域调整
此时男生因为较胖,想为自己多争取点空间,女生答应了请求,于是在原先桌子中间的线两旁,设置了10cm的缓冲区。
2的32次方个地址所占的4GB空间相当于这个区域,我们用unsigned int 类型表示地址。
mm->code_start相当于一个区域的起始地址
mm->code_end相当于一个区域的结束地址
这些地址都是虚拟地址。所谓的区域调整,就是改变 start和end的值。
我们定义局部变量,malloc和new相当于扩大栈区和堆区;
函数调用完毕,及free相当于缩小栈区或堆区。
各个进程在4GB的空间,被分配不同的区域,相当于大富翁为儿子画的大饼,即共分10亿美金。
- 证明此结构
源码
如何让进程找到内存中的代码和收据,用页表。页表用来把虚拟地址和物理地址进行映射。此过程由操作系统自动操作。虚拟地址是连续的,也叫线性地址。
内存和磁盘I/O过程,基本单位是4kb。
每个进程认为自己独占2的32次方地址。其实是虚拟地址,而且这虚拟地址,进程也不会全用到。
为什么要用页表映射地址呢?
可以看做是小时候家长为自己管理压岁钱,怕乱花,家长相当于页表,起到拦截作用。
为什么存在地址空间?
1.如果让进程直接访问物理内存,万一进程越界非法操作呢?非常不安全 。
地址空间让我们写代码出现错误时,比如出现野指针时,并不影响内存及物理地址。
2.可以更方便的进行进程和进程之间数据代码的解耦,保证了进程得独立性。(下面进行解答)
- 回顾一下开篇那个global_value问题
父进程开辟子进程后,子进程相当于父进程的拷贝,子进程在父进程的虚拟地址处开辟,即虚拟地址不变,此时父子进程的物理地址也是相同的,数据也相同,当子进程更改数据时,因为进程具有独立性,一个进程对被共享的数据进行修改时,如果影响了其他进程,不能称子为独立性,所以当任何一个进程要对共享数据进行修改时,操作系统首先要重新在内存上为这个进程开辟一个物理内存空间,然后把原先数据拷贝到新的空间里面,然后将子进程页表物理地址更改为指向新的物理地址,然后把要更改的变量进行更改。此过程和虚拟地址无关。上层用的虚拟地址的同一区域,底层通过页表被映射到了物理地址的不同区域。此时我们看到虚拟地址一样,但内容却不一样。
我们把任何一方尝试更改数据时,操作系统先进行数据拷贝,再更改页表的映射,然后让进程修改的过程叫写时拷贝。
操作系统为了保证进程的独立性,做了很多工作,通过地址空间,页表,让不同进程映射到不同物理内存中。
进程=内核数据结构+对应的代码和数据,通过写时拷贝让不同进程代码和数据独立,不同进程这俩个都是独立的,保证了进程的独立性。
问题:子进程刚被创建时的物理地址,页表,虚拟地址和数据,和父进程都是一样的,那怎么保证父子进程独立性的?
答:父子进程都有它自己独立的进程,虚拟的空间的,虽然说最开始的时候子进程和父进程一样,页表的映射关系也是一模一样的,但是它是两个独立的页表,各有各的页表,当发生数据修改的时候,会有一个写时拷贝的存在。父子进程发生数据修改时是独立的。进程的独立性主要体现在数据修改时是独立的,而不是体现在代码和数据一样。
3.让进程以统一的视角看待进程对应的代码和数据等各个区域,方便编译器也以统一的视角编译代码。
问题:一个可执行程序,在磁盘中还没有加载到内存时有没有地址?
1.汇编指令有地址。说明程序在汇编阶段(汇编,编译,链接,可执行)代码就有地址,链接就是把库函数中的地址填入到可执行程序中,让程序运行时能找到库函数,此地址是逻辑地址。
2.虚拟地址不仅操作系统会遵守,编译器也会遵守。编译器编译代码时,就是按照虚拟地址空间的方式对代码和数据进行编址的。程序的代码区和数据区是以32位地址编址的。
main函数调用fun函数,是在代码的内部进行跳转的。
函数加载到内存后,函数内部的东西不变。
这些地址是编译阶段就有的, 栈空间和堆空间编译生成可执行程序时,这些地址没有,因为它们是在内存中动态申请的,
3.上面说的地址是程序内部的地址。是虚拟地址(逻辑地址)。代码要占空间,要在内存中保存,当程序被加载到物理内存中后,该程序对应的指令和数据,都天然具有了物理地址。
当程序加载到内存后,这些函数和变量可以相互通过虚拟地址(逻辑地址)找到。 这些变量和函数都有了物理地址。
我们现在有了俩套地址
- 标识物理内存中代码和数据的地址
- 在程序内部互相跳转用的地址--虚拟地址。
操作系统通过输入到内存中的程序的虚拟地址和大小,给定程序地址空间的区域 。
CPU中有个pc指针,叫程序计数器,它也是读取和输出虚拟地址的。
整个CPU访问过程中,CPU没有见到物理地址。
所以平时debug程序,运行起来时,CPU内部寄存器用的就是虚拟地址。调试时查看的是虚拟地址。
编译器写程序时32位和64位程序指的就是程序编译时虚拟地址(逻辑地址)按32还是64位进行编。
两种程序中逻辑地址编码方式,上面较新,是线性编辑的,下面较旧,是靠偏移量编辑的。