页表 订阅
页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表,PCB表中有指针指向页表。 [1] 展开全文
页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表,PCB表中有指针指向页表。 [1]
信息
中文名
页表
存储方式
基本分页存储管理方式
所属领域
操作系统
页表地址结构
逻辑地址:CPU所生成的地址。CPU产生的逻辑地址被分为 :p (页号) 它包含每个页在物理内存中的基址, 用来作为页表的索引;d (页偏移),同基址相结合,用来确定送入内存设备的物理内存地址。物理地址:内存单元所看到的地址。逻辑地址空间为2^m,且页大小为2^n,那么逻辑地址的高m-n位表示页号,低n位表示页偏移。逻辑地址空间:由程序所生成的所有逻辑地址的集合。物理地址空间:与逻辑地址相对应的内存中所有物理地址的集合,用户程序看不见真正的物理地址。注:用户只生成逻辑地址,且认为进程的地址空间为0到max。物理地址范围从R+0到R+max,R为基地址,地址映射-将程序地址空间中使用的逻辑地址变换成内存中的物理地址的过程。由内存管理单元(MMU)来完成。分页逻辑地址 =P(页号).d(页内位移)分页物理地址=f(页帧号).d(同上)P = 线性逻辑地址/页面大小d= 线性逻辑地址-P*页面大小 [1] 
收起全文
精华内容
下载资源
问答
  • 基于杂凑技术的反置页表方法的页式内存管理的模拟实现 功能要求 (1)设计非虚拟的反置页表页式存储管理模块; (2)内存空间及其划分(界面) 内存物理空间大小可选择:256M bytes,512M bytes; 每个页框的大小...
  • 页表

    2020-12-27 22:45:21
    页表 同任何缓存一样,虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM 中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,...

    虚拟内存

    页表
    同任何缓存一样,虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM 中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到 DRAM 中,替换这个牺牲页。
    这些功能是由软硬件联合提供的,包括操作系统软件、内存管理单元中的地址翻译硬件和一个存放在物理内存中叫做页表的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与 DRAM 之间来回传送页。
    下图展示了一个页表的基本组织结构。页表就是一个页表条目(PTE)的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个 PTE。为了我们的目的,我们将假设每个PTE 是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在 DRAM 中。如果设置了有效位,那么地址字段就表示 DRAM 中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。在这里插入图片描述
    上图展示了一个有 8 个虚拟页和 4 个物理页的系统的页表。四个虚拟页(VP 1、VP 2、VP 4 和 VP7)当前被缓存在 DRAM 中。两个页(VP 0 和 VP 5)还未被分配,而剩下的页(VP 3 和 VP 6)已经被分配了,但是当前为DRAM 缓存是全相联的,所以任意物理页都可以包含任意虚拟页。
    页命中
    考虑一下当 CPU 想要读包含在 VP 2 中的虚拟内存的一个字时会发生什么(如下图),VP2被缓存在 DRAM 中。地址翻译硬件将虚拟地址作为一个索引来定位 PTE 2,并从内存中读取它。因为设置了有效位,那么地址翻译硬件就知道 VP 2 是缓存在内存中的了。所以它使用 PTE 中的物理内存地址(该地址指向 PP 1 中缓存页的起始位置),构造出这个字的物理地址。在这里插入图片描述

    缺页
    在虚拟内存的习惯说法中,DRAM 缓存不命中称为缺页。下图展示了在缺页之前我们的示例页表的状态。CPU 引用了 VP 3 中的一个字,VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取 PTE 3,从有效位推断出 VP 3 未被缓存,并且触发一个缺更异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在 PP 3 中的 VP 4。如果 VP 4 已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改 VP 4 的页表条目,反映出 VP 4 不再缓存在主存中这一事实。在这里插入图片描述
    接下来,内核从磁盘复制 VP 3 到内存中的 PP 3,更新 PTE 3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP 3 已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。下图展示了在缺页之后我们的示例页表的状态。在这里插入图片描述
    虚拟内存是在 20 世纪 60 年代早期发明的,远在 CPU-内存之间差距的加大引发产生SRAM 缓存之前。因此,虚拟内存系统使用了和 SRAM缓存不同的术语,即使它们的许多概念是相似的。在虚拟内存的习惯说法中,块被称为页。在磁盘和内存之间传送页的活动叫做交换或者页面调度。页从磁盘换入(或者页面调入)DRAM 和从DRAM 换出(或者页面调出)磁盘。一直等待,直到最后时刻,也就是当有不命中发生时,才换人页面的这种策略称为按需页面调度。也可以采用其他的方法,例如尝试着预测不命中,在页面实际被引用之前就换人页面。然而,所有现代系统都使用的是按需页面调度的方式。

    展开全文
  • 嵌入式影子页表(ESPT)方法已建议有效减少此地址转换成本。 ESPT直接将GVA映射到HPA,从而避免了冗长的访客虚拟到访客物理,访客物理到主机虚拟主机和虚拟主机来托管物理地址转换。 但是,原始的ESPT工作有一些...
  • MMU是通过页表来查询虚拟地址与物理地址的映射关系。有时候,应用层需要直接访问物理地址,这时应用层就需要调用应用层mmap接口继而调用驱动层的mmap接口将希望的物理地址映射成用户态能访问的虚拟地址。由于操作...
  • 页表 页表

    千次阅读 2020-11-03 10:17:08
    页表 页表项 edgar_01112 2016-04-26 21:11:56 28031 收藏 35 分类专栏: 计算机操作系统 文章标签: 操作系统 // //操作系统和计算机组成原理里都讲到内存管理的页式管理,但是本人以及很多初次学习分页的时候,...

    页 页表 页表项

    edgar_01112 2016-04-26 21:11:56 28031 收藏 35
    分类专栏: 计算机操作系统 文章标签: 操作系统

    //
    //操作系统和计算机组成原理里都讲到内存管理的页式管理,但是本人以及很多初次学习分页的时候,都会迷茫页表大小和页表项大小之间的关系,本人仔细分析了后写了这篇blog,仅当学习交流,个人理解之用,如果有错或者分析不够严谨,欢迎指正。
    //按字、图结合起来分析,相信还是比较容易看懂。

    //一、首先明确几个概念

    逻辑地址:是程序编译后,生成的目标模块进行编址时都是从0号单元开始编址,称之为目标模块的相对地址,即为逻辑地址。

    页:将进程划分的块,对应的大小就叫页面大小。

    页框:将内存划分的块。

    页和页框二者一一对应,一个页放入一个页框,(理论上)页的大小和页框的大小相等。

    页表:就是一个页和页框一一对应的关系表。【存放在内存中】 关系表只是起到一个索引的作用,说白了就是能根据关系表能查到某一个页面和哪一个页框所对应。

    //分页管理时,将若干字节视为一页,比如4K byte。此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号。。。。页帧号也叫页帧数

    //
    //二、用例子说话【例子出现在:《王道考研操作系统》的内存管理部分】

    已知条件:逻辑地址32位、页面大小4KB、页表项大小4B,按字节编址。

    分析:

     首先 32 位的虚拟地址可表示的进程大小应该是2^32B = 4GB(暂时别去想页号P占多少位,W占多少位)             
    

    2.(根据页的定义和页面大小的定义)将进程进行分页:

    3.我们已经知道了页面的数目为:2^20页。现在的迷茫点就在于页表项的问题上。
    上图在页表上已经给出了几个数据:20位,12位,32位,2^20项。一一解释如下【请结合上图一个一个数据分析】:

    2^20项:因为页表的作用是要将页面的页框一一对应起来,所以,每一个页面在页表中都应该有一个页表项:用来表示一个页号对应一页页框号(内存中的块号),故应     
    
                    该有2^20项。【不应该有问题吧,就好像一个班有50个同学,每个人都应该有一个地址一样】
    

    20位:已经很显然了,需要表示出2^20个页表项,就至少需要20位的地址。为什么只取20位而不是21位,22位呢,本人现在还没想这个问题,就暂时定为恰好取20位即可。

    32位:已知条件里告诉了页表项大小为4B,那么自然就应该是32位了。

    12位:32位-20位 = 12位。为什么页框号地址为12位,只能表示212个页框,要小于220个页面呢,因为并不是进程的每一个页面都要调入内存。其实32位、12位、20位这三个数据还是有一定依据的,在二级分页的时候就会发现“哦,原来刚刚好”。此处暂不讨论二级分页。

    4.通过上面的分析我们得出了哪些数据:

    逻辑地址32位,进程大小:4GB。

    页面:大小4KB,数量:2^20页。

    页表项:4B,数量:220项。所以页表就需要4B*220 = 4MB的空间存储(这就是书中说:页表项大小为4MB的由来)进一步,主存的页框大小和页面大小是相等的,也为4KB,所以将页表存在主存就需要占用4MB/4KB = 1024页(因为页表也是存在主存中的,而主存也是按页框划分的。这的确是一种资源浪费,所以就需要建立二级页面,将其大小控制在1页之内,将二级页面存入主存即可)

    页 页表 页表项
    edgar_01112 2016-04-26 21:11:56 28031 收藏 35
    分类专栏: 计算机操作系统 文章标签: 操作系统

    //
    //操作系统和计算机组成原理里都讲到内存管理的页式管理,但是本人以及很多初次学习分页的时候,都会迷茫页表大小和页表项大小之间的关系,本人仔细分析了后写了这篇blog,仅当学习交流,个人理解之用,如果有错或者分析不够严谨,欢迎指正。
    //按字、图结合起来分析,相信还是比较容易看懂。

    //一、首先明确几个概念

    逻辑地址:是程序编译后,生成的目标模块进行编址时都是从0号单元开始编址,称之为目标模块的相对地址,即为逻辑地址。

    页:将进程划分的块,对应的大小就叫页面大小。

    页框:将内存划分的块。

    页和页框二者一一对应,一个页放入一个页框,(理论上)页的大小和页框的大小相等。

    页表:就是一个页和页框一一对应的关系表。【存放在内存中】 关系表只是起到一个索引的作用,说白了就是能根据关系表能查到某一个页面和哪一个页框所对应。

    //

    //
    //二、用例子说话【例子出现在:《王道考研操作系统》的内存管理部分】

    已知条件:逻辑地址32位、页面大小4KB、页表项大小4B,按字节编址。

    分析:

     首先 32 位的虚拟地址可表示的进程大小应该是2^32B = 4GB(暂时别去想页号P占多少位,W占多少位)             
    

    2.(根据页的定义和页面大小的定义)将进程进行分页:

    3.我们已经知道了页面的数目为:2^20页。现在的迷茫点就在于页表项的问题上。
    上图在页表上已经给出了几个数据:20位,12位,32位,2^20项。一一解释如下【请结合上图一个一个数据分析】:

    2^20项:因为页表的作用是要将页面的页框一一对应起来,所以,每一个页面在页表中都应该有一个页表项:用来表示一个页号对应一页页框号(内存中的块号),故应     
    
                    该有2^20项。【不应该有问题吧,就好像一个班有50个同学,每个人都应该有一个地址一样】
    

    20位:已经很显然了,需要表示出2^20个页表项,就至少需要20位的地址。为什么只取20位而不是21位,22位呢,本人现在还没想这个问题,就暂时定为恰好取20位即可。

    32位:已知条件里告诉了页表项大小为4B,那么自然就应该是32位了。

    12位:32位-20位 = 12位。为什么页框号地址为12位,只能表示212个页框,要小于220个页面呢,因为并不是进程的每一个页面都要调入内存。其实32位、12位、20位这三个数据还是有一定依据的,在二级分页的时候就会发现“哦,原来刚刚好”。此处暂不讨论二级分页。

    4.通过上面的分析我们得出了哪些数据:

    逻辑地址32位,进程大小:4GB。

    页面:大小4KB,数量:2^20页。

    页表项:4B,数量:220项。所以页表就需要4B*220 = 4MB的空间存储(这就是书中说:页表项大小为4MB的由来)进一步,主存的页框大小和页面大小是相等的,也为4KB,所以将页表存在主存就需要占用4MB/4KB = 1024页(因为页表也是存在主存中的,而主存也是按页框划分的。这的确是一种资源浪费,所以就需要建立二级页面,将其大小控制在1页之内,将二级页面存入主存即可)

    页 页表 页表项
    edgar_01112 2016-04-26 21:11:56 28031 收藏 35
    分类专栏: 计算机操作系统 文章标签: 操作系统

    //
    //操作系统和计算机组成原理里都讲到内存管理的页式管理,但是本人以及很多初次学习分页的时候,都会迷茫页表大小和页表项大小之间的关系,本人仔细分析了后写了这篇blog,仅当学习交流,个人理解之用,如果有错或者分析不够严谨,欢迎指正。
    //按字、图结合起来分析,相信还是比较容易看懂。

    //一、首先明确几个概念

    逻辑地址:是程序编译后,生成的目标模块进行编址时都是从0号单元开始编址,称之为目标模块的相对地址,即为逻辑地址。

    页:将进程划分的块,对应的大小就叫页面大小。

    页框:将内存划分的块。

    页和页框二者一一对应,一个页放入一个页框,(理论上)页的大小和页框的大小相等。

    页表:就是一个页和页框一一对应的关系表。【存放在内存中】 关系表只是起到一个索引的作用,说白了就是能根据关系表能查到某一个页面和哪一个页框所对应。

    //

    //
    //二、用例子说话【例子出现在:《王道考研操作系统》的内存管理部分】

    已知条件:逻辑地址32位、页面大小4KB、页表项大小4B,按字节编址。

    分析:

     首先 32 位的虚拟地址可表示的进程大小应该是2^32B = 4GB(暂时别去想页号P占多少位,W占多少位)             
    

    2.(根据页的定义和页面大小的定义)将进程进行分页:

    3.我们已经知道了页面的数目为:2^20页。现在的迷茫点就在于页表项的问题上。
    上图在页表上已经给出了几个数据:20位,12位,32位,2^20项。一一解释如下【请结合上图一个一个数据分析】:

    2^20项:因为页表的作用是要将页面的页框一一对应起来,所以,每一个页面在页表中都应该有一个页表项:用来表示一个页号对应一页页框号(内存中的块号),故应     
    
                    该有2^20项。【不应该有问题吧,就好像一个班有50个同学,每个人都应该有一个地址一样】
    

    20位:已经很显然了,需要表示出2^20个页表项,就至少需要20位的地址。为什么只取20位而不是21位,22位呢,本人现在还没想这个问题,就暂时定为恰好取20位即可。

    32位:已知条件里告诉了页表项大小为4B,那么自然就应该是32位了。

    12位:32位-20位 = 12位。为什么页框号地址为12位,只能表示212个页框,要小于220个页面呢,因为并不是进程的每一个页面都要调入内存。其实32位、12位、20位这三个数据还是有一定依据的,在二级分页的时候就会发现“哦,原来刚刚好”。此处暂不讨论二级分页。

    4.通过上面的分析我们得出了哪些数据:

    逻辑地址32位,进程大小:4GB。

    页面:大小4KB,数量:2^20页。

    页表项:4B,数量:220项。所以页表就需要4B*220 = 4MB的空间存储(这就是书中说:页表项大小为4MB的由来)进一步,主存的页框大小和页面大小是相等的,也为4KB,所以将页表存在主存就需要占用4MB/4KB = 1024页(因为页表也是存在主存中的,而主存也是按页框划分的。这的确是一种资源浪费,所以就需要建立二级页面,将其大小控制在1页之内,将二级页面存入主存即可)

    展开全文
  • Linux页表与ARM硬件页表说明ARM二级页表映射关系 说明 Kernel版本:4.0.0 ARM处理器,Contex-A9,QEMU平台   内核初始化arm页表的内容,low_memory映射过程,之前也大概写了。但是在arm平台中,引入了硬件页表、...

    说明

    Kernel版本:4.0.0
    ARM处理器,Contex-A9,QEMU平台
      内核初始化arm页表的内容,low_memory映射过程,之前也大概写了。但是在arm平台中,引入了硬件页表、linux页表的概念,本文描述为何要这样处理,以及内核、arm硬件页表属性进行说明。
      参考书籍《奔跑吧Linux内核:基于Linux4.x内核源代码问题分析》2.2.1 ARM32页表映射。

    ARM二级页表映射关系

      Linux中ARM32位架构采用2级页表映射方式,MMU查找地址关系如下图,画图麻烦,直接从书抄了一张过来。
    ARM MMU映射过程
      当片上TLB中不包含要求访问的虚拟地址入口是时,转换过程被启动。转换表基地址寄存器保存着第一级转换表基址的物理地址。只有bit[31:14]有效,bits[13:0]应该是0。所以第一级页表必须16KB对齐。对应的32位系统中,section大小1M,总共4096个section,每个section 4个字节,所以一级页表大小是16Kb。
      ARM MMU页表如上图所示。如果采用单层的段映射,内存中有个段映射表,表中4096个表项,每个表项的大小是4Byte,所以这个段映射表的大小是16KB。每个段对应1MB大小的地址空间。当cpu访问内存时,32位虚拟地址的高12位用于访问段映射表的索引,从表中找到相应的表项,每个表项提供了一个12位的物理段地址,以及相应的标志位,如可读、可写等标志位。将这个12位物理地址和虚拟地址的低20位拼凑在一起,就可以得到32位的物理地址。linux初始化时,内核low memory使用的大部分是段映射的方式。
      如果采用页表映射的方式,段映射表就变成一级映射表,linux内核中成为pmd。其表项提供的不再是物理段地址,而是二级页表的基地址。32位虚拟地址的高12位作为一级页表的索引值,找到相应的一级页表,每个表项指向一个二级页表。从虚拟地址的次8位作为访问二级页表的索引值,得到相应的页表项,从这个页表项中找到20位的物理页面地址。最后将这20位物理页面地址和虚拟地址的低12位拼凑在一起,得到最终的32位物理地址。这个过程在ARM32架构中由MMU硬件完成,软件不需要介入。

    Linux页表机制

      早期Linux内核是基于x86体系结构设计的,x86页表中有3个标志位是ARM32硬件页表没有的。
    PTE_DIRTY:cpu在写操作时会设置该标志位,表示对应页面被写过,为脏页。
    PTE_YOUNG:CPU访问该页时会设置该标志位。在页面换出时,如果该标志位位置了,说明该页刚被访问过,页面是young的,不适合把该页换出,同时清除该标志位。
    PTE_PRESENT:表示页在内存中。
      针对这种情况,Linux维护了一份Linux版本的页表,用于模拟这些标志位。由于一个页面是4KB大小,每个section 1MB空间,需要256个pte页表项,占用1KB,Linux版本也需要占用1KB,所以Linux创建二级页表时,以2MB为单位,申请1个PAGE存放对应的页表项。前2KB存放linux版本页表,后2KB存放硬件页表。存放地址如下。

     *    pgd             pte
     * |        |
     * +--------+
     * |        |       +------------+ +0
     * +- - - - +       | Linux pt 0 |
     * |        |       +------------+ +1024
     * +--------+ +0    | Linux pt 1 |
     * |        |-----> +------------+ +2048
     * +- - - - + +4    |  h/w pt 0  |
     * |        |-----> +------------+ +3072
     * +--------+ +8    |  h/w pt 1  |
     * |        |       +------------+ +4096
    

    在这里插入图片描述

    ARM/Linux PTE属性

    ARM硬件PTE属性

      下图表示ARM硬件二级页表属性,Linux内核使用的是Small page。在这里插入图片描述

      可以看到每个页表项中不仅包含了下级页表或者物理地址的映射地址(除了无效页表项),而且包含了诸如:TEX S SBZ AP 等属性位,这些属性位影响着 CPU 对内存的访问动作。
    C 位代表是否cache。
    B 位代表是否使能高速buffer。
    TEX C B 用来设置内存种类与缓存策略。正常内核开发时,需要关心的基本也就是是否使能cache和buffer。
    APX/AP 被称作访问权限标志位,规定了映射到的内存的访问权限:可读、可写、只读等等。
    S 标志某块内存的共享性,设置了标志位 S 的表项所映射的物理内存是可共享的,反之则是非可共享的。
    XN 代表可执行,设置了表示页面不可执行。

    内核预定义宏

      内核中预定义了一些宏,用于页表属性设置,包括Linux版本页表,以及硬件版本页表。
      先看Linux版本页表对应的宏,L开头代表Linux。由于使用2级映射,所以宏位于pgtable-2level.h。

    /*
     * "Linux" PTE definitions.
     *
     * We keep two sets of PTEs - the hardware and the linux version.
     * This allows greater flexibility in the way we map the Linux bits
     * onto the hardware tables, and allows us to have YOUNG and DIRTY
     * bits.
     *
     * The PTE table pointer refers to the hardware entries; the "Linux"
     * entries are stored 1024 bytes below.
     */
    #define L_PTE_VALID		(_AT(pteval_t, 1) << 0)		/* Valid */
    #define L_PTE_PRESENT		(_AT(pteval_t, 1) << 0)
    #define L_PTE_YOUNG		(_AT(pteval_t, 1) << 1)
    #define L_PTE_DIRTY		(_AT(pteval_t, 1) << 6)
    #define L_PTE_RDONLY		(_AT(pteval_t, 1) << 7)
    #define L_PTE_USER		(_AT(pteval_t, 1) << 8)
    #define L_PTE_XN		(_AT(pteval_t, 1) << 9)
    #define L_PTE_SHARED		(_AT(pteval_t, 1) << 10)	/* shared(v6), coherent(xsc3) */
    #define L_PTE_NONE		(_AT(pteval_t, 1) << 11)
    
    /*
     * These are the memory types, defined to be compatible with
     * pre-ARMv6 CPUs cacheable and bufferable bits: n/a,n/a,C,B
     * ARMv6+ without TEX remapping, they are a table index.
     * ARMv6+ with TEX remapping, they correspond to n/a,TEX(0),C,B
     *
     * MT type		Pre-ARMv6	ARMv6+ type / cacheable status
     * UNCACHED		Uncached	Strongly ordered
     * BUFFERABLE		Bufferable	Normal memory / non-cacheable
     * WRITETHROUGH		Writethrough	Normal memory / write through
     * WRITEBACK		Writeback	Normal memory / write back, read alloc
     * MINICACHE		Minicache	N/A
     * WRITEALLOC		Writeback	Normal memory / write back, write alloc
     * DEV_SHARED		Uncached	Device memory (shared)
     * DEV_NONSHARED	Uncached	Device memory (non-shared)
     * DEV_WC		Bufferable	Normal memory / non-cacheable
     * DEV_CACHED		Writeback	Normal memory / write back, read alloc
     * VECTORS		Variable	Normal memory / variable
     *
     * All normal memory mappings have the following properties:
     * - reads can be repeated with no side effects
     * - repeated reads return the last value written
     * - reads can fetch additional locations without side effects
     * - writes can be repeated (in certain cases) with no side effects
     * - writes can be merged before accessing the target
     * - unaligned accesses can be supported
     *
     * All device mappings have the following properties:
     * - no access speculation
     * - no repetition (eg, on return from an exception)
     * - number, order and size of accesses are maintained
     * - unaligned accesses are "unpredictable"
     */
    #define L_PTE_MT_UNCACHED	(_AT(pteval_t, 0x00) << 2)	/* 0000 */
    #define L_PTE_MT_BUFFERABLE	(_AT(pteval_t, 0x01) << 2)	/* 0001 */
    #define L_PTE_MT_WRITETHROUGH	(_AT(pteval_t, 0x02) << 2)	/* 0010 */
    #define L_PTE_MT_WRITEBACK	(_AT(pteval_t, 0x03) << 2)	/* 0011 */
    #define L_PTE_MT_MINICACHE	(_AT(pteval_t, 0x06) << 2)	/* 0110 (sa1100, xscale) */
    #define L_PTE_MT_WRITEALLOC	(_AT(pteval_t, 0x07) << 2)	/* 0111 */
    #define L_PTE_MT_DEV_SHARED	(_AT(pteval_t, 0x04) << 2)	/* 0100 */
    #define L_PTE_MT_DEV_NONSHARED	(_AT(pteval_t, 0x0c) << 2)	/* 1100 */
    #define L_PTE_MT_DEV_WC		(_AT(pteval_t, 0x09) << 2)	/* 1001 */
    #define L_PTE_MT_DEV_CACHED	(_AT(pteval_t, 0x0b) << 2)	/* 1011 */
    #define L_PTE_MT_VECTORS	(_AT(pteval_t, 0x0f) << 2)	/* 1111 */
    #define L_PTE_MT_MASK		(_AT(pteval_t, 0x0f) << 2)
    

      内核对应的宏,位于pgtable-2level-hwdef.h

    /*
     * + Level 2 descriptor (PTE)
     *   - common
     */
    #define PTE_TYPE_MASK		(_AT(pteval_t, 3) << 0)
    #define PTE_TYPE_FAULT		(_AT(pteval_t, 0) << 0)
    #define PTE_TYPE_LARGE		(_AT(pteval_t, 1) << 0)
    #define PTE_TYPE_SMALL		(_AT(pteval_t, 2) << 0)
    #define PTE_TYPE_EXT		(_AT(pteval_t, 3) << 0)		/* v5 */
    #define PTE_BUFFERABLE		(_AT(pteval_t, 1) << 2)
    #define PTE_CACHEABLE		(_AT(pteval_t, 1) << 3)
    
    /*
     *   - extended small page/tiny page
     */
    #define PTE_EXT_XN		(_AT(pteval_t, 1) << 0)		/* v6 */
    #define PTE_EXT_AP_MASK		(_AT(pteval_t, 3) << 4)
    #define PTE_EXT_AP0		(_AT(pteval_t, 1) << 4)
    #define PTE_EXT_AP1		(_AT(pteval_t, 2) << 4)
    #define PTE_EXT_AP_UNO_SRO	(_AT(pteval_t, 0) << 4)
    #define PTE_EXT_AP_UNO_SRW	(PTE_EXT_AP0)
    #define PTE_EXT_AP_URO_SRW	(PTE_EXT_AP1)
    #define PTE_EXT_AP_URW_SRW	(PTE_EXT_AP1|PTE_EXT_AP0)
    #define PTE_EXT_TEX(x)		(_AT(pteval_t, (x)) << 6)	/* v5 */
    #define PTE_EXT_APX		(_AT(pteval_t, 1) << 9)		/* v6 */
    #define PTE_EXT_COHERENT	(_AT(pteval_t, 1) << 9)		/* XScale3 */
    #define PTE_EXT_SHARED		(_AT(pteval_t, 1) << 10)	/* v6 */
    #define PTE_EXT_NG		(_AT(pteval_t, 1) << 11)	/* v6 */
    

    PTE设置函数

      ARMv7架构,set_pte_ext位于arch/arm/mm/proc-v7-2level.S中,代码实现如下:

    #define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
    

    该接口有三个参数,Linux版本pte地址,要写入页表的属性,以及额外的属性。

    ENTRY(cpu_v7_set_pte_ext)
    #ifdef CONFIG_MMU
    	str	r1, [r0]			@ linux version  @直接把pte属性写入Linux版本页表
    									
    									
    	bic	r3, r1, #0x000003f0			@r1值清除bit4~bit9存储到r3,r3作为临时变量存储硬件pte属性。
    	bic	r3, r3, #PTE_TYPE_MASK		@清除r3 bit0、bit1,没有清除bit3、bit2是因为cache、buffer这两位,hw和linux保持一致。
    	orr	r3, r3, r2					@r3=r3|r2,r2是额外属性
    	orr	r3, r3, #PTE_EXT_AP0 | 2	@设置hw bit4(AP0bit),设置hw bit1(根据上图,可以得知small page下bit1必须等于1)
    
    	tst	r1, #1 << 4                 @判断r1传入的页表属性,是否设置了bit4(tex位)
    	orrne	r3, r3, #PTE_EXT_TEX(1)	@如果linux属性设置了tex位,则硬件版本也要置位tex(hw bit6~8),两者bit偏移位置不一样。
    
    	eor	r1, r1, #L_PTE_DIRTY				@翻转r1 DIRTY位(bit6)
    	tst	r1, #L_PTE_RDONLY | L_PTE_DIRTY		@判断是否设置了只读。+
    	orrne	r3, r3, #PTE_EXT_APX			@设置硬件版本hw bit9(APX值(置1只读))
    
    	tst	r1, #L_PTE_USER						@判断传入属性是否设置L_PTE_USR
    	orrne	r3, r3, #PTE_EXT_AP1			@设置硬件版本hw bit5(AP1bit)
    
    	tst	r1, #L_PTE_XN						@判断传入属性是否设置XN(不可执行)
    	orrne	r3, r3, #PTE_EXT_XN				@设置硬件版本hw bit0(不可执行位)
    
    	tst	r1, #L_PTE_YOUNG
    	tstne	r1, #L_PTE_VALID
    	eorne	r1, r1, #L_PTE_NONE
    	tstne	r1, #L_PTE_NONE
    	moveq	r3, #0
    
     ARM(	str	r3, [r0, #2048]! )
     THUMB(	add	r0, r0, #2048 )
     THUMB(	str	r3, [r0] )
    	ALT_SMP(W(nop))
    	ALT_UP (mcr	p15, 0, r0, c7, c10, 1)		@ flush_pte
    #endif
    	bx	lr
    ENDPROC(cpu_v7_set_pte_ext)
    

      根据汇编代码,可以看到清除了linux页表属性的bit0、bit1、bit4~bit9。也即bit2、bit3 linux页表和硬件页表一致。其余bit不一样。基本注释都已在上面写了。
      DIRTY页模拟,创建映射时,设置硬件页表是只读的。当往页面写入时,会触发写权限缺页中断。(虽然Linux版本页面表标记了可写权限,但是ARM硬件表,没有写入权限。)在缺页中断handle_pte_fault中会在该页的Linux版本PTE中标记为“dirty”。

    tst	r1, #L_PTE_YOUNG
    tstne	r1, #L_PTE_VALID
    eorne	r1, r1, #L_PTE_NONE
    tstne	r1, #L_PTE_NONE
    moveq	r3, #0
    

      这部分代码,可以等同于如下:

    tst r1, @L_PTE_YOUNG
    tstne r1, #L_PTE_PRESENT
    moveq r3, #0
    

      如果没有设置L_PTE_YOUNG并且L_PTE_PRESENT置位,那么保持Linux版本页表不变,把ARM硬件版本页表项内容清0。

    展开全文
  • 当CPU拿到虚拟地址去访问实际物理地址的数据时,需要从MMU的页表查询某个虚拟地址对应的实际物理地址,在某款平台中有双核A9和ARM11两种CPU。A9的MMU页表是由linux管理和创建的,ARM11运行的是rtthreaed,MMU页表是...
  • 此示例演示如何扩展Bareflank虚拟机管理程序以使用扩展页表来捕获对页面的读取访问。 编译/用法 要设置此扩展,请运行以下命令(假定为Linux): git clone https://github.com/bareflank/hypervisor git clone ...
  • 【操作系统 xv6】页表

    2021-11-20 19:15:53
    【操作系统】页表页表概述设计页表不合理的设计方法以单表为细粒度 页表概述 设计页表的目的:实现进程之间的内存强隔离性,如果进程都直接对物理地址操作,很可能污染其他进程的内容。 实现内存隔离方式:地址空间...

    页表概述

    设计页表的目的:实现进程之间的内存强隔离性,如果进程都直接对物理地址操作,很可能污染其他进程的内容。
    实现内存隔离方式:地址空间,给内核在内的所有程序分配专属的地址空间。每个进程的虚拟地址空间都从0开始,到某个地址结束。
    实现地址空间方式:页表,在硬件中通过处理器和内存管理单元实现。CPU执行的指令中的地址都是虚拟地址,需要将虚拟地址交给MMU翻译成物理地址。MMU通过查询页表,可以将虚拟地址转换为物理地址,页表存放于内存中,因此CPU需要用一个叫做SATP的寄存器保存页表的物理地址。
    (1)转换过程:当CPU执行一条含有虚拟地址的指令时,将虚拟地址和表单的物理地址交给MMU,MMU去内存读取表单,将指令中的虚拟地址翻译成物理地址。
    (2)页表管理:每个进程的相同虚拟地址映射到不同的物理地址,显然每个进程都需要有自己的页表,从一个进程切换到另一个进程时,内核需要更新SATP寄存器的值,使其指向内存中新的页表。

    设计页表

    不合理的设计方法

    RISC-V的寄存器是64位的,能够索引的最大内存地址为2^64。注意页表是存储在内存中的,如果页表中的每个虚拟地址对应一个内存的物理地址,这需要占用大量的内存,所有的内存都被页表耗尽,这不合理。

    以page为细粒度

    我们有一张表单用于映射虚拟地址到物理地址,每个进程有一张页表,现在不要为每个虚拟地址创建一条表单条目,而是为每页(4096字节)创建一条表单条目。
    xv6虚拟地址的低39位有效,其中低12位作为一页内存的偏移量,27位作为页表项(PTE)的索引项,一个PTE 64位占8字节。因此要索引一个进程的所有虚拟地址需要2^27个PTE。
    xv6的物理地址占56位,其中低12位和虚拟地址的低12位相等,其余44位由PTE中的44位决定。
    这种单页表仍然需要占用很大内存。

    三级页表树

    物理地址按照页来划分,一页4096个字节。
    每张页表的大小为一页,即4096个字节,一个PTE8字节,因此一张页表有512个页表项。CPU的寄存器保存了第一级页表的首地址,虚拟的高27位分为3个9位,每9位索引一张页表,2^9=512。
    举例:CPU会将第一张页表的首地址+虚拟地址高9位,获取PTE,若PTE中的valid标志位有效,则该虚拟地址合法,根据PTE中的44位确定第二级页表的物理首地址;将第二级页表的物理首地址+虚拟地址的中间9位,获得第三级页表的首地址;将第三级页表首地址+低9位,获得虚拟地址所在页的首地址;将虚拟地址所在页的首地址+虚拟地址低12位偏移量,获得最终的物理地址。

    练习1 打印页表

    问题描述
    实现函数vmprint(),接收pagetable_t作为参数,按照指定格式打印信息。
    输出结果
    (1)第一行打印vmprint的参数
    (2)在这之后,每个PTE都有一行,包括引用树中更深层次的页表页的PTE。每个PTE行都用…缩进,表示它在树中的深度。每个PTE行显示其页表页中的PTE索引、PTE位以及从PTE中提取的物理地址。不要打印无效的PTE。在上面的示例中,顶级页表页具有条目0和255的映射。条目0的下一层只映射了索引0,而该索引0的底层映射了条目0、1和2。

    page table 0x0000000087f6e000
    ..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
    .. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
    .. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
    .. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
    .. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
    ..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
    .. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
    .. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
    .. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
    

    实现
    (1)在kernel/exec.c的return argc前插入: if(p->pid==1) vmprint(p->pagetable);打印第一个进程的页表。
    (2)在kernel/vm.c中添加vmprint()

    void vmprint(pagetable_t pagetable)
    {
    	printf("page table %p\n", pagetable);
    	for(int i=0; i<512; i++){
    		pte_t *pte = &pagetable[i];
    		if(*pte & PTE_V){
    			pagetable_t pt1 = (pagetable_t)PTE2PA(*pte);
    			printf("..%d: pte %p pa %p\n", i, *pte, pt1);
    			for(int j=0; j<512; j++){
    				pte_t *pte1 = &pt1[j];
    				if(*pte1 & PTE_V){
    					pagetable_t pt0 = (pagetable_t)PTE2PA(*pte1);
    					printf(".. ..%d: pte %p pa %p\n", j, *pte1, pt0);
    					for(int k=0; k<512; k++)
    						if(pt0[k] & PTE_V)
    							printf(".. .. ..%d: pte %p pa %p\n", k, pt0[k], PTE2PA(pt0[k]));
    				}
    			}
    		}
    	}
    }
    

    (3)在kernel/defs.h中添加声明

    内核页表

    在这里插入图片描述
    直接映射
    (1)物理内存:地址从0x80000000开始,到0x86400000结束。
    (2)外设IO:0x80000000以下的地址作为IO设备的地址。
    非直接映射:
    (1)trampoline页
    (2)内核栈kernel stack页:每个进程有自己的内核栈,它被映射的虚拟内存地址很大,从而在栈的下面留下一部分空间作为未映射的guard page。所以当栈溢出时,会引起异常处理,避免了污染其他内核代码。
    尽管内核使用的是高虚拟地址的栈,这些地址也能通过直接映射的虚拟地址访问到,
    权限:
    (1)trampoline和kernel text的权限为可读、可执行;
    (2)kernel data及其他部分:可读、可写

    内核栈

    内核给每个进程分配一个固定大小的内核栈,内核栈很小,通常只有1~2页,从而减少内存的消耗,而且内核也无须负担太重的栈管理任务。因此在内核栈上申请空间尽量使用动态申请的方式,不要静态申请大容量内存,否则很容易造成栈溢出。

    练习2 内核页表

    xv6处于内核态时使用内核页表,内核页表映射的虚拟地址和物理地址相等。xv6处于用户态时,每个进程有自己的页表,映射该进程的虚拟地址到物理地址。由于内核页表不包含这些映射,用户地址在内核中是无效的,因此当系统调用传递一个用户指针给内核时,内核需要将指针映射为物理地址。为了让进程陷入内核时,内核可以直接引用进程的虚拟地址,我们要给每个进程添加一个内核页表字段,并在改进程的内核页表中添加用户虚拟地址的映射关系。
    问题描述
    修改内核,使得每个进程陷入内核后使用的是它自己的那份内核页表副本。这需要修改struct proc为每个进程维护一个内核页表,修改调度器实现在切换进程时切换内核页表,切换后的进程的内核页表拷贝自当前全局内核页表。
    实现
    (1)在struct proc中添加一个内核页表字段;

    // proc.h
    ...
    enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
     
    // Per-process state
    struct proc {
     	struct spinlock lock;
     
       	// p->lock must be held when using these:
        enum procstate state;        // Process state
        struct proc *parent;         // Parent process
        void *chan;                  // If non-zero, sleeping on chan
        int killed;                  // If non-zero, have been killed
        int xstate;                  // Exit status to be returned to parent's wait
        int pid;                     // Process ID
        // these are private to the process, so p->lock need not be held.
        uint64 kstack;               // Virtual address of kernel stack
        uint64 sz;                   // Size of process memory (bytes)
        pagetable_t pagetable;       // User page table
        pagetable_t kpagetable;      // 内核页表
        struct trapframe *trapframe; // data page for trampoline.S
        struct context context;      // swtch() here to run process
        struct file *ofile[NOFILE];  // Open files
        struct inode *cwd;           // Current directory
        char name[16];               // Process name (debugging)
    };
    

    (2)阅读proc.c

    // boot时期初始化所有的进程:在内核页表上为每个进程创建内核栈,开启页表映射模式
    void procinit(void);
    
    // 返回cpu上的当前进程.
    struct proc* myproc(void);
    
    // 遍历进程表找到第一个状态为UNUSED的进程:分配pid、初始化user page
    static struct proc* allocproc(void);
    

    (3)为新进程创建内核页表

    • 创建proc_kpagetable函数:为新进程拷贝当前的内核页表
    • 在allocproc中调用proc_kpagetable。
    • 在defs.h中加入该函数的声明。
    // proc.c
    ...
    
    // 拷贝当前的内核页表
    pagetable_t proc_kpagetable(struct proc *p)
    {
    	pagetable_t kpagetable = (pagetable_t) kalloc();
        memset(kpagetable, 0, PGSIZE);
    
        // uart registers
    	if(mappages(kpagetable, UART0, PGSIZE, UART0, PTE_R | PTE_W) != 0)
      		panic("proc_cpykm");
    
        // virtio mmio disk interface
    	if(mappages(kpagetable, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W) != 0)
      		panic("proc_cpykm");
     
        // CLINT
    	if(mappages(kpagetable, CLINT, 0X10000, CLINT, PTE_R | PTE_W) != 0)
      		panic("proc_cpykm");
     
        // PLIC
    	if(mappages(kpagetable, PLIC, 0X400000, PLIC, PTE_R | PTE_W) != 0)
      		panic("proc_cpykm");
     
        // map kernel text executable and read-only.
    	if(mappages(kpagetable, KERNBASE, (uint64)etext-KERNBASE, KERNBASE, PTE_R | PTE_X) != 0)
      		panic("proc_cpykm");
    
        // map kernel data and the physical RAM we'll make use of.
    	if(mappages(kpagetable, (uint64)etext, PHYSTOP-(uint64)etext, (uint64)etext, PTE_R | PTE_W) != 0)
      		panic("proc_cpykm");
     
      // map the trampoline for trap entry/exit to
      // the highest virtual address in the kernel.
    	if(mappages(kpagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) != 0)
      		panic("proc_cpykm");
    
    	return kpagetable;
    }
    

    (4)在进程的内核页表中添加内核栈的映射

    • 在allocproc中添加功能,确保进程的内核页表有该进程的内核堆栈的映射。
    // proc.c
    ...
    
    static struct proc* allocproc(void) {
      ...
    
      // An empty user page table.
      p->pagetable = proc_pagetable(p);
      if(p->pagetable == 0){
        freeproc(p);
        release(&p->lock);
        return 0;
      }
    
      // 拷贝内核页表
      p->kpagetable = proc_kpagetable(p);
      if(p->kpagetable == 0){
    	  freeproc(p);
    	  release(&p->lock);
    	  return 0;
      }
    
      // 创建内核堆栈的映射
      pte_t * pte_kstack = walk(kernel_pagetable, p->kstack, 0);
      uint64 pa = PTE2PA(*pte_kstack);
      if(mappages(p->kpagetable, p->kstack, PGSIZE, pa, PTE_R | PTE_W) != 0)
      		panic("proc_cpykm");
      
      ...
      return p;
    }
    

    (5)scheduler()加载内核页表

    • 有可执行的进程时,将进程的内核页表加载到处理器的satp寄存器中。
    • 当没有进程运行时,应该使用kernel_pagetable。
    // proc.c
    ...
    void scheduler(void){
      struct proc *p;
      struct cpu *c = mycpu();
      
      c->proc = 0;
      for(;;){
        intr_on();
        int found = 0;
        for(p = proc; p < &proc[NPROC]; p++) {
          acquire(&p->lock);
          if(p->state == RUNNABLE) {
            p->state = RUNNING;
            c->proc = p;
    
            // 加载进程内核页表
            w_satp(MAKE_SATP(p->kpagetable));
            sfence_vma();
    
            swtch(&c->context, &p->context);
            
            // 加载内核页表
            kvminithart();
            
            c->proc = 0;
            found = 1;
          }
          release(&p->lock);
        }
        ...
    }
    

    (6)free内核页表

    • 在freeproc中释放一个进程的内核页表。
    // proc.c
    
    static void freeproc(struct proc *p)
    {
      ...   
      // free 进程的内核页表
      if(p->kpagetable)
        freeprockwalk(p->kpagetable);
      p->kpagetable = 0;
      ...  
    }
    
    • 创建函数freeprockwalk:释放页表同时,不释放物理内存页。
    // vm.c
    
    // 释放进程的内核页表
    void freeprockwalk(pagetable_t kpagetable)
    {
      // 遍历第1级页表的页表项
      for(int i = 0; i < 512; i++) {
        pte_t pte = kpagetable[i];
        if(pte & PTE_V){
          // 页表项指向第2级页表
          pagetable_t kpagetable1 = (pagetable_t)PTE2PA(pte);
    
          // 遍历第2级页表的页表项
          for(int j = 0; j < 512; j++) {
            pte_t pte1 = kpagetable1[j];
            if(pte1 & PTE_V){
              // 页表项指向第3级页表,释放第3级页表
              kfree((void*)PTE2PA(pte1));
              kpagetable1[j] = 0;
            }
          }
          kfree((void*)kpagetable1);
          kpagetable[i] = 0;
        }
      }
      kfree((void*)kpagetable);
    }
    

    改变用户页表的函数

    sbrk系统调用

    用于增加或减少进程的内存大小。

    // pseudocode
    sbrk(N){
    	if(N>0){
    		for(uint64 a = 0; a < N; a+= PGSIZE){
    			mem = kalloc(); // 分配物理内存
    			mappages(pagetable, oldsize+a, PGSIZE, mem, W|X|R|U); // 映射到用户页表
    		}
    	} else if(N<0){
    		pte = walk(pagetable, oldsize+N, 0); // 找到物理页
    		kfree(PTE2PA(pte));  // 回收物理页内存
    		pte=0;				 // 页表项置为无效
    	}
    }
    

    exec系统调用

    用于创建地址空间中的用户空间,把事先存放在文件系统的ELF文件加载到内存,回收原来的用户内存。
    namei:传入文件路径,打开文件
    ELF格式
    xv6应用文件大都是ELF格式,ELF二进制文件由ELF header和Program header组成,分别用elfhdr和proghdr结构体描述。
    (1)ELF文件由elfhdr结构体开头,最前面四个字节组成magic_num字段,表明这是一个正确的ELF文件格式。
    (2)紧跟着proghdr结构体描述了一段要被加载到内存中的代码。
    exec
    (1)初始化一个新的页表,通过uvmalloc为每个ELF代码段分配内存,通过loadseg加载代码段至内存,通过readi从文件中读取。
    (2)申请并初始化用户堆栈,将系统调用参数入栈。
    (3)提交新的页表,回收旧的用户页表

    练习3 优化copyin/copyinstr

    问题描述
    内核的copyin函数功能:翻译用户虚拟地址为实际的物理地址,读取被用户指针指向的内存。
    (1)当前版本:通过查询页表实现;
    (2)优化版本:每个进程的内核页表中添加用户页表的映射,从而使copyin和copyinstr可以直接使用用户指针。但前提条件是用户虚拟地址和内核虚拟地址不能重叠,xv6的用户虚拟地址从0开始,内核虚拟地址开始于0xC000000,因此只要用户虚拟空间小于这个值就能满足条件。
    实现
    (1)修改内核代码,防止用户进程的虚拟内存大于PLIC的地址。只有以下3个系统调用会改变进程的用户空间,其中只有sbrk会改变用户空间大小,因此只要在sbrk系统调用中限定进程大小,sbrk实际调用的是growproc函数。

    • fork:只拷贝父进程的用户空间,不会改变用户空间的大小;
    • exec:加载文件系统中的代码作为进程的新内容,我们默认它不会超出PCIL大小;
    • sbrk:会增加或减少进程的用户空间大小;
    // proc.c
    
    int growproc(int n) {
      uint sz;
      struct proc *p = myproc();
      sz = p->sz;
    
      if(sz + n >= PLIC) // (+) 限制用户内存的大小
        return -1;
    
      ...
      return 0;
    }
    

    (2)用copyin_new替换copyin,copyinstr_new替换copyinstr,在defs.h中添加函数声明

    // vm.c
    
    int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len) {
      return copyin_new(pagetable, dst, srcva, len);
    }
    
    int copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max) {
      return copyinstr_new(pagetable, dst, srcva, max);
    }
    

    (3)创建函数:addupage2kpage,在defs.h中添加声明

    • 不能直接把第三级页表项直接拷贝到内核页表,因为MMU会检测PTE_U,若该位为1,内核不能使用该物理页。因此需要把PTE_U置0。
    // proc.c
    
    // 拷贝进程的用户页表至进程内核页表
    int addupage2kpage(pagetable_t upage, pagetable_t kpage, uint64 oldsz, uint64 newsz) 
    {
      uint64 a;
      if(!upage || !kpage) // 第一级页表不存在
        return -1;
    
      if(oldsz < newsz){ // 进程用户空间增大了
        oldsz = PGROUNDUP(oldsz);
        for(a = oldsz; a < newsz; a+=PGSIZE){ // a是页对齐的虚拟地址
          pte_t *upte = walk(upage, a, 0);
    
          if(upte && (*upte & PTE_V)){
            pte_t *kpte = walk(kpage, a, 1);
            *kpte = (*upte) & (~PTE_U); // 为内核页表添加映射关系
          } else{
            return -1;
          }
        }
      } else { // 进程用户空间减小了
        oldsz = PGROUNDDOWN(oldsz);
        for(a = oldsz; a > newsz; a-=PGSIZE){ // a是页对齐的虚拟地址
          pte_t *kpte = walk(kpage, a, 0);
          if(kpte)
            *kpte = 0;
        }
      }
      return 0;
    }
    

    (4)每当内核改变进程的用户地址映射时,将变化同步到进程的内核页表中

    • fork
    // proc.c
    
    int fork(void)
    {
      int i, pid;
      struct proc *np;
      struct proc *p = myproc();
    
      ...
      np->sz = p->sz;
      np->parent = p;
    
      // 拷贝用户页表到内核页表
      addupage2kpage(np->pagetable, np->kpagetable, 0, np->sz); // (+)
    
      ...
      return pid;
    }
    
    • exec
    // exec.c
    
    int exec(char *path, char **argv)
    {
      ...
      p = myproc();
      ...
      // Commit to the user image.
      oldpagetable = p->pagetable;
      p->pagetable = pagetable;
      p->sz = sz;
      p->trapframe->epc = elf.entry;  // initial program counter = main
      p->trapframe->sp = sp; // initial stack pointer
      proc_freepagetable(oldpagetable, oldsz);
    
      // 拷贝用户页表到内核页表
      if(addupage2kpage(p->pagetable, p->kpagetable, 0, p->sz)<0)  // (+)
        goto bad;
      ...
      return -1;
    }
    
    • sbrk
    // proc.c
    
    int growproc(int n) {
      uint oldsz, sz; // +
      struct proc *p = myproc();
      oldsz = sz = p->sz; // +
    
      if(sz + n >= PLIC) // 限制用户内存的大小 +
        return -1;
    
      if(n > 0){
        if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
          return -1;
        }
      } else if(n < 0){
        sz = uvmdealloc(p->pagetable, sz, sz + n);
      }
      p->sz = sz;
    
      // 拷贝用户页表到内核页表 +
      addupage2kpage(p->pagetable, p->kpagetable, oldsz, p->sz);
      return 0;
    }
    

    (5)在userinit中将第一个进程的用户页表加入到它的内核页表中

    // proc.c
    
    void userinit(void)
    {
      struct proc *p;
      p = allocproc();
      initproc = p;
    
      ...
      p->trapframe->sp = PGSIZE;  // user stack pointer
    
      // 拷贝用户页表到内核页表
      addupage2kpage(p->pagetable, p->kpagetable, 0, p->sz); // (+)
      ...
      release(&p->lock);
    }
    

    测试结果
    在这里插入图片描述

    用户、内核空间在物理内存上的分布

    xv6用链表维护所有的物理内存kmem,链表的字段是指向下一个物理页首地址的指针,如下。每次申请物理内存时,把第一个结点拿出来使用,链表结点的物理地址从前到后依次增加。

    struct run {
      struct run *next;
    };
    
    struct {
      struct spinlock lock;
      struct run *freelist;
    } kmem;
    

    (1)系统启动时首先为内核分配物理地址,规定内核物理地址从0x0~0x86400000,即这些物理页都被使用了。
    (2)创建内核页表,为内核物理地址添加映射关系,此时为内核页表申请的物理内存地址在0x86400000之后。
    (3)为每个进程创建内核堆栈,内核堆栈的物理地址位于物理内存的最上面。
    (4)为每个进程分配物理内存,显然,内核堆栈和内核之间的空间是有限的,因此系统能创建的进程数量也是有限的,进程用户空间的物理地址位于0x86400000之后,内核堆栈之前。

    • 以上结论都可以使用vmprint函数得到验证。
    展开全文
  • 基于类虚拟化的影子页表加速方法,谈鉴锋,王瑀屏,影子页表作为内存虚拟化地址转换问题的解决方案之一,其性能开销主要源于客户机页表和影子页表不一致造成的缺页异常。实验发现,
  • Linux内核页表的建立

    2021-06-07 23:00:35
    Linux内核页表的临时映射背景初始阶段内存的使用情况 背景 由于Linux由BIOS加载后,起始阶段其实是运行在实模式,此时并没有开启分页机制。那Linux在开启分页机制之前需要先做哪些准备工作以支持分页机制?答案是页...
  • 页表管理及多级页表

    千次阅读 2019-08-02 14:17:18
    页表说需空间大小计算: 1、32 位地址空间、4KB 的页大小、页表的每项大小为 4Byte。 32 位地址空间可寻址2^32 = 4GB 每4KB=3^12为1个物理页,故需要总TLB项为2^32/2^12=2^20 每个TLB项的大小为4B,故...
  • 虚拟内存,页表,快表,多级页表,倒排页表

    千次阅读 多人点赞 2018-05-03 11:45:10
    除了虚拟页号(不是必须放在页表中的),这些域与页表中的域是一一对应的。另外还有一位用来记录这个表项 是否有效 (即是否在使用)。   如果一个进程在虚拟地址19、20和21之间有一个循环,那么可能会生成图3-12中...
  • ARMv8页表

    2020-02-18 19:23:47
    内核页表,即是0号进程(静态宏定义创建init_task, 然后演变成idle进程, comm一直是‘swapper’)。 其task_struct->mm 为空,task_struct->active_mm 为init_mm。 记住内核页表是0号进程的active_mm->...
  • 在本实验中,我们将探索页表并对其进行修改,以加快某些系统调用,并检测那些页面已被访问。 在开始编码之前,请阅读xv6 book[1]和以下相关源程序: kern/memlayout.h,定义内存的布局。 kern/vm.c,包含大多数...
  • 1.开场白 环境: 处理器架构:arm64 内核源码:linux-5.11 ubuntu版本:20.04.1 代码阅读工具:vim+ctags+cscope 通用操作系统,通常都会开启mmu来支持虚拟内存管理,而页表管理是在虚拟内存管理中尤为重要,本文...
  • 页表与虚拟内存

    2021-09-02 20:19:11
    引言   虚拟内存也叫做虚拟存储器,作为现代...一、页表   分页技术是用来管理内存空间的。分页技术可以解决内存不连续的问题,让碎片化空间最小化。 页面:分页储存管理将进程的逻辑地址空间分成若干个页,并为各
  • linux四级页表及其原理

    千次阅读 2019-11-26 17:35:22
    description: linux四级页表及其原理 一.多级页表的概念   介绍页表之前,我们先来回顾一个操作系统里的基本概念,虚拟内存。 1.1 虚拟内存   在用户的视角里,每个进程都有自己独立的地址空间,A进程的4GB和B...
  • 页表入门概念

    2021-07-20 20:58:03
    页表在分页内存管理系统的地位十分关键。 **页表的根本功能是提供从虚拟页面到物理页面的映射。**因此,页表的记录条数与虚拟页面数相同。 例如,对于32位寻址的虚拟地址,如果页面大小为4KB,则虚拟页面数最多可以...
  • ARM MMU页表框架

    2019-10-16 13:31:30
    其中第一级页表(L1)是由虚拟地址的高12bit(bits[31:20])组成,所以第一级页表有4096个item,每个item占4个字节,所以一级页表的大小为16KB,而在第一级页表中的每个entry的最低2bit可以用来区分具体是什么种类的...
  • 页表的主要作用是完成虚拟地址到物理地址的转换,更详细的介绍可以参考这个优秀的博客,很好地介绍了页表的理论。Linux如何实现这个页表理论呢?以及如何进行寻址呢?本文将会结合代码,从代码出发,基于ARM64的架构...
  • 简单的说,页表就是一个存储物理页地址的表,我们知道,现在的程序使用的都是虚拟内存,CPU在取指令或者取数据的时候使用的是虚拟地址,为了能够从内存中取得数据,需要将虚拟地址转换为物理地址 ...
  • 深入浅出内存管理--页表的创建

    千次阅读 2018-12-03 20:05:08
    页表的创建 Linux在启动过程中,要首先进行内存的初始化,那么就一定要首先创建页表。我们知道每个进程都拥有各自的进程空间,而每个进程空间又分为内核空间和用户空间。 以arm32为例,每个进程有4G的虚拟空间,其中...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 50,748
精华内容 20,299
关键字:

页表