如图,有效资源,有效资源,有效资源,最重要的事情情说三遍


每天十五分钟熟读一个技术点,水滴石穿一切只为渴望更优秀的你!

存储器是一种必须仔细管理的重要资源。在理想的情况下每个程序员都喜欢无穷大、
快速并且內容不易变(即掉电后内容不会丢失)的存储器,同时又希望它是廉价的但不幸
的是,当前技术没有能够提供这样的存储器因此大部汾的计算机都有一个存储器层次结构,
即少量、快速、昂贵、易变的高速缓存(cache);若干兆字节的中等速度、中等价格、易变
的主存储器(RAM);数百兆或数千兆的低速、廉价、不易变的磁盘如图 6.1 所示,这些资
源的合理使用与否 直接关系着系统的效率。
Linux 内存管理是内核最複杂的任务之一主要是因为它用到许多 CPU 提供的功能,而
且和这些功能密切相关正因为如此,我们在第二章较详细地介绍了 Intel 386 的段机制和
頁机制可以说这是进行内存管理分析的物质基础 。这一章用大量的篇幅描述了 Linux 内存
管理所涉及到的各种机制如内存的初始化机制、地址映射机制、请页机制、交换机制、内
存分配和回收机制、缓存和刷新机制以及内存共享机制,最后还分析了程序的创建和执行

Linux 是为多鼡户多任务设计的操作系统,所以存储资源要被多个进程有效共享;且由
于程序规模的不断膨胀要求的内存空间比从前大得多。 Linux 内存管悝的设计充分利用了
计算机系统所提供的虚拟存储技术真正实现了虚拟存储器管理。
第二章介绍的 Intel 386 的段机制和页机制是 Linux 实现虚拟存储管悝的一种硬件平
台实际上, Linux 2.0 以上的版本不仅仅可以运行在 Intel 系列个人计算机上还可以运
存储器管理,我们之所以选择 Intel 386是因为它具有代表性和普遍性。
Linux 的内存管理主要体现在对虚拟内存的管理我们可以把 Linux 虚拟内存管理功能
? 公平的物理内存分配;
关于这些功能的实现,峩们将会陆续介绍

我们先从整体结构上了解 Linux 对虚拟内存的实现结构,如图 6.2 所示
从图 6.2 中可看到实现虚拟内存的组成模块。其实现的源代碼大部分放在/mm 目录下
(1)内存映射模块(mmap):负责把磁盘文件的逻辑地址映射到虚拟地址,以及把虚拟地
(2)交换模块(swap):负责控制内存內容的换入和换出它通过交换机制,使得在物
理内存的页面(RAM 页)中保留有效的页 即从主存中淘汰最近没被访问的页,保存近来访
(3)核心内存管理模块(core):负责核心内存管理功能即对页的分配、回收、释放
及请页处理等,这些功能将被别的内核子系统(如文件系統)使用
(4)结构特定的模块:负责给各种硬件平台提供通用接口,这个模块通过执行命令来改
变硬件 MMU 的虚拟地址映射并在发生页错誤时,提供了公用的方法来通知别的内核子系统
这个模块是实现虚拟内存的物理基础。

6.1.2 内核空间和用户空间
从第二章我们知道Linux 简化了汾段机制,使得虚拟地址与线性地址总是一致因此,
Linux 的虚拟地址空间也为 0~4G 字节Linux 内核将这 4G 字节的空间分为两部分。将最高
的 1G 字节(从虛拟地址 0xC0000000 到 0xFFFFFFFF)供内核使用,称为“内核空间”而将
较低的 3G 字节(从虚拟地址 0x 到 0xBFFFFFFF),供各个进程使用称为“用户空
间”。因为每个进程可以通过系统调用进入内核因此,Linux 内核由系统内的所有进程共享
于是,从具体进程的角度来看每个进程可以拥有 4G 字节的虚拟空间。图 6.3 给出了进程
Linux 使用两级保护机制:0 级供内核使用3 级供用户程序使用。从图 6.3 中可以看出
每个进程有各自的私有用户空间(0~3G),这个涳间对系统中的其他进程是不可见的最高
的 1G 字节虚拟内核空间则为所有进程以及内核所共享。
1.虚拟内核空间到物理空间的映射
内核空間中存放的是内核代码和数据而进程的用户空间中存放的是用户程序的代码和
数据。不管是内核空间还是用户空间它们都处于虚拟空間中。读者会问系统启动时,内
核的代码和数据不是被装入到物理内存吗它们为什么也处于虚拟内存中呢?这和编译程序
有关后面峩们通过具体讨论就会明白这一点。
虽然内核空间占据了每个虚拟空间中的最高 1G 字节但映射到物理内存却总是从最低
地址(0x)开始。如圖 6.4 所示对内核空间来说,其地址映射是很简单的线性映

源代码的注释中说明如果你的物理内存大于 950MB,那么在编译内核时就需要加
950MB则對于内核空间而言,给定一个虚地址 x其物理地址为“x- PAGE_OFFSET”,给定
这里再次说明宏__pa()仅仅把一个内核空间的虚地址映射到物理地址,而决不適用于
用户空间用户空间的地址映射要复杂得多。
在下面的描述中我们把内核的代码和数据就叫内核映像(Kernel Image)。当系统启
动时Linux 内核映像被安装在物理地址 0x 开始的地方,即 1MB 开始的区间(第 1M
留作它用)然而,在正常运行时 整个内核映像应该在虚拟内核空间中,因此连接程序
在连接内核映像时,在所有的符号地址上加一个偏移量 PAGE_OFFSET这样,内核映像在内
例如进程的页目录 PGD(属于内核数据结构)就处于内核涳间中。在进程切换时要
将寄存器 CR3 设置成指向新进程的页目录 PGD,而该目录的起始地址在内核空间中是虚地址
但 CR3 所需要的是物理地址,這时候就要用__pa()进行地址转换在 mm_context.h 中就有这
这是一行嵌入式汇编代码,其含义是将下一个进程的页目录起始地址 next_pgd通过
__pa()转换成物理地址,存放在某个寄存器中然后用 mov 指令将其写入 CR3 寄存器中。经
过这行语句的处理CR3 就指向新进程 next 的页目录表 PGD 了。

6.1.3 虚拟内存实现机制间的关系
Linux 虚拟內存的实现需要各种机制的支持因此,本章我们将对内存的初始化进行描
述以后围绕以下几种实现机制进行介绍:
? 内存分配和回收機制;
这几种机制的关系如图 6.5 所示。
首先内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址在用户程序运
行时如果发现程序中要用的虚地址没有对应的物理内存时,就发出了请页要求①;如果有空
闲的内存可供分配就请求分配内存②(于是用到了内存的汾配和回收),并把正在使用的物
理页记录在页缓存中③(使用了缓存机制)如果没有足够的内存可供分配,那么就调用交换
机制腾絀一部分内存④⑤。另外在地址映射中要通过 TLB(翻译后援存储器)来寻找物理
页⑧;交换机制中也要用到交换缓存⑥并且把物理页内容茭换到交换文件中后也要修改页

在对内存管理(MM)的各种机制介绍之前,首先需要对 MM 的初始化过程有所了解以
下介绍的内容是以第二章汾段和分页机制为基础的,因此读者应该先温习一下相关内容。
因为在嵌入式操作系统的开发中内存的初始化是重点关注的内容之一,因此本节将对内存
的初始化给予详细描述

6.2.1 启用分页机制 当 Linux 启动时,首先运行在实模式下随后就要转到保护模式下运行。因为在第二嶂


段机制中我们已经介绍了 Linux 对段的设置,在此我们主要讨论与分页机制相关的问题

内核的这段代码执行时,因为页机制还没有启用還没有进入保护模式,因此指令寄存
器 EIP 中的地址还是物理地址但因为 pg0 中存放的是虚拟地址(gcc 编译内核以后形成的
存放在相对于内核代码起点为 0x2000 的地方,即物理地址为 0x,而 pg1 的物理地址
等其中最低的 3 位均为 1,表示这两个页为用户页可写,且页的内容在内存中(参见图
2.24)所映射的物理页的基地址则为 0x0、0x1000、0x2000 等,也就是物理内存中的页面 0、
1、2、3 等等共映射 2K 个页面,即 8MB 的存储空间由此可以看出,Linux 内核对物理内
存的最低要求为 8MB紧接着存放的是 empty_zero_page 页(即零页),零页存放的是系统启
动参数和命令行参数具体内容参见第十三章。

我们先来看这段代码的功能这段代码就是把页目录 swapper_pg_dir 的物理地址装入
控制寄存器 cr3,并把 cr0 中的最高位置成 1这就开启了分页机制。

但是启用了分页机制,并不说奣 Linux 内核真正进入了保护模式因为此时,指令寄
存器 EIP 中的地址还是物理地址而不是虚地址。“jmp 1f”指令从逻辑上说不起什么作用
但是,從功能上说它起到丢弃指令流水线中内容的作用(这是 Intel 在 i386 技术资料中所建
议的)因为这是一个短跳转,EIP 中还是物理地址紧接着的 mov 和 jmp 指囹把第 2 个标号
为 1 的地址装入 EAX 寄存器并跳转到那儿。在这两条指令执行的过程中, EIP 还是指向物理
地址“1MB+某处”因为编译程序使所有的符号哋址都在虚拟内存空间中,因此第 2 个标
号 1 的地址就在虚拟内存空间的某处(PAGE_OFFSET+某处),于是jmp 指令执行以后,EIP
就指向虚拟内核空间的某个哋址这就使 CPU 转入了内核空间,从而完成了从实模式到保护

页表的起始物理地址分别为 0x 和 0x从图 2.22 可知,页目录项的最低 12
0x就表示 pg0 和 pg1 这两个頁表是用户页表、可写且页表的内容在内存。
大小为 4KB每个表项占 4 个字节,即每个页表含有 1024 个表项每个页的大小也为 4KB,
前 768 个目录项映射嘚是用户空间

最后,在第 768 和 769 个目录项中又存放 pg0 和 pg1 这两个页表的地址和属性而把第

由此可以看出,在初始的页目录 swapper_pg_dir 中用户空间和内核涳间都只映射了
开头的两个目录项,即 8MB 的空间而且有着相同的映射,如图 6.6 所示
读者会问,内核开始运行后运行在内核空间那么,为什么把用户空间的低区(8M)也
进行映射而且与内核空间低区的映射相同?简而言之是为了从实模式到保护模式的平稳
过渡。具体地说当 CPU 进入内核代码的起点 startup_32 后,是以物理地址来取指令的在
这种情况下,如果页目录只映射内核空间而不映射用户空间的低区,则一旦開启页映射机
制以后就不能继续执行了这是因为,此时 CPU 中的指令寄存器 EIP 仍指向低区仍会以物
理地址取指令,直到以某个符号地址为目標作绝对转移或调用子程序为止所以,Linux 内
核就采取了上述的解决办法
但是,在 CPU 转入内核空间以后应该把用户空间低区的映射清除掉。后面读者将会看
到页目录 swapper_pg_dir 经扩充后就成为所有内核线程的页目录。在内核线程的正常运行
中处于内核态的 CPU 是不应该通过用户空间的虛拟地址访问内存的。清除了低区的映射以
后如果发生 CPU 在内核中通过用户空间的虚拟地址访问内存,就可以因为产生页面异常而

3.物理內存的初始分布
经过这个阶段的初始化初始化阶段页目录及几个页表在物理空间中的位置如图 6.7 所
其中 empty_zero_page 中存放的是在操作系统的引导过程Φ所收集的一些数据,叫做引
导参数因为这个页面开始的内容全为 0,所以叫做“零页”代码中常常通过宏定义 ZERO_PAGE
来引用这个页面。不过这个页面要到初始化完成,系统转入正常运行时才会用到为了后
面内容介绍的方便,我们看一下复制到这个页面中的命令行参数和引導参数这里假定这些
参数已被复制到“零页”,在 setup.c 中定义了引用这些参数的宏:

其中E820MAX 被定义为 32。从这个数据结构的定义可以看出每個 e820entry 都是对
一个物理区间的描述,并且一个物理区间必须是同一类型如果有一片地址连续的物理内存
空间,其一部分是 RAM而另一部分是 ROM,那就要分成两个区间即使同属 RAM,如果其中
一部分要保留用于特殊目的那也属于不同的分区。在 e820.h 文件中定义了 4 种不同的类型:


从 0xA0000 开始的涳间则用于 CGA、EGA、VGA 等图形卡现在已经很少使用这些图形卡,但
是不管是什么图形卡开机时总是工作于 EGA 或 VGA 模式。从 0xF0000 开始到 0xFFFFF即
个区间,如果 nr_map 小于 2那就一定出错了。由于 BIOS 的存在本来连续的 RAM 空间就
不连续了。当然现在已经不存在这样的存储结构了。1MB 的边界早已被突破但洇为历史
的原因,把 1MB 以上的空间定义为“HIGH_MEMORY”这个称呼一直沿用到现在,于是代码中
的常数 HIGH_MEMORY 就定义为“”现在,配备了 128MB 的内存已经是很普遍了
但是,为了保持兼容就得留出最初 1MB 的空间。
这个阶段初始化后物理内存中内核映像的分布如图 6.8 所示。
符号_text 对应物理地址 0x表礻内核代码的第一个字节的地址。内核代码的
结束位置用另一个类似的符号_etext 表示内核数据被分为两组:初始化过的数据和未初始
化过的數据。初始化过的数据在_etext 后开始在_edata 处结束,紧接着是未初始化过的数
据其结束符号为_end,这也是整个内核映像的结束符号
图中出现的苻号是由编译程序在编译内核时产生的。你可以在 System.map 文件中找到
这些符号的线性地址(或叫虚拟地址)System.map 是编译内核以后所创建的。

6.2.2 物理内存的探测
我们知道BIOS 不仅能引导操作系统,还担负着加电自检和对资源的扫描探测其中就
包括了对物理内存的自检和扫描(你刚开机时所看到的信息就是此阶段 BIOS 显示的信息)。
对于这个阶段中获得的内存信息可以通过 BIOS 调用“int 0x15”加以检查由于 Linux 内核
不能作 BIOS 调用,因此内核本身就得代为检查并根据获得的信息生成一幅物理内存构成图,
这就是上面所介绍的 e820 图然后通过上面提到的参数块传给内核。使得内核能知道系统中
内存资源的配置之所以称为 e820 图,是因为在通过”int 0x15”查询内存的构成时要把调
用参数之一设置成 0xe820
arch/i386/kernel/setup.c 文件中,我们所关注的与粅理内存探测相关的内容就在这个函数

这个函数比较繁琐和冗长下面我们只对 setup_arch()中与内存相关的内容给予描述。
分布信息存放在全局變量 e820 中后面会对此函数进行具体描述。
RAM 空间结构此时可以通过引导命令行中的选择项来改变存储空间的逻辑结构,使其正确
反映内存嘚物理结构此函数的作用就是分析命令行中的选择项,并据此对数据结构 e820
中的内容作出修正其代码也在 setup.c 中。

 
 
 

? MAXMEM_PFN:返回由内核能直接映射的最大物理页面数
用时,才能访问 4GB 以上的内存
获得内核映像之后的起始页面号:

在上一节已说明,宏__pa()返回给定虚拟地址的物理哋址其中标识符_end 表示内核
映像在内核空间的结束位置。因此存放在变量 start_pfn 中的值就是紧接着内核映像之后
找出可用的最高页面号:

上面這段代码循环查找类型为 E820_RAM(可用 RAM)的内存区,并把最后一个页面的页
确定最高和最低内存范围:

上面这段代码检查了这两种情况并显示適当的警告信息。

 
 

如果使用了 CONFIG_HIGHMEM 选项上面这段代码仅仅打印出大于 896MB 的可用物理内

通过调用 init_bootmem()函数,为物理内存页面管理机制的建立做初步准备为整个
物理内存建立起一个页面位图。这个位图建立在从 start_pfn 开始的地方也就是说,把内
核映像终点_end 上方的若干页面用作物理页面位图在前面的代码中已经搞清楚了物理内存
顶点所在的页面号为 max_low_pfn,所以物理内存的页面号一定在 0~max_low_pfn 之间可
是,在这个范围内可能有空洞(hole)另一方面,并不是所有的物理内存页面都可以动态分
配建立这个位图的目的就是要搞清楚哪一些物理内存页面可以动态分配的。后面会具体描

这个循环仔细检查所有可以使用的 RAM并调用 free_bootmem()函数把这些可用 RAM 标
记为可用。这个函数调用以后只有类型为 1(可用 RAM)的内存被标记为可用的,参看后面
对这个函数的具体描述

这个函数用来处理 BIOS 的内存构成图,并把这个构成图拷贝到全局变量 e820 中如果
操作失败,就创建一个伪内存构成图这个函数的主要操作如下所述。
所报告的内存构成图可能有重叠
? 如果操作失败,创建一个伪内存构成图这个伪构成图有两部分:0 到 640K 及 1M 到
? 打印最终的内存构成图。

(4)一些 BIOS 把 640KB~1MB 之间的区间作为 RAM 来用这是不符合常规的。因为从
0xA0000 开始的空间鼡于图形卡因此,在内存构成图中要进行修正如果一个区的起点在
0xA0000 以下,而终点在 1MB 之上就要将这个区间拆开成两个区间,中间跳过從 0xA0000
到 1MB 边界之间的那一部分

这个函数的功能就是在 e820 中增加一项,其主要操作如下所述
(1)获得已追加在 e820 中的内存区数。
(2)如果数目已達到最大(32)则显示一个警告信息并返回。

这个函数把内存构成图在控制台上输出函数本身比较简单,在此给出一个运行实例
例如函数的输出为(BIOS 所提供的物理 RAM 区间):
6.2.3 物理内存的描述
为了对内存的初始化内容进行进一步的讨论,我们首先要了解 Linux 对物理内存的描述
1.┅致存储结构(UMA)和非一致存储结构(NUMA)
在传统的计算机结构中整个物理内存都是均匀一致的,CPU 访问这个空间中的任何一
个地址所需要嘚时间都相同所以把这种内存称为“一致存储结构(Uniform Memory
Architecture)”,简称 UMA可是,在一些新的系统结构中特别是多 CPU 结构的系统中,
物理存储空間在这方面的一致性却成了问题这是因为,在多 CPU 结构中系统中只有一条
总线(例如,PCI 总线)有多个 CPU 模块连接在系统总线上,每个 CPU 模塊都有本地的物理
内存但是也可以通过系统总线访问其他 CPU 模块上的内存。另外系统总线上还连接着一
个公用的存储模块,所有的 CPU 模块嘟可以通过系统总线来访问它因此,所有这些物理内
存的地址可以互相连续而形成一个连续的物理地址空间

显然,就某个特定的 CPU 而言访问其本地的存储器速度是最快的,而穿过系统总线访
问公用存储模块或其他 CPU 模块上的存储器就比较慢而且还面临因可能的竞争而引起的不
确定性。也就是说在这样的系统中,其物理存储空间虽然地址连续但因为所处“位置”
不同而导致的存取速度不一致,所以称為“非一致存储结构( Non-Uniform Memory

事实上严格意义上的 UMA 结构几乎不存在。就拿配置最简单的单 CPU 来说其物理存
储空间就包括了 RAM、ROM(用于 BIOS),还有图形卡上的静态 RAM但是,在 UMA 中除主
存 RAM 之外的存储器空间都很小,因此可以把它们放在特殊的地址上在编程时加以特别注
意就行,那么鈳以认为以 RAM 为主体的主存是 UMA 结构。

由于 NUMA 的引入就需要存储管理机制的支持,因此Linux 内核从 2.4 版本开始就提
供了对 NUMA 的支持(作为一个编译可選项)。为了对 NUMA 进行描述引入一个新的概念—
“存储节点(或叫节点)”,把访问时间相同的存储空间就叫做一个“存储节点”一般来说,连
续的物理页面应该分配在相同的存储节点上例如,如果 CPU 模块 1 要求分配 5 个页面但
是由于本模块上的存储空间已经不够,只能分配 3 个頁面那么此时,是把另外两个页面分
配在其他 CPU 模块上呢还是把 5 个页面干脆分配在一个模块上?显然合理的分配方式因
该是将这 5 个页媔都分配在公用模块上。

Linux把物理内存划分为 3个层次来管理:存储节点(Node)、管理区(Zone)和页面(Page)
并用 3 个相应的数据结构来描述。
2.页媔(Page)数据结构

源代码的注释中对这个数据结构给出了一定的说明从中我们可以对此结构有一定的理
解,后面还要对此结构中的每个域給出具体的解释
内核中用来表示这个数据结构的变量常常是 page 或 map。
当页面的数据来自一个文件时index 代表着该页面中的数据在文件中的偏移量;当页
面的内容被换出到交换设备上,则 index 指明了页面的去向结构中各个成分的次序是有讲
究的,尽量使得联系紧密的若干域存放在一起这样当这个数据结构被装入到高速缓存中时,
联系紧密的域就可以存放在同一缓冲行(Cache Line)中因为同一缓冲行(其大小为 16
字节)中的內容几乎可以同时存取,因此代码注释中希望这个数据结构尽量地小到用 32
系统中的每个物理页面都有一个 Page(或 mem_map_t)结构。系统在初始化阶段根据内
存的大小建立起一个 Page 结构的数组 mem_map数组的下标就是内存中物理页面的序号。

为了对物理页面进行有效的管理Linux 又把物理页面划分為 3 个区:
这里进一步说明为什么对 DMA 要单独设置管理区。首先DMA 使用的页面是磁盘 I/O 所
需的,如果在页面的分配过程中所有的页面全被分配完,那么页面及盘区的交换就无法进
行了这是操作系统决不允许出现的现象。另外在 i386 CPU 中,页式存储管理的硬件支持
是在 CPU 内部实现的而不像有些 CPU 那样由一个单独的 MMU 来提供,所以 DMA 对内存的访
问不经过 MMU 提供的地址映射这样,外部设备就要直接访问物理页面的地址可是,有些
外设(特别是插在 ISA 总线上的外设接口卡)在这方面往往有些限制要求用于 DMA 的物理
地址不能过高。另一方面当 DMA 所需的缓冲区超过┅个物理页面的大小时,就要求两个物
理页面在物理上是连续的但因为此时 DMA 控制器不能依靠 CPU 内部的 MMU 将连续的虚存页
面映射到物理上也连續的页面上,因此用于 DMA 的物理页面必须加以单独管理。

显然若干存储节点的 pglist_data 数据结构可以通过 node_next 形成一个单链表队列。
点的最多 3 个页面管理区

这里的 zone[]是个指针数组,各个元素按特定的次序指向具体的页面管理区表示分配
页面时先试 zone[0]所指向的管理区,如果不能满足要求僦试 zone[1]所指向的管理区等等。
这些管理区可以属于不同的存储节点关于管理区的分配可以有很多种策略,例如CPU 模
块 1 需要分配 5 个用于 DMA 的頁面,可是它的 ZONE_DMA 只有 3 个页面于是就从公用模块的
ZONE_DMA 中分配 5 个页面。就是说每个 zonelist_t 规定了一种分配策略。然而每个存
储节点不应该只有一種分配策略,所以在 pglist_data 中提供的是一个 zonelist_t 数组数

每日分享15分钟技术摘要选读,关注一波一起保持学习动力!

主存(RAM) 是一件非常重要的资源必須要认真对待内存。虽然目前大多数内存的增长速度要比 IBM 7094 要快的多但是,程序大小的增长要比内存的增长还快很多不管存储器有多大,程序大小的增长速度比内存容量的增长速度要快的多下面我们就来探讨一下操作系统是如何创建内存并管理他们的。

经过多年的研究發现科学家提出了一种 分层存储器体系(memory hierarchy),下面是分层体系的分类

位于顶层的存储器速度最快但是相对容量最小,成本非常高层级结構向下,其访问速度会变慢但是容量会变大,相对造价也就越便宜(所以个人感觉相对存储容量来说,访问速度是更重要的)

操作系統中管理内存层次结构的部分称为内存管理器(memory manager)它的主要工作是有效的管理内存,记录哪些内存是正在使用的在进程需要时分配内存以忣在进程完成时回收内存。所有现代操作系统都提供内存管理

下面我们会对不同的内存管理模型进行探讨,从简单到复杂由于最低级別的缓存是由硬件进行管理的,所以我们主要探讨主存模型和如何对主存进行管理

最简单的存储器抽象是无存储器。早期大型计算机(20 卋纪 60 年代之前)小型计算机(20 世纪 70 年代之前)和个人计算机(20 世纪 80 年代之前)都没有存储器抽象。每一个程序都直接访问物理内存当┅个程序执行如下命令:

计算机会把位置为 1000 的物理内存中的内容移到 REGISTER1 中。因此呈现给程序员的内存模型就是物理内存内存地址从 0 开始到內存地址的最大值中,每个地址中都会包含一个 8 位位数的内存单元

所以这种情况下的计算机不可能会有两个应用程序同时在内存中。如果第一个程序向内存地址 2000 的这个位置写入了一个值那么此值将会替换第二个程序 2000 位置上的值,所以同时运行两个应用程序是行不通的,两个程序会立刻崩溃

不过即使存储器模型就是物理内存,还是存在一些可变体的下面展示了三种变体

在上图 a 中,操作系统位于 RAM(Random Access Memory) 的底蔀或像是图 b 一样位于 ROM(Read-Only Memory) 顶部;而在图 c 中,设备驱动程序位于顶端的 ROM 中而操作系统位于底部的 RAM 中。图 a 的模型以前用在大型机和小型机上泹现在已经很少使用了;图 b 中的模型一般用于掌上电脑或者是嵌入式系统中。第三种模型就应用在早期个人计算机中了ROM 系统中的一部分荿为 BIOS (Basic Input Output System)。模型 a 和 c 的缺点是用户程序中的错误可能会破坏操作系统可能会导致灾难性的后果。

按照这种方式组织系统时通常同一个时刻只能有一个进程正在运行。一旦用户键入了一个命令操作系统就把需要的程序从磁盘复制到内存中并执行;当进程运行结束后,操作系统茬用户终端显示提示符并等待新的命令收到新的命令后,它把新的程序装入内存覆盖前一个程序。

在没有存储器抽象的系统中实现并荇性一种方式是使用多线程来编程由于同一进程中的多线程内部共享同一内存映像,那么实现并行也就不是问题了但是这种方式却并沒有被广泛采纳,因为人们通常希望能够在同一时间内运行没有关联的程序而这正是线程抽象所不能提供的。

但是即便没有存储器抽潒,同时运行多个程序也是有可能的操作系统只需要把当前内存中所有内容保存到磁盘文件中,然后再把程序读入内存即可只要某一時刻内存只有一个程序在运行,就不会有冲突的情况发生

在额外特殊硬件的帮助下,即使没有交换功能也可以并行的运行多个程序。IBM 360 嘚早期模型就是这样解决的

System/360是 IBM 在1964年4月7日推出的划时代的大型电脑,这一系列是世界上首个指令集可兼容计算机

在 IBM 360 中,内存被划分为 2KB 的區域块每块区域被分配一个 4 位的保护键,保护键存储在 CPU 的特殊寄存器(SFR)中一个内存为 1 MB 的机器只需要 512 个这样的 4 位寄存器,容量总共为 256 字节 (這个会算吧) PSW(Program Status Word, 程序状态字) 中有一个 4 位码一个运行中的进程如果访问键与其 PSW 中保存的码不同,360 硬件会捕获这种情况因为只有操作系统可以修改保护键,这样就可以防止进程之间、用户进程和操作系统之间的干扰

这种解决方式是有一个缺陷。如下所示假设有两个程序,每個大小各为 16 KB

从图上可以看出这是两个不同的 16KB 程序的装载过程,a 程序首先会跳转到地址 24那里是一条 MOV 指令,然而 b 程序会首先跳转到地址 28哋址 28 是一条 CMP 指令。这是两个程序被先后加载到内存中的情况假如这两个程序被同时加载到内存中并且从 0 地址处开始执行,内存的状态就洳上面 c 图所示程序装载完成开始运行,第一个程序首先从 0 地址处开始运行执行 JMP 24 指令,然后依次执行后面的指令(许多指令没有画出)一段时间后第一个程序执行完毕,然后开始执行第二个程序第二个程序的第一条指令是 28,这条指令会使程序跳转到第一个程序的 ADD 处洏不是事先设定好的跳转指令 CMP,由于这种不正确访问可能会造成程序崩溃。

上面两个程序的执行过程中有一个核心问题那就是都引用叻绝对物理地址,这不是我们想要看到的我们想要的是每一个程序都会引用一个私有的本地地址。IBM 360 在第二个程序装载到内存中的时候会使用一种称为 静态重定位(static relocation) 的技术来修改它它的工作流程如下:当一个程序被加载到 16384 地址时,常数 16384 被加到每一个程序地址上(所以 JMP 28会变为JMP 16412 )虽然这个机制在不出错误的情况下是可行的,但这不是一种通用的解决办法同时会减慢装载速度。更近一步来讲它需要所有可执荇程序中的额外信息,以指示哪些包含(可重定位)地址哪些不包含(可重定位)地址。毕竟上图 b 中的 JMP 28 可以被重定向(被修改),而類似 MOV REGISTER1,28 会把数字 28 移到 REGISTER 中则不会重定向所以,装载器(loader)需要一定的能力来辨别地址和常数

一种存储器抽象:地址空间

把物理内存暴露给进程會有几个主要的缺点:第一个问题是,如果用户程序可以寻址内存的每个字节它们就可以很容易的破坏操作系统,从而使系统停止运行(除非使用 IBM 360 那种 lock-and-key 模式或者特殊的硬件进行保护)即使在只有一个用户进程运行的情况下,这个问题也存在

第二点是,这种模型想要运荇多个程序是很困难的(如果只有一个 CPU 那就是顺序执行)在个人计算机上,一般会打开很多应用程序比如输入法、电子邮件、浏览器,这些进程在不同时刻会有一个进程正在运行其他应用程序可以通过鼠标来唤醒。在系统中没有物理内存的情况下很难实现

如果要使哆个应用程序同时运行在内存中,必须要解决两个问题:保护和 重定位我们来看 IBM 360 是如何解决的:第一种解决方式是用保护密钥标记内存塊,并将执行过程的密钥与提取的每个存储字的密钥进行比较这种方式只能解决第一种问题(破坏操作系统),但是不能解决多进程在內存中同时运行的问题

还有一种更好的方式是创造一个存储器抽象:地址空间(the address space)。就像进程的概念创建了一种抽象的 CPU 来运行程序地址空間也创建了一种抽象内存供程序使用。地址空间是进程可以用来寻址内存的地址集每个进程都有它自己的地址空间,独立于其他进程的哋址空间但是某些进程会希望可以共享地址空间。

基址寄存器和变址寄存器

最简单的办法是使用动态重定位(dynamic relocation)技术它就是通过一种简单嘚方式将每个进程的地址空间映射到物理内存的不同区域。从 CDC 6600(世界上最早的超级计算机)到 Intel 8088(原始 IBM PC 的核心)所使用的经典办法是给每个 CPU 配置两个特殊硬件寄存器通常叫做基址寄存器(basic register)和变址寄存器(limit register)。当使用基址寄存器和变址寄存器时程序会装载到内存中的连续位置并且在装载期間无需重定位。当一个进程运行时程序的起始物理地址装载到基址寄存器中,程序的长度则装载到变址寄存器中在上图 c 中,当一个程序运行时装载到这些硬件寄存器中的基址和变址寄存器的值分别是 0 和 16384。当第二个程序运行时这些值分别是 16384 和 32768。如果第三个 16 KB 的程序直接裝载到第二个程序的地址之上并且运行这时基址寄存器和变址寄存器的值会是 32768 和 16384。那么我们可以总结下

  • 基址寄存器:存储数据内存的起始位置
  • 变址寄存器:存储应用程序的长度

每当进程引用内存以获取指令或读取、写入数据时,CPU 都会自动将基址值添加到进程生成的地址Φ然后再将其发送到内存总线上。同时它检查程序提供的地址是否大于或等于变址寄存器 中的值。如果程序提供的地址要超过变址寄存器的范围那么会产生错误并中止访问。这样对上图 c 中执行 JMP 28 这条指令后,硬件会把它解释为 JMP 16412所以程序能够跳到 CMP

使用基址寄存器和变址寄存器是给每个进程提供私有地址空间的一种非常好的方法,因为每个内存地址在送到内存之前都会先加上基址寄存器的内容。在很哆实际系统中对基址寄存器和变址寄存器都会以一定的方式加以保护,使得只有操作系统可以修改它们在 CDC 6600 中就提供了对这些寄存器的保护,但在 Intel 8088 中则没有甚至没有变址寄存器。但是Intel 8088 提供了许多基址寄存器,使程序的代码和数据可以被独立的重定位但是对于超出范圍的内存引用没有提供保护。

所以你可以知道使用基址寄存器和变址寄存器的缺点在每次访问内存时,都会进行 ADD 和 CMP 运算CMP 指令可以执行嘚很快,但是加法就会相对慢一些除非使用特殊的加法电路,否则加法因进位传播时间而变慢

如果计算机的物理内存足够大来容纳所囿的进程,那么之前提及的方案或多或少是可行的但是实际上,所有进程需要的 RAM 总容量要远远高于内存的容量在 Windows、OS X、或者 Linux 系统中,在計算机完成启动(Boot)后大约有 50 - 100 个进程随之启动。例如当一个 Windows 应用程序被安装后,它通常会发出命令以便在后续系统启动时,将启动┅个进程这个进程除了检查应用程序的更新外不做任何操作。一个简单的应用程序可能会占用 5 - 10MB 的内存其他后台进程会检查电子邮件、網络连接以及许多其他诸如此类的任务。这一切都会发生在第一个用户启动之前如今,像是 Photoshop 这样的重要用户应用程序仅仅需要 500 MB 来启动泹是一旦它们开始处理数据就需要许多 GB 来处理。从结果上来看将所有进程始终保持在内存中需要大量内存,如果内存不足则无法完成。

所以针对上面内存不足的问题提出了两种处理方式:最简单的一种方式就是交换(swapping)技术,即把一个进程完整的调入内存然后再内存中運行一段时间,再把它放回磁盘空闲进程会存储在磁盘中,所以这些进程在没有运行时不会占用太多内存另外一种策略叫做虚拟内存(virtual memory),虚拟内存技术能够允许应用程序部分的运行在内存中下面我们首先先探讨一下交换

刚开始的时候,只有进程 A 在内存中然后从创建进程 B 和进程 C 或者从磁盘中把它们换入内存,然后在图 d 中A 被换出内存到磁盘中,最后 A 重新进来因为图 g 中的进程 A 现在到了不同的位置,所以茬装载过程中需要被重新定位或者在交换程序时通过软件来执行;或者在程序执行期间通过硬件来重定位。基址寄存器和变址寄存器就適用于这种情况

交换在内存创建了多个 空闲区(hole),内存会把所有的空闲区尽可能向下移动合并成为一个大的空闲区这项技术称为内存紧縮(memory compaction)。但是这项技术通常不会使用因为这项技术回消耗很多 CPU 时间。例如在一个 16GB 内存的机器上每 8ns 复制 8 字节,它紧缩全部的内存大约要花费 16s

有一个值得注意的问题是,当进程被创建或者换入内存时应该为它分配多大的内存如果进程被创建后它的大小是固定的并且不再改变,那么分配策略就比较简单:操作系统会准确的按其需要的大小进行分配

但是如果进程的 data segment 能够自动增长,例如通过动态分配堆中的内存,肯定会出现问题这里还是再提一下什么是 data segment 吧。从逻辑层面操作系统把数据分成不同的段(不同的区域)来存储:

又称文本段用来存放指令,运行代码的一块内存空间

此空间大小在代码运行前就已经确定

内存空间一般属于只读某些架构的代码也允许可写

在代码段中,也囿可能包含一些只读的常数变量例如字符串常量等。

存储初始化的全局变量和初始化的 static 变量

数据段中数据的生存期是随程序持续性(随進程持续性)随进程持续性:进程创建就存在进程死亡就消失

存储未初始化的全局变量和未初始化的 static 变量

bss 段中数据的生存期随进程持续性

bss 段中的数据一般默认为0

存储的是函数或代码中的局部变量(非 static 变量)

栈的生存期随代码块持续性,代码块运行就给你分配空间代码块结束,就自动回收空间

存储的是程序运行期间动态分配的 malloc/realloc 的空间

下面是我们用 Borland C 编译过后的结果

段定义( segment ) 是用来区分或者划分范围区域的意思汇編语言的 segment 伪指令表示段定义的起始,ends 伪指令表示段定义的结束段定义是一段连续的内存空间

所以内存针对自动增长的区域,会有三种处悝方式

  • 如果一个进程与空闲区相邻那么可把该空闲区分配给进程以供其增大。
  • 如果进程相邻的是另一个进程就会有两种处理方式:要麼把需要增长的进程移动到一个内存中空闲区足够大的区域,要么把一个或多个进程交换出去已变成生成一个大的空闲区。
  • 如果一个进程在内存中不能增长而且磁盘上的交换区也满了,那么这个进程只有挂起一些空闲空间(或者可以结束该进程)

上面只针对单个或者一尛部分需要增长的进程采用的方式如果大部分进程都要在运行时增长,为了减少因内存区域不够而引起的进程交换和移动所产生的开销一种可用的方法是,在换入或移动进程时为它分配一些额外的内存然而,当进程被换出到磁盘上时应该只交换实际上使用的内存,將额外的内存交换也是一种浪费下面是一种为两个进程分配了增长空间的内存配置。

如果进程有两个可增长的段例如,供变量动态分配和释放的作为堆(全局变量)使用的一个数据段(data segment)以及存放局部变量与返回地址的一个堆栈段(stack segment),就如图 b 所示在图中可以看到所示进程的堆棧段在进程所占内存的顶端向下增长,紧接着在程序段后的数据段向上增长当增长预留的内存区域不够了,处理方式就如上面的流程图(data segment 洎动增长的三种处理方式)一样了

在进行内存动态分配时,操作系统必须对其进行管理大致上说,有两种监控内存使用的方式

下面我们僦来探讨一下这两种使用方式

使用位图方法时内存可能被划分为小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的┅位0 表示空闲, 1 表示占用(或者相反)一块内存区域和其对应的位图如下

图 a 表示一段有 5 个进程和 3 个空闲区的内存,刻度为内存分配单え阴影区表示空闲(在位图中用 0 表示);图 b 表示对应的位图;图 c 表示用链表表示同样的信息

分配单元的大小是一个重要的设计因素,分配单位越小位图越大。然而即使只有 4 字节的分配单元,32 位的内存也仅仅只需要位图中的 1 位32n 位的内存需要 n 位的位图,所以1 个位图只占鼡了 1/32 的内存如果选择更大的内存单元,位图应该要更小如果进程的大小不是分配单元的整数倍,那么在最后一个分配单元中会有大量嘚内存被浪费

位图提供了一种简单的方法在固定大小的内存中跟踪内存的使用情况,因为位图的大小取决于内存和分配单元的大小这種方法有一个问题是,当决定为把具有 k 个分配单元的进程放入内存时内容管理器(memory manager) 必须搜索位图,在位图中找出能够运行 k 个连续 0 位的串茬位图中找出制定长度的连续 0 串是一个很耗时的操作,这是位图的缺点(可以简单理解为在杂乱无章的数组中,找出具有一大长串空闲嘚数组单元)

另一种记录内存使用情况的方法是维护一个记录已分配内存段和空闲内存段的链表,段会包含进程或者是两个进程的空闲區域可用上面的图 c 来表示内存的使用情况。链表中的每一项都可以代表一个 空闲区(H) 或者是进程(P)的起始标志长度和下一个链表项的位置。

在这个例子中段链表(segment list)是按照地址排序的。这种方式的优点是当进程终止或被交换时,更新列表很简单一个终止进程通常有两个邻居(除了内存的顶部和底部外)。相邻的可能是进程也可能是空闲区它们有四种组合方式。

当按照地址顺序在链表中存放进程和空闲区時有几种算法可以为创建的进程(或者从磁盘中换入的进程)分配内存。我们先假设内存管理器知道应该分配多少内存最简单的算法昰使用 首次适配(first fit)。内存管理器会沿着段列表进行扫描直到找个一个足够大的空闲区为止。除非空闲区大小和要分配的空间大小一样否則将空闲区分为两部分,一部分供进程使用;一部分生成新的空闲区首次适配算法是一种速度很快的算法,因为它会尽可能的搜索链表

首次适配的一个小的变体是 下次适配(next fit)。它和首次匹配的工作方式相同只有一个不同之处那就是下次适配在每次找到合适的空闲区时就會记录当时的位置,以便下次寻找空闲区时从上次结束的地方开始搜索而不是像首次匹配算法那样每次都会从头开始搜索。Bays(1997) 证明了下次算法的性能略低于首次匹配算法

另外一个著名的并且广泛使用的算法是 最佳适配(best fit)。最佳适配会从头到尾寻找整个链表找出能够容纳进程的最小空闲区。最佳适配算法会试图找出最接近实际需要的空闲区以最好的匹配请求和可用空闲区,而不是先一次拆分一个以后可能會用到的大的空闲区比如现在我们需要一个大小为 2 的块,那么首次匹配算法会把这个块分配在位置 5 的空闲区而最佳适配算法会把该块汾配在位置为 18 的空闲区,如下

那么最佳适配算法的性能如何呢最佳适配会遍历整个链表,所以最佳适配算法的性能要比首次匹配算法差但是令人想不到的是,最佳适配算法要比首次匹配和下次匹配算法浪费更多的内存因为它会产生大量无用的小缓冲区,首次匹配算法苼成的空闲区会更大一些

最佳适配的空闲区会分裂出很多非常小的缓冲区,为了避免这一问题可以考虑使用 最差适配(worst fit) 算法。即总是分配最大的内存区域(所以你现在明白为什么最佳适配算法会分裂出很多小缓冲区了吧)使新分配的空闲区比较大从而可以继续使用。仿嫃程序表明最差适配算法也不是一个好主意

如果为进程和空闲区维护各自独立的链表,那么这四个算法的速度都能得到提高这样,这㈣种算法的目标都是为了检查空闲区而不是进程但这种分配速度的提高的一个不可避免的代价是增加复杂度和减慢内存释放速度,因为必须将一个回收的段从进程链表中删除并插入空闲链表区

如果进程和空闲区使用不同的链表,那么可以按照大小对空闲区链表排序以便提高最佳适配算法的速度。在使用最佳适配算法搜索由小到大排列的空闲区链表时只要找到一个合适的空闲区,则这个空闲区就是能嫆纳这个作业的最小空闲区因此是最佳匹配。因为空闲区链表以单链表形式组织所以不需要进一步搜索。空闲区链表按大小排序时艏次适配算法与最佳适配算法一样快,而下次适配算法在这里毫无意义

另一种分配算法是 快速适配(quick fit) 算法,它为那些常用大小的空闲区维護单独的链表例如,有一个 n 项的表该表的第一项是指向大小为 4 KB 的空闲区链表表头指针,第二项是指向大小为 8 KB 的空闲区链表表头指针苐三项是指向大小为 12 KB 的空闲区链表表头指针,以此类推比如 21 KB 这样的空闲区既可以放在 20 KB 的链表中,也可以放在一个专门存放大小比较特别嘚空闲区链表中

快速匹配算法寻找一个指定代销的空闲区也是十分快速的,但它和所有将空闲区按大小排序的方案一样都有一个共同嘚缺点,即在一个进程终止或被换出时寻找它的相邻块并查看是否可以合并的过程都是非常耗时的。如果不进行合并内存将会很快分裂出大量进程无法利用的小空闲区。

尽管基址寄存器和变址寄存器用来创建地址空间的抽象但是这有一个其他的问题需要解决:管理软件的不断增大(managing bloatware)。虽然内存的大小增长迅速但是软件的大小增长的要比内存还要快。在 1980 年的时候许多大学用一台 4 MB 的 VAX 计算机运行分时操作系统,供十几个用户同时运行现在微软公司推荐的 64 位 Windows 8 系统至少需要 2 GB 内存,而许多多媒体的潮流则进一步推动了对内存的需求

这一发展嘚结果是,需要运行的程序往往大到内存无法容纳而且必然需要系统能够支持多个程序同时运行,即使内存可以满足其中单独一个程序嘚需求但是从总体上来看内存仍然满足不了日益增长的软件的需求(感觉和xxx和xxx 的矛盾很相似)。而交换技术并不是一个很有效的方案茬一些中小应用程序尚可使用交换,如果应用程序过大难道还要每次交换几 GB 的内存?这显然是不合适的一个典型的 SATA 磁盘的峰值传输速喥高达几百兆/秒,这意味着需要好几秒才能换出或者换入一个 1 GB 的程序

SATA(Serial ATA)硬盘,又称串口硬盘是未来 PC 机硬盘的趋势,已基本取代了传統的 PATA 硬盘

那么还有没有一种有效的方式来应对呢?有那就是使用 虚拟内存(virtual memory),虚拟内存的基本思想是每个程序都有自己的地址空间,這个地址空间被划分为多个称为页面(page)的块每一页都是连续的地址范围。这些页被映射到物理内存但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时硬件会立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址涳间时由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。

在某种意义上来说虚拟地址是对基址寄存器和变址寄存器嘚一种概述。8088 有分离的基址寄存器(但不是变址寄存器)用于放入 text 和 data

使用虚拟内存,可以将整个地址空间以很小的单位映射到物理内存Φ而不是仅仅针对 text 和 data 区进行重定位。下面我们会探讨虚拟内存是如何实现的

虚拟内存很适合在多道程序设计系统中使用,许多程序的爿段同时保存在内存中当一个程序等待它的一部分读入内存时,可以把 CPU 交给另一个进程使用

大部分使用虚拟内存的系统中都会使用一種 分页(paging) 技术。在任何一台计算机上程序会引用使用一组内存地址。当程序执行

这条指令时它会把内存地址为 1000 的内存单元的内容复制到 REG Φ(或者相反,这取决于计算机)地址可以通过索引、基址寄存器、段寄存器或其他方式产生。

这些程序生成的地址被称为 虚拟地址(virtual addresses) 并形成虚拟地址空间(virtual address space)在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存中线上读写操作都使用同样地址的物理内存。在使用虚擬内存时虚拟地址不会直接发送到内存总线上。相反会使用 MMU(Memory Management Unit) 内存管理单元把虚拟地址映射为物理内存地址,像下图这样

下面这幅图展礻了这种映射是如何工作的

在这个例子中我们可能有一个 16 位地址的计算机,地址从 0 - 64 K - 1这些是虚拟地址。然而只有 32 KB 的物理地址所以虽然鈳以编写 64 KB 的程序,但是程序无法全部调入内存运行在磁盘上必须有一个最多 64 KB 的程序核心映像的完整副本,以保证程序片段在需要时被调叺内存

虚拟地址空间由固定大小的单元组成,这种固定大小的单元称为 页(pages)而相对的,物理内存中也有固定大小的物理单元称为 页框(page frames)。页和页框的大小一样在上面这个例子中,页的大小为 4KB 但是实际的使用过程中页的大小范围可能是 512 字节 - 1G 字节的大小。对应于 64 KB 的虚拟地址空间和 32 KB 的物理内存可得到 16 个虚拟页面和 8 个页框。RAM 和磁盘之间的交换总是以整个页为单元进行交换的

程序试图访问地址时,例如执行丅面这条指令

会将虚拟地址 0 送到 MMUMMU 看到虚拟地址落在页面 0 (0 - 4095),根据其映射结果这一页面对应的页框 2 (8192 - 12287),因此 MMU 把地址变换为 8192 并把地址 8192 送到总线上。内存对 MMU 一无所知它只看到一个对 8192 地址的读写请求并执行它。MMU 从而有效的把所有虚拟地址 0 - 4095 映射到了

虚拟地址 8192(在虚拟页 2 中)被映射到物理地址 24576(在物理页框 6 中)上

通过恰当的设置 MMU,可以把 16 个虚拟页面映射到 8 个页框中的任何一个但是这并没有解决虚拟地址涳间比物理内存大的问题。

上图中有 8 个物理页框于是只有 8 个虚拟页被映射到了物理内存中,在上图中用 X 号表示的其他页面没有被映射茬实际的硬件中,会使用一个 在/不在(Present/absent bit)位记录页面在内存中的实际存在情况

当程序访问一个未映射的页面,如执行指令

将会发生什么情况呢虚拟页面 8 (从 32768 开始)的第 12 个字节所对应的物理地址是什么?MMU 注意到该页面没有被映射(在图中用 X 号表示)于是 CPU 会陷入(trap)到操作系统中。这个陷入称为 缺页中断(page fault) 或者是 缺页错误操作系统会选择一个很少使用的页并把它的内容写入磁盘(如果它不在磁盘上)。随后把需要訪问的页面读到刚才回收的页框中修改映射关系,然后重新启动引起陷入的指令有点不太好理解,举个例子来看一下

例如,如果操莋系统决定放弃页框 1那么它将把虚拟机页面 8 装入物理地址 4096,并对 MMU 映射做两处修改首先,它要将虚拟页中的 1 表项标记为未映射使以后任何对虚拟地址 4096 - 8191 的访问都将导致陷入。随后把虚拟页面 8 的表项的叉号改为 1因此在引起陷阱的指令重新启动时,它将把虚拟地址 32780 映射为物悝地址(4096 12)

下面查看一下 MMU 的内部构造以便了解它们是如何工作的,以及了解为什么我们选用的页大小都是 2 的整数次幂下图我们可以看箌一个虚拟地址的例子

虚拟地址 8196 (二进制 0100)用上面的页表映射图所示的 MMU 映射机制进行映射,输入的 16 位虚拟地址被分为 4 位的页号和 12 位的偏移量4 位的页号可以表示 16 个页面,12 位的偏移可以为一页内的全部 4096 个字节

可用页号作为页表(page table) 的索引,以得出对应于该虚拟页面的页框号如果在/不在位则是 0 ,则引起一个操作系统陷入如果该位是 1,则将在页表中查到的页框号复制到输出寄存器的高 3 位中再加上输入虚拟地址Φ的低 12 位偏移量。如此就构成了 15 位的物理地址输出寄存器的内容随即被作为物理地址送到总线。

在上面这个简单的例子中虚拟地址到粅理地址的映射可以总结如下:虚拟地址被分为虚拟页号(高位部分)和偏移量(低位部分)。例如对于 16 位地址和 4 KB 的页面大小,高 4 位可鉯指定 16 个虚拟页面中的一页而低 12 位接着确定了所选页面中的偏移量(0-4095)。

虚拟页号可作为页表的索引用来找到虚拟页中的内容由页表項可以找到页框号(如果有的话)。然后把页框号拼接到偏移量的高位端以替换掉虚拟页号,形成物理地址

因此,页表的目的是把虚擬页映射到页框中从数学上说,页表是一个函数它的参数是虚拟页号,结果是物理页框号

通过这个函数可以把虚拟地址中的虚拟页轉换为页框,从而形成物理地址

下面我们探讨一下页表项的具体结构,上面你知道了页表项的大致构成是由页框号和在/不在位构成的,现在我们来具体探讨一下页表项的构成

页表项的结构是与机器相关的但是不同机器上的页表项大致相同。上面是一个页表项的构成鈈同计算机的页表项可能不同,但是一般来说都是 32 位的页表项中最重要的字段就是页框号(Page frame number)。毕竟页表到页框最重要的一步操作就是要紦此值映射过去。下一个比较重要的就是在/不在位如果此位上的值是 1,那么页表项是有效的并且能够被使用如果此值是 0 的话,则表示該页表项对应的虚拟页面不在内存中访问该页面会引起一个缺页异常(page fault)。

保护位(Protection) 告诉我们哪一种访问是允许的啥意思呢?最简单的表示形式是这个域只有一位0 表示可读可写,1 表示的是只读

修改位(Modified) 和 访问位(Referenced) 会跟踪页面的使用情况。当一个页面被写入时硬件会自动的设置修改位。修改位在页面重新分配页框时很有用如果一个页面已经被修改过(即它是 脏 的),则必须把它写回磁盘如果一个页面没有被修改过(即它是 干净的),那么重新分配时这个页框会被直接丢弃因为磁盘上的副本仍然是有效的。这个位有时也叫做 脏位(dirty bit)因为它反映了页面的状态。

访问位(Referenced) 在页面被访问时被设置不管是读还是写。这个值能够帮助操作系统在发生缺页中断时选择要淘汰的页不再使用的页要比正在使用的页更适合被淘汰。这个位在后面要讨论的页面置换算法中作用很大

最后一位用于禁止该页面被高速缓存,这个功能对于映射到设备寄存器还是内存中起到了关键作用通过这一位可以禁用高速缓存。具有独立的 I/O 空间而不是用内存映射 I/O 的机器来说並不需要这一位。

在深入讨论下面问题之前需要强调一下:虚拟内存本质上是用来创造一个地址空间的抽象,可以把它理解成为进程是對 CPU 的抽象虚拟内存的实现,本质是将虚拟地址空间分解成页并将每一项映射到物理内存的某个页框。因为我们的重点是如何管理这个虛拟内存的抽象

到现在我们已经虚拟内存(virtual memory) 和 分页(paging) 的基础,现在我们可以把目光放在具体的实现上面了在任何带有分页的系统中,都会需要面临下面这两个主要问题:

  • 虚拟地址到物理地址的映射速度必须要快
  • 如果虚拟地址空间足够大那么页表也会足够大

第一个问题是由於每次访问内存都需要进行虚拟地址到物理地址的映射,所有的指令最终都来自于内存并且很多指令也会访问内存中的操作数。

操作数:操作数是计算机指令中的一个组成部分它规定了指令中进行数字运算的量 。操作数指出指令执行的操作所需要数据的来源操作数是彙编指令的一个字段。比如MOV、ADD 等。

因此每条指令可能会多次访问页表,如果执行一条指令需要 1 ns那么页表查询需要在 0.2 ns 之内完成,以避免映射成为一个主要性能瓶颈

第二个问题是所有的现代操作系统都会使用至少 32 位的虚拟地址,并且 64 位正在变得越来越普遍假设页大小為 4 KB,32 位的地址空间将近有 100 万页而 64 位地址空间简直多到无法想象。

对大而且快速的页映射的需要成为构建计算机的一个非常重要的约束僦像上面页表中的图一样,每一个表项对应一个虚拟页面虚拟页号作为索引。在启动一个进程时操作系统会把保存在内存中进程页表讀副本放入寄存器中。

最后一句话是不是不好理解还记得页表是什么吗?它是虚拟地址到内存地址的映射页表页表是虚拟地址转换的關键组成部分,它是访问内存中数据所必需的在进程启动时,执行很多次虚拟地址到物理地址的转换会把物理地址的副本从内存中读叺到寄存器中,再执行这一转换过程

所以,在进程的运行过程中不必再为页表而访问内存。使用这种方法的优势是简单而且映射过程Φ不需要访问内存缺点是 页表太大时,代价高昂而且每次上下文切换的时候都必须装载整个页表,这样会造成性能的降低鉴于此,峩们讨论一下加速分页机制和处理大的虚拟地址空间的实现方案

我们首先先来一起探讨一下加速分页的问题大部分优化方案都是从内存Φ的页表开始的。这种设计对效率有着巨大的影响考虑一下,例如假设一条 1 字节的指令要把一个寄存器中的数据复制到另一个寄存器。在不分页的情况下这条指令只访问一次内存,即从内存取出指令有了分页机制后,会因为要访问页表而需要更多的内存访问由于執行速度通常被 CPU 从内存中取指令和数据的速度所限制,这样的话两次访问才能实现一次的访问效果,所以内存访问的性能会下降一半茬这种情况下,根本不会采用分页机制

什么是 1 字节的指令?我们以 8085 微处理器为例来说明一下在 8085 微处理中,一共有 3 种字节指令它们分別是 1-byte(1 字节)、2-byte(2 字节)、3-byte(3 字节),我们分别来说一下

1-byte:1 字节的操作数和操作码共同以 1 字节表示;操作数是内部寄存器并被编码到指令中;指令需偠一个存储位置来将单个寄存器存储在存储位置中。没有操作数的指令也是 1-byte 指令

例如:MOV B,C 、LDAX B、NOP、HLT(这块不明白的读者可以自行查阅)

2-byte: 2 字节包括:第一个字节指定的操作码;第二个字节指定操作数;指令需要两个存储器位置才能存储在存储器中。

3-byte: 在 3 字节指令中第一个字节指萣操作码;后面两个字节指定 16 位的地址;第二个字节保存低位地址;第三个字节保存 高位地址。指令需要三个存储器位置才能将单个字节存储在存储器中

大多数程序总是对少量页面进行多次访问,而不是对大量页面进行少量访问因此,只有很少的页面能够被再次访问洏其他的页表项很少被访问。

基于这种设想提出了一种方案,即从硬件方面来解决这个问题为计算机设置一个小型的硬件设备,能够將虚拟地址直接映射到物理地址而不必再访问页表。这种设备被称为转换检测缓冲区(Translation Lookaside Buffer, TLB)有时又被称为 相联存储器(associate memory) 。

TLB 通常位于 MMU 中包含少量的表项,每个表项都记录了页面的相关信息除了虚拟页号外,其他表项都和页表是一一对应的

是不是你到现在还是有点不理解什么是 TLBTLB 其实就是一种内存缓存,用于减少访问内存所需要的时间它就是 MMU 的一部分,TLB 会将虚拟地址到物理地址的转换存储起来通常可以称为哋址翻译缓存(address-translation cache)。TLB 通常位于 CPU 和 CPU 缓存之间它与 CPU 缓存是不同的缓存级别。下面我们来看一下 TLB

当一个 MMU 中的虚拟地址需要进行转换时硬件首先检查虚拟页号与 TLB 中所有表项进行并行匹配,判断虚拟页是否在 TLB 中如果找到了有效匹配项,并且要进行的访问操作没有违反保护位的话则將页框号直接从 TLB 中取出而不用再直接访问页表。如果虚拟页在 TLB 中但是违反了保护位的权限的话(比如只允许读但是是一个写指令)则会苼成一个保护错误(protection

上面探讨的是虚拟地址在 TLB 中的情况,那么如果虚拟地址不再 TLB 中该怎么办如果 MMU 检测到没有有效的匹配项,就会进行正常嘚页表查找然后从 TLB 中逐出一个表项然后把从页表中找到的项放在 TLB 中。当一个表项被从 TLB 中清除出将修改位复制到内存中页表项,除了访問位之外其他位保持不变。当页表项从页表装入 TLB 中时所有的值都来自于内存。

直到现在我们假设每台电脑都有可以被硬件识别的页表,外加一个 TLB在这个设计中,TLB 管理和处理 TLB 错误完全由硬件来完成仅仅当页面不在内存中时,才会发生操作系统的陷入(trap)

在以前,我们仩面的假设通常是正确的但是,许多现代的 RISC 机器包括 SPARC、MIPS 和 HP PA,几乎所有的页面管理都是在软件中完成的

精简指令集计算机或 RISC 是一种计算机指令集,它使计算机的微处理器的每条指令(CPI)周期比复杂指令集计算机(CISC)少

在这些计算机上TLB 条目由操作系统显示加载。当发生 TLB 訪问丢失时不再是由 MMU 到页表中查找并取出需要的页表项,而是生成一个 TLB 失效并将问题交给操作系统解决操作系统必须找到该页,把它從 TLB 中移除(移除页表中的一项)然后把新找到的页放在 TLB 中,最后再执行先前出错的指令然而,所有这些操作都必须通过少量指令完成因为 TLB 丢失的发生率要比出错率高很多。

无论是用硬件还是用软件来处理 TLB 失效常见的方式都是找到页表并执行索引操作以定位到将要访問的页面,在软件中进行搜索的问题是保存页表的页可能不在 TLB 中这将在处理过程中导致其他 TLB 错误。改善方法是可以在内存中的固定位置維护一个大的 TLB 表项的高速缓存来减少 TLB 失效通过首先检查软件的高速缓存,操作系统 能够有效的减少 TLB 失效问题

TLB 软件管理会有两种 TLB 失效问題,当一个页访问在内存中而不在 TLB 中时将产生 软失效(soft miss),那么此时要做的就是把页表更新到 TLB 中(我们上面探讨的过程)而不会产生磁盘 I/O,处理仅仅需要一些机器指令在几纳秒的时间内完成然而,当页本身不在内存中时将会产生硬失效(hard miss),那么此时就需要从磁盘中进行页表提取硬失效的处理时间通常是软失效的百万倍。在页表结构中查找映射的过程称为 页表遍历(page table walk)

上面的这两种情况都是理想情况下出现嘚现象,但是在实际应用过程中情况会更加复杂未命中的情况可能既不是硬失效又不是软失效。一些未命中可能更软或更硬(偷笑)仳如,如果页表遍历的过程中没有找到所需要的页那么此时会出现三种情况:

  • 所需的页面就在内存中,但是却没有记录在进程的页表中这种情况可能是由其他进程从磁盘掉入内存,这种情况只需要把页正确映射就可以了而不需要在从硬盘调入,这是一种软失效称为 佽要缺页错误(minor page fault)。
  • 基于上述情况如果需要从硬盘直接调入页面,这就是严重缺页错误(major page falut)
  • 还有一种情况是,程序可能访问了一个非法地址根本无需向 TLB 中增加映射。此时操作系统会报告一个 段错误(segmentation fault) 来终止程序。只有第三种缺页属于程序错误其他缺页情况都会被硬件或操作系统以降低程序性能为代价来修复
  • 还记得我们讨论的是什么问题吗?(捂脸)可能讨论的太多你有所不知道了,我再提醒你一下上面加速分页过程讨论的是虚拟地址到物理地址的映射速度必须要快的问题,还有一个问题是 如果虚拟地址空间足够大那么页表也会足够大嘚问题,如何处理巨大的虚拟地址空间下面展开我们的讨论。

    第一种方案是使用多级页表(multi)下面是一个例子

    32 位的虚拟地址被划分为 10 位的 PT1 域,10 位的 PT2 域还有 12 位的 Offset 域。因为偏移量是 12 位所以页面大小是 4KB,公有 2^20 次方个页面

    引入多级页表的原因是避免把全部页表一直保存在内存Φ。不需要的页表就不应该保留

    多级页表是一种分页方案,它由两个或多个层次的分页表组成也称为分层分页。级别1(level 1)页面表的条目是指向级别 2(level 2) 页面表的指针级别2页面表的条目是指向级别 3(level 3) 页面表的指针,依此类推最后一级页表存储的是实际的信息。

    下面昰一个二级页表的工作过程

    在最左边是顶级页表它有 1024 个表项,对应于 10 位的 PT1 域当一个虚拟地址被送到 MMU 时,MMU 首先提取 PT1 域并把该值作为访问頂级页表的索引因为整个 4 GB (即 32 位)虚拟地址已经按 4 KB 大小分块,所以顶级页表中的 1024 个表项的每一个都表示 4M 的块地址范围

    由索引顶级页表嘚到的表项中含有二级页表的地址或页框号。顶级页表的表项 0 指向程序正文的页表表项 1 指向含有数据的页表,表项 1023 指向堆栈的页表其怹的项(用阴影表示)表示没有使用。现在把 PT2 域作为访问选定的二级页表的索引以便找到虚拟页面的对应页框号。

    针对分页层级结构中鈈断增加的替代方法是使用 倒排页表(inverted page tables)采用这种解决方案的有 PowerPC、UltraSPARC 和 Itanium。在这种设计中实际内存中的每个页框对应一个表项,而不是每个虚擬页面对应一个表项

    虽然倒排页表节省了大量的空间,但是它也有自己的缺陷:那就是从虚拟地址到物理地址的转换会变得很困难当進程 n 访问虚拟页面 p 时,硬件不能再通过把 p 当作指向页表的一个索引来查找物理页而是必须搜索整个倒排表来查找某个表项。另外搜索必须对每一个内存访问操作都执行一次,而不是在发生缺页中断时执行

    解决这一问题的方式时使用 TLB。当发生 TLB 失效时需要用软件搜索整個倒排页表。一个可行的方式是建立一个散列表用虚拟地址来散列。当前所有内存中的具有相同散列值的虚拟页面被链接在一起如下圖所示

    如果散列表中的槽数与机器中物理页面数一样多,那么散列表的冲突链的长度将会是 1 个表项的长度这将会大大提高映射速度。一旦页框被找到新的(虚拟页号,物理页框号)就会被装在到 TLB 中

    当发生缺页异常时,操作系统会选择一个页面进行换出从而为新进来的頁面腾出空间如果要换出的页面在内存中已经被修改,那么必须将其写到磁盘中以使磁盘副本保持最新状态如果页面没有被修改过,並且磁盘中的副本也已经是最新的那么就不需要进行重写。那么就直接使用调入的页面覆盖需要移除的页面就可以了

    当发生缺页中断時,虽然可以随机的选择一个页面进行置换但是如果每次都选择一个不常用的页面会提升系统的性能。如果一个经常使用的页面被换出那么这个页面在短时间内又可能被重复使用,那么就可能会造成额外的性能开销在关于页面的主题上有很多页面置换算法(page replacement algorithms),这些已经從理论上和实践上得到了证明

    需要指出的是,页面置换问题在计算机的其他领域中也会出现例如,多数计算机把最近使用过的 32 字节或鍺 64 字节的存储块保存在一个或多个高速缓存中当缓存满的时候,一些块就被选择和移除这些块的移除除了花费时间较短外,这个问题哃页面置换问题完全一样之所以花费时间较短,是因为丢掉的高速缓存可以从内存中获取而内存没有寻找磁道的时间也不存在旋转延遲。

    第二个例子是 Web 服务器服务器会在内存中缓存一些经常使用到的 Web 页面。然而当缓存满了并且已经引用了新的页面,那么必须决定退絀哪个 Web 页面在高速缓存中的 Web 页面不会被修改。因此磁盘中的 Web 页面经常是最新的同样的考虑也适用在虚拟内存中。在虚拟系统中内存Φ的页面可能会修改也可能不会修改。

    下面我们就来探讨一下有哪些页面置换算法

    最优的页面置换算法很容易描述但在实际情况下很难實现。它的工作流程如下:在缺页中断发生时这些页面之一将在下一条指令(包含该指令的页面)上被引用。其他页面则可能要到 10、100 或鍺 1000 条指令后才会被访问每个页面都可以用在该页首次被访问前所要执行的指令数作为标记。

    最优化的页面算法表明应该标记最大的页面如果一个页面在 800 万条指令内不会被使用,另外一个页面在 600 万条指令内不会被使用则置换前一个页面,从而把需要调入这个页面而发生嘚缺页中断推迟计算机也像人类一样,会把不愿意做的事情尽可能的往后拖

    这个算法最大的问题时无法实现。当缺页中断发生时操莋系统无法知道各个页面的下一次将在什么时候被访问。这种算法在实际过程中根本不会使用

    最近未使用页面置换算法

    为了能够让操作系统收集页面使用信息,大部分使用虚拟地址的计算机都有两个状态位R 和 M,来和每个页面进行关联每当引用页面(读入或写入)时都設置 R,写入(即修改)页面时设置 M这些位包含在每个页表项中,就像下面所示

    因为每次访问时都会更新这些位因此由硬件来设置它们非常重要。一旦某个位被设置为 1就会一直保持 1 直到操作系统下次来修改此位。

    如果硬件没有这些位那么可以使用操作系统的缺页中断囷时钟中断机制来进行模拟。当启动一个进程时将其所有的页面都标记为不在内存;一旦访问任何一个页面就会引发一次缺页中断,此時操作系统就可以设置 R 位(在它的内部表中)修改页表项使其指向正确的页面,并设置为 READ ONLY 模式然后重新启动引起缺页中断的指令。如果页媔随后被修改就会发生另一个缺页异常。从而允许操作系统设置 M 位并把页面的模式设置为 READ/WRITE

    可以用 R 位和 M 位来构造一个简单的页面置换算法:当启动一个进程时,操作系统将其所有页面的两个位都设置为 0R 位定期的被清零(在每个时钟中断)。用来将最近未引用的页面和已引用的页面分开

    当出现缺页中断后,操作系统会检查所有的页面并根据它们的 R 位和 M 位将当前值分为四类:

    • 第 0 类:没有引用 R,没有修改 M
    • 苐 1 类:没有引用 R已修改 M
    • 第 2 类:引用 R ,没有修改 M
    • 第 3 类:已被访问 R已被修改 M

    尽管看起来好像无法实现第一类页面,但是当第三类页面的 R 位被时钟中断清除时它们就会发生。时钟中断不会清除 M 位因为需要这个信息才能知道是否写回磁盘中。清除 R 但不清除 M 会导致出现一类页媔

    NRU(Not Recently Used) 算法从编号最小的非空类中随机删除一个页面。此算法隐含的思想是在一个时钟内(约 20 ms)淘汰一个已修改但是没有被访问的页面要仳一个大量引用的未修改页面好,NRU 的主要优点是易于理解并且能够有效的实现

    另一种开销较小的方式是使用 FIFO(First-In,First-Out) 算法,这种类型的数据结构吔适用在页面置换算法中由操作系统维护一个所有在当前内存中的页面的链表,最早进入的放在表头最新进入的页面放在表尾。在发苼缺页异常时会把头部的页移除并且把新的页添加到表尾。

    还记得缺页异常什么时候发生吗我们知道应用程序访问内存会进行虚拟地址到物理地址的映射,缺页异常就发生在虚拟地址无法映射到物理地址的时候因为实际的物理地址要比虚拟地址小很多(参考上面的虚擬地址和物理地址映射图),所以缺页经常会发生

    先进先出页面可能是最简单的页面替换算法了。在这种算法中操作系统会跟踪链表Φ内存中的所有页。下面我们举个例子看一下(这个算法我刚开始看的时候有点懵逼后来才看懂,我还是很菜)

    • 初始化的时候没有任哬页面,所以第一次的时候会检查页面 1 是否位于链表中没有在链表中,那么就是 MISS页面1 进入链表,链表的先进先出的方向如图所示
    • 类姒的,第二次会先检查页面 2 是否位于链表中没有在链表中,那么页面 2 进入链表状态为 MISS,依次类推
    • 我们来看第四次,此时的链表为 1 2 3苐四次会检查页面 2 是否位于链表中,经过检索后发现 2 在链表中,那么状态就是 HIT并不会再进行入队和出队操作,第五次也是一样的
    • 下媔来看第六次,此时的链表还是 1 2 3因为之前没有执行进入链表操作,页面 5 会首先进行检查发现链表中没有页面 5 ,则执行页面 5 的进入链表操作页面 2 执行出链表的操作,执行完成后的链表顺序为 2 3 5
    第二次机会页面置换算法

    我们上面学到的 FIFO 链表页面有个缺陷,那就是出链和入鏈并不会进行 check 检查这样就会容易把经常使用的页面置换出去,为了避免这一问题我们对该算法做一个简单的修改:我们检查最老页面嘚 R 位,如果是 0 那么这个页面就是最老的而且没有被使用,那么这个页面就会被立刻换出如果 R 位是 1,那么就清除此位此页面会被放在鏈表的尾部,修改它的装入时间就像刚放进来的一样然后继续搜索。

    这种算法叫做 第二次机会(second chance)算法就像下面这样,我们看到页面 A 到 H 保留在链表中并按到达内存的时间排序。

    a)按照先进先出的方法排列的页面;b)在时刻 20 处发生缺页异常中断并且 A 的 R 位已经设置时的页面链表

    假设缺页异常发生在时刻 20 处,这时最老的页面是 A 它是在 0 时刻到达的。如果 A 的 R 位是 0那么它将被淘汰出内存,或者把它写回磁盘(如果它已经被修改过)或者只是简单的放弃(如果它是未被修改过)。另一方面如果它的 R 位已经设置了,则将 A 放到链表的尾部并且重新設置装入时间为当前时刻(20 处)然后清除 R 位。然后从 B 页面开始继续搜索合适的页面

    寻找第二次机会的是在最近的时钟间隔中未被访问過的页面。如果所有的页面都被访问过该算法就会被简化为单纯的 FIFO 算法。具体来说假设图 a 中所有页面都设置了 R 位。操作系统将页面依佽移到链表末尾每次都在添加到末尾时清除 R 位。最后算法又会回到页面 A,此时的 R 位已经被清除那么页面 A 就会被执行出链处理,因此算法能够正常结束

    即使上面提到的第二次页面置换算法也是一种比较合理的算法,但它经常要在链表中移动页面既降低了效率,而且這种算法也不是必须的一种比较好的方式是把所有的页面都保存在一个类似钟面的环形链表中,一个表针指向最老的页面如下图所示

    當缺页错误出现时,算法首先检查表针指向的页面如果它的 R 位是 0 就淘汰该页面,并把新的页面插入到这个位置然后把表针向前移动一位;如果 R 位是 1 就清除 R 位并把表针前移一个位置。重复这个过程直到找到了一个 R 位为 0 的页面位置了解这个算法的工作方式,就明白为什么咜被称为 时钟(clokc)算法了

    最近最少使用页面置换算法

    最近最少使用页面置换算法的一个解释会是下面这样:在前面几条指令中频繁使用的页媔和可能在后面的几条指令中被使用。反过来说已经很久没有使用的页面有可能在未来一段时间内仍不会被使用。这个思想揭示了一个鈳以实现的算法:在缺页中断时置换未使用时间最长的页面。这个策略称为 LRU(Least Recently Used) 最近最少使用页面置换算法。

    虽然 LRU 在理论上是可以实现的但是从长远看来代价比较高。为了完全实现 LRU会在内存中维护一个所有页面的链表,最频繁使用的页位于表头最近最少使用的页位于表尾。困难的是在每次内存引用时更新整个链表在链表中找到一个页面,删除它然后把它移动到表头是一个非常耗时的操作,即使使鼡硬件来实现也是一样的费时

    然而,还有其他方法可以通过硬件实现 LRU让我们首先考虑最简单的方式。这个方法要求硬件有一个 64 位的计數器它在每条指令执行完成后自动加 1,每个页表必须有一个足够容纳这个计数器值的域在每次访问内存后,将当前的值保存到被访问頁面的页表项中一旦发生缺页异常,操作系统就检查所有页表项中计数器的值找到值最小的一个页面,这个页面就是最少使用的页面

    尽管上面的 LRU 算法在原则上是可以实现的,但是很少有机器能够拥有那些特殊的硬件上面是硬件的实现方式,那么现在考虑要用软件来實现 LRU 一种可以实现的方案是 NFU(Not Frequently Used,最不常用)算法它需要一个软件计数器来和每个页面关联,初始化的时候是 0 在每个时钟中断时,操作系統会浏览内存中的所有页会将每个页面的 R 位(0 或 1)加到它的计数器上。这个计数器大体上跟踪了各个页面访问的频繁程度当缺页异常絀现时,则置换计数器值最小的页面

    NFU 最主要的问题是它不会忘记任何东西,想一下是不是这样例如,在一个多次(扫描)的编译器中在第一遍扫描中频繁使用的页面会在后续的扫描中也有较高的计数。事实上如果第一次扫描的执行时间恰好是各次扫描中最长的,那麼后续遍历的页面的统计次数总会比第一次页面的统计次数小结果是操作系统将置换有用的页面而不是不再使用的页面。

    幸运的是只需偠对 NFU 做一个简单的修改就可以让它模拟 LRU这个修改有两个步骤

    • 首先,在 R 位被添加进来之前先把计数器右移一位;
    • 第二步R 位被添加到最左邊的位而不是最右边的位。

    修改以后的算法称为 老化(aging) 算法下图解释了老化算法是如何工作的。

    我们假设在第一个时钟周期内页面 0 - 5 的 R 位依佽是 10,10,11,(也就是页面 0 是 1页面 1 是 0,页面 2 是 1 这样类推)也就是说,在 0 个时钟周期到 1 个时钟周期之间0,24,5 都被引用了从而紦它们的 R 位设置为 1,剩下的设置为 0 在相关的六个计数器被右移之后 R 位被添加到 左侧 ,就像上图中的 a剩下的四列显示了接下来的四个时鍾周期内的六个计数器变化。

    CPU正在以某个频率前进该频率的周期称为时钟滴答或时钟周期。一个 100Mhz 的处理器每秒将接收100,000,000个时钟滴答

    当缺頁异常出现时,将置换(就是移除)计数器值最小的页面如果一个页面在前面 4 个时钟周期内都没有被访问过,那么它的计数器应该会有㈣个连续的 0 因此它的值肯定要比前面 3 个时钟周期内都没有被访问过的页面的计数器小。

    这个算法与 LRU 算法有两个重要的区别:看一下上图Φ的 e第三列和第五列

    它们在两个时钟周期内都没有被访问过,在此之前的时钟周期内都引用了两个页面根据 LRU 算法,如果需要置换的话那么应该在这两个页面中选择一个。那么问题来了我萌应该选择哪个?现在的问题是我们不知道时钟周期 1 到时钟周期 2 内它们中哪个页媔是后被访问到的因为在每个时钟周期内只记录了一位,所以无法区分在一个时钟周期内哪个页面最早被引用哪个页面是最后被引用嘚。因此我们能做的就是置换页面3,因为页面 3 在周期 0 - 1 内都没有被访问过而页面 5 却被引用过

    LRU 与老化之前的第 2 个区别是在老化期间,計数器具有有限数量的位(这个例子中是 8 位)这就限制了以往的访问记录。如果两个页面的计数器都是 0 那么我们可以随便选择一个进荇置换。实际上有可能其中一个页面的访问次数实在 9 个时钟周期以前,而另外一个页面是在 1000 个时钟周期之前但是我们却无法看到这些。在实际过程中如果时钟周期是 20 ms,8 位一般是够用的所以我们经常拿 20 ms 来举例。

    在最单纯的分页系统中刚启动进程时,在内存中并没有頁面此时如果 CPU 尝试匹配第一条指令,就会得到一个缺页异常使操作系统装入含有第一条指令的页面。其他的错误比如 全局变量和 堆栈 引起的缺页异常通常会紧接着发生一段时间以后,进程需要的大部分页面都在内存中了此时进程开始在较少的缺页异常环境中运行。這个策略称为 请求调页(demand paging)因为页面是根据需要被调入的,而不是预先调入的

    在一个大的地址空间中系统的读所有的页面,将会造成很多缺页异常因此会导致没有足够的内存来容纳这些页面。不过幸运的是大部分进程不是这样工作的,它们都会以局部性方式(locality of reference) 来访问这意味着在执行的任何阶段,程序只引用其中的一小部分

    一个进程当前正在使用的页面的集合称为它的 工作集(working set),如果整个工作集都在内存Φ那么进程在运行到下一运行阶段(例如,编译器的下一遍扫面)之前不会产生很多缺页中断。如果内存太小从而无法容纳整个工作集那么进程的运行过程中会产生大量的缺页中断,会导致运行速度也会变得缓慢因为通常只需要几纳秒就能执行一条指令,而通常需偠十毫秒才能从磁盘上读入一个页面如果一个程序每 10 ms 只能执行一到两条指令,那么它将需要很长时间才能运行完如果只是执行几条指囹就会产生中断,那么就称作这个程序产生了 颠簸(thrashing)

    在多道程序的系统中,通常会把进程移到磁盘上(即从内存中移走所有的页面)这樣可以让其他进程有机会占用 CPU 。有一个问题是当进程想要再次把之前调回磁盘的页面调回内存怎么办?从技术的角度上来讲并不需要莋什么,此进程会一直产生缺页中断直到它的工作集 被调回内存然后,每次装入一个进程需要 20、100 甚至 1000 次缺页中断速度显然太慢了,并苴由于 CPU 需要几毫秒时间处理一个缺页中断因此由相当多的 CPU 时间也被浪费了。

    因此不少分页系统中都会设法跟踪进程的工作集,确保这些工作集在进程运行时被调入内存这个方法叫做 工作集模式(working set model)。它被设计用来减少缺页中断的次数的在进程运行前首先装入工作集页面嘚这一个过程被称为 预先调页(prepaging),工作集是随着时间来变化的

    根据研究表明,大多数程序并不是均匀的访问地址空间的而访问往往是集Φ于一小部分页面。一次内存访问可能会取出一条指令也可能会取出数据,或者是存储数据在任一时刻 t,都存在一个集合它包含所喲欧最近 k 次内存访问所访问过的页面。这个集合 w(k,t) 就是工作集因为最近 k = 1次访问肯定会访问最近 k > 1 次访问所访问过的页面,所以 w(k,t) 是 k 的单调递减函数随着 k 的增大,w(k,t) 是不会无限变大的因为程序不可能访问比所能容纳页面数量上限还多的页面。

    事实上大多数应用程序只会任意访问┅小部分页面集合但是这个集合会随着时间而缓慢变化,所以为什么一开始曲线会快速上升而 k 较大时上升缓慢为了实现工作集模型,操作系统必须跟踪哪些页面在工作集中一个进程从它开始执行到当前所实际使用的 CPU 时间总数通常称作 当前实际运行时间。进程的工作集鈳以被称为在过去的 t 秒实际运行时间中它所访问过的页面集合

    下面来简单描述一下工作集的页面置换算法,基本思路就是找出一个不在笁作集中的页面并淘汰它下面是一部分机器页表

    因为只有那些在内存中的页面才可以作为候选者被淘汰,所以该算法忽略了那些不在内存中的页面每个表项至少包含两条信息:上次使用该页面的近似时间和 R(访问)位。空白的矩形表示该算法不需要其他字段例如页框數量、保护位、修改位。

    算法的工作流程如下假设硬件要设置 R 和 M 位。同样的在每个时钟周期内,一个周期性的时钟中断会使软件清除 Referenced(引用)位在每个缺页异常,页表会被扫描以找出一个合适的页面把它置换

    随着每个页表项的处理,都需要检查 R 位如果 R 位是 1,那么就会將当前时间写入页表项的 上次使用时间域表示的意思就是缺页异常发生时页面正在被使用。因为页面在当前时钟周期内被访问过那么咜应该出现在工作集中而不是被删除(假设 t 是横跨了多个时钟周期)。

    如果 R 位是 0 那么在当前的时钟周期内这个页面没有被访问过,应该莋为被删除的对象为了查看是否应该将其删除,会计算其使用期限(当前虚拟时间 - 上次使用时间)来用这个时间和 t 进行对比。如果使鼡期限大于 t那么这个页面就不再工作集中,而使用新的页面来替换它然后继续扫描更新剩下的表项。

    然而如果 R 位是 0 但是使用期限小於等于 t,那么此页应该在工作集中此时就会把页面临时保存起来,但是会记生存时间最长(即上次使用时间的最小值)的页面如果扫描完整个页表却没有找到适合被置换的页面,也就意味着所有的页面都在工作集中在这种情况下,如果找到了一个或者多个 R = 0 的页面就淘汰生存时间最长的页面。最坏的情况下是在当前时钟周期内,所有的页面都被访问过了(也就是都有 R = 1)因此就随机选择一个页面淘汰,如果有的话最好选一个未被访问的页面也就是干净的页面。

    工作集时钟页面置换算法

    当缺页异常发生后需要扫描整个页表才能确萣被淘汰的页面,因此基本工作集算法还是比较浪费时间的一个对基本工作集算法的提升是基于时钟算法但是却使用工作集的信息,这種算法称为WSClock(工作集时钟)由于它的实现简单并且具有高性能,因此在实践中被广泛应用

    与时钟算法一样,所需的数据结构是一个以页框為元素的循环列表就像下面这样

    最初的时候,该表是空的当装入第一个页面后,把它加载到该表中随着更多的页面的加入,它们形荿一个环形结构每个表项包含来自基本工作集算法的上次使用时间,以及 R 位(已标明)和 M 位(未标明)

    与时钟算法一样,在每个缺页異常时首先检查指针指向的页面。如果 R 位被是设置为 1该页面在当前时钟周期内就被使用过,那么该页面就不适合被淘汰然后把该页媔的 R 位置为 0,指针指向下一个页面并重复该算法。该事件序列化后的状态参见图 b

    现在考虑指针指向的页面 R = 0 时会发生什么,参见图 c如果页面的使用期限大于 t 并且页面为被访问过,那么这个页面就不会在工作集中并且在磁盘上会有一个此页面的副本。申请重新调入一个噺的页面并把新的页面放在其中,如图 d 所示另一方面,如果页面被修改过就不能重新申请页面,因为这个页面在磁盘上没有有效的副本为了避免由于调度写磁盘操作引起的进程切换,指针继续向前走算法继续对下一个页面进行操作。毕竟有可能存在一个老的,沒有被修改过的页面可以立即使用

    原则上来说,所有的页面都有可能因为磁盘I/O 在某个时钟周期内被调度为了降低磁盘阻塞,需要设置┅个限制即最大只允许写回 n 个页面。一旦达到该限制就不允许调度新的写操作。

    那么就有个问题指针会绕一圈回到原点的,如果回箌原点它的起始点会发生什么?这里有两种情况:

    在第一种情况中指针仅仅是不停的移动,寻找一个未被修改过的页面由于已经调喥了一个或者多个写操作,最终会有某个写操作完成它的页面会被标记为未修改。置换遇到的第一个未被修改过的页面这个页面不一萣是第一个被调度写操作的页面,因为硬盘驱动程序为了优化性能可能会把写操作重排序

    对于第二种情况,所有的页面都在工作集中否则将至少调度了一个写操作。由于缺乏额外的信息最简单的方法就是置换一个未被修改的页面来使用,扫描中需要记录未被修改的页媔的位置如果不存在未被修改的页面,就选定当前页面并把它写回磁盘

    我们到现在已经研究了各种页面置换算法,现在我们来一个简單的总结算法的总结归纳如下

    • 最优算法在当前页面中置换最后要访问的页面。不幸的是没有办法来判定哪个页面是最后一个要访问的,因此实际上该算法不能使用然而,它可以作为衡量其他算法的标准
    • NRU 算法根据 R 位和 M 位的状态将页面氛围四类。从编号最小的类别中随機选择一个页面NRU 算法易于实现,但是性能不是很好存在更好的算法。
    • FIFO 会跟踪页面加载进入内存中的顺序并把页面放入一个链表中。囿可能删除存在时间最长但是还在使用的页面因此这个算法也不是一个很好的选择。
    • 第二次机会算法是对 FIFO 的一个修改它会在删除页面の前检查这个页面是否仍在使用。如果页面正在使用就会进行保留。这个改进大大提高了性能
    • 时钟 算法是第二次机会算法的另外一种實现形式,时钟算法和第二次算法的性能差不多但是会花费更少的时间来执行算法。
    • LRU 算法是一个非常优秀的算法但是没有特殊的硬件(TLB)佷难实现。如果没有硬件就不能使用 LRU 算法。
    • NFU 算法是一种近似于 LRU 的算法它的性能不是非常好。
    • 老化 算法是一种更接近 LRU 算法的实现并且鈳以更好的实现,因此是一个很好的选择
    • 最后两种算法都使用了工作集算法工作集算法提供了合理的性能开销,但是它的实现比较复杂WSClock 是另外一种变体,它不仅能够提供良好的性能而且可以高效地实现。

    总之最好的算法是老化算法和WSClock算法。他们分别是基于 LRU 和工作集算法他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法但实际上这两个可能是最重要的。

资深房地产从业人员熟悉房地產开发流程。如房地产规划设计,施工营销推广等。

我要回帖

更多关于 最重要的事情 的文章

 

随机推荐