段页式存储管理

内存分页存储

一级页表

本质上是将虚拟地址页号 ( vpn) 通过页表映射为物理页号 (ppn) 的过程

中间涉及

  • 根据页面大小 (PAGE_SIZE),将虚拟地址 (va) 拆分为虚拟地址页号 ( vpn) 和 页内偏移 (offset),做法是算数位移
  • 根据页表起始地址(page_table*)、数组元素长度 (sizeof(uint32_t)) 和偏移量 (即 虚拟地址页号 ( vpn) ) 得到物理页表的值 (ppn),做法是计算数组偏移量
  • 组装物理页表 (ppn) 和页内偏移 (offset),获得物理地址 (pa)

C 语言程序示例(加入了缺页和越界错误判断,以及预留 1 bit 用于判断页表是否有效)

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
 
#define PAGE_SIZE            4096U               // 4KB
#define PAGE_OFFSET_BITS     12U                 // log2(PAGE_SIZE)
#define PAGE_TABLE_ENTRIES   1024U               // 示例:支持 1024 个虚拟页
#define INVALID_PTE          0U                  // 表示无效的页表项
 
// 页表:每个条目为一个 32 位无符号数
// bit0: valid (1 = 映射有效;0 = 无效/未映射)
// bit31..1: 物理页号 (PPN)
static uint32_t page_table[PAGE_TABLE_ENTRIES];
 
/**
 * 初始化指定虚拟页号 vpn 到物理页号 ppn 的映射,
 * 并将其标记为有效。
 */
void map_page(uint32_t vpn, uint32_t ppn) {
    if (vpn >= PAGE_TABLE_ENTRIES) {
        fprintf(stderr, "Error: vpn out of range\n");
        exit(1);
    }
    // 物理页号左移 1 位,高 31 位存 PPN;最低位设为 1(valid)
    page_table[vpn] = (ppn << 1) | 1U;
}
 
/**
 * 将虚拟地址 va 翻译为物理地址,若发生缺页则返回 INVALID_PTE。
 */
uint32_t translate_va_to_pa(uint32_t va) {
    // 1) 拆分:页号和页内偏移
    uint32_t vpn    = va >> PAGE_OFFSET_BITS;            // 虚拟页号
    uint32_t offset = va & (PAGE_SIZE - 1U);             // 页内偏移
 
    if (vpn >= PAGE_TABLE_ENTRIES) {
        // 虚拟地址超出支持范围
        return INVALID_PTE;
    }
 
    // 2) 取出页表项
    uint32_t pte = page_table[vpn];
    if ((pte & 1U) == 0) {
        // valid 位为 0,视为缺页(Page Fault)
        return INVALID_PTE;
    }
 
    // 3) 提取物理页号
    uint32_t ppn = pte >> 1;
 
    // 4) 合成物理地址
    return (ppn << PAGE_OFFSET_BITS) | offset;
}
 
int main(void) {
    // 示例:将虚拟页 1 映射到物理页 0x10
    map_page(1, 0x10);
 
    // 测试几组虚拟地址
    uint32_t test_vas[] = {
        0x00000000,   // VPN=0, offset=0
        0x00001000,   // VPN=1, offset=0
        0x00001ABC,   // VPN=1, offset=0xABC
        0x00400000    // VPN=1024, 越界
    };
 
    for (size_t i = 0; i < sizeof(test_vas)/sizeof(test_vas[0]); i++) {
        uint32_t va = test_vas[i];
        uint32_t pa = translate_va_to_pa(va);
        if (pa == INVALID_PTE) {
            printf("VA 0x%08X → Page Fault / Invalid mapping\n", va);
        } else {
            printf("VA 0x%08X → PA 0x%08X\n", va, pa);
        }
    }
 
    return 0;
}

二级页表

二级页表地址变换流程

  1. 虚拟地址拆分

    | 一级页号 (P1) | 二级页号 (P2) | 页内偏移 (Offset) |
    • 通过虚拟地址位数和页大小自动划分(如 32 位系统 +4KB 页时:P1=10bit, P2=10bit, Offset=12bit)
  2. 一级页表查询

    • 通过 页目录基址寄存器(CR3)找到页目录表
    • P1 作为索引定位页目录项(PDE)
    • 检查 PDE 的有效位(若无效触发缺页异常)
  3. 二级页表查询

    • 从 PDE 中获取二级页表物理基址
    • P2 作为索引定位页表项(PTE)
    • 检查 PTE 的有效位(若无效触发缺页异常)
  4. 物理地址合成

    物理地址 = (PTE中的物理页框号 << Offset位数) | Offset

硬件辅助机制

  • TLB 加速:缓存最近使用的页表项
  • 缺页处理:当 PTE 无效时触发异常,由 OS 加载缺失页
  • 权限检查:同时验证 PTE 中的 RWX 权限位

对比单级页表

特性单级页表二级页表
空间占用连续大空间(4MB)离散小空间(按需分配)
查询速度一次访存两次访存(可被 TLB 优化)
适用场景小地址空间系统32/64 位现代系统

分段式存储管理

  • 给定一个逻辑地址 A,其由段号 S 和段内偏移 W 组成。
  • 段表中包含每个段的 基址(Base)段长(Limit)
  • 转换过程:
    1. 取出段号 S 对应的段表项。
    2. 检查 W 是否小于该段的段长 M(越界检查)。
    3. 检查段表项中的段长 C 是否大于偏移量 W。
    4. 如果不越界,则物理地址 E = 基址 + 偏移量
#include <stdio.h>
 
#define NUM_SEGMENTS 4
 
typedef struct {
    int limit;  // 段长
    int base;   // 段基址
} SegmentDescriptor;
 
// 模拟段表
SegmentDescriptor segment_table[NUM_SEGMENTS] = {
    {1024, 6144},  // 段0: 1KB, 基址6KB
    {600, 4096},   // 段1: 600B, 基址4KB
    {500, 8192},   // 段2: 500B, 基址8KB
    {200, 9200}    // 段3: 200B, 基址9200
};
 
// 逻辑地址结构:段号 + 段内偏移
typedef struct {
    int segment;  // 段号S
    int offset;   // 段内偏移W
} LogicalAddress;
 
// 地址转换函数
int logical_to_physical(LogicalAddress la) {
    if (la.segment < 0 || la.segment >= NUM_SEGMENTS) {
        printf("错误:无效的段号 %d\n", la.segment);
        return -1;
    }
 
    SegmentDescriptor sd = segment_table[la.segment];
 
    if (la.offset >= sd.limit) {
        printf("错误:段内偏移越界,offset = %d, limit = %d\n", la.offset, sd.limit);
        return -1;
    }
 
    int physical_address = sd.base + la.offset;
    printf("逻辑地址 (段号: %d, 偏移: %d) => 物理地址: %d\n", la.segment, la.offset, physical_address);
    return physical_address;
}
 
int main() {
    LogicalAddress la = {2, 100};  // 图中的示例:段号2,偏移100
    logical_to_physical(la);
    return 0;
}
 

优缺点及对比

一、表项存储位置与进程对应关系

表项类型存储位置进程对应关系
页表项 (PTE)存放在内存的页表中(多级页表时分散存储)每个进程有独立的页目录和页表,表项数量=虚拟地址空间大小/页大小
段表项存放在内核空间的段表中每个进程有独立的段表,表项数量=进程拥有的段数量(通常 4-16 个)

二、分页与分段的核心对比

1. 分页机制(Paging)

✅ 优点

  • 内存利用率高:消除外部碎片,按固定大小分配
  • 透明性:对程序员不可见,由 OS 和硬件管理
  • 支持虚拟内存:缺页中断实现按需调页
  • 适合大地址空间:多级页表节省内存

❌ 缺点

  • 存在内部碎片(平均半页浪费)
  • 共享困难:需保持各进程页表同步
  • 无逻辑保护:单页可能包含混合类型数据

2. 分段机制(Segmentation)

✅ 优点

  • 逻辑性强:按程序自然结构(代码/数据/堆栈)划分
  • 易于共享:以段为单位设置权限
  • 动态扩展:段长度可动态增长
  • 安全保护:段边界检查防止越界访问

❌ 缺点

  • 产生外部碎片:需配合紧凑技术
  • 内存管理复杂:需维护可变大小的内存块
  • 不适合大地址空间:段表会变得庞大

三、现代系统的典型实现

  1. x86 架构:采用段页式混合管理

    • 先通过段机制进行逻辑地址转换(现代 OS 通常设为平坦模式)
    • 再通过多级页表(如 4 级页表)转换线性地址到物理地址
  2. 进程切换代价

    • 页表切换:需更新 CR3 寄存器(TLB 会失效)
    • 段表切换:修改段寄存器(如 GDTR/LDTR)