在之前学习的过程中,我们知道在单道程序设计系统中,内存划分为了两部分:一部分供操作系统使用,用于驻留监控程序、内核。另一部分供当前正在执行的程序使用。
在多道程序设计系统中,则必须在内存中进一步细分出“用户”部分,以满足多个进程的要求。
而细分的任务由操作系统动态完成,这成为内存管理。
在第七章中,我们会首先考察内存管理要满足的需求,然后说一些简单的内存管理模式。
明确一些内存中的管理术语:
页框:内存中固定长度的块
页:固定长度的数据块,存储在二级存储器中(如磁盘)。数据页可以临时复制到内存的页框中。
段:变长数据块,存储在二级存储器中。整个段可以临时复制到内存的一个可用区域中(分段),或可以将一个段分为许多页,然后将每页单独复制到内存中(分段与分页相结合)
7.1内存管理的需求
内存管理的需求如下:
重定位,保护,共享,逻辑组织,物理组织
7.1.1重定位
重定位是指在程序运行时,将程序中使用的内存地址从逻辑地址(也称虚拟地址)转换为物理地址的过程。在执行程序时,由于每个程序都有自己的内存空间,所以需要将程序中使用的逻辑地址映射到实际的内存地址,这个过程就是重定位。
重定位的目的是使程序能够在任意可用的内存地址空间中运行,而不必担心内存地址的冲突问题。当程序被加载到内存中时,操作系统会将程序中使用的逻辑地址映射为实际的物理地址。如果程序需要访问另一个模块或库,则需要对该模块或库的地址进行重定位,以确保它们可以正确地被访问。
重定位可以通过多种方式实现,包括基址寄存器、段寄存器等。无论哪种方式,其本质都是将逻辑地址转换为物理地址,以便程序可以正确地运行。
举个栗子~
假设你的程序需要在不同的内存地址上运行,为此需要进行重定位。首先,在编译程序时就需要使用重定位表来指示可执行文件中哪些部分需要进行重定位,以及如何进行重定位。
然后,在程序运行时,操作系统需要将程序加载到内存中并解析重定位表。当程序需要访问内存中固定位置的变量或数据时,操作系统会根据重定位表中的信息计算出实际的内存地址,并将其返回给程序。
举个例子,如果程序需要访问内存地址0x1000处的数据,但实际加载到内存中的位置是0x2000处,那么操作系统会根据重定位表中的信息将地址0x1000重定位为地址0x2000,使得程序可以正确地访问数据。
重定位可以帮助程序在不同的内存地址上运行,从而更好地适应不同的硬件环境和操作系统。
7.1.2保护
每个进程都应该受到保护,以免被其他进程所干扰。
7.1.3共享
任何保护机制都具有一定的灵活性,以允许多个进程访问内存的统一部分。
例如,多个进程正在执行同一个程序时,允许每个进程访问该程序的同一个部分,要比让每个进程有自己单独的副本更有优势。合作完成同一个任务的进程可能需要共享访问相同的数据结构。
7.1.4逻辑组织
计算机系统中的内存总是被组织成线性(或一维)的地址空间,且地址空间由一系列字节或字组成。外部存储器(简称外存)在物理层上也是按类似方式组织的。尽管这种组织方式类似于实际的机器硬件,但它并不符合程序构造的典型方法。大多数程序被组织成模块,某些模块时不可修改的(只读、只执行),某些模块包含可以修改的数据。
若操作系统和计算机硬件能够有效地处理以某种模块形式组织的用户程序与数据,会有如下好处:
1.可以独立地编写和编译模块,系统在运行时解析从一个模块到其他模块的所有引用。
2.通过适度的额外开销,可以为不同的模块提供不同的保护级别(只读、只执行)
3.可以引入某种机制,使得模块可被多个进程共享。
最易于满足这些需求的工具是分段,它也是本章将要探讨的一种内存管理技术。
举个栗子~
假设操作系统需要管理一台计算机上的物理内存,该计算机有1GB的物理内存空间。为了最大化利用这些内存资源,操作系统可以将内存分为多个区域,例如:
-
内核空间:用于存储操作系统及其服务所需的数据结构和程序代码,通常占用100MB左右的空间。
-
用户空间:用于存储用户进程所需的数据结构和程序代码,占用剩余的物理内存空间。
在内存映射中,操作系统需要将进程逻辑地址空间映射到物理内存中。例如,当一个进程需要访问某个变量时,它会使用逻辑地址来寻址该变量,而操作系统则会根据逻辑地址的映射关系,将该变量从物理内存中读取出来并交给进程使用。
为了保证不同进程之间不能互相干扰,操作系统采用了内存保护的措施。例如,在虚拟内存中,操作系统可以通过设置页面权限等方式,确保每个进程只能访问自己被授权的内存地址范围,从而防止恶意进程对其他进程的内存进行非法访问。
最后,在内存回收方面,当一个进程不再需要内存时,操作系统需要及时回收这些内存并将其返回给空闲内存池。例如,在Linux下,操作系统会使用Page Cache来管理内存,当一个进程所占用的页面不再被使用时,该页面会被放入Page Cache中,并在需要时被重新分配给其他进程使用。
这里扩展一个内容,叫做内存映射
内存映射是指操作系统将进程的逻辑地址空间映射到物理内存中的过程。在内存映射中,操作系统通过建立虚拟地址和物理地址之间的映射关系,使得进程可以访问内存中的数据。
在内存映射中,每个进程都有自己的逻辑地址空间,该空间通常是一个连续的、线性的地址空间。这些逻辑地址可以被映射到物理内存中的任意位置,因此进程并不需要知道其所使用的实际物理地址。当进程需要访问某个内存地址时,它只需要使用逻辑地址即可,由操作系统负责将其转换为实际的物理地址。
内存映射可以带来很多好处。首先,它方便了程序员编写程序,因为程序员不再需要考虑物理内存地址的分配与管理。其次,内存映射可以提高进程访问内存的效率,因为操作系统可以将相邻的内存页一起加载到内存中,从而减少内存访问的频率。最后,内存映射还可以支持多进程共享内存的特性,这对于需要共享数据的多进程应用非常重要。
内存映射是操作系统中非常重要的一个概念,它在操作系统中有着广泛的应用。例如,在Linux系统中,内存映射被广泛用于文件I/O、进程间通信等方面。
7.1.5物理组织
在机组中我们学到了,计算机存储器至少要分为两级---内存和外存。
内存提供快速的访问,成本也相对较高。此外,内存是易失性的,即它不能提供长久性存储。
外存比内存慢而且便宜,且通常是非易失性的。
因此,大容量的外存可用于长期存储程序和数据,而较小的内存则用于保存当前使用的程序和数据。
在这两级方案中,系统主要关注的是内存和外存之间信息流的组织。我们可以让程序员负责组织这一信息流,但由于以下两方面的原因,这种方法是不切实际的:
1.供程序和数据使用的内存可能不足。此时,程序员必须采用覆盖技术来组织程序和数据。不同的模块被分配到内存中的同一块区域,主程序负责在需要时换入或换出模块。即使有编译工具的帮助,覆盖技术的实现仍然非常浪费程序员的时间。
2.在多道程序设计环境中,程序员在编写代码时并不知道可用空间的大小及位置。
7.2内存分区
内存管理的主要操作是处理器把程序装入内存中执行。在几乎所有的现代多道程序设计系统中,内存管理涉及一种称为虚存的复杂方案。虚存基于分段和分页这两种基本技术,或基于这两种技术中的一种。在考虑虚存技术之前,先考虑一些不涉及虚存的简单技术,其中分区技术曾用于已过时的操作系统中。另外两种技术,即简单分页和简单分段,实际中并未使用过。在不考虑虚存的前提下,先分析这两种技术有助于阐明虚存的概念。
7.2.1固定分区
大多数内存管理方案都假定操作系统占据内存中的某些固定部分,而内存中的其余部分则供多个用户进程使用。管理用户内存空间的最简方案就是对它分区,以形成若干边界固定的区域。
7.2.1.1分区大小:固定分区有两种选择,如下图所示
一种是使用大小相等的分区,此时小于等于分区大小的任何进程都可装入任何可用的分区中。若所有的分区都已满,且没有进程处于就绪态或运行态,则操作系统可以换出一个进程的所有分区,并装入另一个进程,使得处理器有事可做。
很明显,a的难点:程序可能太大而不能放到一个分区中。此时,程序员必须使用覆盖技术设计程序,使得在任何时候该程序只有一部分需要放到内存中。当需要的模块不在时,用户程序必须把这个模块装入程序的分区,覆盖该分区中的任何程序和数据。
还有一个缺点就是内存的利用率很低,当一个2MB的程序占据了一个8MB的分区。由于装入的数据块小于分区大小,因而分区内部存在空间浪费,这种现象称为内部碎片
7.2.1.2放置算法:
对于大小相等的分区策略,进程在内存中的放置十分简单。只要存在可用的分区,进程就能装入分区。
若所有的分区都处于可运行状态的进程占据,则这些进程中的一个必须被换出,以便为新进程让出空间。换出哪个进程属于调度问题,后续会学到。
对于大小不等的分区策略,有如下两种方法:
1.把每个进程分配到能够容纳它的最小分区中。在这种情况下,每个分区都需要维护一个调度队列,用于保存从这个分区换出的进程。优点:若所有进程都按这种方式分配,则可使每个分区内部浪费的空间最少。
但是若有如下情况:在某个确定的时刻,系统中没有大小在12MB到16MB之间的进程。此时,即使系统中有一些更小的进程本可以分配到16MB的分区,但16MB的分区仍会保持闲置。
2.因此,一种更可取的方法是为所有进程只提供一个队列。当需要把一个进程装入内存时,选择可以容纳该进程的最小可用分区。若所有的分区都已被占据,则必须进行交换。一般优先考虑换出能容纳新进程的最小分区中的进程,或考虑一些诸如优先级之类的其他因素。
使用该种方法为固定分区带来了一定的灵活性,分区方案比较简单。
缺点:
分区的数量在系统生成阶段已经确定,因而限制了系统中活动(为挂起)进程的数量
由于分区的大小是在系统生成阶段事先设置的,因而小作业的不能有效地利用分区空间。在事先直到所有作业的内存需求的情况下,这种方法也许是合理的,但大多数情况下这种技术非常低效。
7.2.2动态分区
对于动态分区而言,分区长度和数量是可变的。当进程装入内存时,系统会给它分配一块与其所需容量完全相等的内存空间。
7.2.2.1定义
从上图的过程来理解动态分区。最初内存中只有操作系统的8MB。从操作系统结束处开始,装入的前三个进程分别占据各自所需的空间大小b~d,这样子在内存末尾就存在一个“空洞”。但是空洞对于进程4来说又太小。在某个时刻,内存中没有一个就绪进程。操作系统换出进程2,这为装入进程4腾出了足够的空间。由于进程4小于进程2,形成了另一个6mb的空洞,如f所示。然后,在另一个时刻,内存中没有一个进程是就绪的,但处于就绪挂起状态的进程2可用。由于内存中没有足够的空间容纳进程2,操作系统换出进程1,然后换入进程2。
缺点很明显:形成了许多的空洞,内存的利用率随之下降。这种现象称为外部碎片。指在所有分区外的存储空间变成了越来越多的碎片,与内部碎片相对应。
克服外部碎片的一种技术是压缩:操作系统不时地移动进程,使得进程占用的空间连续,并使所有空闲空间连成一片。例如在h中,若压缩后,则会产生一个16MB的空闲空间,足以装下另一个进程。
当然压缩的缺点是:压缩是一个十分费时的过程,且会浪费处理器时间。
另外,压缩需要动态重定位的能力,也就是说,必须能够把程序从内存的一块区域移动到另一块区域,且不会使程序中的内存访问无效。
7.2.2.2放置算法
由于内存压缩非常费时,因而操作系统需要巧妙地把进程分配到内存中,以便盖住内存中那些“空洞”。当把一个进程装入或换入内存时,若内存中有多个足够大的空闲块,则操作系统必须确定要为此进程分配哪个空闲块。
有三种放置算法:
最佳适配、首次适配、下次适配。最佳适配选择与要求大小最接近的块;首次适配从头开始扫描内存,选择大小足够的第一个可用块;下次适配从上一次放置的位置开始扫描内存,选择下一个大小足够的可用块。
首次适配算法不仅是最简单的,而且通常也是最快的和最好的,下次适配算法通常要比首次适配的结果差,且常常会在内存的末尾分配空间,通常导致位于存储空间末尾的最大空闲存储块很快分裂为小碎片。因此,使用下次适配算法可能需要更多次数的压缩。另一方面,首次适配算法会使得内存的前端出现很多小空闲分区。最佳适配算法尽管称为最佳,但是由于其要查找满足要求的最小块,因而性能是最差的。
7.2.2.3置换算法
使用动态分区的多道程序设计系统中,有时会出现内存中的所有进程都处于阻塞带的情况,即使进行了压缩,新进程仍然没有足够的内存空间。为避免由于等待一个活动进程接触阻塞状态引起的处理器时间浪费,操作系统将把一个阻塞的进程换出内存,给新进程或处于就绪-挂起态的进程让出空间。因此,操作系统必须选择要替换哪个进程。由于置换算法的一些细节设计各种虚存方法,因此将在讨论虚存时再讨论置换细节。
7.2.3伙伴系统
固定分区和动态分区都有缺陷,当可用分区大小与进程大小很不匹配时,内存空间的利用率就会很低。而且动态分区的维护特别复杂,会引入压缩的开销。这里介绍伙伴系统。
伙伴系统的思路如下:
7.3分页
分页技术是将内存分为大小固定、相等的块,且块相对比较小,从而将每个进程分为许多大小相同的小块。进程中称为页的块可以分配到内存中称为页框的可用块。
下面是分页法的过程:存储在磁盘上的进程A由4页组成。装入这个进程时,操作系统查找4个空闲页框,并将进程A的4页装入这4个页框中。进程B包含3页,进程C包含4页,它们依次装入。然后进程B被挂起,并被换出内存。此后,内存中的所有进程被阻塞,操作系统需要换入一个新进程,即进程D,由5页组成。可以看到进程D在内存中的存储是不连续的,为了方便的使用,需要使用逻辑地址来解决这个问题。操作系统为每个进程维护一个页表,页表给出了该进程的每页所对应页框的位置。
每个逻辑地址包括一个页号和在该页中的偏移量。
在简单分区情况下,逻辑地址是一个字相对于程序开始处的位置,处理器把它转换为一个物理地址。
在分页中,逻辑地址到物理地址的转换仍然由处理器硬件完成,且处理器必须知道如何访问当前进程的页表。给出逻辑地址后(页号,偏移量),处理器使用页表产生物理地址(页框号,偏移量)
逻辑地址转换为物理地址的过程如下:
这里使用16位地址,页大小为1KB,由于页大小为1KB,因此偏移量为10位,剩下6位为页号,因此一个程序最多由2^6=64页组成,每页大小为1KB。
例如:相对地址1502的二进制形式为:0000 0101 1101 1110
因此相对地址的页号为:000001,偏移量为:0111011110=478
7.4分段
分段技术是将程序与其相关的数据划分到几个段,尽管段有最大长度限制,但并不要求所有程序的所有段的长度都相等。
与分页类似,采用分段技术的逻辑地址也由两部分组成:段号和偏移量
由于使用大小不等的段,分段类似于动态分区。在未采用覆盖方案或使用虚存的情况下,为执行一个程序,需要把它的所有段都装入内存,与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但和动态分区一样,它会产生外部碎片。
总之,采用简单的分段技术,进程可划分为许多段,段的大小无须相等;调入一个进程时,其所有段都装入内存的可用区域,并建立一个段表。