本文采用 Linux 内核 v3.10 版本 x86_64架构
为了降低代码复杂性,本文假定内核未开启半虚拟化配置 CONFIG_PARAVIRT
一、 x86_64 内存分页理论基础
x86_64 架构支持 4 级及 5 级分页。在 Intel 手册上,对于分页结构的命名,由高到低分别是:5 级页映射表(Page Map Level 5,PML5)、4 级页映射表(Page Map Level 4,PML4)、页目录指针表(Page-Directory-Pointer Table,PDPT)、页目录(Page Directory,PD)以及页表(Page Table,PT)。对应的页结构项(Paging Structure Entry),分别为:PML5E( PML5 Entry)、PML4E( PML4 Entry)、PDPTE(Page-Directory-Pointer Table Entry)、PDE(Page Directory Entry)、PTE(Page Table Entry)。在下文中,我们把页结构项,简称为表项。
4 级分页及 5 级分页下,可以把线性地址映射到 4KB 、2MB 或者 1GB 大小的页。
1.1 4 级分页的地址转换
在 4 级分页下,页大小为 4KB 时,线性地址的组成及地址转换如下图所示:
在 4 级分页下,页大小为 2MB 时,线性地址的组成及地址转换如下图所示:
之所以要提到 2MB 的页,是因为在构建早期页表时,使用了 2MB 的页。在本文随后的小节中,会看到内核是如何构建页表的。
1.2 页结构项/表项
每级页表由多个表项组成,页表可以看做一个数组,而表项则是数组中的元素。各级页表的表项格式比较类似,但并不完全相同。
在 4 级分页下,各级表项的名称如下:
- 4 级页表项(PML4E)
- 页目录指针表项(PDPTE,3 级页表项)
- 页目录项( PDE ,2 级页表项)
- 页表项(PTE,1 级页表项)
其中,页目录指针表项(3 级页表项)和 页目录项(2 级页表项)可以直接映射到页,也可以引用下级页表。根据不同的映射情况,它们各自又有 2 种不同的格式。但页目录指针表项(3 级页表项)直接映射到页时,页的大小为 1GB,这种大页很少使用,而且不是所有处理器都支持,所以我们不做介绍。
下面我们列出常用的表项格式。
1.2.1 4 级页表项
4 级页表项格式如下:
1.2.2 页目录指针表项(引用页目录)
页目录指针表项(引用页目录),其格式如下:
1.2.3 页目录项(映射到 2MB 页)
映射到 2MB 页面的页目录项,其格式如下:
1.2.4 页目录项(引用页表)
页目录项(引用页表),其格式如下:
1.2.5 页表项
页表项格式如下:
1.3 页标志/页属性
在各级表项中,除了包含下级页表的物理地址之外,还有各种标志位,我们称之为页标志或者页属性。直接映射到页的表项和引用了其它页表的表项,其标志位是不同的。
当表项直接映射到页时,它启用了 Dirty 标志位(脏位,位 6)、Global 标志位(全局标志位,位 8)以及 PAT 标志位( Page Attribute Table,位 7 或 位12);否则,这 3 个标志位被保留不使用。
另外,当表项直接映射到页时,PAT 标志位在表项的位置也不相同。当表项映射到 4KB 的页时(1 级页表项),PAT 标志位在第 7 位,该表项没有 PS 位;当表项映射到 2MB (2 级页表项)或 1 GB (3 级页表项)的页时,PAT 标志位在第 12 位。
各标志位说明如下:
-
P 位:位 0,存在(Present)位。指示表项映射的页或页表是否存在于内存中。当该位为 1 时,说明存在;否则,说明不存在。在地址转换过程中,遇到 P 位为 0 的表项会触发 Page-Fault 异常。
-
R/W 位:位 1,读写(Read/Write)位。当该位为 0 时,表项所控制的内存区域不允许写入。
-
U/S 位:位 2,用户/管理模式(User/Supervisor)位。为 0 时,表示管理模式;否则,表示用户模式。该位控制着访问权限,当该位为 0 时,不允许从用户态访问表项控制的内存区域。
-
PWT 位:位 3,页级直写( Page-level Write-Through)位。该位间接控制内存的缓存类型。
-
PCD 位:位 4,缓存禁止(Page-level Cache Disable)位。该位间接控制内存的缓存类型。
-
A 位:位 5,访问(Accessed)位。指示在线性地址转换过程中是否使用了该表项。
-
D 位:位 6,脏(Dirty)位。指示表项控制的内存区域是否写入了数据。
-
PS 位:位 7,页大小(Page Size)位。当表项直接映射到页时,为 1;当表项引用了其它页表时,为 0。页表项(PTE)没有 PS 位。
-
G 位:位 8,全局(Global)位。指示页面的 TLB 是否是全局的。
-
R 位:位 9,重启(Restart)位。普通分页忽略该标志位,只对 HALT 分页有效。
-
PAT 位:位 7 或位 12,页属性表(Page Attribute Table)位。该位间接控制内存的缓存类型。
-
XD 位:位 63,禁止执行(eXecute-Disable)位。如果为 1,该表项控制的内存区域不允许指令查询。
更多 x86_64 内存分页原理相关内容,请参考 x86-64架构:内存分页机制。
二、 虚拟内存布局
x86_64 架构下,虚拟内存中属于内核空间的各内存区域,其起始地址、空间大小、用途都是预先设计好的。4 级分页下,内存布局如下所示:
// file: Documentation/x86/x86_64/mm.txt
Virtual memory map with 4 level page tables:
0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm
hole caused by [48:63] sign extension
ffff800000000000 - ffff80ffffffffff (=40 bits) guard hole
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory
ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole
ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space
ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole
ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB)
... unused hole ...
ffffffff80000000 - ffffffffa0000000 (=512 MB) kernel text mapping, from phys 0
ffffffffa0000000 - ffffffffff5fffff (=1525 MB) module mapping space
ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls
ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole
其中,地址 0x0000 7FFF FFFF FFFF - 0x0000 7FFF FFFF FFFF 共128T(47位),属于用户空间;地址 0xFFFF 8000 0000 0000 - **0xFFFF FFFF FFFF FFFF **共128T(47位), 属于内核空间。
示意图如下:
这里我们重点关注下物理内存直接映射区和内核代码映射区。
物理内存直接映射区,虚拟地址区间为 0xFFFF 8800 0000 0000 - 0xFFFF E900 0000 0000, 共 64T 大小。Linux 内核会把所有的物理内存映射到该虚拟地址区间。内核定义了宏 __PAGE_OFFSET 以及 PAGE_OFFSET,用来表示该区间的起始地址:
// file: arch/x86/include/asm/page_64_types.h
#define __PAGE_OFFSET _AC(0xffff880000000000, UL)
// file: arch/x86/include/asm/page_types.h
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
该区间内的地址减去 __PAGE_OFFSET,就可以得到对应的物理地址。
内核代码映射区,虚拟地址区间为 0xFFFF FFFF 8000 0000 - 0xFFFF FFFF A000 0000,共 512M 大小。该区域用于映射内核代码段、数据段、bss 段等内容。内核定义了宏 __START_KERNEL_map 来表示该区间的起始地址:
// file: arch/x86/include/asm/page_64_types.h
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
同理,该区域内的地址减去 __START_KERNEL_map后,就能得到对应的物理地址。
三、 物理/虚拟 地址转换
3.1 宏 __pa
宏__pa 用来把虚拟地址转换为物理地址,该宏定义在头文件 arch/x86/include/asm/page.h 中:
// file: arch/x86/include/asm/page.h
#define __pa(x) __phys_addr((unsigned long)(x))
__pa(x) 扩展为内联函数 __phys_addr_nodebug 。
// file: arch/x86/include/asm/page_64.h
#define __phys_addr(x) __phys_addr_nodebug(x)
static inline unsigned long __phys_addr_nodebug(unsigned long x)
{
unsigned long y = x - __START_KERNEL_map;
/* use the carry flag to determine if x was < __START_KERNEL_map */
x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));
return x;
}
宏 __START_KERNEL_map ( 0xffffffff80000000)是虚拟地址中内核代码映射区的起始地址; __PAGE_OFFSET (0xffff880000000000)是物理内存直接映射区的起始地址。
y = x - __START_KERNEL_map计算待转换虚拟地址和内核代码映射区起始地址的差值:
- 当 x > y 时,说明待转换虚拟地址处于内核代码映射区,返回值 x = y + phys_base。根据本文第八节可知,
phys_base是内核映射区的物理基地址,y + phys_base就是 x 的物理地址。 - 当 x < y 时,说明待转换虚拟地址处于物理内存直接映射区,返回值
x = y + __START_KERNEL_map - PAGE_OFFSET。y + __START_KERNEL_map会回绕到 x,所以最终x = x - PAGE_OFFSET,得到 x 的物理地址。
3.2 宏 __va
宏__va 将物理地址转虚拟地址,定义如下:
// file: arch/x86/include/asm/page.h
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
可以看到,将物理地址直接加上 PAGE_OFFSET,得到虚拟地址。
四、 页表相关的数据结构及 APIs
在 Linux 内核中,各级页表有了不同名称。 4 级分页下,它们的名称分别为全局页目录(Page Global Directory,PGD)、上层页目录(Page Upper Directory,PUD)、中层页目录(Page Middle Directory,PMD)、页表(Page Table,PT)。针对各级页表及表项,内核定义了一些数据结构及接口。
4.1 常量
4.1.1 页表容量及表项偏移
我们从分页理论可以得知,在 4 级分页下,虚拟地址实际使用长度为 48 位。当页大小为 4KB 时,其低 12 位为页内偏移,用来定位页内地址;第 21 ~ 29 位(共 9 位)为页表项索引,用来定位页表项在页表中的位置;同理,第 30 ~ 38位(共 9 位)表示中层页目录项索引;第 31 ~ 39位(共 9 位)表示上层页目录项索引;第 40 ~ 47 位(共 9 位)表示全局页目录索引。
对于线性地址中各级表项的偏移位数及页表中的表项数量,内核定义了如下宏:
// file: arch/x86/include/asm/pgtable_64_types.h
#define PAGETABLE_LEVELS 4 // 页表级数
/*
* PGDIR_SHIFT determines what a top-level page table entry can map
*/
#define PGDIR_SHIFT 39 // 全局页目录偏移位数
#define PTRS_PER_PGD 512 // 每个全局页目录中包含的表项数
/*
* 3rd level page
*/
#define PUD_SHIFT 30 // 上层页目录偏移位数
#define PTRS_PER_PUD 512 // 每个上层页目录中包含的表项数量
/*
* PMD_SHIFT determines the size of the area a middle-level
* page table can map
*/
#define PMD_SHIFT 21 // 中层页目录偏移位数
#define PTRS_PER_PMD 512 // 每个中层页目录中包含的表项数量
/*
* entries per page directory level
*/
#define PTRS_PER_PTE 512 // 每个页表中包含的表项数量
// arch/x86/include/asm/page_types.h
#define PAGE_SHIFT 12 // 页表偏移位数
宏 PAGETABLE_LEVELS 定义了页表的级数,可以看到,该值为 4,说明内核使用的是 4 级页表。
表项偏移与线性地址的关系如下图所示(4KB 页):
另外,虚拟地址中各级表项索引的位数为 9 位,所以每级页表包含 512() 个表项。
4.1.2 表项掩码及控制区域
每个页表项控制一个页(4KB)的内存大小;每个中层页目录项控制着 512() 个页表项,共计 2MB 的内存区域;每个上层页目录项控制着 512() 个中层页目录项,共计 1GB 的内存区域;每个全局页目录项控制着 512() 个上层页目录项,共计 512GB 的内存区域;全局页目录有 512() 个表项,可控制 256TB 的内存区域。
根据各级表项的偏移位数,可以计算出表项控制的区域大小:
// file: arch/x86/include/asm/page_types.h
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))
// file: arch/x86/include/asm/pgtable_64_types.h
#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT) // 扩展为 2MB,PMD_SHIFT 扩展为 21
#define PMD_MASK (~(PMD_SIZE - 1))
#define PUD_SIZE (_AC(1, UL) << PUD_SHIFT) // 扩展为 1GB,PUD_SHIFT 扩展为 30
#define PUD_MASK (~(PUD_SIZE - 1))
#define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT) // 扩展为 512GB,PGDIR_SHIFT 扩展为 39
#define PGDIR_MASK (~(PGDIR_SIZE - 1))
其中,PMD_SIZE 扩展为 2MB;PUD_SIZE 扩展为 1GB;PGDIR_SIZE 扩展为 512GB。
另外,将各级表项控制区域大小减一取反后,得到表项控制区域的掩码。
4.1.3 页标志位
对于各标志位在表项中的位置,内核定义了如下常量:
// file: arch/x86/include/asm/pgtable_types.h
#define _PAGE_BIT_PRESENT 0 /* is present */
#define _PAGE_BIT_RW 1 /* writeable */
#define _PAGE_BIT_USER 2 /* userspace addressable */
#define _PAGE_BIT_PWT 3 /* page write through */
#define _PAGE_BIT_PCD 4 /* page cache disabled */
#define _PAGE_BIT_ACCESSED 5 /* was accessed (raised by CPU) */
#define _PAGE_BIT_DIRTY 6 /* was written to (raised by CPU) */
#define _PAGE_BIT_PSE 7 /* 4 MB (or 2MB) page */
#define _PAGE_BIT_PAT 7 /* on 4KB pages */
#define _PAGE_BIT_GLOBAL 8 /* Global TLB entry PPro+ */
#define _PAGE_BIT_UNUSED1 9 /* available for programmer */
#define _PAGE_BIT_IOMAP 10 /* flag used to indicate IO mapping */
#define _PAGE_BIT_HIDDEN 11 /* hidden by kmemcheck */
#define _PAGE_BIT_PAT_LARGE 12 /* On 2MB or 1GB pages */
#define _PAGE_BIT_SPECIAL _PAGE_BIT_UNUSED1
#define _PAGE_BIT_CPA_TEST _PAGE_BIT_UNUSED1
#define _PAGE_BIT_SPLITTING _PAGE_BIT_UNUSED1 /* only valid on a PSE pmd */
#define _PAGE_BIT_NX 63 /* No execute: only valid after cpuid check */
其中,位 9 ~ 11,在普通分页中是不支持的。
另外,PS 位和 PAT 位都使用了位 7;位 7 和 位 12 都可能是 PAT 位。具体可参考 1.3 节。
有了位置之后,内核又定义了一系列常量,将标志位放到对应的位置上,用来组合位图:
// file: arch/x86/include/asm/pgtable_types.h
#define _PAGE_PRESENT (_AT(pteval_t, 1) << _PAGE_BIT_PRESENT)
#define _PAGE_RW (_AT(pteval_t, 1) << _PAGE_BIT_RW)
#define _PAGE_USER (_AT(pteval_t, 1) << _PAGE_BIT_USER)
#define _PAGE_PWT (_AT(pteval_t, 1) << _PAGE_BIT_PWT)
#define _PAGE_PCD (_AT(pteval_t, 1) << _PAGE_BIT_PCD)
#define _PAGE_ACCESSED (_AT(pteval_t, 1) << _PAGE_BIT_ACCESSED)
#define _PAGE_DIRTY (_AT(pteval_t, 1) << _PAGE_BIT_DIRTY)
#define _PAGE_PSE (_AT(pteval_t, 1) << _PAGE_BIT_PSE)
#define _PAGE_GLOBAL (_AT(pteval_t, 1) << _PAGE_BIT_GLOBAL)
#define _PAGE_UNUSED1 (_AT(pteval_t, 1) << _PAGE_BIT_UNUSED1)
#define _PAGE_IOMAP (_AT(pteval_t, 1) << _PAGE_BIT_IOMAP)
#define _PAGE_PAT (_AT(pteval_t, 1) << _PAGE_BIT_PAT)
#define _PAGE_PAT_LARGE (_AT(pteval_t, 1) << _PAGE_BIT_PAT_LARGE)
#define _PAGE_SPECIAL (_AT(pteval_t, 1) << _PAGE_BIT_SPECIAL)
#define _PAGE_CPA_TEST (_AT(pteval_t, 1) << _PAGE_BIT_CPA_TEST)
#define _PAGE_SPLITTING (_AT(pteval_t, 1) << _PAGE_BIT_SPLITTING)
#define _PAGE_NX (_AT(pteval_t, 1) << _PAGE_BIT_NX)
// file: include/uapi/linux/const.h
#define _AT(T,X) ((T)(X))
最后,将各标志位组合成不同的位图。组合比较多,我们展示几个常用的:
// file: arch/x86/include/asm/pgtable_types.h
#define _PAGE_TABLE (_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | \
_PAGE_ACCESSED | _PAGE_DIRTY)
#define _KERNPG_TABLE (_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED | \
_PAGE_DIRTY)
#define __PAGE_KERNEL_EXEC \
(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_GLOBAL)
#define __PAGE_KERNEL (__PAGE_KERNEL_EXEC | _PAGE_NX)
#define __PAGE_KERNEL_LARGE (__PAGE_KERNEL | _PAGE_PSE)
#define __PAGE_KERNEL_LARGE_NOCACHE (__PAGE_KERNEL | _PAGE_CACHE_UC | _PAGE_PSE)
#define __PAGE_KERNEL_LARGE_EXEC (__PAGE_KERNEL_EXEC | _PAGE_PSE)
#define __PAGE_KERNEL_IDENT_LARGE_EXEC __PAGE_KERNEL_LARGE_EXEC
4.1.4 页帧掩码 -- PTE_PFN_MASK
各级表项主要由下级页表(或页)的物理地址和标志位组成。由于页表的物理地址对齐到页基地址(4KB),所以表项中的低 12 位,在地址转换中并没有用到,被用来存储标志位。另外,根据 Intel 文档描述,其处理器最大支持 52 位的物理地址,但并不是每种处理器都能达到该极限值。我在自己的虚拟机上查看到,其处理器支持 39 位的物理地址:
$ cpuid|grep "maximum physical address"
maximum physical address bits = 0x27 (39)
内核中使用宏 __PHYSICAL_MASK_SHIFT 定义了其支持的最大物理地址位数,可以看到该值为 46。使用宏 __PHYSICAL_MASK 定义了最大物理地址掩码,该宏会屏蔽掉第 46 ~ 63 位。同时,内核还定义了页大小的掩码 PAGE_MASK,该掩码会屏蔽掉低 12 位。通过这 2 个掩码的组合,就可以从表项中提取出物理页的页帧号,内核定义了宏PTE_PFN_MASK来完成此功能。
宏PTE_PFN_MASK最终扩展为 0x00003ffffffff000,会屏蔽掉(pte|pmd|pud|pgd)val_t类型的低 12 位(标志位)以及高位的保留位和状态位,得到物理基地址,即页帧。
// file: arch/x86/include/asm/pgtable_types.h
/* PTE_PFN_MASK extracts the PFN from a (pte|pmd|pud|pgd)val_t */
#define PTE_PFN_MASK ((pteval_t)PHYSICAL_PAGE_MASK) // PTE_PFN_MASK 扩展为 0x00003ffffffff000
// file: arch/x86/include/asm/page_types.h
/* Cast PAGE_MASK to a signed type so that it is sign-extended if
virtual addresses are 32-bits but physical addresses are larger
(ie, 32-bit PAE). */
#define PHYSICAL_PAGE_MASK (((signed long)PAGE_MASK) & __PHYSICAL_MASK) // PHYSICAL_PAGE_MASK 扩展为 0x00003ffffffff000
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT) // PAGE_SIZE 扩展为 4096
#define PAGE_MASK (~(PAGE_SIZE-1)) // PAGE_MASK 扩展为 0xfffffffffffff000
#define __PHYSICAL_MASK ((phys_addr_t)((1ULL << __PHYSICAL_MASK_SHIFT) - 1)) // __PHYSICAL_MASK 扩展为 0x00003fffffffffff
// file: arch/x86/include/asm/page_64_types.h
#define __PHYSICAL_MASK_SHIFT 46 // linux 支持的最大物理地址位数
PTE_PFN_MASK 如下图所示:
4.1.5 页属性掩码 -- PTE_FLAGS_MASK
通过对页帧掩码按位取反,就可以得到页属性掩码。
内核使用宏 PTE_FLAGS_MASK来表示页属性掩码,该宏能够提取 (pte|pmd|pud|pgd)val_t类型的页标志。
/* PTE_FLAGS_MASK extracts the flags from a (pte|pmd|pud|pgd)val_t */
#define PTE_FLAGS_MASK (~PTE_PFN_MASK)
PTE_FLAGS_MASK 如下图所示:
4.2 数据结构
4.2.1 表项和页属性的包装结构
各级表项本质上都是 64 位的数据,可以使用基本类型 unsigned long 来表示。但内核使用了结构体类型,对各级表项进行了包装。不定义成基本类型的原因在于这样可以让 gcc 在编译时加以更严格的类型检查。
内核定义了结构体 pgd_t 来表示全局页目录项;结构体 pud_t 表示上层页目录项;结构体pmd_t 表示中层页目录项;结构体pte_t 表示页表项。
除了各级表项外,页属性本质上也是一个 64 位的数据,可以使用基本类型来表示。类似的,内核定义了结构体 pgprot_t 来对页属性进行包装。
// file: arch/x86/include/asm/pgtable_64_types.h
typedef struct { pteval_t pte; } pte_t;
// file: arch/x86/include/asm/pgtable_types.h
typedef struct { pgdval_t pgd; } pgd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pmdval_t pmd; } pmd_t;
typedef struct pgprot { pgprotval_t pgprot; } pgprot_t;
这些结构体内部又引用了其它类型,这些类型都是 unsigned long 的别名。
// file: arch/x86/include/asm/pgtable_64_types.h
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;
4.2.2 struct mm_struct 与 init_mm
每个进程都有自己的内存空间,内核使用结构体 task_struct 来描述进程,使用结构体 mm_struct 来描述进程地址空间。mm_struct 与 task_struct 的关系如下所示:
// file: include/linux/sched.h
struct task_struct {
...
struct mm_struct *mm, *active_mm;
...
}
结构体 mm_struct 包含与地址空间有关的许多不同字段,其中的一个字段 pgd 指向进程使用的全局页目录。
// file: include/linux/mm_types.h
struct mm_struct {
...
pgd_t * pgd;
...
}
对于内核来讲,也定义了一个 mm_struct 类型的变量 init_mm,其 pgd 字段指向 swapper_pg_dir。
// file: mm/init-mm.c
struct mm_struct init_mm = {
...
.pgd = swapper_pg_dir,
...
};
swapper_pg_dir 其实就是全局页目录 init_level4_pgt:
// file: arch/x86/include/asm/pgtable_64.h
#define swapper_pg_dir init_level4_pgt
init_mm 是作为 init_task 的一部分存在的:
// file: include/linux/init_task.h
#define INIT_TASK(tsk) \
{
...
...
...
.mm = NULL, \
.active_mm = &init_mm, \
...
}
// file: init/init_task.c
/* Initial task structure */
struct task_struct init_task = INIT_TASK(init_task);
4.3 APIs
说完了表项相关的常量和数据结构之后,我们来看看相关的接口。
4.3.1 从虚拟地址提取各级表项的索引值 -- pgd_index、pud_index、pmd_index、pte_index
全局页目录可以看做是包含 PTRS_PER_PGD 个元素的数组 pgd_t[PTRS_PER_PGD]。 宏 pgd_index的功能,从虚拟地址中提取出对应的全局页目录项索引值。宏 PTRS_PER_PGD 扩展为 512,宏 PGDIR_SHIFT 扩展为 39,具体可参考 4.1 节。
// file: arch/x86/include/asm/pgtable.h
/*
* the pgd page can be thought of an array like this: pgd_t[PTRS_PER_PGD]
*
* this macro returns the index of the entry in the pgd page which would
* control the given virtual address
*/
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
类似的,宏 pud_index 返回虚拟地址对应的上层页目录项索引:
// file: arch/x86/kernel/head_64.S
#define pud_index(x) (((x) >> PUD_SHIFT) & (PTRS_PER_PUD-1))
宏 pmd_index返回虚拟地址对应的中层页目录项索引:
// file: arch/x86/include/asm/pgtable.h
/*
* the pmd page can be thought of an array like this: pmd_t[PTRS_PER_PMD]
*
* this macro returns the index of the entry in the pmd page which would
* control the given virtual address
*/
static inline unsigned long pmd_index(unsigned long address)
{
return (address >> PMD_SHIFT) & (PTRS_PER_PMD - 1);
}
宏 pte_index 返回虚拟地址对应的页表项索引:
// file: arch/x86/include/asm/pgtable.h
/*
* the pte page can be thought of an array like this: pte_t[PTRS_PER_PTE]
*
* this function returns the index of the entry in the pte page which would
* control the given virtual address
*/
static inline unsigned long pte_index(unsigned long address)
{
return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}
4.3.2 包装/解包 各级表项
上文曾说过,内核将各级表项包装成了结构体。所以,就出现了针对各级表项的包装以及解包函数。包装函数将基本类型包装成结构体类型;解包函数从结构体中提取基本类型的表项值。
native_make_pgd 函数将基本类型的值包装成结构体 pgd_t 类型;而 native_pgd_val 函数将结构体 pgd_t 解包装以获取全局页目录项的内容。
// file: arch/x86/include/asm/pgtable_types.h
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;
}
对于上层页目录项、中层页目录项、页表项,也有类似的操作。
// file: arch/x86/include/asm/pgtable_types.h
/**
* 上层页目录项相关函数
*/
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;
}
/**
* 中层页目录项相关函数
*/
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;
}
/**
* 页表项相关函数
*/
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;
}
同时,内核也为这些基本操作定义了一些宏:
// file: arch/x86/include/asm/pgtable.h
#define pgd_val(x) native_pgd_val(x)
#define __pgd(x) native_make_pgd(x)
#define pud_val(x) native_pud_val(x)
#define __pud(x) native_make_pud(x)
#define pmd_val(x) native_pmd_val(x)
#define __pmd(x) native_make_pmd(x)
#define pte_val(x) native_pte_val(x)
#define __pte(x) native_make_pte(x)
4.3.3 设置/清除 各级表项
内核定义了一些函数,来为表项赋值或者清除表项内容使其失效。
我们以全局页目录项操作为例,介绍下相关函数。
函数 native_set_pgd 接收 2 个参数:指向 pgd_t 类型的指针以及 pgd_t 类型的值。该函数实现非常简单,就是给指针指向的变量赋值,也就是将全局页目录项保存到指定的地址处。
函数 native_pgd_clear内部引用了native_set_pgd 和 native_make_pgd 函数,使用数值 0 来填充全局页目录项。填充后,全局页目录项的存在位为 0,所以变成无效表项。
// file: arch/x86/include/asm/pgtable_64.h
static inline void native_set_pgd(pgd_t *pgdp, pgd_t pgd)
{
*pgdp = pgd;
}
static inline void native_pgd_clear(pgd_t *pgd)
{
native_set_pgd(pgd, native_make_pgd(0));
}
类似的,内核提供了 native_set_pud 和 native_pud_clear 函数,用于操作上层页目录项:
// file: arch/x86/include/asm/pgtable_64.h
static inline void native_set_pud(pud_t *pudp, pud_t pud)
{
*pudp = pud;
}
static inline void native_pud_clear(pud_t *pud)
{
native_set_pud(pud, native_make_pud(0));
}
内核提供了 native_set_pmd 、native_set_pmd_at 和 native_pmd_clear 函数,用于操作中层页目录项:
// file: arch/x86/include/asm/pgtable_64.h
static inline void native_set_pmd(pmd_t *pmdp, pmd_t pmd)
{
*pmdp = pmd;
}
static inline void native_pmd_clear(pmd_t *pmd)
{
native_set_pmd(pmd, native_make_pmd(0));
}
// file: arch/x86/include/asm/pgtable.h
static inline void native_set_pmd_at(struct mm_struct *mm, unsigned long addr,
pmd_t *pmdp , pmd_t pmd)
{
native_set_pmd(pmdp, pmd);
}
内核提供了 native_set_pte 、native_set_pte_atomic 、native_set_pmd_at 和 native_pte_clear 函数,用于操作页表项:
// file: arch/x86/include/asm/pgtable_64.h
static inline void native_pte_clear(struct mm_struct *mm, unsigned long addr,
pte_t *ptep)
{
*ptep = native_make_pte(0);
}
static inline void native_set_pte(pte_t *ptep, pte_t pte)
{
*ptep = pte;
}
static inline void native_set_pte_atomic(pte_t *ptep, pte_t pte)
{
native_set_pte(ptep, pte);
}
// file: arch/x86/include/asm/pgtable.h
static inline void native_set_pte_at(struct mm_struct *mm, unsigned long addr,
pte_t *ptep , pte_t pte)
{
native_set_pte(ptep, pte);
}
同时,内核为这些函数定义了宏:
// file: arch/x86/include/asm/pgtable.h
#define set_pgd(pgdp, pgd) native_set_pgd(pgdp, pgd)
#define pgd_clear(pgd) native_pgd_clear(pgd)
# define set_pud(pudp, pud) native_set_pud(pudp, pud)
#define pud_clear(pud) native_pud_clear(pud)
#define set_pmd(pmdp, pmd) native_set_pmd(pmdp, pmd)
#define pmd_clear(pmd) native_pmd_clear(pmd)
#define set_pte(ptep, pte) native_set_pte(ptep, pte)
#define pte_clear(mm, addr, ptep) native_pte_clear(mm, addr, ptep)
#define set_pte_at(mm, addr, ptep, pte) native_set_pte_at(mm, addr, ptep, pte)
#define set_pmd_at(mm, addr, pmdp, pmd) native_set_pmd_at(mm, addr, pmdp, pmd)
#define set_pte_atomic(ptep, pte) \
native_set_pte_atomic(ptep, pte)
原生操作表示最底层的操作,而这些宏表示的接口根据内核配置选项的不同,有着不同的行为。比如当内核选项 CONFIG_PARAVIRT=yes时,set_pud 被定义成如下函数:
// file: arch/x86/include/asm/paravirt.h
static inline void set_pud(pud_t *pudp, pud_t pud)
{
pudval_t val = native_pud_val(pud);
if (sizeof(pudval_t) > sizeof(long))
PVOP_VCALL3(pv_mmu_ops.set_pud, pudp,
val, (u64)val >> 32);
else
PVOP_VCALL2(pv_mmu_ops.set_pud, pudp,
val);
}
但是其底层仍然调用了 native_set_pud 函数,只不过是增加了一些其它功能。
// file: arch/x86/kernel/paravirt.c
struct pv_mmu_ops pv_mmu_ops = {
...
.set_pud = native_set_pud,
...
}
为了降低代码复杂性,我们默认未开启半虚拟化配置。
4.3.4 包装/解包 页属性
宏 __pgprot将给定值包装成 pgprot_t 结构体。宏 pgprot_val从将结构体解包装以获取实际的属性值;
// file: arch/x86/include/asm/pgtable_types.h
#define pgprot_val(x) ((x).pgprot)
#define __pgprot(x) ((pgprot_t) { (x) } )
4.3.5 检查表项是否有效
内核在对各级表项进行初始化时,无效的表项都填充了数值 0。内核提供 pgd_none、pud_none、pmd_none 和 pte_none 4 个函数来检查各级表项是否为 0,同时也能够判断出表项是否有效。
// file: arch/x86/include/asm/pgtable.h
static inline int pgd_none(pgd_t pgd)
{
return !native_pgd_val(pgd);
}
static inline int pud_none(pud_t pud)
{
return native_pud_val(pud) == 0;
}
static inline int pmd_none(pmd_t pmd)
{
/* Only check low word on 32-bit platforms, since it might be
out of sync with upper half. */
return (unsigned long)native_pmd_val(pmd) == 0;
}
static inline int pte_none(pte_t pte)
{
return !pte.pte;
}
4.3.6 获取页属性
我们在 4.1.5 节介绍过页属性掩码,各级表项与该掩码进行按位与操作后,就能得到各标志位的值。
内核提供了pte_flags、pmd_flags、pud_flags 和 pgd_flags函数,分别用来获取页表项、中层页目录项、上层页目录项以及全局页目录项的页属性。其内部引用的 native_pte_val 等函数用来对结构体进行解包,具体可参考 4.3.2 节。
// file: arch/x86/include/asm/pgtable_types.h
/**
* 获取页表项中的页属性
*/
static inline pteval_t pte_flags(pte_t pte)
{
return native_pte_val(pte) & PTE_FLAGS_MASK;
}
/**
* 获取中层页目录项中的页属性
*/
static inline pmdval_t pmd_flags(pmd_t pmd)
{
return native_pmd_val(pmd) & PTE_FLAGS_MASK;
}
/**
* 获取上层页目录项中的页属性
*/
static inline pudval_t pud_flags(pud_t pud)
{
return native_pud_val(pud) & PTE_FLAGS_MASK;
}
/**
* 获取全局页目录项中的页属性
*/
static inline pgdval_t pgd_flags(pgd_t pgd)
{
return native_pgd_val(pgd) & PTE_FLAGS_MASK;
}
4.3.7 表项中的物理地址到虚拟地址的转换
我们在 4.1.4 节介绍了页帧掩码 PTE_PFN_MASK,该掩码会屏蔽掉表项中的标志位,仅保留表项中的物理地址。内核提供了函数 pgd_page_vaddr、pud_page_vaddr、pmd_page_vaddr 分别将全局页目录项、上层页目录项、中层页目录项中的物理地址转换成虚拟地址。
由于各级表项中引用的是下级页表的物理地址,所以这三个函数得到的也是对应下级页表的虚拟地址。
// file: arch/x86/include/asm/pgtable.h
/**
* 将全局页目录项中的物理地址转换成虚拟地址,
* 转换后得到上层页目录的基地址,该地址对齐到 4KB 的页
*/
static inline unsigned long pgd_page_vaddr(pgd_t pgd)
{
return (unsigned long)__va((unsigned long)pgd_val(pgd) & PTE_PFN_MASK);
}
/**
* 将上层页目录项中的物理地址转换成虚拟地址,
* 转换后得到中层页目录的基地址,该地址对齐到 4KB 的页
*/
static inline unsigned long pud_page_vaddr(pud_t pud)
{
return (unsigned long)__va((unsigned long)pud_val(pud) & PTE_PFN_MASK);
}
/**
* 将中层页目录项中的物理地址转换成虚拟地址,
* 转换后得到页表的基地址,该地址对齐到 4KB 的页
*/
static inline unsigned long pmd_page_vaddr(pmd_t pmd)
{
return (unsigned long)__va(pmd_val(pmd) & PTE_PFN_MASK);
}
我们以 pgd_page_vaddr函数为例说明其转换过程,其余函数执行过程类似。在 pgd_page_vaddr函数中,首先通过 pgd_val(pgd)从 pgd_t结构体中提取出全局页目录项的值;然后将该值与页帧掩码进行按位与操作,从而屏蔽掉标志位,获取到全局页目录项中的物理地址。该物理地址就是全局页目录项所引用的上层页目录的基地址。最后,通过宏 __va,将物理地址转成虚拟地址。所以,pgd_page_vaddr函数获取到的是上层页目录(PUD)的基地址。
pgd_page_vaddr函数得到上层页目录的虚拟地址,如下图所示:
类似的,pud_page_vaddr函数获取到的是上层页目录项中所引用的中层页目录(PMD)的基地址。
pmd_page_vaddr函数获取到的是中层页目录项中引用的页表(PT)基地址。
4.3.8 获取表项自身的虚拟地址
4.3.7 小节中的接口,获取到的是各级表项所引用的下级页表的基地址;本小节中的接口,获取到的是表项自身的虚拟地址。
我们以pgd_offset为例进行说明。 宏 pgd_offset接收 2 个参数:
mm_struct结构体指针- 需要计算的虚拟地址
从 4.2.2 小节可以知道,每个进程的内存描述符 mm_struct 中有一个字段 pgd 指向进程使用的全局页目录。所以,在 pgd_offset 中,使用 (mm)->pgd获取到全局页目录的基地址,使用 pgd_index((address))获取到虚拟地址对应的全局页目录项的索引,两着相加就得到了该表项的虚拟地址。
pgd_offset 得到全局页目录项的虚拟地址,如下图所示:
pgd_offset_k 是 pgd_offset的一个特例。因为pgd_offset_k的第一个参数 init_mm 是内核使用的 mm_struct 变量,所以 pgd_offset_k 获取到的是内核态地址对应的全局页目录项的虚拟地址。
类似的,使用 pud_offset获取到的是上层页目录项的虚拟地址;pmd_offset 获取到的是中层页目录项的虚拟地址;pte_offset_kernel 获取到的是页表项的虚拟地址。
// file: arch/x86/include/asm/pgtable.h
/*
* pgd_offset() returns a (pgd_t *)
* pgd_index() is used get the offset into the pgd page's array of pgd_t's;
*/
#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))
/*
* a shortcut which implies the use of the kernel's pgd, instead
* of a process's
*/
#define pgd_offset_k(address) pgd_offset(&init_mm, (address))
static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)
{
return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);
}
/* Find an entry in the second-level page table.. */
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
{
return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
}
static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}
4.3.9 填充各级表项
内核提供了 pgd_populate、 pud_populate 以及 pmd_populate_kernel 等多个函数来填充各级表项。我们以 pgd_populate 函数为例,来说明填充过程。其余函数执行过程类似。
pgd_populate函数接收 3 个参数,分别是:
mm_struct结构体指针- 全局页目录项指针
- 上层页目录基地址
该函数实现的功能,是把上层页目录物理地址与页属性组合后,写入全局页目录项中。
pgd_populate 函数执行过程如下:
-
首先调用了
paravirt_alloc_pud函数。- 该函数是一个空函数,未执行任何操作;
-
然后调用
set_pgd函数将组装好的全局页目录项保存到pgd_t指针指向的地址。全局页目录项组装过程如下:
- 调用宏
__pa将上层页目录的基地址转换成物理地址 - 将物理地址和页属性进行按位或操作,得到全局页目录项的值
- 调用
__pgd将全局页目录项包装成pgd_t结构体。
- 调用宏
关于 set_pgd 和 __pgd 函数的实现,请参考 4.3.3 和 4.3.2 小节。
pud_populate 和 pmd_populate_kernel 函数的执行过程与 pgd_populate 类似,此处不再赘述。
// file: arch/x86/include/asm/pgalloc.h
static inline void pgd_populate(struct mm_struct *mm, pgd_t *pgd, pud_t *pud)
{
paravirt_alloc_pud(mm, __pa(pud) >> PAGE_SHIFT);
set_pgd(pgd, __pgd(_PAGE_TABLE | __pa(pud)));
}
static inline void pud_populate(struct mm_struct *mm, pud_t *pud, pmd_t *pmd)
{
paravirt_alloc_pmd(mm, __pa(pmd) >> PAGE_SHIFT);
set_pud(pud, __pud(_PAGE_TABLE | __pa(pmd)));
}
static inline void pmd_populate_kernel(struct mm_struct *mm,
pmd_t *pmd, pte_t *pte)
{
paravirt_alloc_pte(mm, __pa(pte) >> PAGE_SHIFT);
set_pmd(pmd, __pmd(__pa(pte) | _PAGE_TABLE));
}
// file: arch/x86/include/asm/pgalloc.h
#define paravirt_pgd_alloc(mm) __paravirt_pgd_alloc(mm)
static inline int __paravirt_pgd_alloc(struct mm_struct *mm) { return 0; }
static inline void paravirt_alloc_pte(struct mm_struct *mm, unsigned long pfn) {}
static inline void paravirt_alloc_pmd(struct mm_struct *mm, unsigned long pfn) {}
static inline void paravirt_alloc_pud(struct mm_struct *mm, unsigned long pfn) {}
五、 内核代码的映射
在 Linux 内核的链接脚本中,将内核的虚拟地址起点设定为 __START_KERNEL:
// file: arch/x86/kernel/vmlinux.lds.S
SECTIONS
{
#ifdef CONFIG_X86_32
...
#else
. = __START_KERNEL;
phys_startup_64 = startup_64 - LOAD_OFFSET;
#endif
...
}
宏 __START_KERNEL 扩展为 0xffffffff81000000, 定义在文件 arch/x86/include/asm/page_64_types.h中:
// file: arch/x86/include/asm/page_64_types.h
#define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START) // __START_KERNEL 扩展为 0xffffffff81000000
#define __PHYSICAL_START ((CONFIG_PHYSICAL_START + \ // __PHYSICAL_START 扩展为 0x1000000
(CONFIG_PHYSICAL_ALIGN - 1)) & \
~(CONFIG_PHYSICAL_ALIGN - 1))
// file: include/generated/autoconf.h
#define CONFIG_PHYSICAL_START 0x1000000
从定义中可以看到,__START_KERNEL 由 __START_KERNEL_map和 __PHYSICAL_START 相加得到。__PHYSICAL_START( 0x1000000)是个编译时常量,指定了内核加载的物理地址; __START_KERNEL_map是内核映射区的起始地址;__START_KERNEL表示内核代码的起始地址。
内核物理地址和虚拟地址的映射关系如下:
物理地址 0x0 映射到虚拟地址 __START_KERNEL_map(0xffffffff80000000);内核加载的物理地址 __PHYSICAL_START(0x1000000)映射到虚拟地址 __START_KERNEL(0xffffffff81000000)。内核映射区的最大尺寸为 512M,但内核实际大小可能会小于该值。不管怎样,都会分配一个单独的中层页目录(PMD,可映射 512M 内存),来映射内核。
内核最大尺寸限制:
// file: arch/x86/include/asm/page_64_types.h
/*
* Kernel image size is limited to 512 MB (see level2_kernel_pgt in
* arch/x86/kernel/head_64.S), and it is mapped here:
*/
#define KERNEL_IMAGE_SIZE (512 * 1024 * 1024) // 内核最大尺寸限制
注:内核的默认加载地址是 __PHYSICAL_START(0x1000000);但是,如果配置选项 CONFIG_RELOCATABLE=y,内核会被加载到其它地址,此时映射关系会有变化。
// file: arch/x86/Kconfig
config PHYSICAL_START
hex "Physical address where the kernel is loaded" if (EXPERT || CRASH_DUMP)
default "0x1000000"
---help---
This gives the physical address where the kernel is loaded.
If kernel is a not relocatable (CONFIG_RELOCATABLE=n) then
bzImage will decompress itself to above physical address and
run from there. Otherwise, bzImage will run from the address where
it has been loaded by the boot loader and will ignore above physical
address.
...
...
六、 早期页表的创建
内核定义了一系列全局变量,用来存储早期的各级页目录。
// file: arch/x86/kernel/head_64.S
__INITDATA
NEXT_PAGE(early_level4_pgt)
.fill 511,8,0
.quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE
NEXT_PAGE(early_dynamic_pgts)
.fill 512*EARLY_DYNAMIC_PAGE_TABLES,8,0
.data
...
NEXT_PAGE(level3_kernel_pgt)
.fill L3_START_KERNEL,8,0
/* (2^48-(2*1024*1024*1024)-((2^39)*511))/(2^30) = 510 */
.quad level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE
.quad level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
NEXT_PAGE(level2_kernel_pgt)
/*
* 512 MB kernel mapping. We spend a full page on this pagetable
* anyway.
*
* The kernel code+data+bss must not be bigger than that.
*
* (NOTE: at +512MB starts the module area, see MODULES_VADDR.
* If you want to increase this then increase MODULES_VADDR
* too.)
*/
PMDS(0, __PAGE_KERNEL_LARGE_EXEC,
KERNEL_IMAGE_SIZE/PMD_SIZE)
NEXT_PAGE(level2_fixmap_pgt)
.fill 506,8,0
.quad level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
/* 8MB reserved for vsyscalls + a 2MB hole = 4 + 1 entries */
.fill 5,8,0
NEXT_PAGE(level1_fixmap_pgt)
.fill 512,8,0
early_level4_pgt、early_dynamic_pgts、level3_kernel_pgt、level2_kernel_pgt、level2_fixmap_pgt 以及 level1_fixmap_pgt 这些数据结构,都和早期页表的创建相关。其中,以 early_*命名的变量,只在内核初始化时使用,初始化完成后,占用的空间会被释放。以 *_kernel_* 字样命名的变量,跟内核代码映射相关。
代码开头,我们看到了宏 __INITDATA,该宏定义了节 ".init.data" 。以 .init 开头的节(section),都是内核初始化时临时使用的,初始化完成后,这些数据占用的内存会释放掉。
// file: include/linux/init.h
#define __INITDATA .section ".init.data","aw",%progbits
从__INITDATA 到 .data 之间的数据,都属于.init.data节,该节包含 early_level4_pgt 和 early_dynamic_pgts 变量。early_level4_pgt就是早期使用的顶级页表,即全局页目录(PGD);early_dynamic_pgts 是用来临时分配页表的。
宏 NEXT_PAGE 会将内存对齐到 PAGE_SIZE(扩展为 4096)字节大小,并将入参 name 声明为全局符号。
// file: arch/x86/kernel/head_64.S
#define NEXT_PAGE(name) \
.balign PAGE_SIZE; \
GLOBAL(name)
宏 PAGE_SIZE 定义如下:
// file: 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) // 扩展为 4096
宏 GLOBAL 定义如下:
// file: arch/x86/include/asm/linkage.h
#define GLOBAL(name) \
.globl name; \ // 将 name 声明为全局符号
name:
6.1 早期的全局页目录 -- early_level4_pgt
来看下 early_level4_pgt 变量,先是通过 .fill 指令,用数字 0 填充了 511 个 8 字节大小的空间(因为填充的是全 0,这些页表项都没有用到);然后通过 .quad 指令,将 level3_kernel_pgt 的物理地址以及访问权限和状态标志写入随后的 8个字节内。最终,early_level4_pgt 拥有 512个元素,总大小为 字节。early_level4_pgt 本质上是拥有 512 个元素的数组,如下所示:
// file: arch/x86/kernel/head64.c
extern pgd_t early_level4_pgt[PTRS_PER_PGD];
宏 PTRS_PER_PGD 表示每个全局页目录中的表项数量,扩展为 512。
由于在 Linux 内核的链接脚本中,将内核的虚拟地址起点设定为 __START_KERNEL,所以内核编译文件中符号的地址,都是大于 __START_KERNEL的,当然更是大于 __START_KERNEL_map。换句话说,内核文件中的这些数据,将会被加载到虚拟地址空间的内核代码映射区。level3_kernel_pgt 当然也处于内核代码映射区中,所以 level3_kernel_pgt - __START_KERNEL_map,就会得到 level3_kernel_pgt的物理地址。
注:如上文所述,编译时,__START_KERNEL_map 映射到物理地址 0x0。
内核镜像 vmlinux 中,符号 level3_kernel_pgt 的地址如下所示:
$ nm vmlinux|grep level3_kernel_pgt
ffffffff81c11000 D level3_kernel_pgt
注:.fill 及 .quad 指令详情,见 GAS 在线文档。
宏 _PAGE_TABLE 定义了表项的访问权限,用位图来表示:
// file: arch/x86/include/asm/pgtable_types.h
#define _PAGE_TABLE (_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | \
_PAGE_ACCESSED | _PAGE_DIRTY)
由于level3_kernel_pgt的地址对齐到 4KB,所以其低 12 位全是0,可用来存储访问权限及状态信息。
至于为什么要把内核代码映射区安装到 PGD 的第 511 项,只要把 __START_KERNEL_map(0xFFFF80000000,48位) 右移 PGDIR_SHIFT(39) 位就知道了,正好等于 511。
early_level4_pgt 是 4 级页表,即 Intel 文档中的 PML4(Page Map Level 4) 表,4 级页表项的格式如下所示:
6.2 临时页目录空间 -- early_dynamic_pgts
early_dynamic_pgts是为内核创建临时页表分配的空间,其地址同样是对齐到 4KB,该空间大小为 64 个 4KB。其中,宏 EARLY_DYNAMIC_PAGE_TABLES扩展为 64:
// file: arch/x86/include/asm/pgtable_64_types.h
#define EARLY_DYNAMIC_PAGE_TABLES 64
使用时,其声明如下:
// file: arch/x86/kernel/head64.c
extern pmd_t early_dynamic_pgts[EARLY_DYNAMIC_PAGE_TABLES][PTRS_PER_PMD];
宏 PTRS_PER_PMD扩展为 512。
6.3 上层页目录 -- level3_kernel_pgt
// file: arch/x86/kernel/head_64.S
NEXT_PAGE(level3_kernel_pgt)
.fill L3_START_KERNEL,8,0
/* (2^48-(2*1024*1024*1024)-((2^39)*511))/(2^30) = 510 */
.quad level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE
.quad level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
level3_kernel_pgt 表示 3 级页表(上层页目录,PUD),其地址同样是 4KB 对齐的(使用 NEXT_PAGE 宏创建的符号,其地址都会对齐到 4KB)。
先计算出虚拟地址__START_KERNEL_map在 上层页目录(PUD )的索引,即 L3_START_KERNEL;然后把小于该索引的所有项全部置 0。
来看下 L3_START_KERNEL 的计算:
L3_START_KERNEL = pud_index(__START_KERNEL_map) // L3_START_KERNEL 等于 510
#define pud_index(x) (((x) >> PUD_SHIFT) & (PTRS_PER_PUD-1)) // PUD_SHIFT 扩展为 30,PTRS_PER_PUD 扩展为 512
填充完 510 个无效页表项之后,把 level2_kernel_pgt 的物理地址 level2_kernel_pgt - __START_KERNEL_map 存入对应的 PUD 表项(即索引为 L3_START_KERNEL 的表项)中。由于 level2_kernel_pgt的地址是 4KB 对齐的,低 12 位全为 0,可用来存储页表项访问权限及信息。
宏 _KERNPG_TABLE 定义了访问权限及信息:
// file: arch/x86/include/asm/pgtable_types.h
#define _KERNPG_TABLE (_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED | \
_PAGE_DIRTY)
同理,把 level2_fixmap_pgt的物理地址写入到第 511 个表项。
level3_kernel_pgt是 3 级页表,对应于 Intel 文档中的页目录指针表(Page-Directory-pointer table,PDPT)。当该表中的项(PDPTE)指向页目录(Page )时,其格式如下图所示:
各字段说明如下:
6.4 中层页目录 -- level2_kernel_pgt
// file: arch/x86/kernel/head_64.S
NEXT_PAGE(level2_kernel_pgt)
/*
* 512 MB kernel mapping. We spend a full page on this pagetable
* anyway.
*
* The kernel code+data+bss must not be bigger than that.
*
* (NOTE: at +512MB starts the module area, see MODULES_VADDR.
* If you want to increase this then increase MODULES_VADDR
* too.)
*/
PMDS(0, __PAGE_KERNEL_LARGE_EXEC,
KERNEL_IMAGE_SIZE/PMD_SIZE)
先来看一下 PMDS宏,该宏接收 3 个参数-- 物理起始地址 START 、权限 PERM、数量 COUNT,其功能是把从物理起始地址 START 开始,共COUNT 个中层页目录项,写入当前位置。
// file: arch/x86/kernel/head_64.S
/* Automate the creation of 1 to 1 mapping pmd entries */
#define PMDS(START, PERM, COUNT) \
i = 0 ; \
.rept (COUNT) ; \
.quad (START) + (i << PMD_SHIFT) + (PERM) ; \
i = i + 1 ; \
.endr
宏 __PAGE_KERNEL_LARGE_EXEC定义了访问权限和标志位:
// file: arch/x86/include/asm/pgtable_types.h
#define __PAGE_KERNEL_LARGE_EXEC (__PAGE_KERNEL_EXEC | _PAGE_PSE)
#define __PAGE_KERNEL_EXEC \
(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_GLOBAL)
我们重点关注下_PAGE_PSE位(位 7),该位置位后,中层页目录项会映射到页,不会再引用页表(PT)了。现在我们是在二级页表(PMD)的表项里将该位置位的,也就是说每个中层页表项直接映射到页了,页大小为 2MB。所以,在建立早期页表时,使用的是 2MB 大小的页。
level2_kernel_pgt 是 2 级页表,对应 Intel 文档中的页目录(Page Directory,PD)。当 PD 直接映射到页时,页目录项(PDE)的格式如下图所示:
各字段说明如下:
KERNEL_IMAGE_SIZE/PMD_SIZE计算了需要安装的页表项数量,其中宏 KERNEL_IMAGE_SIZE定义了内核镜像的允许的最大尺寸,扩展为 512M;宏PMD_SIZE定义了每个 PMD 项映射的内存大小,扩展为 2M;这 2 个宏定义如下:
// file: arch/x86/include/asm/page_64_types.h
/*
* Kernel image size is limited to 512 MB (see level2_kernel_pgt in
* arch/x86/kernel/head_64.S), and it is mapped here:
*/
#define KERNEL_IMAGE_SIZE (512 * 1024 * 1024)
// file: arch/x86/include/asm/pgtable_64_types.h
#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT) // PMD_SIZE 扩展为 2M
#define PMD_SHIFT 21
最终,将物理地址 0 ~ 512M 处共 512M 空间,256 个中层页目录项(PMDE),写入到 level2_kernel_pgt 地址处。
因为内核就处于物理地址 0 ~ 512M 处,所以这段代码执行完后,就把内核代码(.text+.data+.tss)映射到了内核代码映射区。正如第五节示意图中所展示的那样。
6.5 level2_fixmap_pgt
NEXT_PAGE(level2_fixmap_pgt)
.fill 506,8,0
.quad level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
/* 8MB reserved for vsyscalls + a 2MB hole = 4 + 1 entries */
.fill 5,8,0
level2_fixmap_pgt 处定义了一个 4KB 的 PMD表,其第 506 项存储着 level1_fixmap_pgt 页表的物理地址及访问权限;其余各项均初始化为 0。
6.6 level1_fixmap_pgt
全部 512 个表项均初始化为 0,未使用。
6.7 早期页表结构图
页表创建后,其结构如图所示:
七、正式页表的创建
正式页表的各级目录声明,可以在文件 arch/x86/include/asm/pgtable_64.h 找到:
// file: arch/x86/include/asm/pgtable_64.h
extern pud_t level3_kernel_pgt[512];
extern pud_t level3_ident_pgt[512];
extern pmd_t level2_kernel_pgt[512];
extern pmd_t level2_fixmap_pgt[512];
extern pmd_t level2_ident_pgt[512];
extern pgd_t init_level4_pgt[];
#define swapper_pg_dir init_level4_pgt
其中,swapper_pg_dir 指向内核的全局页目录 init_level4_pgt,同时出现的还有其它几个页表。我们可以回忆一下内核的虚拟地址空间布局,其中 XXX_ident_pgt 对应的是直接映射区,XXX_kernel_pgt 对应的是内核代码区,XXX_fixmap_pgt 对应的是固定映射区。
上述声明的这些变量,在文件 arch\x86\kernel\head_64.S里进行初始化。
// file: arch/x86/kernel/head_64.S
NEXT_PAGE(init_level4_pgt)
.quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
.org init_level4_pgt + L4_PAGE_OFFSET*8, 0
.quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
.org init_level4_pgt + L4_START_KERNEL*8, 0
/* (2^48-(2*1024*1024*1024))/(2^39) = 511 */
.quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE
NEXT_PAGE(level3_ident_pgt)
.quad level2_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
.fill 511, 8, 0
NEXT_PAGE(level2_ident_pgt)
/* Since I easily can, map the first 1G.
* Don't set NX because code runs from these pages.
*/
PMDS(0, __PAGE_KERNEL_IDENT_LARGE_EXEC, PTRS_PER_PMD)
NEXT_PAGE(level3_kernel_pgt)
.fill L3_START_KERNEL,8,0
/* (2^48-(2*1024*1024*1024)-((2^39)*511))/(2^30) = 510 */
.quad level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE
.quad level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
NEXT_PAGE(level2_kernel_pgt)
/*
* 512 MB kernel mapping. We spend a full page on this pagetable
* anyway.
*
* The kernel code+data+bss must not be bigger than that.
*
* (NOTE: at +512MB starts the module area, see MODULES_VADDR.
* If you want to increase this then increase MODULES_VADDR
* too.)
*/
PMDS(0, __PAGE_KERNEL_LARGE_EXEC,
KERNEL_IMAGE_SIZE/PMD_SIZE)
NEXT_PAGE(level2_fixmap_pgt)
.fill 506,8,0
.quad level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
/* 8MB reserved for vsyscalls + a 2MB hole = 4 + 1 entries */
.fill 5,8,0
NEXT_PAGE(level1_fixmap_pgt)
.fill 512,8,
7.1 全局页目录 -- init_level4_pgt
全局页目录包含 512 个表项,其中有效表项为 level3_ident_pgt 和 level3_kernel_pgt。 level3_kernel_pgt 在早期页表创建时已经介绍过了,此处不再赘述。
在全局页目录中,先是通过 .quad 指令,将 level3_ident_pgt的物理地址以及访问权限和状态标志写入起始的 8个字节内。
.org 指令格式如下:
.org new-lc , fill
该指令会把位置计数器推进到参数 new-lc 指示的位置,原位置和新位置之间的空间,使用参数 fill 指定的数值来填充。.org 指令的详细说明请参考 as 文档。
宏 L4_PAGE_OFFSET 定义如下:
// file: arch/x86/kernel/head_64.S
L4_PAGE_OFFSET = pgd_index(__PAGE_OFFSET)
其中,宏 pgd_index 定义如下:
// file: arch/x86/include/asm/pgtable.h
/*
* the pgd page can be thought of an array like this: pgd_t[PTRS_PER_PGD]
*
* this macro returns the index of the entry in the pgd page which would
* control the given virtual address
*/
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
全局页目录可以看做是包含 PTRS_PER_PGD 个元素的数组 pgd_t[PTRS_PER_PGD]。 pgd_index的功能,是计算出虚拟地址对应的全局页目录项的索引值。宏 PTRS_PER_PGD 扩展为 512,宏 PGDIR_SHIFT 扩展为 39。
综上,宏 L4_PAGE_OFFSET,表示地址 __PAGE_OFFSET 在全局页目录中的索引。从虚拟内存的布局中可以看到,宏 __PAGE_OFFSET 是直接映射区的起始地址。所以,通过 .org指令,跳到了地址 __PAGE_OFFSET 所在的全局页目录项,跳过的字节使用数值 0 填充。使用 0 填充的表项为无效表项,因为其存在位(Present bit)为 0。
接下来,通过 .quad 指令,将 level3_ident_pgt的物理地址以及访问权限和状态标志写入到随后的 8个字节内。
然后,跳到宏 L4_START_KERNEL 指示的索引处。宏 L4_START_KERNEL 定义如下:
L4_START_KERNEL = pgd_index(__START_KERNEL_map)
L4_START_KERNEL 计算出的是地址 __START_KERNEL_map 所对应的全局页目录索引,__START_KERNEL_map 是内核映射区的起始地址。
所以,通过 .org指令,跳到了地址__START_KERNEL_map所在的全局页目录项,跳过的空间使用数值 0 填充。
然后,将 level3_kernel_pgt 的物理地址以及访问权限和状态标志写入最后一个全局页目录项。
7.2 上层页目录 -- level3_ident_pgt
NEXT_PAGE(level3_ident_pgt)
.quad level2_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
.fill 511, 8, 0
在上层页目录 level3_ident_pgt的索引 0 处,填写的是中层页目录(二级页表) level2_ident_pgt 的物理地址与标志位组合而成的表项。其余 511 个表项,使用数组 0 填充,为无效表项。
7.3 中层页目录 -- level2_ident_pgt
NEXT_PAGE(level2_ident_pgt)
/* Since I easily can, map the first 1G.
* Don't set NX because code runs from these pages.
*/
PMDS(0, __PAGE_KERNEL_IDENT_LARGE_EXEC, PTRS_PER_PMD)
宏 PMDS 我们在 6.4 节中介绍过,该宏接收 3 个参数-- 物理起始地址 START 、权限 PERM、数量 COUNT,其功能是把从物理起始地址 START 开始,共COUNT 个中层页目录项,写入当前位置。
宏 PTRS_PER_PMD扩展为 512,上文已经介绍过了。
宏 __PAGE_KERNEL_IDENT_LARGE_EXEC 是页表项标志,被定义为 __PAGE_KERNEL_LARGE_EXEC, 其定义如下:
// file: arch/x86/include/asm/pgtable_types.h
#define __PAGE_KERNEL_IDENT_LARGE_EXEC __PAGE_KERNEL_LARGE_EXEC
宏 __PAGE_KERNEL_LARGE_EXEC 在 5.4 节介绍过,该标志位里包含 _PAGE_PSE,即 PS(Page Size)位,说明该表项直接映射到页。由于是二级表项,所以每个表项映射到 2MB 大小的页。
宏 PMDS 执行完成后,在中层页目录 level2_ident_pgt 里填充了512 个表项。由于是从物理地址 0 开始填充的,且每个页表项映射到 2MB 的页,所以 level2_ident_pgt 实际映射了物理地址 0 ~ 1GB 之间的内存。
7.4 上层页目录 -- level3_kernel_pgt
上层页目录 level3_kernel_pgt 及其子目录项的填充,请见 6.3、6.4、6.5、6.6 小节。
7.5 正式页表结构图
可以看到,在用户空间和内核空间直接映射区的最低地址区域,都映射了相同的 1GB 物理地址;在内核代码区,映射了 512MB 的物理地址。
八、 运行地址与编译地址的差值 -- phys_base
phys_base是指内核的物理基地址。编译阶段,初始化为 0:
// file: arch/x86/kernel/head_64.S
ENTRY(phys_base)
/* This must match the first entry in level2_kernel_pgt */
.quad 0x0000000000000000
使用时,声明如下:
// file: arch/x86/include/asm/page_64.h
extern unsigned long phys_base;
在上文中,我们介绍过,编译时内核的加载地址为 __PHYSICAL_START,即 0x1000000。如果开启了配置选项 CONFIG_RELOCATABLE=y,运行时,内核会被加载到其它地址,而不再是 __PHYSICAL_START。我们需要计算出编译时和运行时加载地址的差值,来修正编译时写入页表的那些物理地址。phys_base中保存的就是这个差值。
// file: arch/x86/kernel/head_64.S
startup_64:
/*
* Compute the delta between the address I am compiled to run at and the
* address I am actually running at.
*/
leaq _text(%rip), %rbp
subq $_text - __START_KERNEL_map, %rbp
...
/* Fixup phys_base */
addq %rbp, phys_base(%rip)
...
首先,我们看到,把有效地址 _text(%rip)存入了 %rbp 寄存器。符号 _text 定义在链接文件 vmlinux.lds.S 中,其值为 __START_KERNEL。
// file: arch/x86/kernel/vmlinux.lds.S
SECTIONS
{
#ifdef CONFIG_X86_32
...
#else
. = __START_KERNEL;
phys_startup_64 = startup_64 - LOAD_OFFSET;
#endif
/* Text and read-only data */
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
...
} :text = 0x9090
...
}
_text(%rip)是指令指针相对寻址,获取到的是 _text实际运行的地址。由于运行到 startup_64时,内核代码段的虚拟地址与物理地址是相等的(identity mapping),都处于内存的低端位置,所以_text(%rip) 得到的是内核代码段的实际加载地址,即物理地址。
关于 _text(%rip) 的计算,可参考 Linux Kernel: _text(%rip) 的值如何计算?。
接下来,计算编译时和运行时加载地址的差值。$_text - __START_KERNEL_map计算的是编译时的物理地址,其值为 0x1000000;%rbp原始值为实际加载地址,sub 指令执行后,保存的是两者的差值 delta。
最后,把 %rbp 的值,存入 phys_base处。
所以,在运行时,phys_base 处保存的是内核实际加载地址与编译时的加载地址之差。
九、 修正页表内的物理地址
在上文中讲到,在编译时期,我们就填充了一些表项,甚至把整个内核代码映射到了虚拟地址。但是,在编译时,我们填充内核映射区的表项时,物理基地址 phys_base 为 0x0,内核代码段的加载地址为 __PHYSICAL_START;运行时,内核代码段实际加载地址为__PHYSICAL_START + delta,其中 delta 为两者的差值。这种情况下,编译时存入页表中的物理地址跟运行时的实际物理地址就不一致了,需要将它们进行修复,否则地址就是错误的。修复过程如下:
startup_64:
/*
* Compute the delta between the address I am compiled to run at and the
* address I am actually running at.
*/
leaq _text(%rip), %rbp // 内核运行时加载地址
subq $_text - __START_KERNEL_map, %rbp // 计算 内核运行时地址 与 编译时加载地址 之差
...
/*
* Fixup the physical addresses in the page table
*/
addq %rbp, early_level4_pgt + (L4_START_KERNEL*8)(%rip) // 修正 level3_kernel_pgt 的物理地址
addq %rbp, level3_kernel_pgt + (510*8)(%rip) // 修正 level2_kernel_pgt 的物理地址
addq %rbp, level3_kernel_pgt + (511*8)(%rip) // 修正 level2_fixmap_pgt 的物理地址
addq %rbp, level2_fixmap_pgt + (506*8)(%rip) // 修正 level1_fixmap_pgt 的物理地址
...
/*
* Fixup the kernel text+data virtual addresses. Note that
* we might write invalid pmds, when the kernel is relocated
* cleanup_highmap() fixes this up along with the mappings
* beyond _end.
*/
leaq level2_kernel_pgt(%rip), %rdi
leaq 4096(%rdi), %r8
/* See if it is a valid page table entry */
1: testq $1, 0(%rdi)
jz 2f
addq %rbp, 0(%rdi)
/* Go to the next page */
2: addq $8, %rdi
cmp %r8, %rdi
jne 1b
...
...
头 2 行计算出了内核代码段编译时加载地址和运行时加载地址的差值 delta,并保存在 %rbp 寄存器。这个我们在前面的小节里已经分析过了。
在编译时,向页表项里填入了一些地址:
early_level4_pgt[511] => level3_kernel_pgt
level3_kernel_pgt[510] => level2_kernel_pgt
level3_kernel_pgt[511] => level2_fixmap_pgt
level2_fixmap_pgt[506] => level1_fixmap_pgt
随后的 4 行代码,将这些物理地址加上 %rbp,完成对这些页表项的修正。
另外,编译时,还将地址 0x0 ~ 0x10000000 处共 512M 的物理内存映射到了level2_kernel_pgt 表里,共写入了 256 项,4096 字节大小。现在这些页表项中的物理地址也需要修正。
随后的代码,遍历这些项,对有效的表项进行修复;无效的表项,直接跳过。表项的 P 位(Present,位 0)表明该项是否有效,P 位为 1 时,说明该项有效;否则,该项无效。
这里通过标签 1 处的testq指令来验证表项是否有效,该指令将常量 1 与表项内容进行按位与操作。当结果为 0 时,说明 P 位为 0,表项无效,则跳到下一项进行处理;当结果为 1 时,说明表项有效,把该表项中保存的地址增加 delta(即 %rbp的值)进行修正;如此反复循环,直到达到页表尺寸 4096 字节。
修正之后,内核地址的映射关系变化如下:
十、参考资料
1、Intel 开发者手册:Intel 64 and IA-32 Architectures Software Developer Manuals Volume 3A, Chapter 4 Paging
3、 GAS 在线文档
4、 Linux Kernel: _text(%rip) 的值如何计算?
5、 as 在线文档