基本分段存储管理
"分段"与"分页"的最大区别就是离散分配时所分配的地址空间的基本单位不同
分段
进程的地址空间会按照程序的逻辑关系划分为若干个段,每个段都有一个段名(段名是程序自己定义的,在低级语言中,程序员使用段名来编程),每段都从0开始编址
内存分配规则:以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻
如进程A的大小为16KB,按照它的逻辑关系将它划分为三个段,每个段就代表一个完整的逻辑模块,比如0号段的段名叫MAIN,段内存放的是有关main函数的数据和指令;1号段的段名为X,其内部保存着进程A中的一些子函数;2号段段名叫D,里面保存了进程A中使用到的一些全局变量
由于是按逻辑功能进行划分,因此用户编程更方便,程序的可读写更高
asm
LOAD 1, [D] | <A>; // 将分段D中A单元内的值读入寄存器1中
STORE 1, [X] | <B>; // 将寄存器1的内容存入X分段的B单元中
编译程序在编译时会将指令的段名转换为段号
程序员编程时使用段名来编程,但CPU具体执行指令时使用的是段号,即CPU是根据段号来区分不同的段
分段系统的逻辑地址结构由段号和段内地址(段内偏移量)组成:
段号的位数决定了每个进程最多可以分为几个段
段内地址的位数决定了每个段的最大长度是多少
如上图,若系统按字节寻址,则段号占16位,因此在该系统中,每个进程最多有2^16 = 64K个段
段内地址占16位,因此每个段的最大长度是2^16 = 64KB
汇编指令LOAD 1, [D] | <A>
在编译时,段名[D]
会被编译程序翻译成对应的段号,单元<A>
会被编译程序翻译成段内地址
段表
将程序分为多个段,各段离散地装入内存,为了保证程序能正常运行,就必须保证能从物理内存中找到各个逻辑段的存放位置。为此,需要为每个进程建立一张段映射表,简称段表
段表的作用和页表类似
页表中记录了各个逻辑页面到实际的物理页框之间的映射关系
段表中记录了各个逻辑段到实际的物理内存存放位置之间的映射关系
每个段对应一个段表项,其中记录了该段在内存中的起始位置(又称"基址")和段的长度(即该段在内存中所占地址空间的大小)
段表相比与页表,段表中多了一个段长字段,因为每一个分段的长度可能是不一样的,而分页存储中每一个页面的长度是相等的,因此在页表中不需要显式地记录页面大小,而在段表中就需要显式记录段长信息
各个段表项的长度是相同的
由于各个段表项的长度是相同的,因此段表中的段号字段也是可以隐含的
例:某系统按字节寻址,采用分段存储管理,逻辑地址结构中段内地址占16位 ,因此用16个bit即可表示最大段长(即段长部分用16个bit即可)。物理内存大小为4GB,可用32个bit表示整个物理内存地址空间(即基址部分用32个bit即可)。因此,可以让每个段表项占16 + 32 = 48位bit,即6B。由于段表项的长度都相同,因此段号可以隐含,不占存储空间。若段表存放的起始地址为M,则K号段对应的段表项存放的地址为M + K * 6
分段存储管理的地址变换
以汇编指令LOAD R1, [D] | <A>
为例,该指令是要将分段D中A单元内的值读入到寄存器1中:
该指令经过编译程序编译之后,会形成等价的机器语言指令,该机器语言指令的具体内容大致为"取出段号为2,段内地址为1024号内存单元中的内容,把它存放到寄存器R1中"
在计算机硬件看来,该机器语言指令中的段号和段内地址其实都是用二进制表示的逻辑地址,CPU在执行该指令时,需要把此逻辑地址转换为物理地址
操作系统会把内存划分为系统区和用户区,系统区会存放一些操作系统对整个计算机软硬件进行管理的一些数据结构,其中就包括进程控制块PCB
当进程被调度,并上处理机运行时,进程切换相关的内核程序就会把该进程相关的运行环境恢复(即把PCB中存放的相关数据存放到CPU内的寄存器中),如把PCB中记录的段表起始地址F和段表长度M存放到段表寄存器中
- 段表起始地址和段表长度这两个信息在进程没有上处理机运行之前,是存放在进程的PCB中的
进程运行的期间,避免不了要访问一些逻辑地址。当访问逻辑地址时,系统会根据逻辑地址自动将其拆分为段号S和段内地址W两个部分
系统得到段号信息后,会将段号S部分与段表寄存器中的段表长度M进行比较,如果发现段号S超过段表长度M(即检查段号是否越界),则认为该逻辑地址非法,系统便会产生一个越界中断,之后会有相应的中断处理程序处理该中断
若段号合法,则会根据段表起始地址找到段表,再根据段号查找段表中相应的段表项,段号S对应的段表项的存放地址为F + S * 段表项长度(段表项长度信息系统是知道的)
找到对应的段表项后,系统还会对逻辑地址当中的段内地址W与段表项中记录的段长C进行比较,也就是检查段内地址是否超过记录的段长。若发现段内地址超过段长(即越界),则产生越界中断,否则继续执行
- 在页式管理系统中,每个页面的大小是固定且相等的,因此系统不需要检查页内偏移量是否超过页面的长度,因为页内偏移量所占的位数就是由页面大小决定的
- 段长是从1开始的,而段内地址是从0开始的
若段内地址合法,就可以根据段表项中的基址与逻辑地址中的段内地址进行相加得到实际的物理地址
- 注意:段表中没有块号这种字段
最后就可以访问该物理地址对应的内存单元了
分段、分页管理的对比
页是信息的物理单位,分页时只考虑各个页面的物理大小。分页的主要目的是为了实现离散分配,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统的行为,对用户是不可见的
用户不知道进程会被分为几个页面,甚至不知道进程是否被分页
段是信息的逻辑单位,分段时需要考虑信息的逻辑关系(如把一个具有完整逻辑关系的模块单独地划分为一个段)。分段的主要目的是更好地满足用户需求(方便用户编程)。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的,用户编程时需要显式地给出段名
用户知道程序会被分段,甚至知道会被分为几个段,每个段的段名是什么
页的大小是固定的,且由系统决定 段的长度不固定,它取决于用户编写的程序结构
分页的用户进程地址空间是一维的,程序员只需要给出一个记忆符即可表示一个地址 分段的用户进程地址空间是二维的,程序员在标识一个地址时,即要给出段名,也要给出段内地址
采用分页存储方式下,一个用户进程的大小是16KB,在用户看来该进程的整个逻辑地址空间就是从0到16K - 1这样连续的16K个地址空间,用户在编程时,只需要用一个记忆符就可以表示一个地址,如
LOAD 1, <A>
中的<A>
表示某个页面某个内存单元
采用分段存储方式下,一个用户进程的大小是16KB,用户知道该进程会被分为几个段,并且知道每个段的逻辑地址都是从0开始的,因此用户编程时,即需要给出段名也要给出段内地址,如LOAD 1, [D] | <A>
分段比分页更容易实现信息的共享和保护
例如一个生产者进程占16KB,采用分段存储方式下,它可以被分为三个段,其中1号段的功能是判断缓冲区此时是否可以访问。该段允许所有生产者、消费者进程共享访问
由于1号段允许所有生产者和消费者进程共享访问,因此为了实现共享访问该段的功能,可以让该段对应的段表项在多个进程的段表中出现
若采用分页存储方式,每个页面4KB,其中第一个页面是进程的0号段中的前半部分,而第二个页面中则是由0号段的后半部分以及1号段的前半部分共同组成,第三个页面中则是由1号段的后半部分以及2号段的前一部分共同组成,第四个页面是由2号段的后面部分组成
因此采用分页存储方式,如果也使用多个进程的页表中出现同一个页表项的方式,显然是不合理的,因为在包含可共享段的页面中还存在不能被共享的部分,导致这种现象的根本原因就在于页面并不是按照逻辑模块划分的
对于信息的保护也是类似,在分段存储管理中,可以对每一个段表项增加一个标记位,对于允许其他进程访问的段,可以将其对应的段表项的标记位设置为"允许其他进程访问"
但在分页存储管理中,由于包含可共享的页面中还存在不可被共享的部分,因此单单采用一个标志位的方式不能实现信息的保护(或者说很难实现)
注意:不能被修改的代码称为纯代码或可重入代码(它不属于临界资源),这样的代码是可以共享的。可修改的代码是不能共享的(比如有一个代码段中有很多变量,各进程并发地同时访问可能造成数据不一致)
探讨:访问一个逻辑地址需要几次访存(均未引入快表机制)?
分页(单级页表):第一次访存 ------ 查内存中的页表,第二次访存 ------ 访问目标内存单元。总共需要两次访存
分段:第一次访存 ------ 查内存中的段表,第二次访存 ------ 访问目标内存单元。总共需要两次访存
与分页系统类似,分段系统中也可以引入快表机构,将近期访问过的段表项放到快表中,这样可以减少一次访存,加快地址变换速度
总结
段页式管理方式
分页、分段的优缺点分析
优点 | 缺点 | |
---|---|---|
分页管理 | 内存空间利用率高,不会产生外部碎片,只会有少量的页内碎片 | 不方便按照逻辑模块实现信息的共享和保护 |
分段管理 | 很方便按照逻辑模块实现信息的共享和保护 | 如果段长过大,为其分配很大的连续空间会很不方便。另外,段式管理会产生外部碎片 |
段式管理方式产生外部碎片的原因和动态分区分配类似:
假设内存中依次进入了三个分段并且三个分段占用了一块连续的内存空间,它们所占内存空间大小如图所示:
此时系统中只剩下一片大小为4MB的内存区域,某一时刻一个分段运行结束,可以先将它移出内存,于是内存中就又出现了一片大小为14MB的空间
某一时刻另一个分段进入内存,它被分配在了大小为14MB的空闲区域的起始位置处,所以此时内存中就剩下一块4MB和一块10MB的空闲空间
如果占用20MB的分段也运行完成,也可以将该分段移出内存,此时内存中又多出一块大小为20MB的空间
某一时刻一个占用14MB的分段进入内存,它被分配在了大小为20MB的空闲区域的起始位置处,所以此时内存中就剩下一块6MB和一块10MB以及一块4MB的空闲空间
某一时刻一个占用20MB的分段需要进入内存,但此时系统中已经没有一块能够容纳20MB大小的连续空闲空间,因此该分段无法进入内存
在这种情况下,这三块空闲空间就是所谓的外部碎片,因为它已经不能满足分段分配的要求了
这三块空闲区域一共拥有了20MB大小空闲空间,采用紧凑技术就可以让这三块不连续的空间合并为一块连续的空闲空间,紧凑技术就是将在内存中的分段进行"挪位"
采用"紧凑"技术解决分段管理中的外部碎片虽然可行,但使用紧凑技术是需要付出较大的时间代价的,因此该方法并不是一个很好的解决方案
基于分段和分页两种管理方式的优缺点,又提出了这两种管理方式的结合 ------ 段页式管理,段页式管理方式同时具备了分页管理和分段管理两者的优点
段页式管理
在采用段页式管理方式的系统中,一个进程首先会按照逻辑模块划分为若干个段,之后还会将划分出的各段进行分页(比如每个页面的大小为4KB)
内存空间同样也会被分块,块大小和页面大小一致
之后进程的各个页面会被依次放入到不同的内存块中
注意:同一个页面中不会同时出现包含两个段的情况,因此有些页面中会出现页内碎片(即页面空间并没有被分段占满)
段页式管理的逻辑地址结构
分段系统的逻辑地址结构由段号和段内地址(段内偏移量)组成,如:
段页式系统的逻辑地址结构由段号、页号、页内地址(页内偏移量)组成,如:
在段页式管理方式中,会先将进程分段,之后才将分出的各个段进行分页,因此段页式管理方式中的逻辑地址结构包含三个部分
而段页式管理方式中逻辑地址的页号和页内偏移量其实就是将分段系统中的逻辑地址的段内地址进行拆分后得到的
段号的位数决定了每个进程最多可以被划分为几个段
页号的位数决定了每个段最多会被划分为多少个页面
页内偏移量决定了页面大小、内存块大小是多少
若系统按字节寻址,且段号占16位,页号占4位,页内偏移量占12位
因此在该系统中,每个进程最多有2^16 = 64K个段,每个段最多可以被分为2^4 = 16个页面,每个页面/内存块的大小为2^12 = 4096 = 4KB
在段页式管理方式中,"分段"对用户是可见的,程序员编程时需要显式地给出段号、段内地址。而将各段"分页"对用户是不可见的。系统会根据段内地址自动将其划分为页号和页内偏移量两个部分
因此段页式管理的地址结构是二维的
段表、页表
系统会为每个进程建立一张段表,进程中的一个段会对应段表中的一个段表项,每个段表项由段号、页表长度(即页表中有几个页表项)、页表存放块号(根据该信息可以得到页表的起始地址)组成。每个段表项的长度是相同的,因此段号可以隐含
系统还会为进程中的每一个段建立一张页表,一个段中的一个页面会对应页表中的一个页表项。每个页表项由页号、页面所在内存块号(根据该信息可以得到页面在内存中的存放位置)组成。每个页表项的长度是相同的,因此页号可以隐含
一个进程只会对应一个段表,但可能会对应多个页表
段页式管理的地址变换
系统中存在一个硬件 ------ 段表寄存器,它可以保存段表起始地址F和段表长度M两个信息
进程在上处理机运行之前,会从该进程的PCB中读出这两个信息并存放到段表寄存器中
地址转换时,首先系统会根据提供的逻辑地址,将其划分为段号S、页号P和页内偏移量W三个部分
之后,系统会将段号S与段表寄存器中记录的段表长度M进行比较,判断段号是否越界,越界则产生越界中断,否则继续进行转换工作
若地址合法,则可以根据段号S、段表起始地址F以及段表项长度(该信息系统清楚)找到段号S对应的段表项
由于各个段的长度是不一样的,将各个段分页之后,可能会分为数量不等的页面,因此在找到段表项后,还需要将页号P与段表项中记录的页表长度信息进行比较,判断页号是否越界
若地址合法,则就可以根据段表项中记录的页表存放块号信息与内存块大小找到页表,再根据页号P、页表项长度在页表中找到页号P对应的页表项
找到页表项后,就可以知道该页面所在的内存块号,最后就可以将内存块号与页内地址进行拼接得到物理地址,并对该地址所处的内存单元进行访问
段页式管理对逻辑地址访问需要进行三次访存,第一次访存 ------ 访问段表,第二次访存 ------ 访问页表,第三次访存 ------ 访问实际内存单元
也可以引入快表机构,用段号以及页号作为查询快表的关键字。若快表均命中,则访问逻辑地址仅需一次访存(即最终访问内存单元)
总结
虚拟内存的基本概念
传统存储管理方式的特征、缺点
传统存储管理方式包括连续分配和非连续分配两种,连续分配方式又进一步分为单一连续分配方式、固定分区分配方式和动态分区分配方式;非连续分配方式又进一步分为基本分页存储管理、基本分段存储管理和基本段页式存储管理
如果系统仅仅采用传统存储管理方案,会有很多暂时用不到的数据长期占用内存,导致内存利用率不高
传统管理方式存在很明显的特征(缺点):
-
一次性:作业必须一次性全部装入内存后才能运行。这会造成两个问题:
① 作业很大时,不能全部装入内存,导致大作业无法运行
② 当大量作业要求运行时,由于内存无法容纳所有作业,因此只有少量作业能运行,导致多道程序并发度下降
-
驻留性:一旦作业被装入内存,就会一直驻留在内存中,直至作业运行结束。事实上,在一个时间段内,只需要访问作业的一小部分数据即可正常运行,这就导致了内存中会驻留大量的、暂时用不到的数据,浪费了宝贵的内存资源
传统管理方式的这两个缺点都可以使用虚拟存储技术来解决
虚拟存储技术是基于局部性原理提出的
局部性原理
时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次被执行;如果某个数据被访问过,那么不久之后该数据很可能再次被访问(因为程序中存在大量的循环)
空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问(因为很多数据在内存中都是连续存放的,并且程序的指令在内存中也是顺序地存放的)
虚拟内存的定义和特征
基于局部性原理,在程序装入时,可以将程序中很快会用到的部分装入内存,暂时用不到的部分留在外存,就可以让程序开始执行
基于局部性原理,人们也提出了高速缓存技术,其核心思想是将使用频繁的数据放到更高速的存储器中,暂时用不到或使用频率低的放到相对低速的存储器中
在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序
若内存空间不足时,由操作系统负责将内存中暂时用不到的信息换出到外存
因此在操作系统的管理下,在用户看来似乎有一个比实际内存大得多的内存,这就是虚拟内存
虚拟内存是操作系统虚拟性的一个体现,实际的物理内存大小没变,只是在逻辑上进行了扩充
虚拟内存有以下三个特征:
- 多次性:无需在作业运行时一次性全部装入内存,而是允许被分成多次调入内存
- 对换性:在作业运行时无需全部一直常驻内存,而是允许在作业运行过程中,将部分作业换入、换出内存
- 虚拟性:从逻辑上看扩充了内存的容量,但用户看到的内存容量远大于实际的内存容量
虚拟内存中的多次性对应基本存储方式中的一次性,对换性对应基本存储方式中的驻留性
如何实现虚拟内存技术
虚拟内存技术,允许一个作业分多次调入内存。如果采用连续分配方式,则不太方便实现。因此,虚拟内存的实现需要建立在离散分配的内存管理方式的基础上
如果采用连续分配方式,那么一个作业如果先调入了一部分,则后一部分要想调入的话,就必须为其分配与在原先前一部分空间相连续的空间,因此不方便实现虚拟内存技术
传统的非连续分配存储管理方式在应用了虚拟内存技术后:
- 基本分页存储管理 + 虚拟内存技术 ------ 请求分页存储管理
- 基本分段存储管理 + 虚拟内存技术 ------ 请求分段存储管理
- 基本段页式存储管理 + 虚拟内存技术 ------ 请求段页式存储管理
传统管理方式和加入了虚拟内存技术的管理方式的最大区别在于:
- 在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序
- 若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存
因此,为了实现这两种全新的特性,操作系统需要在基本的存储方式的基础上,增加两个主要的功能:
- 请求调页(或请求调段)功能:指在请求分页(或请求分段)存储管理方式中,如果此时需要的页面(或段)暂时还不在内存中,则操作系统还需要将页面(或段)从外存调入内存,并且还需要进行一系列的完善工作
- 页面置换(或段置换)功能:当内存空间不够时,操作系统需要通过置换功能把暂时用不到的分页(或段)换出到外存中
总结
请求分页管理方式
请求分页存储管理与基本分页存储管理的主要区别:
- 在程序执行的过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序(即操作系统需要提供请求调页功能,将缺失的页面从外存调入内存)
- 若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存(即操作系统需要提供页面置换功能,将暂时用不到的页面换出到外存)
页表机制
与基本分页管理相比,请求分页管理中,为了实现"请求调页",操作系统需要知道每个页面是否已经调入内存;如果还没有调入,那么也需要知道该页面在外存中的存放位置
当内存空间不够时,要实现"页面置换",操作系统需要通过某些指标来决定到底换出哪个页面;有的页面没有被修改过,就不用再浪费时间写回到外存。有的页面修改过,就需要将外存中的旧数据覆盖。因此,操作系统也需要记录各个页面是否被修改过的信息
因此,相比于基本分页存储管理的页表,请求分页存储管理的页表结构会增加一些字段:
- 状态位:记录是否已经调入内存
- 访问字段:可以用来记录该页面最近被访问过几次,或记录上次访问的时间,以供给置换算法选择换出页面时参考
- 修改位:记录页面调入内存后是否被修改过
- 外存地址:记录页面在外存中的存放位置
缺页中断机构
为了实现请求调页功能,系统中需要引入缺页中断机构
在请求分页的系统中,每当要访问的页面不在内存时,便产生一个缺页中断,然后操作系统的缺页中断处理程序会处理该中断
之后,发生缺页的进程将会被阻塞,并放入到阻塞队列中,调页工作完成后再将其唤醒,放回到就绪队列
如果内存中有空闲块,则为进程分配一个空闲块,并将所缺页面装入该块,并修改页表中相应的页表项
如果内存中没有空闲块,则由页面置换算法选择内存中的一个页面淘汰,若被选中的淘汰页面在内存期间被修改过,则需要先将其写回外存,未修改过的页面不用写回外存
假设在一个请求分页的系统中,要访问逻辑地址 (0, 1024)
为了访问该逻辑地址,需要根据逻辑地址中的页号查询页表,缺页中断机构会根据查询到的页表项信息判断此时该页面是否已经处在内存中
如果页面没有在内存当中,即页表项的状态位值为0时,那么缺页中断机构便会产生一个缺页中断信号,之后操作系统的缺页中断处理程序就会负责处理该中断信号
由于中断处理的过程涉及I/O操作,即把页面从外存调入内存(通过页表项的外存地址信息就可以知道页面在外存中的位置),所以在I/O操作完成之前,之前发生缺页的进程会被阻塞,并放入到阻塞队列中,调页工作完成后再将其唤醒,放回到就绪队列
如果内存中有空闲块(如a号块),则为进程分配一个空闲块,并将所缺页面装入该块,并修改页表中相应的页表项
如果内存中没有空闲块,则由页面置换算法选择处在内存中的一个页面将其淘汰,若选中的页面在内存期间被修改过,则要将其写回外存,未修改过的页面不用写回外存
如根据算法规则选择淘汰2号页面,由于2号页面对应的页表项的修改位为1,即在内存期间被修改过,因此该页面中的内容需要从内存中写回到外存,把外存中保存的旧数据覆盖掉
上面的操作完成后,内存中就多出了一个空闲块,即原先2号页面所占用的c号块,该空闲块就可以提供给0号页面使用
相应的,换入换出工作完成后,还需要修改相应页面的页表项
缺页中断是因为当前执行的指令想要访问的目标页面未调入内存而产生的,也就是与当前执行的指令有关,因此属于内中断
缺页中断属于内中断中的故障类别,此类内中断是由错误条件引起的,可能被故障处理程序修复
一条指令在执行期间,可能会产生多次缺页中断,因为一条指令的执行可能需要访问多个内存单元
如一条指令是将A中的内容复制一份到B中,即将逻辑地址A中的数据复制到逻辑地址B,若A、B属于不同的页面,并且这两个页面都没有被调入内存,则在执行该指令时就会产生两次缺页中断
地址变换机构
请求分页存储管理与基本分页存储管理相比,在地址变换的过程中,增加了以下几个步骤:
- 请求调页:在查找到页面对应的页表项后,需要根据页表项中的状态位信息判断该页面是否已经处在内存中
- 页面置换:若此时想要访问的页面不在内存中且内存此时已经没有空闲块时,则需要进行页面置换工作,将某些内存块中的页面换出到外存中
- 当页面被调入或调出,以及被访问和被修改时都需要将页面对应的页表项中的字段进行修改
请求分页存储管理的地址变换
首先,检查页号的合法性,即将页号与页表寄存器中的页表长度信息进行比较,判断页号是否越界
- 注意:在请求分页存储方式的系统中,页表记录了进程的所有页面的相关信息,因此页表长度是指进程所有页面的数量之和,这些页面不仅包含已在内存中的页面,还包含未被调入内存的页面
若地址合法,则检查快表当中是否有页号对应的页表项,若快表命中,就可以直接得到最终的物理地址
- 注意:快表命中意味着该页面一定存在于内存当中
若快表没有命中,则需要根据页表寄存器中的页表起始地址找到内存中的页表,之后再通过页号和页表项大小找到页表中与页号对应的页表项
- 在请求分页存储管理中,每个页表项的大小依旧是相同的
找到对应的页表项后,需要检查此时页面是否已经在内存中,如果页面没有在内存中,缺页机构会产生一个缺页中断,之后由操作系统的缺页中断处理程序处理,其中就包括了调页和页面置换等工作
- 注意:当页面调入内存后,需要修改页面对应页表项中的某些字段的值
注意:快表中保存的页表项对应的页面一定是在内存中的。若某个页面被换出外存,则快表中的相应页表项也要被删除,否则就可能访问到错误的页面
地址变换流程图
注意:
- 只有在执行写指令时才需要修改"修改位"。并且,一般来说只需要修改快表中的数据,只有要将快表中的页表项删除时才需要将其重新写回到内存中的页表,这种做法可以减少访存次数
- 和普通的中断处理一样,缺页中断处理之前依然需要先保留CPU现场,然后让进程进入阻塞态,只有当进程可以重新回处理机运行时,才将其置为就绪态并恢复其CPU现场
- "选择一页换出"步骤需要使用某种"页面置换算法",根据算法决定将哪个页面换出到外存
- 换入/换出页面的工作都需要启动慢速的I/O操作,可见,如果换入/换出太过频繁,那么系统将会有很大部分的时间用于等待I/O操作完成,也就会有很大的开销,因此,换入/换出操作不应该太过频繁
- 页面调入内存后,需要修改页表中的页表项,(在引入快表机制的情况下)同时也需要将该修改后的页表项复制一份到快表中
总结
页面置换算法
在请求分页管理方式中,若想要访问的页面不在内存中且内存空间不够时,需要将内存中暂时用不到的信息换出到外存
页面置换算法就是决定将哪个处在内存中的页面换出到外存
页面的换入、换出都需要启动磁盘进行I/O操作,因此频繁的换入换出需要付出较大的代价,所以好的页面置换算法应该追求更少的缺页率
页面置换算法可以分为五种:
- 最佳置换算法(OPT)
- 先进先出置换算法(FIFO)
- 最近最久未使用置换算法(LRU)
- 时钟置换算法(CLOCK)
- 改进型的时钟置换算法
最佳置换算法(OPT,Optimal)
思想:每次选择淘汰以后永不使用的,或者在最长时间内不再被访问的页面,这样可以保证最低的缺页率
假设系统为进程分配了三个内存块,并考虑到有以下页面会被按顺序引用(会依次访问这些页面):
7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1
刚开始,内存中的内存块均为空,因此第一次的访问7号页面时会发生缺页,并可以把7号页面放入到内存块1中
第二次访问的是0号页,内存中没有保存0号页,因此也会发生缺页,此时可以把0号页面放入到空闲内存块 ------ 内存块2中
第三次访问的是1号页,内存中没有保存1号页,因此会发生缺页,此时可以把1号页面放入到空闲内存块 ------ 内存块3中
第三次访问的是2号页,内存中没有保存2号页,因此会发生缺页,但由于此时内存中已经没有空闲内存块了,因此需要使用页面置换算法选择淘汰某一个页面让它让出一个空闲块,根据最佳置换算法的规则,会选择最长时间不再被访问的页面 ------ 7号页面,因此可以让2号页面装入到原先保存着7号页面的内存块1中
- 手动做题时,可以从当前访问的页号的这一列开始向后查找,查找0、1、7号页面(即在执行页面置换前一刻的内存块中保存的页面)中哪个是最后出现的,最后出现的就是在最长时间内不会被访问的页面
第四次访问的是0号页面,由于0号页面已经在内存中,因此不会发生缺页,并可以直接访问到0号页面
第五次访问的是3号页面,由于3号页面不在内存中,因此会发生缺页,但由于此时内存中已经没有空闲内存块了,因此需要使用页面置换算法选择淘汰某一个页面让它让出一个空闲块,根据最佳置换算法的规则,会选择最长时间不再被访问的页面 ------ 1号页面,因此可以让3号页面装入到原先保存着1号页面的内存块3中
第六次访问的是0号页面,由于0号页面已经在内存中,因此不会发生缺页,并可以直接访问到0号页面
...
可以发现在整个访问的过程中,缺页中断刚发生了9次,页面置换发生了6次,也就是刚开始访问7、0、1这三个页面时发生中断,但此时内存中都还存在空闲内存块,因此不需要进行页面置换
缺页率 = 9 / 20 = 45%
注意:缺页时未必发生页面置换。若内存中还存在可用的空闲内存块时,就不需要进行页面置换
最佳置换算法可以保证最低的缺页率,但最佳置换算法执行的前提条件是系统必须知道程序会访问的页面是哪些,但实际上,只有在进程执行的过程中才能知道接下来会访问到的是哪个页面,也就是说操作系统无法提前预判页面访问的序列。所以,最佳置换算法是无法实现的
先进先出置换算法(FIFO)
思想:每次选择淘汰的页面是最早进入内存的页面
实现方法:把调入内存的页面根据调入的先后顺序排成一个队列,需要换出页面时选择队头页面即可。队列的最大长度取决于系统为进程分配了多少个内存块
假设系统为某进程分配了三个内存块,并考虑到有以下页面会按顺序被引用:
3, 2, 1, 0, 3, 2, 4, 3, 2, 1, 0, 4
首先访问3号页面,由于3号页面不在内存中,并且此时为进程分配的内存空间中存在空闲内存块,因此可以给3号页面分配一个空闲内存块 ------ 内存块1,让其装入进去。将3号页面装入到内存块1中后,还需要在队列中加入与3号页面相关的数据结构
3号页面加入到队列后,队列的情况为:
3
第二次访问2号页面,由于2号页面不在内存中,并且此时为进程分配的内存空间中存在空闲内存块,因此可以给3号页面分配一个空闲内存块 ------ 内存块2,让其装入进去。将2号页面装入到内存块2中后,还需要在队列中加入与2号页面相关的数据结构
3号页面加入到队列后,队列的情况为:
3
→2
第三次访问1号页面,由于1号页面不在内存中,并且此时为进程分配的内存空间中存在空闲内存块,因此可以给1号页面分配一个空闲内存块 ------ 内存块3,让其装入进去。将1号页面装入到内存块3中后,还需要在队列中加入与1号页面相关的数据结构
3号页面加入到队列后,队列的情况为:
3
→2
→1
第四次访问0号页面,由于0号页面不在内存中,并且此时为进程分配的内存空间中没有空闲内存块,因此必须选择淘汰一个页面,根据先进先出置换算法的规则,会选择(此时内存中的)最先被调入内存的页面将其淘汰。根据队列中的信息可以知道,队头位置的3号页面是最早被调入内存的,因此可以选择淘汰3号页面,并将保存3号页面的内存块1将其分配给0号页面,并将队头的3号页面删除,0号页面进入到队列队尾
- 注意:将队头页面淘汰后,队头就会发生相应变化
0号页面加入到队列后,队列的情况为:
2
→1
→0
第五次访问3号页面,由于3号页面不在内存中,并且此时为进程分配的内存空间中没有空闲内存块,因此必须选择淘汰一个页面,根据先进先出置换算法的规则,会选择(此时内存中的)最先被调入内存的页面将其淘汰。根据队列中的信息可以知道,队头位置的2号页面是最早被调入内存的,因此可以选择淘汰2号页面,并将保存2号页面的内存块2将其分配给3号页面,并将队头的2号页面删除,3号页面进入到队列队尾
3号页面加入到队列后,队列的情况为:
1
→0
→3
...
因此,在为进程分配三个内存块的情况下,访问这些页面一共会发生9次缺页
如果其它条件不变,为进程分配的是4个内存块,那么页面访问情况就会变为:
因此,在为进程分配四个内存块的情况下,访问这些页面一共会发生10次缺页
可以看到,为进程分配更多的内存块不一定能够减少缺页情况的发生次数,这就是所谓的Belady异常
Belady异常 ------ 当为进程分配的物理块数增加时,缺页次数不减反增的异常现象
注意:只有FIFO算法会产生Belady异常。另外,FIFO算法虽然实现简单,但是该算法与进程实际运行时访问页面的规律不适应,因为先进入内存的页面也有可能是最经常被访问的。因此,该算法性能差
最近最久未使用置换算法(LRU,Least Recently Used)
思想:每次淘汰的页面是最近最近未使用的页面
实现方法:在每个页面对应的页表项中,增加一个访问字段,该字段用于记录该页面自上次被访问以来所经历的时间t。当需要淘汰一个页面时,选择淘汰现有的页面(即在内存中的页面)中t值最大的,即最久未使用(最久未访问)的页面
假设系统为某进程分配了四个内存块,并考虑到有以下页面引用串:
1, 8, 1, 7, 8, 2, 7, 2, 1, 8, 3, 8, 2, 1, 3, 1, 7, 1, 3, 7
首先访问1号页,将其放入内存块1中
第二次访问8号页,将其放入到内存块2中
第三次访问1号页,内存中存在,直接访问
第四次访问7号页,将其放入到内存块3中
...
第十一次访问3号页,由于此时内存中没有空闲的内存块,因此根据页面置换算法的规则,需要选择最近最久未使用的页面 ------ 7号页面,将其淘汰,并将其原先所处的内存块3提供给3号页面使用
- 手动做题时,若需要淘汰页面,则在淘汰页面之前,可先查看此时内存的几个页面号,再从当前访问的页面的前一列开始向前检查这些页面号,在检查的过程中最后一个出现的页号就是要淘汰的页号
...
最近最久未使用算法的实现需要使用到专门的硬件,因此该算法虽然性能好,但是实现困难,开销大
在几种页面置换算法中,LRU算法的性能是最接近OPT算法的
时钟置换算法(CLOCK)
最佳置换算法虽然性能最好,但是无法实现;先进先出置换算法虽然实现简单,但算法性能差;最近最久未使用置换算法虽然性能好,但是实现起来需要专门的硬件支持,算法开销大
时钟置换算法是一种性能和开销较为均衡的算法,又称CLOCK算法,或最近未使用算法(NRU,Not Recently Used)
普通的时钟置换算法的实现方法:为每个页面的页表项设置一个访问位,并将内存中的页面都通过链接指针链接成一个循环队列。当某个页面被访问时,将其访问位置为1。当需要淘汰一个页面时,只需要检查页面对应页表项的访问位。如果是0,就选择该页将其换出;如果是1,则将其访问位置为0,但暂时不换出,然后继续检查下一个页表项。若一轮检查后发现所有页面的访问位均为1,则将这些页面的访问位依次置为0后,还需要进行第二轮的检查(在第二轮检查中,一定能找到访问位为0的页表项,因为第一轮检查后会把所有页表项的访问位都设置为0,因此普通的CLOCK算法在选择一个淘汰页面的过程最多需要经过两轮的页表检查)
注意:将处于外存的页面调入内存后,循环队列中相应的页面项的访问位也需要设置为1,因为将页面调入内存的目的就是为了访问它
假设系统为某进程分配了五个内存块,并考虑到有以下页面号引用串:
1, 3, 4, 2, 5, 6, 3, 4, 7
由于系统为进程分配了五个内存块,因此在访问前五个页面是都可以将对应的页面调入内存中不同的空闲内存块中
在将这五个不同的页面调入内存后,会将它们通过链接指针链接成一个循环队列,由于这五个页面此时都已经被访问过,因此五个页面对应的页表项中的访问位都设为1
注意:循环队列中各页面元素的顺序是按照页面被装入的时间进行排序的,而不是根据页号进行排序的
当第六次的访问6号页面时,就需要考虑淘汰某个页面。根据时钟置换算法的规则,选择一个最近没有被访问过的页面
具体的做法是:
刚开始,检查指针会指向循环队列的队首,因此最先检查循环队列队首的页表项它的访问位是否为0,若为0,则将其所在内存块让出,否则将队首页面的访问位置为0并通过链接指针继续向循环队列的下一个结点进行查找,循环查找直到找到访问位为0的页面为止
由于检查前循环队列中所有元素的访问位均为1,因此本次淘汰页面的过程需要进行两轮检查。第二轮检查循环队列的1号页面时发现其访问位为0,所以将其作为要淘汰的页面
接下来,6号页面就会被装入到1号页面所在的内存块中,并且需要修改循环队列的结构,还需要把检查指针指向下一个页面元素
当第七次和第八次的访问3和4号页面后,需要修改相应页表项的访问位为1
- 注意:只有检查时检查指针才需要移动,修改页面的访问位是不需要移动检查指针的
当第八次的访问7号页面时,由于内存中没有空闲内存块,因此需要使用置换算法淘汰某个页面
根据时钟置换算法的规则,需要从循环队列中找出一个最近没访问过的页面
具体做法为:
- 从检查指针指向的元素开始查找,发现其最近被方法过,于是将其访问位设置为0然后修改检查指针让其指向下一个元素
- ...
- 检查到2号页面时,发现其最近没被访问过,因此选择将其淘汰,并将7号页面装入到原2号页面所在的内存块中,同时修改循环队列中相应的元素(即将2号页面[0]修改为7号页面[1]),并将检查指针指向下一个元素(即5号页面[0])
改进型的时钟置换算法
简单的时钟置换算法仅考虑到一个页面是否被访问过。但如果被淘汰的页面没有被修改过,就不需要使用I/O操作将页面写回外存。只有被淘汰的页面被修改过时,才需要写回外存
因此,除了考虑一个页面最近是否被访问过之外,操作系统还应该考虑页面有没有被修改过。在其他条件都相同时,应该优先淘汰没有修改过的页面,以减少I/O操作的发生
为了实现上述操作,还需要给每个页面对应的页表项增加一个修改位字段,表示页面是否被修改过
- 修改位为1时,表示页面被修改过
和简单的时钟置换算法一样,改进型的时钟置换算法也需要建立一个循环队列
假设用" (访问位, 修改位) "的形式表示各页面状态。如 (1, 1) 表示一个页面近期被访问过,且被修改过
算法规则:
- 将所有可能被置换的页面(在内存中的页面)排成一个循环队列
- 第一轮:从当前位置开始扫描到第一个 (0, 0) 的帧用于替换。本轮扫描不会修改任何标志位
- 第二轮:若第一轮扫描失败,则进行第二轮扫描,查找第一个 (0, 1) 的页面用于替换。本轮会将所有扫描过的页面的访问位设置为0
- 第三轮:若第二轮扫描失败,则进行第三轮扫描,查找第一个 (0, 0) 的页面用于替换。本轮扫描不会修改任何标志位
- 第四轮:若第三轮扫描失败,则进行第四轮扫描,查找第一个 (0, 1) 的页面用于替换
由于第二轮已经将所有页面的访问位设置为0,因此经过第三轮、第四轮扫描后一定会有一个页面被选中,因此改进型CLOCK置换算法选择一个淘汰页面最多需要进行四轮扫描
假设系统为进程分配了五个内存块,当各个内存块都被占满后,处在内存中的各个页面会通过链接指针形成一个循环队列
假设此时循环队列的结构为:
如果要淘汰一个页面,则需要从循环队列的队头位置开始查找,直到找到第一个 (0, 0) 的页面
在第一轮扫描中,能够找到要淘汰的目标页面,因此不需要进行后续几轮的扫描
假设此时循环队列的结构为:
如果要淘汰一个页面,则需要从循环队列的队头位置开始查找,尝试找到一个 (0, 0) 的页面
在第一轮扫描中,找不到要淘汰的目标页面,因此接下来需要进行的第二轮扫描
- 注意:当检查指针再次指向队头位置时(此时仅仅只是指向,还未对指向的页面进行检查),则说明上一轮检查已经结束,接下来需要进行新一轮的检查,新一轮的检查依旧是从队头开始,直到检查指针再次指向队头位置
在第二轮扫描中,会尝试找到一个 (0, 1) 的页面,在该例中能够找到要淘汰的目标页面,因此不需要进行后续几轮的扫描
- 在第二轮扫描的过程中,如果发现当前检查的页面访问位为1的,则需要将其访问位修改为0,再将检查指针指向下一个页面,之后就是检查该下一个页面
假设此时循环队列的结构为:
第一轮扫描后,未能找到满足 (0, 0) 条件的页面,因此进行第二轮扫描
第一轮扫描结束后,第二轮扫描开始前,循环队列的状态为:
第二轮扫描后,未能找到满足 (0, 1) 条件的页面,因此进行第三轮扫描
第二轮扫描结束后,第三轮扫描开始前,循环队列的状态为:
第三轮扫描中,找到了满足 (0, 0) 条件的页面,将其淘汰
假设此时循环队列的结构为:
第一轮扫描后,未能找到满足 (0, 0) 条件的页面,因此进行第二轮扫描
第一轮扫描结束后,第二轮扫描开始前,循环队列的状态为:
第二轮扫描后,未能找到满足 (0, 1) 条件的页面,因此进行第三轮扫描
第二轮扫描结束后,第三轮扫描开始前,循环队列的状态为:
第三轮扫描后,未能找到满足 (0, 0) 条件的页面,因此进行第四轮扫描
第三轮扫描结束后,第四轮扫描开始前,循环队列的状态为:
第四轮扫描中,找到了满足 (0, 1) 条件的页面,将其淘汰
本质上,改进型时钟置换算法的置换规则还是按照(0, 0) > (0, 1) > (1, 0) > (1, 1)
的优先级顺序进行选择的淘汰优先级:
- 第一优先级:最近没访问过且没修改过的页面
- 第二优先级:最近没访问过但修改过的页面
- 第三优先级:最近访问过但没修改过的页面
- 第四优先级:最近访问过且修改过的页面
总结
算法规则 | 优缺点 | |
---|---|---|
OPT | 优先淘汰最长时间内不会被访问的页面 | 缺页率最小,性能最好,但无法实现 |
FIFO | 优先淘汰最先进入内存的页面 | 实现简单,但性能很差,可能出现Belady异常 |
LRU | 优先淘汰最近最久没访问的页面 | 性能很好,但需要硬件支持,算法开销大 |
CLOCK(NRU) | 循环扫描各页面 第一轮淘汰访问位为0的,并将扫描过的页面的访问位改为1。若第一轮没选中,则进行第二轮扫描 | 实现简单,算法开销小,但未考虑页面是否被修改过 |
改进型CLOCK | 第一轮:淘汰 (0, 0) 第二轮:淘汰 (0, 1) 第三轮:淘汰 (0, 0) 第四轮:淘汰 (0, 1) | 算法开销小,性能也不错 |
页面分配策略、抖动、工作集
驻留集:指请求分页存储管理中给进程分配的物理块的集合
进程运行时系统给进程分配的内存块数量为n就表示该进程的初始驻留集为n
在采用了虚拟存储技术的系统中,驻留集的大小一般小于进程的总大小
若某进程共有100个页面,并且该进程的驻留集大小为100时进程的所有页面可以全部放入内存,此时运行期间将不可能发生缺页。若驻留集大小为1,则进程在运行期间必定会极为频繁地出现缺页现象
若驻留集太小,会导致缺页频繁,系统要花费大量的时间来处理缺页,实际用于进程推进的时间很少;若驻留集太大,又会导致多道程序的并发度下降,导致资源利用率低。所以,应该选择一个合适的驻留集大小
根据驻留集的大小在进程的整个运行期间是否可变:
- 固定分配:操作系统为进程分配一组固定数目的物理块,在进程的运行期间不再改变。即,驻留集大小不变
- 可变分配:先为每个进程分配一定数目的物理块,在进程的运行期间,可根据情况适当增加或减少进程所拥有的物理块数目,即,驻留集大小可变
当发生页面置换时,选取淘汰页面时的选择范围也是可以调整的:
- 局部置换:发生缺页时只能选择进程自己拥有的物理块进行置换
- 全局置换:可以将操作系统中保留的空闲物理块分配给缺页进程,也可以将别的进程所持有的物理块中的页面置换到外存,再将其物理块分配给缺页进程(因此在这种情况下,被置换的进程所拥有的物理块会减少,缺页进程所拥有的物理块会增加)
将两种物理块分配策略与置换方式进行组合,可以得到三种页面分配置换方式:
- 固定分配局部置换
- 可变分配局部置换
- 可变分配全局置换
不存在固定分配全局置换,因为全局置换必然会导致进程的驻留集变化,这和固定分配策略相矛盾
页面分配、置换策略
固定分配局部置换
系统为进程分配一定数量的物理块,在进程的整个运行期间都再不改变。若进程在运行期间发生缺页,则只能从该进程在内存中的页面中选出一页进行换出,然后再调入需要的页面
缺点:很难在刚开始就确定应该为进程分配多少个物理块才算合理
采用这种策略的系统可以根据进程的大小、优先级、或是根据程序员给出的参数来确定为一个进程分配的内存块数,但即使这样还是不太合理
该策略的灵活性差
可变分配全局置换
刚开始时会为进程分配一定数量的物理块,同时操作系统会保持一个空闲物理块队列。当某进程发生缺页时,就从空闲物理块队列中取出一个空闲物理块并分配给该进程;若已经没有空闲物理块,则可以选择一个未锁定的页面将其换出到外存,再将该物理块分配给缺页的进程
系统会锁定一些页面,这些页面中的内容不能被置换到外存,如:重要的内核数据可以将其是设置为"锁定"
允许被置换到外存的页面就是未锁定的页面
采用这种策略时,只要某进程发生缺页,都将获得新的物理块,仅当空闲物理块用完时,系统才选择一个未锁定的页面调出。被选择调出的页面可能是系统中任何一个进程中的页,因此这个被选中的进程拥有的物理块会减少,缺页率会增加
可变分配局部置换
刚开始会为每个进程分配一定数量的物理块。当某进程发生缺页(无物理块可用)时,只允许从该进程自己的物理块中选出一个,并将其内部的页面换出到外存。如果进程在运行中频繁地缺页,系统会为该进程多分配几个物理块,直至该进程缺页率趋向于适当的程度;反之,如果进程在运行中缺页率特别低,则可以适当减少分配给该进程的物理块
可变分配全局置换:只要缺页就给进程分配新的物理块
可变分配局部置换:系统会根据进程运行期间发生的缺页率来动态地增加或减少进程的物理块
何时调入页面
预调页策略
根据局部性原理(特别是空间局部性),一次调入若干个相邻的页面可能比一次调入一个页面更高效(调页的次数越少,请求I/O操作的次数也就越少,系统开销也就越小)。但如果提前调入的页面中大多数都没被访问过,则该策略又是很低效的
空间局部性:当前在访问页面时,与其相邻的几个页面也有可能会被接着访问到
这种策略主要应用于进程首次调入时,由程序员指出应该先调入哪些部分,因此该策略是在进程运行前进行调页的
请求调页策略
进程在运行期间发现缺页时才将所缺页面调入内存
由于这种策略调入的页面一定会被访问到,但由于每次只能调入一页,而每次调页都需要启动磁盘并进行I/O操作,因此开销较大
该策略是在进程运行期间进行调页的
在实际应用中,两种策略会同时被使用,进程开始运行前,使用预调页策略,运行期间采用请求调页策略
从何处调入页面
磁盘当中的存储区域分为对换区和文件区
对换区采用的是连续分配方式,文件区采用离散分配方式
对换区的读/写速度更快,文件区的读/写速度更慢
一般来说,文件区的大小要比对换区更大
程序在没有运行时,相关的数据都保存在文件区当中
由于对换区的读写速度更快,因此如果磁盘拥有足够的对换区空间时,页面的调入、调出都是在内存与对换区之间进行的,这样可以保证页面的调入、调出速度更快
在进程运行前,需要将进程相关的数据从文件区复制到对换区
如果磁盘的对换区空间比较少,那么凡是不会被修改的数据都会直接文件区调入内存,由于这些页面不会被修改,因此换出时不必写回磁盘,下次需要时再从文件区调入即可。对于可能被修改的部分,换出时需要写回磁盘的对换区,下次需要时再从对换区调入
UNIX系统使用的方式:运行之前与进程有关的数据会全部放在文件区,故未使用过的页面,都可从文件区调入内存。若被使用过的页面需要换出,则写回到对换区,下次需要时直接从对换区调入即可
抖动(颠簸)现象
指刚刚换出的页面很快又要被换入内存,刚刚换入的页面很快又被换出外存,这种频繁的页面调度行为称为抖动,或颠簸。产生抖动的主要原因是进程频繁访问的页面数目高于进程的可用物理块数(即分配给进程的物理块不够)
注意:抖动只和系统给进程分配的物理块数和物理块大小有关,和虚拟内存的外存部分的最大容量无关
如果发生了抖动现象,那么系统会耗费大量的时间用于处理页面的调入和调出,而实际进程运行的时间就占比很少
为了避免抖动现象发生,就需要为进程分配足够的物理块。但为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率
工作集可以用于确定应该为进程分配几个物理块更为合适
工作集:指在某段时间间隔内,进程实际访问页面的集合
操作系统会根据"窗口尺寸"来算出工作集
某进程的页面访问序列如下,窗口尺寸为4,各时刻的工作集为?
24, 15, 18, 23, 24, 17, 18, 24, 18, 17, 17, 15
在访问完23号页面时,根据窗口尺寸,因此此时的工作集为
24, 15, 18, 23
在访问完17号页面时,根据窗口尺寸,因此此时的工作集为
18, 24, 17
工作集的大小可能会小于窗口尺寸,实际应用中,操作系统可以统计进程的工作集大小,根据工作集大小给进程分配若干个内存块。如,窗口尺寸为5,经过一段时间的检测发现进程的工作集最大为3,那么说明该进程具备很好的局部性,可以给该进程分配3个或以上的内存块即可满足进程的运行需要
工作集越小,进程就具有更好的局部性
一般来说,驻留集的大小不能小于工作集的大小,否则进程运行过程中将频繁缺页
基于局部性原理,进程此时访问的页面与在不久之后会访问的页面可能是相关的,因此,可以根据进程近期访问的页面集合(工作集)来设计一种页面置换算法 ------ 选择一个不在工作集中的页面进行淘汰
总结
内存映射文件
内存映射文件 ------ 操作系统向上层程序员提供的功能(系统调用)
通过该功能,程序员可以很方便地访问文件数据、也能很方便地实现多个进程共享同一文件
传统的文件访问方式
磁盘的存储空间是以块为单位进行划分的
一个文件要存放到磁盘中,那么这个文件也需要被拆分为若干个与磁盘块大小相等的块(假设文件被划分为3个块)
文件的每个块会被离散地保存在磁盘的各个块中
每个进程都有独属于自己的逻辑地址空间(虚拟地址空间),当一个进程要访问文件中的数据时,首先需要使用open系统调用来打开文件,然后使用seek系统调用将文件的读写指针移动到文件的某个位置,接下来,进程可以使用read系统调用来指明从读写指针所指位置开始向后读入多少个数据
若接下来要读入的数据刚好存放在文件的2号块中,那么操作系统会把2号块的内容从磁盘读入到内存
2号块读入到内存后,进程就可以对其进行访问了
假设进程在访问该块的期间对其进行了修改,那么如果要将修改过后的块写回磁盘,则需要使用write系统调用
open系统调用 ------ 打开文件
seek系统调用 ------ 将读写指针移动到文件的某个位置
read系统调用 ------ 从读写指针所指位置读入若干数据(从磁盘读入内存)
write系统调用 ------ 将内存中的指定数据写回磁盘(根据读写指针确定要写回到什么位置)
内存映射文件(Memory-Mapped Files)
方便程序员访问文件
如果操作系统支持内存映射文件功能,那么进程访问文件的方式:
首先,使用open系统调用来打开文件
然后,使用mmap系统调用来将文件(中的每一块)映射到进程的虚拟地址空间中,该系统调用会返回一个指针,指针指向映射的空间的起始位置
图中之所以把虚拟地址空间中文件的各个块画成灰色,是因为这仅仅只是把文件的内容映射进了内存之中,并非是真正的从外存复制一份并保存到内存
当进程要根据虚拟地址访问映射块时,系统还是会产生一个缺页中断,因此文件映射完后仍相当于是一种缺页的状态
接下来,进程就可以像正常访问访问内存中的物理块那样对文件的映射块进行访问
通过起始地址可以附加上偏移量信息就可以访问到文件的各个映射块了
假设此时要访问2号块的内容,操作系统根据此时内存块的情况得知2号块还没有调入内存,因此会产生一个缺页异常
之后,操作系统会自动地将2号块读入到内存(而不需要使用read系统调用)
进程对文件的其它块的访问也如此
假设进程在访问该块的期间对其进行了修改,那么在进程关闭文件时(需要使用close系统调用关闭文件),操作系统会自动将文件被修改的数据所在的块写回磁盘(未修改的部分所在的块不用写回)(而不需要使用write系统调用)
方便实现文件共享
同一个文件,可以同时被多个进程通过mmap系统调将其映射到自己的虚拟地址空间中
各个进程的逻辑地址空间是相互独立的,但操作系统会把这些进程的有关文件的逻辑地址空间映射到相同的物理内存上,在这些物理内存中保存着真正的文件数据
操作系统只需要修改各个进程的页表中对应的页表项的内容即可
在这种情况下,一个进程修改了某个块的数据,另一个进程就可以立马"看到"