OS实战笔记(4)-- 虚拟地址和物理地址的转换

        在大学的时候,《操作系统原理》这门课我没有好好听,里面讲到过虚拟地址的相关知识。虽然在大学的时候,接过一两个项目(代码写的很烂,哈哈),偶尔也会自己练习几个编程题。但对虚拟地址、物理地址的理解基本上停留在概念上,没有认真想过为什么要整虚拟地址。

        出来工作后的第一年,由于搞嵌入式开发方面的工作,渐渐开始发现大学里欠的总是有该还回去的时候。第一年进了一家小公司,第一个任务就是bootloader,linux内核移植。那时候对于内核内存管理开始有了初步的认识,不过理解的很浅。后来在工作摸索过程中渐渐想明白了为什么需要虚拟地址这个玩意儿。

        本笔记会先从个人角度出发谈谈虚拟地址出现的原因,然后会对X86的保护模式和长模式下虚拟地址相关知识进行总结。

        码字不易,如果要转载的,麻烦帮忙注明一下转载,附上原地址帮忙推广一下,感谢(之前有一篇写HDR的文章,另外一个网站一字不差COPY也不说明,哭了)。

为什么需要虚拟地址

        要回答这个问题,我们以一个假设的场景来开始讨论,仅供娱乐,勿喷。

        假设某天你在上班途中,突然被人套上麻袋打晕。等你醒来后,你发现你被送到了一个奇怪的房间。房间不大,有一个门在你左边,房间里所有的东西几乎都是白色的,灯光、天花板、地板和墙都是白色的,你坐在一个白色椅子上,脚被镣铐链条锁在地板上。你面前有一张白色桌子以及桌上的白色的电脑。你抬头看了看正前方的天花板,发现上面还有一个显示器通过一根杆悬挂着。

        在你还在疑惑的时候,头上的显示器突然开了,一个戴着和《德州电锯杀人狂》电影中变态杀人狂面具一样的神秘人出现在了画面中。他用低沉到让人害怕的声音对你说到:“Hi, 来自高新西区软件园A区的地中海加格子衬衫码农,I want to play a game。在你面前的这台电脑,配置比较惨淡,内存只有2K,硬盘1MB,只有一个单核CPU,CPU发起的地址都是物理地址。我会给你出一些编程题,你需要在指定时间内搞定并在电脑中运行你的程序。如果任务失败,你脚下的地板会打开,然后你会掉进一个硫酸池子里被活活溶掉。如果你能完成所有任务,那么你的脚镣会自动打开,你可以从你左边的门出去,重获自由。”

        哈哈,上面扯了这么多,只是为了把脑袋里的变态画面给写出来。我们这里重点关注内存使用情况。

        神秘人告诉你,第一个任务是写一个程序A,A运行时间为5秒,A运行的时候总共占用0.5K(仅做说明用,不要较真考虑堆、栈之类的空间)。由于CPU只能访问物理地址,也只会跑一个程序,因此你决定将程序A放到内存的0地址开始的0.5K上。程序A加载到前0.5K后,运行了5秒后,正常结束。如果需要重复做程序A,只要再次将程序A放到0地址开始的位置运行即可。程序A内存使用情况示意图如下:

        你顺利地通过了第一关,神秘人说他很满意,不过让你不要高兴的太早。第二个任务是,再写一个程序B,B总运行时间也是5秒。B运行的时候也占用0.5K内存。A和B各自运行一秒钟,交替循环直到两个任务都完成。

        你想了想,这个任务也还好,你先写好了程序B,B使用的内存从0.5K的地方开始。然后又写了一个简单的调度程序C,C会一直在后台运行,负责监控A和B执行的时间,到达了一秒后就切换任务,C使用的内存你也规定为1K开始,保留了1K内存。此时内存使用情况如下图:

       但这次你遇到了点麻烦,任务A运行一秒后切换到B运行一秒后再切换回A的时候,任务A挂了。你开始变得有点着急了,赶紧开始了debug。神秘人从监视器中看到冷汗从你的额头上像雨滴打在玻璃上贴着你的脸往下流,忍不住开始对你发起了嘲讽攻击,每隔1分钟就提醒你,时间快到了哦。你的小宇宙爆发了,在还剩两分钟的时候,你找到了问题。原因是B的一个地址计算bug,导致B修改到了A的内存。你赶紧做了修改,在还剩7秒的时候,任务成功完成。你长呼了一口气,天,差点就要被硫酸给弄死了,好险。

        虽然第二个任务耗时有点长,但是你还是完成了。目前为止,似乎用物理地址直接访问内存的方式也没有什么问题,对于任务A、B、C来说,只要规划好各自使用的内存空间,在链接的时候,把程序用到的代码和数据都放到这些地址范围之内就可以解决了。

        但这里我们发现了单纯使用物理地址的一个坏处:不相关的程序的地址空间没有隔离开,任意一个程序都可以访问到所有内存,除了意外的bug外,程序如果想做坏事,就可以随便修改其他程序的内存,走自己的路,让别的程序无路可走。

        神秘人对你的表现还算满意,然后他提出了第三个任务。这次又要多一个程序D,D运行时占用0.5K。规则和前面一样,A,B,D每个程序依次运行一秒,直到最后完成所有任务。由于内存当前已经使用完,你不得不利用硬盘保存暂时没有调度的程序。你在程序C中用一个数组记录了A,B,D程序要加载到内存的位置,同时记录了当前暂未运行的保存在硬盘中的程序数据位置和大小。你使用了一张表来维护这种关系。

        这次费了很大的劲去完成了这个任务,同时我们也可以看到使用物理地址的第二个坏处:随着程序的变多,各个程序的内存使用如何规划成了一个大问题。如果这台计算机上所有程序都只由一个人管理还好,但如果计算机要使用其他人或其他公司的程序,程序又必须放到特定位置来执行。那么情况会变得混乱无比。

        让我们回到白色房间里,神秘人看到你能活这么久,很是惊讶。他逐渐对你的极限产生了极大的兴趣,他对你说:“你很不错,我这里目前为止来了611个秃头格子衫的程序员,你是第5个能撑到这里的。现在你已经过了三关了,接下来的任务和之前的加新任务不同。你需要修改程序A,让程序A能够增加1个功能,这个功能一会儿会通过屏幕发给你。其他两个程序还是和以前一样,你要注意的是,A现在需要的内存会增加到1K。现在开始,请看屏幕提示。希望你能活到最后,祝你好运。”

        你现在精神有点疲惫了,但为了活命,你的小宇宙再次爆发出了强大的伽马射线。由于A的空间有变化,A运行的时候需要1K,调度程序C也需要1K,内存中无法再放下B,D两个程序。于是你只有重新去调整了一下A,B,D的内存规划。好在第三个任务中已经有一个比较完整的内存和硬盘空间映射的机制了,这次除了增加了一点A的代码和调整了一下内存规划。你还是完成了任务。

        这里我们又看到了只能使用物理地址的第三个问题,就是如果程序被修改导致所需要的内存空间有变化,则会对内存地址的使用规划造成影响。如果被修改的程序很多很频繁,那么对于内存的规划使用来说是个灾难。

        神秘人很快又提出了第五个任务,这个任务让你开始有点抓狂了,他继续向A增加了功能,导致现在A运行需要1.5K的内存才能满足。可现在C必须使用1K,加上A已经超出了内存大小。于是你只有分段加载A,先加载1K,等到A运行到特定函数后通知C并等待C将剩下的0.5K从硬盘加载到内存后,跳到内存对应的地址上再继续A的后续工作。

        到这里,你已经感觉有点吃不消了,我们也看到了只能使用物理地址的第四个问题,当程序所需要的内存空间超出了物理内存的范围时,虽然通过分段加载运行能够解决问题,但程序的内存管理会变得异常艰难

        神秘人看到你居然能挺到这里,很意外但也很满意。这次他没有先给出下一个任务,而是问了你一个问题:“你快成功了,后面总共还有5个任务,你现在有一次机会升级你的电脑,你可以选择升级,也可以选择不升级。如果要升级,请你把你的要求告诉我,但是升级电脑会让后面的挑战多加5个。如果不升级,我会直接开始下一个任务,不会新增任务。现在,请说出你的选择。”

        作为一个混迹IT圈做嵌入式开发N年的老油条,你的大脑开始飞速运转。首先,神秘人提到后面的任务会越来越难,所要实现的程序应该也会越来越多,如果不解决上面的问题,沿用现在这种简单的机制,迟早会被玩死。你理了理思路,你现在想要的状态是:

  •         所有程序的地址空间都从0开始,实际大小可以超过物理内存大小,这个地址空间就是虚拟地址空间,和物理地址不相关。各个程序之间访问虚拟地址进行工作,相同的虚拟地址可能对应不同的物理地址,也可以对应相同的物理地址,视应用的需求而定。
  •         有一个类似内核的软件会管理物理内存的分配使用,这个软件负责根据程序的需求来合理分配内存,并且如果物理内存不够的情况下,能够完成内存和硬盘之间的数据交互过程,对于程序的运行透明。
  •         需要有一个能够翻译虚拟内存到物理内存的硬件,这个硬件要能够识别出程序访问非法地址并通知CPU发生了问题,也要能够记录程序的合法虚拟地址中哪些目前在内存上,哪些不在,访问不在内存的虚拟地址后,这个硬件要能够报告CPU。

        你想清楚后,就对神秘人说到:“我需要升级电脑,这台电脑的CPU上电后访问的是物理地址,可以通过控制寄存器切换到访问虚拟地址模式。电脑需要一个MMU做虚拟地址和物理地址的转换和权限访问管理功能。”

        神秘人觉得你果然是专业的,之前有很多人到了这里选择不升级电脑导致后面的任务根本没办法在规定时间内完成。不过他还是和你玩了一下心理战:“你确定要升级吗?如果升级电脑,挑战会增加到10个。要不这样,你如果不升级,接下来我就只再让你多做一个挑战,成功了就让你走,怎么样?”

        你犹豫了几秒,但你觉得神秘人故意这么说,肯定是有陷阱。于是你还是坚持升级电脑。神秘人没有说什么。随后一阵耀眼的白光闪过,闪到你睁不开眼睛去看究竟发生了什么。耳朵里传来了齿轮和电机的声音,白光消失后,你揉了揉眼睛看向面前。发现桌上的电脑已经变了,虽然还是白色的,但这外观看起来就和之前那台电脑不一样。

        有了这台电脑后,在重新实现了一个简单的类似OS的管理软件,加上MMU的帮助。后面神秘人的挑战你终于顺利完成。神秘人在看到你完成最后一个任务之后,对你发出了赞赏,随后按照约定让你安全地走出了房间。当房间门再次关上后不久,房间里再次传出了熟悉的声音:“Hi, 来自高新南区软件园D区的地中海加格子衬衫的小码农。I want to play a game......”。

Linux进程虚拟地址空间简介

        下面来看看一个简单的Linux进程的虚拟地址空间划分图

 (此图是很久前我发布在内核工匠的一篇文章中的,感兴趣的看这个链接

深入理解 Linux 位置无关代码 PIC_内核工匠的博客-CSDN博客_linux pic是什么)        

 虚拟地址和物理地址的转换

        上面所说的虚拟地址是如何转换到物理地址的呢?这里就涉及到了MMU这个硬件。MMU是Memory Management Unit内存管理单元的意思。下面是一张MMU工作简图:

        上图展示的是虚拟地址0x80000 - 0x84000,经过MMU查找地址转换关系表后,得出了其对应物理地址空间0x10000 - 0x14000这段空间。CPU访问0x80000 - 0x84000这段虚拟地址的时候,实际的数据放到了对应的物理地址空间上。

        目前基本上所能接触到的平台,基本都采用了一种被称为分页模型的管理方式来管理内存。虚拟地址空间和物理地址空间都被划分成了相同大小的块,这个块叫做页(page),页的大小可以配置,可以是4K, 2MB,4MB,1GB等。下图是分页模型的简图:

 X86 MMU说明

        X86 CPU要使用MMU的话,必须先要开启保护模式或长模式,实模式下不能开启MMU。

        前面有提到X86的保护模式,保护模式的内存模型是分段模型,它并不适合MMU的分页模型,因此我们使用保护模式的平坦模式来绕过分段模型。平坦模式和长模式下,都能让段基址和段长度成为虚设。X86上地址产生的过程如下:

         如果MMU处于关闭状态,这个线性地址就是物理地址。长模式下的分段由于弱化了地址空间的隔离,因此长模式下必须开启MMU。

 MMU页表

        前面有提到“地址关系转换表”,对应专业的名字叫做页表。页表描述了虚拟页和物理页的映射关系。为增加灵活性和节约物理内存,页表中并不会存放虚拟地址和物理地址的对应关系,而是存放物理页面的地址。下面以一张图来说明:

       图中说明的是四级页表的框图。

       首先,MMU当前所使用的页表所在内存地址,会通过CR3寄存器记录。当CPU发出一个虚拟地址时,MMU首先到第一级页目录中去查询第二级页目录的内存位置,然后根据第二级页目录查询第三级页目录,然后根据第三级页目录中所记录的物理地址 + 页内偏移,计算出实际要访问的物理地址。

       上面的说法还是过于模糊。我们以32 bits的地址空间,三级页表,页大小为4K,一个页目录项占4 bytes这种为例来说明。

       1. 页内偏移是多少?

        由于一个页大小是4K,因此页内偏移可以覆盖的地址范围也是4K(0x0 - 0xFFF),总共占12 bits, address[11:0]这低12 bits就是页内偏移。

       2. 一级页目录中可以放多少二级页目录表项?

           由于内存按照一页划分,因此一级页目录项总共大小也是4K,每个页目录项占4字节。总共可以存放4096 / 4 = 1024个二级页目录项。MMU查表时,address[31:22]会被取出来作为索引值index,结合CR3里基地址,用pgd来指代,来确定二级页目录地址:pdg[index]。

        3. 二级页目录项可以放多少页表项?

            和一级页目录中的分析差不多,也是1024个。MMU查表时,根据前面拿到的二级页目录项地址,用pmd来指代,结合address[21:12]作为索引index,拿到页表项的地址:pmd[index]。

        查到最后一级页表项的基地址后,MMU会拿到页表项里记录的页面基地址,记为pte,加上页内偏移address[11:0],得到实际的物理地址:pte + address[11:0]。

        下面的例子可能不太严谨,但如果能帮助理解也挺好:

        假设地址空间大小是32 bits。我们将这个地址空间看做一块土地,我们是国王,我们规定这块土地每个最小管理的单位是1个村(1 byte),每个村都被标记了一个独一无二的编号(物理地址)。当你要下命令到某个村(访问某个地址)的时候,你做了分级管理(分页),一个块叫做一个镇(页表项),每个镇管理4096个村。一个城市(二级页目录项)管理了1024个镇,一个省(一级页目录项)管理1024个城市。一共1024个省,你把所有省的信息画到了一副地图上(页表基地址)。

        因此,当你想要精细下命令到某个村的时候,过程就是,你先通过地图找到要管理的省(根据10 bits的省名字),然后在这个省下去找到对应的城市(根据10 bits城市名字),最后再在这个城市下找到对应的实际村的编号。管理上你不再通过实际的村的编号(物理地址)来查询,而是通过“省的名字 + 城市名字 + 村的名字”(虚拟地址)来查到实际的村子的编号(物理地址)。

        另外,关于虚拟地址和物理地址的关系,大家还可以用域名和IP来做类比。域名可以看做是虚拟地址,我们实际要通信的IP地址就是物理地址,DNS服务器就是MMU。

保护模式下的分页

 页大小为4KB

        三级页表结构,结构如下图。

        其中,CR3,页目录项、页表项格式如下:

页大小为4MB

        两级页表。

        CR3不再指向一级页目录,而是指向一个4KB的页表,页表项有1024个,格式如下:

长模式下的分页

页大小为4KB

        这里需要注意的一点是,虚拟地址[63:48]这16位的值是0还是1,根据虚拟地址的第47位来决定。如果47位为1,则它们都为1;如果为0则它们都为0。

  

页大小为2MB

开启MMU

         要使用分页模式,就要先开启MMU,要开启MMU,CPU必须进入保护模式或长模式。开MMU的步骤:

        1. CPU进入保护模式或长模式

        2. 在内存中准备好页表,页表的组织形式参考前面的描述。

        3. 把顶级页目录的物理地址写入CR3


mov eax, PGD_ADDR 
mov cr3, eax

        4. CR0寄存器的PE位设置为1,开启MMU


;开启 保护模式和分页模式
mov eax, cr0
bts eax, 0    ;CR0.PE =1
bts eax, 31   ;CR0.P = 1
mov cr0, eax 

 MMU异常

        MMU在工作过程中可能会碰到问题,常见的问题有页表项数据为空,用户访问没有权限的地址,向只读页面写数据等。这些问题会导致MMU异常。MMU遇到问题后,会执行以下操作:

        1. MMU停止地址转换

        2. MMU将出问题的虚拟地址写入CR2

        3. MMU触发CPU的14号中断,CPU停止执行当前指令

        4. CPU处理14号中断,检查原因并做相应处理(例如:如果是严重问题,发信号给进程或终止进程运行。如果是缺页,则申请内存页并写入MMU表项。)

猜你喜欢

转载自blog.csdn.net/vivo01/article/details/125948673#comments_30746259
今日推荐