深入理解虚拟内存:请求分页、缺页中断与页面置换算法(LRU/Clock)

一、虚拟内存与请求分页

虚拟内存是现代操作系统的重要抽象,它让每个进程拥有一个连续、独立的逻辑地址空间,即使物理内存总量不足。其核心实现依赖于 分页 机制:逻辑地址空间被划分为固定大小的页面,物理内存被划分为同样大小的页框。进程运行时,只有当前需要的部分页面被加载到物理内存中,其余页面存放在磁盘交换区。

请求分页(Demand Paging)在进程启动时不加载所有页面,而是等到访问某个页面而该页面不在内存时,才由操作系统动态加载。这种方式极大地提高了内存利用率,并支持运行比物理内存大得多的程序。

二、缺页中断处理

当 CPU 访问的逻辑页面对应的页表项中“存在位”为 0(即页面不在内存)时,硬件会触发 缺页中断(page fault)。操作系统缺页中断处理函数执行以下步骤:

  1. 暂停当前进程,保存上下文。
  2. 查找磁盘:根据逻辑地址找到页面在外存(交换区或可执行文件)的位置。
  3. 分配页框:如果物理内存中有空闲页框,直接分配;否则,触发页面置换算法,选择一页换出(若为脏页则写回磁盘)。
  4. 加载页面:从磁盘读入选定的页框,更新页表项(存在位置 1,修改位等)。
  5. 恢复进程:重新执行导致缺页的指令。

这个处理过程对用户进程是透明的,但频繁的缺页会严重影响性能,因此页面置换算法的选择至关重要。

三、页面置换算法

目标:选择未来最长时间不会被访问的页面换出,以最小化缺页次数。下面介绍两种经典算法。

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)。理解这些算法的原理与实现,有助于我们优化内存密集型应用的性能。

posted @ 2026-06-23 11:40  XYu1230  阅读(26)  评论(0)    收藏  举报