操作系统7:内存管理

内存管理

基本概念

image-20200206150345976.png
内存管理主要分为连续区管理和非连续区管理

image-20200206150429477.png
image-20200206150828979.png
image-20200206150913675.png
image-20200206150932299.png
注意汇编语言也需要翻译下,但不是编译,因为它基本上就是一对一的翻译了下,比编译任务简单的多

image-20200206151058997.png
base表示该进程的最小的地址位置,limit表示最大的

image-20200206151513335.png
image-20200206151704937.png
image-20200206151757778.png
由存储管理单元在地址送往地址总线之前进行逻辑地址到物理地址的转换

image-20200206151908093.png
image-20200206152016114.png
两种方法可以实现运行时不将所有代码装入内存:

image-20200206152051955.png
动态连接提供系统级的支持,操作系统升级时,动态链接库可以直接升级,不需要重新编译应用程序

image-20200206152215691.png
image-20200206153141284.png
存储管理的基本内存就是逻辑地址和物理地址之间的映射

交换

image-20200206152416921.png
后备存储空间一般是一个单独划出的存储空间,要提供直接访问机制

来源:https://zhidao.baidu.com/question/72920995.html

内存交换(对换)的基本思想是,把处于等待状态(或在CPU调度原则下被剥夺运行权利) 的程序从内存移到辅存,把内存空间腾出来,这一过程又叫换出;把准备好竞争CPU运行的程序从辅存移到内存,这一过程又称为换入。
  有关交换需要注意以下几个问题:
  1、交换需要备份存储,通常是快速磁盘。它必须足够大,并且提供对这些内存映像的直接访问。
  2、为了有效使用CPU,需要每个进程的执行时间比交换时间长,而影响交换时间的主要是转移时间。转移时间与所交换的内存空间成正比。
  3、如果换出进程,必须确保该进程是完全处于空闲状态。
  4、交换空间通常作为磁盘的一整块,且独立于文件系统,因此使用就可能很快。
  5、交换通常在有许多进程运行且内存空间吃紧时开始启动,而系统负荷降低就暂停。
  6、普通的交换使用不多,但交换策略的某些变种在许多系统中(如UNIX系统)仍发挥作用。

Windows中它称为虚拟内存

image-20200206152847883.png
image-20200206152911850.png
只需要保证就绪队列的进程都在内存就好

覆盖和交换的区别?

image-20200206153021268.png

连续区内存分配

image-20200206153610786.png
image-20200206153642940.png
image-20200206153721605.png
如果不在范围内,操作系统就会报错,通常情况下用户进程会被终止

image-20200206153828759.png
使用空闲块进行分配

image-20200206153915721.png
image-20200206154053217.png
image-20200206154144648.png
注意上图出错了不是“最小”而是“最大”

在这样的过程中,可能会产生内存碎片:

image-20200206154348187.png
碎片分为外部碎片和内部碎片,也是两种看碎片的角度

可以使用紧缩的方法来减少外部碎片

image-20200206164327471.png
划重点,重定位可以在运行时操作,除此之外,它的时间花费也是较高的,重定位过程中无法进行其他操作

非连续区内存分配

页式内存管理

image-20200206164504182.png
image-20200206164659836.png
page table(页表),建立了页和页帧之间的一对一关系

image-20200206164839634.png
image-20200206164949804.png
在页式内存管理中,操作系统主要负责管理所有的空闲页帧

image-20200206165100322.png
因为所有的空闲页帧都是一页一页分配给进程的(进程需要的未必有那么大),所以会存在内部碎片问题

如何实现页表:

image-20200206165251134.png
注意,每个进程有一个页表(这也好理解,否则无法区分内存空间属于哪些进程)

image-20200206165338401.png
两次内存访问主要是开销大

TLBS:转换旁氏缓冲区,它支持并行搜索

image-20200206165507321.png
其实就是对页表内容的一个缓存,因为是单独的硬件设备,硬件配置可以较高(类似于内存和cache之间的关系),读写速度加快

image-20200206165649183.png
image-20200206165739316.png
对进程进行内存保护:

image-20200206165831262.png
image-20200206165948655.png
但是使用有效/无效位判断,不能判断有内部碎片的页,因为它是一部分有效,一部分无效

image-20200206170120309.png
image-20200206170202491.png
例如有一个模块有多个进程调用,就可以将该代码放在共享的页上

页表的数据结构

image-20200206170328275.png
翻译一下:

  • 层次页表
  • 哈希页表
  • 反向页表

image-20200206170353698.png
image-20200206170403406.png
image-20200206170454509.png
页表被分为外层和内层

image-20200206170544490.png
image-20200206170603258.png
image-20200206170645647.png
哈希页表常见于64位的CPU

image-20200206170714395.png
image-20200206171019530.png
反向页表:

image-20200206171103410.png
使用进程的pid和页号p作为key进行搜索,得到的下标i和偏移量组合对页帧进行访问

通过使用反向页表,我们可以使得整个操作系统共用一个页表,从而节省了页表空间,但是相应的,查询的时候比较麻烦。当然也可以借助哈希技术改造,实现一次命中

段式内存管理

image-20200206172133648.png
image-20200206172227976.png
从用户的角度来看,一个进程的逻辑地址可以用段来表示

image-20200206172311041.png
段式管理中使用段表而不是页表

image-20200206172455418.png
image-20200206172538145.png
image-20200206172621708.png
image-20200206172705216.png
可以设置特权位来控制进程对内存的读写和执行

image-20200206172748920.png
段式管理存在外部碎片问题(因为段长度可变,所以不存在内部碎片问题)

image-20200206172941688.png
image-20200206173146768.png
image-20200206173156085.png

i386内存管理技术

image-20200206173233425.png
i386支持多种方式,具体怎么用看操作系统的,不同操作系统有不同的用法

段内还可以分页

image-20200206173321648.png
地址是32位的

image-20200206173405809.png
采用段描述符的管理,维护一个段描述符表

i386有两种页式管理模式:小页模式和大页模式

image-20200206173515155.png
小页模式:页的大小为4kb,页表为二级页表

大页模式:页的大小为4mb,页表为直接的页表

image-20200206173638764.png
段式-页式-实际地址

Linux的逻辑地址

image-20200206173734872.png
Linux采用三级页表进行内存管理

image-20200206173959044.png
为了在i386上运行,linux可以将第二级地址的长度设置为0

虚拟存储

虚拟存储思想

image-20200206174810186.png
进程的逻辑空间可以远大于分配给它的物理空间,因为每次只装入一部分代码

image-20200206175056209.png
image-20200206175112168.png
如果不动态申请的话,heap可以放在外面

虚拟存储依然可以支持共享空间:

image-20200206175255547.png
image-20200206175309807.png
现在的操作系统主要使用的就是按需调页

按需调页

image-20200206175504693.png
如何判断页面是否已经装入内存?已经装入内存的页面可以在页表上查询到页面-页帧项

image-20200206180212263.png
物理内存被装满怎么办?这就需要把其他进程的占用部分换出,放置到外存中,再将该进程的代码换入

image-20200206205318304.png
有效位这种设定之前就有,但是现在我们可以改写它的含义,i对应的原来是一个处理异常的中断响应程序(MMU查页表,发现它是invalid的,然后引起内存缺页中断),我们只需要修改这个内核程序即可

image-20200206205917720.png
如何找到保存在外存的页帧呢?可以借助页表来实现:因为页表项虽然是invaild的,但是依然有存储单元,所以只需要在这个存储单元中保存对应的外存的地址即可

装载之后要更新页表,置为valid

第六步要调整PC寄存器,重新执行该语句

image-20200206210502816.png
image-20200206210732404.png
应用程序代码只有一部分是在执行时候需要的,其他的都是异常处理或者不常用的模块,这些代码没必要装载进来

但是它使用中断,还有io操作,效率上也有损失

正因为它的好处非常明显,现代操作系统基本上全都使用了虚拟存储技术

Linux缺页中断程序的实现

image-20200206211220822.png
注意,虽然每个进程都要一个页表,但是每个页表都要包括所有的页帧,其实是比较大的

image-20200206211654995.png
中断矢量表示中断服务程序在内存中的地址

image-20200206212707599.png
image-20200206212723073.png
这段代码向中断响应表中写入了14号中断的中断响应程序的地址

页表项的结构:

image-20200206212828993.png
这是Intel规定的格式,为的是与它的CPU的mmu相配套

  • P这个位就是valid/invalid bit
  • U/S=0表示只能在内核态访问
  • A=0表示该位自从装入内存之后从来没有访问过,只要访问过就是1
  • D(dirty bit)只要从被装入内存之后被写过就是1,说明和硬盘数据不一致了,需要回写
  • 20位页帧号

image-20200206213350623.png
pushl $SYMBOL——NAME(do_page_fault) 将do_oage_fault这个函数名地址(也就是函数的首地址)压栈

image-20200206213612401.png
保护现场,将寄存器的值保存到内存中,以便于在中断响应程序中使用寄存器

通过call语句跳转到这个C程序中:

image-20200206214501276.png
定义tsk用来指向PCB,mm指向PCB中的内存管理结构,vma用来指向连续块,address用来指向引起缺页的地址

什么是连续块呢?

在内存中,一个进程可以分为多个块:代码块、数据块、堆、栈等等,每个块之间不是紧密相连的,而是有一段空白的

image-20200206214825032.png>
vm_area_struct的vm_start,vm_end分别指向一段的开始和结束,由此可见对于一个进程,应该有多个vma,它们之间使用链表连接

image-20200206215035145.png>
image-20200206215023718.png>
整个过程如下:

PCB中的mm指向一个mm_struct,其中的mmap指针指向vma双向链表。所以,通过便利vma双向链表就可以查询一个页是否是在vma所指的范围内,从而得知对页的访问是否是valid的

页面置换

image-20200206222334590.png
如果要提高内存利用率的话,经常就要面对物理内存满的情况,甚至可以说是一种常态

要有一个合理的算法进行页面置换,使得整体的换页次数最少(注意磁盘操作是很慢的!)

image-20200206222757365.png
image-20200206222842186.png
例如:

image-20200206223038397.png
可见缺页率对于效率有多大的影响,所以必须要有一个非常合适的置换算法

image-20200206223210191.png
这里要主要,页面置换算法只有在物理内存满的时候才会被调用,客观上来说被调用的次数并不多,但是每次调用对效率的影响都是挺大的

image-20200206223358471.png
注意这个“换出“的意思是和磁盘中的内容同步,是要写的

理论上:

image-20200206223728003.png

评估置换算法

image-20200206223621636.png
这些数字表示页号

先来先服务算法

image-20200206223816169.png
先进来的页先被换出,包子一屉顶一屉

image-20200206224033054.png
image-20200206224106395.png
注意这里,物理页帧变多了,缺页次数反而增加了。这是算法本身带来的异常,问题很大,所以实际应用时候不使用

optimal算法/最优化算法

image-20200206224253479.png
这其实是最优的算法,也就是说它的缺页次数实际上已经是最少的了。但是这是不实际的:”最长时间“指的是今后的时间,而不是之前的时间,而我们是不知道之后的页使用情况的。所以说这个算法是“可望不可即”

最近最少使用算法(LRU)

因为上面的算法难以实现,所以这里做了一个小改变:将“最长时间”从今后的时候,改为了之前的使用,也就是假设之前最久没有调用的,之后被调用的概率也比较小。当然这种说法没有理论保证,只是一种假设而已

image-20200206231230065.png
image-20200206231538057.png
时钟值是从开机时间计算,初始值是0

这种算法可以实现了,但是时间开销很大,尤其是如果我们要在所有进程的所有页表项中找的话

也有其他的实现方法:

image-20200206231803501.png
久而久之,链表的尾就是最长时间最少使用的页面了

但是每次引用时,找该页面的节点比较花时间,花费时间更多了

为了减少时间开销,有以下优化方案:

image-20200206232150936.png
这种方法的问题也是显然的:

  • 不一定存在这样的页面
  • 目前为止没有被修改过不一定代表是最近最少被访问的页面

image-20200206232433925.png
image-20200206233219550.png
它的时间开销比较小,是实际可行的

设置一个计数器的话就可以实现其他的算法:

image-20200206232449108.png
这两个恰好是相反的2333,反正都是假设,没什么理论基础的

LRU算法在这几个中相比是最现实可行的,所以现在的操作系统都在使用LRU算法进行页面置换,甚至于像页式内存管理中的TLB中,对于哪些页表项要存在TLB中、满了怎么办,也是使用的LRU算法

页帧分配和系统抖动

这里讨论这些问题:

  • 进程刚刚创建的时候如何分配内存
  • 系统抖动

image-20200207000408831.png
至少要分配一条指令,这就需要多个页面了

分配思路:

固定式分配

image-20200207000653890.png

优先权分配

image-20200207000749780.png
这里归纳了全局分配和局部分配两种思路:

image-20200207000858884.png

系统抖动

image-20200207000958714.png
CPU利用率低是因为忙于做换入和换出,都在做io操作,使得从指标上有欺骗性,不搭配IO使用率看不容易发现

例如:

image-20200207001240800.png
它至少需要三个页帧,一个放指令,一个源操作数,一个目标操作数,而只有两个页帧,这就导致它一直处于内存不够的状态、一直忙于换入换出找空间,发生了抖动

image-20200207002415633.png
增加进程数,CPU利用率反而下降了,这就是发生了抖动

image-20200207002452860.png
我们可以使用工作集的方式来计算Locality

image-20200207002650262.png
image-20200207002812018.png
image-20200207002854348.png
m是物理内存的总的页帧数

使用工作集方式,用各个进程的工作集的和(它们是没有重叠的,因为不同进程引用的是各自的页面)作为近似的locality(用一段时间的进程引用观测值近似locality的量),如果比总页帧数大,说明物理内存早晚不能满足这些进程的要求,就需要杀掉一些进程

Linux存储管理

这一部分老师讲的乱糟糟的,凑合看吧

image-20200207101454014.png
image-20200207101517518.png
一级页表和二级页表都是1024个项,每项4k

image-20200207102042998

0:内核态,3:用户态

低级别的申请者访问高权限的内存部分就会被拒绝

所有进程共享一个GDT,每个进程有自己的LDT:共享代码段就可以通过GDT实现

index有13位,就表示了查询的范围只有8k,所以一个页表只能对应8k个段

image-20200207102416326.png
image-20200207102432929.png
D:dirty bit

G:精细度,0表示段表的段长度以字节为单位,1表示以4k字节为单位(此时一个段的长度可以达到4g)

image-20200207102829353.png
2,3项是内核态的数据

从第8项开始,每个进程占用两段

image-20200207103017862

image-20200207103031231

image-20200207103141353

Linux不仅要支持32位系统,也要支持64位。在64位中,使用三层页目录

image-20200207103320991.png
image-20200207103344925.png
image-20200207103409359.png
image-20200207103746062.png
但是使用wma速度不够快,因为是线性的,插入和访问都是O(n)的,为了加快速度,Linux引进了树结构:

image-20200207104122687.png
AVL树:左子树的每一个地址范围都小于根节点,右子树的都大于根节点

这样查询和插入就是O(lgn)了

物理地址

image-20200207104542444.png
内核代码从24k开始放

bitmap用Bit表示各个页帧是否被使用

image-20200207110413398.png
image-20200207145649710.png
以多个页帧为单位的Bitmap中,一旦其中有一个page被占用了,就是1,使用这些bitmap使得搜索更加快速。这样的bitmap一共有6套,一直到一个Bit对应32个页帧

1M之前的内容因为要和8086兼容,所以限制比较大,内容格式也比较复杂,1M之后的内容就比较自由了

image-20200207105050153.png
为每一个页帧设计了一个数据结构来描述,通过这些成员变量就能看出这个页帧是被使用的,也可以看出使用的用途

对于用户态的进程,申请资源时是以页为单位的,但是在内核态中却不是整页的申请。而内核态申请的内存空间如果大小不一的话会使得内存空间很凌乱、产生碎片,所以设计了伙伴算法来分配空间给内核态中的进程

image-20200207150108664.png
伙伴算法分配出的空间固然有一点浪费,但是回收的空间却是整块的,有利于再分配:有内部碎片,但是没有外部碎片.

image-20200207151145741.png
用户态申请和释放内存:

image-20200207151309364.png
image-20200207151435307.png

SLAB算法

image-20200207151538133.png
image-20200207151722775.png
image-20200207151801697.png
一个slab是几个页帧的组合

image-20200207151937859.png
image-20200207152107292.png
image-20200207152240967.png

换出和换入

image-20200207152426474.png
注意这里的页面不够,不一定是完全满了,可能只是使用率达到一定程度了。同时,这也说明了在Linux中换出和换入不一定是在资源申请的时候才进行的,平时也可以进行,当然如果来不及在平时进行,换出和换入也可能在申请资源时才进行

cache

image-20200207152834055.png
如果要换入的页面在缓存区中,就只需要从缓冲器拷贝即可

image-20200207153135320.png
image-20200207153252564.png
page cache使用逻辑地址,buffer Cachee使用物理地址

image-20200207153355663.png
通过cache,可以避免直接和外部设计接触

来源:https://blog.csdn.net/zzhongcy/article/details/89399399

Buffer cache 也叫块缓冲,是对物理磁盘上的一个磁盘块进行的缓冲,其大小为通常为1k,磁盘块也是磁盘的组织单位。*设立buffer cache的目的是为在程序多次访问同一磁盘块时,减少访问时间*。系统将磁盘块首先读入buffer cache 如果cache空间不够时,会通过一定的策略将一些过时或多次未被访问的buffer cache清空。程序在下一次访问磁盘时首先查看是否在buffer cache找到所需块,命中可减少访问磁盘时间。不命中时需重新读入buffer cache。对buffer cache 的写分为两种,一是直接写,这是程序在写buffer cache后也写磁盘,要读时从buffer cache 上读,二是后台写,程序在写完buffer cache 后并不立即写磁盘,因为有可能程序在很短时间内又需要写文件,如果直接写,就需多次写磁盘了。这样效率很低,而是过一段时间后由后台写,减少了多次访磁盘 的时间。

Buffer cache 是由物理内存分配,**linux系统为提高内存使用率,会将空闲内存全分给buffer cache **,当其他程序需要更多内存时,系统会减少cahce大小。

Page cache 也叫页缓冲或文件缓冲,是由好几个磁盘块构成,大小通常为4k,在64位系统上为8k,构成的几个磁盘块在物理磁盘上不一定连续,文件的组织单位为一页, 也就是一个page cache大小,文件读取是由外存上不连续的几个磁盘块,到buffer cache,然后组成page cache,然后供给应用程序。

Page cache在linux读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问。具体说是加速对文件内容的访问,buffer cache缓存文件的具体内容——物理磁盘上的磁盘块,这是加速对磁盘的访问。

磁盘的操作有逻辑级(文件系统)和物理级(磁盘块),这两种Cache就是分别缓存逻辑和物理级数据的。

*Page cache实际上是针对文件系统的,是文件的缓存*,在文件层面上的数据会缓存到page cache。文件的逻辑层需要映射到实际的物理磁盘,这种映射关系由文件系统来完成。当page cache的数据需要刷新时,page cache中的数据交给buffer cache,但是这种处理在2.6版本的内核之后就变的很简单了,没有真正意义上的cache操作。

*Buffer cache是针对磁盘块的缓存*,也就是在没有文件系统的情况下,直接对磁盘进行操作的数据会缓存到buffer cache中,例如,文件系统的元数据都会缓存到buffer cache中。

简单说来,page cache用来缓存文件数据,buffer cache用来缓存磁盘数据。在有文件系统的情况下,对文件操作,那么数据会缓存到page cache,如果直接采用dd等工具对磁盘进行读写,那么数据会缓存到buffer cache。

Buffer(Buffer Cache)以块形式缓冲了块设备的操作,定时或手动的同步到硬盘,它是为了缓冲写操作然后一次性将很多改动写入硬盘,避免频繁写硬盘,提高写入效率。

Cache(Page Cache)以页面形式缓存了文件系统的文件,给需要使用的程序读取,它是为了给读操作提供缓冲,避免频繁读硬盘,提高读取效率。

原文地址:https://www.cnblogs.com/jiading/p/12289058.html