32位内存分页机制 linux_linux内存分页机制 - CSDN
  • Linux内存分页机制原理
  • Linux内存寻址之分段机制及分页机制

    千次阅读 2016-03-04 17:32:34
    原本以为自己对分段分页机制已经理解了,结果发现其实是一知半解。于是,查找了很多资料,最终理顺了内存寻址的知识。现在把我的理解记录下来,希望对内核学习者有一定帮助,也希望大家指出错误之处。 分段到底是...

    前言

    最近在学习Linux内核,读到《深入理解Linux内核》的内存寻址一章。原本以为自己对分段分页机制已经理解了,结果发现其实是一知半解。于是,查找了很多资料,最终理顺了内存寻址的知识。现在把我的理解记录下来,希望对内核学习者有一定帮助,也希望大家指出错误之处。

    分段到底是怎么回事

    相信学过操作系统课程的人都知道分段分页,但是奇怪的是书上基本没提分段分页是怎么产生的,这就导致我们知其然不知其所以然。下面我们先扒一下分段机制产生的历史。

    实模式的诞生(16位处理器及寻址)

    在8086处理器诞生之前,内存寻址方式就是直接访问物理地址。8086处理器为了寻址1M的内存空间,把地址总线扩展到了20位。但是,一个尴尬的问题出现了,ALU的宽度只有16位,也就是说,ALU不能计算20位的地址。为了解决这个问题,分段机制被引入,登上了历史舞台。
    为了支持分段,8086处理器设置了四个段寄存器:CS, DS, SS, ES.每个段寄存器都是16位的,同时访问内存的指令中的地址也是16位的。但是,在送入地址总线之前,CPU先把它与某个段寄存器内的值相加。这里要注意:段寄存器的值对应于20位地址总线的中的高16位,所以相加时实际上是内存总线中的高12位与段寄存器中的16位相加,而低4位保留不变,这样就形成一个20位的实际地址,也就实现了从16位内存地址到20位实际地址的转换,或者叫“映射”。

    保护模式的诞生(32位处理器及寻址)

    • 80286处理器的地址总线为24位,寻址空间达16M,同时引入了保护模式(内存段的访问受到限制)
    • 80386处理器是一个32位处理器,ALU和地址总线都是32位的,寻址空间达 4G。也就是说它可以不通过分段机制,直接访问4G的内存空间。虽然它是新时代的小王子,超越它的无数前辈,然而,它需要背负家族的使命–兼容前代的处理器。也就是说,它必须支持实模式和保护模式。所以,80386在段寄存器的基础上构筑保护模式,并且保留16位的段寄存器。
    • 从80386之后的处理器,架构基本相似,统称为IA32(32 Bit Intel Architecture)。

    IA32的内存寻址机制

    寻址硬件

    在 8086 的实模式下,把某一段寄存器左移4位,然后与地址ADDR相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的这个地址就叫逻辑地址(或叫虚地址)。在IA32的保护模式下,这个逻辑地址不是被直接送到内存总线而是被送到内存管理单元(MMU)。MMU由一个或一组芯片组成,其功能是把逻辑地址映射为物理地址,即进行地址转换,如图所示。
    MMUMMU

    IA32的三种地址

    • 逻辑地址:
      机器语言指令仍用这种地址指定一个操作数的地址或一条指令的地址。 这种寻址方式在Intel的分段结构中表现得尤为具体,它使得MS-DOS或Windows程序员把程序分为若干段。每个逻辑地址都由一个段和偏移量组成。
    • 线性地址:
      线性地址是一个32位的无符号整数,可以表达高达232(4GB)的地址。通常用16进制表示线性地址,其取值范围为0x00000000~0xffffffff。
    • 物理地址:
      也就是内存单元的实际地址,用于芯片级内存单元寻址。 物理地址也由32位无符号整数表示。

    MMU地址转化过程

    MMU是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件,在此,我们把它们分别叫做分段机制和分页机制,以利于从逻辑的角度来理解硬件的实现机制。分段机制把一个逻辑地址转换为线性地址;接着,分页机制把一个线性地址转换为物理地址。
    MMU_translateMMU_translate

    IA32的段寄存器

    IA32中有六个16位段寄存器:CS, DS, SS, ES,FS, GS.跟8086的段寄存器不同的是,这些寄存器存放的不再是某个段的基地址,而是某个段的选择符(Selector)。

    分段机制的实现

    段是虚拟地址空间的基本单位,分段机制必须把虚拟地址空间的一个地址转换为线性地址空间的一个线性地址。

    为了实现这种映射,仅仅用段寄存器来确定一个基地址是不够的,至少还得描述段的长度,并且还需要段的一些其他信息,比如访问权之类。所以,这里需要的是一个数据结构,这个结构包括三个方面的内容:

    1. 段的基地址(Base Address):在线性地址空间中段的起始地址。
    2. 段的界限(Limit):在虚拟地址空间中,段内可以使用的最大偏移量。
    3. 段的保护属性(Attribute):表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等等。

    上面的数据结构我们称为段描述符,多个段描述符组成的表称为段描述符表

    段描述符

    所谓描述符(Descriptor),就是描述段的属性的一个8字节存储单元。在实模式下,段的属性不外乎是代码段、堆栈段、数据段、段的起始地址、段的长度等等,而在保护模式下则复杂一些。IA32将它们结合在一起用一个8字节的数表示,称为描述符 。
    IA32的一个通用的段描述符的结构IA32的一个通用的段描述符的结构
    从图可以看出,一个段描述符指出了段的32位基地址和20位段界限(即段长)。这里我们只关注基地址和段界限,其他的属性略过。

    段描述符表

    各种各样的用户描述符和系统描述符,都放在对应的全局描述符表、局部描述符表和中断描述符表中。描述符表(即段表)定义了IA32系统的所有段的情况。所有的描述符表本身都占据一个字节为8的倍数的存储器空间,空间大小在8个字节(至少含一个描述符)到64K字节(至多含8K)个描述符之间。

    1. 全局描述符表(GDT)
      全局描述符表GDT(Global Descriptor Table),除了任务门,中断门和陷阱门描述符外,包含着系统中所有任务都共用的那些段的描述符。 它的第一个8字节位置没有使用。
    2. 中断描述符表IDT(Interrupt Descriptor Table)
      中断描述符表IDT(Interrupt Descriptor Table),包含256个门描述符。IDT中只能包含任务门、中断门和陷阱门描述符,虽然IDT表最长也可以为64K字节,但只能存取2K字节以内的描述符,即256个描述符,这个数字是为了和8086保持兼容。
    3. 局部描述符表(LDT)
      局部描述符表LDT(local Descriptor Table),包含了与一个给定任务有关的描述符,每个任务各自有一个的LDT。 有了LDT,就可以使给定任务的代码、 数据与别的任务相隔离。每一个任务的局部描述符表LDT本身也用一个描述符来表示,称为LDT描述符,它包含了有关局部描述符表的信息,被放在全局描述符表GDT中。

    总结

    IA32的内存寻址机制完成从逻辑地址–线性地址–物理地址的转换。其中,逻辑地址的段寄存器中的值提供段描述符,然后从段描述符中得到段基址和段界限,然后加上逻辑地址的偏移量,就得到了线性地址,线性地址通过分页机制得到物理地址。
    首先,我们要明确,分段机制是IA32提供的寻址方式,这是硬件层面的。就是说,不管你是windows还是linux,只要使用IA32的CPU访问内存,都要经过MMU的转换流程才能得到物理地址,也就是说必须经过逻辑地址–线性地址–物理地址的转换。

    Linux中分段的实现

    前面说了那么多关于分段机制的实现,其实,对于Linux来说,并没有什么卵用。因为,Linux基本不使用分段的机制,或者说,Linux中的分段机制只是为了兼容IA32的硬件而设计的。

    Intel微处理器的段机制是从8086开始提出的, 那时引入的段机制解决了从CPU内部16位地址到20位实地址的转换。为了保持这种兼容性,386仍然使用段机制,但比以前复杂得多。因此,Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地使用了一下分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。但是,对段机制相关知识的了解是进入Linux内核的必经之路。

    从2.2版开始,Linux让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表LDT。但内核中也用到LDT,那只是在VM86模式中运行Wine,因为就是说在Linux上模拟运行Winodws软件或DOS软件的程序时才使用。

    在 IA32 上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在IA32上设计操作系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。 但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让 Linux 具有更好的可移植性,我们需要去掉段机制而只使用分页机制。但不幸的是,IA32规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。万般无奈之下,Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量<4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。

    另外,由于IA32段机制还规定,必须为代码段和数据段创建不同的段,所以Linux必须为代码段和数据段分别创建一个基地址为0,段界限为4GB的段描述符。不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级别3,根据IA32段保护机制规定,特权级3的程序是无法访问特权级为0的段的,所以Linux必须为内核用户程序分别创建其代码段和数据段。这就意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。

    总结

    分段机制是IA32架构CPU的特色,并不是操作系统寻址方式的必然选择。Linux为了跨平台,巧妙的绕开段机制,主要使用分页机制来寻址。

    参考资料
    《深入分析Linux内核源码》

    在上一篇文章Linux内存寻址之分段机制中,我们了解逻辑地址通过分段机制转换为线性地址的过程。下面,我们就来看看更加重要和复杂的分页机制。

    分页机制在段机制之后进行,以完成线性—物理地址的转换过程。段机制把逻辑地址转换为线性地址,分页机制进一步把该线性地址再转换为物理地址。

    硬件中的分页

    分页机制由CR0中的PG位启用。如PG=1,启用分页机制,并使用本节要描述的机制,把线性地址转换为物理地址。如PG=0,禁用分页机制,直接把段机制产生的线性地址当作物理地址使用。分页机制管理的对象是固定大小的存储块,称之为页(page)。分页机制把整个线性地址空间及整个物理地址空间都看成由页组成,在线性地址空间中的任何一页,可以映射为物理地址空间中的任何一页(我们把物理空间中的一页叫做一个页面或页框(page frame))

    80386使用4K字节大小的页。每一页都有4K字节长,并在4K字节的边界上对齐,即每一页的起始地址都能被4K整除。因此,80386把4G字节的线性地址空间,划分为1G个页面,每页有4K字节大小。分页机制通过把线性地址空间中的页,重新定位到物理地址空间来进行管理,因为每个页面的整个4K字节作为一个单位进行映射,并且每个页面都对齐4K字节的边界,因此,线性地址的低12位经过分页机制直接地作为物理地址的低12位使用。

    为什么使用两级页表

    假设每个进程都占用了4G的线性地址空间,页表共含1M个表项,每个表项占4个字节,那么每个进程的页表要占据4M的内存空间。为了节省页表占用的空间,我们使用两级页表。每个进程都会被分配一个页目录,但是只有被实际使用页表才会被分配到内存里面。一级页表需要一次分配所有页表空间,两级页表则可以在需要的时候再分配页表空间。

    两级页表结构

    两级表结构的第一级称为页目录,存储在一个4K字节的页面中。页目录表共有1K个表项,每个表项为4个字节,并指向第二级表。线性地址的最高10位(即位31~位32)用来产生第一级的索引,由索引得到的表项中,指定并选择了1K个二级表中的一个表。
    两级表结构的第二级称为页表,也刚好存储在一个4K字节的页面中,包含1K个字节的表项,每个表项包含一个页的物理基地址。第二级页表由线性地址的中间10位(即位21~位12)进行索引,以获得包含页的物理地址的页表项,这个物理地址的高20位与线性地址的低12位形成了最后的物理地址,也就是页转化过程输出的物理地址。
    两级页表结构两级页表结构

    页目录项

    页目录项结构页目录项结构

    • 第31~12位是20位页表地址,由于页表地址的低12位总为0,所以用高20位指出32位页表地址就可以了。因此,一个页目录最多包含1024个页表地址。
    • 第0位是存在位,如果P=1,表示页表地址指向的该页在内存中,如果P=0,表示不在内存中。
    • 第1位是读/写位,第2位是用户/管理员位,这两位为页目录项提供硬件保护。当特权级为3的进程要想访问页面时,需要通过页保护检查,而特权级为0的进程就可以绕过页保护。
    • 第3位是PWT(Page Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式
    • 第4位是PCD(Page Cache Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。
    • 第5位是访问位,当对页目录项进行访问时,A位=1。
    • 第7位是Page Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页面,请看后面的扩展分页。
    • 第9~11位由操作系统专用,Linux也没有做特殊之用。

    页面项

    页面项结构页面项结构
    80386的每个页目录项指向一个页表,页表最多含有1024个页面项,每项4个字节,包含页面的起始地址和有关该页面的信息。页面的起始地址也是4K的整数倍,所以页面的低12位也留作它用。
    第31~12位是20位物理页面地址,除第6位外第0~5位及9~11位的用途和页目录项一样,第6位是页面项独有的,当对涉及的页面进行写操作时,D位被置1。
    4GB的内存只有一个页目录,它最多有1024个页目录项,每个页目录项又含有1024个页面项,因此,内存一共可以分成1024×1024=1M个页面。由于每个页面为4K个字节,所以,存储器的大小正好最多为4GB。

    线性地址到物理地址的转换

    32位线性地址到物理地址的转换32位线性地址到物理地址的转换

    1. CR3包含着页目录的起始地址,用32位线性地址的最高10位A31~A22作为页目录的页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,形成相应页表的地址。
    2. 从指定的地址中取出32位页目录项,它的低12位为0,这32位是页表的起始地址。用32位线性地址中的A21~A12位作为页表中的页面的索引,将它乘以4,与页表的起始地址相加,形成32位页面地址。
    3. 将A11~A0作为相对于页面地址的偏移量,与32位页面地址相加,形成32位物理地址。

    扩展分页

    从奔腾处理器开始,Intel微处理器引进了扩展分页,它允许页的大小为4MB。
    扩展分页扩展分页
    在扩展分页的情况下,分页机制把32位线性地址分成两个域:最高10位的目录域和其余22位的偏移量。

    页面高速缓存

    页面高速缓存页面高速缓存
    由于在分页情况下,每次存储器访问都要存取两级页表,这就大大降低了访问速度。所以,为了提高速度,在386中设置一个最近存取页面的高速缓存硬件机制,它自动保持32项处理器最近使用的页面地址,因此,可以覆盖128K字节的存储器地址。当进行存储器访问时,先检查要访问的页面是否在高速缓存中,如果在,就不必经过两级访问了,如果不在,再进行两级访问。平均来说,页面高速缓存大约有98%的命中率,也就是说每次访问存储器时,只有2%的情况必须访问两级分页机构。这就大大加快了速度。

    Linux中的分页机制

    Linux使用了一个适合32位和64位系统的分页机制。
    Linux分页模型Linux分页模型

    • 页全局目录
    • 页顶级目录
    • 页中间目录
    • 页表

    页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址。每一个页表项指向一个页框。线性地址因此被分成五个部分。图中没有显示位数,因为每一部分的大小与具体的计算机体系结构有关。

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

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

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

    总结

    这里我们不讨论代码实现,只关注原理。从上面的讨论可以看到分页机制主要依赖硬件的实现。Linux采用的四级页表只是为了最大化兼容不同的硬件实现,单就IA32架构的CPU来说,就有多种分页实现,常规分页机制,PAE机制等。

    我们虽然讨论的是Linux的分页机制,实际上我们用了大部分篇幅来讨论Intel CPU的分页机制实现。因为Linux的分页机制是建立在硬件基础之上的,不同的平台需要有不同的实现。Linux在软件层面构造的虚拟地址,最终还是要通过MMU转换为物理地址,也就是说,不管Linux的分页机制是怎样实现的,CPU只按照它的分页实现来解读线性地址,所以Linux传给CPU的线性地址必然是满足硬件实现的。例如说:Linux在32位CPU上,它的四级页表结构就会兼容到硬件的两级页表结构。可见,Linux在软件层面上做了一层抽象,用四级页表的方式兼容32位和64位CPU内存寻址的不同硬件实现。

    最后分享两篇linux内存寻址的实验文档,结合实例更容易理解。
    Linux内存地址映射
    Linux内核在x86_64 CPU中地址映射

    参考资料
    《深入理解Linux内核》
    《深入分析Linux内核源码》



    展开全文
  • 日期 内核版本 架构 作者 GitHub ... Linux内存管理 [注意] 如果您当前使用的系统并不是linux,或者您的系统中只有一份linux源码,而您又期待能够查看或者检索不同版本的linux源码 可以使用 h
    日期 内核版本 架构 作者 GitHub CSDN
    2016-09-01 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理

    [注意]

    如果您当前使用的系统并不是linux,或者您的系统中只有一份linux源码,而您又期待能够查看或者检索不同版本的linux源码

    可以使用 http://lxr.free-electrons.com/

    LXR (Linux Cross Reference)是比较流行的linux源代码查看工具,而这里集成了全版本的linux源码的索引

    1 linux的分页机制


    1.1 四级分页机制


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

    • 页全局目录(Page Global Directory)
    • 页上级目录(Page Upper Directory)
    • 页中间目录(Page Middle Directory)
    • 页表(Page Table)
    • 页全局目录包含若干页上级目录的地址;
    • 页上级目录又依次包含若干页中间目录的地址;
    • 而页中间目录又包含若干页表的地址;
    • 每一个页表项指向一个页框。
      因此线性地址因此被分成五个部分,而每一部分的大小与具体的计算机体系结构有关。

    Linux四级页表

    1.3 不同架构的分页机制


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

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

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

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

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


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

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

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

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

    2 linux中页表处理数据结构


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


    Linux分别采用pgd_t、pmd_t、pud_t和pte_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.4 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是212=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 产生的值为221 或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部分,如下图:

    这里写图片描述

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

    3.1.1 页表


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

    3.1.2 页表项


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

    这里写图片描述

    这个图是一个通用模型,其中页表项的前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.第五次读取内存得到最终要访问的数据。

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

    内存索引

    内存索引

    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)
    }
    展开全文
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-09-01 Linux-4.7 X86 & arm gatieme ...32位,在保护方式下,其能够访问的线性地址空间可达4GB,而且允许几乎不受存储空间限制的虚拟存储器程序。虚拟存
    日期 内核版本 架构 作者 GitHub CSDN
    2016-09-01 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理

    1 前景回顾


    前面我们讲解了操作系统段式存储管理的主要内容.

    • 32位,在保护方式下,其能够访问的线性地址空间可达4GB,而且允许几乎不受存储空间限制的虚拟存储器程序。虚拟存储器地址空间能够可达64TB。它还提供了复杂的存储管理和硬件辅助的保护机构和增加了支持多任务操作系统的特别优化的指令。实际上,64TB的虚拟地址空间是有磁盘等外部存储器的支持下实现的。在编写程序是可以放在磁盘存储器上,但在执行时,必须把程序加载到物理存储器中。而存储器管理就是要将46位虚拟地址变换成32位物理地址。

    • 将程序分成不同的段进行管理,我们编程访问内存地址时,访问的其实是操作系统抽象给我们的虚拟地址,通过段基址:段偏移的方式访问内存虚拟地址,极大了简化了程序员的编程结构。

    • 通过硬件和操作系统的段式管理机制,实模式下通过段基址左移四位+段内偏移,保护模式下通过段选择子select从段描述符GDT/LDT表中获取到段描述符,然后对基地址和段偏移处理,将虚拟地址转换为线性地址。

    如果没有采用存储器分页管理机制,那么我们得到的线性地址就直接对应与物理地址,否则,则需要将线性地址转换为物理地址。

    从80386开始,所有的80x86处理器都支持分页,它通过设置CR0寄存器的PG标志启用分页。当PG=0时,线性地址就被解释成物理地址。

    具体请参见深入理解计算机系统-之-内存管理(三)–(分段管理机制–段描述符,段选择子,描述符表)

    cpu模式

    2 页式管理


    2.1 分段机制存在的问题


    分段,是指将程序所需要的内存空间大小的虚拟空间,通过映射机制映射到某个物理地址空间(映射的操作由硬件完成)。分段映射机制解决了之前操作系统存在的两个问题:

    • (1)地址空间没有隔离

    • (2)程序运行的地址不确定

    不过分段方法存在一个严重的问题:内存的使用效率低。

    分段的内存映射单位是整个程序;如果内存不足,被换入换出到磁盘的空间都是整个程序的所需空间,这会造成大量的磁盘访问操作,并且严重降低了运行速度.

    事实上,很多时候程序运行所需要的数据只是很小的一部分,加入到内存的数据大小可能会很小,并没有必要整体的写入和写出.

    分页机制解决了上面分段方法所存在的一个内存使用效率问题;其核心思想是系统为程序执行文件中的第x页分配了内存中的第y页,同时y页会添加到进程虚拟空间地址的映射表中(页表),这样程序就可以通过映射访问到内存页y了。

    2.2 分页存储的基本内容


    分页的基本方法是将地址空间人为地等分成某一个固定大小的页;每一页大小由硬件来决定,或者是由操作系统来决定(如果硬件支持多种大小的页)。目前,以大小为4KB的分页是绝大多数PC操作系统的选择.

    • 逻辑空间等分为页;并从0开始编号

    • 内存空间等分为块,与页面大小相同;从0开始编号

    • 分配内存时,以块为单位将进程中的若干个页分别装入

    关于进程分页. 当我们把进程的虚拟地址空间按页来分割,常用的数据和代码会被装在到内存;暂时没用到的是数据和代码则保存在磁盘中,需要用到的时候,再从磁盘中加载到内存中即可.

    这里需要了解三个概念:

    1. 虚拟页(VP, Virtual Page),虚拟空间中的页;

    2. 物理页(PP, Physical Page),物理内存中的页;

    3. 磁盘页(DP, Disk Page),磁盘中的页。

    虚拟内存的实现需要硬件的支持,从Virtual Address到Physical Address的映射,通过一个叫MMU(Memory Mangement Unit)的部件来完成

    3 分页机制支持


    3.1 硬件分页支持


    分页单元(paging unit)把线性地址转换成物理地址。其中的一个关键任务就是把所请求的访问类型与线性地址的访问权限相比较,如果这次内存访问是无效的,就产生一个缺页异常。

    • :为了更高效和更经济的管理内存,线性地址被分为以固定长度为单位的组,成为页。页内部连续的线性地址空间被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址和对应的存取权限,而不用指定全部线性地址的存取权限。这里说页,同时指一组线性地址以及这组地址包含的数据

    • 页框:分页单元把所有的 RAM 分成固定长度的页框(page frame)(有时叫做物理页)。每一个页框包含一个页(page),也就是说一个页框的长度与一个页的长度一致。页框是主存的一部分,因此也是一个存储区域。区分一页和一个页框是很重要的,前者只是一个数据块,可以存放在任何页框或磁盘中。

    • 页表:把线性地址映射到物理地址的数据结构称为页表(page table)。页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。

    3.2 常规的32bit分页


    常规4KB分页,32位的线性地址被分成3个域

    Directory(目录) Table(页表) Offset(偏移量)
    最高10位 中间10位 最低12位

    常规的32bit分页

    线性地址的转换分为两步完成,每一步都基于一种转换表,第一种转换表称为页目录表(page directory),第二种转换表称为页表(page table)。

    为什么需要两级呢?
    目的在于减少每个进程页表所需的 RAM 的数量。如果使用简单的一级页表,将需要高达220 个表项来表示每个进程的页表,即时一个进程并不使用所有的地址,二级模式通过职位进程实际使用的那些虚拟内存区请求页表来减少内存容量。

    每个活动的进程必须有一个页目录,但是却没有必要马上为所有进程的所有页表都分配 RAM,只有在实际需要一个页表时候才给该页表分配 RAM。

    页目录项和页表项有同样的结构,每项都包含下面的字段:

    字段 描述
    Present标志 如果被置为1,所指的页(或页表)就在主存中;如果该标志为0,则这一页不在主存中,此时这个表项剩余的位可由操作系统用于自己的目的。如果执行一个地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址存放在控制寄存器cr2中,并产生14号异常:缺页异常。
    包含页框物理地址最高20位的字段。由于每一个页框有4KB的容量,它的物理地址必须是4096的倍数,因此物理地址的最低12位总为0.如果这个字段指向一个页目录,相应的页框就含有一个页表;如果它指向一个页表,相应的页框就含有一页数据
    Accessed标志 每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做
    Dirty标志 只应用于页表项中。每当对一个页框进行写操作时就设置这个标志。与Accessed标志一样,当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做。
    Read/Write标志 含有页或页表的存取权限(Read/Write或Read)。
    User/Supervisor标志 含有访问页或页表所需的特权级。
    PCD和PWT标志 控制硬件高速缓存处理页或页表的方式。
    Page Size标志 只应用于页目录项。如果设置为1,则页目录项指的是2MB或4MB页框。
    Global标志 只应用于页表项。这个标志是在Pentium Pro中引入的,用来防止常用页从TLB高速缓存中刷新出去。只有在cr4寄存器的页全局启用(Page Global Enable, PGE)标志置位时这个标志才起作用。

    正在使用的页目录的物理地址存放在控制寄存器CR3中。

    了解了以上结构之后,我们看看如何从线性地址转换到物理地址的 :

    • 线性地址中的 Directory 字段决定页目录中的目录项,目录项指向适当的页表

    • 线性地址中的 Table 字段又决定页表的页表项,页表项含有页所在页框的物理地址

    • 线性地址中的 Offset 地段决定了页框内的相对位置,由于 offset 为 12 为,所以一页含有 4096 字节的数据

    Directory字段和Table字段都是10位长,因此页目录和页表都可以多达1024项。那么一个页目录可以寻址到高达1024*1024*4096=232个存储单元,这和32位地址所期望的一样。

    3.3 物理地址扩展(PAE)分页机制和扩展分页(PSE)


    处理器所支持的RAM容易受到连接到地址总线上的地址管脚树限制. 早期Intel处理器从80386到Pentium使用32位物理地址.

    从理论上讲, 这样的系统可以使用高达2^32=4GB的RAM, 而实际上, 由于用户进程现行地址空间的需要, 4GB的虚拟地址按照1:3的比例划分给内核虚拟地址空间和进程虚拟地址空间. 则内核只能直接对1GB的线性地址空间进行寻址.

    然而, 大型服务器需要大于4GB的RAM来同时运行数以钱计的进程, 所以必须扩展32位80x86架构所支持的RAM容量.

    Intel通过在它的处理器上把管脚数从32增加到36满足这样的需要, 从Pentinum Pro开始, Intel所有处理器的寻址能力可达到2^36=64GB, 但是只有引入一种新的分页机制才能把32位现行地址转换为36位物理地址才能使用所增加的物理地址.

    从Pentinum Pro处理器开始, Intel引入一种叫做物理地址扩展(Physical Address Extension, PAE)的机制.

    从Pentium模型开始,80x86微处理器引入了扩展分页(externded paging),也叫页大小扩展[Page Size Extension], 它允许页框大小为4MB而不是4KB。扩展分页用于把大段连续的线性地址转换成相应的物理地址,在这种情况下,内核可以不用中间页表进行地址转换,从而节省内存并保留TLB项。

    但是Linux并没有采用这种机制

    正如前面所述,通过设置页目录项的Page Size标志启用扩展分页功能。在这种情况下,分页单元把32位线性地址分成两个字段:
    Directory:最高10位。
    Offfset:其余22位。

    扩展分页和正常分页的页目录项基本相同,除了
    * Page Size标志必须被设置。
    * 20位物理地址字段只有最高10位是有意义的。这是因为每一个物理地址都是在以4MB为边界的地方开始的,故这个地址的最低22位为0。

    通过设置cr4处理器寄存器的PSE标志能使扩展分页与常规分页共存

    Intel为了支持PAE改变了分页机制

    • 64GB的RAM被分成了2^24个页框, 页表项的物理地址字段从20位扩展到了24位. 因为PAE页表项必须包含12个标志位和24个物理地址位, 总数之和为36, 页表项大小从32位扩展到了64位, 结果, 一个4KB的页表项包含512个表项而不是1024个表项

    • 引入一个页目录指针表(Page Directory Pointer Table, PDPT)的页表新级别, 它由4个64位表项组成.

    • cr3控制寄存器包含一个27位的页目录指针表(PDPT)基地址字段. 因为PDPT存放在RAM的前4GB中, 并在32字节(2^5)的倍数上对其, 因此27位足以表示这种表的基地址

    • 当把线性地址映射到4KB的页时(页目录项中的PS标准清0), 32位线性地址将按照如下方式解释

    字段 描述 位数
    cr3 指向一个PDPT crs寄存器存储
    PGD 指向PDPT中4个项中的一个 位31~30
    PMD 指向页目录中512项中的一个 位29~21
    PTE 指向页表中512项中的一个 位20~12
    page offset 4KB页中的偏移 位11~0

    * 当把现行地址映射到2MB的页时(页目录项中的PS标志置为1), 32位线性地址按照如下方式解释

    字段 描述 位数
    cr3 指向一个PDPT crs寄存器存储
    PGD 指向PDPT中4个项中的一个 位31~30
    PMD 指向页目录中512项中的一个 位29~21
    page offset 2MB页中的偏移 位20~0

    总之, 一旦cr3被设置, 就可能寻址高达4GB RAM, 如果我们期望堆更多的RAM进行寻址, 就必须在cr3中放置一个新值, 或改变PDPT的内容.

    但是PAE的主要问题是线性地址仍然是32位长, 这就需要内核黑客用同一线性地址映射不同的RAM区. 很显然, PAE并没有扩大进程的线性地址空间, 因为它只处理物理地址. 此外, 只有内核能够修改进程的页表, 所以在用户态下运行的程序不可能使用大于4GB的物理地址空间. 另一方面, PAE允许内核使用容量高达64GB的RAM, 从而显著的增加系统中的进程数目

    3.4 64位系统中的分页


    32位处理器普遍采用两级分页。然而两级分页并不适用于采用64位系统的计算机。

    原因如下 :

    首先假设一个大小为4KB的标准页,4KB覆盖2^12个地址,所以offset字段是12位。如果我们现在决定仅仅使用64位中的48位来寻址(这个限制仍然能是我们自在地拥有256TB的寻址空间!),剩下的48-12=36位被分配给Table和Directory字段。如果我们决定为两个字段个预留18位,那么每个进程的页目录和页表都含有2^18个项,即超过256000个项。

    由于这个原因,所有64位处理器的硬件分页系统都使用了额外的分页级别。使用的级别数量取决于处理器的类型。

    平台名称 页大小 寻址使用位数 分页级别 线性地址分级
    alpha 8KB 43 3 10+10+10+13
    ia64 4KB 39 3 9+9+9+12
    ppc64 4KB 41 3 10+10+9+12
    x86_64 4KB 48 4 9+9+9+9+12

    注:ia64是intel的一门高端技术,不与x86_64系统兼容
    IA-32e Paging机制下线性地址映射到4KB的页

    3.5 硬件保护方案

    与页和页表相关的特权级只有两个,因为特权由前面“常规分页”一节中所提到的User/Supervisor标志所控制。若这个标志为0,只有当CPL小于3(这意味着对于Linux而言,处理器处于内核态)时才能对页寻址;若该标志为1,则总能对页寻址。

    此外,与段的3种存取权限(读,写,执行)不同的是,页的存取权限只有两种(读,写)。如果页目录项或页表项的Read/Write标志等于0,说明相应的页表或页是只读的,否则是可读写的。

    4 总结


    80386 使用4K字节大小的页。每一页都有4K字节长,并在4K字节的边界上对齐,即每一页的起始地址都能被4K整除。因此,80386把4G字节的线性地址空间,划分为1G个页面,每页有4K字节大小。分页机制通过把线性地址空间中的页,重新定位到物理地址空间来进行管理,因为每个页面的整个4K字节作为一个单位进行映射,并且每个页面都对齐4K字节的边界,因此,线性地址的低12位经过分页机制直接地作为物理地址的低12位使用。

    4.1 为什么使用多级页表


    假设每个进程都占用了4G的线性地址空间,页表共含1M个表项,每个表项占4个字节,那么每个进程的页表要占据4M的内存空间。为了节省页表占用的空间,我们使用两级页表。每个进程都会被分配一个页目录,但是只有被实际使用页表才会被分配到内存里面。一级页表需要一次分配所有页表空间,两级页表则可以在需要的时候再分配页表空间。

    4.2 x86_32两级页表结构


    两级表结构的第一级称为页目录,存储在一个4K字节的页面中。页目录表共有1K个表项,每个表项为4个字节,并指向第二级表。线性地址的最高10位(即位31~位32)用来产生第一级的索引,由索引得到的表项中,指定并选择了1K个二级表中的一个表。

    两级表结构的第二级称为页表,也刚好存储在一个4K字节的页面中,包含1K个字节的表项,每个表项包含一个页的物理基地址。第二级页表由线性地址的中间10 位(即位21~位12)进行索引,以获得包含页的物理地址的页表项,这个物理地址的高20位与线性地址的低12位形成了最后的物理地址,也就是页转化过程输出的物理地址。

    两级页表结构


    页目录项结构

    • 第31~12位是20位页表地址,由于页表地址的低12位总为0,所以用高20位指出32位页表地址就可以了。因此,一个页目录最多包含1024个页表地址。

    • 第0位是存在位,如果P=1,表示页表地址指向的该页在内存中,如果P=0,表示不在内存中。

    • 第1位是读/写位,第2位是用户/管理员位,这两位为页目录项提供硬件保护。当特权级为3的进程要想访问页面时,需要通过页保护检查,而特权级为0的进程就可以绕过页保护。

    • 第3位是PWT(Page Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式

    • 第4位是PCD(Page Cache Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。

    • 第5位是访问位,当对页目录项进行访问时,A位=1。

    • 第7位是Page Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页面,请看后面的扩展分页。

    • 第9~11位由操作系统专用,Linux也没有做特殊之用。

    页面项结构

    80386的每个页目录项指向一个页表,页表最多含有1024个页面项,每项4个字节,包含页面的起始地址和有关该页面的信息。页面的起始地址也是4K的整数倍,所以页面的低12位也留作它用.

    第31~12位是20位物理页面地址,除第6位外第0~5位及9~11位的用途和页目录项一样,第6位是页面项独有的,当对涉及的页面进行写操作时,D位被置1.

    4GB的内存只有一个页目录,它最多有1024个页目录项,每个页目录项又含有1024个页面项,因此,内存一共可以分成1024×1024=1M个页面。由于每个页面为4K个字节,所以,存储器的大小正好最多为4GB.

    4.3 线性地址到物理地址的转换


    32位线性地址到物理地址的转换

    • 1.CR3包含着页目录的起始地址,用32位线性地址的最高10位A31~A22作为页目录的页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,形成相应页表的地址。

    • 2.从指定的地址中取出32位页目录项,它的低12位为0,这32位是页表的起始地址。用32位线性地址中的A21~A12位作为页表中的页面的索引,将它乘以4,与页表的起始地址相加,形成32位页面地址。

    • 3.将A11~A0作为相对于页面地址的偏移量,与32位页面地址相加,形成32位物理地址。

    4.4 扩展分页


    从奔腾处理器开始,Intel微处理器引进了扩展分页,它允许页的大小为4MB

    扩展分页

    在扩展分页的情况下,分页机制把32位线性地址分成两个域:最高10位的目录域和其余22位的偏移量。

    4.5 页面高速缓存


    页面高速缓存

    由于在分页情况下,每次存储器访问都要存取两级页表,这就大大降低了访问速度。所以,为了提高速度,在386中设置一个最近存取页面的高速缓存硬件机制,它 自动保持32项处理器最近使用的页面地址,因此,可以覆盖128K字节的存储器地址。当进行存储器访问时,先检查要访问的页面是否在高速缓存中,如果在, 就不必经过两级访问了,如果不在,再进行两级访问。平均来说,页面高速缓存大约有98%的命中率,也就是说每次访问存储器时,只有2%的情况必须访问两级分页机构。这就大大加快了速度.

    展开全文
  •  Linux(x64CPU)使用基于分页机制的虚拟内存。每个进程有256TB(48)的虚拟地址空间。基于分页机制,这256TB地址空间的一些部分 被映射了物理内存,一些部分什么也没有映射。程序中使用的都是256TB地址空间中的...

    http://www.cnblogs.com/jiurl/p/4925007.html


        Linux(x64CPU)使用基于分页机制的虚拟内存。每个进程有256TB(48位)的虚拟地址空间。基于分页机制,这256TB地址空间的一些部分 被映射了物理内存,一些部分什么也没有映射。程序中使用的都是256TB地址空间中的虚拟地址。而访问物理内存,需要使用物理地址。

       物 理地址(physical address):放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果 是写,电路根据这个地址每位的值就将相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址与寻址的。

        虚拟地址(virtual address): 256TB虚拟地址空间中的地址,程序中使用的都是虚拟地址。

       如 果CPU相关寄存器中的分页标志位被设置,那么执行内存操作的机器指令时,CPU会自动根据相应转换结构(PML4,PDPT,PD,PT)中的信息,把 虚拟地址转换成物理地址,完成该指令。比如"mov eax,dword ptr[13FF3904Ch]",就是把地址13FF3904Ch处的值赋给寄存器的汇编代码,13FF3904Ch这个地址就是虚拟地址。CPU在执 行这行代码时,发现寄存器中的分页标志位已经被设定,就自动完成虚拟地址到物理地址的转换,使用物理地址取出值,完成指令。对于x86-64CPU 来说,寄存器CR0的第31位是分页标志位,为1表示使用分页,为0表示不使用分页。对于初始化之后的 Linux 我们通过qemu monitor 观察 CR0,发现第31位的值为1。表明Linux是使用分页的。有多个寄存器中的控制位与分页有关,具体细节可以参考Intel软件开发手册。

        x86-64 CPU的实际虚拟地址大小为48位(64位中的低48位,高16位不用于地址转换),可寻址256TB地址空间。x86-64 CPU的实际物理地址大小,不同的CPU不相同,但最大不超过52位。可以通过机器指令cpuid来查询该CPU的实际虚拟地址长度和实际物理地址长度。 例如,我查询了两台计算机,虚拟地址的大小两台计算机都为0x30(16进制30就是十进制48),物理地址的大小一台计算机为0x24(16进制24就 是十进制36,36位可寻址64GB),另一台计算机为0x27(16进制27就是十进制39,39位可寻址512GB)。为了后面的叙述方便,我们把 CPU实际物理地址的位数叫做M,比如,对于物理地址大小为36位的CPU,M就是36。

       使用了分页机制之后,256TB的地址空 间被分成了固定大小的页(有三种大小,4KB,2MB,1GB),每一页或者被映射到物理内存,或者没有映射任何东西。对于一般程序来说,256TB的地 址空间,只有一小部分(沧海一粟)映射了物理内存,大片大片的部分是没有映射任何东西。物理内存也被分页,来映射地址空间。对于x86-64,页的大小有 三种,分别是4KB,2MB,1GB。CPU用来把虚拟地址转换成物理地址的信息存放在叫做"page map level 4"," page directory pointer","pagedirectory"(页目录),"pagetable"(页表)的结构里。每个进程都有自己的一套 PML4,PDPT,PD,PT结构。一个进程中的虚拟地址,最多需要四级转换,来得到对应的物理地址。

       物理内存分页,一个物理页 的大小为4KB,第0个物理页从物理地址 0x0000000000000000处开始,大小为4KB(0x1000B),第1个物理页从物理地址 0x0000000000001000 处开始。第2页从物理地址0x0000000000002000处开始。由于页的大小是4KB,所以只需要64位的地址中的M-12bit(低12bit 之后)来寻址物理页。

        "pagetable"(页表),一个页表的大小为4KB,放在一个物理页中。由512个8字节的PTE(页表项)组成。页表项的大小为8个字节(64 位),所以一个页表中有512个页表项。页表中的每一项的内容(每项8个字节,64位)低12位之后的M-12位用来放一个物理页的物理地址,低 12bit放着一些标志。

        "pagedirectory"(页目录),一个页目录大小为4KB,放在一个物理页中。由512个8字节的PDE(页目录项)组成。页目录项的大小为8 个字节(64位),所以一个页目录中有512个页目录项。页目录中的每一项的内容(每项8个字节,64位)低12位之后的M-12位用来放一个页表(页表 放在一个物理页中)的物理地址,低12bit放着一些标志。

        "pagedirectory pointer"表,一个page directorypointer表大小为4KB,放在一个物理页中。由512个8字节的PDPTE项组成。PDPTE项的大小为8个字节(64位),所 以一个pagedirectory pointer表中有512个PDPTE。page directorypointer表中的每一项的内容(每项8个字节,64位)低12位之后的M-12位用来放一个页目录(页目录放在一个物理页中)的物 理地址,低12bit放着一些标志。

        "page maplevel 4"表,一个page map level4表大小为4KB,放在一个物理页中。由512个8字节的PML4E项组成。PML4E项的大小为8个字节(64位),所以一个pagemap level 4表中有512个PML4E。page map level4表中的每一项的内容(每项8个字节,64位)低12位之后的M-12位用来放一个page directory pointer表(pagedirectory pointer表放在一个物理页中)的物理地址,低12bit放着一些标志。

       对于x86-64系统,"page map level 4"表的物理地址放在CPU的CR3寄存器中。

       CPU把虚拟地址转换成物理地址:
       一 个虚拟地址,大小8个字节(64位,实际只使用低48位),包含着找到物理地址的信息,分为5个部分:第39位到第47位这9位(最高9位) 是"pagemap level 4"表中的索引,第30位到第38位这9位是"page directorypointer"表中的索引,第21位到第29位这9位是页目录中的索引,第12位到第20位这9位是页表中的索引,第0位到第11位 这12位(低12位)是页内偏移。对于一个要转换成物理地址的虚拟地址,CPU首先根据CR3中的值,找到"pagemap level4"表所在的物理页,然后根据虚拟地址的第39位到第47位这9位(最高9位)的值作为索引,找到相应的PML4E项,PML4E项中有这个虚 拟地址所对应的"pagedirectory pointer"表的物理地址。有了"page directorypointer"表的物理地址,根据虚拟地址的第30位到第38位这9位的值作为索引,找到该"page directorypointer"表中相应的PDPTE项,PDPTE项中有这个虚拟地址所对应的页目录的物理地址。有了页目录的物理地址,根据虚拟地 址的第21位到第29位这9位的值作为索引,找到该页目录中相应的页目录项,页目录项中有这个虚拟地址所对应的页表的物理地址。有了页表的物理地址,根据 虚拟地址的第12位到第20位这9位的值作为索引,找到该页表中相应的页表项,页表项中有这个虚拟地址所对应的物理页的物理地址。最后用虚拟地址的最低 12位,也就是页内偏移,加上这个物理页的物理地址,就得到了该虚拟地址所对应的物理地址。

        一个"page maplevel 4"表有512项,虚拟地址从48位向低走的9位刚好可以索引512项(2的9次方等于512),一个"pagedirectorypointer"表有 512项,虚拟地址接下来的9位刚好索引512项。一个页目录有512项,虚拟地址接下来的9位刚好索引512项。一个页表有512项,虚拟地址接下来的 9位刚好索引512项。虚拟地址最低的12位(2的12次方等于4096),作为页内偏移,刚好可以索引4KB,也就是一个物理页中的每个字节。

       一 个虚拟地址转换成物理地址的计算过程就是,处理器通过CR3找到当前"page map level4"表所在物理页,取虚拟地址从48位向低走的9位,然后把这9位右移3位(因为每个PML4E项8个字节长,右移3位相当于乘8)得到在该页 中的地址,取出该地址处的PML4E(8个字节),就找到了该虚拟地址对应"pagedirectorypointer"表所在物理页,然后同样方法依次 找出该虚拟地址对应的页目录所在物理页,该虚拟地址对应的页表所在物理页,该虚拟地址对应的物理页的物理地址,最后将虚拟地址对应的物理页的物理地址加上 12位的页内偏移得到了物理地址。

       48位的一个指针,可以寻址范围0x000000000000-0xFFFFFFFFFFFF,256TB大小。也就是说一个48位的指针可以寻址整个256TB地址空间的每一个字节。
    一个页表项负责4KB的地址空间和物理内存的映射,一个页表512项,也就是负责512*4KB=2MB的地址空间的映射。
    一个页目录项,对应一个页表。一个页目录有512项,也就对应着512个页表,每个页表负责2MB地址空间的映射,512个页表负责512*2MB=1GB的地址空间映射。
    一个PDPTE项,对应一个页目录。一个"page directorypointer"表有512项,也就对应着512个页目录,每个页目录负责1GB地址空间的映射。512个页目录负责512*1GB=512GB的地址空间映射。
    一 个"page map level 4"表项,对应一个"page directory pointer"表。一个"page maplevel 4"表有512项,也就对应着512个"page directory pointer"表,每个"page directorypointer"表负责512GB地址空间的映射。512个"page directorypointer"表负责512*512GB=256TB的地址空间映射。
    一个进程有一个"page map level4"表。所以以页为单位,一套PML4,PDPT,PD,PT结构可以保证256TB的地址空间中的每页和物理内存的映射。

       一 个虚拟地址在转换过程中,如果发现对应的PDPTE项的PS位为0则继续之后的转换步骤,如果发现对应的PDPTE项的PS位为1,则本PDPTE项中的 物理地址,就是该虚拟地址对应的一个大小为1GB物理页的地址,该虚拟地址之后的30位为这个1GB物理页的页内偏移,就可以得到该虚拟地址对应的物理地 址。

       一个虚拟地址在转换过程中,如果发现对应的页目录项的PS位为0则继续之后的转换步骤,如果发现对应的页目录项的PS位为1, 则本页目录项中的物理地址,就是该虚拟地址对应的一个大小为2MB物理页的地址,该虚拟地址之后的21位为这个2MB物理页的页内偏移,就可以得到该虚拟 地址对应的物理地址。

       虚拟地址48位,寻址256TB。48位地址:9bit+9bit+9bit+9bit+12bit。
    每一级表的大小都为4KB。表项大小8B(64位),所以每个表512项。
    4级转换:CR3-> "page map level 4"表-> "page directorypointer"表-> 页目录-> 页表-> 得到物理地址。
    一个PTE项对应4KB地址范围。一张PT表对应512*4KB=2MB地址范围。
    一个PDE项对应2MB地址范围。一张PD表对应512*2MB=1GB地址范围。
    一个PDPTE项对应1GB地址范围。一张PDPT表对应512*1GB=512GB地址范围。
    一个PML4E项对应512GB地址范围。一张PML4表对应512*512GB=256TB地址范围。
    页大小有3种,4KB,2MB,1GB。

       每 个进程都有自己的256TB地址空间,"0000000000000000 - 00007fffffffffff" 和"ffff800000000000 - ffffffffffffffff" 。因为CPU会忽略高16位,所以相当于"000000000000 - 7fffffffffff" 和 "800000000000 -ffffffffffff"。
    因为CPU有特殊规定,被忽略的高16位的每一 位必须等于第47位的值(并会做检查,不符合规定将引发异常)。"000000000000 -7fffffffffff"的第47位是0,所以高16位也必须都为0。"800000000000 -ffffffffffff"的第47位是1,所以高16位也必须都为1。
    每个进程都有自己的256TB地址空间,通过每个进程自己的一套 PML4,PDPT,PD,PT结构来实现。由于每个进程有自己的一套PML4,PDPT,PD,PT结构,所以每个进程的地址空间映射的物理内存是不一 样的。两个进程的同一个虚拟地址处(如果都有物理内存映射)的值一般是不同的,因为他们往往对应不同的物理页。

       256TB地址空间 中低128TB,0x000000000000-0x7fffffffffff是用户地址空间,256TB地址空间中高 128TB,0x800000000000-0xffffffffffff是系统地址空间。访问系统地址空间需要程序有ring0的权限。
    展开全文
  • 一、 分页机制 分页机制在段机制之后进行,完成线性地址到物理地址的转换。段机制把虚拟地址转换为线性地址,分页机制进一步把该线性地址转换为物理地址。 如果不允许分页(控制寄存器CR0的最高位置0),那么经过...
  • 分页管理机制 内存管理 在linux0.11中对内存的管理是以页为单位的,一个页面是指连续的4K字节物理内存。 通过页目录和页表项可以寻址或管理指定的页面。 内存管理的主要功能 虚拟地址–&...
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-09-01 ...1 分页机制在虚拟内存中,页表是个映射表的概念, 即从进程能理解的线性地址(linear address)映射到存储器上的物理地址(phisical address).
  • Linux分页机制

    2013-09-29 21:41:05
    先前我们介绍段机制的时候说到,x86的段机制把程序的逻辑地址转换成线性地址,这里要讲的分页机制是把线性地址映射成物理地址,也就说说,x86其实是用了两套机制把逻辑地址转换成物理地址的。我们也提到linux内核是...
  • linux 内存分页机制

    2013-10-15 21:52:02
    涉及的硬件平台是X86,如果... ——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,
  • 内存分段机制与分页机制

    千次阅读 2017-01-12 14:29:49
    (一):逻辑地址(Logical Address)   指由程式产生的和段相关的偏移...只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel保护模式
  • ——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到 最大空量逐字节的编号的大数组,然后把这个数组叫做物理...
  • linux下的虚拟内存分页分段机制

    千次阅读 2017-08-04 12:08:57
    前言:由于计算机的内存是有限的,比0到2的32次方就是对应4G,这时候操作系统就引入了虚拟内存这一个概念, 1,首先可以从程序的编译下手,对于java或者c++而言,程序在编译的时候需要内存,但是此时程序并没有在真...
  • 内存分页机制

    千次阅读 2018-09-28 10:07:26
    为什么要分页 在保护模式中,内存访问使用分段机制——即&amp;quot;段基址:段内偏移地址&amp;quot;的方式,为加强段内存的安全性和可管理性还引入了段描述符的概念对段内存加强管理。但仅仅这样还是不够的...
  • 内存分段分页机制理解

    千次阅读 2019-09-03 23:22:45
    现代操作系统,计算机内存是按照先分段再分页的方式管理的。 注意: 以下描述都是基于32位计算机进行描述的。 1. 分段 1) 程序直接使用物理地址的问题 考虑最原始,最直接的情况,程序中访问的地址都直接对应于物理...
  • 分段机制将逻辑地址转换成线性地址后,还需要借助分页机制,将线性地址转换为物理地址;  内存分页管理机制的基本原理是将整个线性内存区域划分成4KB为单位的内存页面;  32位处理器可以支持的寻址空间为2^32 =4GB...
  • 分页机制让我们在程序开发的时候不需要管理内存的,也不需要操心内存够不够用它是实现虚拟存储的关键,位于线性地址与物理地址之间。 整个主内存区域划分成 4096 字节(4k)为一页的内存页面 程序申请使用内存时,...
  • 内存管理与分页机制

    万次阅读 多人点赞 2017-09-04 19:02:44
    一、问题提出:我们经常会使用malloc()以及free()函数进行堆区内存申请与释放。那么你是否会这样做:int * p = malloc(0);/*malloc分配了0个字节吗,如果是那么p指向谁呢,是NULL吗*/ free(p);/*假如malloc分配了0个...
  • Linux分页机制分页机制的演变--Linux内存管理(七) 2016年09月01日 20:01:31JeanCheng阅读数:4543 https://blog.csdn.net/gatieme/article/details/52402967 ~ 版权声明:本文为博...
1 2 3 4 5 ... 20
收藏数 22,566
精华内容 9,026
关键字:

32位内存分页机制 linux