6.2 地址空间与分页
地址空间和分页是现代操作系统内存管理的核心机制,它们让每个进程都拥有独立的、连续的、巨大的虚拟地址空间,同时高效地利用物理内存。理解虚拟内存和分页机制,是理解整个内存管理的基础。
物理地址 vs 虚拟地址
物理地址
物理地址是内存硬件的实际地址,是地址总线上传输的真实地址,对应内存芯片上的存储单元。
- 32位系统最大支持4GB物理地址空间(2^32 = 4GB)
- 64位系统理论上最大支持2^64地址空间,实际中通常只使用48位,支持256TB地址空间
早期的内存管理:直接使用物理地址
早期的计算机和简单的嵌入式系统中,程序直接使用物理地址:
- 程序加载时要分配连续的物理内存空间
- 所有程序都直接访问真实的物理内存
- 存在的问题:
- 内存空间不足:多个程序运行时,总内存需求超过物理内存大小就无法运行
- 安全问题:程序可以访问任意物理地址,恶意程序可以修改其他进程甚至操作系统的内存
- 效率低下:内存容易产生碎片,利用率低
- 重定位问题:程序每次加载的地址可能不同,需要重定位,开发复杂
为了解决这些问题,现代操作系统引入了虚拟内存机制。
虚拟内存的设计思想
虚拟内存的核心思想是:
每个进程拥有独立的、连续的、私有的虚拟地址空间,操作系统负责将虚拟地址转换为实际的物理地址,进程不需要关心物理内存的实际分配情况。
虚拟内存的优势
- 地址空间隔离:每个进程的地址空间是独立的,互相不影响,一个进程崩溃不会影响其他进程,提升了系统的稳定性和安全性
- 简化编程:程序员不需要关心物理内存的分配,程序链接时可以使用固定的地址,不需要重定位
- 内存利用率高:物理内存不需要连续分配,可以离散分配,减少碎片,允许多个进程共享物理内存页
- 扩大地址空间:可以使用比物理内存更大的地址空间,不常用的内存页可以换出到磁盘上,需要的时候再换入,相当于扩大了内存容量
分页机制
分页是实现虚拟内存最常用的方式,现在几乎所有的现代操作系统都使用分页机制。
分页的基本原理
- 把虚拟地址空间和物理地址空间都划分为固定大小的页(Page),页的大小通常是4KB,也支持2MB、1GB的大页(Huge Page)
- 虚拟地址空间的页称为虚拟页(Virtual Page, VP)
- 物理地址空间的页称为物理页(Physical Page, PP),也叫页帧(Page Frame)
- 操作系统维护一个页表(Page Table),记录虚拟页和物理页的映射关系
- 内存管理单元(MMU)硬件负责将虚拟地址转换为物理地址
地址转换过程
一个虚拟地址分为两部分:虚拟页号(VPN)和页内偏移(Offset)
- 虚拟地址 = 虚拟页号 + 页内偏移
- 物理地址 = 物理页号 + 页内偏移
- 页内偏移的位数由页大小决定,4KB页的页内偏移是12位(2^12=4096)
地址转换步骤:
- 根据虚拟地址提取虚拟页号
- 在页表中查找虚拟页号对应的物理页号
- 如果该页不在物理内存中,触发缺页异常,操作系统负责将该页从磁盘加载到物理内存,更新页表
- 将物理页号和页内偏移拼接成物理地址
- 访问物理地址对应的内存
页表
页表是存储虚拟页到物理页映射关系的表,每个进程有自己独立的页表:
- 每个页表项(PTE)除了存储物理页号外,还包含一些标志位:
- 有效位:表示该页是否在物理内存中
- 读写权限:控制该页是否可读、可写、可执行
- 用户位:控制该页是否可以被用户态访问
- 脏位:表示该页是否被修改过
- 访问位:表示该页最近是否被访问过,用于页面置换算法
多级页表
如果只有一级页表,32位系统4KB页的话,页表需要包含2^20 = 100万 个页表项,每个页表项4字节的话需要4MB,每个进程都需要4MB的页表空间,64位系统需要的空间更是大得无法接受。
为了减少页表的内存占用,现代操作系统使用多级页表:
- 把虚拟地址分为多段,每段对应一级页表的索引
- 32位系统通常用两级页表:页目录(一级) + 页表(二级)
- 64位系统通常用四级或五级页表
- 只有用到的虚拟地址区域才会分配页表项,大大节省了页表占用的内存空间
TLB(Translation Lookaside Buffer,快表)
每次地址转换都需要访问页表,而页表存在内存中,这样每次内存访问实际上需要两次内存访问(一次查页表,一次访问数据),性能会下降一半。
为了加速地址转换,CPU中集成了TLB快表,用来缓存最近使用的虚拟页号到物理页号的映射:
- TLB是一个高速的相联存储器,速度和CPU缓存差不多
- 地址转换时首先查找TLB,如果命中(TLB Hit),直接得到物理页号,不需要访问内存中的页表
- 如果TLB不命中(TLB Miss),才需要访问内存中的页表,并把映射关系缓存到TLB中
- TLB的容量通常很小,只有几十到几百个表项,但因为程序的局部性,TLB命中率通常在99%以上,大大提升了地址转换的性能
TLB优化技巧:
- 使用大页(Huge Page)可以减少TLB miss,因为一个大页覆盖更大的地址空间,同样的TLB容量可以覆盖更多的内存,适合内存密集型应用(如数据库、虚拟机)
- 尽量让内存访问具有空间局部性,提升TLB命中率
缺页异常与页面置换
当CPU访问的虚拟页不在物理内存中时,会触发缺页异常(Page Fault),操作系统的缺页异常处理程序会处理这个异常:
- 检查虚拟地址是否合法,如果不合法,发送SIGSEGV信号,段错误(Segmentation Fault)
- 如果合法,查找空闲的物理页,如果没有空闲页,调用页面置换算法选择一个要淘汰的页
- 如果要淘汰的页被修改过(脏页),先把它写回磁盘
- 从磁盘(交换分区或者文件)读取需要的页到物理内存
- 更新页表项,标记为有效
- 重新执行触发异常的指令
交换分区(Swap)
操作系统会在磁盘上划分一块区域作为交换分区,用来存储不常用的内存页。当物理内存不足时,操作系统会把不常用的页换出到交换分区,需要的时候再换入,这样可以运行比物理内存更大的程序。
- 交换分区的速度比内存慢很多,所以当系统大量使用交换分区时,性能会急剧下降
- 现在内存价格很低,服务器通常配置足够的内存,减少交换分区的使用
- 桌面系统建议配置和内存差不多大的交换分区,支持休眠等功能
页面置换算法
当物理内存不足时,需要选择一个内存页淘汰,把它换出到磁盘,页面置换算法的目标是尽可能减少页面置换的次数(降低缺页率)。
常见的页面置换算法:
- 最佳置换算法(OPT):选择未来最长时间不会被访问的页淘汰,性能最好,但无法实现,只能作为理论参考
- 先进先出算法(FIFO):选择最早进入内存的页淘汰,实现简单,但性能差,可能出现Belady异常(增加物理内存缺页率反而升高)
- 最近最少使用算法(LRU,Least Recently Used):选择最近最久没有被访问的页淘汰,性能接近OPT,是最常用的算法
- 实现LRU需要记录每个页的访问时间,硬件支持的话可以通过页表的访问位实现
- 实际系统中通常使用近似LRU算法,比如时钟算法(Clock Algorithm),性能接近LRU但实现简单
- 最不常用算法(LFU,Least Frequently Used):选择访问次数最少的页淘汰,适合访问模式比较固定的场景
颠簸(Thrashing)
当系统的物理内存严重不足时,会频繁地进行页面换入换出,大部分时间都在处理缺页异常,CPU利用率很低,这种现象称为颠簸。
- 原因:进程太多,每个进程分到的物理页太少,频繁发生缺页
- 解决方法:增加物理内存,或者减少同时运行的进程数量
思考问题
- 虚拟内存有什么好处?如果没有虚拟内存会怎么样?
- 分页机制中,为什么页大小通常是4KB?如果页太大或太小会有什么问题?
- TLB的作用是什么?为什么TLB能大大提升地址转换的性能?
- 什么是缺页异常?发生缺页异常时操作系统会做哪些处理?