深入理解虚拟内存:请求分页、缺页中断与页面置换算法(LRU/Clock)
一、虚拟内存与请求分页
虚拟内存是现代操作系统的重要抽象,它让每个进程拥有一个连续、独立的逻辑地址空间,即使物理内存总量不足。其核心实现依赖于 分页 机制:逻辑地址空间被划分为固定大小的页面,物理内存被划分为同样大小的页框。进程运行时,只有当前需要的部分页面被加载到物理内存中,其余页面存放在磁盘交换区。
请求分页(Demand Paging)在进程启动时不加载所有页面,而是等到访问某个页面而该页面不在内存时,才由操作系统动态加载。这种方式极大地提高了内存利用率,并支持运行比物理内存大得多的程序。
二、缺页中断处理
当 CPU 访问的逻辑页面对应的页表项中“存在位”为 0(即页面不在内存)时,硬件会触发 缺页中断(page fault)。操作系统缺页中断处理函数执行以下步骤:
- 暂停当前进程,保存上下文。
- 查找磁盘:根据逻辑地址找到页面在外存(交换区或可执行文件)的位置。
- 分配页框:如果物理内存中有空闲页框,直接分配;否则,触发页面置换算法,选择一页换出(若为脏页则写回磁盘)。
- 加载页面:从磁盘读入选定的页框,更新页表项(存在位置 1,修改位等)。
- 恢复进程:重新执行导致缺页的指令。
这个处理过程对用户进程是透明的,但频繁的缺页会严重影响性能,因此页面置换算法的选择至关重要。
三、页面置换算法
目标:选择未来最长时间不会被访问的页面换出,以最小化缺页次数。下面介绍两种经典算法。
3.1 LRU(Least Recently Used)算法
原理:置换最近最少被使用的页面。它基于局部性原理:最近未使用的页面将来很可能也不会使用。LRU 需要记录每个页面最后一次被访问的时间,实现成本较高(可通过硬件计数器或栈实现)。
模拟实现:我们可以用一个列表(或队列)来维护页面访问顺序,每次访问页面时将其移到列表末尾,置换时取出列表头部(最久未使用)。
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.frames = [] # 保存页号,最近访问的在末尾
def access(self, page):
if page in self.frames:
# 页面已存在,移到末尾(最近使用)
self.frames.remove(page)
self.frames.append(page)
return False # 不产生缺页
else:
# 页面不在内存中
if len(self.frames) >= self.capacity:
self.frames.pop(0) # 淘汰最久未使用
self.frames.append(page)
return True # 产生缺页
# 测试
lru = LRUCache(3)
page_sequence = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2]
page_faults = sum(lru.access(p) for p in page_sequence)
print(f"LRU缺页次数: {page_faults}")
3.2 Clock(二次机会 / 循环扫描)算法
原理:每个页框关联一个访问位。当页面被访问时,硬件将访问位置 1。置换时,时钟指针循环扫描页框,若当前页面的访问位为 0,则选择该页面换出;若为 1,则置 0 并继续扫描下一个。这相当于给使用过的页面第二次机会。
模拟实现:维护一个环形列表(页框号或页面号),以及对应的访问位(布尔列表)。指针每步移动,检查访问位。
class ClockCache:
def __init__(self, capacity):
self.capacity = capacity
self.frames = [-1] * capacity # 页框内容,-1表示空
self.reference_bits = [0] * capacity
self.hand = 0 # 时钟指针
def access(self, page):
# 检查页面是否已在内存
if page in self.frames:
idx = self.frames.index(page)
self.reference_bits[idx] = 1 # 设置访问位
return False # 不缺页
else:
# 缺页,需要替换
while True:
if self.reference_bits[self.hand] == 0:
# 选择该页框
self.frames[self.hand] = page
self.reference_bits[self.hand] = 1 # 新加载页面,访问位置1(或初始化为0,根据设计)
self.hand = (self.hand + 1) % self.capacity
return True
else:
# 有机会,清除访问位并继续
self.reference_bits[self.hand] = 0
self.hand = (self.hand + 1) % self.capacity
# 测试
clock = ClockCache(3)
page_sequence = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2]
page_faults = sum(clock.access(p) for p in page_sequence)
print(f"Clock缺页次数: {page_faults}")
四、总结
虚拟内存通过请求分页和缺页中断机制,使得进程可以透明地使用比物理内存更大的地址空间。页面置换算法直接影响系统性能:LRU 理论上最优但实现代价高,Clock 算法通过近似 LRU 且硬件支持简单(一个访问位)而被实际系统广泛采用(例如 Linux 的 CLOCK 或改进的 NRU)。理解这些算法的原理与实现,有助于我们优化内存密集型应用的性能。

浙公网安备 33010602011771号