操作系统的一个首要的任务就是管理内存,包括从外存装载数据块和换出数据块到外存。
然而,内存I/O是一个很慢的操作,其速度相对于处理器指令周期时间来说差距越来越大。
为了保持处理器处于繁忙状态从而维持效率,操作系统必须巧妙地选择换入换出数据的时机以最小化内存I/O对性能的影响。
在单道程序设计系统中,内存被划分为两个部分:一部分供操作系统使用(驻留监控程序和内核),一部分供当前正在执行的程序使用。
在多道程序设计系统中,必须在内存中进一步细化出“用户”部分,以满足多个进程的要求,细化的任务由操作系统动态完成,这称为内存管理。
内存管理术语:
页框:内存中一个固定长度的块
页:一个固定长度的数据块,存储在二级存储器中(如磁盘)。数据页可以临时复制入内存中的页框中。
段:一个变长的数据块,存储在二级存储器中。
整个段可以临时复制到内存的一个可用区域内(分段),或者可以将一个段分成多个页,将每页单独复制到内存中(分段与分页结合)
1 内存管理的需求
1.1 重定向
在多道程序设计系统中,可用的内存空间通常被多个进程共享。
通常情况下,程序员并不能事先知道在某个程序执行期间会有哪些程序驻留在内存中。
此外还希望通过提供一个巨大的就绪进程池,能够把活动进程换入或换出内存,以便使处理器的利用率最大化。
一旦程序被换出,那么下一次换入时,如果必须放在和换出前相同的内存区域,那么这将会是一个很大的限制。
为了避免这个限制,需要把程序重定向到内存的不同区域。
操作系统需要知道进程控制信息和执行栈的位置,以及进程开始执行程序的入口点。
由于操作系统管理内存并负责把进程放入内存,因此很容易地访问这些地址。
此外,处理器必须处理程序内部的内存访问。
跳转指令包含下一步要执行的指令的位置,数据访问指令报哈被访问数据的字节或字的地址。
处理器硬件和操作系统软件必须能够通过某种方式把程序代码中的内存访问转换成实际的物理内存地址,并反映程序在内存中的当前位置。
1.2 保护
每个进程都应该受到保护,以免被其他进程有意或无意地干涉。
因此,进程以外的其他进程中的程序不能未经授权地访问(包括读写操作)该进程的内存单元。
由于程序在内存中的地址是不可预测的,因而在编译时不可能检查绝对地址来确保保护,并且大多数程序设计语言允许在运行时进行地址的动态计算。
因此,必须在运行时检查进程产生的所有内存访问,以确保它们只访问了分配给该进程的内存空间。
通常,用户进程不能访问操作系统的任何部分,不论是程序还是数据。并且,通常一个进程中的程序不能跳转到另一个进程的指令。
如果没有特别的许可,一个进程中的程序不能访问其他进程的数据区,处理器必须能够在执行时终止这样的指令。
内存保护的需求必须由处理器(硬件)来满足,不能由操作系统软件满足。
操作系统不能预测程序可能产生的所有内存访问,预测内存访问是否违法也很费时。
只能在指令访问内存时来判断这个内存访问是否违法。
1.3 共享
灵活性:允许多个进程访问内存的同一个部分
内存管理系统必须允许对内存共享区域的受控访问,而不会损害基本的保护。
1.4 逻辑组织
计算机系统中的内存总是被组织成线性的(或一维的)地址空间,并且地址空间是由一系列字节或字组成。
外部存储器(简称外存)在物理层上也是按照类似方式组织的。
然而大多数程序被组织成模块,某些不可修改(只读、只执行),某些模块包含可以修改的数据。
操作系统和计算机能够有效地处理以某种模块的形式组织的用户程序和数据,会有很多好处:
1)可以独立地编写和编译模块,系统在运行时解析从一个模块到其他模块的所有引用
2)通过适度的额外开销,可以给不同的模块以不同的保护级别
3)可以引入某些机制,使得模块可以被多个进程共享。
最易于满足这些需求的工具是分段
1.5 物理组织
计算机存储器二级系统:
大容量的外存可以用于长期存储程序和数据,而较小的内存则用于保存当前使用的程序和数据。
系统主要关注的是内存和外存之间信息流的组织
2 内存分区
内存管理最基本的操作是由处理器把程序装入内存中执行。
虚拟内存基于分段和分页这两种基本技术或其中的一种。
2.1 固定分区
大多数的内存管理方案中,可以假定操作系统占据了内存中的某些固定部分,内存的其它部分可供多个用户进程使用。
管理用户内存空间的最简单的方案就是分区,从而形成若干个边界固定的区域。
分区大小:
1)使用大小相等的分区:
难点:
# 程序可能太大而不能放到一个分区中,程序只有一部分放入内存
# 利用率低,由于被装入的数据块小于分区大小,从而导致分区内部有空间浪费,称为内部碎片internal fragmentation。
2)使用大小不等的分区,以产生较少的内部碎片
放置算法:
1)大小相等的分区
只要有可用的分区,进程就可以装入分区
2)大小不等的分区
# 简单办法:把每个进程分配到能够容纳它的最小分区,每个分区维护一个调度队列
# 为所有进程只提供一个队列,可以是优先级队列
当需要把一个进程装入内存时,选择可以容纳该进程的最小可用分区,而不是最小分区(意味着如果不能是最小分区,可以是次小)
2.2 动态分区
分区的长度和数目是可变的。
当进程被装入内存时,系统会给它一块它所需容量完全相同的内存空间。
外部碎片external fragmentation:
在动态分区中,随着进程的换出,内存中出现越来越多的碎片,内存的利用率随之下降,称为外部碎片。
所有分区外的存储空间会变成越来越多的碎片。
克服外部碎片的技术:压缩compaction
操作系统不时地移动进程,使得进程占用的空间连续,并且所有的空闲空间连成一片。
注意压缩需要动态重定位的能力。
放置算法:
都是在内存中选择等于或大于该进程的空闲块
1)最佳适配
选择一个与要求的大小最接近的块
2)首次适配
从头开始扫描内存,选择大小足够的第一个可用块
3)下次适配或邻近适配
从上一次放置的位置开始扫描内存,选择下一个足够的可用块
首次适配算法是最简单的,而且通常也是最好和最快的。
反而最佳适配算法常常是最差的,同时也是导致内存中出现更加零碎的外部碎片,最需要压缩。
置换算法:
出现所有的进程都处于阻塞态,并且即使使用了压缩也没有找到给予新进程足够的空间。
2.3 伙伴系统
固定分区方案限制了活动进程的数目,并且如果可用分区的大小与进程的大小非常不匹配,则内存空间的利用率非常低。
动态分区的维护特别复杂,并且引入了进行压缩的额外开销。
在伙伴系统中,可用的内存块大小是2的K次方个字,且L<= K <= U,其中2的L次方表示分配最小块的尺寸,2的U次方表示分配最大块的尺寸。
开始是,可用于分配的整个空间被看做是一个大小为2的U次方的块。
如果请求的大小s满足在2的u次方和u-1次方之间,那么就分配整个内存空间;否则,该块被分成两个大小相等的伙伴。
继续,如果s满足在2的u-1次方和u-2次方之间,那么就分配两个伙伴中的任何一个,否则,其中一个伙伴又被分成两半。
如此循环,在任何时候,伙伴系统中为所有的的大小为2的i次方的洞维护一个列表。
一个洞可以通过对半分裂从i+1列表中移出,并且在i列表中产生两个大小为2的i次方的伙伴。
当i列表中高端一堆伙伴都变成未分配的块时,他们从该i列表中移出,合并成i+1列表的一个块。
请求大小为k的块,满足k在2的i-1次方之间和2的i次方之间。
伙伴系统是一个合理的折中方案,它克服了固定分区和动态分区的缺陷。
但在当前的操作系统中,基于分页和分段机制的虚拟内存更为先进。
2.4 重定位
进程的换出和换入,程序被放置在不同的内存分区上。
逻辑地址:
与当前数据在内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换成物理地址。
相对地址:
逻辑地址的一个特例,是相对与某些已知点(通常是程序的开始处)的存储单元。
物理地址or绝对地址:
数据在内存中的实际位置。
系统采用运行时动态加载的方式把使用相对地址的程序加载到内存。
通常情况下,被加载进程中的所含有内存访问都相对于程序的开始点。
因此,在执行包括这类访问的指令时,需要一个硬件机制把相对地址转换成物理内存地址。
地址转换的一个典型方法:
当程序被装入内存或当进程的镜像被换入时,设置基址寄存器和界限寄存器,分别指示程序在内存中的起始位置和终止位置。
注意界限寄存器的位置只记录到进程控制块、程序和数据的边界,不包括可变的栈空间。
在进程的执行过程中会遇到相对地址,包括指令寄存器的内容、跳转或调用指令中的指令地址以及加载和存储指令中的数据地址。
每个这样的相对地址都要经过处理器的两步操作:
1)加法器:基址寄存器中的值加上相对地址产生一个绝对地址
2)比较器:
得到的结果与界限寄存器的值进行比较,是否在界限范围内,如果是,继续该指令的执行,否则,向操作系统发出一个中断信号
3 分页
大小不等的固定分区和大小可变的分区技术在内存的使用上都是低效的,前者会产生内部碎片,后者产生外部碎片。
分页:
内存被划分成大小固定相等的块,且块相对较小,每个进程也被分成同样大小的小块,那么进程中称为页的块可以指定到内存中称为页框的可用块。
使用分页技术,在内存中为每个进程浪费的空间仅仅是进程最后一页的一小部分形成的内部碎片。
注意:页是进程中逻辑地址的概念,页框是内存中物理地址的概念
逻辑地址分配不连续页框:
操作系统维护空闲页框的列表,即使没有连续的页框可以使用,采用逻辑地址解决这个问题。
操作系统为每个进程维护一个页表,页表给出了该进程的每一页对应的页框的位置,每一个页表项包含内存中的用于保存相应页的页框的页框号。
在程序中,每个逻辑地址包括一个页号和在该页中的偏移量。
给出逻辑地址(页号,偏移量),处理器使用页表产生物理地址(页框号,偏移量)。
简单分页类似于固定分区,它们的不同之处在于,采用分页技术的分区相当小,一个程序可以占据多个分区,并且这些分区是可以不连续的。
为了使分页方案更加方便,规定页的大小以及页框的大小必须是2的幂,以便容易地表示出相对地址。
相对地址由程序的起点和逻辑地址定义,可以用页号和偏移量表示。
使用页大小为2的幂的页的结果是双重的:
1)逻辑地址方案对编程者、汇编器和连接器是透明的
程序的每个逻辑地址(页号、偏移量)与它的相对地址是一致的。
2)硬件使用运行时动态地址转换的功能相对比较容易
考虑一个n+m的地址,最左边的n位是页号,最右边的m位是偏移量
地址转换的过程:
1.提取页号:即逻辑地址最左边的n位
2.以这个页号为索引,查找该进程页表中相应的页框号k
3.物理地址不需要计算,可以简单地把偏移量附加到页框号后面构造物理地址
采用简单分页技术,内存被分成许多大小相等且很小的页框,每个进程被划分成同样大小的页;
当一个进程被装入时,它的所有页都装入可用页框中,并且建立一个页表。
4 分段
把程序及其相关数据划分到几个段中,不要求段的长度都相同。
和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。
分段依然会产生外部碎片。
分区对于程序员是透明的,分段通常是可见的,并且作为组织程序和数据的一种方便手段提供给程序员。
一般情况下,程序员或编译器会把程序和数据指定到不同的段,不过前提是程序员需要清楚段的最大长度限制。
采用大小不等的段的另一个结果是,逻辑地址和物理地址间不再是简单的对应关系。
类似于分页,在简单分段中,系统维护一个内存中的空闲块列表,每个进程都有一个段表。
每个段表项给出相应段在内存中的起始地址,还必须指明段的长度,以确保不会使用无效地址。
当进程进入运行状态时,系统会把其段表的地址装载到一个寄存器中,由内存管理硬件来使用这个寄存器。
考虑一个n+m位的地址,最左边n位是段号,最右边m位是偏移量,2的m次方表示着段的最大长度。
进行地址转换的步骤:
1)提取段号,即逻辑地址最左n位
2)以段号为索引,查找该进程段表中该段的起始物理地址
3)最右边m位表示偏移量,偏移量和段长度作比较,偏移量超出段长度则地址无效
4)物理地址为该段的其实物理地址和偏移量的和
5 安全问题
内存和虚拟内存是容易受到安全威胁的系统资源,因此需要采取一些安全对策来保护它们。
最明显的安全需求是防止进程中的内容遭受未授权访问。
5.1 缓冲区溢出攻击
缓冲区溢出buffer overflow,也叫内存越界buffer overrun:
输入到一个缓冲区或者数据保存区域的数据量超过了其容量,从而导致覆盖了其他信息的一种状况。
攻击者造成并利用这种状况是系统崩溃或者通过插入特制的代码来控制系统。
当一个进程试图向一个固定大小的缓冲区中存储的数据量超过了这个缓冲区的限制时,会覆盖相邻的内存单元,可能是由编程错误造成的。
任何未经检查的向缓冲区复制数据的行为都有可能导致相邻内存单元数据毁坏。
缓冲区可以位于堆、栈或进程的数据段。
当攻击者成功攻击了一个系统之后,作为攻击的一部分,程序的控制流可能会跳转到攻击者选择的代码处,造成的结果是被攻击的进程尅执行任意的特权代码。
为了利用缓冲区溢出,攻击者需要:
1)找到一些程序中可能被攻击者控制机的外源数据触发的缓冲区溢出漏洞
2)了解缓冲区将如何在进程内存中存储,从而发现毁坏相邻内存单元以及改变程序执行流的可能性
5.2 预防缓冲区溢出
广义的保护对策:
1)编译时防御系统,目的是强化系统以抵制潜伏于新程序中的恶意攻击
2)运行时防御系统,目的是检测并中止现有程序中的恶意攻击
小结:
内存管理是操作系统最重要、最复杂的任务之一。
内存管理把内存看做是一个资源,可以分配给多个活动进程,或者多个活动进程共享。
为了有效地使用处理器和I/O设备,需要在内存中保留尽可能多的进程。
内存管理的基本工具是分页和分段。
采用分页技术,每个进程被划分成相对较小的、大小固定的页。
采用分段技术,可以使用大小不同的块。
还可以在一个单独的内存管理方案中把分页技术和分段技术结合起来使用。
附录:加载与链接
创建活动进程的第一步是吧程序装入内存,并创建一个进程映像。
应用程序由许多已编译过的或汇编过的模块组成,这些模块以目标代码的形式存在,并被连接起来以解析模块间的任何访问和对例程库的访问。
例程库可以被合并到程序中,或作为操作系统在运行时提供的共享代码被访问。
1.加载 加载需要满足寻址需求。
1)绝对加载
要求给定的加载模块总是被加载到内存中的同一位置,所有的地址访问必须是确定的即绝对的内存地址
2)可重定位加载
汇编器或编译器不产生实际的内存地址(绝对地址),而是相对于某些已知点的地址,如相对于程序的起点
加载模块的起点被指定为相对地址0,模块中所有其他内存访问都用于该模块起点的相对值来表示
3)动态运行时加载
程序换出换入的需要,在运行时真正在使用某个绝对地址时再计算它。
加载模块被加载到内存中时,它的所有内存访问都以相对形式表示。为了确保该功能不会降低性能,这些工作必须由特殊的处理器硬件完成。
动态地址计算提供了完全的灵活性,一个程序可以加载到内存中的任何区域,程序的执行可以中断,程序还可以被换出内存,以后再换回到不同的位置。
2.链接
连接器的功能是把一组目标模块作为输入,产生一个包含完整的程序和数据模块的加载模块,传递给加载器。
在每个目标模块中,可能有到其他模块的地址访问,每个这样的访问可以在未链接的目标模块中用符号表示。
连接器会创建一个单独的加载模块,它把所有目标模块一个接一个地链接起来。
每个模块内部的引用必须从符号地址转换成对整个加载模块中的一个位置的引用。
动态连接器
动态链接dynamic linking:
把某些外部模块的链接推迟到创建加载模块之后。
因此,加载模块包含到其他程序的未解析的引用,这些引用可以在加载时或运行时被解析。
步骤:
1)将被加载的加载模块(应用模块)读入内存
2)应用模块中到一个外部模块(目标模块)的任何引用都导致加载程序查找目标模块,加载它,并把这些引用修改成相应于应用程序模块开始处的相对位置。
相较于静态链接的优点:
1)更容易并入已经改变或已升级的模板模块
2)在动态链接文件中的目标代码为自动代码共享铺平了道路
3)它使得独立软件开发人员可以更容易地扩展诸如Linux之类的广泛使用的操作系统功能,如自己包装动态链接模块
使用运行时动态链接时,某些链接工作被推迟到执行时。
这样一些对目标模块的外部引用保留在被加载的程序中,当调用的模块不存在时,操作系统定位该模块,加载它,并把它连接到调研模块中,这些模块一般是共享的,如Windows下的动态链接库DLL。
如果一个进程已经使用了动态链接共享模块,那么该模块就位于内存中,新的进程只需要简单地链接上已经加载好的模块了。
DLL HELL地狱:
两个或多个的进程共享一个DLL模块,但是它们期望链接不同版本的模块。