一、C语言内存管理基础
引入:以前我们知道一个指针指向的如果是一个常量字符串,那么这个就是指向的常量区,只读不可被修改,因此下面的程序会崩溃。
1、在我们C语言内存管理机制里面线性地址是有区域划分的。
我们如何验证这个区域的划分是否正确呢?
可以通过代码的方式在不同的区域创建变量然后来取地址获取再进行比较
2、栈区和堆区是相对而行的!!
验证栈区:地址在变低
验证堆区:地址在变高
3、静态变量会被定义在全局区 只不过只会在作用域里使用。
二、fork遗留问题
历史遗留问题:为什么一个变量可以同时等于0又同时>0 ??
实验:
我们会发现同一个地址竟然读到了不同的内容!! 如果变量的地址是一个物理地址,是绝对不可能出现这种情况的,因此我们的变量地址必然是不是物理地址!!
——>结论:我们平时C/C++里面使用的地址全都不是物理地址,而是虚拟地址! 用户是看不到物理地址的,而OS必须要负责将我们所看到的虚拟地址转化成物理地址!
三、进程地址空间
其实我们的之前所学的线性地址,并不是真正的物理内存,而是在PCB内部有一个指针指向了一块进程地址空间,然后虚拟地址会通过页表来映射到具体的物理地址。 ——>所以当我们创建出一个子进程后,他会拷贝一份和父进程一样的地址空间,然后当子进程想要修改对应的数据时,此时就会发生写时拷贝(由操作系统自动完成),也就是重新开辟空间,在这个过程当中只有页表对应的物理地址发生了变化,左边的地址空间不会有任何的感知。
3.1 什么叫做地址空间
在32位的机器中,有32位的地址和数据总线,所以每一根地址总线有0或1,其实从本质上来说计算机能够识别是高低电频而并非二进制,所以1代表的是高电频,0代表的是低电频。——>这个过程就是CPU通过像内存充电的形式告诉内存我需要哪个地址,然后内存就能够通过识别高低电频,形成一个物理数据,将地址对应的数据以同样的方式交给CPU。
所以地址空间就是地址总线排列组合形成的地址的范围【0,2^32】
3.2 如何理解地址空间的区域划分?
举个例子:比方说当前的桌子有100cm长,坐着小胖和小美,但是小胖经常骚扰小美,所以就在桌子中间画了一个三八线。 一人只有50cm的空间。所以从结构上就可以如下划分:
区域划分就是通过结构体内部的start和end去做划分
如何理解区域的变大或者变小呢??——>修改对应结构体内部的start和end即可
我们不仅要看到地址空间的范围,我们要知道在范围内连续的空间中,每一个最小单位都可以有地址,这个地址可以被直接使用!!
3.3 什么是进程地址空间
所谓进程地址空间,本质上就是一个描述进程可视化范围的地址空间内存在各种区域划分,对线性地址进行start、end即可 。本质上其实就是一个内核数据结构,和PCB一样,地址空间也是需要被/操作系统管理的:先描述再组织。 而每一个进程都有自己的进程地址空间,PCB内部有一个指针指向这块空间!
四、页表
共识:现代操作系统中,几乎不做浪费空间和时间的事情!
4.1 写时拷贝、缺页中断、惰性加载
页表具体有哪些内容呢??——>虚拟地址、物理地址、读写权限、标志位(对应的代码和数据是否被加载到内存中)
读写权限就可以帮助我们做检查,比方说当前是常量字符区但是你却想修改,就会被/操作系统拦截,该非法请求就不会被发送到物理内存。
标志位就是帮助们判断进程的代码和数据是否被加载到内存中,因为我们知道我们的进程对应的代码和数据是有可能处于挂起状态的(还没加载到内存)。
惰性加载:其实就是需要多少就加载多少。操作系统对大文件是可以实现分批加载,也就是说当前的进程可能只有PCB在内存中,但是代码和数据可能还没马上加载进来。
缺页中断:在执行进程的时候如果发现标记位显示当前代码和数据没有加载起来,就会发生缺页中断,也就是暂时中断这个进程,然后等代码和数据加载进来之后,再恢复原来的状态继续运行。
问题:一次加载进去不是更快吗,为什么需要检测了之后才通过缺页中断加载进去??
——> 一方面是因为可能这个文件特别大,所以没办法一次加载进去,就算是可以一次加载进去,可是你用不也是一点点去用么?? 所以缺页中断解决的是初步局部性加载的问题,能够更合理的去使用内存!!
写时拷贝:数据区的数据是按道理是可写的,但是一开始权限会被设置成只读(意思就是当前父子进程共享),一旦父子进程任意一方尝试做修改的时候,发现当前的数据是只读的(但是这里不做异常处理,而是转而发生写时拷贝),然后开辟一块新的物理内存,修改页表的映射
4.2 进程地址空间是如何切换的
进程PCB结构体里有对应的进程地址空间指针,所以进程切换就以为这进程空间地址空间被切换,而页表会被存储在CPU的cr3寄存器中,这其实属于进程的上下文信息,在进程切换的时候会被进程带走,后面再恢复过来!!
4.3 进程创建的具体过程分析
进程被创建的时候,优先加载的是PCB结构体以及里面对应的进程地址空间结构体,然后他的代码和数据可能不会马上被加载进来。
4.4 再次理解进程具有独立性
1、在内核数据结构上是独立的
2、物理内存中加载的代码和数据,只需要再页表上去体现。虚拟地址可以一样,但是通过页表映射不同的物理地址,就可以让父子进程解耦,一旦发生了任何异常,你释放你的我释放我的。
3、通过页表的虚拟地址映射物理地址,可以随便取地址,甚至是乱序。但是虚拟地址可以将一个线性的地址呈现给进程。
五、为什么要有进程地址空间?(重点)
所以我们可以对进程进行一个再总结:
讲个故事1:
一个大富翁(操作系统)有10亿美金,而他有四个私生子,但是四个私生子(进程)都并不知道对方的存在,所以他们都认为大富翁只有他唯一一个儿子,而大富翁告诉他们一旦自己去世了,就把所有的家产留给他,所以每个儿子也都信了,所以大富翁其实给每个私生子都画了一个大饼(进程地址空间)。每个人都认为自己有十亿家产。 但实际上是这些私生子要多少才会给多少(进程需要多少空间操作系统就给多少空间)
结论1:让进程以统一的视角看待内存 这样我进程就不需要关心说具体应该放在物理内存的什么位置,也不需要关心当前这个物理内存是否会影响别人的数据,这些工作都由操作系统去完成。
故事2:
你过年的时候经常有压岁钱,但是你还小所以你经常会买到一些没有用的东西,于是你的妈妈就让你把钱交给他保管,等你需要买什么的时候,他再把钱给你,比如说当你想要买个一块钱的橡皮时,你妈妈就给了你一块钱,但如果你想花100块钱买个游戏机的时候,你的妈妈就不给你买,所以这个过程其实妈妈的作用就是会阻止你做一些不太适合的事情。
结论2:增加虚拟地址空间,可以让我们访问的时候增加一个转换的过程,在这个转化的过程中我们可以对我们的寻址进行审查,所以一旦异常访问,直接拦截,该请求就不会到达物理内存,从而保护物理内存
结论3:因为有地址空间和页表的存在,将进程管理模块和内存模块进行解耦合 !
申请物理内存的哪一块?优先加载可执行程序的哪一部分??又或者页表填写到什么地方??这是有Linux的内存模块去管理的,进程并不需要关心。
结论4:其实变量名在定义的时候就已经被转化成一个个虚拟地址了,而我们之所以有a和&a,本质上是为了区分想获取的是变量的值还是地址。
结论5:以前我们所学习的C内存管理,其实本质上是进程地址空间,而内存管理是由Linux替我们完成的,我们上层语言并不需要关心具体的细节,只需要正常去通过对应的线性地址去使用就行了。
六、命令行参数和环境变量在栈的上面
所以环境变量和命令行参数是在栈之上的一个独立空间。 所以为什么子进程可以继承父进程的环境变量,因为子进程启动时,父进程已经把对应的环境变量信息加载进去了, 他也是地址空间的一部分,所以他必然有页表去帮助我们建立虚拟地址和物理地址的映射,而当子进程创建的时候,子进程也会将父进程虚拟地址空间当中的环境变量的相关参数也给我们建立了一份映射,所以即使你传参数,子进程也照样能够获得父进程的环境变量信息。
我们目前所关注的是用户空间。