精华内容
下载资源
问答
  • 分页机制

    2019-10-05 18:24:51
    本文为<x86汇编语言:从实模式到保护模式> 第16章笔记 因为段的长度不定, 在分配内存时, 可能会发生内存中的... 为了解决这个问题, 从80386处理器开始, 引入了分页机制. 分页功能从总体上来说, 是用长度固定...

    本文为<x86汇编语言:从实模式到保护模式> 第16章笔记

     

    因为段的长度不定, 在分配内存时, 可能会发生内存中的空闲区域小于要加载的段, 或者空闲区域远远大于要加载的段. 在前一种情况下, 需要另外寻找合适的空闲区域; 在后一种情况下, 分配会成功, 但太过于浪费. 为了解决这个问题, 从80386处理器开始, 引入了分页机制. 分页功能从总体上来说, 是用长度固定的页来代替长度不一定的段, 藉此解决因段长度不同而带来的内存空间管理问题. 尽管操作系统也可以用软件来实施固定长度的内存分配, 但太过于复杂, 由处理器固件来做这件事, 可以使速度和效率最大化.

    分页机制概述

    简单的分页模型

    处理器中有负责分段管理的段部件, 每个程序或任务都有自己的段, 这些段都用段描述符定义. 随着程序的执行, 当要访问内存, 就用段地址加上偏移量, 段部件就会输出一个线性地址. 在单纯的分段模式下, 线性地址就是物理地址.

    一旦决定采用页式内存管理, 就应当把4GB内存分成大小相同的页. 页的最小单位是4KB, 也就是4096字节, 用十六进制表示就是0x1000. 因此, 第一个页的物理地址就是0x00000000, 第2个页的物理地址是0x00001000, 第3个页的物理地址是0x00002000....最后一个页的物理地址是0xfffff000. 这样,  4GB内存划分为1048576(0x100000)个页. 很显然, 页的物理地址, 其低12位始终为0.

    段管理机制对于Intel处理器来说是最基本的, 任何时候都无法关闭. 也就是说, 即使启用页管理功能, 分段机制依然是起作用的, 段部件依然工作.

    如上图所示, 内存的分配设计段空间的分配和页分配. 左边是虚幻的, 或者说虚拟的4GB内存空间, 称为虚拟内存; 右边是实实在在的内存, 被分成1048576个4KB的页面(每个方框4KB, 灰色代表已分配).

    在分页模式下, 操作系统可以创建一个为所有任务公用的4GB虚拟内存空间, 也可以为每一个任务创建独立的4GB虚拟内存空间, 这都是可行的. 当一个程序加载时, 操作系统既要在左边的虚拟内存中分配段空间, 又要在右边的物理内存中分配相应的页面. 因此, 第一步骤是寻找空闲的段空间, 该段空间既没有被其他程序使用, 也没有被同一程序内的其他段使用. 比如上图, 假设已经成功找到并分配了一个段空间, 基地址为0x00200000, 长度为8200字节.

    页的最小尺寸是4KB, 也就是4096字节, 因此, 8200字节的段, 需要占用3个页面, 其中最后一个页面只用了8个字节, 其余都是浪费着, 但这无关紧要, 如果允许页共享, 多个段或多个程序可以用同一个页来存放各自的数据. 在分段之后, 操作系统的任务就是把段拆开, 并分别映射到物理页. 注意, 段必须是连续的, 但不要求所分配的页都是连续的, 挨在一起的.

    就上图中的列子来说, 该段有8200字节, 需要分配3个页面. 操作系统在物理内存中搜索可用的空闲页, 接下来, 要建立线性地址和也之间的对应关系, 在图中, 0x200000~0x00200FFF对应着物理地址为0x00002000的页, 0x00201000~0x00201FFF对应着0x00004000, 0x00202000~0x00202007对应着0x00007000的页, 当然, 这里只是示例, 线性地址区间和页的对应关系可以随意.

    4GB虚拟内存空间不可能用来保存任何数据, 因为它是虚拟的, 它只是用来指示内存的使用情况. 当操作系统加载一个程序并创建为任务时, 操作系统在虚拟内存空间寻找空闲的段, 并映射到空闲的页, 然后, 到真正开始加载程序时, 再把原本属于段的数据按页的尺寸拆开, 分开写入对应的页中.

    从段部件输出的是线性地址, 或者叫做虚拟地址. 为了根据线性地址找到页的物理地址, 操作系统必须维护一张表, 把线性地址转换成物理地址, 这是一个反过程.

    如上图所示, 因为有1048576个页, 故转换表有1048576个表项. 这是个一维表格, 每个表项占4字节, 内容为页的物理地址. 这个表格的用法是这样的: 因为页的尺寸是4KB, 故, 线性地址的低12位可用于访问页内偏移, 高20位可用于指定一个物理页. 因此, 把线性地址的高20位当成索引, 乘以4, 作为表内偏移量, 从表中取出一个双字, 那就是该线性地址做对应的页的物理地址. 举个例子: mov edx, [0x0002]    执行这条指令, 段部件用段地址0x00200000加上指令中给出的偏移量0x2002,  得到线性地址0x00200002. 线性地址的高20位是表格索引, 即0x00200, 将索引乘以4, 得到0x00800, 这就是表内偏移, 看图, 从该单元可以取出一个双字0x00007000, 这就是页物理地址. 线性地址的低12位是页内偏移, 用页物理地址加上页内偏移量, 就是最终的物理内存地址. 0x00007000加上0x0002, 得到0x00007002, 这就是实际要访问的物理内存地址. 这里有个问题, 为什么表内表内偏移为0x000800的地方, 会恰好是物理地址0x00007000,  而不是其他页地址呢? 当程序加载时, 操作系统会首先在虚拟内存中分配段, 然后, 根据段需要分成多少页, 来搜索空闲页面. 当段较大时, 要按页的尺寸分成好几个地址区段, 操作系统用每个区段的首地址, 取高20位, 乘以4, 作为偏移量访问表格, 并将分配给区段的页的物理地址写入该表项. 最后, 把原本需要写入每个区段的程序数据, 写到对应的页中. 注意了, 在页式内存管理中, 页面的管理和分配是独立的, 和分段以及段地址没有关系.操作系统所要做的, 就是寻找空闲页面, 把它分配给需要的段, 并将页的物理地址填写到映射表内.  很显然, 也很重要的结论是, 线性地址, 包括线性地址空间, 和页面分配机制没有关系.

    基于以上特点, 同时为了充分挖掘分页内存管理的潜力, 一般来说, 每个任务都可以拥有4GB的虚拟内存空间; 同时, 每个任务都有自己的4GB虚拟内存空间, 但是, 很重要的是, 在整个系统中, 物理页面是统一调配的. 考虑这样一种情景: 任务A有一个段, 基地址为0x00050000, 长度为3000自己, 系统为它分配了物理地址0x08001000的页. 过了一会, 任务B加载了, 它也有一个段, 基地址也是0x00050000, 长度为4096字节, 此时, 操作系统为它分配了另外一个不同的, 物理地址为0x00700000的页.  在这种情况下, 在任务A内访问线性地址0x00050006, 访问的其实是物理地址0x08001006; 在任务B内访问同样的线性地址时, 访问的其实是物理地址0x00700006.

    另一个问题是, 每个任务都有4GB虚拟内存空间, 而物理内存只有一个, 最大也才4GB, 根本不够分的. 事实上, 确实不够分, 但是操作系统可以暂时将不用页退避到磁盘, 调入马上要使用的页, 通过这种手段来实现分页内存管理.

    以上, 就是基本的段页式内存管理机制. 基本的段页式内存管理示意图:

    页目录, 页表和页

    我们知道, 为了完成从虚拟地址(线性地址)到物理地址的转换, 操作系统应当为每个任务准备一张页映射表. 因为任务的虚拟地址空间为4GB, 可以分出1048576个页, 所以, 映射表需要1048756个表项, 又因为每个表项4字节, 故映射表总大小为4MB. 没错, 这张表很大, 要占用相当一部分空间, 考虑到在实践中, 没有哪个任务会真的用到所有表项, 充其量只是很小一部分, 这就很浪费了.  为了解决这个问题, 处理器设计了层次化的分页结构.

    分页结构层次化的主要手段是不采用单一的映射表, 取而代之的是页目录表和页表. 如下图所示:

    首先, 因为4GB的虚拟内存空间对应着1048576个4KB页, 可以随机的抽取这些页, 将它们组织在1024个页表内, 每个页表可以容纳1024个页. 页表内的每个项目叫做页表项, 占4字节, 存放的是页的物理地址, 故每个页表的大小是4KB, 正好是一个标准页的长度. 注意, 页在页表内的分布是随机的, 哪个页位于哪个页表中, 这是没有规律的.

    如图所示, 在将1048576个页归拢到1024个页表之后, 接着, 再用一个表来指向1024个页表, 这就是页目录表(Page Directory Table: PDT), 和页表一样, 页目录项的长度为4字节, 填写的是页表的物理地址, 共指向1024个页表, 所以页目录表的大小是4KB, 正好一个标准页的长度.

    这样的层次化分页结构是每个任务都拥有的, 或者说, 每个任务都有自己的页目录和页表. 如下图所示, 在处理器中有个控制寄存器CR3, 存放着当前任务页目录的物理地址, 故又叫做页目录基址寄存器(Page Directory Base Register: PDBR). 每个任务都有自己的TSS, 其中就包括了CR3寄存器域, 存放了任务自己的页目录物理地址. 当任务切换时, 处理器切换到新任务开始执行, 而CR3寄存器的内容也被更新, 以指向新任务的页目录位置. 相应的, 页目录又指向一个个的页表, 这就使得每个任务都只在自己的地址空间内运行.  从下图可以看出, 页目录和页表也是普通的页, 混迹于全部的物理页中. 它们和普通页的不同支持仅仅在于功能不一样. 当任务撤销之后, 它们和任务所占用的普通页一样会被回收, 并分配给其他任务.

    地址变换的具体过程

    对于Intel处理器来说, 有关分页, 最简单和最基本的机制就是这些; CR3寄存器给出了页目录的物理地址; 页目录给出了所有页表的物理地址, 而每个页表给出了它所包含的页的物理地址. 好了, 该清楚的都清楚了, 唯一还不明白的, 应该是如何用这种层次性的分页结构把线性地址转换成物理地址? 这里举个例子, 某任务加载后, 在4GB虚拟地址空间创建了一个段, 起始地址为0x00800000, 段界限为0x5000, 字节粒度. 当前任务执行时, 段寄存器DS指向该段. 又假设执行了下面一条指令

    1. mov edx, [0x1050]  
    mov edx, [0x1050]

    此时, 段部件会输出线性地址0x00801050. 在没有开启分页机制时, 这就是要访问的物理地址. 但现在开启了分页机制, 所以这是一个下虚拟地址, 要经过页部件转换, 才能得到物理地址.

     

    如下图所示, 处理器的页部件专门负责线性地址到物理地址的转换工作. 它首先将段部件送来的32位线性地址分为3段, 分别是高10位, 中间10位, 低12位. 高10位是页目录的索引, 中间10位是页表的索引, 低12位则作为页内偏移量来用.

    当前任务页目录的物理地址在处理器的CR3寄存器中, 假设它的内容为0x00005000. 段管理部件输出的线性地址是0x00801050, 其二进制的形式如图中给出. 高10位是十六进制的0x002, 它是页目录表内的索引, 处理器将它乘以4(因为每个目录项4字节), 作为偏移量访问页目录. 最终处理器从物理地址00005008处取得页表的物理地址0x08001000.

    线性地址的中间10位为0x001, 处理器用它作为页表索引取得页的物理地址. 将该值乘以4, 作为偏移量访问页表. 最终, 处理器又从物理地址08001004处取得页的物理地址, 这就是我们一直努力寻找的那个页.

    页的物理地址是0x0000c000, 而线性地址的低12位是数据所在的业内偏移量. 故处理器将它们相加, 得到物理地址0x0000C050, 这就是线性地址0x00801050所对应的物理地址, 要访问的数据就在这里.

    注意, 这种变换不是无缘无故的, 而是事先安排好的. 当任务加载时, 操作系统先创建虚拟的段, 并根据段地址的高20位决定它要用到哪些页目录项和页表项. 然后, 寻找空闲的也, 将原本应该写入段中的数据写到一个或者多个页中, 并将页的物理地址填写到相对应的页表项中. 只有这样做了, 当程序运行的时候, 才能以相反的顺序进行地址变换, 并找到正确的数据.

    页目录项, 页表项, CR3和打开分页

    页目录项和页表项

    页目录和页表中分别存放为页目录项和页表项, 它们的格式如下:

    可以看出, 在页目录和页表中, 只保存了页表或者页物理地址的高20位. 原因很简单, 页表或者页的物理地址, 都要求必须是4KB对齐的, 以便于放在一个页内, 故其低12位全是0. 在这种情况下, 可以只关心其高20位, 低12位安排其他用途.

    • P 是存在位, 为1时, 表示页表或者页位于内存中. 否则, 表示页表或者页不在内存中, 必须先予以创建, 或者从磁盘调入内存后方可使用.
    • RW 是读/写位. 为0时表示这样的页只能读取, 为1时可读可写
    • US 是用户/管理位. 为1时, 允许所有特权级别的程序访问; 为0时, 只允许特权级别为0, 1和2的程序访问.
    • PWT(Page-level Write-Through) 是页级通写位, 和高速缓存有关. "通写"是处理器高速缓存的一种工作方式, 这一位用来间接决定是否采用此种方式来改善页面的访问效率.
    • PCD(Page-level Cache Disable)是页级高速缓存禁止位, 用来间接决定该表项所指向的那个页是否使用高速缓存策略.
    • A 是访问位. 该位由处理器固件设置, 用来指示此表项所指向的页是否被访问过.
    • D(Dirty) 是脏位. 该位由处理器固件设置, 用来指示此表项所指向的页是否写过数据
    • PAT(Page Attribute Table) 页属性表支持位. 此位涉及更复杂的分页系统, 和页高速缓存有关, 可以不予理会, 在普通的4KB分页机制中, 处理器建议将其置0.
    • G 是全局位. 用来指示该表项所指向的页是否为全局性质的. 如果页是全局的, 那么, 它将在高速缓存中一直保存(也就意味着地址转换速度会很快). 因为页高速缓存容量有限, 只能存放频繁使用的那些表项. 而且, 当因任务切换等原因改变CR3寄存器的内容时, 整个页高速缓存的内容都会被刷新.
    • AVL位卑处理器忽略, 软件可以使用.

    CR3(PDBR)和开分页机制

    控制寄存器CR3, 也就是页目录表基地址寄存器PDBR, 该寄存器如上图所示.

    由于页目录表必须位于一个自然页内(4KB对齐), 故其物理地址的低12位是全0. 低12位除了PCD和PWT外, 都没有使用. 这两位用于控制页目录的高速缓存特性, 参见上面解释.

    控制寄存器CR0的最高位PG位, 用于开启分页或者关闭页功能. 当该位清0时, 页功能关闭, 从段部件来的线性地址就是物理地址. 当它置位时, 页功能开启. 只能在保护模式下才能开启分页功能, 当PE位清0时(实模式), 设置PG位将导致处理器产生一个异常中断.

    转载于:https://www.cnblogs.com/Acg-Check/p/4269488.html

    展开全文
  • linux分页机制

    2019-09-26 16:50:14
    2.分页机制实际是把所有的段基址都设置为0,很显然如果关闭分页机制那么线性地址就是物理地址如果开启了就不能是直接的对应关系了。 3.假设我们不知道分页机制怎么做,猜测一下他必须实现的功能有1.隔离不同进程...

    1.分页的机制废弃了分段的设计,之前我们在博客中说过分段模式,即每一个进程有自己的段描述符,记录了自己的各个段基址。分页机制实际上废弃了这种设计模式。

     
    2.分页机制实际是把所有的段基址都设置为0,很显然如果关闭分页机制那么线性地址就是物理地址如果开启了就不能是直接的对应关系了。
     
    3.假设我们不知道分页机制怎么做,猜测一下他必须实现的功能有1.隔离不同进程的相同地址值,也就是说同一个线性地址在不同进程中会被映射到不同的物理地址中。2.实现线性地址向物理地址的映射。
     
    4.我们先说一下分页的设计原理。首先我们把线性地址以4kb的大小切分,同样也把物理地址按照4kb的大小进行切分,是不是想到了磁盘分块?是的道理差不多的,我们只需要把线性地址对应的块对应到物理地址就ok了。对应的方式有很多种我们只说一下一级分页和二级分页。
     
    5.一级分页。我们现在内存弄一块连续的空间(页表),多大呢?线性地址有4G(2^32个内存单元),以4KB(2^12)切分能切分为2^20个单元(页表项),每一个单元有32位。我们得到一个线性地址32位,首先拿到高20位作为这段内存的索引,得到一个32位单元,这32位存放着物理地址单元的基地址,有意思的是由于线性地址和物理地址都是以4kb做切分所以,分开的这些单元首地址最低12位肯定都是0,所以我们的最低12位不会存为0而是存一些该页面的属性比如权限神马的。线性地址高20为索引在页表中得到页表项,页表项的前20位再连接上线性地址的后12位得到一个物理地址,这么看线性地址的后12位可以看做偏移地址了。关于该页的其他属性在页表项的后12位存储着。
     
    6.为了让进程编程地址独立,每一个进程都有自己的页表,与3中提及的一样,那么每一个进程的页表基址怎么得到呢,老办法用寄存器呗。
     
    7.一级页表有两个遗憾,1页表在内存分配必须连续,不然怎么索引啊2每一个进程的页表必须都是实实在在占用4M的,必须全部分配完毕,进程才可用奥。
     
    8.为了解决这两个遗憾人们开始用二级分页。一级分页中线性地址被分为两部分高20位索引页表项取得物理地址基地址,低12位物理地址偏移地址。在二级分页中线性地址被分为3部分,高10位,中10位,低20位。具体的名称就不多说了,因为说了反而会觉着混乱。高10位用于在一张2^10大的表中获得一个地址,这个地址又指向一个内存表,这个内存表很明显了也是2^10大小,这个内存表中存放物理地址的高20位,连接上线性地址的低12获得物理地址。你会问了尼玛这一点没有节约啊,妙处在于当一级表被索引的时候,索引到的单元指向的第二张表才会被动态分配。二级表很明显也不用在内存连续分配。
     
    参考 http://blog.163.com/wmk_2000_ren/blog/static/1388461922010228101742134/

    转载于:https://www.cnblogs.com/SimpleISP/p/5280312.html

    展开全文
  • 页面的分页机制

    2020-11-24 14:47:44
    <div><p>我看了一下这个页面的分页机制,实际上是进行内存分页,并不是使用异步加载分页机制。这里是有开关控制吗</p><p>该提问来源于开源项目:MuYunyun/reactSPA</p></div>
  • 1 linux的分页机制 1.1 四级分页机制 前面我们提到Linux内核仅使用了较少的分段机制,但是却对分页机制的依赖性很强,其使用一种适合32位和64位结构的通用分页模型,该模型使用四级分页机制,即 页全局目录(Page ...

    1 linux的分页机制

    1.1 四级分页机制

    前面我们提到Linux内核仅使用了较少的分段机制,但是却对分页机制的依赖性很强,其使用一种适合32位和64位结构的通用分页模型,该模型使用四级分页机制,即

    • 页全局目录(Page Global Directory)
    • 页上级目录(Page Upper Directory)
    • 页中间目录(Page Middle Directory)
    • 页表(Page Table)
    • 页全局目录包含若干页上级目录的地址;
    • 页上级目录又依次包含若干页中间目录的地址;
    • 而页中间目录又包含若干页表的地址;
    • 每一个页表项指向一个页框。

      因此线性地址因此被分成五个部分,而每一部分的大小与具体的计算机体系结构有关。

    1.2 不同架构的分页机制

    对于不同的体系结构,Linux采用的四级页表目录的大小有所不同:对于i386而言,仅采用二级页表,即页上层目录和页中层目录长度为0;对于启用PAE的i386,采用了三级页表,即页上层目录长度为0;对于64位体系结构,可以采用三级或四级页表,具体选择由硬件决定。

    对于没有启用物理地址扩展的32位系统,两级页表已经足够了。从本质上说Linux通过使“页上级目录”位和“页中间目录”位全为0,彻底取消了页上级目录和页中间目录字段。不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为1,并把这两个目录项映射到页全局目录的一个合适的目录项而实现的。

    启用了物理地址扩展的32 位系统使用了三级页表。Linux 的页全局目录对应80x86 的页目录指针表(PDPT),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。

    最终,64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分。

    1.3 为什么linux热衷:分页>分段

    那么,为什么Linux是如此地热衷使用分页技术而对分段机制表现得那么地冷淡呢,因为Linux的进程处理很大程度上依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目标变得可行:

    • 给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。
    • 区别页(即一组数据)和页框(即主存中的物理地址)之不同。这就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又被装在不同的页框中。这就是虚拟内存机制的基本要素。

    每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时,Linux把cr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。因此,当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。

    把线性地址映射到物理地址虽然有点复杂,但现在已经成了一种机械式的任务。

    2 linux中页表处理数据结构

    2.1 页表类型定义pgd_t、pmd_t、pud_t和pte_t

    Linux分别采用pgd_tpmd_tpud_tpte_t四种数据结构来表示页全局目录项、页上级目录项、页中间目录项和页表项。这四种 数据结构本质上都是无符号长整型unsigned long

    Linux为了更严格数据类型检查,将无符号长整型unsigned long分别封装成四种不同的页表项。如果不采用这种方法,那么一个无符号长整型数据可以传入任何一个与四种页表相关的函数或宏中,这将大大降低程序的健壮性。

    pgprot_t是另一个64位(PAE激活时)或32位(PAE禁用时)的数据类型,它表示与一个单独表项相关的保护标志。

    首先我们查看一下子这些类型是如何定义的

    2.1.1 pteval_t,pmdval_t,pudval_t,pgdval_t

    参照arch/x86/include/asm/pgtable_64_types.h

    #ifndef __ASSEMBLY__
    #include <linux/types.h>
    
    /*
     * These are used to make use of C type-checking..
     */
    typedef unsigned long   pteval_t;
    typedef unsigned long   pmdval_t;
    typedef unsigned long   pudval_t;
    typedef unsigned long   pgdval_t;
    typedef unsigned long   pgprotval_t;
    
    typedef struct { pteval_t pte; } pte_t;
    
    #endif  /* !__ASSEMBLY__ */

    2.1.2 pgd_t、pmd_t、pud_t和pte_t

    参照 /arch/x86/include/asm/pgtable_types.h

    typedef struct { pgdval_t pgd; } pgd_t;
    
    static inline pgd_t native_make_pgd(pgdval_t val)
    {
            return (pgd_t) { val };
    }
    
    static inline pgdval_t native_pgd_val(pgd_t pgd)
    {
            return pgd.pgd;
    }
    
    static inline pgdval_t pgd_flags(pgd_t pgd)
    {
            return native_pgd_val(pgd) & PTE_FLAGS_MASK;
    }
    
    #if CONFIG_PGTABLE_LEVELS > 3
    typedef struct { pudval_t pud; } pud_t;
    
    static inline pud_t native_make_pud(pmdval_t val)
    {
            return (pud_t) { val };
    }
    
    static inline pudval_t native_pud_val(pud_t pud)
    {
            return pud.pud;
    }
    #else
    #include <asm-generic/pgtable-nopud.h>
    
    static inline pudval_t native_pud_val(pud_t pud)
    {
            return native_pgd_val(pud.pgd);
    }
    #endif
    
    #if CONFIG_PGTABLE_LEVELS > 2
    typedef struct { pmdval_t pmd; } pmd_t;
    
    static inline pmd_t native_make_pmd(pmdval_t val)
    {
            return (pmd_t) { val };
    }
    
    static inline pmdval_t native_pmd_val(pmd_t pmd)
    {
            return pmd.pmd;
    }
    #else
    #include <asm-generic/pgtable-nopmd.h>
    
    static inline pmdval_t native_pmd_val(pmd_t pmd)
    {
            return native_pgd_val(pmd.pud.pgd);
    }
    #endif
    
    static inline pudval_t pud_pfn_mask(pud_t pud)
    {
            if (native_pud_val(pud) & _PAGE_PSE)
                    return PHYSICAL_PUD_PAGE_MASK;
            else
                    return PTE_PFN_MASK;
    }
    
    static inline pudval_t pud_flags_mask(pud_t pud)
    {
            return ~pud_pfn_mask(pud);
    }
    
    static inline pudval_t pud_flags(pud_t pud)
    {
            return native_pud_val(pud) & pud_flags_mask(pud);
    }
    
    static inline pmdval_t pmd_pfn_mask(pmd_t pmd)
    {
            if (native_pmd_val(pmd) & _PAGE_PSE)
                    return PHYSICAL_PMD_PAGE_MASK;
            else
                    return PTE_PFN_MASK;
    }
    
    static inline pmdval_t pmd_flags_mask(pmd_t pmd)
    {
            return ~pmd_pfn_mask(pmd);
    }
    
    static inline pmdval_t pmd_flags(pmd_t pmd)
    {
            return native_pmd_val(pmd) & pmd_flags_mask(pmd);
    }
    
    static inline pte_t native_make_pte(pteval_t val)
    {
            return (pte_t) { .pte = val };
    }
    
    static inline pteval_t native_pte_val(pte_t pte)
    {
            return pte.pte;
    }
    
    static inline pteval_t pte_flags(pte_t pte)
    {
            return native_pte_val(pte) & PTE_FLAGS_MASK;
    }

    2.1.3 xxx_val和__xxx

    参照/arch/x86/include/asm/pgtable.h

    五个类型转换宏(_ pte、_ pmd、_ pud、_ pgd和__ pgprot)把一个无符号整数转换成所需的类型。

    另外的五个类型转换宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。

    #define pgd_val(x)      native_pgd_val(x)
    #define __pgd(x)        native_make_pgd(x)
    
    #ifndef __PAGETABLE_PUD_FOLDED
    #define pud_val(x)      native_pud_val(x)
    #define __pud(x)        native_make_pud(x)
    #endif
    
    #ifndef __PAGETABLE_PMD_FOLDED
    #define pmd_val(x)      native_pmd_val(x)
    #define __pmd(x)        native_make_pmd(x)
    #endif
    
    #define pte_val(x)      native_pte_val(x)
    #define __pte(x)        native_make_pte(x)

    这里需要区别指向页表项的指针和页表项所代表的数据。以pgd_t类型为例子,如果已知一个pgd_t类型的指针pgd,那么通过pgd_val(*pgd)即可获得该页表项(也就是一个无符号长整型数据),这里利用了面向对象的思想。

    2.2 页表描述宏

    参照arch/x86/include/asm/pgtable_64

    linux中使用下列宏简化了页表处理,对于每一级页表都使用有以下三个关键描述宏:

    宏字段 描述
    XXX_SHIFT 指定Offset字段的位数
    XXX_SIZE 页的大小
    XXX_MASK 用以屏蔽Offset字段的所有位。

    我们的四级页表,对应的宏分别由PAGE,PMD,PUD,PGDIR

    宏字段前缀 描述
    PGDIR 页全局目录(Page Global Directory)
    PUD 页上级目录(Page Upper Directory)
    PMD 页中间目录(Page Middle Directory)
    PAGE 页表(Page Table)

    2.2.1 PAGE宏–页表(Page Table)

    字段 描述
    PAGE_SHIFT 指定Offset字段的位数
    PAGE_SIZE 页的大小
    PAGE_MASK 用以屏蔽Offset字段的所有位。

    定义如下,在/arch/x86/include/asm/page_types.h文件中

    /* PAGE_SHIFT determines the page size */
     #define PAGE_SHIFT      12
     #define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)
     #define PAGE_MASK       (~(PAGE_SIZE-1))

    当用于80x86处理器时,PAGE_SHIFT返回的值为12。

    由于页内所有地址都必须放在Offset字段, 因此80x86系统的页的大小PAGE_SIZE是2^12=4096字节。

    PAGE_MASK宏产生的值为0xfffff000,用以屏蔽Offset字段的所有位。

    2.2.2 PMD-Page Middle Directory (页目录)

    字段 描述
    PMD_SHIFT 指定线性地址的Offset和Table字段的总位数;换句话说,是页中间目录项可以映射的区域大小的对数
    PMD_SIZE 用于计算由页中间目录的一个单独表项所映射的区域大小,也就是一个页表的大小
    PMD_MASK 用于屏蔽Offset字段与Table字段的所有位

    当PAE 被禁用时,PMD_SHIFT 产生的值为22(来自Offset 的12 位加上来自Table 的10 位),
    PMD_SIZE 产生的值为222 或 4 MB,
    PMD_MASK产生的值为 0xffc00000。

    相反,当PAE被激活时,
    PMD_SHIFT 产生的值为21 (来自Offset的12位加上来自Table的9位),
    PMD_SIZE 产生的值为2^21 或2 MB
    PMD_MASK产生的值为 0xffe00000。

    大型页不使用最后一级页表,所以产生大型页尺寸的LARGE_PAGE_SIZE 宏等于PMD_SIZE(2PMD_SHIFT),而在大型页地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK。

    2.2.3 PUD_SHIFT-页上级目录(Page Upper Directory)

    字段 描述
    PUD_SHIFT 确定页上级目录项能映射的区域大小的位数
    PUD_SIZE 用于计算页全局目录中的一个单独表项所能映射的区域大小。
    PUD_MASK 用于屏蔽Offset字段,Table字段,Middle Air字段和Upper Air字段的所有位

    在80x86处理器上,PUD_SHIFT总是等价于PMD_SHIFT,而PUD_SIZE则等于4MB或2MB。

    2.2.4 PGDIR_SHIFT-页全局目录(Page Global Directory)

    字段 描述
    PGDIR_SHIFT 确定页全局页目录项能映射的区域大小的位数
    PGDIR_SIZE 用于计算页全局目录中一个单独表项所能映射区域的大小
    PGDIR_MASK 用于屏蔽Offset, Table,Middle Air及Upper Air的所有位

    当PAE 被禁止时,
    PGDIR_SHIFT 产生的值为22(与PMD_SHIFT 和PUD_SHIFT 产生的值相同),
    PGDIR_SIZE 产生的值为 222 或 4 MB,
    PGDIR_MASK 产生的值为 0xffc00000。

    相反,当PAE被激活时,
    PGDIR_SHIFT 产生的值为30 (12 位Offset 加 9 位Table再加 9位 Middle Air),
    PGDIR_SIZE 产生的值为230 或 1 GB
    PGDIR_MASK产生的值为0xc0000000

    PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD

    用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024。当PAE被激活时,产生的值分别为512,512,1和4。

    2.3 页表处理函数

    [注意]

    以下内容主要参见 深入理解linux内核第二章内存寻址中页表处理

    内核还提供了许多宏和函数用于读或修改页表表项:

    • 如果相应的表项值为0,那么,宏pte_none、pmd_none、pud_none和 pgd_none产生的值为1,否则产生的值为0。
    • 宏pte_clear、pmd_clear、pud_clear和 pgd_clear清除相应页表的一个表项,由此禁止进程使用由该页表项映射的线性地址。ptep_get_and_clear( )函数清除一个页表项并返回前一个值。
    • set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值。set_pte_atomic与set_pte作用相同,但是当PAE被激活时它同样能保证64位的值能被原子地写入。
    • 如果a和b两个页表项指向同一页并且指定相同访问优先级,pte_same(a,b)返回1,否则返回0。
    • 如果页中间目录项指向一个大型页(2MB或4MB),pmd_large(e)返回1,否则返回0。

    宏pmd_bad由函数使用并通过输入参数传递来检查页中间目录项。如果目录项指向一个不能使用的页表,也就是说,如果至少出现以下条件中的一个,则这个宏产生的值为1:

    • 页不在主存中(Present标志被清除)。

    • 页只允许读访问(Read/Write标志被清除)。

    • Acessed或者Dirty位被清除(对于每个现有的页表,Linux总是
      强制设置这些标志)。

    pud_bad宏和pgd_bad宏总是产生0。没有定义pte_bad宏,因为页表项引用一个不在主存中的页,一个不可写的页或一个根本无法访问的页都是合法的。

    如果一个页表项的Present标志或者Page Size标志等于1,则pte_present宏产生的值为1,否则为0。

    前面讲过页表项的Page Size标志对微处理器的分页部件来讲没有意义,然而,对于当前在主存中却又没有读、写或执行权限的页,内核将其Present和Page Size分别标记为0和1。

    这样,任何试图对此类页的访问都会引起一个缺页异常,因为页的Present标志被清0,而内核可以通过检查Page Size的值来检测到产生异常并不是因为缺页。

    如果相应表项的Present标志等于1,也就是说,如果对应的页或页表被装载入主存,pmd_present宏产生的值为1。pud_present宏和pgd_present宏产生的值总是1。

    2.3.1 查询页表项中任意一个标志的当前值

    下表中列出的函数用来查询页表项中任意一个标志的当前值;除了pte_file()外,其他函数只有在pte_present返回1的时候,才能正常返回页表项中任意一个标志。

    函数名称 说明
    pte_user( ) 读 User/Supervisor 标志
    pte_read( ) 读 User/Supervisor 标志(表示 80x86 处理器上的页不受读的保护)
    pte_write( ) 读 Read/Write 标志
    pte_exec( ) 读 User/Supervisor 标志( 80x86 处理器上的页不受代码执行的保护)
    pte_dirty( ) 读 Dirty 标志
    pte_young( ) 读 Accessed 标志
    pte_file( ) 读 Dirty 标志(当 Present 标志被清除而 Dirty 标志被设置时,页属于一个非线性磁盘文件映射)

    2.3.2 设置页表项中各标志的值

    下表列出的另一组函数用于设置页表项中各标志的值

    函数名称 说明
    mk_pte_huge( ) 设置页表项中的 Page Size 和 Present 标志
    pte_wrprotect( ) 清除 Read/Write 标志
    pte_rdprotect( ) 清除 User/Supervisor 标志
    pte_exprotect( ) 清除 User/Supervisor 标志
    pte_mkwrite( ) 设置 Read/Write 标志
    pte_mkread( ) 设置 User/Supervisor 标志
    pte_mkexec( ) 设置 User/Supervisor 标志
    pte_mkclean( ) 清除 Dirty 标志
    pte_mkdirty( ) 设置 Dirty 标志
    pte_mkold( ) 清除 Accessed 标志(把此页标记为未访问)
    pte_mkyoung( ) 设置 Accessed 标志(把此页标记为访问过)
    pte_modify(p,v) 把页表项 p 的所有访问权限设置为指定的值
    ptep_set_wrprotect() 与 pte_wrprotect( ) 类似,但作用于指向页表项的指针
    ptep_set_access_flags( ) 如果 Dirty 标志被设置为 1 则将页的访问权设置为指定的值,并调用flush_tlb_page() 函数
    ptep_mkdirty() 与 pte_mkdirty( ) 类似,但作用于指向页表项的指针。
    ptep_test_and_clear_dirty( ) 与 pte_mkclean( ) 类似,但作用于指向页表项的指针并返回 Dirty 标志的旧值
    ptep_test_and_clear_young( ) 与 pte_mkold( ) 类似,但作用于指向页表项的指针并返回 Accessed标志的旧值

    2.3.3 宏函数-把一个页地址和一组保护标志组合成页表项,或者执行相反的操作

    现在,我们来讨论下表中列出的宏,它们把一个页地址和一组保护标志组合成页表项,或者执行相反的操作,从一个页表项中提取出页地址。请注意这其中的一些宏对页的引用是通过 “页描述符”的线性地址,而不是通过该页本身的线性地址。

    宏名称 说明
    pgd_index(addr) 找到线性地址 addr 对应的的目录项在页全局目录中的索引(相对位置)
    pgd_offset(mm, addr) 接收内存描述符地址 mm 和线性地址 addr 作为参数。这个宏产生地址addr 在页全局目录中相应表项的线性地址;通过内存描述符 mm 内的一个指针可以找到这个页全局目录
    pgd_offset_k(addr) 产生主内核页全局目录中的某个项的线性地址,该项对应于地址 addr
    pgd_page(pgd) 通过页全局目录项 pgd 产生页上级目录所在页框的页描述符地址。在两级或三级分页系统中,该宏等价于 pud_page() ,后者应用于页上级目录项
    pud_offset(pgd, addr) 参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址
    pud_page(pud) 通过页上级目录项 pud 产生相应的页中间目录的线性地址。在两级分页系统中,该宏等价于 pmd_page() ,后者应用于页中间目录项
    pmd_index(addr) 产生线性地址 addr 在页中间目录中所对应目录项的索引(相对位置)
    pmd_offset(pud, addr) 接收指向页上级目录项的指针 pud 和线性地址 addr 作为参数。这个宏产生目录项 addr 在页中间目录中的偏移地址。在两级或三级分页系统中,它产生 pud ,即页全局目录项的地址
    pmd_page(pmd) 通过页中间目录项 pmd 产生相应页表的页描述符地址。在两级或三级分页系统中, pmd 实际上是页全局目录中的一项
    mk_pte(p,prot) 接收页描述符地址 p 和一组访问权限 prot 作为参数,并创建相应的页表项
    pte_index(addr) 产生线性地址 addr 对应的表项在页表中的索引(相对位置)
    pte_offset_kernel(dir,addr) 线性地址 addr 在页中间目录 dir 中有一个对应的项,该宏就产生这个对应项,即页表的线性地址。另外,该宏只在主内核页表上使用
    pte_offset_map(dir, addr) 接收指向一个页中间目录项的指针 dir 和线性地址 addr 作为参数,它产生与线性地址 addr 相对应的页表项的线性地址。如果页表被保存在高端存储器中,那么内核建立一个临时内核映射,并用 pte_unmap 对它进行释放。 pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它们使用不同的临时内核映射
    pte_page( x ) 返回页表项 x 所引用页的描述符地址
    pte_to_pgoff( pte ) 从一个页表项的 pte 字段内容中提取出文件偏移量,这个偏移量对应着一个非线性文件内存映射所在的页
    pgoff_to_pte(offset ) 为非线性文件内存映射所在的页创建对应页表项的内容

    2.3.4 简化页表项的创建和撤消

    下面我们罗列最后一组函数来简化页表项的创建和撤消。当使用两级页表时,创建或删除一个页中间目录项是不重要的。如本节前部分所述,页中间目录仅含有一个指向下属页表的目录项。所以,页中间目录项只是页全局目录中的一项而已。然而当处理页表时,创建一个页表项可能很复杂,因为包含页表项的那个页表可能就不存在。在这样的情况下,有必要分配一个新页框,把它填写为 0 ,并把这个表项加入。

    如果 PAE 被激活,内核使用三级页表。当内核创建一个新的页全局目录时,同时也分配四个相应的页中间目录;只有当父页全局目录被释放时,这四个页中间目录才得以释放。当使用两级或三级分页时,页上级目录项总是被映射为页全局目录中的一个单独项。与以往一样,下表中列出的函数描述是针对 80x86 构架的。

    函数名称 说明
    pgd_alloc( mm ) 分配一个新的页全局目录。如果 PAE 被激活,它还分配三个对应用户态线性地址的子页中间目录。参数 mm( 内存描述符的地址 )在 80x86 构架上被忽略
    pgd_free( pgd) 释放页全局目录中地址为 pgd 的项。如果 PAE 被激活,它还将释放用户态线性地址对应的三个页中间目录
    pud_alloc(mm, pgd, addr) 在两级或三级分页系统下,这个函数什么也不做:它仅仅返回页全局目录项 pgd 的线性地址
    pud_free(x) 在两级或三级分页系统下,这个宏什么也不做
    pmd_alloc(mm, pud, addr) 定义这个函数以使普通三级分页系统可以为线性地址 addr 分配一个新的页中间目录。如果 PAE 未被激活,这个函数只是返回输入参数 pud 的值,也就是说,返回页全局目录中目录项的地址。如果 PAE 被激活,该函数返回线性地址 addr 对应的页中间目录项的线性地址。参数 mm 被忽略
    pmd_free(x) 该函数什么也不做,因为页中间目录的分配和释放是随同它们的父全局目录一同进行的
    pte_alloc_map(mm, pmd, addr) 接收页中间目录项的地址 pmd 和线性地址 addr 作为参数,并返回与 addr 对应的页表项的地址。如果页中间目录项为空,该函数通过调用函数 pte_alloc_one( ) 分配一个新页表。如果分配了一个新页表, addr 对应的项就被创建,同时 User/Supervisor 标志被设置为 1 。如果页表被保存在高端内存,则内核建立一个临时内核映射,并用 pte_unmap 对它进行释放
    pte_alloc_kernel(mm, pmd, addr) 如果与地址 addr 相关的页中间目录项 pmd 为空,该函数分配一个新页表。然后返回与 addr 相关的页表项的线性地址。该函数仅被主内核页表使用
    pte_free(pte) 释放与页描述符指针 pte 相关的页表
    pte_free_kernel(pte) 等价于 pte_free( ) ,但由主内核页表使用
    clear_page_range(mmu, start,end) 从线性地址 start 到 end 通过反复释放页表和清除页中间目录项来清除进程页表的内容

    3 线性地址转换

    3.1 分页模式下的的线性地址转换

    线性地址、页表和页表项线性地址不管系统采用多少级分页模型,线性地址本质上都是索引+偏移量的形式,甚至你可以将整个线性地址看作N+1个索引的组合,N是系统采用的分页级数。在四级分页模型下,线性地址被分为5部分,如下图:

    811006-20181129142203281-438058558.png

    在线性地址中,每个页表索引即代表线性地址在对应级别的页表中中关联的页表项。正是这种索引与页表项的对应关系形成了整个页表映射机制。

    3.1.1 页表

    多个页表项的集合则为页表,一个页表内的所有页表项是连续存放的。页表本质上是一堆数据,因此也是以页为单位存放在主存中的。因此,在虚拟地址转化物理物理地址的过程中,每访问一级页表就会访问一次内存。

    3.1.2 页表项

    页表项从四种页表项的数据结构可以看出,每个页表项其实就是一个无符号长整型数据。每个页表项分两大类信息:页框基地址和页的属性信息。在x86-32体系结构中,每个页表项的结构图如下:

    811006-20181129142415161-1892905015.png

    这个图是一个通用模型,其中页表项的前20位是物理页的基地址。由于32位的系统采用4kb大小的 页,因此每个页表项的后12位均为0。内核将后12位充分利用,每个位都表示对应虚拟页的相关属性。

    不管是那一级的页表,它的功能就是建立虚拟地址和物理地址之间的映射关系,一个页和一个页框之间的映射关系体现在页表项中。上图中的物理页基地址是 个抽象的说明,如果当前的页表项位于页全局目录中,这个物理页基址是指页上级目录所在物理页的基地址;如果当前页表项位于页表中,这个物理页基地址是指最 终要访问数据所在物理页的基地址。

    3.1.3 地址转换过程

    地址转换过程有了上述的基本知识,就很好理解四级页表模式下如何将虚拟地址转化为逻辑地址了。基本过程如下:

    1. 从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。
    2. 第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出(具体位数与平台相关,如果是32系统,则为20位),即页上级页目录的物理基地址。
    3. 从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。
    4. 第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。
    5. 从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。
    6. 第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。
    7. 从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。
    8. 第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。
    9. 从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。
    10. 第五次读取内存得到最终要访问的数据。

    811006-20181129142532481-1233411941.png

    整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单 位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。

    3.2 Linux中通过4级页表访问物理内存

    linux中每个进程有它自己的PGD( Page Global Directory),它是一个物理页,并包含一个pgd_t数组。

    进程的pgd_t数据见 task_struct -> mm_struct -> pgd_t * pgd;

    PTEs, PMDs和PGDs分别由pte_t, pmd_t 和pgd_t来描述。为了存储保护位,pgprot_t被定义,它拥有相关的flags并经常被存储在page table entry低位(lower bits),其具体的存储方式依赖于CPU架构。

    前面我们讲了页表处理的大多数函数信息,在上面我们又讲了线性地址如何转换为物理地址,其实就是不断索引的过程。

    通过如下几个函数,不断向下索引,就可以从进程的页表中搜索特定地址对应的页面对象

    宏函数 说明
    pgd_offset 根据当前虚拟地址和当前进程的mm_struct获取pgd项
    pud_offset 参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址
    pmd_offset 根据通过pgd_offset获取的pgd 项和虚拟地址,获取相关的pmd项(即pte表的起始地址)
    pte_offset 根据通过pmd_offset获取的pmd项和虚拟地址,获取相关的pte项(即物理页的起始地址)

    根据虚拟地址获取物理页的示例代码详见mm/memory.c中的函数follow_page

    不同的版本可能有所不同,早起内核中存在follow_page,而后来的内核中被follow_page_mask替代,目前最新的发布4.4中为查找到此函数

    我们从早期的linux-3.8的源代码中, 截取的代码如下

    /**
     * follow_page - look up a page descriptor from a user-virtual address
     * @vma: vm_area_struct mapping @address
     * @address: virtual address to look up
     * @flags: flags modifying lookup behaviour
     *
     * @flags can have FOLL_ flags set, defined in <linux/mm.h>
     *
     * Returns the mapped (struct page *), %NULL if no mapping exists, or
     * an error pointer if there is a mapping to something not represented
     * by a page descriptor (see also vm_normal_page()).
     */
    struct page *follow_page(struct vm_area_struct *vma, unsigned long address,
                unsigned int flags)
    {
        pgd_t *pgd;
        pud_t *pud;
        pmd_t *pmd;
        pte_t *ptep, pte;
        spinlock_t *ptl;
        struct page *page;
        struct mm_struct *mm = vma->vm_mm;
    
        page = follow_huge_addr(mm, address, flags & FOLL_WRITE);
        if (!IS_ERR(page)) {
            BUG_ON(flags & FOLL_GET);
            goto out;
        }
    
        page = NULL;
        pgd = pgd_offset(mm, address);
        if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
            goto no_page_table;
    
        pud = pud_offset(pgd, address);
        if (pud_none(*pud))
            goto no_page_table;
        if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {
            BUG_ON(flags & FOLL_GET);
            page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);
            goto out;
        }
        if (unlikely(pud_bad(*pud)))
            goto no_page_table;
    
        pmd = pmd_offset(pud, address);
        if (pmd_none(*pmd))
            goto no_page_table;
        if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {
            BUG_ON(flags & FOLL_GET);
            page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);
            goto out;
        }
        if (pmd_trans_huge(*pmd)) {
            if (flags & FOLL_SPLIT) {
                split_huge_page_pmd(mm, pmd);
                goto split_fallthrough;
            }
            spin_lock(&mm->page_table_lock);
            if (likely(pmd_trans_huge(*pmd))) {
                if (unlikely(pmd_trans_splitting(*pmd))) {
                    spin_unlock(&mm->page_table_lock);
                    wait_split_huge_page(vma->anon_vma, pmd);
                } else {
                    page = follow_trans_huge_pmd(mm, address,
                                     pmd, flags);
                    spin_unlock(&mm->page_table_lock);
                    goto out;
                }
            } else
                spin_unlock(&mm->page_table_lock);
            /* fall through */
        }
    split_fallthrough:
        if (unlikely(pmd_bad(*pmd)))
            goto no_page_table;
    
        ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
    
        pte = *ptep;
        if (!pte_present(pte))
            goto no_page;
        if ((flags & FOLL_WRITE) && !pte_write(pte))
            goto unlock;
    
        page = vm_normal_page(vma, address, pte);
        if (unlikely(!page)) {
            if ((flags & FOLL_DUMP) ||
                !is_zero_pfn(pte_pfn(pte)))
                goto bad_page;
            page = pte_page(pte);
        }
    
        if (flags & FOLL_GET)
            get_page(page);
        if (flags & FOLL_TOUCH) {
            if ((flags & FOLL_WRITE) &&
                !pte_dirty(pte) && !PageDirty(page))
                set_page_dirty(page);
            /*
             * pte_mkyoung() would be more correct here, but atomic care
             * is needed to avoid losing the dirty bit: it is easier to use
             * mark_page_accessed().
             */
            mark_page_accessed(page);
        }
        if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
            /*
             * The preliminary mapping check is mainly to avoid the
             * pointless overhead of lock_page on the ZERO_PAGE
             * which might bounce very badly if there is contention.
             *
             * If the page is already locked, we don't need to
             * handle it now - vmscan will handle it later if and
             * when it attempts to reclaim the page.
             */
            if (page->mapping && trylock_page(page)) {
                lru_add_drain();  /* push cached pages to LRU */
                /*
                 * Because we lock page here and migration is
                 * blocked by the pte's page reference, we need
                 * only check for file-cache page truncation.
                 */
                if (page->mapping)
                    mlock_vma_page(page);
                unlock_page(page);
            }
        }
    unlock:
        pte_unmap_unlock(ptep, ptl);
    out:
        return page;
    
    bad_page:
        pte_unmap_unlock(ptep, ptl);
        return ERR_PTR(-EFAULT);
    
    no_page:
        pte_unmap_unlock(ptep, ptl);
        if (!pte_none(pte))
            return page;
    
    no_page_table:
        /*
         * When core dumping an enormous anonymous area that nobody
         * has touched so far, we don't want to allocate unnecessary pages or
         * page tables.  Return error instead of NULL to skip handle_mm_fault,
         * then get_dump_page() will return NULL to leave a hole in the dump.
         * But we can only make this optimization where a hole would surely
         * be zero-filled if handle_mm_fault() actually did handle it.
         */
        if ((flags & FOLL_DUMP) &&
            (!vma->vm_ops || !vma->vm_ops->fault))
            return ERR_PTR(-EFAULT);
        return page;
    }

    以上代码可以精简为

    unsigned long v2p(int pid unsigned long va)
    {
            unsigned long pa = 0;
            struct task_struct *pcb_tmp = NULL;
            pgd_t *pgd_tmp = NULL;
            pud_t *pud_tmp = NULL;
            pmd_t *pmd_tmp = NULL;
            pte_t *pte_tmp = NULL;
    
            printk(KERN_INFO"PAGE_OFFSET = 0x%lx\n",PAGE_OFFSET);
            printk(KERN_INFO"PGDIR_SHIFT = %d\n",PGDIR_SHIFT);
            printk(KERN_INFO"PUD_SHIFT = %d\n",PUD_SHIFT);
            printk(KERN_INFO"PMD_SHIFT = %d\n",PMD_SHIFT);
            printk(KERN_INFO"PAGE_SHIFT = %d\n",PAGE_SHIFT);
    
            printk(KERN_INFO"PTRS_PER_PGD = %d\n",PTRS_PER_PGD);
            printk(KERN_INFO"PTRS_PER_PUD = %d\n",PTRS_PER_PUD);
            printk(KERN_INFO"PTRS_PER_PMD = %d\n",PTRS_PER_PMD);
            printk(KERN_INFO"PTRS_PER_PTE = %d\n",PTRS_PER_PTE);
    
            printk(KERN_INFO"PAGE_MASK = 0x%lx\n",PAGE_MASK);
    
            //if(!(pcb_tmp = find_task_by_pid(pid)))
            if(!(pcb_tmp = findTaskByPid(pid)))
            {
                    printk(KERN_INFO"Can't find the task %d .\n",pid);
                    return 0;
            }
            printk(KERN_INFO"pgd = 0x%p\n",pcb_tmp->mm->pgd);
    
            /* 判断给出的地址va是否合法(va&lt;vm_end)*/
            if(!find_vma(pcb_tmp->mm,va))
            {
                    printk(KERN_INFO"virt_addr 0x%lx not available.\n",va);
                    return 0;
            }
    
            pgd_tmp = pgd_offset(pcb_tmp->mm,va);
            printk(KERN_INFO"pgd_tmp = 0x%p\n",pgd_tmp);
            printk(KERN_INFO"pgd_val(*pgd_tmp) = 0x%lx\n",pgd_val(*pgd_tmp));
            if(pgd_none(*pgd_tmp))
            {
                    printk(KERN_INFO"Not mapped in pgd.\n");
                    return 0;
            }
    
            pud_tmp = pud_offset(pgd_tmp,va);
            printk(KERN_INFO"pud_tmp = 0x%p\n",pud_tmp);
            printk(KERN_INFO"pud_val(*pud_tmp) = 0x%lx\n",pud_val(*pud_tmp));
            if(pud_none(*pud_tmp))
            {
                    printk(KERN_INFO"Not mapped in pud.\n");
                    return 0;
            }
    
            pmd_tmp = pmd_offset(pud_tmp,va);
            printk(KERN_INFO"pmd_tmp = 0x%p\n",pmd_tmp);
            printk(KERN_INFO"pmd_val(*pmd_tmp) = 0x%lx\n",pmd_val(*pmd_tmp));
            if(pmd_none(*pmd_tmp))
            {
                    printk(KERN_INFO"Not mapped in pmd.\n");
                    return 0;
            }
    
            /*在这里,把原来的pte_offset_map()改成了pte_offset_kernel*/
            pte_tmp = pte_offset_kernel(pmd_tmp,va);
    
            printk(KERN_INFO"pte_tmp = 0x%p\n",pte_tmp);
            printk(KERN_INFO"pte_val(*pte_tmp) = 0x%lx\n",pte_val(*pte_tmp));
            if(pte_none(*pte_tmp))
            {
                    printk(KERN_INFO"Not mapped in pte.\n");
                    return 0;
            }
            if(!pte_present(*pte_tmp)){
                    printk(KERN_INFO"pte not in RAM.\n");
                    return 0;
            }
    
            pa = (pte_val(*pte_tmp) & PAGE_MASK) | (va & ~PAGE_MASK);
            printk(KERN_INFO"virt_addr 0x%lx in RAM is 0x%lx t .\n",va,pa);
            printk(KERN_INFO"contect in 0x%lx is 0x%lx\n", pa, *(unsigned long *)((char *)pa + PAGE_OFFSET)
    }

    转载于:https://www.cnblogs.com/linhaostudy/p/10038100.html

    展开全文
  • 分页机制+实践

    2020-09-14 15:10:19
    分页机制+实践 段机制把逻辑地址转换为线性地址,分页机制进一步把该线性地址再转换为物理地址。分页功能从总体上来说, 是用长度固定的页来代替长度不一定的段,分页就是将程序分成若干相同大小的页,每页4K个字节。...

    分页机制+实践

    段机制把逻辑地址转换为线性地址,分页机制进一步把该线性地址再转换为物理地址。分页功能从总体上来说, 是用长度固定的页来代替长度不一定的段,分页就是将程序分成若干相同大小的页,每页4K个字节。从80386开始,所有的80x86处理器都支持分页,它通过设置CR0寄存器的PG标志启用。当CR0中的PG=0时,禁止使用分页机制,此时分段机制产生的线性地址直接当做物理地址使用;若PG=1,则启用分页机制。
    页:将线性地址空间划分成若干大小相等的片。
    物理块或页面:将物理地址空间分成与页大小相等的若干存储块。
    页表:把线性地址映射到物理地址的一种数据结构,每个页表的大小是4KB。
    页表项:页表内的每个项目叫做页表项,占4字节,存放的是页的物理地址。
    简单的分页模型:
    页式管理,就是把4GB内存分成大小相同的页,页的最小单位是4KB,即4096字节,所以页的物理地址,其低12位始终为0.线性地址的转换分俩步完成,每一步都基于一种转换表,第一种转化表称为页目录表,第二中转换表称为页表。
    两级页表结构:
    80386页表共含1M个表项,每个表项占4个字节。如果把所有的页表项存储在一个表中,则该表最大将占4M字节连续的物理存储空间,对页表采取两级表结构后,将线性地址的高20位分两步完成,每步使用其中的10位。
    第一级称为页目录,存储在一个4K字节的页面中。页目录表中共有1K个表项,线性表的最高10位用来产生第一级的索引。两级表结构的第二级称为页表,第二级页表由线性地址的中间10位(21-12位)进行索引,最低12位表示页内偏移。

    在这里插入图片描述

    线性地址到物理地址的转换:
    在这里插入图片描述

    32位线性地址的转换:
    ①:(位31-位22)*4+CR3中页目录中的起始地址=相应页表的地址。
    ②:从指定的地址中取出32位页目录项,将它的低12位为0,即得到页表的起始地址。(位21-位12)*4+页表的起始地址=32位页面地址。
    ③:32位页面地址+(位11-位0)=32位物理地址。
    物理地址扩展(PAE)分页
    为了提高处理器的寻址能力,处理器的管脚数从32增加到了36,其寻址能力达到了2^36=64GB,但是线性地址的位数仍然是32位,为此,引入了一种新的分页机制:PAE机制。
    64GB的RAM被分成了2^24个页框,页表项的物理地址字段从20位扩展到了24位。因为PAE页表项必须包含12个标志位和24个物理地址位,总数之和为36,因为36>32,考虑到对齐,应将页表项扩大一倍到64位。
    由于页表项大小为64位,即8个字节,所以原有4K大小的页表中,就只能包含512个表项(4K/8字节=512),占用了32位线性地址中的9位。12位页内偏移量+9位指示页表中的索引+9位指示页目录表中的索引=30位。同时引入了一个叫页目录指针表(Page Directory Pointer Table)的页表新级别,它由4个64位表项组成,所以剩余的2位用来指向PDPT中的4个项中的一个。
    当把线性地址映射到4KB的页时,32位线性地址按下列方式分配。
    在这里插入图片描述

    CR3:指向一个PDPT + 位31-30:指向PDPT中4个项中的一个 + 位29-21:指向页目录中512个项中的一个 + 位20-12:指向页表中512项中的一个 + 位11-0:4KB页中的偏移量。)
    在这里插入图片描述

    当把线性地址映射到4MB的页时,32位线性地址按下列方式分配。
    CR3:指向一个PDPT + 位31-30:指向PDPT中4个项中的一个 + 位29-21:指向页目录中512个项中的一个 + 位20-0:4MB页中的偏移量。)

    LINUX中的分页
    1.四级分页机制
    linux使用了一种适合32位和64位结构的通用分页模型:四级分页机制。即:
    *页全局目录(Page Global Directory) *页上级目录(Page Upper Directory)
    *页中间目录(Page Middle Directory) *页表(Page Table)
    在这里插入图片描述

    linux通过四级页表将虚拟内存空间分成五个部分(4个页表项用于选择页,1个索引用来表示页内的偏移)。对于没有启用物理地址扩展的32位系统,两级页表足够了。Linux通过使页上级目录和页中间目录为0从根本上取消了页上级目录和中间目录。位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把他们的页目录项数设置为1,并把这两个目录项映射到页全局目录的一个合适的目录项尔实现的。
    *页全局目录包含若干页上级目录的地址;
    *页上级目录又依次包含若干页中间目录的地址;
    *页中间目录又包含若干页表的地址
    *每一个页表项指向一个页框。
    2.Linux中页表处理数据结构
    Linux分别采用pgd_t,pmd_t,pud_t和pte_t四种数据结构来表示页全局目录项、页中间目录项、页上级目录项和页表项。
    Linux使用下列宏简化了页表处理,对于每一级页表都具有以下三个关键描述宏:
    XXX_SHIFT:指定OFFSET字段的位数
    XXX_SIZE:页的大小
    XXX_MASK:用以屏蔽OFFSET字段的所有位
    宏字段前缀:PGDIR(页全局目录)、PUD(页上级目录)、PMD(页中间目录)、PAGE(页表)。
    PAGE_SHIFT返回的值为12;页的大小PAGE_SIZE=2^12=4096字节;PAGE_MASK宏产生的值为0xffff000,用以屏蔽OFFSET字段的所有位。
    当PAE被禁用时,PMD_SHIFT产生的值为22(OFFSET12位+table10位);PMD_SIZE产生的值为222=4MB,PMD_MASK产生的值为0xffc00000;当PAE被激活时,PMD_SHIFT产生的值为21(OFFSET12位+table9位),PMD_SIZE产生的值为221=2MB。PMD_MASK产生的值为0xffe00000.
    当PAE被禁用时,PGDIR_SHIFT产生的值为22(OFFSET12位+table10位);PGDIR_SIZE产生的值为2^22=4MB,PGDIR_MASK产生的值为0xffc00000;当PAE被激活时,PGDIR_SHIFT产生的值为30(OFFSET12位+table9位+9位Middle Air),PMD_SIZE产生的值为2^30=1GB。PGDIR_MASK产生的值为0xc0000000.
    PTRS_PER_PTE,PTRS_PER_PMD,PTRS_PER_PUD,PTRS_PER_PGD用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024.当PAE被激活时,产生的值分别为512,512,1和4.
    地址转换过程:
    1.从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。
    2.第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出(具体位数与平台相关,如果是32系统,则为20位),即页上级页目录的物理基地址。
    3.从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。
    4.第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。
    5.从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。
    6.第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。
    7.从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。
    8.第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。
    9.从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。
    10.第五次读取内存得到最终要访问的数据。
    打印页机制中的一些重要参数:
    在这里插入图片描述

    输出结果:
    在这里插入图片描述

    *pgd_offset(mm,addr):接收内存描述符地址mm和线性地址addr作为参数。这个宏产生地址addr在页全局目录中相应表项的线性地址;通过内存描述符mm内的一个指针可以找到这个页全局目录表。
    根据分页机制一步步查询,最终得到物理地址。PID–task-struct–mm–pgd–p4d–pud–pmd–pte。
    在这里插入图片描述

    输出结果:
    在这里插入图片描述

    根据pte计算物理地址。
    页框的物理地址:page_addr=pte_val(*pte)&PAGE_MASK
    页偏移地址:page_offset=vaddr&~PAGE_MASK
    物理地址:paddr=page_addr|page_offset
    在这里插入图片描述

    加载内核模块:
    在这里插入图片描述

    卸载内核模块:
    在这里插入图片描述

    展开全文
  • Windows的分页机制

    2020-06-11 11:00:34
    对于Windows的分页机制, 我一直还是比较自信的, 但是今天我发现在重写NewBluePill的X64分页机制到Win32上面的时候, 居然感觉有些棘手, 看来啊, 很多东西还是不是看上去那样简单, 难点主要集中在原来倒腾没有分页的...
  • x86分页机制详解

    2020-07-13 23:10:18
    为什么会有分页机制?2. 从虚拟地址到物理地址3. 简单的分页模型4. 页表和页目录4.1 层次化的分页结构4.2 页表4.3 页目录5. 地址变换的具体过程 1. 为什么会有分页机制? 有些资料说是为了实现“虚拟内存”,真的是...
  • CPU 分页机制

    千次阅读 2014-10-21 22:44:18
    对于X86架构的CPU,分页机制是其中一个非常重要的特性。分页机制将内存地址变为虚拟地址,通过页目录,页表转换成物理地址。这为虚拟存储器的实现提供了硬件支持。 通常80386中页的大小为4kb,即内存被划分为一个个...
  • 分页机制总结

    2015-09-08 17:16:30
    【0】写在前面(分页机制) 0.0) source code from orange’s implemention of a os and text description from Zhaojiong’s perfect analysis of Linux kernel 0.1)本代码旨在演示 怎样开启分页机制 + 怎样构建...
  • Linux分页机制分页机制的演变--Linux内存管理(七) 2016年09月01日 20:01:31JeanCheng阅读数:4543 https://blog.csdn.net/gatieme/article/details/52402967 ~ 版权声明:本文为博...
  • 分段分页机制

    2018-12-11 16:55:44
    意义:分页机制是为了充分利用空间,将琐碎的地址空间利用起来;  分段机制是为了解决冲突问题,它是一种机制,这种机制使得很方便地管理内存; &nbsp; 1. 内存分段 &nbsp; &nbsp; 1.1 为什么分段? &...
  • Linux分页机制

    2017-08-15 21:15:26
    分页机制为传统需求页、虚拟内存系统提供了实现机制 系统中的线性地址比实际的物理地址大的多,所以进行虚拟化线性地址空间,即虚拟存储技术
  • 内存寻址之分页机制

    2019-01-01 13:32:35
    分页机制完成线性地址到物理地址的转换 80x86 规定分页机制是可选的。分段和分页没有什么必然联系,分段可以说是 Intel 的 CPU 一直保持着的一种机制,而分页只是保护模式下的一种内存管理策略。想开启分页机制,CPU...
  • 这两天在逆向分析MmIsAddressValid这个函数学习了下PAE分页机制,并且也发现了一个问题就是本机ntoskrnl中导出的MmIsAddressValid函数是采用非PAE方式的而本机XP SP2系统采用的却是PAE方式的分页机制这个可以通过...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,292
精华内容 1,316
关键字:

分页机制