目录
测一测代码
在之前我们通过调用fork()函数创建子进程,我们看到之后同一个变量同一地址可以输出不同的值,那么这个地址真实的物理地址吗?我们所说的内存布局是物理内存布局吗?答案是:不是,那它是什么?我们以这个为切入点来谈进程地址空间。
首先我们来见见代码:
在代码中我们定义一个全局变量,在创建子进程后,子进程对这一变量进行写入修改变量的值,而父进程则不改变变量的值。代码在刚运行时子进程的g_val和交进程的g-val输出的值是一样的它们的地址编号也是一样的,但随着程序的运行,子进程将g_val的值自增了,也就是改了,可是父进程的g_val还是没变,但父进程的g_val和子进程g_val的地址编号从始至终都是一样的,也就是子进程对全局变量数据的修改并不影响父进程,这是因为进程具有独立性,那么操作系统为了让进程具有独立性它做了什么?我们知道进程=内核数据结构+代码和数据,从代码和数据上父进程的代码和数据应与子进程的代码和数据互相不干扰,也就是父子进程对数据的修改应不影响对方,那应该怎么做?就是写时拷贝,关键在于怎么做到写时拷贝?
按照我们以前的认知子进程的g_val和父进程的g_val值不同,他们应该时不同的变量,可是我们通过取地址输出地址编号发现他们地址编号一样,也就是说他们是同一个变量,假设这是物理地址,那么父子进程在读取变量的值时,不可能做到值不一样,所以我们可以得出结论:该地址编号一定不是真实的物理地址编号,也就是我们在语言层面上的地址,不是物理地址。
这个地址编号我们称为虚拟地址编号或线性地址。
引入地址空间
故事:假设有一个非常有钱的大富翁他有10亿美金,他的私生子很多,并且私生子之间都不知道对方的存在,都以为自己是这10亿美金的唯一合法继承人。
有一天大富翁对A说你好好做生意,将来10亿美金是你的,过了几天他又对B说,你们化装品做的挺好, 销量的也不错,好好干,等老爹西去10亿美金就都给你了。
同样他也给这C和D都说过类似的话,也就是画大饼。有一天A对大富翁说我要投资某某生意需要5万美金做启动资金,大富翁将5万金与自己的10亿美金一对比觉得5万美金就是一小钱,就批准了,同样有一天C也向大富翁说自己没生活费了,一听小钱打就给C打了生活费,过几天D打电话给大富翁说给我5亿美金,我有点事需要摆平一下。大富翁一听说:一边去!ABCD都认为自己未来会有10亿美金。而大富翁还在世,他知道ABCD这四个儿子不会一下子就问自己拿10亿美金。当然如果其中有人问了他也可以拒绝。
在上面的故事中呢:画的饼叫地址空间,大富翁对应操作系统,而10亿美金是内存,ABCD表示多个进程。
上面的故事: 对应到我们的知识就是操作系统管理着一定容量的内存,而进程可以向操作系统申请内存、操作系统判断申请的内存大小是否合理从而通过或拒绝申请。
如果大富有40个私生子,那么大富翁要不要把“饼”管理起来?故事中的“饼”在系统中称进程地址空间,而进程地址空间本质就是一个内核数据结构。struct mm_struct{ ... };记录每个进程所申清的内存。
在故事中富翁并不担心其中某一个儿子一次向自己拿10亿,因为即使他拿了自己也可以拒绝,他也不会让四个儿子所要的钱口一起来超过10亿美金,因为他会管理,结合到我们平时写的代码中我们也不会一次申请过多的空间,因为操作系统会拒绝。
task_struct{ ... };中会有一个指针指向进程地址空间,地址空间也是是一个结构体,结构体中放的是区域划分的信息,而区域中的数据最终都会放到物理内存中,就好比以上, 故事中的在ABCD的脑海中10亿美金是虚拟的,而真正的10亿美金是放在画具体的某一家银行中。
线性地址中的代码区、数据区、堆区区域如何理解?再来说一个故事,我们在上小学时,小胖和小花是同桌,小胖不讲卫生,在小学时女生发育比男生早,所以小胖害怕小花,小花嫌弃小胖,小花将100厘米的桌子进行区域划分,小胖小花各占一半,说从此往后我们井水不犯河水,不能越界否则我揍你,小胖被迫接受,小花划线的本质是区域划分!
在计算机述语中区域划分:限制起始和结束即可。
对线性区域进行指定start和end 即可完成区划分,到这里上面的故事和地址空间又有什么关系?
如果我们限制了区域,那区域[1000,2000]之间的数据是什么。这些数据就是线性地址也叫虚拟地址。
故事再继续、在区域划分完后,小花一直在好好学习,而小胖坐得东倒西歪,时常将书和衣服扔到小花的区域,有一天小花忍不住了就打了小胖一顿,然后又将区域向小胖边移了30厘米一 小胖只能在剩下的20厘米区域从事学习活动、小花的行为叫扩大区域对小胖为缩小区域,在计算机语中就是修改小花的end值和小胖的start,在实际中,操作系统的实际实现很复杂。那虚拟地址如何对应到物理地址?
通过找到虚拟地址而管理该虚拟地址对应的物理内存的内容,这里我们先理解虚拟地址通过页表+MMU映射后转为了物理地址。当我们知道虚拟地址通过页表+MMU就可以知道这个虚拟地址对应的物理地址上的内容,然后将地址对应的内容放到CPU中去执行等。
写时拷贝的理解
父进程PCB有一个指针指向虚拟地址空间,全局数据在全局数据区中有4字节的空间大小,这个数据有自己的起始地址编号假设为0×1123344,然后进程都有对应面的页表+MMU,当我们调用fork()函数后,操作系统以父进程PCB为模板创建PCB也就是创建子进程,子进程也继承了父进程的虚拟地址空间。而且全局变量g_val在进程A 和A的子进程上的地址编号是一样的,在映射到相应物理地址上,在不修改数据大小情况下,A进程和A的子进程在读取时都指向同一物理地址。当子进程要修改g_val数据大小时,它不会直接在物理地址上修改,因为这时如果直接在物理地址上修改了,父反进程在读取g_val时得到的是子进程改后的数据。这不符合进程具有独立性的做法。所以不能这样修改, 那操作系统是如何做的呢?它会在物理内存上拿出一块和变量g_val一样大的内存,将子进程想要修改为的数据放入其中,然后再修改页表的映射关系子进程的虚拟地址编号不变,而修改映射到物理内在的编号,从而使子进程和父进程虽然虚拟地址一样但物理地址不一样,达到变量的地址(虚拟地址)相同而,变量的值不同,(本质是物理地址不同)。
现在我们可以来说说为什么fork()函数返回值不同。fork()在返回时,操作系统已经建好子进程,父子进程都会执行return语句,也就是返回两次,id是pid_t类型定义的变量,返回的本质就是写入,父进程在返时对blid写入一次,而子进程在返回时又写入这时发生了写时拷贝。
扩展
1为什么要有虚拟地址?
我们假没没有虚拟地址空间,操作系统是如何工作的!虚拟地址空间是计算机发展的产物,在早期是没有这一概念的,在早期操作系统是这样的。
在早期时,要执行一个可执行程序,可执行程序会直接加载到物理内在,在物理内存上放着可执行程序的代码和数据,CPU在执行某一个程序时拿着程序按顺序执行,当启动另一个可执行程序时,这个可执行程序会被加载到上一个可执行程序的尾部紧挨着,因为要避免内存浪费。CPU通过PCB去执行相应的代码字段,但如果代码中有寻址操作,CPU在拿到这个指令后就去寻址了,比如读取某个变量,如果我们的代码有问题,那么CPU会找到另一个进程的物理内存中去,如果代码是写习操作时,将会影响另一个进程的数据。所以这种方式我们担心的是野指针问题,一个进程访问到另一个进程的上下文导致进程出现问题,无法保证进程独立性。当有了虚拟地址空间后呢,我们要访问物理内存,通过页表+MMU映射找到相应的地址,如果操作不合理就会被拦截也就是野指崩溃。所以虚拟地址空间可以防止一个进程去防问其他进程的内存地址,保中物理内存与其他进程。
2.malloc的本质
我们思考一个问题,我们在向操作系统申请内存时操作系统是立即给我们,还是在需要时再给呢?
答案是我们在需要时操作系统才会给我们相应的物理内存。
因为:
1、操作系统不允许资源浪费.
2、申请内存下一定立即使用
3、在申请成功后,和在你使用(写入数据)之前,这一小段时间内这个空间没有被正常使用,但是别人使用不了,造成了资源浪费,这是OS不允许的,所以我们只有在使用时操系统才会给我们相应的物理内存。我们申请成功时,是在虚拟地址空间上申请成功,并在我们使用前这一虚拟内存并未映射到物理内存上,这就是缺页中断的初步理解。
3.重新理解地址空间
我们的程序再被编译时,没有被加载到内存,程序内部有内部有没有地址?有的,因为源代码在被编译时,就是按照虚拟地址空间的方式对代码、数据进了对应的编址,不要认为虚拟地址空间只会影响OS,编译器也会遵守这样的规则?也就是代码在被编译时,其中的变量函数已分配了相应的虚拟地址。当程序被加载到物理内内存、指令和函数就会有对应的物理地址。当程序开始执行时, 在用户角度、程序从main()函数开始通过虚拟地址页表整映射到物理内存中找到相关指令,这个指令是用某个函数时CPU会得到函数的虚拟地址空间又通过页表映射找到物理内存上找到函数代码的实现。CPU读到的地址是虚拟地址,程序在编译是就有了天然的虚拟地址,在加载到内存时又有了相应的物理地址。虚拟地址可以让进程以统一的视角看待代码和数据。
虚拟地址空间是什么?为什么?
虚拟地址空间是一种在操作系统内部为进程创建出来的一种数据结构对象,用来让进程以统一视角看待物理内存,因为看了虚拟地址空间存在,可以让进程管理和内存管理独立起来。并且因为有虚拟地址空间的存在,可以让程序在编译时代码和数据也按照虚拟地址空间排布好,这样程序在加载到物理内存时CPU在读取代码时,读到的是虚拟地址然后再经页表映射找到相应物理地址。
为什么?
1.保据物理内存
2.解耦
3.让进程以统一视角看待内存