第三部分 内存管理
第8章 内存管理
8.1 背景
内存:是现代计算机运行中心。内存由很大一组字或字节组成,每个字或字节都有他们自己的地址。CPU根据程序计数器(PC)值从内存中提取指令,这些指令可能会引起进一步对特定内存地址的读取和写入。
8.1.1 基本硬件
CPU所能访问的存储器只有内存和处理器内的寄存器;
- 保证物理内存的相对速度:高速缓存(cache)[k??]:CPU和内存之间增加高速内存;
- 确保操作系统不被用户进程所访问,以及确保用户进程不被其他用户进程所访问。其可通过硬件来实现。
首先确保每个进程都有独立的内存空间,需确定进程可访问的合法地址的范围并确保进程只访问其合法地址。
基地址寄存器(base register):含有最小的合法物理内存地址;
界限地址寄存器(limit register):j决定范围的大小;
内存空间保护的实现,是通过CPU硬件对用户模式所产生的每一个地址与寄存器地址进行比较来来完成的。
只有操作系统可以通过特殊的特权指令来加载基地址寄存器和界限地址寄存器。(在内核模式下运行)
8.1.2 地址绑定
用户程序在执行前的处理步骤:源程序–>编译器和汇编器(编译时间)–>目标模块–>连接器(其他模块)–>加载模块—>加载器(系统库)(加载时间)–>二进制内存镜像(动态链接)(执行时间,运行时间);
在这些步骤中,地址可能有不同的表现形式。源程序中的地址通常是符号来表示。编译器通常将这些符号地址绑定(bind)在可重定位的地址。链接程序或加载程序再讲这些可重定位的地址绑定成绝对地址。每次绑定都是一个地址空间到另外一个地址空间的映射。
通常将指令与数据绑定到内存地址有一下情况:
- 编译时(compile time):如果在编译时就知道进程将在内存中的驻留地址,那么就可以生成绝对代码(absolute code)。
- 加载时(load time):如果编译时不知道进程将驻留在内存的什么地方,那么编译器必须生成可重定位代码(relocatable code)。对于这种情况,最后绑定会延迟到加载时才进行。
- 执行时(execution time):如果进程在执行时可以从一个内存段移到另一个内存段,那么绑定必须延迟到执行时才进行。
8.1.3 逻辑地址空间和物理地址空间
CPU所生成的地址通常称为逻辑地址(logical adresss),而内存单元所看到的地址(即加载到内存地址寄存器(memory-adress register)中的地址) 通常称为物理地址(physical adress)。
编译和加载时的地址绑定方法生成相同的逻辑地址和物理地址,但执行时的地址绑定方案导致不同的逻辑地址和物理地址。对于这种情况,通常称逻辑地址为虚拟地址(virtual adress)。
运行时从虚拟地址到物理地址的映射是由被称为内存单元(memory-management unit,MMU)的硬件设备来完成的。
用户进程所生成的地址在交送内存之前,在将加上重定位寄存器的值才得到物理地址。
用户处理逻辑地址,内存映射硬件将逻辑地址转变为物理地址。
逻辑地址(范围为0~max),物理地址为(R+0~R+max,其中R为基地址)。
8.1.4 动态加载
动态加载(dynamic loading):采用动态加载时,一个子程序只有在调用时才被加载。所有子程序都以可重定位的形式保存在磁盘上。主程序装入内存并执行。当一个子程序需要调用另一个子程序时,调用子程序首先检查另一个子程序是否已加载。如果没有,可重定位的链接程序将用来加载所需的子程序,并更新程序的地址表以反映这一变换。紧接着,控制传递给新加载的子程序。
动态加载的优点是不用的子程序绝不会加载。如果大多数的代码用来处理异常情况,如错误处理,那么这种方法特别有用。
8.1.5 动态链接与共享库
动态链接库(dynamic linked library):此时系统语言库和其他目标模块一样,由加载程序合并到二进制程序镜像中。动态链接库的概念与动态加载相似,只是这里不是将加载延迟到运行时,而是将链接延迟到运行时,其不需要复制库的副本,节省空间。
共享库:在程序的链接时候并不像静态库那样在拷贝使用函数的代码,而只是作些标记。然后在程序开始启动运行的时候,动态地加载所需模块。所以,应用程序在运行的时候仍然需要共享库的支持。 共享库链接出来的文件比静态库要小得多。
8.2 交换
进程需要在内存中以便执行,不过,进程可以短暂从内存中交换(swap)到备份存储(backing store)上,当需要再次执行时再调回内存中。
8.3 连续内存分配
内存通常分为两个区:一个驻留操作系统(一般位于低内存),另一个用于用户进程。
8.3.1 内存映射和保护
通过重定位寄存器和界限地址寄存器实现。
8.3.2 内存分配
内存分配方法:
- 多分区方法(multile-partion method):将内存分为多个固定大小的分区。
- 可变分区(variable-partition):操作系统有一个表,用于记录那些内存可用。操作系统根据所有进程的内存需要和现有可用内存情况来决定哪些进程可分配内存。即是一种通用动态存储分配问题(根据一组空闲孔来分配大小为n的请求)。方法有:首次适应、最佳适应、最差适应;
8.3.3 碎片
首次适应和最佳适应都有外部碎片问题(external fragmentation):随着进程装入和移出内存,空闲内存空间被分为小片段。当所有总的可用内存之和可以满足请求,但并不连续时,这时就出现了外部碎片问题。
内部碎片:通常将内存以固定大小的块为单元来分配。进程所分配内存可能比所要的要大。这两者数字只差为内部碎片,这部分内存在分区内,但又不能用。
外部碎片解决办法:
- 紧缩(compaction):紧缩的目的是移动内存内容,以便所有空闲空间合并成一整块。但紧缩仅在重定位是动态并在运行时可采用。首先移动程序和数据,然后再根据新基地址的值来改变基地址寄存器。
- 允许物理地址非连续:分页和分段。
8.4 分页
分页(paging):内存管理方案允许进程的物理地址空间可以是非连续的。
8.4.1 基本方法
实现分页的基本方法:将物理内存分为固定大小的块,称为帧(frame);而将逻辑内存也分为同样大小的块,称为页(page)。当需要执行进程时,其页从备份存储中调入到可用的内存帧中。备份内存也分为固定大小的块,其大小与内存帧一样。
分页硬件支持:由cpu生成的每个地址分为两部分:页号(p)和页偏移(d)。页号作为页表的索引。页表包含煤业所在物理内存的基地址,这些基地址与页偏移的组合就形成了物理地址,就可送交物理地址。
也大小(与帧的大小一样)是由硬件来决定的。页的大小通常为2的幂。512B~16MB大小不等。
分页是一种动态重定位。每个逻辑地址有分页硬件绑定为一定的物理地址。
采用分页技术不会产生外部碎片:每个帧都可以分配给需要它的进程。不过,分页有内部碎片。
分页的一个重要特点是:用户视角的内存和实际的物理内存的分离。
8.4.2 硬件支持
绝大多数操作系统都为每个进程分配一个页表。页表的指针域其他寄存器的值一起存入进程控制块中。
页表的硬件实现:将页表作为一组专用寄存器(register)来实现。这些寄存器的应用高速逻辑电路来构造,以便有效的进行分页地址的转换。
页表比较大时需将页表保存在内存中,并将页表基寄存器(page-table base),PTBR):指向页表。但问题是访问用户内存需要一些时间,即采用小但专业且快速的硬件缓冲,这种缓冲称为转换表缓冲区(translation look-aside buffer,TLB)。TLB是关联的快速内存。
TLB:TLB只包含页表中的一部分条目。当CPU产生逻辑地址时,其页号提交给TLB.如果找到页号,那么也就找到帧号,并可用来访问内存。如果页码不再TLB中(TLB失效),那么久需要访问页表。当得到帧号后就可以访问内存。同时将页号和帧号增加到TLB中,这样下次再用就可以很快查找到。
8.4.3 保护
在分页的环境下,内存保护是通过与每个帧相关联的保护位来实现的。通常这些位保存在页表中。
8.4.4 共享页
分页的优点之一就是可以共享公共代码(可重入代码),则可共享。
8.5 页表结构
- 层次页表:现代操作系统支持特大逻辑地址空间(2^32~2^64),在这种情况下,页表本身可以非常大,但事实不可能连续的分配这个页表。于是将页表话费为更小的页表,使用多级分页。
- 哈希页表(hashed page table):用以处理超过32位地址空间的常用方法,并以虚拟页码作为哈希值。哈希表的每一条目都包括一个链表的元素,这些元素哈希成同一个位置(处理碰撞)。每个元素有3个域:(1)虚拟页码;(2)所映射的帧号;(3)指向链表下一个元素的指针。
- 反向页表:
8.6 分段
分段(segmentation):支持用户视角的内存管理方案。逻辑地址空间是由一组段组成的。每个段都有名称和长度。地址指定了段名称和段内偏移(用户指定)。
8.6.2 硬件
段表(segment table):将二维的用户定义地址映射为一维物理地址。段表的每个条目都有段基地址和段界限。
分页和分段的区别:
- 页是信息的物理单位;段是信息的逻辑单位。
- 页的大小固定且由系统确定;段的长度不固定(取决于用户所编写的程序,通常由编译程序在对源程序编译时,根据信息的性质来划分)。
- 分页的作业地址空间是一维的,只需一个记忆符,就可表示一个地址;分段的作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。
- 分段物理空间不连续,但段内是连续的;分页物理空间不连续。
第9章 虚拟内存
虚拟内存技术允许执行进程不必完全在内存中,其显著优点是程序可以比物理内存大。而且,虚拟内存将内存抽象成一个巨大的,统一的存储数组,进而将用户看到的逻辑内存与物理内存分开。这种技术允许程序员不受内存存储的限制。
9.1
虚拟内存(virtual memory):是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
9.2 按需调页
按需调页(demand paging):只有程序执行需要时才才载入页。
调页程序将那些必须的页调入内存,避免读入那些不使用的页,也减少了交换时间和所需的物理内存空间。
需要硬件支持来区分那些页在内存里,哪些页在磁盘上。
支持按需调页的硬件:
- 页表
- 次级存储器
9.3 写时复制
写时复制(copy-on-write):允许父进程与子进程开始时共享同一页面。且这些页面标记为写时复制页,即如果任何一个进程需要对页进程写操作,那么久创建一个共享的副本。
当确定一个页要采用写时复制是,许多操作系统为这类请求提供了空闲缓冲池。通常采用按需填零的技术以分配这些页。
UNIX :vfork(虚拟内存);vfork()会将父进程挂起,子进程使用父进程的地址空间。由于vfork()不使用写时复制,因此子进程修改父进程的地址空间的任何页,那么修改过的页在父进程重启时是可见的。vfork()主要用于在子进程被创建后立即调用exec()的情况。由于没有出现复制页面,vforK()是一种非常有效的进程创建方法,有时用于实现UNIX命令行shell的接口。
9.4 页面置换
页置换:如果没有空闲帧,那么就查找当前没有使用的帧,并将其释放。可将其内容写到交换空间,并改变页表,以表示该页不在内存中。
页置换是按需调页的基础。它分开了逻辑内存与物理内存。采用这种机制,小的物理内存能为程序员提供巨大的虚拟内存。
为实现按需调页,必须解决两个问题:帧分配算法(frame-allocation algorithm)和页置换算法(page-replacement algorithm)。
页置换方法:
- FIFO页置换
- 最优页置换(optimal page-replacement algorithm):产生页错误率最低,它会置换最长时间不会使用的页;
- LRU页置换(最近最少使用算法 least-recently-used(LRU) algorithm):FIFO算法使用的是页调入内存的时间,OPT算法使用的是页将来使用的时间。如果使用的是离过去最近作为不远将来的近似,那么可置换最长时间没有使用的页。
- 基于计数的页置换:最不经常使用页置换算法(least frequently used(LFU) page-replacement algorithm):要求置换计数最小的页;
- 页缓冲算法:
9.5 帧分配
帧的最小数量,全局置换、局部置换;
颠簸(thrashing):频繁的页调度行为;
9.7 内存映射文件
内存映射(memory mapping):使用虚拟内存技术奖文件I/O作为普通内存访问。
9.7.1 基本机制
文件的内存映射可将一磁盘块映射成内存的一页(或多页)。开始的文件访问按普通页面调度来进行,会产生页错误。这样,一页大小的部分文件从文件系统读入物理页。以后文件的读写就按通常的内存访问来处理,由于是通过内存操作文件而不是使用系统调用read()和write(),从而简化了文件的访问。
9.7.3 内存映射I/O
通常,每个I/O控制器包括存放命令及传递数据的寄存器。专用I/O指令允许寄存器和系统内存之间进行数据传递。为了更方便的访问I/O设备,许多计算机都提供内存映射I/O。这样一组内存地址就专门映射到设备寄存器。对这些内存地址读写就如同对设备寄存器的读写。
9.8 内核内存的分配
内核内存的分配通常是从空闲内存池中获取的,而不是从满足普通用户模式进程的内存链表中获取的。主要原因如下:
- 内核需要为不同大小的数据结构分配内存,其中有的不到一页。因此必须谨慎使用内存,并试图减低碎片浪费。
- 用户进程所分配的页不必要在连续的物理内存。而有的硬件要直接与物理内存打交道,而不需要经过虚拟内存接口,因此需要内存常住在连续的物理页中。
对内核进程进行内存管理的两个方法:
- Buddy系统
Buddy系统:从物理上连续的大小固定的段上进行分配。内存按2的幂的大小来进行分配。及4KB,8KB,16KB等。如果请求大小不为2的幂,那么需要调整到下一个更大的2的幂。Buddy系统的一个优点是可通过合并而迅速形成更大的段。缺点是由于调整到下一个2的幂容易产生碎片。
- slab分配
slab分配:slab是由一个或多个物理上连续的页组成的。高速缓存(cache)含有一个或多个slab。slab分配算法采用cache存储内核对象。当初创建cache时,起初包括若干标记为空闲的对象。对象的数量与slab的大小有关。开始所有的对象都标记为空闲。当需要内核数据结构的对象时,可以从cache上直接获取,并将该对象标记为使用。
Linux的slab可有三种状态:
- 满的
- 空的
- 部分
slab分配器的优点:
- 没有内存碎片:每个内核数据结构都有相应的cache,而每个cache都由若干slab组成,而每个slab又分为若干个与对象大小相同的部分。因此,当内核请求对象内存时,slab分配器可以返回刚好可以表示对象所需的内存。
- 内存请求可以快速满足:由于对象预先创建,所以可以从cache上快速分配。另外,当用完对象并释放时,只需要标记为空闲并返回给cache,以便下次再用。