清华操作系统课程学习笔记3

第四章 非连续内存分配

一、为什么需要非连续内存分配

1、连续内存分配

分配给一个程序的物理内存是连续的。

缺点:内存利用率低、存在内碎片、外碎片的问题。

2、非连续内存分配

分配给一个程序的物理地址空间是非连续的。

优点:更好的内存利用和管理、允许共享代码与数据、支持动态加载和动态链接。

缺点:带来额外的管理开销:如何建立虚拟地址和物理地址之间的转换。两种实现:软件方案和硬件方案。

两种硬件方案:分段、分页。

二、分段、分页寻址

计算机的程序其实是由各种各样的段组成的,不同的段之间有不同的属性,比如主程序、子程序、各种库等。数据有栈段、堆段,共享数据段等。所谓分段,就是根据应用程序不同段的特点,将它们区别出来,进行分离、管理。

逻辑地址空间虽然是连续的,但通过一些特定的方法,把各种段分离出来,它们位于不同的区域,物理地址实际上是不同的。比如,左边是连续的逻辑地址,右边是分离的,分成一块一块的物理地址空间。我们需要一个映射机制把逻辑地址空间映射到物理地址空间:

分段的管理机制可以用软件来实现,也可以用硬件。但软件实现的开销非常大,所以我们要考虑如何用硬件实现。

1、分段寻址方案

逻辑地址实际上是一维的,那我们如何把一维的逻辑地址跟物理地址对应?可以将一维的地址分成两部分,一部分是段寻址,一部分是段内偏移的寻址。具体来说有如下图的两种寻址方式。

X86使用的是段寄存器+地址寄存器实现方案。

下图表示了硬件实现方案:

                                    

从左上角开始看。当CPU在执行某条指令的时候,需要进行寻址(找数据,代码等),此时CPU得到的是一个逻辑地址。CPU将逻辑地址按照前半部分段号和后半部分偏移分开。

首先希望根据段号得到段所在物理内存的起始地址。这时候需要使用段表。

段表中存放了一个对应关系,那就是逻辑地址的段号和物理地址的对应关系。

另外,由于每个段的大小是不一样的。我们需要了解每个段的长度限制,这也要存在段表中。

段表有个索引,索引的每一项就是由段号决定的。

这样就可以根据段号,在段表中查到段对应的物理内存的起始地址,以及大小的限制。

找到之后,CPU比对这个大小限制和逻辑地址(通过Limit Register),看是否满足条件。如果不满足条件,就产生一个异常。

如果在合理区间之内,将起始地址和偏移量相加,得到一个实际的物理地址。

段表是操作系统在正式寻址之前建立好的。

2、分页寻址方案

分段机制现在用得比较少,绝大部分现代计算机都用的分页机制。

分页机制跟分段类似,也需要一个页号和页内偏移量。分页跟分段的一个最大区别是,页的大小是不变的。

步骤如下:

(1)帧(Frame)

                             

物理地址=就表示帧的大小。

下面有个地址计算的实例

(2)页(Page)

计算方式跟前面是类似的。但因为这是逻辑地址,因此页号跟物理地址对应的帧号可能会不一样,但每一页的大小,以及页内偏移跟帧是一样的。

跟前面分段的方法一样,首先CPU得到逻辑地址,逻辑地址分成两块,一块表示页号,一块表示页内偏移地址。跟前面一样,有一个页表,页表的索引就是页号,内容是帧号。我们可以通过页号查找到对应的帧号,帧号加上偏移,就能得到物理地址,找到物理地址对应的内存空间。

页表是由操作系统建立的。操作系统在操作系统初始化的时候,enable分页机制的时候就建立好了。

页寻址的好处就在于,页内偏移量和帧内偏移量一样,页跟帧的大小是一样的,这样就不用像段寻址那样还要去判断段内偏移是否合法,方便实现。

另外,逻辑地址空间是个连续的空间,但通过页寻址,映射到物理空间中,就不是连续的了,分散在不同的帧中。好处是有助于减少碎片。

有一种情况是,逻辑地址空间大于物理空间的时候,需要考虑虚拟内存。

四、页表、TLB

前面提到了分页机制的基本原理,接下来的问题是,分页机制应该如何去有效地实现呢?

1、页表

页表其实就是个大数组。索引是页号,索引所对应的页表项存放的就是帧号。

页表的每一项还会有一些标志位(Flags),用来说明这个页面的一些性质。比如上图中,

dirty bit是页面已被修改(Dirty)标志。当处理器对一个页面执行写操作时,就会设置对应页表表项的D标志。处理器并不会修改页目录项中的D标志;

resisdent bit表示这页是否已被占用。

比如可以表示这个页是否合法(没办法对应到一个物理空间)、这个页读写的情况等等。

首先看逻辑地址(4,0),首先通过页号4,在页表中找到相应的项(从下往上数,从0开始)。此时对应图中靠上的位置,标志位Flags为100,中间的resident bit为0. resident bit为0,表示这个页在物理内存中不存在对应的帧,此时返回一个异常。

再看(3,1023),通过页号4,找到了相应的项,标志位为011,resident bit为1,可以找到对应的帧。对应的帧号为00100,也就是帧号为4。偏移量跟逻辑地址的页偏移量一样,因此就可以找到对应的物理内存地址。

分页机制的性能问题:

页表可能会非常大。比如,一个64位的机器如果每页1024字节,那么一个页表的大小会是多少?页表会有项,这也实在是太大了。也就是说,一个较大的逻辑地址空间,会导致页表本身也变得特别大。

每个应用程序都要维护一个页表,那就更加耗费空间了。

另外,页表特别大,所以页表没办法放在CPU里,只能放在内存中,那每次访问一个内存单元需要2次内存访问:一次用于获取页表项,一次用于访问数据。这也是一个可观的开销。

那么如何解决这些问题呢?一般来说,大部分的计算问题都是通过

缓存(Caching)(将一些最常用的数据存在距离CPU更近的地方)。

间接(Indirection)来解决的。

2、转换后备缓冲区(TLB)

前面提到过的CPU中的内存管理单元(MMU)里,有Translation Look-aside Buffer(TLB),转换后备缓冲区,缓冲的是页表的内容。TLB是CPU内部特殊的一块区域,TLB的表项由Key 和Value组成。

TLB是用一种相关存储器实现的,是一种快速查询存储器,可以并发地查找,但实现的代价很大,所以容量是有限的。可以把当前经常访问的页表项放到页表中。当CPU得到逻辑地址的时候,先去查TLB,当查找到的时候,就可以快速地访问到对应的物理内存。当TLB里查不到的时候,只能去页表里查。如果页表里有对应项的存在位为1,就将那一项取回来放在TLB里(X86中,这一过程是用硬件实现的,而有些CPU,如MIPS则是靠软件实现的)。这样,最常用的就会留在TLB里。

在中TLB找不到的情况不会很频繁,比如32位系统中,一个页为4k字节,一般访问4k次才会产生一个缺失,这是可以接受的。为了让TLB找不到的情况尽量少出现,设计程序的时候可以让程序具有访问的局部性,让大部分的访问都集中在一个区域中。

TLB可以让寻址的时间开销得到极大的降低。

3、二级/多级页表

TLB提高了页表访问的速度。而为了缓解页表占据太多空间的问题,使用了多级列表。

将逻辑地址的页号分成多个部分,如下图,分成一级页表的页号p1和二级页表的页号p2,使得将对一个大地址范围的寻址拆分为对几个小的页表的寻址。

为什么说二级/多级页表可以节省空间?

第一,如果一级页表中的第一个PTE(分页)是空的,那么相应的二级页表就根本不会存在,这代表着一种巨大的潜在节约,因为对于一个典型的程序,比如4GB,8GB的虚拟地址空间的大部分都将是未匹配的。

第二,只有一级页表才需要总是在主存中;虚拟存储器系统可以在需要时创建,页面掉入或调出二级页表,这就减少了主存的压力;只有最经常使用的二级页表才需要缓存在主存中,这种离散的存储方式是非常便利的。这就是多级页表的一个根本性的优点:可以离散存储。(单级页表为了随机访问必须连续存储,如果虚拟内存空间很大,就需要很多页表项,就需要很大的连续内存空间,但是多级页表不需要)

为什么多级表可以节省进程的页表空间,下面有个具体的例子:

如果是32位机(地址线是32位的,也就是4G的线性地址空间),页的大小一般为4kb,则页表有个表项。地址的高20位当做页表地址,低12位当做页偏移量。如果页表的每项大小为4B,则需要4MB的ram来存储页表,即使进程并不会使用所有的地址。

问题来了,所有操作系统课程都会告诉你,使用多级页表可以减少每个进程的页表所需的内存。为什么节省了呢?从你最终要存储的表项来看,无论如何你存储的表项是不会少的(都要用到这么多内存),而且多级页表还会增加存储开销。

其实是这样的,二级表只是从进程的角度来看,为进程节省了页表项(其实所有的页表存储空间增大了)。二级模式通过只为进程实际使用的那些虚拟内存区请求页表来减少页表,就是进程未使用的页暂时可以不用为其建立页表,因为如果使用一级页表的话,你就必须为所有的4G范围内分配页表,不能细分。每个活动进程必须有一个分配给它的页目录,不过没必要马上为进程的所有页表都分配ram,只有在进程实际需要一个页表时才给该页表分配ram,这样就提高了效率。

常见CPU都带有mmu和缓存,使用二级页表性能几乎没有损失,实际上三级页表也很常见。如下图,二级页表可以推广到多级页表:

4、反向页表

随着程序越来越大,逻辑(虚拟)地址空间的增长速度远快于物理地址空间。反向页表要解决的是大地址空间的问题,不是让页表与逻辑地址空间的大小相对应,而是让页表与物理地址空间的大小相对应。

反向页表跟前向映射页表的映射关系相反,以物理页号(帧)作为索引来查找对应的逻辑页页号。下面介绍他如何实现:

(1)基于页寄存器(Page Registers)的方案

使用页寄存器的最大问题是,如何根据页号来找到对应的帧号?(要注意,页寄存器的索引是帧号,并不是页号)。

从上图可以看到,反向页表可以有效地节省页表所占的内存空间。

(2)基于关联内存(associative memory)的方案

关联存储器(相关存储器)是一种特殊的存储器,实现它的硬件逻辑很复杂,开销很大,因此它的容量无法做得很大。

如何在反向页表中搜索一个页对应的帧号:

基于上面的理由,这种方式不是很实用。

(3)基于哈希(hash)查找的方案

将查找通过哈希表来实现,这样就可以直接在反向页表中通过页号来查帧号了。

无论有多少个进程,反向页表在内存中只需要一个,不需要像正向实现那样,每个进程都要有一个页表。

第五章 虚拟内存技术

一、虚拟内存

1、为什么要用虚拟内存?

程序规模的增长速度远远大于存储器容量的增长速度。 下图说明了计算机中常用的一些存储器的层次:

程序越来越大,内存容量却没办法快速增长,物理内存渐渐不够用了,怎么办?

用容量更大的硬盘来凑,将一些不常用的数据放在硬盘中,常用的放在内存中。但如果让程序员来考虑这个问题的话,就非常地麻烦了,因此把这个工作交给操作系统。

2、几种克服内存不够的方法

二、虚拟内存出现之前的技术

1、覆盖技术

目标:在较小的可用内存中运行较大的程序,常用于多道程序系统,与分区存储管理配合使用。

举个栗子,下面这个程序分为了A-E五个模块。左边的树状图表明了这个程序各个模块之间的调用关系。首先,A需要常驻内存,因为它会调用其他模块。B和C都被A调用,他们之间没有相互调用关系,所以B跟C分在一个区。同理,D、E、F也分在一个区。

我们可以看到,覆盖方法是不止一种的,可以通过程序员的精心安排,设计一个所需内存更小的覆盖方法。粒度是每个模块的大小。

2、交换技术

交换技术是早期Unix提供的一种方法。大致过程如下图所示:

交换技术在具体实现的过程中,需要解决一些具体问题:

重点说一下程序换入时的重定位。程序在执行过程中,之前换出去的空间可能已经被占用了。此时换入的程序只能放在一个新的地方,如何确保寻址不出问题?前面已经提到过,页表机制里,逻辑地址跟物理地址存在一个映射关系。因此,只要维护一个动态的页表,就能通过逻辑地址找到换入后的物理地址,这就是动态地址映射的方法。

要注意,每次交换的都是一个程序,因此交换的粒度为一个程序的大小,是比较大的,这也是它跟覆盖技术的一个区别,这两种方法的粒度是不一样的。

三、虚拟内存管理技术

在有了覆盖和交换的情况下,为什么还要发展虚拟内存管理技术?

虚拟内存技术是为了克服覆盖和交换技术的缺点而发展起来的,既有交换技术的"不需程序员介入"的优点,也有覆盖技术"内外存之间的交换粒度小"的优点。

1、程序的局部性

为了实现这个目标,需要程序具有局部性。

下面有一个例子,表明了程序的编写方式对缺页率的影响

要注意,int型在32位机中占据的空间是4个字节,也就是说,程序提供给这个进程的4k空间,刚刚好能够存放A一行的数据,也就是1024个整数。

按照编写方法1,访问顺序为A[0][0]、A[1][0]、A[2][0]、……、A[1023][0]、A[0][1]、A[1][1]、……,比如当我们从A[0][0]访问下一个数据A[1][0]的时候,因为第一行A[0][0]——A[0][1023]已经在内存中占据了所有分配给这个进程的空间,所以这个数组的其他数据其实还在硬盘中,并不在内存里,所以此时会产生一个缺页中断,操作系统就需要先把第一行保存到硬盘中,再把第二行,也就是A[1][0]所在的行从硬盘中挪到内存。依此类推,解法1发生了1024*1024次缺页中断。

按照编写方法2,因为是逐行访问的,所以对每一行进行赋值时,只有当访问这一行的第一个元素时,操作系统才会产生缺页中断,把A这一行的数据搬到内存中。访问这一行接下来的数据时,并不会产生缺页中断(毕竟这一行已经放在内存里了)。依此类推,共产生1024次中断。

完成同一个任务(对一个二维数组赋值),上面两个方法的开销是截然不同的。

因此,在编写程序的时候,我们希望程序具有良好的局部性。

2、基本概念

有了虚拟内存管理技术,如果地址线有32位,那么即使物理内存空间很小,通过硬盘的补充配合,每个应用程序都能使用4G的地址空间(虚拟)。

3、实现思路

下面这张图表示了虚拟页式内存管理的基本思路

在页式管理中,页表完成逻辑页到物理页帧的映射。如果在某一次查找中,逻辑地址的页号在页表中找不到相应的物理页帧号(表示物理页帧号是否存在的标志位invalid为1表示存在,为0表示不存在),会返回一个中断。利用这个机制,可以成为虚拟内存管理的有效手段。

四、虚拟内存管理技术的实现

1、虚拟页式存储管理技术的基本思路

随着程序的执行,占用的内存越来越多,可能内存就不够用了。此时就要把不常用的页换出去,放到外存中。把需要使用的页换进来,放到内存里。这就是页面置换功能。这是虚拟内存管理中很重要的一步,这个功能实现的好坏直接决定了虚拟内存管理的效率。

2、页表项

为了实现虚拟内存管理的请求调页和页面置换功能,需要在页表项中增加一些位来实现一些功能。比较重要的四个如下:

驻留位:表示该页存放在内存还是外存。

保护位:对一个页的访问类型的控制。

修改位:表明该页在内存中是否被修改过。如果没被修改过,如果需要回收这一物理页的时候,就不用再把它写回外存了(因为没被修改过,所以它在硬盘中的内容跟内存中的内容一样),直接释放那块空间即可。

访问位:表明这页是否被访问过。这个位用于页面置换算法。如果这个页在当前没被访问过,就可以将其置换出去。

下图是个例子,左半部分表示虚拟内存和物理页帧的映射关系。X表示驻留位为0,也就是这页目前保存在外存中,没有相应的物理页帧跟其对应。其他数据表示与其对应的物理页帧号。

下面我们考虑两个命令:

(1)MOVE REG,0

这个命令的含义是:将逻辑地址0中的数据存读到寄存器中。

0地址在页表中对应的页号是0,偏移量为0。页号0对应的物理页帧号是2,偏移量不变也为0。因此,这个命令,实际上访问的是物理地址8k+0,也就是8192。

(2)MOVE REG,32780

这个命令的含义是:将逻辑地址32780中的数据读到寄存器中。

32780,对应的页号是32780/4k=8,也就是32k到36k的区间,这项的驻留位为0,也就是说,这页的内容其实是存放在外存中的,物理内存中不存在对应的页。此时返回一个缺页中断。

3、缺页中断

缺页中断的处理过程如下:

前面是缺页中断的一个最基本流程,我们可以发现,驻留位和修改位发挥了重要的作用。

4、后备存储(Backing Store)/二级存储

通过前面的学习,我们知道,一个程序中的大量数据其实是存放在外存中的,需要的时候才把它们读到内存中。那我们就会想知道,外存(硬盘)是用什么形式来放置内存中的数据和代码的呢。

5、虚拟内存的性能

上面的EAT=10(1-p)+5000000p(1+q),10代表访问一次内存的时间,p表示出现缺页的概率。5000000表示访问一次硬盘所用的时间,可以看到,访问一次硬盘所用的时间比访问一次内存多得多。

需要注意的是,为什么要乘以(1+q)呢?q表示"需要换出的页已经被修改过的概率",也就是说,如果需要换出的页已经被修改,那还需要将它在硬盘中对应的内容进行修改,还需要进行写硬盘操作,因此还要乘以(1+q)。

访问一次硬盘所用的时间比访问一次内存多得多,只有当p足够小的时候,才能使虚拟内存的平均访问时间接近物理内存。恰巧的是,大部分程序设计中都会尽量使之具有局部性,因此虚拟内存管理的性能是可以得到保障的。

猜你喜欢

转载自blog.csdn.net/qq_26286193/article/details/80250854