文章目录
C9 超越物理内存:机制
到目前为止,一直假设所有的页都常驻在物理内存中,但是为了支持更大的地址空间操作系统需要把当前没有用的那部分地址空间找个地方存储起来,一般来说,这个地方有一个特点,即拥有比内存更大的容量,因此也更慢一些,磁盘通常能满足这个需求
增加交换空间让操作系统为多个并发运行的进程都提供巨大地址空间的假象,有了巨大的地址空间,就不必担心程序的数据结构是否有足够空间存储,只需自然地编写程序,根据需要分配内存
9.1 交换空间
我们要做的第一件事,就是在硬盘上开辟一部分空间用于物理页的移入和移出,在操作系统中,这样的空间称为交换空间,假设操作系统能以页大小为单元读取或写入交换空间,为此操作系统需要记住给定页的硬盘地址
交换空间的大小是非常重要的,它决定了系统在某一时刻能使用的最大内存页数
示例,物理内存中存在3个进程0,1,2,但这三个进程都只有一部分有效页在内存中,剩下的在硬盘的交换空间中,进程3的所有页都存放在交换空间内,由此得知它没有运行,使用交换空间让系统假装拥有了更大的物理内存
9.2 存在位
现在硬盘上有了交换空间,因此需要在系统中添加一些更高级的机制,来支持从硬盘交换页,假设有一个硬件管理TLB的系统
先回顾内存访问,硬件将虚拟地址转换为物理地址,再从内存中获取所需数据,硬件首先从虚拟地址获得虚拟页号(VPN),检查TLB是否匹配,如果TLB命中,则获得最终的物理地址并访问内存,如果TLB未命中,则硬件在内存中查找页表,并将虚拟页号作为索引遍历页表项(PTE),如果页有效且存在于物理内存中,则硬件从页表项中获得物理页帧号(PFN),并更新TLB,重试该指令
但是如果希望允许交换到硬盘,就必须添加更多的机制,即TLB未命中时,硬件在页表中查找时,可能发现页不在物理内存中,硬件(软件管理TLB)判断页是否在内存中的方法是通过页表项中的存在位判断的,如果存在位设置为1,则表示该页存在于物理内存,如果存在位设置为0,则页在硬盘上,访问不在物理内存中的页,这种行为称为页错误
在页错误时,操作系统被唤醒处理页错误,一段称为“页错误处理程序”的代码将会执行,来处理页错误
9.3 页错误
在TLB未命中的情况下,有两种类型的系统,即硬件管理的TLB和软件管理的TLB,不论在哪种系统中,如果页不存在,都由操作系统负责处理页错误
如果一个页不存在,已经被交换到了硬盘,在处理页错误时,操作系统需要将该页交换到内存中,操作系统可以用PTE中的某些位来存储硬盘地址,当操作系统接收到页错误时,它会在PTE中查找该页在磁盘中的地址,并将请求发送到硬盘,将页读取到内存中
当硬盘I/O完成时,操作系统会更新页表,将该页标记为存在,更新页表项的PFN字段以记录该页的物理内存位置,并重试指令。下一次重新访问TLB还是未命中,而这次因为页在内存中,因此会将该页的PFN更新到TLB中,再重试,TLB命中,从TLB中找到地址映射,访问物理地址
注意:当操作系统将页从磁盘加载到内存中时(I/O),进程将处于阻塞态,因此当页错误发生时,操作系统可以自由地运行其它可执行的进程
9.4 内存满了怎么办
上述考虑问题的过程,都默认假设有足够大的空闲内存空间来存储交换空间换入的页,然而实际情况可能并非如此
内存可能已经存满(或接近存满),因此操作系统可能希望先交换出一个或多个页,以便为操作系统即将交换入的页留够空间,选择哪些页被交换出磁盘,这被称为页交换策略
9.5 页错误处理流程
当发生TLB未命中时有3种重要情景:
1,该页存在且有效,这种情况下,TLB未命中处理程序或硬件简单地从PTE种获取PFN,然后重试该指令
2,页错误处理程序需要运行,虽然这是一个进程可以访问的合法页,但它并不存在于内存中
3,访问一个无效页或非法页,这种情况下,硬件捕获到这个非法访问,操作系统陷阱处理程序运行,可能会终止非法进程
对于情景2,即会发生页错误的情况,操作系统首先要为将换入的页找到一个物理页帧,如果没有这样的物理页帧,就得等待交换算法运行,从内存中踢出一些页,释放空间以供换入页使用,在获得物理页帧后,处理程序发出I/O将目标页换入到内存中,最后操作系统更新页表并重试访问指令,第一次TLB未命中,将该页PFN更新到TLB,第二次TLB命中,找到了物理地址
9.6 交换何时真正发生
为了保证一直有少量的物理内存,大多数操作系会设置
高水位线(High Watermark) 和 低水位线(Low Watermark)
来帮助决定何时从内存中踢出页
原理是:操作系统发现有少于LW个页可用时,后台负责释放内存的线程开始运行,直到可用页到达HW个,这个后台线程也称为页守护进程
9.7 小结
要做到访问物理内存外的数据(页),需要在页表中添加一些额外信息,比如增加一个存在位,告诉我们该页是不是存在于内存中,如果不存在,需要在PTE中留下该页的磁盘地址,操作系统会运行页错误处理程序,将所需的页从硬盘读取到内存中,可能会先换出一些页,为即将换入的页腾出一些空间
这些行为对进程都是透明的,即进程认为自己访问的是私有的,连续的内存,但实际上有些物理页甚至不在内存中,最坏的情况下,从硬盘中换入一页可能都需要数毫秒才能完成
C10 超越物理内存:策略
当内存不够时,由于内存压力迫使操作系统要换出一些页,为常用的页腾出空间,确定要踢出哪些页封装在操作系统的替换策略中
10.1 平均内存访问时间 AMAT
在研究替换策略之前,先看看将要实现的目标:
将内存看作缓存,让缓存未命中最少,即从磁盘获取页的次数最少,或者可以说让缓存命中最多,即从内存中找到待访问页的次数最多
看一个计算程序**平均内存访问时间(AMAT)**的公式:
AMAT = (Phit * Tm) + (Pmiss * Td)
Phit:命中概率
Pmiss:未命中概率
Tm:访问内存的成本
Td:访问磁盘的成本
在现代系统中,磁盘访问的成本非常高,即使很小概率的未命中也会拉低运行程序总体的AMAT,必须尽可能地避免或减少缓存未命中,避免程序以磁盘的速度运行
10.2 最优替换策略
最优替换策略:替换内存中在最远将来才会被访问到的页,可以达到缓存未命中率最低
最优替换策略的想法:如果不得不踢出一些页,为什么不踢出在最远将来才会访问的页呢?即缓存中所有其它的页都比这个页重要
追踪一个简单的例子,将内存比作缓存,且缓存只能容纳3个页,现在一个程序按如下顺序访问虚拟页
0,1,2,0,1,3,0,3,1,2,1
前三次未命中是正常的,因为缓存一开始为空,这种未命中也称为冷启动未命中,当需要访问虚拟页3时,缓存已经满了,需要替换,最优替换策略检查当前缓存中的页在未来的使用情况,页0马上要被访问,页1稍后被访问,因此此时踢出页2,换入页3
但遗憾的是,未来的访问是无法知道的,无法为通用的操作系统实现最优替换策略,因此最优替换策略只能作为比较
10.3 简单策略 FIFO,随机
(1) FIFO 先入先出
一些早期系统采用FIFO 先入先出替换策略,页在进入内存时,简单地被放入一个队列,当要发生替换时,先踢出先入的(队尾的)页,它最大的优势就在于实现简单
先入先出根本无法确定页的重要性,即使页0已经被多次访问,FIFO仍会将其踢出,就因为它是第一个进入内存的
(2) 随机替换
另一种类似的简单策略是随机替换,在内存满的时候随机选择一个页进行替换,随机替换实现起来也很简单,但是它在挑选替换哪个页时不够智能
随机策略完全取决于运气
遗憾的是像FIFO或随机这样的简单策略都可能有一个共同的问题:它可能会踢出一个重要的页,而这个页马上将要被访问,如先入先出策略将先进的页踢出,如果这页恰好是一个包含重要代码或数据结构的页,它还是会被踢出,尽管它很快会被重新载入,因此FIFO,Random和类似的策略不太可能达到最优,需要更智能的策略
10.4 利用历史数据 LRU(Least-Recently-Used)
(1) LRU的原理
正如在进程调度策略可以使用的一个历史信息是频率,如果一个页被访问了多次,也许它不应该被替换,因为它显然更有价值,页更常用的属性是访问的周期性,越近被访问过的页,也许再次访问的可能性就越大
由LRU策略来替换最少最近使用的页,通过示例可以看到,当第一次需要页替换时,LRU会踢出页2,因为0和1的访问时间更近,第二次它踢出页0,因为1和3最近被访问过,这两种情况下,基于历史的LRU所做的决定更为准确
(2) LRU的实现
像LRU这样的算法通常优于简单的策略(FIFO,随机),它们可能会踢出重要的页,但该如何实现LRU?
在每次页访问时,都必须更新一些数据,记录哪些页是最近最少被使用的,系统必须每次为内存引用的页做一些记录工作
有一种方法可以实现,即增加一点硬件支持,硬件可以在每个页访问时更新该页的时间字段,当该页被访问时,时间字段被设置为当前时间,然后在需要替换页时,操作系统简单地扫描系统中所有页的时间字段以找到最近最少使用的页
但是随着系统中的页不断增多,扫描所有页只为找到一个最近最少使用的页,这个代价过于高昂,是否可以不需要找到最旧的页,差不多旧的可以吗?
10.5 近似LRU – 时钟算法
完美的实现LRU的开销过于庞大,为了避免这高昂的代价,退而求其次,找到差不多的就能用,虽然性能不如完美的LRU,但是也差不了多少且避免了扫描所有页这种行为
近似LRU这个策略的实现需要每页增加一个引用位,每当页被引用时,硬件将该页引用位设置为1,但是将引用位置0的操作由操作系统完成
时钟算法:系统中的所有页放在一个循环列表中,时钟指针开始指向某个页,当进行页替换时,操作系统检查当前时钟指针指向的页的引用位是1还是0,如果是1,操作系统将其引用位置0,时钟指针指向下一个页,直到找到一个引用位为0的页,将其换出
引用位为1意味着该页最近被使用过,引用位为0则意味着该页最近没被使用过
时钟算法不用扫描内存中的所有页,虽然时钟算法性能不如LRU,但是它避免了高昂的遍历,且它的性能比大多数简单策略要好
10.6 考虑脏页
某些页可能在内存中会被修改,如果该页被修改过(变脏),那么踢出它时要将它写入到磁盘中,这很昂贵,如果没有修改直接踢出即可,因此一些系统更倾向于踢出干净页,而不是脏页,以避免额外的I/O
为了支持识别该页是否被修改,该页应该增加一个脏位,每次该页被写入时都会设置脏位,用以区分它和没有被修改的页,时钟算法也可以扫描既少使用又干净的页先踢出,无法找到这种页时,再查找脏的少使用的页踢出
10.7 抖动
当内存被超额请求时,某进程的需求超出了当前可用的物理内存,系统将不断地进行换页,将不必要的页换到磁盘中,这种情况称为抖动
现代的一些系统采用更严格的方法处理过载内存,如当内存超额请求时,某些版本的Linux会运行“内存不足的杀手程序”,这个进程会选择一个内存密集型进程并终止它,从而扩大可用内存,虽然减轻了内存压力,但是如果终止了一个服务器进程,就会导致所有需要显示的应用程序不可用
10.8 小结
虽然有着各种替换页的策略,简单的策略如FIFO,随机策略性能低,且可能会踢出重要的,频繁使用的页,所以基于历史信息的策略是最好的选择,LRU由于要扫描所有的页以找到最近最少使用的页,它的代价过于高昂,所以采用时钟算法找出差不多旧的页进行替换是更好的算法
但是由于内存访问和磁盘访问时间的速度之间的差异增加,这些算法的重要性也降低了,由于内存到磁盘之间I/O非常昂贵,因此频繁在内存和磁盘间分页的成本太高,所以最好的解决方法是:购买更多的内存