【操作系统入门】虚拟内存与页面置换

【操作系统入门】第八章:虚拟内存与页面置换——超越物理限制的魔法

本系列共10篇,这是第8/10篇。在第七章,我们探讨了分页、分段等内存管理基础。今天,我们将深入虚拟内存这一革命性技术,探索操作系统如何让进程使用比物理内存大得多的地址空间。

开篇:虚拟内存的奇迹

想象一个魔术师的手提箱:从外面看只是一个普通箱子,但打开后却能不断取出远大于箱子容量的物品。虚拟内存就是计算机世界的这种"魔术"——让每个进程都相信自己拥有完整且私有的巨大内存空间,而实际上物理内存可能只有其中的一小部分。


第一部分:虚拟内存概念与按需分页

1.1 虚拟内存的核心思想

虚拟内存通过结合主内存二级存储(磁盘),为进程提供一个远大于物理内存的虚拟地址空间

关键优势

  • 进程隔离:每个进程拥有独立的地址空间
  • 简化编程:程序员无需关心物理内存布局
  • 内存超售:所有进程的虚拟内存总和可以远超物理内存
  • 高效文件映射:文件可以直接映射到地址空间

1.2 按需分页

虚拟内存的核心实现机制是按需分页——只有在进程实际访问页面时,才将其加载到物理内存。

// 页错误处理程序核心逻辑
void handle_page_fault(virt_addr_t fault_addr, fault_reason_t reason) {
    // 获取对应的页表项
    pte_t *pte = get_pte(current_process->page_table, fault_addr);
    
    switch (reason) {
    case PAGE_NOT_PRESENT:
        // 页面不在内存中,需要从磁盘加载
        if (!pte->present) {
            handle_page_not_present(pte, fault_addr);
        } else {
            // TLB失效,重新加载TLB
            reload_tlb_entry(fault_addr, pte);
        }
        break;
        
    case PAGE_PROTECTION_VIOLATION:
        // 权限错误:写只读页面或用户访问内核页面
        handle_protection_fault(pte, fault_addr);
        break;
        
    case INVALID_ADDRESS:
        // 访问了未分配的地址
        send_signal(current_process, SIGSEGV);
        break;
    }
}

1.3 页表项的扩展

支持虚拟内存的页表项需要额外信息:

typedef struct {
    uint32_t frame_number   : 20;   // 物理帧号
    uint32_t present        : 1;    // 页是否在内存中
    uint32_t writable       : 1;    // 是否可写
    uint32_t user_accessible: 1;    // 用户模式可访问
    uint32_t accessed       : 1;    // 最近是否被访问(用于置换算法)
    uint32_t dirty          : 1;    // 是否被修改过(决定是否需要写回)
    uint32_t paging_file    : 1;    // 是否在分页文件中
    uint32_t swap_offset    : 24;   // 在交换空间中的位置
    uint32_t reserved       : 2;    // 保留位
} virtual_pte_t;

第二部分:页面置换算法理论

当物理内存不足时,操作系统必须选择牺牲页面换出到磁盘。不同的置换算法对系统性能有巨大影响。

2.1 评估指标

  • 缺页率:缺页次数与内存访问次数的比例
  • Belady异常:增加物理帧数反而导致缺页率上升的现象

2.2 最优置换算法(OPT)

理论上的最优算法:置换在未来最长时间不会被访问的页面。

// 理论上最优,但需要预知未来
page_t *optimal_replacement(memory_t *mem, access_sequence_t *future) {
    page_t *victim = NULL;
    uint32_t farthest_use = 0;
    
    // 遍历所有内存中的页面
    for (int i = 0; i < mem->frame_count; i++) {
        page_t *page = &mem->frames[i];
        
        // 查找该页面下一次被访问的位置
        uint32_t next_use = find_next_access(page, future);
        
        // 选择最远才会被使用的页面
        if (next_use > farthest_use) {
            farthest_use = next_use;
            victim = page;
        }
    }
    
    return victim;
}

局限性:需要预知未来的页面访问序列,实际中不可实现。

2.3 先进先出算法(FIFO)

置换在内存中驻留时间最长的页面。

// FIFO页面置换
typedef struct {
    page_t **frames;
    uint32_t count;
    uint32_t head;  // 指向最早进入的页面
} fifo_memory_t;

page_t *fifo_replace(fifo_memory_t *fifo) {
    page_t *victim = fifo->frames[fifo->head];
    
    // 循环队列,头指针前移
    fifo->head = (fifo->head + 1) % fifo->count;
    
    return victim;
}

问题:可能置换掉经常访问的页面,存在Belady异常。

2.4 最近最少使用算法(LRU)

基于局部性原理:最近被访问的页面很可能在不久的将来再次被访问。

实现方案1:计数器法

// 为每个页表项添加计数器
typedef struct {
    uint64_t last_accessed;  // 最后访问时间戳
    // ... 其他字段
} lru_pte_t;

page_t *lru_counter_replace(memory_t *mem) {
    page_t *victim = NULL;
    uint64_t oldest_time = UINT64_MAX;
    
    for (int i = 0; i < mem->frame_count; i++) {
        if (mem->frames[i].last_accessed < oldest_time) {
            oldest_time = mem->frames[i].last_accessed;
            victim = &mem->frames[i];
        }
    }
    return victim;
}

实现方案2:栈法

// 维护访问顺序栈
typedef struct lru_stack_node {
    page_t *page;
    struct lru_stack_node *prev;
    struct lru_stack_node *next;
} lru_stack_node_t;

void lru_stack_access(lru_stack_t *stack, page_t *page) {
    // 如果页面已在栈中,移动到栈顶
    lru_stack_node_t *node = find_node(stack, page);
    if (node) {
        remove_node(stack, node);
    } else {
        node = create_node(page);
    }
    push_top(stack, node);  // 放到栈顶
}

page_t *lru_stack_replace(lru_stack_t *stack) {
    // 栈底就是最近最少使用的页面
    return stack->bottom->page;
}

第三部分:实用的近似LRU算法

精确LRU实现开销大,实际系统使用近似算法。

3.1 时钟算法(第二次机会算法)

typedef struct {
    page_t **frames;
    uint32_t count;
    uint32_t clock_hand;  // 时钟指针
} clock_memory_t;

page_t *clock_replace(clock_memory_t *clock_mem) {
    while (true) {
        page_t *candidate = clock_mem->frames[clock_mem->clock_hand];
        
        if (candidate->accessed == 0) {
            // 引用位为0,选择该页面
            clock_mem->clock_hand = (clock_mem->clock_hand + 1) % clock_mem->count;
            return candidate;
        } else {
            // 给第二次机会:清空引用位,继续检查下一个
            candidate->accessed = 0;
            clock_mem->clock_hand = (clock_mem->clock_hand + 1) % clock_mem->count;
        }
    }
}

3.2 改进型时钟算法

考虑页面的修改状态(脏位),置换代价不同:

  • 未修改页面:直接丢弃
  • 已修改页面:需要写回磁盘
// 四类页面优先级
typedef enum {
    CLASS_0 = 0,  // (引用位=0, 脏位=0) - 最佳牺牲品
    CLASS_1 = 1,  // (引用位=0, 脏位=1) - 需要写回
    CLASS_2 = 2,  // (引用位=1, 脏位=0) - 清空引用位后变为CLASS_0
    CLASS_3 = 3   // (引用位=1, 脏位=1) - 清空引用位后变为CLASS_1
} page_class_t;

page_t *enhanced_clock_replace(clock_memory_t *clock_mem) {
    // 第一轮扫描:寻找(0,0)页面
    for (int scan = 0; scan < 2; scan++) {
        for (int i = 0; i < clock_mem->count; i++) {
            page_t *candidate = clock_mem->frames[clock_mem->clock_hand];
            page_class_t class = get_page_class(candidate);
            
            if (scan == 0 && class == CLASS_0) {
                // 找到最佳牺牲品
                clock_mem->clock_hand = (clock_mem->clock_hand + 1) % clock_mem->count;
                return candidate;
            }
            else if (scan == 1 && class == CLASS_1) {
                // 找到需要写回的页面
                clock_mem->clock_hand = (clock_mem->clock_hand + 1) % clock_mem->count;
                return candidate;
            }
            else {
                // 清空引用位,继续寻找
                candidate->accessed = 0;
                clock_mem->clock_hand = (clock_mem->clock_hand + 1) % clock_mem->count;
            }
        }
    }
    
    // 如果前两轮没找到,重新开始(此时所有页面引用位都已清空)
    return enhanced_clock_replace(clock_mem);
}

3.3 老化算法

使用移位寄存器近似LRU:

// 每个页面有一个8位的访问历史寄存器
typedef struct {
    uint8_t age_counter;    // 老化计数器
    uint8_t referenced;     // 当前引用位
} aging_pte_t;

void aging_update(aging_pte_t *pages, int count) {
    for (int i = 0; i < count; i++) {
        // 右移一位,最高位设置为当前引用位
        pages[i].age_counter = (pages[i].age_counter >> 1) | 
                              (pages[i].referenced << 7);
        pages[i].referenced = 0;  // 清空引用位
    }
}

page_t *aging_replace(aging_pte_t *pages, int count) {
    page_t *victim = NULL;
    uint8_t min_age = UINT8_MAX;
    
    for (int i = 0; i < count; i++) {
        if (pages[i].age_counter < min_age) {
            min_age = pages[i].age_counter;
            victim = get_page_from_pte(&pages[i]);
        }
    }
    return victim;
}

第四部分:工作集模型与颠簸

4.1 工作集模型

Denning的工作集理论:进程在时间窗口Δ内访问的页面集合称为工作集。

typedef struct {
    page_t **pages;         // 工作集页面
    uint64_t last_accessed; // 最后访问时间
    uint32_t size;          // 工作集大小
} working_set_t;

bool is_in_working_set(working_set_t *ws, page_t *page, uint64_t current_time, uint64_t tau) {
    // 检查页面是否在最近tau时间内被访问过
    return (current_time - page->last_accessed) <= tau;
}

void update_working_set(working_set_t *ws, page_t *page, uint64_t current_time) {
    page->last_accessed = current_time;
    // 更新工作集统计信息...
}

4.2 页面错误频率算法

动态调整分配给进程的帧数来控制缺页率:

typedef struct {
    uint32_t page_fault_count;      // 缺页计数
    uint64_t last_check_time;       // 上次检查时间
    uint32_t allocated_frames;      // 分配的帧数
    double page_fault_rate;         // 缺页率
} pff_control_t;

void pff_adjust_allocation(pff_control_t *ctrl, process_t *proc) {
    uint64_t current_time = get_current_time();
    uint64_t interval = current_time - ctrl->last_check_time;
    
    // 计算当前缺页率
    double current_rate = (double)ctrl->page_fault_count / interval;
    
    if (current_rate > ctrl->page_fault_rate * 1.1) {
        // 缺页率过高,增加分配帧数
        ctrl->allocated_frames = min(ctrl->allocated_frames + 1, MAX_FRAMES);
        adjust_process_frames(proc, ctrl->allocated_frames);
    }
    else if (current_rate < ctrl->page_fault_rate * 0.9) {
        // 缺页率过低,减少分配帧数
        ctrl->allocated_frames = max(ctrl->allocated_frames - 1, MIN_FRAMES);
        adjust_process_frames(proc, ctrl->allocated_frames);
    }
    
    ctrl->page_fault_count = 0;
    ctrl->last_check_time = current_time;
}

4.3 系统颠簸

当系统过度分页时,CPU大部分时间用于页面置换而非有用工作:

// 检测和缓解系统颠簸
void handle_thrashing(memory_manager_t *mm) {
    double cpu_utilization = get_cpu_utilization();
    double page_fault_rate = get_system_page_fault_rate();
    
    if (cpu_utilization < 10.0 && page_fault_rate > 1000.0) {
        // 检测到颠簸:CPU利用率低但缺页率高
        printf("System thrashing detected! CPU util: %.1f%%, Page faults/sec: %.0f\n",
               cpu_utilization, page_fault_rate);
        
        // 缓解措施:挂起部分进程
        process_t *victim_process = select_process_to_suspend();
        if (victim_process) {
            suspend_process(victim_process);
            printf("Suspended process %d to alleviate thrashing\n", victim_process->pid);
        }
    }
}

第五部分:实际系统实现

5.1 Linux页面置换

Linux使用基于LRU的复杂策略:

// Linux的双链表LRU结构
struct lru_list {
    struct list_head active_list;   // 活跃页面列表
    struct list_head inactive_list; // 非活跃页面列表
    unsigned long active_count;     // 活跃页面计数
    unsigned long inactive_count;   // 非活跃页面计数
};

// 页面回收核心逻辑
static void shrink_page_list(struct list_head *page_list) {
    struct page *page;
    struct page *next;
    
    list_for_each_entry_safe(page, next, page_list, lru) {
        // 检查页面是否可回收
        if (PageDirty(page) && !PageWriteback(page)) {
            // 脏页面,需要写回
            set_page_writeback(page);
            submit_page_for_writeback(page);
            continue;
        }
        
        if (PageActive(page)) {
            // 活跃页面,移到非活跃列表
            del_page_from_active_list(page);
            add_page_to_inactive_list(page);
        } else {
            // 非活跃页面,尝试回收
            if (try_to_reclaim_page(page)) {
                // 成功回收
                free_page(page);
            }
        }
    }
}

5.2 Windows工作集管理器

Windows使用复杂的工作集管理:

// 工作集管理器核心逻辑
VOID MmWorkingSetManager(VOID) {
    // 平衡集管理器定期运行
    while (TRUE) {
        KeDelayExecutionThread(KernelMode, FALSE, &interval);
        
        // 检查每个进程的工作集
        for (process in all_processes) {
            working_set_size = process->WorkingSetSize;
            page_fault_count = process->PageFaultCount;
            
            // 根据缺页率调整工作集大小
            if (page_fault_count > threshold_high) {
                // 增加工作集
                process->WorkingSetSize = min(working_set_size * 1.1, MAX_WORKING_SET);
            } else if (page_fault_count < threshold_low) {
                // 减少工作集
                process->WorkingSetSize = max(working_set_size * 0.9, MIN_WORKING_SET);
            }
            
            // 修剪工作集:移出最近未使用的页面
            MmTrimWorkingSet(process, TRIM_AMOUNT);
        }
    }
}

第六部分:高级优化技术

6.1 预取优化

基于访问模式预测未来需要的页面:

// 顺序预取:检测顺序访问模式
void sequential_prefetch(virt_addr_t current_addr) {
    static virt_addr_t last_addr = 0;
    
    if (current_addr == last_addr + PAGE_SIZE) {
        // 检测到顺序访问,预取后续页面
        for (int i = 1; i <= PREFETCH_DEGREE; i++) {
            virt_addr_t prefetch_addr = current_addr + i * PAGE_SIZE;
            if (is_valid_address(prefetch_addr)) {
                prefetch_page(prefetch_addr);
            }
        }
    }
    
    last_addr = current_addr;
}

6.2 交换空间管理

磁盘交换空间的高效管理:

// 交换空间分配策略
typedef struct {
    disk_block_t *free_blocks;      // 空闲块列表
    uint32_t block_size;            // 交换块大小(通常等于页大小)
    uint32_t total_blocks;          // 总块数
    uint32_t used_blocks;           // 已用块数
} swap_space_t;

swap_offset_t allocate_swap_block(swap_space_t *swap) {
    if (swap->free_blocks == NULL) {
        // 交换空间不足
        handle_swap_space_exhaustion();
        return INVALID_SWAP_OFFSET;
    }
    
    disk_block_t *block = swap->free_blocks;
    swap->free_blocks = block->next;
    swap->used_blocks++;
    
    return block->offset;
}

void free_swap_block(swap_space_t *swap, swap_offset_t offset) {
    disk_block_t *block = get_block_from_offset(offset);
    block->next = swap->free_blocks;
    swap->free_blocks = block;
    swap->used_blocks--;
}

6.3 内存压缩

现代系统的内存压缩技术:

// 内存页面压缩
typedef struct {
    uint8_t *compressed_data;   // 压缩后的数据
    size_t compressed_size;     // 压缩后大小
    uint32_t original_crc;      // 原始数据校验和
} compressed_page_t;

bool try_compress_page(page_t *page, compressed_page_t *compressed) {
    // 尝试压缩页面
    size_t max_compressed_size = PAGE_SIZE;
    int result = compression_algorithm(page->data, PAGE_SIZE, 
                                      compressed->compressed_data, 
                                      &max_compressed_size);
    
    if (result == COMPRESSION_SUCCESS && 
        max_compressed_size < PAGE_SIZE * COMPRESSION_THRESHOLD) {
        // 压缩比达到阈值,使用压缩版本
        compressed->compressed_size = max_compressed_size;
        compressed->original_crc = calculate_crc32(page->data, PAGE_SIZE);
        return true;
    }
    
    return false;
}

第七部分:性能监控与调优

7.1 性能计数器

利用硬件性能计数器监控内存行为:

// 内存性能监控
typedef struct {
    uint64_t page_faults;           // 缺页次数
    uint64_t tlb_misses;            // TLB未命中
    uint64_t cache_misses;          // 缓存未命中
    uint64_t memory_accesses;       // 内存访问次数
} memory_stats_t;

void update_memory_stats(memory_stats_t *stats) {
    // 读取硬件性能计数器
    stats->page_faults = read_pmc(PMC_PAGE_FAULTS);
    stats->tlb_misses = read_pmc(PMC_TLB_MISSES);
    stats->cache_misses = read_pmc(PMC_CACHE_MISSES);
    
    // 计算缺页率、TLB命中率等指标
    double page_fault_rate = (double)stats->page_faults / stats->memory_accesses;
    double tlb_hit_rate = 1.0 - (double)stats->tlb_misses / stats->memory_accesses;
    
    // 根据指标进行调优
    if (page_fault_rate > 0.01) {
        adjust_memory_parameters(MORE_AGGRESSIVE_PREFETCH);
    }
}

7.2 自动调优参数

// 自适应内存参数调优
void adaptive_memory_tuning(system_t *sys) {
    memory_stats_t stats = collect_memory_stats();
    
    // 根据工作负载特征调整参数
    if (stats.working_set_size > sys->physical_memory * 0.8) {
        // 工作集接近物理内存大小,使用更积极的置换策略
        sys->page_replacement_aggressiveness = HIGH_AGGRESSIVENESS;
        sys->prefetch_enabled = true;
    } else {
        // 内存充足,使用保守策略
        sys->page_replacement_aggressiveness = LOW_AGGRESSIVENESS;
        sys->prefetch_enabled = false;
    }
    
    // 调整TLB和缓存相关参数
    if (stats.tlb_miss_rate > 0.02) {
        enable_large_pages();  // 启用大页减少TLB压力
    }
}
总结与展望

今天我们深入探讨了:

  • 虚拟内存的基本原理和按需分页机制
  • 各种页面置换算法的理论基础和实践实现
  • 工作集模型和系统颠簸的检测缓解
  • 实际系统中虚拟内存的工程实现
  • 高级优化技术和性能监控方法

虚拟内存是操作系统中最精妙的设计之一,它通过巧妙的页面置换策略,在有限的物理资源上为进程提供了近乎无限的内存空间。


系列导航:

  • 上一篇:[操作系统入门] 第七章:内存管理(一)—— 分页、分段与地址空间
  • 下一篇:[操作系统入门] 第九章:文件系统——数据的持久化存储

(你的名字) | (你的博客链接/签名)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

star _chen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值