目录
一、前景回顾
二、物理地址、线性地址和虚拟地址
三、内存为什么要分页
四、一级页表
五、二级页表
前面我们说到,保护模式下有着三大特点:地址映射、特权级和分时机制。从我的学习角度来说,我认为地址映射这一块的知识点尤为繁杂,所以会花费相对比较多的时间来讲述,话不多说,开整。
在认识地址映射之前,我们来搞懂这三个地址的含义。
物理地址就是物理内存中真正的地址,相当于内存中每一个存储单元的门牌号,具有唯一性。不管在什么模式下,CPU最终都是以物理地址去访问内存的,一定要充分认识到这一点。
在实模式下,“段基址+段内偏移地址”经过段部件的处理,直接输出的地址就是物理地址,CPU可以直接使用此地址访问内存。
而在保护模式下,“段基址+段内偏移地址”经过段部件的处理,输出的地址被称为线性地址,其实此处段基址已经不再是真正的地址了,而是段选择子,它本质上是一个索引,通过这个索引便能在GDT中找到相应的段描述符,而段基址就在段描述符中,这个内容在上一回已经提到过。得到了线性地址后,此时我们需要判断一下系统是否开启了分页机制(分页机制在下面就会提到),如果开启了分页机制,那么此时的线性地址又称为虚拟地址,虚拟地址需要通过页表映射得到真实的物理地址,这样CPU才能访问到内存。如果没有开启分页机制,此时线性地址就被认为是物理地址,将被CPU直接用来访问内存。
总结一下就是下面这张图:

浓缩一下,其实上面讲的东西就是我们常听说的MMU,它的作用其实就是地址转换。

MMU是内存管理单元,我们的每一个线性地址,通过MMU转换后,便能得到其实际对应的物理地址,MMU是硬件上提供的地址转换电路,我们不必操心。我们实际要关心的是,MMU是如何转换的呢?换句话说,应该是有一个类似表格的结构,表中每一个虚拟地址都一一对应了一个物理地址,我们提供给MMU一个虚拟地址,MMU通过该表查询,便能得到该虚拟地址对应的真实地址。实际上的确是有这么一个表格,它的名称叫做页表,页表的构建是基于分页机制的,而不是随随便便划分的页表。
一直以来我们都是在内存分段机制下运行的,目前未出问题看似良好,可是想象一下,当我们的物理内存不足时会怎么办?如下图所示:

当系统中有三个进程A、B、C在运行,物理内存还剩余15MB。此时如果进程B结束了,但是新来了一个进程D需要占用20MB+3KB的物理内存,此时由于运行环境未开启分页功能,“段基址+段内偏移”产生的线性地址就是物理地址,程序中的物理地址是连续的,就导致没有足够的内存空间供进程D使用。此时就需要将进程A的段A3或者进程C的段C1换出,具体换出哪一部分是需要参考换入换出算法,这里不深入讲解,总之只需要知道,我们需要换出一个段来腾出空间给进程D。
问题解决了,但是又没完全解决,这个方法中,如果进程的段特别大,那么换出时要将整个段全部搬运到外存,也就是硬盘上,这种IO操作太多了导致系统响应奇慢无比,令人无法接受。这个问题的本质是因为在我们的进程中,代码和数据是以段为基本单位进行存储,而每个进程的段的大小是不一致的。既然段的大小不固定,于是接下来我们做了一点改变:我们规定:页是段的更小的划分,且页的大小是固定的。目前普遍的操作系统中规定页的大小是4KB,这样一个段就可以被划分成多个页,下次再换入换出时,我们就只需要换出部分页即可,而不用将整个段换出,这样便能避免IO操作太多导致系统响应慢的问题。
可是仔细一想,这个解决方法还是有未能尽善尽美的地方,假如进程A段的A3和进程C的段C1现在都在运行,不允许换出部分页,这该如何是好?
究其本质是因为在我们没有开启分页机制时,程序中使用的线性地址就是真实的物理地址,这两个地址都是连续的。我们知道线性地址是编译器编译得出的,它必须是连续的,所以连带着物理地址也是连续的。如果有这么一个方法,让线性地址依旧连续(因为这是编译器决定的)但是让物理地址不连续,这样不就可以将内存空间中的不连续物理地址被利用起来了么?
于是分页机制就呼之欲出了,分页机制结合前面说的分页方法,将物理内存和线性内存划分为同等大小的页,一页线性内存可以对应一页真实的物理内存,这样就可以让连续的线性地址对应上不连续的物理地址。
说这么多,我们从宏观角度来看看分页机制的实现吧。

我们的CPU进入保护模式后有了4GB的寻址空间,这就是寻址空间就是指的线性地址空间,它在逻辑上是连续的。分页机制将所有段都划分为同等大小的页,与此同时,假设我们的物理内存也是4GB,我们将物理内存页划分为若干个页,虚拟地址空间的每一页通过一个映射关系就可以一一对应到物理地址空间中每一页。这个映射关系就是接下来我们要讲的页表。
前面说过分页机制可以让连续的线性地址通过某种映射关系对应上不连续的物理地址,页表就是这个映射关系。
通常来说,一页的大小是4KB,现在我们来计算一下4GB的空间可以划分为多少个页,即4GB/4KB=1M个页。也就是说4GB的空间可以容纳1048576个页,页表中自然也要有1048576个页表项。也就是我们要说的一级页表,如图所示。

由于页的大小是4KB,所以页表项中的物理地址都是4K的整数倍,用十六进制来表示的地址,其低3位都是0。页表介绍完了,具体如何使用呢?也就是如何将线性地址转换成物理地址呢?还是举一个小小的例子来说明:
在保护模式下的线性地址有32位,低12位被视为页内偏移地址,因为我们知道任何一个线性地址肯定是要落在一个物理页中。所以低12位是用于在物理页中偏移地址的。高20位用来表示页的数量,也就是用来在页表中索引物理页的。假设现在有一个线性地址为0x00001234,其地址转换过程如下图所示:

一级页表就到此为止,接下来我们看看二级页表。
前面讲述了一级页表,并以一级页表作为原型讲述了地址转换过程,既然有了一级页表为什么还要来弄一个二级页表呢?原因如下:
1、一级页表中最多容纳1M个页表项,每个页表项是4个字节(实际只需要3个字节就可以存储了,只是应用中为了方便页表的查询,便让一个页表项占用四个字节,使得每个页面刚好可以装的下1024个页表项),如果页表项全满的话,那就是4MB大小、而且还得是连续的,显然这是不现实的。
2、每个进程都有自己的页表,进程一多,光是页表占用的空间就很可观了。
归根结底,我们要解决的问题是:能否不要一次性地将全部页表项建好,而是在需要时动态创建页表项。
如何解决呢?二级页表采用了两个方法来解决这一问题:
1、对于页表所需的内存空间,采用离散分配内存的方式,以解决难以找到一块连续的大内存空间的问题。
2、只将当前所需要的部分页表项调入内存。
先来看看二级页表的模样,如图所示:

我们有一个页目录表,表中共有1024个页表项,每个页表项中记录了一个页表物理页地址,每一个页表中又记录了1024个物理页的地址,这里的每一个物理页和一级页表一样,依旧是一页4KB大小。故一个页表项能记录的内存容量为1024x4KB=4MB,一个页目录表能记录的内存容量为1024x4MB=4GB,这就达到了32位地址空间的最大容量。所以理论上每一个线性地址都能落在一个物理页中。
我们还是来看看,在二级页表下,给定一个线性地址如何通过二级页表来转换成物理地址:
1、用虚拟地址的高10位乘以4,作为页目录表的索引号,再加上页目录表的物理地址,所得到的就是页目录项的物理地址,读取该页目录项,从中获取到页表的物理地址。
2、用虚拟地址的中间10位乘以4,作为页表的索引号,再加上在第一步中得到的页表物理地址,便是页表项的物理地址,读取该页表项,从中获取到物理页的地址。
3、虚拟地址的低12位是物理页内的偏移量,用低12位加上第二步得到的物理页地址的和,便是最终转换的物理地址。
以虚拟地址0x01234567为例,其转换为物理地址的流程图如下所示:

这里说明一下,页目录表的物理地址是存放在CR3寄存器中的,后面我们设计的二级页表的页目录表物理地址也将会存放在CR3寄存器中,方便CPU调用。
熟悉了二级页表的工作原理,我们回过头来看看,为什么说二级页表能解决前面一级页表的那三个问题。这三个问题的重点就是想要说明一级页表占用的内存空间会过大,那么二级页表占用的内存空间为多少呢?我们试着来分析一下,首先,对于每个进程来说,页目录表是必不可少的,页目录表占用的内存只有4KB。而页表是可以不事先建好的,当进程有换入的请求时,假设此时进程请求从硬盘中换入4MB的数据,如果此时内存中具有空闲的4MB的内存,那么CPU就将该4MB的内存分配给该进程,我们知道,4MB刚好是1M个物理页的大小。CPU会在内存中划定一页空闲的物理页来作为这4MB内存的页表,随后将该页表的地址填入到页目录表中即可。当这4MB的数据不再需要时,CPU又可以将其换出,然后删除相应的页表和页目录表中的页表项信息。这样就实现了动态增减页表,避免页表占用过多内存的问题。
最后我们来看看页目录项和页表项的内容。

因为页目录项和页表项都是记录的物理页的地址,物理页的大小是4KB,所以地址都是4K的倍数,也就是地址的低12位都是0,所以只需要记住物理地址的高20位即可,这也就是为什么我们看图中页目录项和页表项记录的地址只有20位的原因,空出来的12位可以用来添加其他属性。
本回到此结束,下面我们将开始着手实现一个二级页表,使系统进入分页机制下运行。欲知后事如何,请看下回分解。
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende
我有一个存储主机名的Ruby数组server_names。如果我打印出来,它看起来像这样:["hostname.abc.com","hostname2.abc.com","hostname3.abc.com"]相当标准。我想要做的是获取这些服务器的IP(可能将它们存储在另一个变量中)。看起来IPSocket类可以做到这一点,但我不确定如何使用IPSocket类遍历它。如果它只是尝试像这样打印出IP:server_names.eachdo|name|IPSocket::getaddress(name)pnameend它提示我没有提供服务器名称。这是语法问题还是我没有正确使用类?输出:ge
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。 准备工作: 1、U盘一个(尽量使用8G以上的U盘)。 2、一台正常联网可使用的电脑。 3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。 4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。 U盘启动盘制作步骤: 注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc
我有一个使用SeleniumWebdriver和Nokogiri的Ruby应用程序。我想选择一个类,然后对于那个类对应的每个div,我想根据div的内容执行一个Action。例如,我正在解析以下页面:https://www.google.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=puppies这是一个搜索结果页面,我正在寻找描述中包含“Adoption”一词的第一个结果。因此机器人应该寻找带有className:"result"的div,对于每个检查它的.descriptiondiv是否包含单词“adoption
我正在我的Rails项目中安装Grape以构建RESTfulAPI。现在一些端点的操作需要身份验证,而另一些则不需要身份验证。例如,我有users端点,看起来像这样:moduleBackendmoduleV1classUsers现在如您所见,除了password/forget之外的所有操作都需要用户登录/验证。创建一个新的端点也没有意义,比如passwords并且只是删除password/forget从逻辑上讲,这个端点应该与用户资源。问题是Grapebefore过滤器没有像except,only这样的选项,我可以在其中说对某些操作应用过滤器。您通常如何干净利落地处理这种情况?