【操作系统入门】内存管理(一)

【操作系统入门】第七章:内存管理(一)------ 分页、分段与地址空间

本系列共10篇,这是第7/10篇。在第六章,我们探讨了死锁问题。现在,我们将进入操作系统另一个核心领域------内存管理,探索如何为众多进程高效、安全地分配有限的内存资源。

开篇:内存管理的根本挑战

想象一个大型图书馆:

  • 成千上万的书籍(进程数据)
  • 有限的阅览桌空间(物理内存)
  • 读者需要快速找到特定书籍(内存访问)
  • 书籍需要合理摆放避免混乱(内存分配)

操作系统作为图书管理员,必须解决:如何让数百个进程共享有限物理内存,同时让每个进程都感觉自己拥有完整的私人图书馆?


第一部分:内存管理基础

1.1 内存层次结构

现代计算机采用金字塔形的存储层次:

复制代码
寄存器 → L1/L2/L3缓存 → 主内存(DRAM) → 磁盘(SSD/HDD)
    ↑          ↑           ↑          ↑
  最快        快         中等        慢
  最小       小          大         巨大

内存管理主要关注主内存的高效利用。

1.2 地址绑定时机

程序中的内存地址在何时被绑定到物理地址?

  • 编译时:如果内存位置已知,生成绝对代码
  • 加载时:编译生成可重定位代码,加载时确定最终位置
  • 执行时:地址在运行时才确定,需要硬件支持(MMU)

1.3 逻辑地址 vs 物理地址

  • 逻辑地址:CPU生成的地址,也称为虚拟地址
  • 物理地址:内存单元的实际地址
  • 内存管理单元(MMU):硬件设备,负责逻辑地址到物理地址的动态映射

第二部分:连续内存分配

2.1 内存分配策略

早期系统使用连续分配,主要有三种策略:

首次适应

c 复制代码
// 从内存开始顺序查找第一个足够大的空闲分区
void* first_fit(int size) {
    for (partition *p = free_list; p != NULL; p = p->next) {
        if (p->size >= size) {
            return allocate_from(p, size);
        }
    }
    return NULL; // 没有足够大的分区
}

最佳适应

c 复制代码
// 查找最接近需求大小的空闲分区
void* best_fit(int size) {
    partition *best = NULL;
    for (partition *p = free_list; p != NULL; p = p->next) {
        if (p->size >= size && (best == NULL || p->size < best->size)) {
            best = p;
        }
    }
    return best ? allocate_from(best, size) : NULL;
}

最差适应

c 复制代码
// 总是分配最大的空闲分区
void* worst_fit(int size) {
    partition *worst = NULL;
    for (partition *p = free_list; p != NULL; p = p->next) {
        if (p->size >= size && (worst == NULL || p->size > worst->size)) {
            worst = p;
        }
    }
    return worst ? allocate_from(worst, size) : NULL;
}

2.2 碎片化问题

连续分配面临的核心问题:

外部碎片

复制代码
内存布局:[进程A:100K][空闲:50K][进程B:200K][空闲:80K][进程C:150K]
问题:虽然总空闲空间=130K,但无法分配120K的进程

内部碎片

复制代码
分配策略:按2^n大小分配
请求:5KB → 分配8KB → 内部碎片=3KB

2.3 压缩技术

通过移动进程来合并空闲分区:

c 复制代码
// 内存压缩算法
void compact_memory() {
    uint32_t new_base = 0;
    
    // 移动所有进程到内存低端
    for (process *p = process_list; p != NULL; p = p->next) {
        if (p->base != new_base) {
            move_process(p, new_base);  // 移动进程
            update_page_tables(p);      // 更新页表
        }
        new_base += p->size;
    }
    
    // 合并剩余空间为一个大的空闲分区
    free_list->base = new_base;
    free_list->size = total_memory - new_base;
}

问题:压缩开销大,且需要所有进程可重定位。


第三部分:分页------非连续分配的突破

3.1 分页的基本概念

分页将物理内存和逻辑地址空间都划分为固定大小的块:

  • :逻辑地址空间的固定长度块
  • :物理内存的固定长度块
  • 页表:维护页到帧的映射关系

3.2 地址转换机制

逻辑地址结构:

复制代码
| 页号(p) | 页内偏移(d) |

地址转换过程:

复制代码
物理地址 = 页表[页号].帧号 × 页大小 + 页内偏移

3.3 页表结构

基本页表项(PTE)

c 复制代码
typedef struct {
    uint32_t frame_number   : 20;   // 物理帧号
    uint32_t present        : 1;    // 页是否在内存中
    uint32_t writable       : 1;    // 是否可写
    uint32_t user_mode      : 1;    // 用户模式可访问
    uint32_t accessed       : 1;    // 是否被访问过
    uint32_t dirty          : 1;    // 是否被修改过
    uint32_t cache_disabled : 1;    // 是否禁用缓存
    uint32_t reserved       : 6;    // 保留位
} page_table_entry;

3.4 页表性能问题

简单线性页表的问题:

  • 32位系统,4KB页 → 2²⁰个页表项 → 4MB页表
  • 64位系统,4KB页 → 2⁵²个页表项 → 不可行!

第四部分:多级页表与TLB

4.1 多级页表

通过层次结构减少页表内存占用:

二级页表地址转换

复制代码
逻辑地址:| 页目录索引 | 页表索引 | 页内偏移 |
转换过程:
1. 用页目录索引在页目录中找到页表地址
2. 用页表索引在页表中找到物理帧号
3. 物理帧号 + 页内偏移 = 物理地址

x86-64四级页表示例

c 复制代码
// 64位虚拟地址结构
typedef struct {
    uint64_t page_offset    : 12;   // 4KB页内偏移
    uint64_t table_index    : 9;    // 第四级页表索引
    uint64_t directory_index: 9;    // 第三级页目录索引
    uint64_t directory_ptr_index: 9; // 第二级页目录指针索引
    uint64_t pml4_index     : 9;    // 第一级PML4索引
    uint64_t sign_extension : 16;   // 符号扩展
} virtual_address_64bit;

4.2 转换检测缓冲区(TLB)

TLB是MMU中的高速缓存,存储最近使用的页表项:

TLB查找过程

c 复制代码
phys_addr_t translate_address(virt_addr_t vaddr) {
    // 1. 首先检查TLB
    tlb_entry *entry = tlb_lookup(vaddr.page_number);
    if (entry != NULL && entry->valid) {
        tlb_hits++;
        return (entry->frame_number << PAGE_SHIFT) | vaddr.offset;
    }
    
    // 2. TLB未命中,查找页表
    tlb_misses++;
    page_table_entry pte = walk_page_table(vaddr);
    
    // 3. 更新TLB
    tlb_insert(vaddr.page_number, pte.frame_number, pte.flags);
    
    return (pte.frame_number << PAGE_SHIFT) | vaddr.offset;
}

TLB性能分析

假设:

  • TLB命中率:99%
  • TLB访问时间:1ns
  • 内存访问时间:100ns
  • 页表遍历:4次内存访问

平均内存访问时间:

复制代码
TLB命中:1ns + 100ns = 101ns
TLB未命中:1ns + 4×100ns = 401ns
平均:0.99×101 + 0.01×401 = 105ns

4.3 反向页表

解决传统页表空间开销大的另一种方案:

基于哈希的反向页表

c 复制代码
typedef struct {
    virt_addr_t page_number;    // 虚拟页号
    pid_t pid;                  // 进程ID
    phys_addr_t frame_number;   // 物理帧号
} inverted_pte_t;

// 哈希表查找
inverted_pte_t* inverted_page_lookup(virt_addr_t vaddr, pid_t pid) {
    uint32_t hash = hash_function(vaddr.page_number, pid);
    
    for (int i = 0; i < HASH_CHAIN_LENGTH; i++) {
        inverted_pte_t *entry = &inverted_table[hash];
        if (entry->pid == pid && entry->page_number == vaddr.page_number) {
            return entry;
        }
        hash = (hash + 1) % INVERTED_TABLE_SIZE;
    }
    return NULL; // 页错误
}

第五部分:分段------逻辑视图的内存管理

5.1 分段的基本概念

分段基于程序的逻辑结构划分:

  • 代码段、数据段、堆段、栈段等
  • 每个段有独立的逻辑地址空间

逻辑地址结构

复制代码
| 段号(s) | 段内偏移(d) |

5.2 段表与地址转换

段表项结构

c 复制代码
typedef struct {
    uint32_t base_address;   // 段在物理内存中的基址
    uint32_t segment_limit;  // 段长度限制
    uint8_t  present    : 1; // 段是否在内存中
    uint8_t  readable   : 1; // 代码段可读
    uint8_t  writable   : 1; // 数据段可写
    uint8_t  executable : 1; // 代码段可执行
    uint8_t  privilege  : 2; // 特权级
    uint8_t  granularity: 1; // 粒度(字节/4KB页)
} segment_descriptor;

地址转换

c 复制代码
phys_addr_t segment_translation(logic_addr_t laddr) {
    // 1. 检查段选择子
    if (laddr.segment_index >= MAX_SEGMENTS) {
        raise_protection_fault();
    }
    
    // 2. 获取段描述符
    segment_descriptor *desc = &segment_table[laddr.segment_index];
    
    // 3. 段界限检查
    if (laddr.offset >= desc->segment_limit) {
        raise_general_protection_fault();
    }
    
    // 4. 权限检查
    if (!check_privilege(desc->privilege, current_privilege_level)) {
        raise_protection_fault();
    }
    
    // 5. 计算物理地址
    return desc->base_address + laddr.offset;
}

5.3 分段 vs 分页

特性 分页 分段
划分单位 固定大小的页 可变长度的段
视角 系统视角 用户/程序视角
碎片 内部碎片 外部碎片
共享 页级共享 段级共享
保护 页级保护 段级保护
实现 硬件自动 需要软件参与

第六部分:段页式内存管理

6.1 结合分段与分页

现代系统(如x86)结合两种技术:

  1. 分段:提供逻辑隔离和保护
  2. 分页:提供虚拟内存和物理内存管理

地址转换流程

复制代码
逻辑地址 → 分段单元 → 线性地址 → 分页单元 → 物理地址

6.2 x86段页式实例

c 复制代码
// x86-32地址转换
phys_addr_t x86_address_translation(logic_addr_t laddr) {
    // 第一阶段:分段
    segment_descriptor *seg_desc = 
        get_descriptor(laddr.segment_selector);
    
    linear_addr_t linear_addr = 
        seg_desc->base + laddr.offset;
    
    // 第二阶段:分页
    uint32_t dir_index = (linear_addr >> 22) & 0x3FF;
    uint32_t table_index = (linear_addr >> 12) & 0x3FF;
    uint32_t page_offset = linear_addr & 0xFFF;
    
    page_dir_entry *pde = current_page_dir[dir_index];
    page_table_entry *pte = pde->page_table[table_index];
    
    return (pte->frame_number << 12) | page_offset;
}

6.3 现代趋势:平坦地址空间

现代64位系统倾向于简化分段:

  • 代码段、数据段基址=0,界限=最大
  • 实际使用纯分页模型
  • 分段主要用于特权级控制和系统段

第七部分:内存保护机制

7.1 基于页的保护

通过页表项控制访问权限:

c 复制代码
void handle_page_fault(virt_addr_t vaddr, fault_reason_t reason) {
    page_table_entry *pte = find_pte(vaddr);
    
    switch (reason) {
        case FAULT_NOT_PRESENT:
            if (pte->present) {
                // TLB无效,重新加载
                reload_tlb(vaddr);
            } else {
                // 真正的页错误,调页
                handle_page_in(vaddr);
            }
            break;
            
        case FAULT_PROTECTION:
            if (current_privilege > pte->user_mode && !pte->writable) {
                // 用户模式访问内核页或写只读页
                kill_process(current_process, SIGSEGV);
            }
            break;
            
        case FAULT_RESERVED:
            // 保留位被设置,可能是内核漏洞
            panic("Reserved bit set in PTE");
    }
}

7.2 基于段的保护

通过段描述符实现:

  • 特权级检查(Ring 0-3)
  • 段类型检查(代码/数据)
  • 段界限检查

7.3 内存保护键

现代CPU支持内存保护键:

c 复制代码
// 设置内存区域保护
void set_memory_protection(virt_addr_t start, size_t len, 
                          uint8_t protection_key, uint16_t permissions) {
    // 配置页表项的保护键字段
    for (virt_addr_t addr = start; addr < start + len; addr += PAGE_SIZE) {
        page_table_entry *pte = find_pte(addr);
        pte->protection_key = protection_key;
        pte->access_permissions = permissions;
    }
    
    // 配置PKRU寄存器
    uint32_t pkru = (permissions << (2 * protection_key));
    write_pkru_register(pkru);
}

第八部分:实际系统分析

8.1 Linux内存管理

Linux采用三级/四级页表模型:

c 复制代码
// 四级页表遍历
pgd_t *pgd = pgd_offset(mm, address);      // 页全局目录
p4d_t *p4d = p4d_offset(pgd, address);     // 第四级目录
pud_t *pud = pud_offset(p4d, address);     // 页上层目录
pmd_t *pmd = pmd_offset(pud, address);     // 页中间目录
pte_t *pte = pte_offset_map(pmd, address); // 页表项

8.2 Windows内存管理

Windows使用工作集管理器:

  • 每个进程有工作集(常驻内存页集合)
  • 平衡集管理器定期调整工作集大小
  • 使用备用列表和修改列表管理页帧

8.3 性能优化技术

大页支持

c 复制代码
// 配置2MB大页
void setup_large_pages() {
    // 设置CR4.PSE=1启用大页扩展
    write_cr4(read_cr4() | CR4_PSE);
    
    // 配置页目录项为大页
    page_dir_entry *pde = &page_dir[directory_index];
    pde->page_size = 1;        // 大页模式
    pde->frame_number = large_frame_base >> 22;
}

预取优化

c 复制代码
// 硬件预取提示
void prefetch_pages(virt_addr_t start, int count) {
    for (int i = 0; i < count; i++) {
        // 预取到TLB和缓存
        __builtin_prefetch((void*)(start + i * PAGE_SIZE), 0, 3);
    }
}
总结与展望

今天我们深入探讨了:

  • 连续内存分配的策略和碎片问题
  • 分页系统的地址转换机制和性能优化
  • 分段系统的逻辑视图和保护机制
  • 段页式结合的现代内存管理实践
  • 各种内存保护技术和实际系统实现

内存管理的核心思想是:通过层次化、虚拟化的手段,在有限的物理资源上为进程提供看似无限、安全隔离的地址空间


系列导航:

  • 上一篇:[操作系统入门] 第六章:死锁------当进程陷入僵局
  • 下一篇:[操作系统入门] 第八章:内存管理(二)------虚拟内存与页面置换

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

相关推荐
云卓SKYDROID8 小时前
无人机遥控器CPU技术要点解析
系统架构·无人机·高科技·云卓科技·载荷系统
六边形架构1 天前
别再盲目地堆砌技术了!大部份大数据项目的失败,都是因为架构设计没做对!
大数据·系统架构
淡水瑜4 天前
Ignition System Architectures系统架构
系统架构
2401_861277555 天前
分层架构系统测试的主要要点
功能测试·系统架构·单元测试·集成测试·模块测试
后端小张5 天前
【AI 学习】AI Agent 开发进阶:架构、规划、记忆与工具编排
java·人工智能·ai·架构·系统架构·agent·智能体
endcy20166 天前
基于Spring AI的RAG和智能体应用实践
人工智能·ai·系统架构
武子康6 天前
Java-171 Neo4j 备份与恢复 + 预热与执行计划实战
java·开发语言·数据库·性能优化·系统架构·nosql·neo4j
en-route6 天前
软件设计九大核心原则解析
系统架构