文章目录
首先,我们知道我们的程序是要加载到内存中才能被使用的。cpu执行我们程序的二进制代码,来实现我们的功能。那么,现在的问题就是如何将程序加载到内存中去。
最简单的想法就是直接将整个程序加载到内存中去。例如,将你得hello.c这个源程序编译后的hello.o文件加载到内存的起始地址为0x4350处。当然,这个想法就是最直观的。所以我们的故事也得从这里讲起来!
1.连续分配存贮方式
1.1单一连续分配方式
这个是很早之前的时候,那个时候程序还是只能串行执行的时候,内存里面只有一个程序,采用的是这个方法。就是把内存分为系统区和用户区。
1.2固定分区分配
这个可以将内存分为大小固定的的区域,也可以将内存分为大小不一样的区域。如果大小固定的话,那么大程序无法载入,因为没有一块内存区域可以装下。如果大小不固定,就得使用一个表,将所有内存分区按照大小记录到一张表上,并且还有是否分配出去的标记。这个问题在于,如果有一个作业,现在30kb,但是现在只有40KB及其以上的内存分区了,也就是说,这样子至少产生了10KB的内存碎片。
1.3动态分区分配
后来人们就改进了固定分区分配的方法。我还是将我的内存区域划分为一个一个空闲区,大小不等,并且使用链表将他们串起来。每一次来一个作业,我就分贝一个空闲区给他,但是并不是全部给他们,而是只给他们需要的那一部分。所以,就引出了后面的几种分配分区算法。FF,NF,BF,WF等算法都有自己的优缺点。当然,还有quick fit和buddy system算法。我这里就不具体讲什么算法了。
但是这里要注意的几点是。第一:在内存分配的阶段,是要设置一个最小内存碎片值的。也就是说,如果现在有一个1KB的空闲区给你使用,但是你的程序大小是0.9KB,如果仅仅只给你0.9KB,那还有0.1KB其他程序也不好用,所以就干脆直接全部给你使用了。第二:回收内存的时候,要注意回收区前后是不是空闲的。第三:针对于不同分区算法而言,空闲链表的排列方式也是不一样的。首次适应算法使用的就是空闲分区按地址递增的方式排列。缺点就是造成了低地址不断被划分,留下了很多不可利用的小碎片;循环首次适应就解决了这个问题;最佳适应就是按照空闲分区的容量从小到大排列。这个的问题在于每一次切割后留下来的碎片都是最小的,这样子会留下很多难以利用的碎片;最坏适应算法就是按照容量从大到小的顺序排列。这样子就导致内存中没有大的空闲分区了。
2连续分配的问题
2.1程序中的内存紧张
随着技术的发展,我们的内存的确是越来越大,但是我们的程序所在的内存也是越来愈大。就来一个IDEA来说,有时候它可以占我的电脑500MB的内存(我就是直接到任务管理器上面看到的,也不知道准不准确)。如果采用连续的内存分配的方式,我觉得可能很少会有500MB的连续内存空间使用。它的解决方法就是使用离散的内存分配的方式。也就是后面的段式存贮和页式存贮。
2.2程序碎片利用
就像下面的这张图:现在有一个80KB的程序是没有方法载入进去的,但是实际上内存中还是用这么大的空间的,所以就引入的紧凑技术
但是引入“紧凑”技术要注意的就是程序重定位的问题。因为程序在内存重会移动,必然导致了程序开始的重定位到后来就是没有用了,所以,只能使用动态的重定位来解决这个问题。
这个有关重定位的问题,我应该会有别的博客会讲。
3.离散的内存分配
3.1代码分段
程序被人为的分为“代码段,数据段,栈段…”,然后分别将起存放到内存中不同的地方,记得一点,此时不一定是连续存放的。就是说,利用上面的动态分区的方法,找到一个合适的空闲区来装我们程序的每一段。
代码分段的好处在于,第一:对于程序员来说很是友好;第二:也便于程序重数据的共享。但是缺点在于,会产生内存碎片(也就是外部碎片)。原因就是因为它采用的分配算法还是动态分区分配的算法。
3.2内存分页
我们把内存分成一个一个的页,这样子就不会产生内存碎片。就像我们把一整块的面包分成一块一块的面包。这样子就就不会有面包屑了。即使分给你的一块你吃不完,那也是你自己的浪费,和内存碎片不是一个东西。那这个也有个问题,如果内存分的页太大了,但是代码所分的页太大,又会导致页内碎片过大。也就是说,我的内存分页为1M,但是仅仅只有1KB的代码载入,就会导致页内还有1M-1KB的大小没有用。注意,这个大小别人也使用不好,所以这个不能叫做内存碎片。所以,内存的分页一般为1k–8k最好。(其实在使用分页机制的时候,是会将你的代码也分成对应的页。但是可能往往代码中最后一页不足4K,所以出现内部碎片)。这个内部碎片是很明确的,就是属于某一个进程的。内存分区的时候讲的就是内存碎片,那个碎片不属于任何进程,但是谁也无法使用那个碎片!这个称内部碎片。
大家也都知道,实现分页的机制必须得有页表这个东西在内存中。讲到这个就得讲到那么操作系统是如何实现查表来获取对应的物理地址。这个就是内存管理单元MMU。具体的介绍,在我的文章后面有其参考链接,讲的十分不错。回到这里,我们程序中写的虚拟地址被MMU分成了两个部分(当然,这里指的是一级页表)。一个是页号,一个是偏移量。用32位的机器举例,他能提供的虚拟地址从0x00000000—0xFFFFFFFF(32G),假如我们内存分页的大小是4KB,那么就得分成32GB/4KB=8388608个页表项。也就是说,如果程序使用了虚拟内存30GB----30GB+8KB的位置的话,产生的页面号会很大。所以我们用来存储页号的位置要能足够保存我们的页号。因为我们的4KB的页面大小,产生的页内偏移最多是0xFFFFF,占用了12位(2^12=4096=4KB),那么前20位就得给页号来表示了(2 ^20= 1048576>8388608)。
可以看到,每一个程序我都得建立一个页表,而且每一个页表我还得有8388608个表项。但是其实我的程序根本就用不到这么多页表项。那么最直观的方法就是将不要的页表项删除。但是这个又会引出一个问题。我们每一次通过虚拟地址找到页号的时候,我们就只能顺序查找或者二分查找了。但是都不够快,我们当初如果没有将不要的页表项剔除的话,我们可能直接定位到那里,就像你使用数组一样。所以,即使这个页表项没有使用到,我们还是不能呢将其删除,依然得保留下来。那么多级页表就应运而生了。
3.3多级页表
一级页表的问题在于,很多我没有使用到的页面表项也得加进去。那么,我们只要解决这个问题就可以了(但是使用紧凑的方法是不可取的)。所以得采用二级页表(甚至更多级的页表)。二级页表的MMU处理方式就不一样了,还是针对于32位的机器而言,高10为页目录,中间10位是页表项,低12位是页内地址。那么多级页表是如何做到将页表所占用的内存减少的呢?
我相信大家看到的都是这样的例子:32位地址空间的机器,如果页面大小为4KB,则每个进程可达1M个页,假设每个页表项占用4个字节,这样每个进程仅仅页表项就占用了4MB连续的内存空间。如果使用多级页表,外层目录有1024项,然后每一个目录又指向了具有1024项的页表。那么所占总的内存大小为:10244byte+102410244byte=4kb+4MB。怎么回事,怎么还计算的比一级页表还多了一些了。大家看这个图:
在这个图中,外层页表的确有1024项,也就是说,占用10244byte即4KB内存,但是内层页表并不是有1024个页表,因为在外层页表中并不是所有的都有对应的物理地址映射的,你的程序几乎不可能将所有的内存用到,也就是说,你的页表并不需要将整个4GB的虚拟内存映射到实际的物理内存上。在这里,我们假设内层页表仅仅使用了3张。所以总的大小是:4KB(外层页表)+102434(内层页表)=16KB大小,相比原来一级页表4MB还是减少了很大一部分内存空间的。
也就是说,之所以多级分页机制有利于内存的开销,是因为一级页表一次性得占连续内存空间4M,这个条件是不太好满足的。所以,多级页表的出现,首先解决的是页表需要连续存放的问题。在上面的多级页表中可以看到,每一个页表是可以不用连续存放的。其二解决了页表项占用内存过多的问题。这个目的很好的实现在了虚拟内存管理中。在虚拟内存管理中,我们没有必要一次性将所有的页表项调入内存,我们仅仅调入一部分能够让程序正常跑起来的页表项即可。所以,在上面的计算中,才仅仅计算了3张内层页表的大小。
那么这样看来就直接使用分页管理内存不就好了吗?这个似乎看起来没有任何缺点。但是对程序员来说是一件不太好的事情。如果大家有过写汇编程序的经历,就会知道程序分段的好处,很方便我们地址的使用的。但是分页管理显然做不到这一点。这里解释的的优点牵强,但是我们得知道,分页管理有利于高效利用存贮空间,分段有对开发者友好。所以我们实现段页式存贮,综合他们的优点。
4.段页存贮
这个就是段页式存贮的基本原理图。
在左边,我们的程序还是被分成代码段,数据段等等。他们会被映射到一个虚拟内存上面。也就是说,此时得到的地址不是真的物理地址,是不能转送到地址总线上面的。我们还得将这个用户的数据段,分成三页,存到物理物理内存中去。
4.1请求分页
谈到虚拟内存管理,就不得不说请求分页机制。正是因为这个机制,OS才实现了真正的虚拟内存管理。上面所说的连续分配和离散分配的一个共同的问题在于,都要一次性将所有的程序装入,但是这个就是解决了这个缺点。
请求分页简单的来讲就是:不会一次性将所有的页调入内存,一开始仅仅调入的是一部分。因为OS分给每一个程序的物理块数是不一样的。关于这里的内存分配策略有:固定分配和可变分配物理块。进行置换的时候有:局部置换和全局置换。
固定分配,是指在程序运行期间的物理块数是不变的。也就是说,如果一开始OS仅仅给进程A分配了3块物理块,那么以后他就是只有3块物理块了。可变分配,是指先为每个进程分配一定数目的物理块,但是在运行期间,可根据适当的情况增加或者减少。局部置换,是指当该进程发生缺页中断之后,只能从改进程已经分配的物理块中调出一页来容纳新的一页。全局置换,是指发生缺页中断后,首先从OS所保留的空闲物理块中取出一块分给改进程。如果OS的空闲物理块被使用完毕了,就从内存中任意调出一页(可以不是改进程的物理页),然后来容纳新的一页。
书本指出,上面两两组合只有3种策略,没有固定分配和全局置换的搭配。假设有这个情况,那么进程B在发生缺页后,假设OS没有空闲物理块了,那么如果OS将进程A的物理块给调出了,那么对于进程A而言,不就不是国定分配了。两者相互矛盾。
参考文章
内存管理MMU:
https://blog.csdn.net/hzrandd/article/details/51028681
https://blog.csdn.net/forDreamYue/article/details/78887035
https://blog.csdn.net/whp404/article/details/54564400
https://www.cnblogs.com/vinozly/p/5590428.html
李治军老师的操作系统原理