Linux 虚拟内存与页面置换:从缺页中断到内存性能调优

cover

一、内存的"无限幻觉":进程为什么能用比物理内存更多的地址空间?

32 位系统能寻址 4GB 内存,但物理内存可能只有 1GB——程序为什么不会崩溃?答案是虚拟内存。每个进程拥有独立的虚拟地址空间,通过页表映射到物理内存。当访问的虚拟页不在物理内存中时,触发缺页中断,操作系统从磁盘换入所需页面。这种机制让进程"以为"自己拥有连续的大内存,实际物理内存可能分散且不足。

虚拟内存的核心挑战是"页面置换"——物理内存不够时,选择哪些页面换出到磁盘。选择不当会导致频繁换页(Thrashing),系统性能急剧下降。理解页面置换算法,是诊断内存性能问题和调优的基础。

二、虚拟内存与页面置换机制

graph TB
    subgraph 虚拟地址空间
        A[进程虚拟页 VP0] --> D[页表]
        B[进程虚拟页 VP1] --> D
        C[进程虚拟页 VP2] --> D
    end

    subgraph 页表映射
        D -->|有效位=1| E[物理页帧 PP0]
        D -->|有效位=1| F[物理页帧 PP1]
        D -->|有效位=0| G[缺页中断<br/>从磁盘换入]
    end

    subgraph 页面置换
        G --> H{物理内存满?}
        H -->|是| I[选择牺牲页<br/>LRU/Clock/FIFO]
        I --> J[写回脏页<br/>换入新页]
        H -->|否| J
        J --> K[更新页表<br/>重新执行指令]
    end

虚拟内存的地址翻译过程:CPU 发出虚拟地址 → MMU 查页表 → 有效位为 1 则直接访问物理页 → 有效位为 0 则触发缺页中断 → 操作系统选择牺牲页、换入新页 → 更新页表 → 重新执行指令。整个过程对进程透明。

三、页面置换算法实现

3.1 LRU 页面置换

from collections import OrderedDict
from typing import Optional

class LRUCache:
    """LRU 页面置换算法:最近最少使用"""

    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = OrderedDict()
        self.hit_count = 0
        self.miss_count = 0

    def access(self, page: int) -> bool:
        """访问页面,返回是否命中"""
        if page in self.cache:
            # 命中:移到末尾(最近使用)
            self.cache.move_to_end(page)
            self.hit_count += 1
            return True

        # 缺页:需要换入
        self.miss_count += 1

        if len(self.cache) >= self.capacity:
            # 物理内存满,淘汰最久未使用的页面
            evicted = self.cache.popitem(last=False)
            # 实际场景中,如果 evicted 是脏页需要写回磁盘

        self.cache[page] = True
        return False

    @property
    def hit_rate(self) -> float:
        total = self.hit_count + self.miss_count
        return self.hit_count / total if total > 0 else 0.0


class ClockPageReplacement:
    """Clock 页面置换算法:LRU 的近似实现"""

    def __init__(self, capacity: int):
        self.capacity = capacity
        self.frames = [None] * capacity  # 页面帧
        self.reference_bits = [0] * capacity  # 引用位
        self.hand = 0  # 时钟指针
        self.hit_count = 0
        self.miss_count = 0

    def access(self, page: int) -> bool:
        """访问页面"""
        # 检查是否已在内存中
        for i in range(self.capacity):
            if self.frames[i] == page:
                self.reference_bits[i] = 1  # 设置引用位
                self.hit_count += 1
                return True

        # 缺页:需要换入
        self.miss_count += 1
        victim = self._find_victim()
        self.frames[victim] = page
        self.reference_bits[victim] = 1
        return False

    def _find_victim(self) -> int:
        """Clock 算法寻找牺牲页"""
        while True:
            if self.reference_bits[self.hand] == 0:
                # 引用位为 0,选择此页作为牺牲页
                victim = self.hand
                self.hand = (self.hand + 1) % self.capacity
                return victim
            else:
                # 引用位为 1,给第二次机会
                self.reference_bits[self.hand] = 0
                self.hand = (self.hand + 1) % self.capacity

    @property
    def hit_rate(self) -> float:
        total = self.hit_count + self.miss_count
        return self.hit_count / total if total > 0 else 0.0

3.2 缺页中断处理模拟

from dataclasses import dataclass
from typing import List

@dataclass
class PageFaultEvent:
    """缺页事件"""
    virtual_page: int
    physical_frame: int
    evicted_page: Optional[int]
    is_dirty: bool
    timestamp: float

class VirtualMemorySimulator:
    """虚拟内存模拟器"""

    def __init__(
        self, physical_frames: int,
        algorithm: str = "lru"
    ):
        self.physical_frames = physical_frames
        self.algorithm = algorithm
        self.page_table = {}  # 虚拟页 → 物理帧
        self.dirty_bits = {}  # 虚拟页 → 是否脏页
        self.fault_log: List[PageFaultEvent] = []

        if algorithm == "lru":
            self.replacer = LRUCache(physical_frames)
        elif algorithm == "clock":
            self.replacer = ClockPageReplacement(physical_frames)

    def access_sequence(
        self, pages: List[int], write: List[bool]
    ) -> dict:
        """模拟页面访问序列"""
        for i, (page, is_write) in enumerate(zip(pages, write)):
            hit = self.replacer.access(page)

            if not hit:
                # 缺页处理
                evicted = self._get_evicted_page()
                is_dirty = self.dirty_bits.get(evicted, False)

                self.fault_log.append(PageFaultEvent(
                    virtual_page=page,
                    physical_frame=0,
                    evicted_page=evicted,
                    is_dirty=is_dirty,
                    timestamp=i * 0.1,
                ))

                self.page_table[page] = True
                if is_write:
                    self.dirty_bits[page] = True

        return {
            'total_accesses': len(pages),
            'page_faults': len(self.fault_log),
            'fault_rate': len(self.fault_log) / len(pages),
            'dirty_writes': sum(
                1 for f in self.fault_log if f.is_dirty
            ),
            'hit_rate': self.replacer.hit_rate,
        }

    def _get_evicted_page(self) -> Optional[int]:
        """获取被淘汰的页面"""
        if isinstance(self.replacer, LRUCache):
            # LRU 中最近最少使用的在 OrderedDict 头部
            if self.replacer.cache:
                return next(iter(self.replacer.cache))
        return None

四、页面置换的 Trade-offs 分析

LRU 的理论最优 vs. 实现成本:LRU 是理论最优的置换算法(Belady 定理的逆命题),但精确 LRU 需要维护访问时间戳,每次访问都更新,开销大。Linux 实际使用 Clock 算法(LRU 的近似),只维护引用位,开销小但精度略低。

工作集模型:进程在某个时间段内频繁访问的页面集合称为"工作集"。如果物理内存小于工作集,无论用什么置换算法都会频繁缺页(Thrashing)。解决方案是监控缺页率,当缺页率超过阈值时,暂停部分进程释放内存,而非盲目增加页面置换频率。

预读策略:缺页中断后,除了换入请求的页面,还可以预读相邻页面(Linux 默认预读 128KB)。预读对顺序访问效果好,但对随机访问是浪费。Linux 根据访问模式动态调整预读窗口大小。

大页(Huge Pages):默认页面大小 4KB,大页可以是 2MB 或 1GB。大页减少页表项数量,降低 TLB Miss 率,适合大内存应用(数据库、AI 推理)。但大页的内存浪费(内部碎片)更严重,不适合小内存场景。

五、总结

虚拟内存通过页表映射和缺页中断,让进程使用超过物理内存的地址空间。页面置换算法决定了内存不足时的性能表现——LRU 理论最优但实现成本高,Clock 是工程实用的近似方案。理解页面置换机制,是诊断内存性能问题(Thrashing、高缺页率)和调优(大页、预读、工作集控制)的基础。

落地建议:生产环境监控缺页率(/proc/vmstatpgfault),超过阈值时排查工作集是否过大。数据库和 AI 推理服务启用大页,减少 TLB Miss。避免过度分配内存导致 Thrashing——宁可减少并发进程数,也不要让所有进程都在换页。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐