操作系统基础-内存虚拟化
Posted 云服务与SRE架构师社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统基础-内存虚拟化相关的知识,希望对你有一定的参考价值。
原文发布于微信公众号 - 云服务与SRE架构师社区(ai-cloud-ops),作者李勇。
前言
进程地址空间
-
代码段(图中0-1KB的部分),程序的二进制指令保存在这里 -
用来存放动态数据的堆(Heap,图中1-2KB的部分), malloc的内存就从这里申请,当现有堆大小不够的时候往高地址扩展 -
用来追踪函数调用的栈(Stack,途中16-15KB的部分),里面保存了每个函数调用中的局部变量,参数等信息,每层函数调用都会导致堆往低地址扩展
Base and Bounds
图2 - Base and Bounds
具体实现上,CPU上有两个寄存器用来记录这些信息:
-
Base 寄存器用来记录当前进程在物理内存的起始位置(32K) -
Bounds 寄存器用来记录该进程地址空间的边界(16K)
毫无疑问,这两个寄存器的值在上下切换的时候需要保存到PCB中。
-
这个地址空间太小,虽然我们可以放大这个地址空间(比如说640K……),但无论如何不能超过物理内存的大小 -
更严重的是,堆和栈之间有一大片分配了却没有使用的地址,地址空间越大,这里的浪费越明显
段式寻址
图3 - Segmentation
地址前两位 | 段 |
---|---|
00 | 代码 |
01 | 堆 |
10 | 栈 |
11 | 内核 |
段式寻址带来的另一个好处是,如果运行同一个程序的多个副本时,因为代码段是只读的,这些进程的代码段可以映射到同一个物理内存区域。
但是段式寻址没有解决根本问题,假如一个进程申请了一个巨大的堆,比如说1GB,然后释放了这1GB里面大部分的空间,只留下开头和结尾各1KB的空间,这同样导致的浪费。我们需要更精细的内存分配手段。
Pagetable
图4-页表
图5-物理内存
上图可以看到,进程的逻辑页0、1、3分别映射到物理页3、7、2,而逻辑页2没有使用,因此处于没有分配的状态。这里需要一个叫做valid bit的标记位来确定该页是否已经映射到物理内存。
地址翻译
-
把16383的二进表示(11111111111111,一共14个1)拆成两部分,后面12位(对应4K页大小)作为页内偏移量(offset),前2位为作为页表索引(Virtual Page Number,或VPN),转换成10进制标的话: -
offset = 4095 -
VPN = 3 -
CPU从PTBR中读取出进程的页表 -
从页表中读出第3项(即VPN指向的PTE),从图4中可以看到,它的内容是2,表示这个逻辑页对应第2个物理页。 -
最后可以计算出逻辑地址16383对应的物理地址 2(逻辑页编号) × 4K(页大小) + 4095 (offset) = 12287
Swapping
有些时候,物理内存实在放不下所有进程需要的页,这时候可以在硬盘中划分一个swap分区,把不常用的页换出(Swap out) 到swap分区中,这样物理内存能空出一部分放置别的内容。当需要访问swap分区中的内容时,再用类似的方式淘汰其他不常用的内容,在把swap分区的内容换入(Swap in)到物理内存中。
因此,PTE中其实需要一个叫做present bit的标记位,用来标记这个页对应的内容是否在物理内存中。如果preset bit为1,说明对应的页在物理内存中,PTE的内容表示对应的物理页(PFN);如果为0,说明这个页不在内存中,操作系统可以使用PTE来保存这个页在swap分区中的位置。
Page Table Entry
读者们会注意到,PTE不像图4中展示的那么简单,它至少应该包含两个标记位:
-
Valid bit: 标记该页是否已经映射到物理内存 -
Present bit:标记该页是否在物理内存中。一个页可以是Valid,但是not present的,因为它被换出去了。
PTE上通常还会有些别的标记为,来看看X86的PTE:
图6 - x86 PTE
-
present bit(P):表示页是否在内存中 -
read/write bit(P):页是否可写 -
user/supervisor(P):用户是否可访问这些页 -
PTW/PCD/PAT/G:跟硬件缓存相关 -
access bit(A):该页最近是否访问过,操作系统可以依赖这个位来制定swap的策略 -
dirty bit(D):是否有脏数据需要写回到硬盘的。为什么会有这个位?因为物理页还可以用来缓存文件或者块设备的内容,设置了direct bit的内容需要定期写回到硬盘中。 -
Page Frame Number(PFN):该页对应的物理页号。
我们可以发现:
-
这里缺少了一个valid bit,linux用别的方式实现了valid bit,如果整个PTE的内容全为0,那么这个页是未映射的。 -
跟文件系统相比,这里缺少了一个可执行权限的判断,攻击者这可以通过缓冲区溢出攻击漏洞在栈中注入可恶意代码,参考《CS:APP Attack Lab: 缓冲区溢出攻击》(https://cloud.tencent.com/developer/article/1590156)。后来x86_64中添加了禁止执行位(No-Execute bit,或NX)来解决这个问题。
有些硬件采用了讨厌的段页式的混合寻址,现代操作系统已经不用这种模式了。
Translation Lookaside Buffer
Pagetable 目前看起来很美好,但是它太慢了,每一次访问内存(包括读取代码段的指令)都额外的计算以及多一次的内存操作:
-
根据地址计算出这个地址所在页以及offset -
根据PTBR,从物理内存中读取PTE -
根据PTE和offset计算出物理地址 -
从物理地址读取实际内容
多级页表
这个问题的解决方案是使用多级页表,以一个二级页表为例,一级页表的每一项不再指向一个PTE,而是一个叫Page Directory的页;Page Directory包含多个PTE,如下图右边所示:
图7 - 线性页表(左边)和多级页表(右边)
那么多级页表是如何节省空间的呢?如果某个Page Directory中所有的PTE都没有映射,那么直接不分配这个Page Directory,并且在父页表对应的项中把present bit设置为0。
来看一个现实的例子,x86_64中采用了4级页表,每级页表包含512个PTE,每个PTE的大小是8字节,512*8正好是4K,即一个页的大小:
图8 - x86_64 四级页表
关于作者
不怎么务正业的程序员,BUG制造者、CPU0杀手。从事过开发、运维、SRE、技术支持等多个岗位。原Oracle系统架构和性能服务团队成员,目前在腾讯从事运营系统开发。
以上是关于操作系统基础-内存虚拟化的主要内容,如果未能解决你的问题,请参考以下文章
小林coding阅读笔记:操作系统篇之内存管理基础,虚拟内存分段分页