内存虚拟化之分段、分页、段页机制

当我们运行各类程序在现代操作系统诸如Windows、linux上时,往往无需操心操作系统及硬件如何在内存上“管理”程序的代码及数据,作为一名程序员,无需担心像“我该在哪里存储这个变量”这样的问题,操作系统已经暗地帮你做好了一切,如果不是,那么将十分痛苦。

为了实现进程间内存隔离,兼顾访问效率、空间碎片等因素的影响,操作系统也相应衍生出了多种不同的内存虚拟化机制,耳熟能详的就有分段、分页、段页、多级页表等,下文针对这几种虚拟化机制进行较为详细的阐述。

在开始正文前,我们需要简单了解一下地址重定位相关知识以及逻辑地址、线性地址和物理地址之间的区别

操作系统为了实现内存虚拟化,确保应用程序只访问自己的内存空间,利用了基于硬件的地址转换技术,基于地址转换,硬件每次访问内存(指令获取、数据读取或写入),都会将程序内存引用地址重定位到内存中实际的地址。

进程虚拟地址空间.png

如图展示了一个进程16KB大小的地址空间,可以看到代码及数据都位于进程地址空间中,操作系统希望将这个地址空间放到物理内存中的其他位置进行管理,并不一定从地址0开始
内存地址空间.png

如图所示,操作系统将第一块物理内存留给了自己,并将上面进程16KB的地址空间重定位到了从32KB开始的物理内存地址处

现在的问题是,操作系统如何实现地址的重定位,程序编译链接成二进制码后,其引用的内存地址已经确定了,如何将引用的地址进行转换呢?
主要有两种方法:
早期的系统采用纯软件的重定位方式,其被称为静态重定位,使用一个加载程序(loader)接手将要运行的可执行程序,将它的地址重写到物理内存中期望的偏移位置,没有操作系统的管理,进程中的错误地址可能会导致loader重定位后对其他进程或操作系统的内存进行非法访问,且由于loader的重定位规则固定,很难将内存空间重定位到其它位置
而现代重定位方式是基于硬件的地址转换,具体来说,每个CPU需要两个硬件寄存器,基址寄存器(base)和界限寄存器(bound),进程产生的所有内存引用,都会被处理器通过下列方式转换为物理地址:physical address = virtual address + base 进程使用的虚拟地址,硬件将虚拟地址加上基址寄存器的内容得到物理地址,再对内存进行访问,界限寄存器确保了进程产生的所有地址都在进程的地址‘界限’中 ,这种在CPU中负责地址转换的部分被称为内存管理单元(MMU)

使用硬件的地址重定位,也衍生出了后来基于分段分页的内存虚拟化技术,相应的,也就有了逻辑地址、线性地址、物理地址的概念

  • 逻辑地址由两部份组成,段标识符: 段内偏移量(如cs:77000000,代码段77000000偏移),可以认为是分段机制转换前的地址,通过段描述符找到该段的线性地址基址,加上段内偏移量即可得到线性地址。

  • 线性地址可以认为是分段机制转换后、硬件页式内存的转换前的地址

  • 物理地址是线性地址经分页转换后的地址(采用分页),若不采用分页,则线性地址即物理地址(实模式下)

分段

使用基址及界限寄存器可以很好地将虚拟地址重定位到物理地址(内存)上,但是如上面进程虚拟地址空间图例展示的,在整个虚拟地址空间中,存在大块的空闲空间,如栈和堆之间的地址空间


虚拟地址.png

将整个进程的虚拟空间都重定位到内存上,将会存在大量的内部碎片,且物理内存是有限的,在32位地址寻址的机子上,每个进程可寻址的虚拟空间就有4G,基于基址加界限的方式将会导致物理内存无法提供足够的空间来放置所有进程地址空间,进程便无法运行,其灵活性较差
为了解决这个问题,分段机制应运而生,其主要做法是在MMU中引入不止一个基址跟界限寄存器对,而是给每个逻辑段一对,一个段只是地址空间一个连续定长的区域,典型的地址空间有三个逻辑段:代码、堆和栈。分段机制可以让操作系统将不同的段放到不同的物理内存地址处,通过更为细颗粒度的管理虚拟地址,避免了虚拟地址空间中未使用的部分占用内存

分段机制物理内存.png

从图中可以看到,只有已使用的内存才会在物理内存中分配空间

现在的问题是,使用分段机制,在进行地址转换时,如何判断使用哪个段寄存器、段内的偏移量是多少?

分段机制虚拟地址.png

如图所示,常见的做法是将虚拟地址分成两部分,一部分标识使用哪个段寄存器,一个标识段内偏移量
分段机制虚拟地址.png

由于有三个段,分两位标识段寄存器,剩下的标识偏移量,如图,如果前两位是00,硬件就知道这是代码段的地址,因此使用代码段的基址跟界限重定位到正确的物理地址,其转换过程伪代码如下所示
分段地址转换.png

先通过虚拟地址得到段寄存器索引,再获取段内偏移量,根据段界限寄存器判断偏移量是否在合法地址空间中,若非法,则触发段错误,否则使用段基址寄存器加偏移量得到实际物理地址,并进行访问

特殊的栈
由于栈的地址空间增长方向为高地址往低地址,所以硬件(MMU)进行地址转换时,还需要知道栈的增长方向,因此段寄存器需要使用一位来区分方向(1代表自小而大
增长,0反之)

段寄存器.png

假设要访问虚拟地址11 0100 0000 0000 硬件使用前两位指定栈段,得出段内偏移量1KB,用1KB减去段大小(2KB)得到-1KB的反向偏移量,加上基址28KB就得到了正确的物理地址27KB

支持共享
有时候在地址空间共享某些内存段是非常有用的,比如多个进程间代码段共享,共享某些段,就需要在程序每个段增加几个位,用来标识程序是否能够读写该段或执行其中的代码,这样同样的代码被共享,且不用担心其被破坏

段寄存器.png

这样,访问内存时除了要检查虚拟地址是否越界,硬件还需检查特定访问是否被允许,如果程序尝试写入只读段,或从非执行段执行指令,硬件就会触发异常,操作系统便处理出错进程

分段解决了一些问题,帮助我们实现了更高效的虚拟内存。不只是动态重定位,通过避免地址空间的逻辑段之间的大量潜在的内存浪费,分段能更好地利用物理内存空间,其支持共享可以保证多个运行的程序共享某些段如代码段不会出现问题。

但是由于段的大小不一,随着程序越来越多,物理内存会被分割成各种奇怪的大小(外部碎片),因此内存分配请求会变得更难
一种解决方法是紧凑物理内存,重写安排原有的所有段,操作系统先终止运行的进程,将他们的数据复制到连续的内存区域中,改变段寄存器的值,指向新的段基址,从而解决外部碎片,但是拷贝段是内存密集型的,会占用大量cpu处理时间

内存紧凑.png

另一种做法是利用空闲列表管理算法,试图保留大的内存块进行分配,如使用最优匹配、最坏匹配、首次匹配、伙伴算法等方式,有兴趣的同学可以继续深入了解。

分页

实际上,分段机制还是不足以支持更一般化的稀疏地址空间,对虚拟地址空间细化管理到段为单位,尽管可以使用各类机制(内存管理算法)但还是会存在外部碎片,假想有一个很大的堆,处于逻辑段中,整个堆还是需要完整地加载到内存中,因此,我们需要更一般化的虚拟机制来细化管理内存
为了解决外部碎片的问题,分片机制应运而生,其将空间分割为固定长度的分片,不再将一个进程的地址空间分割成几个长度不同的逻辑段(代码、堆、栈等),其分割后的每个单元称为一页,这时,物理内存可以看成是定长槽块的阵列,每个槽块叫做页帧,一个页帧包含一个虚拟内存页,通过这种机制,我们可以对内存进行更一般化的管理

现在的问题是,使用分页机制,如何进行地址转换,空间和时间开销如何
前文所述使用分段单位是段,通过段寄存器获取基址跟界限后进行地址转换,分页使用更一般化的管理,单位是页,那么怎么完成虚拟页到物理页的转换呢,使用寄存器这时变得不切实际,因为一个进程虚拟空间采用分页的话将产生成千上万的页,而寄存器数量是有限的,因此需要有一种新的机制完成虚拟页到物理页的转换,为了记录地址空间的每个虚拟页放在物理内存中的位置,操作系统通常为每个进程保存一个数据结构(存储于物理内存中),称为页表

抽象页表.png

分页机制.png

如图,对应VPN0->PFN3(虚拟页0->物理帧3)、VPN1->PFN7、VPN2->PFN5、VPN3->PFN2

进程的地址空间也变成了VPN标识+页内偏移量两部分组成


分页虚拟地址.png

可以看到该地址对应虚拟页VPN1,偏移为该页第5个字节处,通过页表查询物理页为PFN7,这样根据物理页跟页内偏移即可得到物理地址。下面利用分页机制访问内存流程


分页转换.png

首先根据虚拟地址获取虚拟页号VPN,通过每个进程的页表基址寄存器加上VPN页表内偏移量得到页表项地址,访问页表项,判断页表项有效位,若无效,抛出页错误,若有效,继续判断页表项保护位,若无法访问,抛出错误,否则根据虚拟地址获取的页内偏移量加上页表项的物理页地址得到完整的物理地址并访问内存。

页表中有什么
页表是一个数据结构,用于将虚拟页号映射到物理帧号,在最简单的一级分页机制中,就是一个数组,操作系统通过虚拟页号来检索数组,获取数组中的一项,我们称之为页表项(PTE),根据PTE得到PFN等内容

页表项.png

其包含一个存在位(P),确定是否允许写入该页面的读/写位(R/W) ,确定用户模式进程是否可以访问该页面的用户/超级用户位(U/S),(PWT、PCD、PAT和G)确定硬件缓存如何为这些页面工作,一个访问位(A)和一个脏位(D),最后是页帧号(PFN)本身。
其中存在位表示该页是在物理存储器还是在磁盘上(即它已被换出),读/写位(R/W)以这些位不允许的方式访问页,会陷入操作系统,脏位表明页面被带入内存后是否被修改过,有效位通常用于指示特定地址转换是否有效。当一个程序开始运行时,它的代码和堆在其地址空间的一端,栈在另一端。所有未使用的中间空间都将被标记为无效(invalid),如果进程尝试访问这种内存,就会陷入操作系统,可能会导致该进程终止。因此,有效位对于支持稀疏地址空间至关重要。通过简单地将地址空间中所有未使用的页面标记为无效,我们不再需要为这些页面分配物理帧,从而节省大量内存。
实际上页表项跟段寄存器实现功能大同小异,存储转换的目标基址地址及相应的管理单位的各类状态位

页表存在的问题
前文所述,进程的虚拟地址空间采用分页机制会产生成千上万的页,而此时页表也会变得非常大,一个典型的32位地址空间,页大小为4KB,虚拟地址这样就会分割成12位的偏移量(4KB)和20位的VPN(寻址虚拟页号),也就是说页表需要最大能寻址到20位的虚拟页号,即页表需包含2的20次方个页表项,每个页表项需要4字节(32位),那么一个进程的页表就有4MB大小,如果操作系统运行100个进程,那么内存中光页表就需要存储400MB内存,如果是64位地址寻址空间,那么页表的大小将十分恐怖

段页机制

为了解决分页机制带来的页表体积太大的问题,出现了较多的解决方案,比如使用更大的页,这样寻址页表项的压力就会变小,页表体积也相应变小(更少的页表项),但是更大的页不可避免地会产生内部碎片,大的内存页可能会只被使用一部分(总会有这样的内存页),随着系统运行,内存很快就会充满这些较大的页,因此,大多数操作系统在常见情况下使用较小的页,4KB(x86)或8KB。

混合的方法 分页和分段

段页式.png

与分页机制不同的是,我们不再为整个进程的地址空间提供单一页表(那会有很多页表项),而是每个逻辑分段提供一个,如上图,可能有三个页表,地址空间的代码、堆、栈各有一个页表,与分段机制不同的是,每一个段寄存器跟界限寄存器现在存储的不再是段在物理内存的地址跟段的偏移,而是每个逻辑分段的页表地址已经每个页表实际有效页表项数,此时的页表(每个段的页表)存储的不再是该段所有的页,而是该段所有的有效页,如上图所示的物理页PFN4、PFN10、PFN23、PFN28,对应于虚拟页VPN0(栈段内虚拟页)、VPN0(代码段内虚拟页)、VPN0(堆段内虚拟页)、VPN1(栈段内虚拟页),堆页表有效页表项数为1,栈页表有效页表项数为2,代码页表有效页表项数为1。

段页虚拟地址.png

如图,段页虚拟地址被分割成3部分,分别是段标识位,段页表项偏移,页表内偏移

段页转换.png

先根据虚拟地址获取段号跟段页表项偏移、页表内偏移,根据段号寻找段寄存器得到段页表基址,根据段页表基址加上段页表项偏移得到段页表项,最后访问页表项得到页基址加上页表内偏移得出最后的物理地址

段页机制解决了单一页表太大的问题(分成多个段页表,且仅仅保留有效的页面),但是还是没有从根本上解决页表大的问题,试想这样一种情况,存在一个大而稀疏的堆(大量内存分配、释放结果),对应的堆页表还是可能会变得很大,当系统中遍布这样的进程时,外部碎片又产生了,因此,需要有一种更好地方式来实现更小的页表

多级页表

多级页表的基本思想很简单。首先,将页表分成页大小的单元(如果页表项很多,那么内存上页表将占用较多页)。然后,使用了名为页目录(page directory)的新结构统一管理页表分成的每个单元,如果一个单元内的页表项(PTE)全部无效,就完全不分配该单元的页表。


分页机制.png

图的左边是经典的线性页表。即使地址空间的大部分中间区域无效(有较多的无效页表项),我们仍然需要为这些区域分配页表空间(即页表的中间两页)。右侧是一个多级页表。页目录仅将页表的两页标记为有效(第一个和最后一个);因此,页表的这两页就驻留在内存中(另外两页不分配内存)。因此,多级页表让线性页表的一部分消失(释放这些帧用于其他用途),并用页目录来记录页表的哪些页被分配。
在一个简单的两级页表中,页目录为每页页表包含了一项。它由多个页目录项(Page Directory Entries,PDE)组成。PDE(至少)拥有有效位(valid bit)和页帧号(page frame number,PFN),类似于PTE。但是,正如上面所暗示的,这个有效位的含义稍有不同:如果PDE项是有效的,则意味着该项指向的页表(通过PFN)中页表项至少有一项是有效的,即在该PDE所指向的页中,至少一个PTE,其有效位被设置为1。如果PDE项无效(即等于零),则PDE的其余部分没有定义。
与单一页表相比,多级页表对页表进行单元管理,若某个单元内(一页)不存在有效页表项,则在页目录内不记录该单元的物理页,且页表中也不会分配该页,页表变得紧凑(去除了部分无效的单元),并且此时页表由于被分割成一个个单元,更支持稀疏的地址空间,更容易管理,不再需要像单一页表机制那样在物理内存中寻找4MB的连续内存

多级页表转换示例

多级页表虚拟地址.png

如图展示了大小为16KB(14位)的小地址空间,页的大小64字节,因此,虚拟地址页内偏移量为6位,虚拟页号VPN有8位,即时在虚拟地址空间中只有一小部分空间被使用,线性页表(单一页表)也会有2的8次方(256)项页表项,那么,如何构建二级页表呢,首先页表的总大小为256 * 4(字节) = 1KB,一个物理页为64B,那么页表总共需要1KB/64B = 16个物理页,我们把页表分成16个单元,每个单元由页目录管理,总共有16个页目录项,需要4位寻址,而每个单元页表项也是16个(64B/4B),页目录索引+页表索引(Page-Table Index,PTIndex) = VPN
多表转换.png

页目录表索引加页表索引得到页表项,通过页表项获取页基址加上页内偏移最后得到物理地址。

多级页表存在的问题
多级页表采用的是时间换空间的做法,在TLB(快速地址转换)未命中时,需要从内存中加载至少两次,才能从页表中获取正确的地址转换信息(一次用于页目录,另一次用于页表项),而线性页表只需要一次加载,我们实现了更小的表(页表单元管理),为了节省宝贵的内存,使页表更加复杂。

分段与多级页表结合
Linux作为现代通用操作系统,使用了分页机制(X86叫保护模式,arm叫MMU机制)来对用户态与内核态进行隔离,也对进程与进程之前进行隔离。但是在X86 cpu架构下,使用分页机制前,必须打开分段机制。所以Linux采用了讨巧的办法,就是绕过分段机制,直接使用分页机制。 那Linux是怎么绕过分段机制的呢? 很简单,就是每个段都是0~4G的地址空间(相当于什么也没有做一样),剩下的管理全由分页机制来实现。所以Linux内核在启动时,自从开始使用了分页机制时,都先打开分段机制,并具这两个功能一直使用。
而windows其实也是只靠分页来隔离的,windows下虚拟地址=线性地址,其CS,DS对应的段描述符实际指向的是同一个段(平坦模型),只是使用了别名技术,顺便规定了访问权限,有兴趣的同学可以深入探索。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258