目录


原理基础

“32-Bit 分页” 模式指的是当系统开启分页功能 (CR0.PG 标志位置位),且关闭 PAE 模式 (CR4.PAE 标志位清零) 时的分页模式。”32-Bit 分页” 模式用于将一个 32 位的线性地址 (虚拟地址) 转换成一个 40 位的物理地址。虽然物理地址的位宽高达 40 位,且可寻址高达 1TiB 的物理内存,但由于线性地址 (虚拟地址) 限制在 32 位,因此在任意时刻,最多 4Gig 的物理内存可以直接使用 (直接寻址).

地址位宽/寻址范围

在 “32-Bit 分页” 模式下,分页支持 32 位的线性地址映射 4KiB 物理页的能力,也支持映射 4MiB 物理页的能力。默认情况下 “32-Bit 分页” 模式支持线性地址映射 4KiB 物理页的能力,至于映射 4MiB 物理页的能力与 CPUID.01H 的 PSE 标志位有关,当该标志位置位,那么 “32-Bit 分页” 模式可以支持映射 4MiB 物理页的能力,此时 “32-Bit 分页” 模式可以通过控制 CR4.PSE 标志位来决定是否启用或关闭映射 4MiB 物理页的能力。在 “32-Bit 分页” 模式下,线性地址 (虚拟地址) 的位宽为 32 位,可以寻址 4Big 线性地址空间 (虚拟内存空间). 在该分页模式下,可映射高达 40 位的物理地址,但由于映射物理页的能力不同,导致物理地址的 32:39 字段的布局有所不同, 具体分如下几种模式:

普通模式

当 “32-Bit 分页” 模式不支持映射 4MiB 物理页,即 CR4.PSE 标志位清零或者 CPUID.01H 的 PSE 标志位直接清零,那么称这种模式为普通模式。普通模式下物理地址的 39:32 字段全为 0,其可以寻址 4Gig 的物理内存.

NPSE36 模式

当 “32-Bit 分页” 模式支持映射 4MiB 物理页但不支持 PSE-36 机制,即 CR4.PSE 标志置位且 CPUID.01H PSE 标志位置位,但 CPUID.01H PSE-36 标志位清零,那么称这种模式为 NPSE36 模式. 在这种模式下,虽然可以映射 4MiB 的物理页,但物理地址位宽还是 32 位,因此可寻址的物理内存为 4 Gig.

PSE36 模式

当 “32-Bit 分页” 模式支持映射 4MiB 物理页且支持 PSE-36 机制,即 CR4.PSE 标志位、CPUID.01H PSE 标志位和 CPUID.01H PSE-36 标志位全置位,那么称这种模式为 PSE36 模式。在这种模式下,原则上 “32-Bit 分页” 模式可以支持 40 位的物理地址,但实际的物理地址位宽与 MAXPHYADDR 有关 (MAXPHYADDR 通过 CPUID.80000008H 寄存器的 EAX 低 8 位进行维护). 如果 MAXPHYADDR 小于 40,那么物理地址 “MAXPHYADDR:39” 字段全为 0,其物理地址可寻址范围为 “1 << MAXPHYADDR”; 如果 MAXPHYADDR 等于 40,那么物理地址位宽可达 40 位,此时可寻址的物理内存高达 1TiB.


页表结构

“32-Bit 分页” 模式采用分级的页表架构将一个线性地址 (虚拟地址) 转换成物理地址。线性地址既可以映射 4 KiB 物理页,也可以映射 4 MiB 物理页。映射物理页大小不同导致页表的级数和页表结构有所不同,但线性地址都可以通过页表找到一个物理地址 (这个结论的前提是页表已经建立好了,不包括没有建立页表的情况).

“32-Bit Paging” 模式映射物理页的大小与 CPUID.01.PSE 标志位、CR4.PSE 标志位以及 PDE.PS 标志位有直接联系,CPUID.01.PSE 标志位用于指明 “32-Bit Paging” 模式是否具有映射 4MiB 物理页的能力, 如果该标志位清零,那么不具有该能力,此时页表只能映射 4KiB 物理页; 反之 CPUID.01.PSE 标志位置位,那么页表具有能力映射 4MiB 物理页,但此时还与 CR4.PSE 标志位和 PDE.PS 标志位有关。CR4.PSE 标志位用于指明是否启用映射 4MiB 物理页的功能,即在 CPUID.01.PSE 置位的情况下,CR4.PSE 标志位置位,那么 “32-Bit Paging” 模式可以启用映射 4MiB 物理页的能力,但此时还与 PDE.PS 标志位有关. PDE.PS 标志位用于指明 “Page Directory Table” 的 PDE 是否映射 4MiB 物理页,如果该标志位置位,那么当前页表正在映射一个 4MiB 的物理页; 反之 PDE.PS 标志位清零,那么当前页表在映射一个 4KiB 的物理页。如果 CPUID.01H.PSE 标志位清零, 那么 “32-Bit Paging” 模式只能映射 4KiB 物理页. 接下来详细介绍两种情况下的页表结构.

Paging Struct for 4 KiB Page

Paging Struct for 4 MiB Page


Paging Struct for 4 KiB Page

在映射 4 KiB 物理页的情况下,”32-Bit Paging” 模式采用两级页表,第一级页表称为 “Page Directory” 页表,第二级页表称为 “Page Table”。每一级页表的大小为一个物理页,内核将页表分成长度为 “unsigend long” 的项,每个项称为下一级页表的 entry (入口项). 在该模式下, “unsigned long” 的长度为 4 个字节,因此每个页表包含了 1024 (PAGE_SIZE/sizeof(unsigned long)) 个 entry。当 MMU 获得一个线性地址之后,MMU 将通过硬件自动查询已经准备好的页表,查询过程如下:


CR3

当 “32-Bit Paging” 模式遍历页表的时候,MMU 硬件首先读取当前进程 CR3 寄存器,CR3 寄存器存储了线性地址 (虚拟地址) 第一级页表所在的物理地址, MMU 可以通过 CR3 寄存器找到 “Page Directory” 的物理地址。

CR3 控制寄存器是系统多个控制寄存器中的一个,其主要目的用于存储第一级页表的物理地址和相应的访问权限,CR3 寄存器的布局如上,每个字段的含义如下:

CR3 31:12

在 “32-Bit Paging” 模式下,CR3 的 31:12 字段存储了第一级页表 (Page Directory) 的起始物理页帧号,MMU 硬件通过该字段可以自动获得下一级页表的物理地址,其计算方法如下:

The Physical Address for next page director table:

   PHY = CR3 & PAGE_MASK

CR3 on 32-Bit Paging Mode


Page Directory Table

当 “32-Bit Paging” 模式从 CR3 寄存器获得第一级页表所在的物理地址时,MMU 硬件会自动找到第一级页表。在 “32-Bit Paging” 模式下,第一级页表称为 “Page Directory” 页表,该页表将一个 PAGE_SIZE 的内存划分为长度为 “sizeof(unsigned long)” 的项,每个项称为 “Page Directory Entry”, 简称 PDE. 因此在 “32-Bit Paging” 模式下,每个 PDE 的长度为 4,那么 “Page Directory” 页表包含了 1024 个 PDE. 在遍历页表时,MMU 硬件以线性地址 (虚拟地址) 31:22 字段的值作为索引,再以 “Page Directory” 页表的起始物理地址作为 “unsigned long” 数组的基地址,在该数组中找到对应的 PDE.

PDE 用于存储下一级页表的物理地址信息和页表访问权限字段,MMU 硬件会根据 PDE 的内容进行权限检测,检测通过之后根据 PDE 找到下一级页表. PDE 的布局如上图,每个字段的含义如下:

PDE Bit-0 P (Present)

PDE Bit-1 R/W (Read/Write)

PDE Bit-2 U/S (User/Supervsion)

PDE Bit-3 PWT

PDE Bit-4 PCD

PDE Bit-5 A (Accessed)

PDE Bit-7 PS (Page Size)

PDE Bit-12:31 Address of Page Table


PDE P Bit-0

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PDE 的 Bit-0 标志位称为 “Present” (P) 标志位,该标志位用于指明当前的 PDE 是否存在. 当该标志位为 1 的时候表示 “32-Bit Paging” 模式的 PDE 存在,MMU 可以通过 PDE 获得下一级页表的物理地址和一些访问信息.

如果 PDE 的 P 标志位清零,那么该 PDE 是一个无效的 PDE,MMU 遇到这种情况可能会触发一个 Page Fault 异常.


PDE R/W Bit-1

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PDE 的 Bit-1 标志位称为 “Read/Write” 标志位。通过一个 PDE 页表可以访问 4MiB 的物理内存空间 (这里与映射 4MiB 物理页区分开来,访问代表 PDE 指向一个 “Page Table” 页表,该页表又包含很多的 PTE,每个 PTE 指向一个 4KiB 的物理页,因此一个 PDE 可以访问 1024 * 4KiB = 4MiB 的物理内存空间). R/W 标志位如果置位,那么可以对 4MiB 的物理内存进行读写操作; 反之如果该标志位清零,那么对 4MiB 的物理内存只能读不能写.


PDE U/S Bit-2

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PDE 的 Bit-2 标志位称为 “User/Supervisor”, 该标志位用于指明线性地址 (虚拟地址) 来自用户空间还是内核空间,也可以理解为用户空间能否范围 4MiB 的物理内存空间 (这里与映射 4MiB 物理页区分开来,访问代表 PDE 指向一个 “Page Table” 页表,该页表又包含很多的 PTE,每个 PTE 指向一个 4KiB 的物理页,因此一个 PDE 可以访问 1024 * 4KiB = 4MiB 的物理内存空间). 当该标志位置位的时候,线性地址 (虚拟地址) 来自用户空间,也就是用户空间可以访问 4MiB 的物理内存空间; 反之线性地址 (虚拟地址) 来自内核空间,也就是 4MiB 的物理内存空间只有内核才能访问.


PDE A Bit-5

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PDE 的 Bit-5 标志位称为 “Accessed” (简称 A),该标志位用于指明 PDE 对应的 4MiB 空间是否被访问过 (这里与映射 4MiB 物理页区分开来,访问代表 PDE 指向一个 “Page Table” 页表,该页表又包含很多的 PTE,每个 PTE 指向一个 4KiB 的物理页,因此一个 PDE 可以访问 1024 * 4KiB = 4MiB 的物理内存空间)。这里的访问包括读和写操作,当 4MiB 的物理内存空间被访问过,那么 MMU 硬件会自动将该标志为置位; 反之如果该标志位保持清零状态,那么对应的 4MiB 物理内存空间没有被访问过.


PDE PS Bit-7

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PDE 的 Bit-7 必须清零,该标志为称为 PS (Page Size), 用于描述映射物理页的大小。”32-Bit Paging” 模式映射物理页的大小与 CPUID.01.PSE、CR4.PSE 和 PDE.PS 标志位有关。当三个标志位同时置位的情况下,页表才能映射 4MiB 的物理页,否则页表只能映射 4KiB 的物理页,因此映射 4KiB 物理页的时候,PS 标志位必须清零.


PDE 31:12

PDE 的 31:12 字段存储了下一级页表 (Page Table) 的起始物理地址对应的物理页帧号, 因此下一节页表的物理地址计算方法如下:

The Physical Address for next page table:

   PHY = PDE & PAGE_MASK


Page Table

当 “32-Bit Paging” 模式从 PDE 中获得下一级页表所在物理地址的页帧号之后,MMU 硬件会自动找到下一级页表。在映射 4KiB 物理页的 “32-Bit Paging” 模式下,找到的页表为最后一级页表,称为 “Page Table”, 该页表大小为 PAGE_SIZE, 同上一级页表一样,将页表划分为 unsigned long 的单元。由于 unsigned long 的长度为 4,因此 “Page Table” 中一共包含了 1024 (PAGE_SIZE / sizeof(unsigned long)) 个单元。将这些单元称为 “Page Table Entry”, 简称 PTE. 在遍历页表的时,MMU 硬件以线性地址 (虚拟地址) 的 21:12 字段作为索引,再以 “Page Table” 的起始物理地址作为 “unsigned long” 数组的基地址,在该数组中找到对应的 PTE.

PTE 用于存储映射物理页对应的物理页帧号,以及页表的访问权限字段,MMU 硬件会根据 PTE 的内容进行权限检测,检测通过之后根据 PTE 找到最终映射的 4KiB 物理页。PTE 的布局如上图,每个字段的含义如下:

PTE Bit-0 P (Present)

PTE Bit-1 R/W (Read/Write)

PTE Bit-2 U/S (User/Supervisor)

PTE Bit-3 PWT

PTE Bit-4 PCD

PTE Bit-5 A (Accessed)

PTE Bit-6 D (Dirty)

PTE Bit-7 PAT

PTE Bit-8 G (Global)

PTE Bit-31:12 Address of 4KiB Page Frame


PTE P Bit-0

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PTE 的 Bit-0 标志位称为 “Present” (P) 标志位,该标志位用于指明当前的 PDE 是否存在。当该标志位为 1 的时候表示 “32-Bit Paging” 模式的 PTE 存在。MMU 可以通过 PTE 获得最终 4 KiB 物理页对应的页帧号和一些访问信息.

如果 PTE 的 P 标志位清零,那么该 PTE 是一个无效的 PTE, MMU 遇到这种情况可能会触发一个 Page Fault 异常.


PTE R/W Bit-1

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PTE 的 Bit-1 标志位称为 “Read/Write” 标志位。通过一个 PTE 页表可以访问 4KiB 的物理内存空间. R/W 标志位如果置位,那么可以对 4KiB 的物理内存进行读写操作; 反之如果该标志位清零,那么对 4KiB 的物理内存只能读不能写.


PTE U/S Bit-2

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PTE 的 Bit-2 标志位称为 “User/Supervisor”, 该标志位用于指明线性地址 (虚拟地址) 来自用户空间还是内核空间,也可以理解为用户空间能否范围 4KiB 的物理内存空间. 当该标志位置位的时候,线性地址 (虚拟地址) 来自用户空间,也就是用户空间可以访问 4KiB 的物理内存空间; 反之线性地址 (虚拟地址) 来自内核空间,也就是 4KiB 的物理内存空间只有内核才能访问.


PTE A Bit-5

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PTE 的 Bit-5 标志位称为 “Accessed” (简称 A),该标志位用于指明 PTE 对应的 4KiB 空间是否被访问过。这里的访问包括读和写操作,当 4KiB 的物理内存空间被访问过,那么 MMU 硬件会自动将该标志为置位; 反之如果该标志位保持清零状态,那么对应的 4KiB 物理内存空间没有被访问过.


PTE D Bit-6

在映射 4 KiB 物理页的 “32-Bit Paging” 模式下,PTE 的 Bit-6 标志位称为 “Dirty” (简称 D),该标志用于指明 PTE 对应的 4 KiB 物理空间是否写入内容。当 4 KiB 的物理内存空间被写入之后,MMU 自动将该标志位置位。如果该标志为清零,那么表示 4 KiB 的物理内存没有被写入.


PTE 31:12

PTE 的 31:12 字段存储了 4 KiB 物理页的物理页帧号, 因此 4 KiB 物理页的物理地址计算方法如下:

The Physical Address for 4 KiB Page:

   PHY = PDE & PAGE_MASK


32-Bit Paging 实践部署


Translate Userspace Address With 4K Page 实践部署

实践准备

在 “32-Bit Paging” 模式下,线性地址 (虚拟地址) 在 i386 架构下默认映射 4 KiB 物理页,因此本节以 BiscuitOS 在 i386 架构下的实践为基础进行讲解。首先开发者基于 BiscuitOS 搭建一个 i386 架构的开发环境,请开发者参考如下文档:


实践部署

在部署完毕开发环境之后,开发者接下来部署实践所需的源码,源码分为内核部分和用户空间部署,具体部署方法参考如下:

cd BiscuitOS
make linux-5.0-i386_defconfig
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Translate Userspace Address With 4K Page (Userspace+)  --->
              -*- Translate Userspace Address With 4K Page (Kernel+)  --->

make

源码配置完毕之后,执行 make 进行实际的源码部署,部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在两个目录 “X86-Paging-32bit-4K-Page-userspace-default” 和 “X86-Paging-32bit-4K-Page-kernel-default”, 两个目录分别存储内核空间部分的代码和用户空间部分的代码。接下来开发者先编译用户空间的代码,使用如下命令:

cd X86-Paging-32bit-4K-Page-userspace-default
make download
make
make install
make pack
tree

执行完上面的命令之后,用户空间的代码已经编译打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是用户空间部分核心实现. 接下来编译内核部分的代码,使用如下命令:

cd X86-Paging-32bit-4K-Page-kernel-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例.


实践执行

环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在 BiscuitOS 内部可以直接运行 RunBiscuitOS.sh 来执行实践,也可以在 BiscuitOS 内部通过如下命令进行:

insmod /lib/modules/$(uname -r)/extra/X86-Paging-32bit-4K-Page-kernel-default.ko
X86-Paging-32bit-4K-Page-userspace-default

从运行的情况可以看出,用户空间访问一个没有建立页表的地址,然后触发缺页,缺页机制在该源码内核部分建立了页表,并使用物理页 0x1f7bf 作为 4KiB 物理页进行映射。页表建立之后,定时器过了一段时间查询用户空间刚刚发生缺页的虚拟地址对应的 PTE 页表,通过找到的 PTE 页表中获得物理页的信息,该物理页与缺页的一致。具体的源码分析可以查看:

Translate Userspace Address With 4K Page 源码分析


Translate Kernel Address With 4K Page 实践部署

实践准备

在 “32-Bit Paging” 模式下,线性地址 (虚拟地址) 在 i386 架构下默认映射 4 KiB 物理页,因此本节以 BiscuitOS 在 i386 架构下的实践为基础进行讲解。首先开发者基于 BiscuitOS 搭建一个 i386 架构的开发环境,请开发者参考如下文档:


实践部署

在部署完毕开发环境之后,开发者接下来部署实践所需的源码,源码只包含内核部分,具体部署方法参考如下:

cd BiscuitOS
make linux-5.0-i386_defconfig
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Translate Kernel Address With 4K Page  --->

make

源码配置完毕之后,执行 make 进行实际的源码部署,部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在 “X86-Paging-32bit-Kernel-4K-Page-default” 目录, 该目录存储相关的内核源代码。接下来开发者使用如下命令:

cd X86-Paging-32bit-Kernel-4K-Page-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例 .


实践执行

环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在内核启动过程中,该模块自动加载运行,从运行的结果可以看到模块为 0xe0ee0000 开始的虚拟地址建立了页表,并将 88520 存储到了该虚拟地址,最后使用 printk() 函数将 0xe0ee0000 处的内容输出,此时输出的内容正好是写入的内容.

Translate Kernel Address With 4K Page 源码分析


Translate Userspace Address With 4M Page 实践部署

实践准备

在 “32-Bit Paging” 模式下,线性地址 (虚拟地址) 在 i386 架构下支持映射 4 MiB 物理页,因此本节以 BiscuitOS 在 i386 架构下的实践为基础进行讲解。首先开发者基于 BiscuitOS 搭建一个 i386 架构的开发环境,请开发者参考如下文档:


实践部署

在部署完毕开发环境之后,开发者接下来部署实践所需的源码,源码分为内核部分和用户空间部署,具体部署方法参考如下:

cd BiscuitOS
make linux-5.0-i386_defconfig
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Translate Userspace Address With 4M Page (Userspace+)  --->
              -*- Translate Userspace Address With 4M Page (Kernel+)  --->

make

源码配置完毕之后,执行 make 进行实际的源码部署,部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在两个目录 “X86-Paging-32bit-4M-Page-userspace-default” 和 “X86-Paging-32bit-4M-Page-kernel-default”, 两个目录分别存储内核空间部分的代码和用户空间部分的代码。接下来开发者先编译用户空间的代码,使用如下命令:

cd X86-Paging-32bit-4M-Page-userspace-default
make download
make
make install
make pack
tree

执行完上面的命令之后,用户空间的代码已经编译打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是用户空间部分核心实现. 接下来编译内核部分的代码,使用如下命令:

cd X86-Paging-32bit-4M-Page-kernel-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例.


实践执行

环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在 BiscuitOS 内部可以直接运行 RunBiscuitOS.sh 来执行实践,也可以在 BiscuitOS 内部通过如下命令进行:

insmod /lib/modules/$(uname -r)/extra/X86-Paging-32bit-4M-Page-kernel-default.ko
X86-Paging-32bit-4M-Page-userspace-default

从运行的情况可以看出,用户空间访问一个没有建立页表的地址,然后触发缺页,缺页机制在该源码内核部分建立了页表,并使用物理页 0x1f7bf 作为 4MiB 物理页进行映射。页表建立之后,定时器过了一段时间查询用户空间刚刚发生缺页的虚拟地址对应的 PDE 页表,通过找到的 PDE 页表中获得物理页的信息,该物理页与缺页的一致。具体的源码分析可以查看:

Translate Userspace Address With 4M Page 源码分析


Translate Kernel Address With 4M Page 实践部署

实践准备

在 “32-Bit Paging” 模式下,线性地址 (虚拟地址) 在 i386 架构下支持映射 4 KiB 物理页,因此本节以 BiscuitOS 在 i386 架构下的实践为基础进行讲解。首先开发者基于 BiscuitOS 搭建一个 i386 架构的开发环境,请开发者参考如下文档:


实践部署

在部署完毕开发环境之后,开发者接下来部署实践所需的源码,源码只包含内核部分,具体部署方法参考如下:

cd BiscuitOS
make linux-5.0-i386_defconfig
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Translate Kernel Address With 4M Page  --->

make

源码配置完毕之后,执行 make 进行实际的源码部署,部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在 “X86-Paging-32bit-Kernel-4M-Page-default” 目录, 该目录存储相关的内核源代码。接下来开发者使用如下命令:

cd X86-Paging-32bit-Kernel-4M-Page-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例 .


实践执行

环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在内核启动过程中,该模块自动加载运行,从运行的结果可以看到模块为 0xbe800000 开始的虚拟地址建立了 4M 页表,并将 88520 存储到了该虚拟地址, 再将 52088 存储在 0xbe900000,最后使用 printk() 函数将 0xbe800000 和 0xbe900000 处的内容输出,此时输出的内容正好是写入的内容.

Translate Kernel Address With 4M Page 源码分析


Translate Userspace Address With 4K Page 源码分析

BiscuitOS 提供了一个完整的用户空间虚拟地址的 “32-Bit Paging” 4K Page 案例,该案例通过在用户空间 mmap 一段虚拟内存之后,对虚拟内存进行写操作并触发缺页操作。发生缺页之后,内核部分的代码捕获到该缺页行为,并为发生缺页的虚拟地址分配物理内存。缺页异常处理完毕之后,用户空间开始正常使用虚拟地址。内核部分同时启用了一个定时器,定时扫描发生缺页的虚拟地址的 PTE 页表。整个流程涵盖了 “32-Bit Paging” 模式下 4KiB 物理页的缺页、建立页表,以及遍历页表。具体函数实现如下:

Translate Userspace Address With 4K Page 源码实践部署

Translate Userspace Address With 4K Page 用户空间源码解析

Translate Userspace Address With 4K Page 内核空间源码解析

Translate Userspace Address With 4K Page 用户空间源码 (Gitee)

Translate Userspace Address With 4K Page 内核空间源码 (Gitee)


Translate Userspace Address With 4K Page 用户空间源码解析

用户空间的源码布局如上图,bsbit 目录以及顶层目录的 Makefile 用于源码在 BiscuitOS 编译逻辑,开发者可以忽略这些文件。X86-Paging-32bit-4K-Page-userspace-default 目录下的 main.c 文件为用户空间触发缺页,以及虚拟地址使用的逻辑。同目录下的 Makefile 则用于编译 main.c 的编译逻辑. main.c 函数的实现逻辑通过 mmap() 从 “/dev/BiscuitOS” 设备映射一段虚拟内存,然后对这段虚拟内存进行写操作,由于此时页表还没有建立,那么此时会触发缺页异常,该异常通过内核缺页异常之后传递给对应的内核部分,内部部分为虚拟地址建立页表。缺页异常处理完毕之后,用户空间代码继续访问虚拟内存。访问完毕之后用户空间释放虚拟地址并关闭 “/dev/BiscuitOS” 节点. main.c 函数具体实现如下:

函数首先在 30 行调用 open() 函数打开 “/dev/BiscuitOS” 节点,以此与内核部分挂钩。函数接着在 37 行调用 mmap() 函数从 “/dev/BiscuitOS” 节点中映射一段虚拟内存,虚拟内存的长度为 BISCUITOS_MAP_SIZE, 即为 PAGE_SIZE. 当映射成功之后函数在 50 行对虚拟地址进行了一次写操作,此时由于映射的虚拟地址还没有与物理内存建立页表映射,因此此时会触发内核的缺页异常。用户空间的程序中断执行,内核执行缺页异常处理程序,缺页异常处理流程找到了 “/dev/BiscuitOS” 对应的接口之后,为虚拟地址建立 “32-Bit Paging” 模式下的 4K Page 页表,并分配物理内存。缺页中断执行完毕之后,用户空间的程序继续执行,函数在 51 行将虚拟地址对应的内容通过 printf() 函数打印出来。函数 sleep 3 秒之后,函数调用 unmmap() 函数将映射的虚拟地址与物理地址解除映射。最后函数在 58 行调用 close() 函数关闭了打开的 “/dev/BiscuitOS” 节点.

Translate Userspace Address With 4K Page 用户空间源码


Translate Userspace Address With 4K Page 内核空间源码解析

内核空间的源码布局如上图,顶层目录的 Makefile 用于源码在 BiscuitOS 编译逻辑,开发者可以忽略该文件。X86-Paging-32bit-4K-Page-kernel-default 目录下的 main.c 文件用于实现用户空间缺页触发的缺页异常处理,为缺页的虚拟地址建立页表和分配物理内存,并启用一个内核定时器,定时查看指定虚拟地址对应的页表内容. 同级目录下的 Makefile 和 Kconfig 用于内核 Kbuild 编译逻辑。main.c 文件的逻辑框架通过 MISC 子系统向内核注射了 “/dev/BiscuitOS” 接口,并对打开该接口的文件提供了 mmap 接口,当用户空间通过 mmap 映射一段虚拟内存时,main.c 文件会对该虚拟地址进行标记,并提供 fault 接口。当用户空间发生缺页时,缺页异常处理程序就会调用到该 fault 接口。在 fault 接口中,main.c 文件为缺页的虚拟地址分配物理内存,并为其建立虚拟地址到物理地址的页表。main.c 文件定义了一个内核定时器,并定期查询指定虚拟地址对应的 PTE 页表. main.c 文件的具体实现如下:

函数在 218 行调用 misc_register() 函数向内核注册了一个混杂设备,注册成功之后会在 “/dev” 目录下创建 BiscuitOS 节点。混杂设备提供了 mmap 接口,其实为 BiscuitOS_mmap() 函数,在 196 行,该函数为每个映射虚拟地址的 struct vm_area_struct 结构绑定了一个 struct vm_operations_struct 数据结构,该结构只提供了 vma 的 fault 接口。函数还在 222-224 行定义了一个定时器,其定时器处理函数为 BiscuitOS_scanner_pte().

当用户空间打开 “/dev/BiscuitOS” 节点,调用 mmap() 函数映射一段虚拟内存,且用户空间访问这段虚拟地址发生缺页异常,那么缺页异常处理逻辑最终会调用到 vm_fault() 函数。当调用到 vm_fault() 函数时,函数首先在 157 行调用 alloc_page() 函数从 Buddy 分配器中分配一个物理页,然后调用 BiscuitOS_build_page_table() 函数为缺页的虚拟地址建立页表,并将虚拟地址映射到刚分配的物理地址上。函数在 168 行将 vmf 的 page 成员指向刚被映射的物理页上,这步操作主要是为了缺页异常处理对绑定的物理页进行统计和加锁等操作。函数接着在 172-172 行记录了缺页的虚拟地址和对应的 struct vm_area_struct 数据结构,函数在 175 行进行返回.

BiscuitOS_build_page_table() 函数用于为缺页的虚拟地址创建页表,并将虚拟地址映射到物理地址上。由于在 “32-Bit Paging” 模式下,映射 4KiB 物理页只需 2 级页表,因此函数首先在 88 行调用 pgd_offset() 函数获得虚拟地址对应的 PDE 入口,函数在 93 行将 pgd 转换成 pde,以此兼容软件层面的 PDE. 函数接着在 96 行调用 __pte_alloc_func() 函数,该函数其实对应着内核的 __pte_alloc() 函数,用于分配一个 Page Table。分配完毕之后,函数在 97 行检测 Page Table 分配是否成功,如果失败,则将 pte 变量设置为 NULL; 反之通过 PDE 和虚拟地址找到对应的 PTE 入口。找到虚拟地址对应的 PTE 之后,函数调用 arch_enter_lazay_mmu_mode() 函数进入 MMU Lazy 模式,接着函数在 104 行调用 get_page() 函数以此增加物理页的引用计数。函数在 105 行调用 inc_mm_counter() 增加系统统计计数。函数在 106 行调用 page_add_file_rmap_func() 函数,其为内核未导出的 page_add_file_rmap() 函数,因此增加物理页的统计计数。函数最核心的调用在 108-109 行,函数调用 set_pte_at() 函数用于将虚拟地址映射到物理页上,函数调用 pte_mkwrite() 函数将 PTE 标记为 Dirty,并将 vma 的 vm_page_prot 权限作为 PTE 的权限,通过以上处理虚拟地址已经建立了与物理内存之间的页表。函数最后在 112 行调用 arch_leave_lazy_mmu_mode() 函数从 MMU Lazy 模式中退出,至此页表建立完毕。

main.c 文件在内核中定义了一个定时器,该定时器会定期调用 BiscuitOS_scanner_pte() 函数,该函数用于遍历指定虚拟地址的 PTE 页表。函数在 123 行判断指定的虚拟地址是否存在,如果存在,那么函数在 128 行调用 BiscuitOS_follow_page_table() 函数遍历 BiscuitOS_address 对应的 PTE 页表,当遍历完毕之后,函数在 130 行检测 pte 变量的有效性,如果有效,那么函数检测该 PTE 是否存在,如果存在,那么函数在 134 行调用 pte_page() 函数从 PTE 中获得其映射的物理页,并在 135 行调用 page_to_pfn() 函数获得物理页对应的物理地址,最后函数在 137 行打印了物理页帧和 PTE 的内容. 函数在 145-146 行进行喂狗操作.

BiscuitOS_follow_page_table() 函数用于通过虚拟地址遍历页表找到对应的 PTE。在 “32-Bit Paging” 模式的 4K Page 时,页表只存在 2 级,为兼容 Linux,将 PDE 作为 pmd 看待。函数首先在 55 行调用 pgd_offset() 函数获得 PDE,接着函数在 63 行调用 pte_offset_map_lock() 函数查找 Page Table 获得虚拟地址对应的 PTE. 函数在 66 行调用 pte_present() 函数检测 PTE 是否存在,如果存在,那么函数返回 PTE,否则 0。

由于 main.c 文件通过模块的方式实现,因此有些内核函数没有导出,因此函数基于 kallsyms 机制来使用这些未导出的函数。在本例子中 __pte_alloc() 函数 page_add_file_rmap() 函数均为导出,因此调用 kallsyms_lookup_name() 函数找到对应的函数进行使用。至此内核部分的代码已经讲解完毕。

Translate Userspace Address With 4K Page 内核空间源码


Translate Kernel Address With 4K Page 源码分析

BiscuitOS 提供了一个完整的内核空间虚拟地址 “32-Bit Paging” 4K Page 案例。该案例通过在内核空间找到一段未使用的虚拟地址,然后为这段虚拟地址分配物理内存并建立页表,然后使用这段虚拟地址。整个流程了涵盖了 “32-Bit Paging” 模式下的内核空间页表的建立过程. 具体函数实现如下:

Translate Kernel Address With 4K Page 源码实践部署

Translate Kernel Address With 4K Page 源码解析

Translate Kernel Address With 4K Page 源码 (Gitee)


Translate Kernel Address With 4K Page 内核空间源码解析

源码布局如上图,顶层的 Makefile 用于源码在 BiscuitOS 编译逻辑,开发者可以忽略该文件。X86-Paging-32bit-Kernel-4K-Page-default 目录下的 main.c 文件用于实现对内核空间虚拟地址建立页表并使用该地址的过程。在映射 4K 物理页的 “32-Bit Paging” 模式下,内核空间虚拟地址只包含了 Page Directory Table 和 Page Table 两级页表。main.c 文件会将一个内核空间的虚拟地址对应的 PDE 和 PTE 填充上指定的内容,使内核可以访问和使用这段虚拟内存。main.c 文件的具体实现如下:

main.c 定义了一个入口函数 BiscuitOS_init(),该函数通过 device_initcall() 函数在内核启动到指定位置进行初始化。案例中首先在内核空间中找到一段没有使用的虚拟地址 BISCUITOS_KERNEL_ADDR, 该值指向了 VMALLOC 内存区域,接着定义了 end 和 val 变量,并将 val 指针指向了内核空间的虚拟地址。案例接着在 120 行调用 BiscuitOS_populate_page_table() 函数为虚拟地址分配物理内存并建立页表,以便内核能否使用这段虚拟内存。案例在 123 行对这段虚拟内存进行了写操作,将 88520 写入了这段虚拟内存的起始地址。最后函数通过 printk() 函数将虚拟地址的内容读出并打印.

BiscuitOS_populate_page_table() 函数用于为内核空间的虚拟地址分配 4K 物理页并建立页表。函数首先在 92 行调用 BiscuitOS_pgd_populate() 函数查找虚拟地址对应的 Page Directory Table, 如果此时 Page Directory Table 不存在,那么为 Page Directory Table 分配物理页并返回 Page Directory Table; 反之如果 Page Directory Table 已经存在,那么函数直接返回 Page Directory Table. 函数接着在 100 行调用 BiscuitOS_pde_populate() 函数找到虚拟地址对应的 PDE,如果此时 PDE 没有映射 Page Table,那么函数为其分配物理页作为 Page Table,并找到 PDE 映射到该 Page Table,最后返回 PDE; 反之如果此时 Page Table 已经存在,那么函数直接返回 PDE. 接着函数在 105 行调用 BiscuitOS_pte_populate() 函数查找虚拟地址对应的 PTE,如果此时 PTE 还没有映射 4K 物理页,那么函数分配一个 4K 物理页,并将 PTE 映射到 4K 物理页上,最后返回 PTE; 反之如果找到的 PTE 已经映射 4K 物理页,那么函数直接返回 PTE.

BiscuitOS_pgd_populate() 函数用于查找虚拟地址对应的 Page Directory Table, 如果虚拟地址没有映射 Page Directory Table, 那么函数虚拟地址分配并映射物理页,以此作为虚拟地址的 Page Directory Table. 函数首先在 65 行调用 pgd_offset_k() 函数查找虚拟地址对应的 Page Directory Table, 如果此时 Page Directory Table 不存在,那么函数在 71 行调用 alloc_page() 函数分配一个物理页,并在 76 行获得物理页对应的虚拟地址,最后函数在 78 行调用 pgd_populate() 函数将虚拟地址与物理页进行绑定,绑定完毕之后,该物理页就是 Page Directory Table. 函数在 80 行返回了 Page Directory Table.

BiscuitOS_pde_populate() 函数用于查找虚拟地址对应的 PDE,并在 PDE 没有映射 Page Table 的情况下为其分配并映射 4K 物理页。函数首先在 45 行调用 pmd_offset() 函数找到虚拟地址对应的 PDE。函数在 46 行调用 pmd_none() 函数检测 PDE 是否已经映射 Page Table,如果没有,那么函数在 50 行调用 alloc_page() 函数分配一个物理页,并在 55 行调用 page_addres() 获得物理页对应的虚拟地址,最后函数在 57 行调用 pmd_populate_kernel() 函数将 PDE 与新分配的 Page Table 进行映射。函数最后返回对应的 PDE。

BiscuitOS_pte_populate() 函数用于查找虚拟地址对应的 PTE,并在 PTE 没有映射 4K Page 的情况下为其分配并映射 4K 物理页。函数首先在 24 行调用 pte_offset_kernel() 函数获得虚拟地址对应的 PTE,如果此时 PTE 没有映射 4K 物理页,那么函数在 29 行调用 alloc_page() 函数分配物理页,并在 35 行调用 pfn_pte() 制作 PTE 的内容,并在 37 行调用 set_pte_at() 函数将 PTE 与 4K 物理页进行映射,最后返回 PTE.

Translate Kernel Address With 4K Page 源码


Translate Userspace Address With 4M Page 源码分析

BiscuitOS 提供了一个完整的用户空间虚拟地址的 “32-Bit Paging” 4M Page 案例,该案例通过在用户空间 mmap 一段虚拟内存之后,对 4M 内的虚拟内存进行写操作并触发缺页操作。触发缺页之后,内核部分的代码捕获到该缺页行为,并为发生缺页的虚拟地址分配物理内存。缺页异常处理完毕之后,用户空间开始正常使用虚拟地址。内核部分同时启用了一个定时器,定时扫描发生缺页的虚拟地址的 PDE 页表。整个流程涵盖了 “32-Bit Paging” 模式下 4MiB 物理页的缺页、建立页表,以及遍历页表。具体函数实现如下:

Translate Userspace Address With 4M Page 源码实践部署

Translate Userspace Address With 4M Page 用户空间源码解析

Translate Userspace Address With 4M Page 内核空间源码解析

Translate Userspace Address With 4M Page 用户空间源码 (Gitee)

Translate Userspace Address With 4M Page 内核空间源码 (Gitee)


Translate Userspace Address With 4M Page 用户空间源码解析

用户空间的源码布局如上图,bsbit 目录以及顶层目录的 Makefile 用于源码在 BiscuitOS 编译逻辑,开发者可以忽略这些文件。X86-Paging-32bit-4M-Page-userspace-default 目录下的 main.c 文件为用户空间触发缺页,以及虚拟地址使用的逻辑。同目录下的 Makefile 则用于编译 main.c 的编译逻辑. main.c 函数的实现逻辑通过 mmap() 从 “/dev/BiscuitOS” 设备映射一段 4M 的虚拟内存,然后对这段虚拟内存进行写操作,由于此时页表还没有建立,那么此时会触发缺页异常,该异常通过内核缺页异常之后传递给对应的内核部分,内部部分为虚拟地址建立页表。缺页异常处理完毕之后,用户空间代码继续访问虚拟内存。当页表建立完毕之后再次对 4M 范围内存的虚拟地址进行访问不用在触发缺页,直接使用即可。访问完毕之后用户空间释放虚拟地址并关闭 “/dev/BiscuitOS” 节点. main.c 函数具体实现如下:

程序在 22 行定义了虚拟内存区域的大小为 4M,并在 31 行调用 open() 函数打开 “/dev/BiscuitOS” 节点,接着在 38 行调用 mmap() 函数从系统中映射一段长度为 BISCUITOS_MAP_SIZE 的虚拟内存,这段虚拟内存可读可写,并且可以其他进程共享映射的物理页。

程序在 49 行将 val 变量指向 4M 虚拟内存的起始地址,然后在 51 行将 88520 写入到该虚拟地址,此时由于没有建立页表会触发内核缺页异常。内核为缺页的线性地址分配物理内存,并建立页表。内核处理完缺页异常之后,返回用户空间,函数在 52 行将 val 指向的虚拟内存的内容通过 printk() 函数打印. 函数接着在 55 行将 range_base 变量指向了 4M 虚拟内存偏移 1M 的位置,接着在 56 行将 val 指向该虚拟地址,同理将 52088 存储到 val 指向的虚拟地址,最后在 59 行通过 printk 打印该虚拟地址的内容。最后函数在 62 行 sleep 3s 之后调用 munmap() 函数和 close() 函数完成程序.

Translate Userspace Address With 4M Page 用户空间源码 (Gitee)


Translate Userspace Address With 4M Page 内核空间源码解析

内核空间的源码布局如上图,顶层目录的 Makefile 用于源码在 BiscuitOS 编译逻辑,开发者可以忽略该文件。X86-Paging-32bit-4M-Page-kernel-default 目录下的 main.c 文件用于实现用户空间缺页触发的缺页异常处理,为缺页的虚拟地址建立页表和分配物理内存,并启用一个内核定时器,定时查看指定虚拟地址对应的页表内容. 同级目录下的 Makefile 和 Kconfig 用于内核 Kbuild 编译逻辑。main.c 文件的逻辑框架通过 MISC 子系统向内核注射了 “/dev/BiscuitOS” 接口,并对打开该接口的文件提供了 mmap 接口,当用户空间通过 mmap 映射一段虚拟内存时,main.c 文件会对该虚拟地址进行标记,并提供 fault 接口。当用户空间发生缺页时,缺页异常处理程序就会调用到该 fault 接口。在 fault 接口中,main.c 文件为缺页的虚拟地址分配物理内存,并为其建立虚拟地址到物理地址的页表。main.c 文件定义了一个内核定时器,并定期查询指定虚拟地址对应的 PDE 页表. main.c 文件的具体实现如下:

函数在 204 行调用 misc_register() 函数向内核注册了一个混杂设备,注册成功之后会在 “/dev” 目录下创建 BiscuitOS 节点。混杂设备提供了 mmap 接口,其实为 BiscuitOS_mmap() 函数,在 182 行,该函数为每个映射虚拟地址的 struct vm_area_struct 结构绑定了一个 struct vm_operations_struct 数据结构,该结构只提供了 vma 的 fault 接口。函数还在 208-210 行定义了一个定时器,其定时器处理函数为 BiscuitOS_scanner_pde().

当用户空间打开 “/dev/BiscuitOS” 节点,调用 mmap() 函数映射一段虚拟内存,且用户空间访问这段虚拟地址发生缺页异常,那么缺页异常处理逻辑最终会调用到 vm_fault() 函数。当调用到 vm_fault() 函数时,函数首先在 157 行调用 alloc_page() 函数从 Buddy 分配器中分配一个物理页,然后调用 BiscuitOS_build_page_table() 函数为缺页的虚拟地址建立页表,并将虚拟地址映射到刚分配的物理地址上。函数在 168 行将 vmf 的 page 成员指向刚被映射的物理页上,这步操作主要是为了缺页异常处理对绑定的物理页进行统计和加锁等操作。函数接着在 172-172 行记录了缺页的虚拟地址和对应的 struct vm_area_struct 数据结构,函数在 175 行进行返回.

BiscuitOS_build_page_table() 函数用于为缺页的虚拟地址创建 4M 页表,并将虚拟地址映射到物理地址上。由于在 “32-Bit Paging” 模式下,映射 4MiB 物理页只需 1 级页表,因此函数首先在 87 行调用 pgd_offset() 函数获得虚拟地址对应的 PDE 入口,函数在 92 行将 pgd 转换成 pde,以此兼容软件层面的 PDE. 由于只有一级页表,那么函数调用 arch_enter_lazay_mmu_mode() 函数进入 MMU Lazy 模式,接着函数在 97 行调用 get_page() 函数以此增加物理页的引用计数。函数在 98 行调用 inc_mm_counter() 增加系统统计计数。函数在 99 行调用 page_add_file_rmap_func() 函数,其为内核未导出的 page_add_file_rmap() 函数,因此增加物理页的统计计数。函数最核心的调用在 102 行,函数调用 set_pmd() 函数用于将虚拟地址映射到物理页上,函数调用 __pgprot() 函数将 _PAGE_4M_USER 标志作为 PDE 页表的标志,通过以上处理虚拟地址已经建立了与物理内存之间的页表。函数最后在 105 行调用 arch_leave_lazy_mmu_mode() 函数从 MMU Lazy 模式中退出,至此页表建立完毕。在页表的权限中使用了 _PAGE_4M_USER 的标志合集,其定义如下:

在 _PAGE_4M_USER 标记合集中, _PAGE_PRESENT 标志确保 PDE 页表存在,_PAGE_RW 标志确保 4M 虚拟区域可读可写, _PAGE_ACCESSED 标志确保 4M 的虚拟地址刚被访问过, _PAGE_DIRTY 标志确保 4M 的虚拟地址刚被写过, _PAGE_USER 标志则确保该虚拟地址来自用户空间, _PAGE_PSE 标志是 4M 页表中最核心的标志,该标志的存在让 MMU 知道 PDE 映射的是 4M 的物理内存。通过将上面的标志作为 PDE 的页表标志,那么在页表建立完毕之后,虚拟地址可以使用.

main.c 文件在内核中定义了一个定时器,该定时器会定期调用 BiscuitOS_scanner_pde() 函数,该函数用于遍历指定虚拟地址的 PDE 页表。函数在 116 行判断指定的虚拟地址是否存在,如果存在,那么函数在 119 行调用 BiscuitOS_follow_page_table() 函数遍历 BiscuitOS_address 对应的 PDE 页表,当遍历完毕之后,函数在 121 行检测 pde 变量的有效性,如果有效,那么函数检测该 PDE 是否存在,如果存在,那么函数在 122 行调用 pmd_pfn() 函数从 PDE 中获得其映射的物理页, 最后打印了物理页帧和 PDE 的内容. 函数在 129-130 行进行喂狗操作.

BiscuitOS_follow_page_table() 函数用于通过虚拟地址遍历页表找到对应的 PDE。在 “32-Bit Paging” 模式的 4M Page 时,页表只存在 1 级,为兼容 Linux,将 PDE 作为 pmd 看待。函数首先在 55 行调用 pgd_offset() 函数获得 PDE, 直接返回即可。

由于 main.c 文件通过模块的方式实现,因此有些内核函数没有导出,因此函数基于 kallsyms 机制来使用这些未导出的函数。在本例子中 flush_tlb_mm_range() 函数 page_add_file_rmap() 函数均为导出,因此调用 kallsyms_lookup_name() 函数找到对应的函数进行使用。至此内核部分的代码已经讲解完毕。

Translate Userspace Address With 4M Page 内核空间源码


Translate Kernel Address With 4M Page 源码分析

BiscuitOS 提供了一个完整的内核空间虚拟地址 “32-Bit Paging” 4M Page 案例。该案例通过在内核空间找到一段未使用的虚拟地址,然后为这段虚拟地址分配物理内存并建立页表,然后使用这段虚拟地址。整个流程了涵盖了 “32-Bit Paging” 模式下的内核空间页表的建立过程. 具体函数实现如下:

Translate Kernel Address With 4M Page 源码实践部署

Translate Kernel Address With 4M Page 源码解析

Translate Kernel Address With 4M Page 源码 (Gitee)


Translate Kernel Address With 4M Page 内核空间源码解析

源码布局如上图,顶层的 Makefile 用于源码在 BiscuitOS 编译逻辑,开发者可以忽略该文件。X86-Paging-32bit-Kernel-4M-Page-default 目录下的 main.c 文件用于实现对内核空间虚拟地址建立页表并使用该地址的过程。在映射 4M 物理页的 “32-Bit Paging” 模式下,内核空间虚拟地址只包含了 Page Directory Table 一级页表。main.c 文件会将一个内核空间的虚拟地址对应的 PDE 填充指定内容,使内核可以访问和使用这段虚拟内存。main.c 文件的具体实现如下:

main.c 定义了一个入口函数 BiscuitOS_init(),该函数通过 device_initcall() 函数在内核启动到指定位置进行初始化。案例中函数首先通过 Buddy 分配器申请了 4M 的连续物理内存,接着定义了 addr 和 addr2 变量。案例接着在 69 行将物理内存作为参数传递给 BiscuitOS_32bit_paging_4M() 函数,以此为虚拟地址分配物理内存并建立页表,并将建立好的虚拟地址存储在 addr 变量里,以便内核能否使用这段虚拟内存,案例接着将 addr2 指向 addr 之后 1M 的虚拟地址。案例在 72-73 行对这段虚拟内存进行了写操作,将 88520 写入了 addr 指向的虚拟内存地址,接着将 52088 写入了 addr2 指向的虚拟内存地址. 最后函数通过 printk() 函数将虚拟地址的内容读出并打印. 使用完毕之后函数调用 BiscuitOS_32bit_paging_4M_unmap() 函数将虚拟地址与物理地址进行解绑,并清除页表,最后释放 4M 的物理页.

BiscuitOS_32bit_paging_4M() 函数用于为 4M 的物理内存建立页表,并映射虚拟内存。函数在 35 行采用了默认的虚拟地址 “BISCUITOS_4M_BASE”, 接着在 36 行调用 pgd_index() 函数和之前的 pgd_base 变量获得虚拟地址对应的 PGD,由于 “32-bit Paging” 映射 4M 物理页的模式只需要一级页表,因此 PGD 即 PDE。接着函数在 39 行滴啊用 set_pmd() 函数以及 pfn_pmd() 函数将 4M 物理内存对应的页帧号和 __pgprot(_PAGE_4M) 标志一同写入到 PDE 页表里,至此 PDE 页表已经建立完毕,最后调用 flush_tlb_kernel_range() 函数刷新 TLB 中对应的虚拟地址. 在建立 4M 页表最关键的是 _PAGE_4M 标志集合,该集合定义如下:

在 _PAGE_4M 标记合集中, _PAGE_PRESENT 标志确保 PDE 页表存在,_PAGE_RW 标志确保 4M 虚拟区域可读可写, _PAGE_ACCESSED 标志确保 4M 的虚拟地址刚被访问过, _PAGE_DIRTY 标志确保 4M 的虚拟地址刚被写过, __PAGE_KERNEL 标志则确保该虚拟地址来自n内核空间, _PAGE_PSE 标志是 4M 页表中最核心的标志,该标志的存在让 MMU 知道 PDE 映射的是 4M 的物理内存。通过将上面的标志作为 PDE 的页表标志,那么在页表建立完毕之后,虚拟地址可以使用.

BiscuitOS_32bit_paging_4M_unmap() 函数用于将一段映射好的 4M 虚拟地址空间接映射。函数通过传入的虚拟地址和 swapper_pg_dir 找到对应的 PGD 入口。由于 “32-bit Paging” 映射 4M 模式只有一级页表,那么函数找到的 PGD 入口即是 PDE. 接着函数在 52 行调用 pmd_clear() 函数清空 PDE 的内容,最后调用 flush_tlb_kernel_range() 函数刷新 TLB 对应的内容,至此 4M 页表接映射完毕.

Translate Kernel Address With 4M Page 源码 (Gitee)


用户空间虚拟地址映射 4K 物理页

本案例通过内核模块为用户空间发生缺页的虚拟地址分配物理内存,并建立相应的页表。代码分作用户空间部分和内核部分。本案例在 BiscuitOS 中的实践如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Translate Userspace Address With 4K Page (Userspace+)  --->
              -*- Translate Userspace Address With 4K Page (Kernel+)  --->

案例在 BiscuitOS 中运行的效果如下:

用户空间源码

内核空间代码


用户空间源码

用户空间虚拟地址映射 4K 物理页源码 (Gitee)

用户空间虚拟地址映射 4K 物理页用户空间源码解析

/*
 * Paging Mechanism: Mapping 4KiB Page With 32-Bit Paging
 *
 * (C) 2021.01.10 BuddyZhang1 <buddy.zhang@aliyun.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>

#define PAGE_SIZE		4096
#define BISCUITOS_MAP_SIZE	(16 * PAGE_SIZE)
#define BISCUITOS_PATH		"/dev/BiscuitOS"

int main()
{
	unsigned long *val;
	char *default_base;
	int fd;

	/* open */
	fd = open(BISCUITOS_PATH, O_RDWR);
	if (fd < 0) {
		printf("ERROR: open %s failed.\n", BISCUITOS_PATH);
		return -1;
	}

	/* mmap */
	default_base = (char *)mmap(NULL, BISCUITOS_MAP_SIZE,
					  PROT_READ | PROT_WRITE,
					  MAP_SHARED,
					  fd, 
					  0);
	if (!default_base) {
		printf("ERROR: mmap failed.\n");
		close(fd);
		return -1;
	}

	val = (unsigned long *)default_base;
	/* Trigger page fault */
	*val = 88520;
	printf("=> %#lx\n", *val);

	/* Hold 3s */
	sleep(3);

	/* unmap */
	munmap(default_base, BISCUITOS_MAP_SIZE);
	close(fd);

	printf("Paging mechanism Applicatiin on BiscuitOS.\n");
	return 0;
}


内核空间源码

用户空间虚拟地址映射 4K 物理页内核空间源码 (Gitee)

用户空间虚拟地址映射 4K 物理页用户空间源码解析

/*
 * Paging Mechanism: Mapping 4KiB Page With 32-Bit Paging
 *
 * (C) 2021.01.20 BuddyZhang1 <buddy.zhang@aliyun.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#ifndef __i386__
#error "This Code only running on Intl-i386 Architecture!"
#endif

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/highmem.h>

/* Paging/fault header*/
#include <linux/mm.h>
#include <linux/kallsyms.h>

/* DD Platform Name */
#define DEV_NAME			"BiscuitOS"

/* Timer */
#define BISCUITOS_SCANNER_PERIOD	1000 /* 1000ms -> 1s */
static struct timer_list BiscuitOS_scanner;

/* Speical */
static struct vm_area_struct *BiscuitOS_vma;
unsigned long BiscuitOS_address;

/* kallsyms unexport symbol */
typedef int (*__pte_alloc_t)(struct mm_struct *, pmd_t *);
typedef void (*page_add_f_rmap_t)(struct page *, bool);

static __pte_alloc_t __pte_alloc_func;
static page_add_f_rmap_t page_add_file_rmap_func;

/* follow pte */
static int BiscuitOS_follow_page_table(struct mm_struct *mm, 
		unsigned long address, pte_t **ptep, spinlock_t **ptl)
{
	pgd_t *pgd;
	pmd_t *pde;
	pte_t *pte;

	/* Follow PGD Entry */
	pgd = pgd_offset(mm, address);
	if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
		goto out;

	/* PDE */
	pde = (pmd_t *)pgd;

	/* Follow PTE */
	pte = pte_offset_map_lock(mm, pde, address, ptl);
	if (!pte)
		goto out;
	if (!pte_present(*pte))
		goto unlock;
	*ptep = pte;
	return 0;

unlock:
	pte_unmap_unlock(ptep, *ptl);
out:
	return -EINVAL;
}

/* Build Page table */
static int BiscuitOS_build_page_table(struct vm_area_struct *vma, 
				unsigned long address, struct page *page)
{
	struct mm_struct *mm = vma->vm_mm;
	unsigned long pfn = page_to_pfn(page);
	spinlock_t *ptl;
	pgd_t *pgd;
	pmd_t *pde;
	pte_t *pte;

	pgd = pgd_offset(mm, address);
	if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
		goto out;

	/* PDE */
	pde = (pmd_t *)pgd;

	/* alloc pte */
	pte = __pte_alloc_func(mm, pde) ? 
		NULL : pte_offset_map_lock(mm, pde, address, &ptl);
	if (!pte)
		goto out;

	/* MMU Lazy mode */
	arch_enter_lazy_mmu_mode();

	get_page(page);
	inc_mm_counter(mm, mm_counter_file(page));
	page_add_file_rmap_func(page, false);

	set_pte_at(mm, address, pte, 
			pte_mkwrite(pfn_pte(pfn, vma->vm_page_prot)));
	pte_unmap_unlock(pte, ptl);
	
	arch_leave_lazy_mmu_mode();

	return 0;

out:
	return -EINVAL;
}

/* PTE Scanner */
static void BiscuitOS_scanner_pte(struct timer_list *unused)
{
	if (BiscuitOS_address) {
		spinlock_t *ptl;
		pte_t *pte;

		/* follow page table */
		BiscuitOS_follow_page_table(BiscuitOS_vma->vm_mm,
				BiscuitOS_address, &pte, &ptl);
		if (pte && pte_present(*pte)) {
			struct page * page;
			unsigned long pfn;

			page = pte_page(*pte);
			pfn = page_to_pfn(page);

			printk("Page %#lx PTE %#lx\n", pfn, pte_val(*pte));
			pte_unmap_unlock(pte, ptl);
		}

		BiscuitOS_address = 0;
	}

	/* watchdog */
	mod_timer(&BiscuitOS_scanner, 
			jiffies + msecs_to_jiffies(BISCUITOS_SCANNER_PERIOD));
}

static vm_fault_t vm_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	unsigned long address = vmf->address;
	struct page *fault_page;
	int r;

	/* Allocate new page from buddy */
	fault_page = alloc_page(GFP_KERNEL);
	if (!fault_page) {
		printk("ERROR: System doesn't has enough physical memory.\n");
		r = -ENOMEM;
		goto err_alloc;
	}

	/* Build page table */
	BiscuitOS_build_page_table(vma, address, fault_page);

	/* bind fault page */
	vmf->page = fault_page;

	/* bind special data */
	BiscuitOS_vma = vma;
	BiscuitOS_address = address;

	printk("Page Fault: %#lx\n", page_to_pfn(fault_page));
	return 0;

err_alloc:
	return r;
}

static inline void init_symbol(void)
{
	__pte_alloc_func = 
		(__pte_alloc_t)kallsyms_lookup_name("__pte_alloc");
	page_add_file_rmap_func = 
		(page_add_f_rmap_t)kallsyms_lookup_name("page_add_file_rmap");
}

static const struct vm_operations_struct BiscuitOS_vm_ops = {
	.fault	= vm_fault,
};

static int BiscuitOS_mmap(struct file *filp, struct vm_area_struct *vma)
{
	/* setup vm_ops */
	vma->vm_ops = &BiscuitOS_vm_ops;

	return 0;
}

/* file operations */
static struct file_operations BiscuitOS_fops = {
	.owner		= THIS_MODULE,
	.mmap		= BiscuitOS_mmap,
};

/* Misc device driver */
static struct miscdevice BiscuitOS_drv = {
	.minor	= MISC_DYNAMIC_MINOR,
	.name	= DEV_NAME,
	.fops	= &BiscuitOS_fops,
};

/* Module initialize entry */
static int __init BiscuitOS_init(void)
{
	/* Register Misc device */
	misc_register(&BiscuitOS_drv);
	init_symbol();

	/* Timer for PTE Scanner */
	timer_setup(&BiscuitOS_scanner, BiscuitOS_scanner_pte, 0);
	mod_timer(&BiscuitOS_scanner, 
			jiffies + msecs_to_jiffies(BISCUITOS_SCANNER_PERIOD));

	return 0;
}

/* Module exit entry */
static void __exit BiscuitOS_exit(void)
{
	del_timer(&BiscuitOS_scanner);
	/* Un-Register Misc device */
	misc_deregister(&BiscuitOS_drv);
}

module_init(BiscuitOS_init);
module_exit(BiscuitOS_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("BiscuitOS <buddy.zhang@aliyun.com>");
MODULE_DESCRIPTION("BiscuitOS Paging/Page-fault Mechanism");


内核空间虚拟地址映射 4K 物理页

本案例通过从内核空间选择一块虚拟地址,然后为虚拟地址建立相应的页表。最后使用这块虚拟地址。本案例在 BiscuitOS 中的实践如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Translate Kernel Address With 4K Page  --->

案例在 BiscuitOS 中运行的效果如下:

内核空间虚拟地址映射 4K 物理页内核空间源码 (Gitee)

用户空间虚拟地址映射 4K 物理页用户空间源码解析

/*
 * Translation 4K Page With Paging Mechanism on Kernel
 *
 * (C) 2021.01.10 BuddyZhang1 <buddy.zhang@aliyun.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/highmem.h>
/* page table */
#include <asm/pgalloc.h>

#define BISCUITOS_KERNEL_VADDR		(VMALLOC_START + 0x700000)
#define BISCUITOS_KERNEL_SIZE		PAGE_SIZE

/* PTE: Page Table Entry */
static pte_t *BiscuitOS_pte_populate(pmd_t *pde, unsigned long addr)
{
	pte_t *pte = pte_offset_kernel(pde, addr);
	if (pte_none(*pte)) {
		struct page *page;
		pte_t entry;

		page = alloc_page(GFP_KERNEL);
		if (!page) {
			printk("Error: Memory in short on pte.\n");
			return NULL;
		}
		/* setup pte entry */
		entry = pfn_pte(page_to_pfn(page), PAGE_KERNEL);
		/* populate pte */
		set_pte_at(&init_mm, addr, pte, entry);
	}
	return pte;
}

/* PDE: Page Directory Entry */
static pmd_t *BiscuitOS_pde_populate(pud_t *pdt, unsigned long addr)
{
	pmd_t *pde = pmd_offset(pdt, addr);
	if (pmd_none(*pde)) {
		struct page *page;
		void *page_table;

		page = alloc_page(GFP_KERNEL);
		if (!page) {
			printk("Error: Memory in short on pmd.\n");
			return NULL;
		}
		page_table = page_address(page);
		/* pmd populate */
		pmd_populate_kernel(&init_mm, pde, page_table);
	}
	return pde;
}

/* Page Directory Table */
static pgd_t *BiscuitOS_pgd_populate(unsigned long addr)
{
	pgd_t *pgd = pgd_offset_k(addr);
	if (pgd_none(*pgd)) {
		struct page *page;
		void *page_table;

		/* Allocate new page for page table */
		page = alloc_page(GFP_KERNEL);
		if (!page) {
			printk("Error: Memory in short on pgd.\n");
			return NULL;
		}
		page_table = page_address(page);
		/* pgd populate */
		pgd_populate(&init_mm, pgd, page_table);
	}
	return pgd;
}

static int BiscuitOS_populate_page_table(unsigned long start,
						unsigned long end)
{
	pgd_t *pgd;
	pud_t *pdt;
	pmd_t *pde;
	pte_t *pte;

	/* BiscuitOS: Page for Directory Table */
	pgd = BiscuitOS_pgd_populate(start);
	if (!pgd)
		return -ENOMEM;

	/* PDT: Page Directory Table */
	pdt = (pud_t *)pgd;

	/* BiscuitOS: PDE - Page Directory Entry */
	pde = BiscuitOS_pde_populate(pdt, start);
	if (!pde)
		return -ENOMEM;

	/* BiscuitOS: PTE - Page Table Entry */
	pte = BiscuitOS_pte_populate(pde, start);
	if (!pte)
		return -ENOMEM;

	return 0;
}

/* Module initialize entry */
static int __init BiscuitOS_init(void)
{
	unsigned long start = BISCUITOS_KERNEL_VADDR;
	unsigned long end = start + BISCUITOS_KERNEL_SIZE;
	unsigned long *val = (unsigned long *)start;

	/* Establish Kernel page table */
	BiscuitOS_populate_page_table(start, end);

	/* Trigger kernel page fault */
	*val = 88520;
	/* Using Virtual address */
	printk("\n\n\n*************BiscuitOS************\n");
	printk("Default Virtual Address: %#lx - %#lx\n", start, end);
	printk("BiscuitOS %#lx => %ld\n", (unsigned long)val, *val);

	printk("Hello BiscuitOS\n");
	printk("**********************************\n\n\n");

	return 0;
}
device_initcall(BiscuitOS_init);


用户空间虚拟地址映射 4M 物理页

本案例通过内核模块为用户空间发生缺页的虚拟地址分配 4M 物理内存,并建立相应的 4M 页表。代码分作用户空间部分和内核部分。本案例在 BiscuitOS 中的实践如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Translate Userspace Address With 4M Page (Userspace+)  --->
              -*- Translate Userspace Address With 4M Page (Kernel+)  --->

案例在 BiscuitOS 中运行的效果如下:

用户空间源码

内核空间代码


用户空间源码

用户空间虚拟地址映射 4M 物理页源码 (Gitee)

用户空间虚拟地址映射 4M 物理页用户空间源码解析

/*
 * Paging Mechanism: Mapping 4MiB Page With 32-Bit Paging
 *
 * (C) 2021.01.10 BuddyZhang1 <buddy.zhang@aliyun.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>

/* 4MiB Virtual Space */
#define BISCUITOS_MAP_SIZE	(1 << 22)
#define BISCUITOS_PATH		"/dev/BiscuitOS"

int main()
{
	unsigned long *val;
	char *default_base;
	char *range_base;
	int fd;

	/* open */
	fd = open(BISCUITOS_PATH, O_RDWR);
	if (fd < 0) {
		printf("ERROR: open %s failed.\n", BISCUITOS_PATH);
		return -1;
	}

	/* mmap */
	default_base = (char *)mmap(NULL, BISCUITOS_MAP_SIZE,
					  PROT_READ | PROT_WRITE,
					  MAP_SHARED,
					  fd, 
					  0);
	if (!default_base) {
		printf("ERROR: mmap failed.\n");
		close(fd);
		return -1;
	}

	val = (unsigned long *)default_base;
	/* Trigger page fault */
	*val = 88520;
	printf("=> %#lx: %ld\n", (unsigned long)val, *val);

	/* Access offset 1M on 4M Page */
	range_base = (char *)((unsigned long)default_base + (1 << 20));
	val = (unsigned long *)range_base;
	/* Don't Trigger page fault */
	*val = 52088;
	printf("=> %#lx: %ld\n", (unsigned long)val, *val);

	/* Hold 3s */
	sleep(3);

	/* unmap */
	munmap(default_base, BISCUITOS_MAP_SIZE);
	close(fd);

	printf("Paging mechanism Applicatiin on BiscuitOS.\n");
	return 0;
}

内核空间源码

用户空间虚拟地址映射 4M 物理页内核空间源码 (Gitee)

用户空间虚拟地址映射 4M 物理页用户空间源码解析

/*
 * Paging Mechanism: Mapping 4MiB Page With 32-Bit Paging
 *
 * (C) 2021.01.20 BuddyZhang1 <buddy.zhang@aliyun.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#ifndef __i386__
#error "This Code only running on Intl-i386 Architecture!"
#endif

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/highmem.h>

/* Paging/fault header*/
#include <linux/mm.h>
#include <linux/kallsyms.h>

/* DD Platform Name */
#define DEV_NAME			"BiscuitOS"

/* Page Table flags */
#define _PAGE_4M_USER			(_PAGE_PRESENT | _PAGE_RW | \
					 _PAGE_ACCESSED | _PAGE_DIRTY | \
					 _PAGE_ENC | _PAGE_USER | \
					 _PAGE_PSE)

/* Timer */
#define BISCUITOS_SCANNER_PERIOD	1000 /* 1000ms -> 1s */
static struct timer_list BiscuitOS_scanner;

/* Speical */
static struct vm_area_struct *BiscuitOS_vma;
unsigned long BiscuitOS_address;

/* kallsyms unexport symbol */
typedef void (*page_add_f_rmap_t)(struct page *, bool);
typedef void (*flush_tlb_mm_range_t)(struct mm_struct *, unsigned long,
				unsigned long, unsigned int, bool);

static page_add_f_rmap_t page_add_file_rmap_func;
static flush_tlb_mm_range_t flush_tlb_mm_range_func;

/* Follow PDE */
static int BiscuitOS_follow_page_table(struct mm_struct *mm, 
				unsigned long address, pmd_t **pdep)
{
	pgd_t *pgd;
	pmd_t *pde;

	/* Follow PGD Entry */
	pgd = pgd_offset(mm, address);
	if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
		goto out;

	/* PDE */
	pde = (pmd_t *)pgd;

	*pdep = pde;
	return 0;

out:
	return -EINVAL;
}

/* Build Page table */
static int BiscuitOS_build_page_table(struct vm_area_struct *vma, 
				unsigned long address, struct page *page)
{
	unsigned long end = address + (1 << 22);
	unsigned long pfn = page_to_pfn(page);
	struct mm_struct *mm = vma->vm_mm;
	pgd_t *pgd;
	pmd_t *pde;

	pgd = pgd_offset(mm, address);
	if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
		goto out;

	/* PDE */
	pde = (pmd_t *)pgd;

	/* MMU Lazy mode */
	arch_enter_lazy_mmu_mode();

	get_page(page);
	inc_mm_counter(mm, mm_counter_file(page));
	page_add_file_rmap_func(page, false);

	/* PDE populate */
	set_pmd(pde, pfn_pmd(pfn, __pgprot(_PAGE_4M_USER)));
	flush_tlb_mm_range_func(vma->vm_mm, address, end, PAGE_SHIFT, false);
	
	arch_leave_lazy_mmu_mode();

	return 0;

out:
	return -EINVAL;
}

/* PDE Scanner */
static void BiscuitOS_scanner_pde(struct timer_list *unused)
{
	if (BiscuitOS_address) {
		pmd_t *pde;

		BiscuitOS_follow_page_table(BiscuitOS_vma->vm_mm, 
						BiscuitOS_address, &pde);
		if (pde && pmd_present(*pde)) {
			printk("PMD %#lx PFN %#lx\n", pmd_val(*pde),
							pmd_pfn(*pde));
		}
		BiscuitOS_address = 0;
	}

	/* watchdog */
	mod_timer(&BiscuitOS_scanner, 
			jiffies + msecs_to_jiffies(BISCUITOS_SCANNER_PERIOD));
}

static vm_fault_t vm_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	unsigned long address = vmf->address;
	struct page *fault_page;
	int r;

	/* Allocate new page from buddy */
	fault_page = alloc_pages(GFP_KERNEL, 10);
	if (!fault_page) {
		printk("ERROR: System doesn't has enough physical memory.\n");
		r = -ENOMEM;
		goto err_alloc;
	}

	/* Build page table */
	BiscuitOS_build_page_table(vma, address, fault_page);

	/* bind fault page */
	vmf->page = fault_page;

	/* bind special data */
	BiscuitOS_vma = vma;
	BiscuitOS_address = address;

	printk("Page Fault: %#lx\n", page_to_pfn(fault_page));
	return 0;

err_alloc:
	return r;
}

static inline void init_symbol(void)
{
	page_add_file_rmap_func = 
	     (page_add_f_rmap_t)kallsyms_lookup_name("page_add_file_rmap");
	flush_tlb_mm_range_func =
	     (flush_tlb_mm_range_t)kallsyms_lookup_name("flush_tlb_mm_range");
}

static const struct vm_operations_struct BiscuitOS_vm_ops = {
	.fault	= vm_fault,
};

static int BiscuitOS_mmap(struct file *filp, struct vm_area_struct *vma)
{
	/* setup vm_ops */
	vma->vm_ops = &BiscuitOS_vm_ops;

	return 0;
}

/* file operations */
static struct file_operations BiscuitOS_fops = {
	.owner		= THIS_MODULE,
	.mmap		= BiscuitOS_mmap,
};

/* Misc device driver */
static struct miscdevice BiscuitOS_drv = {
	.minor	= MISC_DYNAMIC_MINOR,
	.name	= DEV_NAME,
	.fops	= &BiscuitOS_fops,
};

/* Module initialize entry */
static int __init BiscuitOS_init(void)
{
	/* Register Misc device */
	misc_register(&BiscuitOS_drv);
	init_symbol();

	/* Timer for PTE Scanner */
	timer_setup(&BiscuitOS_scanner, BiscuitOS_scanner_pde, 0);
	mod_timer(&BiscuitOS_scanner, 
			jiffies + msecs_to_jiffies(BISCUITOS_SCANNER_PERIOD));

	return 0;
}

/* Module exit entry */
static void __exit BiscuitOS_exit(void)
{
	del_timer(&BiscuitOS_scanner);
	/* Un-Register Misc device */
	misc_deregister(&BiscuitOS_drv);
}

module_init(BiscuitOS_init);
module_exit(BiscuitOS_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("BiscuitOS <buddy.zhang@aliyun.com>");
MODULE_DESCRIPTION("BiscuitOS Paging/Page-fault Mechanism");


内核空间虚拟地址映射 4M 物理页

本案例通过从内核空间选择一块虚拟地址,然后为虚拟地址建立相应的 4M 页表。最后使用这块虚拟地址。本案例在 BiscuitOS 中的实践如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Translate Kernel Address With 4M Page  --->

案例在 BiscuitOS 中运行的效果如下:

内核空间虚拟地址映射 4M 物理页内核空间源码 (Gitee)

用户空间虚拟地址映射 4M 物理页用户空间源码解析

/*
 * 4M Physical Page With 32-Bit Paging
 *
 * (C) 2021.01.10 BuddyZhang1 <buddy.zhang@aliyun.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/gfp.h>
/* page table */
#include <asm/pgalloc.h>

/* BiscuitOS Emulate 4M Virtual Area */
#define BISCUITOS_4M_BASE		((PAGE_OFFSET - 0x1800000) & PMD_MASK)
/* 4M Page */
#define _PAGE_4M			(_PAGE_PRESENT | _PAGE_RW | \
					 _PAGE_ACCESSED | _PAGE_DIRTY | \
					 _PAGE_ENC | __PAGE_KERNEL | \
					 _PAGE_PSE)

/* Build PDE with 4M Page */
static void *BiscuitOS_32bit_paging_4M(struct page *page)
{
	pgd_t *pgd_base = swapper_pg_dir;
	unsigned long pfn = page_to_pfn(page);
	unsigned long vaddr;
	pgd_t *pgd;
	pmd_t *pde;

	vaddr = BISCUITOS_4M_BASE;
	pgd = pgd_base + pgd_index(vaddr);

	pde = (pmd_t *)pgd;
	set_pmd(pde, pfn_pmd(pfn, __pgprot(_PAGE_4M)));
	flush_tlb_kernel_range(vaddr, vaddr + (1 << 22));
	return (void *)vaddr;
}

/* Release PDE with 4M Page */
static void BiscuitOS_32bit_paging_4M_unmap(unsigned long vaddr)
{
	pgd_t *pgd_base = swapper_pg_dir;
	pgd_t *pgd = pgd_base + pgd_index(vaddr);
	pmd_t *pde;

	pde = (pmd_t *)pgd;
	pmd_clear(pde);
	flush_tlb_kernel_range(vaddr, vaddr + (1 << 22));
}

/* Module initialize entry */
static int __init BiscuitOS_init(void)
{
	struct page *page;
	unsigned long *addr;
	unsigned long *addr2;

	/* Alloc 4M Physical Page */
	page = alloc_pages(GFP_KERNEL | __GFP_HIGHMEM, 10);
	if (!page)
		return -ENOMEM;

	/* Mapping for 4M Page  */
	addr  = (unsigned long *)BiscuitOS_32bit_paging_4M(page);
	addr2 = (unsigned long *)((unsigned long)addr + (1 << 20));

	*addr  = 88520;
	*addr2 = 52088;
	printk("\n\n\n\n**************BiscuitOS*****************\n");
	printk("=> %#lx: %ld\n", (unsigned long)addr, *addr);
	printk("=> %#lx: %ld\n", (unsigned long)addr2, *addr2);
	printk("******************************************\n\n\n\n");

	/* Unmapping for 4M Page */
	BiscuitOS_32bit_paging_4M_unmap((unsigned long)addr);

	/* Trigger kernel panic if access addr */
	//*addr = 88520;

	/* Free 4M Physical Page */
	__free_pages(page, 10);
	return 0;
}
device_initcall(BiscuitOS_init);


VMALLOC Memory Allocator With 32-Bit Paging

VMALLOC 内存分配器称为 “Virtual Memory Allocator”, VMALLOC 内存的主要任务就是分配虚拟地址连续但物理地址不一定连续的内存。在 Linux 内核中,划分了一块虚拟内存区域给 VMALLOC 内存分配器进行管理, VMALLOC 分配器提供相应的函数将这段虚拟内存分配 给内核其他子系统,并通过动态建立页表的方式,将不连续的物理内存映射到连续的虚拟地址空间,因此 VMALLOC 分配器分配的内存出现了虚拟地址连续但物理地址不一定连续的特点。在不同的体系结构中 VMALLOC 内存管理器管理的虚拟内存区域可能不同,但可以通过 VMALLOC_START 和 VMALLOC_END 进行确认.

VMALLOC 分配器完整分析

基于 VMALLOC 分配器动态分配的特点,当 VMALLOC 分配器从 VMALLOC 内存区找到一块合适的虚拟区域之后,从 Buddy 分配器中分配一定数量的物理内存,这些物理内存可以不连续,接着通过动态建立页表将虚拟地址映射到物理地址上。在 “32-bit Paging” 模式下,默认映射 4K 物理页,因此 VMALLOC 分配器建立的页表采用两级页表的模式,分别是 “Page Directory Table” 和 “Page Table”。 VMALLOC 分配器将一个 VMALLOC 虚拟地址通过遍历页表找到对应的物理地址,其遍历页表首先通过 CR3 寄存器找到对应的 “Page Directory”, 接着 “Page Directory Table” 通过 VMALLOC 虚拟地址的指定字段找到 PDE, PDE 中存储了 “Page Table” 的信息以及对应 4M 物理内存的访问权限。VMALLOC 分配器通过 PDE 找到对应的 “Page Table”, 再结合 VMALLOC 虚拟地址指定字段找到对应的 PTE,PTE 中包含了 4K 物理页的物理地址以及物理页的访问权限。VMALLOC 分配器最终根据 PTE 和 VMALLOC 虚拟地址指定字段在 4K 物理页中找到对应的物理地址。


BiscuitOS 实践

BiscuitOS 提供了一个简单的案例用于实现一个基于 “32-Bit Paging” 模式的 VMALLOC 分配器,其重点用于实践 VMALLOC 分配器分配物理内存、建立页表、使用、释放页表、以及释放物理内存的完整过程。BiscuitOS 中实践例子的方法如下 (具体实践办法可以参考如下文档):

32-bit Paging BiscuitOS 实践教程

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] VMALLOC Allocator with 32-Bit Paging  ---> 

make

源码配置完毕之后,执行 make 进行实际的源码部署,部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在 “X86-Paging-32bit-VMALLOC-default” 目录, 该目录存储相关的内核源代码。接下来开发者使用如下命令:

cd X86-Paging-32bit-VMALLOC-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例 . 环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在内核启动过程中,该模块自动加载运行,从运行的结果可以看到模块为 0xe0ee0000 开始的虚拟地址建立 4K 页表,并将 88520 存储在该虚拟地址,最后使用 printk() 函数将该虚拟地址的内容输出,此时输出的内容正好是写入的内容.


VMALLOC 源码分析

在本案例中,通过从 VMALLOC 区域中找一块未使用的虚拟地址,然后从 Buddy 分配器中分配一个物理页,接着 VMALLOC 分配器建立页表将虚拟地址映射到物理内存上。案例对这块虚拟地址进行读写操作,操作完毕之后将虚拟地址的页表摧毁,最后回收物理内存。涉及的源码解析如下:

VMALLOC Memory Allocator With 32-Bit Paging Source Code

程序在 230-236 行构造了一个 struct vm_struct 结构,该数据结构用于在 VMALLOC 分配器中定义一个 VMALLOC 内存区域,该区域的起始地址为 BISCUITOS_KERNEL_VADDR, 区域的长度为 BISCUITOS_KERNEL_SIZE,区域包含了 VM_ALLOC、VM_UNINITIALIZED 和 VM_NO_GUARD 标志. 程序接着在 242 行调用 __vamlloc_area_node() 函数将该 VMALLOC 区域传递给 VMALLOC 分配器,用于为该区域分配物理内存和建立页表映射。当函数成功执行,程序在 244 行对 VMALLOC 内存区域进行写操作,此时由于虚拟地址已经和物理地址建立了映射,因此可以正常访问这段虚拟地址。函数接着在 250-253 行将 VMALLOC 虚拟地址的内容通过 printk() 函数进行打印。VMALLOC 虚拟地址使用完毕之后,程序在 256 行调用 __vunmap() 函数将虚拟地址解除映射,并释放对应的物理内存.

__vmalloc_area_node() 函数用于 VMALLOC 分配器实际的物理内存分配和页表映射。函数在 110 行计算了 VMALLOC 虚拟区域占用的物理页的数量,并计算了维护这些物理页需要 struct page 数据结构的数量,接着函数在 115 行调用 kmalloc_node() 函数为维护物理页的 struct page 分配器内存,其通过 area->pages 进行指定。函数接下来分配物理内存,函数在 119 行使用 for 循环按一个物理页的粒度循环 VMALLOC 虚拟区域物理页的数量对应的次数。在每次循环中,函数调用 alloc_page() 或者 alloc_page_node() 函数分配一个物理页,这里采用两个不同的函数是 NUMA 的缘故,这里统一看成没有 NUMA NODE 差异的分配,函数在 133 行将分配的物理内存存储在 area->pages[] 数组里. 函数最后在 137 行调用 BiscuitOS_map_vm_area() 函数建立页表.

BiscuitOS_map_vm_area() 函数用于为指定的 VMALLOC 虚拟内存区域和物理页建立映射。函数通过调用 vmap_page_range() 函数实现实际的页表建立过程。vmap_page_range() 函数的通过调用 vmap_page_range_noflush() 函数进行实际的页表建立,最后调用 flush_cache_vmap() 函数将 TLB 中 VMALLOC 虚拟区域对应的虚拟地址进行刷新.

vmap_page_range_noflush() 用于为 VMALLOC 虚拟区域建立页表。在 “32-bit Paging” 模式下,分页机制只采用两级页表,即 “Page Directory Table” 和 “Page Table”, 因此在建立页表时,函数在 64 行首先调用 pgd_offset_k() 函数找到 VMALLOC 虚拟区域起始虚拟地址对应的 PGD,该 PGD 对应这 “32-bit Paging” 模式的 PDE, 因此函数在 68 行将 PGD 强行转换成 PDE。函数接着在 70 行调用 pgd_addr_end() 函数计算 VMALLOC 虚拟区域的下一个 pgd 对应的起始虚拟地址,接着函数调用 vmap_pte_range() 函数为 VMALLOC 虚拟区域建立 PTE 页表。在建立完一个 PGD 页表之后,函数继续通过 while() 循环遍历下一个 PGD。

vmap_pte_range() 函数用于建立 VMALLOC 虚拟区域的 PTE 页表。函数首先在 39 行调用 pte_alloc_kernel() 为对应的虚拟地址分配 PTE 页表,接着在 do-while 循环中,函数从存储物理页的数组中取出一个物理页,然后检测 PTE 的有效性,最后在 49 行调用 set_pte_at() 函数建立虚拟地址到物理页的页表,至此页表建立完毕. 接下来 while 循环继续为剩下的虚拟地址建立映射.

__vunmap() 函数用于实现将一段 VMALLOC 虚拟内存对应的页表和物理内存进行释放。函数首先在 209 行调用 BiscuitOS_remove_vm_area() 函数获得 VMALLOC 虚拟地址对应的 struct vm_struct 数据结构,然后使用 for 循环将对应的物理内存通过 __free_pages() 函数进行释放。最后将涉及的数据结构进行释放。BiscuitOS_remove_vm_area() 函数用于构造 VMALLOC 分配器释放动作需要的数据结构,其核心在 200 行调用 free_unmap_vmap_area() 函数实现。在 free_unmap_vmap_area() 函数中,通过调用 flush_cache_vunmap() 函数将 VMALLOC 虚拟内存对应的 cache 进行刷新,并在 186 行调用 unmap_vmap_area() 函数将 VMALLOC 虚拟内存对应的页表进行清除。函数最后在 188 行调用 flush_tlb_kernel_range() 函数将 VMALLOC 虚拟内存对应的 TLB 项进行刷新。unmap_vmap_area() 函数通过调用 vunmap_page_range() 函数实现其逻辑。

vunmap_page_range() 用于清除 VMALLOC 虚拟地址对应的页表,在 “32-Bit Paging” 模式下,分页机制只采用两级页表,分别是 “Page Directroy Table” 和 “Page Table”, 因此函数在查找页表时,首先在 162 行滴啊用 pgd_offset_k() 函数获得虚拟地址对应的 pgd,接着在 do-while 循环中将所有的 VMALLOC 虚拟内存对应的页表进行释放。由于只有两级页表,因此 PDE 等同于 PGD,函数在 169 行调用 pgd_addr_next() 函数获得下一个 pgd 对应的起始虚拟地址,函数此时在 172 行调用 vunmap_pte_range() 函数将对应的 PTE 页表进行释放。

vunmap_pte_range() 函数用于清除 VMALLOC 虚拟地址对应的 PTE 页表。函数传入了 PDE 以及对应的虚拟地址,函数在 140 行通过 pte_offset_kernel() 函数获得对应的 PTE,接着函数在 151 行调用 ptep_get_and_clear() 函数将 PTE 内容清零,以此重复将所有的 PTE 都清零。支持 VMALLOC 虚拟内存对应的页表都清零了。


VMALLOC 页表故障分析

在 “32-bit Paging” 模式下的 VMALLOC 分配器,如果非法使用虚拟内存,那么会触发什么样的问题? 正如上图 238-239 行和 257-258 行,如果在 VMALLOC 分配器未对虚拟地址分配物理内存、建立页表,那么这样的访问将应发内核 PANIC. 例如执行 258 行的代码,此时 VMALLOC 虚拟地址对应的页表已经释放,对该地址的访问将引发内核 PANIC:

从上图报错可以看出,258 行在访问 VMALLOC 虚拟地址 0xe0ee0000 的时候,引起了 #PF 写错误,此时 PDE 的值为 0x1eeea067, 根据 PDE 的 layout,PDE 是存在且可读写的,因此不是 PDE 引起的问题。接着来看 PTE,此时 PTE 的值为 0,那么 PTE 不存在,对 PTE 关联的物理地址进行写操作一定触发 PANIC。因此 fault 的发生正符合 PTE 被清除的预期。接着将 239 行的代码启用,此时 VMALLOC 虚拟地址对应的页表还没有建立,此时对该虚拟地址的访问也会触发内核 PANIC:

从上面的报错来看,239 行对未建立页表的 VMALLOC 虚拟地址进行返回时,PDE 和 PTE 都是空,那么引起内核 #PF. 综合上面两个 PANIC,VMALLOC 分配器在给分配连续的虚拟内存之后,还有分配物理内存,并为其建立 PDE 和 PTE 两级页表,以至于让虚拟内存映射到物理内存。当不再使用 VAMLLOC 虚拟内存时,应该清除页表,解除页表关系,最后回收物理内存和虚拟内存。


KMAP Memory Allocator With 32-Bit Paging

KMAP 内存分配器称为 “Temporary Kernel Mappings Allocator”, KMAP 内存分配器的主要任务是为内核提供临时映射的功能。内核有时只期望将某段虚拟内存临时映射到物理内存上,当使用完毕之后就马上解除这种映射。KMAP 从 FIXMAP 管理的虚拟区域中划分了 FIX_KMAP_BEGIN 到 FIX_KMAP_END 的虚拟区域,并为这段虚拟内存提供了一个 “Page Table” kmap_pte. 当内核需要为临时使用某块为映射的物理内存时,可以快速从 KAMP 中分配一段虚拟内存,然后节后物理内存构造相应的 PTE,并将其写入 kmap_pte 中,这样内核就可以访问物理内存; 当内核使用完这段物理内存之后,KMAP 分配器就清除相应的 PTE 即可。

在 “32-bit Paging” 模式下,页表只有 “Page Directory Table” 和 “Page Table” 两级,由于系统在初始化时已经为 KMAP 维护的虚拟区域建立好了 PDE,而对于 “Page Table”,KMAP 分配器使用 kmap_pte 变量指向了 KMAP 分配器的 “Page Table”。因此内核需要进行临时映射的时候,可以结合相关的物理内存直接构造 PTE。并写入 kmap_pte 指定位置即可。KMAP 分配器在构建页表、使用虚拟内存、以及释放页表过程中,KMAP 是禁止内核抢占和禁止缺页发生的,因此内核在使用 KAMP 虚拟内存时要快速使用和快速释放.


BiscuitOS 实践

BiscuitOS 提供了一个简单的案例用于实现一个基于 “32-Bit Paging” 模式的 KMAP 分配器,其重点用于实践 KMAP 分配器分配物理内存、建立页表、使用、释放页表、以及释放物理内存的完整过程。BiscuitOS 中实践例子的方法如下 (具体实践办法可以参考如下文档):

32-bit Paging BiscuitOS 实践教程

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] KMAP Allocator with 32-Bit Paging  --->

make

源码配置完毕之后,执行 make 进行实际的源码部署,由于 KMAP 分配以依赖高端内存,因此在 BiscuitOS 需要配置高端内存使用的物理内存,配置如下:

cd BiscuitOS/output/linux-5.0-i386/
vi RunBiscuitOS.sh

  --> RAM_SIZE=1024

在 RunBiscuitOS.sh 脚本中将 BiscuitOS 使用的物理内存长度设置为 1024M,这样高端内存就有真实的物理内存。部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在 “X86-Paging-32bit-KMAP-default” 目录, 该目录存储相关的内核源代 码。接下来开发者使用如下命令:

cd X86-Paging-32bit-KMAP-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例. 环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在内核启动过程中,该模块自动加载运行,从运行的结果可以看到模块为 0xfffa6000 开始的虚拟地址建立 4K 页表,并将 88520 存储在该虚拟地址,最后使用 printk() 函数将该虚拟地址的内容输出,此时输出的内容正好是写入的内容. KMAP 区域范围从 0xfff1b000 到 0xfffba000.


KMAP 源码分析

在本案例中,通过从 KMAP 区域中找一块未使用的虚拟地址,然后从 Buddy 分配器中分配一个物理页,接着 KMAP 分配器建立页表将虚拟地址映射到物理内存上。案例对这块虚拟地址进行读写操作,操作完毕之后将虚拟地址的页表摧毁,最后回收物理内存。涉及的源码解析如下:

KMAP Memory Allocator With 32-Bit Paging Source Code

在本案例中,程序首先在 119 行调用 kmap_init() 函数对 kmap_pte 页表进行初始化,接着通过 121-123 行分配了一个物理页。函数接着在 126 行调用 BiscuitOS_kmap_atomic() 函数从 KMAP 分配器中分配虚拟内存。并将虚拟内存映射到物理页上,最后将 addr 变量指向映射完毕的虚拟内存上。函数在 129 行使用这段虚拟内存,并在 131-134 行将虚拟内存的值通过 printk() 函数打印出来。当使用完毕之后,函数在 139 行调用 BiscuitOS_kunmap_atomic() 函数将虚拟地址对应的页表清除,并在 144 行将物理内存进行释放。整个过程就是 KAMP 分配器的一个完整周期。

kmap_init() 函数用于初始化 KMAP 分配器使用的 “Page Table”。函数在 82 行调用 __fix_to_virt() 函数将 FIX_KMAP_BEGIN 转换成 KMAP 区域的起始虚拟地址,然后函数调用 kmap_get_fixmap_pte() 函数获得 KMAP 区域对应的 “Page Table”, 并将 BiscuitOS_kmap_pte 变量指向该页表. 在 kmap_get_fixmap_pte() 函数中,函数根据传入的虚拟地,以及 “32-bit Paging” 模式只有两级页表,因此函数通过 pgd_offset_k() 函数和 pte_offset_kernel() 函数就可以找到对应的 “Page Table” 页表。

BiscuitOS_kmap_atomic() 函数用于从 KAMP 区域中分配一段虚拟地址映射到物理内存上,其核心通过调用 BiscuitOS_kmap_atomic_prot() 函数实现,函数首先在 49 行调用 preepmt_disable() 函数禁止内核抢占,并在 50 行调用 pagefault_disable() 函数禁止内核缺页中断,以此让 KMAP 分配器可以安全进行页表映射。函数在 52 行调用 PageHighMem() 函数判断物理页是否来自高端内存,如果不是,那么函数直接通过 page_address() 函数返回物理内存对应的虚拟地址; 反之如果物理内存来自高端物理内存,那么函数在 55 行调用 kmap_atomic_idx_push() 函数在 56 行计算出 idx 的值,该 idx 值也是 “Page Table” PTE 索引值。函数在 57 行根据 idx 的值和 __fix_to_virt() 函数获得一个 KMAP 虚拟地址,接着函数会调用 BUG_ON() 函数检测 idx 指向的 PTE 是否不空,如果不空表示 PTE 已经映射其他物理页了,那么函数会抛出内核错误; 反之 PTE 可用,那么函数在 59 行调用 set_pte() 函数将 mk_pte() 与物理页和 prot 合成的 PTE 写入到 idx 对应的 PTE 中,这样页表建立完毕,此时系统调用 arch_flush_lazy_mmu_mode() 函数进行 MMU LAZY 模式,此时虚拟地址已经可以使用,函数最后返回了虚拟地址.

BiscuitOS_kunmap_atomic() 函数用于将临时映射的虚拟地址解除映射,其通过 __BiscuitOS_kunmap_atomic() 函数实现。

在 __BiscuitOS_kunmap_atomic() 函数中,函数首先在 90-91 行调用 __fix_to_virt() 函数获得 FIX_KMAP_END 和 FIX_KMAP_BEGIN 对应的虚拟地址,并检测即将释放页表的虚拟地址是否在这段虚拟地址范围内,如果在函数在 94 行调用 kmap_atomic_idx() 函数获得 KAMP type,并在 95 行计算该虚拟地址在 KMAP 的 idx,接着函数在 103 行调用 BiscuitOS_kpte_clear_flush() 函数清除对应的 PTE,其实现如上上图,函数通过 pte_clear() 函数清除 PTE 页表,并调用 __flush_tlb_one_kernel() 函数情况虚拟地址在 TLB 对应项。当 KAMP 清除页表之后,函数在 104 行调用 kmap_atomic_idx_pop() 函数进行对称出栈操作,函数接着在 105 行调用 arch_flush_lazy_mmu_mode() 函数退出 MMU Lazy 模式,并在 108 行调用 pagefault_enable() 函数启用内核缺页中断,最后函数在 109 行调用 preempt_enable() 函数启用内核抢占,至此 KMAP 释放虚拟地址页表操作完毕。


KMAP 页表故障分析

在 “32-bit Paging” 模式下的 KMAP 分配器,如果非法使用虚拟内存,那么会触发什么样的问题? 正如上图 142 行,如果在 KMAP 区域内使用已经释放页表的虚拟地址,那么将引发内核 PANIC:

从上图报错可以看出,142 行在访问 KMAP 虚拟地址 0xfffa6000 时,此时 PTE 页表已经删除,但 PDE 页表并未移除,那么 PDE 页表值是合法的,其可以结合虚地址找到对应的 PTE 页表,但此时对应的 PTE 已经被情况。早 PTE 情况的情况下访问虚拟地址,由于没有实际的物理内存进行映射,此时内核会触发 #PF 错误,导致系统 PANIC. 综上所述,不能使用 KMAP 释放页表的虚拟地址.


PKMAP Memory Allocator With 32-Bit Paging

PKMAP 内存分配器称为 “Permanent kamp Allocator”, PKMAP 内存分配器主要用于为内核提供持久的映射的内存。内核加载模块时期望为模块分配一段虚拟内存进行使用,当模块卸载时内核又回收这段虚拟内存。与 KMAP 分配器不同的是,KMAP 分配器只能提供临时且短暂映射的内存,但 PKMAP 分配器则需要提供持久但不一定永久映射的内存。

在 “32-bit Paging” 模式下,页表只有 “Page Directory Table” 和 “Page Table” 两级页表,内核在初始化阶段为 PKMAP 分配器维护的虚拟地址空间建立了对应 “Page Directory Table” 和 “Page Table”, 由于 PKMAP 一般维护长度为 LAST_PKMAP * PAGE_SIZE 虚拟内存,因此 “Page Table” 包含了 LAST_PKMAP 个 PTE。PKMAP 分配器使用变量 pkmap_page_table 指向 “Page Table”. 在 PKMAP 分配器中,PKMAP 维护了一个名为 “pkmap_count[]” 的数组,数组中的每个成员对应 PKMAP 虚拟区域中一个 PAGE_SIZE 内存块,并且按顺序进行映射,因此通过索引和 pkmap_count[] 可以从 PKMAP 虚拟区域中找到一块唯一的 PAGE_SIZE 内存块,另外 PKMAP 分配器将 pkmap_count[] 数组中的成员与 pkmap_page_table[] 数组进行一一对应,因此通过一个索引就可以从 PKMAP 分配器中获得一个虚拟地址和 PTE 页表。最后 PTE 指向一个物理页,因此 PKMAP 虚拟内存的映射可以通过一个合法的索引进行操作.


BiscuitOS 实践

BiscuitOS 提供了一个简单的案例用于实现一个基于 “32-Bit Paging” 模式的 PKMAP 分配器,其重点用于实践 KMAP 分配器分配物理内存、建立页表、使用、释放页表、以及释放物理内存的完整过程。BiscuitOS 中实践例子的方法如下 (具体实践办法可以参考如下文档):

32-bit Paging BiscuitOS 实践教程

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Permanent kamp Allocator (PKMAP) With 32-Bit Paging  --->

make

源码配置完毕之后,执行 make 进行实际的源码部署,由于 PKMAP 分配以依赖高端内存,因此在 BiscuitOS 需要配置高端内存使用的物理内存,配置如下:

cd BiscuitOS/output/linux-5.0-i386/
vi RunBiscuitOS.sh

  --> RAM_SIZE=1024

在 RunBiscuitOS.sh 脚本中将 BiscuitOS 使用的物理内存长度设置为 1024M,这样高端内存就有真实的物理内存。部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在 “X86-Paging-32bit-PKMAP-default” 目录, 该目录存储相关的内核源代 码。接下来开发者使用如下命令:

cd X86-Paging-32bit-PKMAP-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例. 环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在内核启动过程中,该模块自动加载运行,从运行的结果可以看到模块为 0xbe81000 开始的虚拟地址建立 4K 页表,并将 88520 存储在该虚拟地址,最后使用 printk() 函数将该>虚拟地址的内容输出,此时输出的内容正好是写入的内容. PKMAP 区域范围从 0xbe800000 到 0xbec00000.


PKMAP 源码分析

在本案例中,通过从 PKMAP 区域中找一块未使用的虚拟地址,然后从 Buddy 分配器中分配一个物理页,接着 PKMAP 分配器建立页表将虚拟地址映射到物理内存上。案例对这块虚拟地址进行读写操作,操作完毕之后将虚拟地址的页表摧毁,最后回收物理内存。涉及的源码解析如下:

PKMAP Memory Allocator With 32-Bit Paging Source Code

在本案例中,程序首先在 273 行调用 permanent_kmap_init() 函数为 PKMAP 维护的虚拟区域建立 “Page Directory Table” 和 “Page Table”, 接着程序在 275-277 行从 Buddy 分配器中分配一个物理页,最后函数在 280 行调用 BiscuitOS_kmap() 函数将 KMAP 虚拟区域的虚拟地址映射到指定的物理页上,并将 addr 变量指向映射物理页的虚拟地址。程序在 282 行使用了该虚拟地址,并在 282-287 行从虚拟地址中读取内容。程序在使用完虚拟地址后,在 290 行调用 BiscuitOS_kunmap() 函数清除虚拟地址对应的页表,并回收虚拟内存,最后函数在 292 行调用 __free_page() 函数回收物理内存.

permanent_kmap_init() 函数用于在内核初始化阶段为 PKMAP 维护的虚拟内存建立对应的 “Page Directory Table” 和 “Page Table”. 函数首先在 257 行获得 PKMAP 虚拟区域的基地址,然后调用 page_table_range_init() 函数为 PKMAP 虚拟区域的所有虚拟内存建立两级页表。页表建立完毕之后,函数在 260-262 行通过查页表的方式获得 PKMAP 虚拟区起始地址对应的 PTE, 然后使用变量 BiscuitOS_pkmap_page_table 指向该 PTE. page_table_range_init() 函数用于建立 PKMAP 虚拟区的两级页表,函数首先在 228-230 行获得虚拟区域对应的 PGD,然后在 232 行中遍历涉及的所有 PGD 入口。在每次遍历过程中,由于 “32-bit Paging” 模式之后两级页表,因此此时 PGD 即使 PDE。函数接着在 236 行调用 pmd_val() 和 _PAGE_PRESENT 标志查找 PDE 是否存在,如果不存在,那么虚拟地址对应的 “Page Table” 不存在,因此函数首先在 237 行调用 alloc_low_page() 函数为 “Page Table” 分配物理页,接着函数在 241 行调用 set_pmd() 函数设置 PDE,此时 PDE 的权限字段包含了 _PAGE_TABLE 集合,并将 “Page Table” 写入了 PDE 中,此时 “Page Table” 就可以使用了。

BiscuitOS_kmap() 函数用于将 PKMAP 虚拟区域的虚拟地址映射到物理页上。函数在 197 行调用了 might_sleep() 函数,以此告诉内核 PKMAP 在映射页表过程中会睡眠。函数接着在 198 行判断物理页是否来自高端内存,如果物理内存不来自高端内存,那么函数直接调用 page_address() 函数直接返回对线性映射区域的虚拟地址; 反之如果物理物理页来自高端内存,那么函数调用 BiscuitOS_kmap_high() 函数进行实际的页表映射.

BiscuitOS_kmap_high() 函数用于将 PKMAP 虚拟区的虚拟地址映射到高端物理内存上。函数在 144 行调用 page_address() 函数获得对应的虚拟地址,如果此时虚拟地址不存在,那么函数调用 map_new_virtual() 函数映射一个新的虚拟地址。映射完毕之后,函数在 147 行将虚拟地址在 pkmap_count[] 数组中的引用计数加一,如果相加之后的结果小于 2,那么内核就会报错,因此此处认为一个正在使用的 PKMAP 虚拟地址的引用计数不应该小于 2. 函数最后将可用的虚拟地址进行返回.

map_new_virtual() 函数是 PKMAP 分配器的核心实现,其作用是将一个 PKMAP 虚拟地址映射到指定的物理内存上。函数首先在 99 行使用死循环,在每次循环,函数首先调用 get_next_pkmap_nr() 获得上一次 PKMAP 分配的可用虚拟地址对应的索引,函数此时检测该索引是不是已经为 0,如果为 0,那么 PKMAP 目前没有可用的虚拟地址,那么函数在 102 行调用 flush_all_zero_pkmaps() 函数清除没有使用的虚拟地址对应的页表; 函数在 105 行调用检测索引在 pkmap_count[] 数组中的成员是否为 0,如果为 0,那么找到一块可用的虚拟地址; 反之没有找打,函数继续在 107 行进行检测 count 是否为 0,如果不为 0,那么函数继续查找 PKMAP 的下一个可用虚拟地址; 反之如果为 0,那么 PKMAP 没有可用的虚拟内存,那么函数将运行 111-119 行的代码,这些代码的作用是让进程调度执行其他任务,直到有可用的虚拟内存,程序再次被执行,在执行时,函数在 122 行检测物理页是否包含了虚拟地址,如果有则染回虚地址,如果没有跳转到 start 处重新查找; 如果上述在查找可用虚拟内存的时候已经找到,那么函数直接结束循环,函数接着在 128 行调用 BISCUITOS_PKMAP_ADDR() 函数获得 last_pkmap_nr 所有对应的虚拟地址,接下来建立 PTE 页表,函数在 129 行调用 set_pte_at() 函数将索引在 BiscuitOS_pkmap_page_table[] 数组中对应的 PTE 写入 kmap_prot 权限集合,以此建立 PTE 页表。函数最后在 133 行更新了索引在 pkmap_count[] 数组中的引用计数,并在 134 行调用 set_page_address() 函数设置物理页对应的虚拟地址. 至此虚拟地址已经映射物理页,可以正常使用.

BiscuitOS_kunmap() 函数用于结束物理页的映射关系。函数首先在 205 行判断进程是否在中断中,如果在,那么函数直接调用 BUG_ON(); 反之函数在 207 行检测物理页是否来自高端内存,如果不是,那么函数直接返回; 反之函数调用 BiscuitOS_kunmap_high() 函数进行实际的解除操作.

BiscuitOS_kunmap_high() 函数用于解除物理页的映射关系。函数首先在 162 行获得物理页映射的虚拟地址,函数接着在 164 行获得虚拟地址对应的 PKMAP 索引。接着函数在 171 行判断索引在 pkmap_count[] 数组的计数情况进行不同的处理。当计数为 0 时直接报错; 当计数为 1 时说明虚拟地址没有映射物理内存,此时函数唤醒 WAKE_QUEUE, 以此告诉睡眠的进程可以分配期望的 PKMAP 虚拟地址。函数并没有刷新页表或清除页表,清除页表的动作放在了 PKMAP 分配器分配内存的时候.


FIXMAP Memory Allocator With 32-Bit Paging

FIXMAP 内存分配器称为 “Fixed Mapping Allocator”, FIXMAP 内存分配器的主要任务是为内核固定功能的虚拟地址映射物理内存。对于 FIXMAP 管理的虚拟地址,其虚拟地址在内核源码编译阶段已经定义好,FIXMAP 分配器提供了虚拟地址的分配逻辑以及将虚拟地址映射到物理内存。

FIXMAP 内存分配器详解

在 “32-bit Paging” 模式下,页表只有 “Page Directory Table” 和 “Page Table” 两级页表,内核在初始化阶段通过 early_ioremap_page_table_range_init() 函数为 FIXMAP 维护的虚拟区域建立的 “Page Directory Table” 和 “Page Table” 两级页表。FIXMAP 分配内存时通过索引找到指定的虚拟地址,然后遍历页表找到 PDE 和 PTE,最后填充 PTE 将虚拟地址映射到物理内存上。页表建立完毕之后刷新虚拟地址对应的 TLB 之后就可以虚拟内存。当虚拟内存使用完毕之后,FIXMAP 分配器通过虚拟地址找到对应的 PTE 页表,并将其清零和刷新对应的 TLB,至此完整的 FIXMAP 分配器行为完成.


BiscuitOS 实践

BiscuitOS 提供了一个简单的案例用于实现一个基于 “32-Bit Paging” 模式的 FIXMAP 分配器,其重点用于实践 FIXMAP 分配器分配物理内存、建立页表、使用、释放页表、以及释放物理内存的完整过程。BiscuitOS 中实践例子的方法如下 (具体实践办法可以参考如下文档):

32-bit Paging BiscuitOS 实践教程

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] FIXMAP Allocator with 32-Bit Paging  --->

make

源码配置完毕之后,执行 make 进行实际的源码部署,由于 FIXMAP 分配以依赖高端内存,因此在 BiscuitOS 需要配置高端内存使用的物理内存,配置如下:

cd BiscuitOS/output/linux-5.0-i386/
vi RunBiscuitOS.sh

  --> RAM_SIZE=1024

在 RunBiscuitOS.sh 脚本中将 BiscuitOS 使用的物理内存长度设置为 1024M,这样高端内存就有真实的物理内存。部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在 “X86-Paging-32bit-FIXMAP-default” 目录, 该目录存储相关的内核源代码。接下来开发者使用如下命令:

cd X86-Paging-32bit-FIXMAP-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例. 环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在内核启动过程中,该模块自动加载运行,从运行的结果可以看到模块为 0xfffff000 开始的虚拟地址建立 4K 页表,并将 88520 存储在该虚拟地址,最后使用 printk() 函数将该虚拟地址的内容输出,此时输出的内容正好是写入的内容. FIXMAP 区域范围从 0xfffff000 到 0xffd17000.


FIXMAP 源码分析

在本案例中,通过从 FIXMAP 区域中找一块未使用的虚拟地址,然后从 Buddy 分配器中分配一个物理页,接着 PKMAP 分配器建立页表将虚拟地址映射到物理内存上。案例对这块虚拟地址进行读写操作,操作完毕之后将虚拟地址的页表摧毁,最后回收物理内存。涉及的源码解析如下:

FIXMAP Memory Allocator With 32-Bit Paging Source Code

在本案例中,程序首先在 82 行调用 fix_to_virt() 函数获得 FIX_HOLE 对应的虚拟地址,然后函数在 85-88 行调用 alloc_page() 分配物理页并获得对应的物理地址。接着函数在 91 行调用 BiscuitOS_set_fixmap() 函数将虚拟地址与物理地址建立映射,页表建立之后,函数在 93 行将 88520 写入虚拟内存里,接着函数在 95-99 行通过 printk() 函数打印了虚拟内存的内容。在虚拟内存使用完毕之后,函数在 102 行再次调用 BiscuitOS_set_fixmap() 函数解除虚拟地址到物理地址的映射,最后函数在 107 行调用 __free_page() 函数释放物理内存. 以上便是一次完整 FIXMAP 内存使用方法.

BiscuitOS_set_fixmap() 函数的作用是将虚拟地址映射到物理内存上,其通过调用 BiscuitOS_native_set_fixmap() 函数实现,函数在 66 行定义了 FIXMAP PTE 标志集合与 __default_kernel_pte_mask 相与,过滤掉无效的标志。函数接着调用 __BiscuitOS_native_set_fixmap() 函数进行虚拟地址映射物理内存。在 __BiscuitOS_natvive_set_fixmap() 函数里,函数在 52 行通过 __fix_to_virt() 函数获得 idx 索引对应的虚拟地址,接着函数在 54 行检测 idx 索引是否越界,如果是则调用 BUG() 函数报错,并且直接返回; 放置如果 idx 索引符合要求,那么函数在 58 行调用 BiscuitOS_set_pte_vaddr() 函数进行虚拟地址映射物理内存,映射完毕之后,将 BiscuitOS_fixmap_set 加一。

BiscuitOS_set_pte_vaddr() 函数用于将 FIXMAP 虚拟区域的虚拟地址映射到物理内存上。由于在 “32-bit Paging” 模式下,页表只包含两级页表,因此函数首先在 28 行通过 swapper_pg_dir 和 pgd_index() 函数获得虚拟地址对应的 PGD,此时 PGD 即是 PDE。函数接着在 37 行调用 pte_offset_kernel() 函数获得虚拟地址对应的 PTE。函数接着在 38 行通过调用 pte_none() 函数判断 pteval 参数是否为空,如果为空,那么代表清除 PTE,因此函数在 41 行调用 pte_clear() 函数清除对应的 PTE 页表; 反之 pteval 不为空,那么函数调用 set_pte_at() 函数将 pteval 作为 PTE 的内容设置新的 PTE。处理完之后,函数调用 __flush_tlb_one_kernel() 函数刷新虚拟内存对应的 TLB.


FIXMAP 页表故障分析

在 “32-bit Paging” 模式下的 FIXMAP 分配器,如果非法使用虚拟内存,那么会触发什么样的问题? 正如上图 105 行,如果在 FIXMAP 区域内使用已经释放页表的虚拟地址,那么将引发内核 PANIC:

从上图报错可以看出,105 行在访问 FIXMAP 虚拟地址 0xfffff000 时,此时 PTE 页表已经删除,但 PDE 页表并未移除,那么 PDE 页表值是合法的,其可以结合虚地址找到对应的 PTE 页表,但此时对应的 PTE 已经被情况。在这种情况的情况下访问虚拟地址,由于没有实际的物理内存进行映射,此时内核会触发 #PF 错误,导致系统 PANIC. 综上所述,不能使用 FIXMAP 释放页表的虚拟地址.


IO Memory Mapping (ioremap) With 32-Bit Paging

“IO Memory Mapping” 指的是将虚拟地址映射到外设 IO 的物理内存上,使内核可以像访问内存一样访问 IO 的物理地址。外设的地址空间有两种划分方法,第一种将外设的地址空间独立成 “IO 地址空间”,它与内存地址空间并行存在,需要使用特殊的指令才能访问 “IO 地址空间”; 第二种划分方法是将外设的地址空间与内存的地址空间统一编址为物理地址空间,访问外设与内存一致,采用同样的指令. 当内核采用第二种划分方法是,内核可以将虚拟地址映射到外设的物理地址上,并像访问内存一样对外设物理内存进行读写操作。当访问完外设之后,可以将虚拟地址映射到外设物理地址的页表进行清除,那么内核完成了对外设的访问。

在 “32-bit Paging” 模式下,页表只有 “Page Directory Table” 和 “Page Table” 两级页表,内核在初始化阶段通过 early_ioremap_page_table_range_init() 函数为 IO 区域建立的 “Page Directory Table” 和 “Page Table” 两级页表。内核在为外设建立页表时只需从内核空间找到一块未使用的虚拟地址,以及外设的物理地址便可创建页表,页表创建完毕之后刷新虚拟地址对应的 TLB 之后即可访问外设的物理地址。正如上图所示为当前系统注册的外设对应的物理地址。


BiscuitOS 实践

BiscuitOS 提供了一个简单的案例用于实现一个基于 “32-Bit Paging” 模式的外设物理地址的映射,其重点用于实践将虚拟地址映射到外设的物理内存上,并访问外设的物理内存,最后释放页表的完整过程。BiscuitOS 中实践例子的方法如下 (具体实践办法可以参考如下文档):

32-bit Paging BiscuitOS 实践教程

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] IO Memory Mapping (ioremap) with 32-Bit Paging  --->

make

源码配置完毕之后,执行 make 进行实际的源码部署,由于实践中需要使用高端物理内存模拟外设的物理内存,因此在 BiscuitOS 需要配置高端内存使用的物理内存,配置如下:

cd BiscuitOS/output/linux-5.0-i386/
vi RunBiscuitOS.sh

  --> RAM_SIZE=1024

在 RunBiscuitOS.sh 脚本中将 BiscuitOS 使用的物理内存长度设置为 1024M,这样高端内存就有真实的物理内存。部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在 “X86-Paging-32bit-IO-default” 目录, 该目录存储相关的内核源代码。接下来开发者使用如下命令:

cd X86-Paging-32bit-IO-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例. 环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在内核启动过程中,该模块自动加载运行,从运行的结果可以看到模块为 0xf7819000 开始的虚拟地址建立 4K 页表,并将 88520 存储在该虚拟地址,最后使用 printk() 函数将该虚拟地址的内容输出,此时输出的内容正好是写入的内容. “BiscuitOS IO Memory” 的范围正好是 “0xfec10000-0xfec11000”.


IO 源码分析

在本案例中,通过 Platform 设备驱动框架注册了一个设备,设备的物理地址从 BISCUITOS_IO_BASE 开始的 4K 物理内存,接着案例从内核的 VMALLOC 虚拟区域中找到一块没有使用的虚地址通过建立页表的方式映射到物理内存上,然后访问外设的物理内存。当访问完毕之后,又将虚拟地址对应的页表进行释放。涉及的源码解析如下:

IO Memory Mapping (ioremap) With 32-Bit Paging Source Code

在本案例中,程序首先基于 Platform 框架注册了一个 Platform 设备. 函数首先在 221 行调用 platform_driver_register() 函数注册了 BiscuitOS_driver 描述的驱动,BiscuitOS_drver 定义在 207-213 行,其包含了一个 probe 接口,以及驱动的名字为 “BiscuitOS”. 接着函数在 228 行调用了 platform_device_register_simple() 函数注册一个 platform 设备,设备的名字设置为 “BiscuitOS”, 与驱动的名字保持一致,设备还包含了一组资源 BiscuitOS_resources, 其定义在 197-204 行。BiscuitOS_resources[] 资源只定义了外设的一块内存,其物理地址起始于 BISCUITOS_IO_BASE, 长度为 4K.

当设备加载的时候,驱动的 probe 接口函数就会执行,这里调用了 BiscuitOS_probe() 函数。函数首先在 175 行调用 platform_get_resource() 函数获得外设的物理内存信息,其通过一个 struct resource 数据结构进行维护。函数接着在 177 行调用了 BiscuitOS_ioremap() 函数将内核空间的虚拟地址映射到外设的物理内存上,并将变量 addr 指向映射完毕的虚拟地址上。函数在 180 行将 88520 写入到外设的物理内存上,并在 182-186 行将外设物理内存的内容通过 printk() 打印出来。当使用完外设物理内存之后,函数在 189 行调用 BiscuitOS_iounmap() 函数将虚拟地址对应的页表清除,以此完成外设内存的访问流程.

BiscuitOS_ioremap() 函数用于将虚拟地址映射到外设的物理内存上,以便内核可以直接访问外设的物理内存。函数首先在 84 行对外设的物理地址进行对齐操作,使其按物理页进行对齐。函数接着在 91-93 行构造外设 IO 所有使用的 PTE 页表内容,其包含了 PAGE_KERNEL_IO 集合,以及 _PAGE_CACHE_MODE_UC 集合。函数接着在 95 行调用 get_vm_area_caller() 函数从 VMALLOC 虚拟区域中找到一块未使用的虚拟地址,并将相关的信息存储在 struct vm_struct 数据结构了。函数在 97-98 行分别设置了所需的虚拟地址和物理地址之后,在 100 行调用 BiscuitOS_ioremap_page_range() 函数进行实际的映射工作。当映射完毕之后,函数在 102 行将虚拟地址进行返回。

BiscuitOS_ioremap_page_range() 函数进行了实际的页表映射工作,函数首先在 56 行调用 might_sleep() 函数为可能进入睡眠做准备,函数在 57 行如果检测到地址越界,那么函数调用 BUG_ON() 函数进行报错. 函数在 60 行调用 pgd_offset_k() 函数获得虚拟地址对应的 PGD,接着使用一个 do-while 循环,每循环一次,PGD 自加一,物理地址和虚拟地址都指向一下个 PGD,只要虚拟地址每到达最后一个虚拟地址,循环不停止。在每次循环中,由于 “32-bit Paging” 模式只包含两级页表,因此 PGD 即是 PDE,因此函数直接调用 ioremap_pte_range() 函数建立 PTE 页表.

ioremap_pte_range() 函数用于建立 PTE 页表,函数在 33 行首先获得物理地址对应的物理页帧号,接着调用 pte_kernel_alloc() 函数为虚拟地址分配 PTE。如果 PTE 分配失败,则返回 ENOMEM。函数接着使用 do-while 循环为一段虚拟地址建立 PTE 页表。由于没有真实的外设内存,这里使用一个高端内存的物理页进行模拟,因此函数在 40-41 行从高端物理内存分配一个物理页,并获得物理页对应的页帧。函数接着在 43 行检测当前的 PTE 是否有效,如果无效,那么函数调用 BUG_ON() 函数报错; 反之函数调用 set_pte_at() 设置 PTE 页表,PTE 的权限来自参数 prot。设置完毕之后将 pfn、pte、以及虚拟地址加一,直到虚拟地址无效才停止循环。

BiscuitOS_iounmap() 函数用于解除映射外设物理地址的映射关系。函数在 158 行调用 find_vm_area() 函数获得虚拟地址在 VMALLOC 虚拟区域的 struct vm_struct 数据结构,接着函数在 165 行调用 BiscuitOS_remove_vm_area() 函数进行实际的解除操作。最后函数在 166 行将 struct vm_struct 数据结构释放,以便保持 VMALLOC 虚拟区域的完整性。BiscuitOS_remove_vm_area() 函数用于实际的解除映射,函数在 145 行调用 flush_cache_vunmap() 函数将 VMALLOC 虚拟区域的内存进行刷新,接着调用 vunmap_page_range() 函数将对应的 PTE 页表清除,最后调用 flush_tlb_kernel_range() 函数刷新虚拟地址对应的 TLB.

vunmap_page_range() 函数用于清除页表,函数在 127 行调用 pgd_offset_k() 函数获得虚拟地址对应的 PGD,然后通过 do-while 循环将虚拟区域涉及的 PGD 遍历一遍。在每一次 PGD 循环过程中,由于 “32-bit Paging” 只存在两级页表,因此该 PGD 即是 PDE,那么函数在 134 行调用 pgd_addr_end() 函数获得下一个 PGD 的起始地址,只要循环检测到下一个虚拟地址不是 end,那么训话不停止,函数接着调用 vunmap_pte_range() 函数清除 PTE。

vunmap_pte_range() 函数用于清除 PTE。函数首先在 109 行调用 pte_offset_kernel() 函数获得虚拟地址对应的 PTE,接着函数使用 do-while 循环遍历 addr 到 end 之间的所有 PTE。在每次遍历过程中,由于本次实践没有真实的外设物理内存,因此只能使用高端物理内存进行模拟,因此在释放 PTE 的时候也应该释放高端物理内存页,因此函数在 113 行从 PTE 中获得物理页帧号,并通过物理页帧号找到了对应的物理页,函数调用 __free_page() 函数释放物理页。函数接着在 116 行调用 ptep_get_and_clear() 函数清除了 PTE 的内容,清除完毕之后,如果清除前的 PTE 是无效或者不存在的,那么函数将警告。


IO 页表故障分析

在 “32-bit Paging” 模式下,如上图 192 行,如果使用一个已经解除外设物理地址映射的虚拟地址,那么将引发内核 PANIC:

从上图报错可以看出,192 行在访问映射过外设物理内存的虚拟地址 0xf7819000 时,此时 PTE 页表已经删除,但 PDE 页表并未移除,那么 PDE 页表值是合法的,其可以结合虚地址找到对应的 PTE 页表,但此时对应的 PTE 已经被清除。在这种情况的情况下访问虚拟地址,由于没有实际的外设物理内存进行映射,此时内核会触发 #PF 错误,导致系统 PANIC. 综上所述,不能使用外设物理地址释放页表的虚拟地址.


Physical Frame Direct Mapping With 32-Bit Paging

在有的硬件需求场景中需要将某端内存隐藏起来让操作系统无法看到,这些物理内存并没有映射到 mem_map[] 数组中,因此这些物理页没有使用 struct page 数据结构进行管理,而是仅仅使用物理页帧号进行管理。当内核需要使用这段物理内存时,内核可以将虚拟地址直接映射到这些物理内存上,且无需为物理内存创建 struct page 数据结构进行维护。使用该机制的场景不多,但该机制的存在可以大大减少 struct page 的开销,也可以直接将虚拟地址映射到物理内存上,管理简单等特点。

在 “32-bit Paging” 模式下,页表只有 “Page Directory Table” 和 “Page Table” 两级页表,因此内核只需建立两级页表即可使用这些物理内存。由于两级页表都需要新创建,因此需要从 Buddy 分配器中为页表分配内存,然后根据虚拟地址找到对应的 PDE 和 PTE。页表创建完毕之后刷新虚拟地址对应的 TLB 之后即可访问这段物理地址.


BiscuitOS 实践

BiscuitOS 提供了一个简单的案例用于实现一个基于 “32-Bit Paging” 模式下虚拟地址直接映射只有物理页帧的物理内存,其重点用于实践将虚拟地址映射到只有页帧的物理内存上,并对这部分内存进行访问,访问完毕之后释放页表的完整过程。BiscuitOS 中实践例子的方法如下 (具体实践办法可以参考如下文档):

32-bit Paging BiscuitOS 实践教程

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Physical Frame Direct Mapping With 32-Bit Paging  --->

make

源码配置完毕之后,执行 make 进行实际的源码部署,由于实践中需要将一段物理内存进行隐藏,以便让内核不会为这段物理内存创建 struct page,因此需要修改 BiscuitOS 的 CMDLINE,相关配置为 RunBiscuitOS.sh 的 CMDLINE 参数中,配置如下:

cd BiscuitOS/output/linux-5.0-i386/
vi RunBiscuitOS.sh

CMDLINE="root=/dev/sda rw rootfstype=${FS_TYPE} console=ttyS0 init=/linuxrc loglevel=8 mem=510M"

在 RunBiscuitOS.sh 脚本中向 CMDLINE 参数添加 “mem=510M” 字段,该字段添加后会让内核可用的物理内存只有 510M,由于默认的 BiscuitOS 内核可以使用 512M 物理内存,因此最后的 2M 物理内存被隐藏了,那么内核在启动的过程中就不会为最后 2M 创建 struct page 数据结构。部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在 “X86-Paging-32bit-PFN-default” 目录, 该目录存储相关的内核源代码。接下来开发者使用如下命令:

cd X86-Paging-32bit-PFN-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例. 环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在内核启动过程中,该模块自动加载运行,从运行的结果可以看到模块为 0xe0d00000 开始的虚拟地址建立 4K 页表,并将 88520 存储在该虚拟地址,最后使用 printk() 函数将该虚拟地址的内容输出,此时输出的内容正好是写入的内容.


PFN 源码分析

在本案例中,将一个虚拟地址映射到只有物理页帧的物理内存上,映射完毕之后对虚拟地址进行访问,访问完毕之后再将虚拟地址对应的页表清除。涉及的源码解析如下:

Physical Frame Direct Mapping With 32-Bit Paging Source Code

在本案例中,程序首先在 95 行将指定的虚拟地址、只有物理页帧的物理地址,以及物理内存的长度传递给 BiscuitOS_build_page_table() 函数,函数为其建立虚拟地址到物理地址的映射,映射完毕之后将变量 addr 指向已经映射的虚拟地址上。函数接着在 98 行将 88520 存储到该虚拟地址上,并在 100-107 行将虚拟地址的内容通过 printk() 函数打印。函数在使用完虚拟地址之后在 110 行调用 BiscuitOS_clear_page_table() 函数将虚拟地址对应的页表进行清除。

BiscuitOS_build_page_table() 函数的作用是将虚拟地址映射到只有物理页帧的物理地址上。函数首先在 34 行调用 pgd_offset_k() 函数获得虚拟地址对应的 PGD,由于在 “32-paging” 模式下,分页机制只采用两级页表,因此此时 PGD 即是 PDE。函数接着在 37 行检测 PDE 是否存在,如果不存在,那么需要为虚拟地址分配 “Page Table”, 函数在 42-46 行从 Buddy 分配器中分配一个物理页作为 “page table”, 接着通过 48-49 行两个函数将 “page table” 填充到 PDE 中,此时 PDE 别标记为 _PAGE_TABLE, 那么此时 PDE 指向了下一级页表,此时函数在 50 行检测 PTE 与 “page table” 是否一致,如果不一致,那么报错. 函数接着在 54 行调用 pte_offset_kernel() 函数获得虚拟地址对应的 PTE。如果 PTE 存在,那么函数接着在 58 行调用 set_pte_at() 函数将物理页帧和 PAGE_KERNEL 标志填充到 PTE 中,最后调用 __flush_tlb_one_kernel() 函数刷新虚拟地址对应的 TLB.

BiscuitOS_clear_page_table() 函数用于将虚拟地址对应的页表进行清除。函数在 70 行调用 pgd_offset_k() 函数获得虚拟地址对应的 PGD,由于在 “32-bit Paging” 模式下,分页机制只有两级页表,那么此时 PGD 即是 PDE。函数在 73 行检测了 PDE 的有效性,PDE 有效之后,函数在 78 行调用 pte_offset_kernel() 函数获得虚拟地址对应的 PTE,如果该 PTE 有效,那么函数在 85 行调用 pte_clear() 函数将 PTE 内容清空,并调用 __flush_tlb_one_kernel() 函数刷新了虚拟地址对应的 TLB.


PFN 页表故障分析

在 “32-bit Paging” 模式下,如上图 113 行,如果使用一个已经解除物理地址映射的虚拟地址,那么将引发内核 PANIC:

从上图报错可以看出,113 行在访问映射过物理内存的虚拟地址 0xe0d00000 时,此时 PTE 页表已经删除,但 PDE 页表并未移除,那么 PDE 页表值是合法的,其可以结合虚地址找到对应的 PTE 页表,但此时对应的 PTE 已经被清除。在这种情况的情况下访问虚拟地址,由于没有实际的物理内存进行映射,此时内核会触发 #PF 错误,导致系统 PANIC. 综上所述,不能使用外设物理地址释放页表的虚拟地址.


Linear Mapping With 32-Bit Paging

在 Linux 中将虚拟地址分作两部分,一部分是用户程序使用的 “Userspace”, 而另外一部分则是内核和外设使用的内核空间。在内核空间根据映射方式的不同将其又划分为两部分,第一部分是线性映射空间,在该区域内核将整个虚拟地址一一映射到物理内存上,构成了虚拟地址连续和物理地址连续的区域; 另外一部分为非线性映射空间,在该区域内核将虚拟地址动态映射到物理地址上,因此构成了虚拟地址连续到物理地址不一定连续的空间。对于线性映射空间,内核在启动阶段就已经为该区域建立了页表,因此内核在使用这段虚拟内存的时候可以直接使用而不需要动态建立页表,并且由于存在一一映射的关系,可以使用线性公式就可以通过虚拟地址计算出物理地址,也可以通过物理地址计算出虚拟地址.

在 “32-bit Paging” 模式下,页表只有 “Page Directory Table” 和 “Page Table” 两级页表,因此内核只需建立两级页表即可使用这些物理内存。内核在初始化时就为线性映射的区域一一建立好了 “Page Directory Table” 和 “Page Table”, 因此在使用的虚拟地址或物理地址的时候无需查找页表,只需通过简单的线性公式就可以获得虚拟地址和物理地址。但开发者不要简单的认为线性映射区的虚拟内存不需要页表.


BiscuitOS 实践

BiscuitOS 提供了一个简单的案例用于实现一个基于 “32-Bit Paging” 模式下线性映射去虚拟地址和物理地址的使用,其重点用于实践线性区物理地址和虚拟地址的转换和使用,并通过遍历页表查找虚拟地址对应的物理地址的完整过程。BiscuitOS 中实践例子的方法如下 (具体实践办法可以参考如下文档):

32-bit Paging BiscuitOS 实践教程

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] X86 32bit Paging mode  --->
              [*] Linear Mapping with 32-Bit Paging  --->

make

部署完毕之后使用如下命令切到源码路径:

cd BiscuitOS/output/linux-5.0-i386/package/

在该目录下存在 “X86-Paging-32bit-Linear-default” 目录, 该目录存储相关的内核源代码。 接下来开发者使用如下命令:

cd X86-Paging-32bit-Linear-default
make download
make
make install
make pack
tree

执行完上面的命令之后,内核部分的代码已经部署和打包到 BiscuitOS 里,通过 tree 命令可以看到源码结构,其中 main.c 函数就是内核部分的核心实现,接下来就是运行该实例. 环境部署完毕之后,开发者可以直接运行 BiscuitOS. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh

在内核启动过程中,该模块自动加载运行,从运行的结果可以看到模块从线性区分配一块内存,其虚拟地址为 0xde752000,并将 88520 存储在该虚拟地址,最后使用 printk() 函数将该虚拟地址的内容输出,此时输出的内容正好是写入的内容. 内核还输出了通过线性关系和查表找到的虚拟地址和物理地址等信息.


Linear 映射源码分析

在本案例中,从 Buddy 分配器中获得一个线性区的物理页,并通过线性关系和查页表的方式分别获得该物理页对应的虚拟地址和物理地址,以及页表信息。涉及的源码解析如下:

Linear Mapping With 32-Bit Paging Source Code

函数首先在 53 行调用 alloc_page() 从 Buddy 分配器中分配一个线性区的物理页,函数在 58 行调用 page_address() 获得物理页对应的线性地址,接着调用 page_to_pfn() 获得物理页的物理页帧,以及调用 page_to_phys() 获得物理页的物理地址,以上三个信息作为参考信息。函数在 63 行调用 BISCUITOS_va() 函数通过线性区映射公式直接将物理地址转换成虚拟地址,同理在 64 行调用 BISCUITOS_pa() 函数通过线性区映射公式直接将虚拟地址转换成物理地址。接下来函数通过查页表的方式在 67 行调用 BiscuitOS_follow_page_table() 函数获得虚拟地址的 PTE。在获得 PTE 之后,函数在 71 行调用 pte_pfn() 函数从 PTE 中读取出物理页帧的信息,接着函数在 72 行调用 PFN_PHYS() 函数获得物理页帧对应的物理地址。函数继续在 75-76 行将 88520 存储在虚拟内存。函数从 78-91 行将之前获得信息通过 printk() 函数打印。函数最后在 93 行调用 __free_page() 函数释放物理页。

线性区线性映射关系提供了以上两个宏,BISCUITOS_va() 宏用于将一个线性区的物理地址转换成线性区的虚拟地址,其实现很简单,由于物理内存从 0 地址开始映射到虚拟地址上,因此将物理地址加上 PAGE_OFFSET 即可. 同理 BISCUITOS_pa() 宏用于将一个线性区虚拟地址转换成物理地址,其实现也很简单,直接将线性区虚拟地址减去 PAGE_OFFSET 即可。

BiscuitOS_follow_page_table() 函数用于查找线性区虚拟地址对应的 PTE。在 “32-bit Paging” 模式下,分页只有两级页表,因此函数首先在 30 行调用 pgd_offset_k() 函数获得虚拟地址对应的 PGD,此时 PGD 即使 PDE,因此函数在 33 行通过调用 pmd_none() 函数和 pmd_bad() 函数检测 PDE 的有效性,如果有效,函数接着在 36 行调用 pte_offset_kernel() 函数获得虚拟地址对应的 PTE。

线性区页表建立过程可以查看如下链接:

kernel_physical_mapping_init


Intel i386 页表初始化流程分析

init_mem_mapping

probe_page_size_mask

init_memory_mapping

split_mem_range

kernel_physical_mapping_init

one_md_table_init

one_page_table_init

memory_map_tottom_up

early_ioremap_page_table_range_init

page_table_range_init

native_pagetable_init

paging_init

pagetable_init

permanent_kmaps_init

kmap_init

kmap_get_fixmap_pte


init_mem_mapping() 函数用于内核启动阶段为内核创建线性区和 IO 外设区的页表。函数首先在 650 行调用 probe_page_size_mask() 函数检测系统可支持映射物理页大小的情况。接着在 660 行调用 init_memory_mapping() 函数为物理地址 0 到 ISA_END_ADDRESS 的物理内存建立线性映射。建立完毕之后函数在 669 行调用 MEMBLOCK 分配器向上分配内存时,函数在 779-680 行调用 memory_map_bottom_up() 函数为 kernel_end 到 end,以及 ISA_END_ADDRESS 到 kernel_end 之间的物理内存建立线性映射; 反之函数直接调用 memory_map_top_down() 函数为 ISA_END_ADDRESS 到 end 的物理内存区域建立线性映射。函数接着在 691 行为 IO 区域建立页表,这里包含了 FIXMAP、KAMP 映射的虚拟区域. 函数接着在 694 行重新加载了 CR3 寄存器,将 CR3 寄存器指向了 swapper_pg_dir,并调用 __flush_tlb_all() 函数刷新所有的 TLB 和 CACHE。

probe_page_size_mask() 函数用于探测并设置物理页的大小。Intel 在 Linux 上支持的物理页大小分别为 PG_LEVEL_4K、PG_LEVEL_2M、PG_LEVEL_1G 和 PG_LEVEL_512G,因此内核在初始化的时候需要探测并设置物理页的统一大小。函数首先在 179 行检测当前架构是否支持 X86_FEATURE_PSE 功能,如果支持,那么函数继续调用 debug_pagealloc_enable() 函数检测当前系统是否启用 _debug_pagealloc_enabled,如果此时没有启用,那么函数在 180 行设置 page_size_mask 的 PG_LEVEL_2M 的标志; 反之以上两个条件其中一个不满足,那么函数在 182 行将 direct_gbpages 设置为 0. 函数接着在 185 行再次检测当前架构是否支持 X86_FEATURE_PSE 功能,该功能用于描述 “32-bit Paging” 模式下是否支持映射 4MB 的物理页,如果支持该功能,那么函数调用 cr3_set_bits_and_update_boot() 函数将 CR4.PSE 置位.

CPUID.01H.EDX.PSE

CR4.PSE

函数继续在 189 行将 _PAGE_GLOBAL 标志从 __support_pte_mask 中移除,_PAGE_GLOBAL 标志用于描述 PTE 是否支持全局页。函数接着在 190 行检测当前架构是否支持 X86_FEATURE_PGE 标志,如果支持,那么函数在 191 行调用 cr4_set_bit_and_update_boot() 函数将 CR4.PGE 标志置位,并且将 __support_pte_mask 标志集合的 _PAGE_GLOBAL 置位,以此让系统建立页表时,PTE 都支持 GLOBAL PAGE 属性.

CPUID.01H.EDX.PGE

CR4.PGE

函数在 196 行将 __default_kernel_pte_mask 设置为当前 __support_pte_mask 的值,目前值对 X86_FEATURE_PSE 和 X86_FEATURE_PGE 进行筛选. 函数接着在 198 行检测当前系统是否支持 X86_FEATURE_PTI 功能,该功能用于使能内核空间独立的页表功能。如果当前系统支持该功能,那么函数将 __default_kernel_pte_mask 的 _PAGE_GLOBAL 标志去掉,也就是该功能与全局页功能互斥. 函数继续在 202 行检测两个条件,第一个条件是 direct_gbpages 是否为真,第二个条件是当前架构是否支持 1Gig 物理页功能。如果两个条件同时为真,那么当前系统支持映射到 1Gig 物理页的能力,那么函数在 204 行在 page_size_mask 中支持 PAGE_LEVEL_1G; 反之只要两个条件中有一个不满足,那么函数将 direct_gbpages 设置为 0.

init_memory_mapping() 函数用于线性映射一段物理内存区域。函数首先定义了一组 struct map_range[] 数组,该数组用于存储多个用于映射的物理区域。函数在 475 行将该数组进行初始化,接着在 476 行调用 split_mem_range() 函数尽量将物理区域拆分成更多巨型页的形式,最后将拆分的组数存储在 nr_range 变量里。函数在 478 行使用 for 循环将 mr[] 数组里的物理区域一一进行线性映射,函数通过在 479 行调用 kernel_physical_mapping_init() 函数进行实际的线性映射。映射完毕之后在 482 行调用 add_pfn_range_mapped() 函数统计映射的物理页帧数量.

split_mem_range

kernel_physical_mapping_init

split_mem_range() 函数用于在映射前将物理区域拆分成更大的物理页块。函数首先接收到一个内存区域,其范围通过参数 start 和 end 指定。函数首先在 341 行获得 end 对应的物理页帧号,并在 344 行获得 start 对应的页帧号。如果此时起始页帧等于 0,那么这块内存为物理内存的第一块内存,由于在物理内存开始的 2/4M 处经常用于存储固定长度的 MTTRs,如果将 MTTR 存储在巨型页中,那么会降低 MTTR 的访问速度,因此这段区域不建立巨型页。函数在 359 行检测当前找到的物理区域的结束地址是否小于 end_pfn, 如果小于,那么将 end_pfn 设置为物理区域的结束页帧号,以确保 end_pfn 在物理区域内。函数继续在 361 行检测物理区域的起始页帧号是否小于期望的结束页帧号,如果是,那么函数在 362 行将该物理区域存储在 struct map_range 数据结构里,并增加 nr_range 的值,以记录当前可用的 struct map_range 区域的数量。函数接着将 pfn 指向了 end_pfn. 以上的操作都是确保了物理内存的前 2/4 MiB 没有映射巨型页。函数接着处理 2/4 MiB 之后的物理区域,函数在 367 行将 start_pfn 按 PMD_SIZE 的对齐方式找到了第一块可用的页帧浩,同理查找剩下物理区域的结束页帧号。如果此时找到一块物理区域,那么函数在 375 行的判断为真。那么函数就会将该物理区域添加到 struct map_range 数据结构里,并继续将 pfn 指向当前找到的物理区域的结束页帧号。

函数最后在 403-404 行将 start_pfn 指向了上一块物理区域的结束地址,然后将 end_pfn 指向了整块物理区域的结束页帧号,函数将此时 start_pfn 和 end_pfn 对应的区域加入到 struct map_range 数据结构中。函数接着在 411-422 行将所有 struct map_range 进行合并,合并的条件就是两个物理区域相连,且 page_size_mask 标志相同。合并完成之后函数得到一个可以映射的物理区域集合。

kernel_physical_mapping_init() 函数用于为线性区物理内存进行线性映射。函数首先在 256 行判断 page_size_mask 参数中是否包含 PG_LEVEL_2M 标志,如果包含,那么该物理区域是按巨型页进行映射; 反之不包含,那么物理区域按 4K 物理页进行映射。函数接着在 268-269 行分别获得该物理区域的起始物理页帧号和终止物理页帧号。函数在 285 行将 mapping_iter 设置为 1,这里设置 1 之后,函数在为物理区域建立页表时会遍历两次,第一次遍历的时候,内核只修改页表大小的属性,然后更新 TLB,第二次遍历时才将更多的页表属性写入到页表项里,这样做的目的是第一次遍历时只修改了页表的 Size,然后高速 TLB 将对应的虚拟地址刷新, 接着第二次遍历是采用更多的页表属性,这样页表对应的物理内存才可以正确映射。函数接着在 287 行判断当前架构是否支持 X86_FEATURE_PSE, 如果不支持,那么系统不支持映射巨型物理页,因此将 use_pse 设置为 0.

函数在 291 行将 pages_2m 和 pages_4k 变量设置为 0,然后将 pfn 指向物理区域的起始物理页帧号,接着通过调用 pgd_index() 函数计算出起始页帧对应的虚拟地址的 PGD 索引,由于是线性映射,因此虚拟地址的计算就是用物理地址加上 PAGE_OFFSET 即可,函数接着在 294 行计算了起始虚拟地址对应的 PGD. 接着函数在 295 行使用 for 循环遍历物理区域涉及的 PGD。在每次遍历 PGD 的过程中,函数首先在 296 行调用 one_md_table_init() 函数获得 PGD 对应的 PMD,在 “32-bit Paging” 模式下,当前 PGD 即是 PDE,因此 PGD 也是 PMD。如果函数在 298 行检测到 pfn 大于 end_pfn, 那么越界了,所以停止此时遍历。函数在 304 行将 pmd_idx 设置为 0,并在 306 行使用 for 循环遍历当前 PGD 下的所有的 PMD。

PMD 的遍历分作两种,其中上图描述的是第一种,也就是映射巨型页的模式。函数首先在 308 行通过简单的线性关系,将物理地址加上 PAGE_OFFSET 获得对应的虚拟地址。此时 use_pse 为真,那么函数在 316 行将 prot 设置为 PAGE_KERNEL_LARGE 页表属性集合,并在 321 行将 init_prot 设置为 “PTE_IDENT_ATTR | _PAGE_PSE” 的属性集合。函数在 325 行将物理页帧按 PMD_MASK 进行对齐,并在 326 行计算出下一个 PMD 对于的虚拟地址,并存储在 addr2 里。函数在 329-330 行调用 is_kernel_text() 函数判断当前物理区域是否映射了内核的代码段,如果映射则将 prot 设置为 PAGE_KERNEL_LARGE_EXEC 标志。函数将 page_2m 统计的数量加一,接着函数在 334 行判断到 mapping_iter 为 1,那么此时函数进行第一次遍历,那么函数在 335 行调用 set_pmd() 函数为 PMD 设置 init_prot 的属性集合,该属性集合这种保持了 head.S 中设置的一致,只是新增了 _PAGE_PSE 标志; 反之如果此时是第二次遍历,那么函数在 337 行调用 set_pmd() 函数为 PMD 设置为 prot 属性集合,该属性集合中包含了正常页表所需的属性。最后函数在 339 行将 pfn 指向了一下个 PMD 的起始物理页帧号就跳转到下一次循环.

如果函数检测到物理区域只能映射 4K 的物理页,那么函数在每次 PMD 循环过程中,函数在 308 行获得 pfn 对应的虚拟地址之后,函数跳转到 342 行调用 one_page_table_init() 函数为 pfn 建立 “Page Table”,”Page Table” 建立完毕之后返回 “Page Table” 的第一个 PTE。函数在 344 行调用 pte_index() 获得虚拟地址在 “Page Table” 中的 PTE。接着函数在 346 行调用 for 循环遍历涉及的所有 PTE。在每次遍历过程中,函数首先在 348 行将 prot 设置为 PAGE_KERNEL, 并在 353 行将 init_prot 设置为 PTE_IDENT_ATTR 属性集合。函数在 355 行调用 is_kernel_text() 判断虚拟地址是否存储了内核的代码端,如果是则将 prot 设置为 PAGE_KERNEL_EXEC. 函数在 358 行将 pages_4k 统计值加一。函数接着在 359 行判断是否为第一次遍历,如果是,函数则在 360 行调用 set_pte() 函数将 init_prot 属性集合存储到 PTE 中,该属性集合只包含 R/W 和 P 属性。如果此时是第二次遍历,那么函数调用 set_pte() 函数将 prot 属性集合填充到 PTE 中,此时页表是一个正常的集合。

函数在 367 行检测到第一次遍历之后,函数在 372-373 行调用 update_page_count() 函数分别更新了 PG_LEVEL_2M 和 PG_LEVEL_4K 的数量,接着在 379 行调用 __flush_tlb_all() 函数刷新全部的 TLB 内容,最后函数将 mapping_iter 设置为 2,并跳转到 repeat 出进行第二次循环.

one_md_table_init

one_page_table_init

one_pmd_table_init() 函数用于初始化一个 PGD/PDE 内容。在 “32-bit Paging” 模式下,分页只采用两级页表,因此此时 PGD 即使 PDE,也就是 PMD。因此函数调用 pmd_offset() 函数就可以返回 “Page Directory Table” 的地址.

one_page_table_init() 函数用于分配并初始化 “Page Table”. 在 “32-bit Paging” 模式下,函数首先判断 PDE 的值是否为空,如果为空,那么函数在 98 行分配一个物理页,然后在 101 行调用 set_pmd() 函数将物理页帧号和 _PAGE_TABLE 属性集合填充到 PDE 里,接着函数检测 “Page Table” 的地址是否为第一个 PDE 的地址,如果不是,那么系统报错。函数最后在 105 行调用 pte_offset_kernel() 函数获得 “Page Table” 的地址.

memory_map_bottom_up() 函数用于将 MEMBLOCK 分配器中指定的物理区域进行线性映射,此时 MEMBLOCK 分配器是从低地址向高地址分配内存,因此大部分已经分配的内存位于物理内存的底部。函数在 619-620 行获得里物理区域的起始物理地址,并将其设置为 min_pfn_mapped. 函数在 628 行使用 while 循环遍历该物理内存区域,函数在 629 行判断该物理内存区域是否可以映射多个巨型页,如果可以那么函数将 next 指向下一个巨型页对应的物理地址上; 反之则将 next 指向当前物理内存区域的结束地址。函数接着在 637 行调用 init_range_memory_mapping() 函数将 start 到 next 的物理区域进行线性映射,映射完毕之后,mapped_ram_size 统计了映射的长度,函数接着将 start 指向了 next。如果函数在 640 行检测到当前映射的巨型页长度与 step_size 的不同,那么函数调用 get_new_step_size() 函数获得最新的巨型页长度,并更新到 step_size 里。函数继续循环将剩余的物理区域进行映射。

kernel_physical_mapping_init

early_ioremap_page_table_range_init() 函数用于为早期的 IO 空间建立页表。函数首先在 540 行将 pgd_base 指向内核的 PGD 页表 swapper_pg_dir, 然后函数在 547 行调用 __fix_to_virt() 函数计算出 FIXMAP 分配器管理的虚拟区域的起始虚拟地址,将其存储在 vaddr 里。函数接着在 546 行计算出 FIXMAP 分配器管理的虚拟区域的结束虚拟地址。 接着函数在 549 行调用 page_table_range_init() 函数为 FIXMAP 分配器维护的虚拟区域建立相应的页表. 最后函数调用 early_iormap_reset() 函数调整了 IO 空间映射状态。

page_table_range_init

page_table_range_init() 函数用于为一段虚拟内存创建页表,在 “32-bit Paging” 模式下只需建立两级页表 “Page Directory Table” 和 “Page Table”. 函数首先在 214 行调用 page_table_range_init_count() 函数计算虚拟区域是否包 PGD 的数量. 如果 count 不为空,那么函数调用 alloc_low_pages() 为其分配物理页。函数接着在 220-223 行获得寻地址对应的 PGD,并在 225 行使用 for 循环遍历涉及的 PGD。在每次循环中,函数首先在 226 行调用 one_md_table_init() 函数创建并初始话 PMD,此处为 PDE。函数接着使用 for 循环遍历所有涉及的 PMD,在每次遍历过程中函数调用函数 page_table_kmap_check() 函数检测对应的 “Page Table” 是否存在,如果不存在,那么函数调用 one_page_table_init() 函数创建一个 “Page Table”.

one_md_table_init

native_pagetable_init() 函数的作用是为非线性区的虚拟地址建立页表。非线性区包括了 VMALLOC、FIXMAP、PKMAP 以及 KMAP 映射区。函数在 488 行使用 for 循环从线性区末尾开始,一直到虚拟空间的末尾。函数在每次循环的时候,在 489 行通过将物理地址加上 PAGE_OFFSET 获得对应的寻地址,接着基于该虚拟地址进行页表遍历,当遍历到 PMD,在 “32-bit Paging” 模式下也就是 PDE,那么函数在 501 行调用 pmd_large() 函数确认 PDE 是否直接映射物理页了,如果是则报错; 反之函数继续通过 pte_offset_kernel() 函数获得对应的 PTE,如果此时 PTE 不存在,那么没必要清除,直接返回; 如果 PTE 存在,那么函数调用 pte_clear() 函数将 PTE 清除。函数清除完 VMALLOC 映射区之后,在 516 行调用 paging_init() 函数为 PKMAP 和 KMAP 映射区建立页表。

paging_init() 函数的作用是为非线性区建立页表。函数首先在 725 行调用 pagetable_init() 函数为 PKMAP 映射区建立页表,建立完毕之后在 727 行调用 __flush_tlb_all() 函数刷新所有的 TLB。函数接着在 729 行调用 kmap_init() 函数为 KAMP 映射区建立页表,接下里的函数就是 SPARSE Memory Model 相关的初始化.

pagetable_init

kmap_init

pagetable_init() 函数用于为非线性区建立页表。函数在 555 行获得内核的全局页目录 swapper_pg_dir, 然后函数在 557 行调用 permanent_kmap_init() 函数为 PKMAP 映射区建立页表。

permanent_kmaps_init() 函数用于为 PKMAP 分配器维护的虚拟区域建立页表。在 “32-bit Paging” 模式下, 分页机制只存在两级页表 “Page Directory Table” 和 “Page Table”, 因此只需为 PKMAP 映射区建立两级页表,函数在 422 行将 vaddr 指向 PKMAP_BASE, 该地址是 PKMAP 映射区的基地址。接着函数在 423 行调用 page_table_range_init() 函数为虚拟地址建立页表,建立完毕之后通过一些列的查表获得 PKMAP 映射区起始地址对应的 PTE,并使用 pkmap_page_table 指向该 PTE.

page_table_range_init

kmap_init() 函数用于为 KMAP 映射区建立页表。函数首先在 408 行通过 __fix_to_virt() 函数获得 KMAP 映射区的起始虚拟地址,然后通过调用 kmap_get_fixmap_pte() 函数获得虚拟地址对应的 PTE,并使用 kmap_pte 指向该 PTE. 在 kmap_get_fixmap_pte() 函数中,函数通过一系列的查表找到虚拟地址对应的 PTE.


32-Bit Paging 模式下缺页调试

当在用户空间访问一个未建立页表的虚拟地址时,系统会自动触发缺页中断,系统自动执行缺页处理流程。在缺页处理流程中,内核会为未建立页表的虚拟地址分配物理内存,然后建立页表,以使该虚拟地址映射到物理内存上。当页表建立完毕之后,系统从缺页中断中返回,又将执行权归还给用户空间程序,应用程序再次访问该地址。以上过程即是一次完整的缺页流程。很多开发者只在书中或源代码级别看过缺页的执行过程,当没能对该过程进行实践。本节介绍了一种缺页实践的办法,以便大家在缺页学习过程中能有更好的帮助。

本节基于实践章节的 “Translation Userspace Address With 4K Page” 案例进行讲解,如果没有对该案例进行实践的开发者可以先通过下面的链接进行实践,实践过的开发者可以跳过该链接:

Translation Userspace Address With 4K Page 实践部署

本文实践缺页的流程是在内核中新建一个系统调用,然后在用户空间发生缺页的前通过新系统调用打开指定的开关,当缺页发生完毕之后,再在用户空间通过新系统调用关闭指定开关,该开关用于控制调制信息是否输出。通过上面的办法可以在缺页涉及的函数使用指定的函数进行信息输出,这将会很干净的进行信息输出,例如使用情况如下:

为了进行缺页调试,开发者首先需要在当前内核中新建一个系统调用,开发者请参考如下文档进行新系统调用的创建:

新建一个系统调用

新系统调用创建完毕之后,基于案例中的代码,开发者在用户空间发生缺页的地方加入如上代码, 在 23 行处定义一个宏,宏用于指明新系统调用的系统调用号,接着在 51 和 54 行分别加入图中的代码,以此对新新系统进行调用:

接着开发者可以在内核缺页流程的函数中类似图中的用法使用 bs_debug() 添加打印信息, 其运行效果如下 (修改代码之后记得重新编译内核):

从运行的效果来看,代码打印的很干净清爽,因此这个方法推荐开发者使用。开发者也可以感受一下,如果直接在缺页的代码中使用 printk 进行打印的后果,如下图:

运行效果如下 (感受到头疼了吗).

进入系统之后已经被缺页流程的信息打爆了,因此推荐开发者使用上面的方法进行调试。


附录

BiscuitOS Home

BiscuitOS Blog 2.0

Linux Kernel

Bootlin: Elixir Cross Referencer

捐赠一下吧 🙂

MMU