目录
引论,三个地址
硬件中的分段
段描述符
快速访问段描述符
分段单元
Linux GDT
Linux LDT
硬件中的分页
PAE
硬件高速缓存
TLB
Linux中的分页
页表类型定义pgd_t、pmd_t、pud_t和pte_t
pteval_t,pmdval_t,pudval_t,pgdval_t
pgd_t、pmd_t、pud_t和pte_t
xxx_val和__xxx
页表描述宏
PAGE宏–页表(Page Table)
PMD-Page Middle Directory (页目录)
PUD_SHIFT-页上级目录(Page Upper Directory)
PGDIR_SHIFT-页全局目录(Page Global Directory)
页表处理函数
查询页表项中任意一个标志的当前值
2.3.2 设置页表项中各标志的值
宏函数-把一个页地址和一组保护标志组合成页表项,或者执行相反的操作
简化页表项的创建和撤消
处理硬件高速缓存与TLB
Reference
引论,三个地址
下面来说说内存寻址!
程序员偶尔会引用内存地址作为访问内存单元的一种方式。我们有三种不同的地址:逻辑地址,线性地址,和物理地址
逻辑地址:指的是包含在机器语言指令中用来表示一个操作数或一条指令的地址!这种寻址方式在80X86著名的分段结构中表现得尤为具体,,它促使MSDOS程序员或Windows程序员把程序分成若干段(当然说的是汇编程序员hhh)。每一个逻辑地址都由一个段和偏移量组成!偏移量指明了从段开始的地方,到实际地址之间的距离
线性地址也被称为虚拟地址,是一个32位无符号整数(在32位平台上,自然的在64位平台上个就是64位无符号整数),可以用来表达高达4GB的地址!现行地址通常用16进制数字表示!
物理地址用来内存芯片及内存单元寻址。他们从与微处理器的地址引脚,发送到内存总线上的电信号。相对应物理地址由32位或36位无符号整数表示。内存控制单元通过一种分段单元的硬件电路把一个逻辑地址转换成一个线性地址,接着第二个称为分页单元的硬件电路把线性地址转换为一个物理地址!
硬件中的分段
从80286模型开始,英特尔微处理器以两种不同的方式执行地址转换这两种方式!分别被称为实模式和保护模式
段选择符和段寄存器一个逻辑地址有两个部分组成一个段标识符和一个段内相对地址的偏移量段标识符是一个16位长的字段也被称为段选择符为了快速方便地找到段选择符处理器提供一个叫做段寄存器的寄存器他的唯一目的是为了存放段选择符这些段寄存器被称为CS,SS,DS,ES,FS和GS。尽管只有六个段寄存器,但程序可以把同一个段的寄存器用作不同目的。方法是先把这个值保存在内存中,用完再恢复。其中有三者是有专门用途的。
CS(code segment)
:代码段寄存器!指向包含程序指令的段
SS(stack segmment)
:栈段寄存器!指向包含当前程序栈的段
DS(data segment)
:数据段寄存器!指向包含静态数据或者全局数据段!
CS寄存器还有一个重要的功能:它还有一个两位的字段用以指明CPU的当前特权级(Current Privilage Level
)
值为零时代表最高优先级值!为三时表示最低优先级!Linux只用零级和三级,分别被称为内核态和用户态!
段描述符
每个段由八个字节的段描述符表示,他表示了一个段的特征!段描述符放在GDT或LDT中。通常只定义一个GDT而每个进程除了存放在GDP的段,以外如果还创建了附加的段,可以有自己的LDT。段序描述符字段由以下
标志 | 说明 |
---|---|
base | 表示段的首字节的线性地址 |
G | 粒度标志!表示如果该位清零,则段大小以字节为单位。否则以4096字节的倍数。 |
limit | 存放段中最后一个内存单元的偏移量,从而决定了段的长度。这就跟G扯上关系了: 如果既被置为零,则一个段的大小在1B到1MB之间变化,否则则在4KB到4GB之间变化 |
S | 系统标志!如果它被清零则,这是一个系统段,存放着像LDT这种关键数据结构!否则它就是一个普通的代码段或数据段 |
type | 描述了段的类型特征和它的存取权限 |
DPL | 描述符特权级字段,用于限制这对这个段的存取!他表示为访问这个段要求的CPU最小优先级! |
P | segment present标志!表示当前段并不在主存当中,Linux总是把这个标志设为一,因为他从不把整个段交换到磁盘上。 |
D/B | 取决于是代码段还是数据段 |
AVL | 系统使用但是已经被Linux所忽略 |
以下是Linux中被广泛使用的类型!
-
代码段描述符:表示这个段是一个代码段,它可以放在GDP或LDT中此时该描述符志S标志为一
-
数据段描述符:表示这个段描述了一个数据段,也可以被放在GDP或LDT中,S标志为1。栈段是通过一般的数据段所实现的!
-
任务状态的描述符:表示这个段的描述代表一个任务状态段,也就是TSS!这个段用来保存处理寄存器的内容
-
局部描述符表描述符:表示这个段描述符代表一个包含了LDT的段。他只会出现在GDT中!相应的type字段的值为2,S被置为零。
快速访问段描述符
我们回忆一下:逻辑地址是由一个16位段选择符和32位偏移量组成段寄存器!仅仅存放段选择符。为了加速逻辑地址到线性地址的转换,8086处理器提供了一种附加的非编程的寄存器:共六个可编程的段寄存器所使用。每一个非编程的寄存器含有八个字节的段描述符,由相应的段寄存器的段选择符来指定。每当一个段选择符被装入段寄存器时,相应的段描述符就从内存被装到对应的非编程CPU寄存器中!从那个时候起,针对那个段的逻辑地址转换就可以不用访问主存中的GDP或LDT。处理器只需要引用存放段描述符的CPU寄存器即可!仅当段寄存器的内容发生改变时,才会有必要访问GDP或LDT。这体现了一种缓存机制!
段选择符字段有三个字段名:
标识 | 说明 |
---|---|
index | 指定了放在GD T或LBT中的相应段描述符的一个入口 |
TI | 指明断续描述符是在GDP中(TI = 0)还是在LDT中(TI = 1) |
RPL | 请求者特权:即当相应的段选择符装入了CS寄存器中只是CPU当前的特权级!它还可以用来访问数据段时,有选择的削弱处理器的特权级分段单元。 |
分段单元
分段单元会执行以下操作:
-
它会检查段选择符的TI字段,以决定段描述符保存在哪一个描述符表中,TI字段指明段描述符市的GDP中还是在激活的LDT中
-
从段选择符的index字段中计算段描述符地址:index字段的值乘以八,这个结果与gdpr或ldtr寄存器中的内容相加。
-
把逻辑地址的偏移量与段描述符Base字段的值相加就会得到线性地址
Linux当中的分段是非常有限的!实际上分段和分页在某种程度上会显得有些多余,因为它们都可以划分进程的物理地址空间!分段可以给每一个进程分配以不同的线性空间,而分页则可以把同一线性地址空间映射到不同的物理内存。与分段相比,linux更青睐于使用分页方式:因为当所有进程使用相同的段寄存器值时,内存管理变得非常简单!也就是他们可以共享同样的一组线性地址!Linux的设计目标之一就是可以把它们移植到大多数流行的处理平台之下然而RISC体系结构对分段的支持非常有限!
Linux下的逻辑地址与线性地址是一致的:即逻辑地址的偏移量字段的值予以相应的线性地址的值总是一致的
Linux GDT
单处理器系统中只有一个GDT,而在多处理器系统中每个CPU对应一个GDT所。有的GDT都存放在cpu_gdt_table数组中:而所有GDP的地址和它们的大小(这个大小是初始化GDTR计算器使用)被存放在cpu_gdt_desp数组中!
每一个GDP的包含18个段分别指向下列的段:
-
用户态和内核态下的代码段和数数据段共四个任务状态段
-
TSS,每个处理器有一个!
-
一个包含缺省局部描述符表的段,这个段是被所有进程共享的段!
-
三个局部线程存储段TLS,这种机制允许多线程应用程序使用最多三个局部与线程的数据段!
-
与高级电源管理相关的三个段
-
与支持即插即用功能的BIOS服务程序相关的五个段
-
被内核用来处理双重错误异常的特殊TSS段
Linux LDT
大多数Linux程序并不会使用局部描述符!然而在某些情况下进程仍然会需要创建自己的局部描述符表,比如说像Wine那样的程序
硬件中的分页
分页单元把线性地址转换为物理地址。其中一个关键的任务就是把请求的访问类型与线性地址的访问权限所相比较,如果这次访问是无效的,则会产生一个缺页异常!
为了效率,线性地址被分为固定长度为单位的组,称为页!页内不连续的线性地址会被映射到连续的物理地址当中去。这样内核可以指定一个页的物理地址和存取权限,而不用指定页所包含所有的线性地址的存取权限!我们通常遵循习惯使用习惯,让页来表示一组线性地址,包含这组地址中的数据。
分页单元把所有的分成固定长度的页框,有时也叫做物理页。每一个页框包含一个页,也就是说一个框的长度和一个页的长度是一致的。页框是主存的一部分,也就是存储区域。区分一个页和页框很重要,前者只是一个数据块,前者可以存放在任何页框和磁盘中。
把线性地址映射到物理地址的数据结构称为页表,页表存放在主存当中。并且启用分页单元之前就必须由内核对列表进行适当的初始化,从80386开始所有的值80X86处理器都会支持分页!它通过设置CR零寄存器的PG标志启用。当PG等于零时,线性地址就会被解释为物理地址。
32位线性地址经常会被分成三个域:
directory目录 | table(页表) | offset偏移量 |
---|---|---|
10 | 10 | 12 |
线性地址的转换分为两步走:每一步都基于一种转换表。第一种转换表被称为页目录表,第二种则是被称为页表。
使用这种二级模式的目的在于:减少每个进程页表所需要的RAM的数量!(自己想象如果给每一个信息地址的维护一个映射项的话这个页表将会有多么恐怖的大!)。页目录项和页表项有相同的结构!每项都包含下面的字段:
-
present标志:如果被置为1,所指的页或页表它就在主存当中!如果该标志被置为零,则这一页并不在主存!当中如果执行一个地址转换所需的页表项或页目录项中的present被置为零,那么分页单元就会把该线性地址存放在控制寄存器CR2中,并产生14号异常缺页异常!操作系统会介入进行相应的处理。
-
Field:包含页框物理地址最高二十位的字段!由于每一个页框有4kb标志的容量它的物理地址必须是4096的倍数,因此物理地址的最低12位总是零
-
access:每当分页单元对应的页框进行寻址时,就会设置这个标志!当选中的页被交换出去,这个标志可以作系统所使用。分页单元从不会重置这个标志,而是必须由操作系统去做!
-
read/write标志:含有页或页表的存取权限
-
dirty标志:只用来列表项中每当对一个页框进行写操作时就会设置这个标识,当选中的页被交换出去,这个标志可以作系统所使用。分页单元从不会重置这个标志,而是必须由操作系统去做!
-
user/supervisor标志:标志很有访问页或页表所需的特权级
-
PCD和PWT标志:硬件控制硬件高速缓存处理页或页表的方式
-
page size:只用于页目录项,如果被置为一则该目录页目录项指的是2mb或4mb的页框!
-
global标识:只应用于页表项,这个标志是用来防止常用页从TLB高速缓存中刷新出去
PAE
常规分页机制32位地址线理论上可以寻址4GB的RAM地址空间。但是,大型的服务器需要大于4GB的RAM来同时运行数以千计的进程。
因此:Intel通过在处理器上把管脚数从32增加到36,以提高处理器的寻址能力,使其达到2^36=64GB,同时引入了一种新的分页机制PAE(Physical Address Extension,物理地址扩展)把32位线性地址转换为36位物理地址才能使用所增加的物理内存,通过设置CR4的第5位来开启对PAE的支持。引入PAE就是为了访问大于4GB的RAM,线性地址仍然是32位,而物理地址是36位。
把一个32位的虚拟地址分成4个部分:
-
0-11:页内偏移
-
12-20:页表(Page Table)
-
21-29:页表目录表(Page Table Directory)
-
30-31:页目录指针表(Page Directory Pointer Table)
硬件高速缓存
这是为了缓解微处理器的频率与访问RAM芯片的频率相差过大的矛盾所引入的!为了缩小CPU和RAM之间的速度不匹配,引入了硬件高速缓存内存机制:硬件高速缓存则是基于著名的局部性原理,该原理既适用于程序结构,也适用于数据结构!这表明由于程序的循环结构与相关数组可以组织成线性数组,最近最常用的相邻地址在最近的将来又被用到的可能性极大,因此引入小而快的内存来存放最近最常使用的代码和数据变得很有意义!
高速缓存又被细分为行的子集。在一种极端情况下高速缓存可以是直接映射的这是主存中的一个行,总是存放在高速缓存中完全相同的位置!在另一种极端情况下高速缓存是充分关联的,这意味着主存中的任意一个行,可以存放在高速缓存中的任意位置!
TLB
除了通用硬件高速缓存之外,还有一种转换后缓冲器或TLB的高速缓存用于加快线性地址转换
当线性地址第一次被使用时,通过慢速访问RAM中的页表,计算出相应的物理地址。同时物理地址被存放在一个TLB表象中,以便以后对同一个线性地址的引用时,得到快速的转换。
在多处理器系统中每个CPU都有自己的TLB这叫做CPU的本地TLB。与硬件高速缓存相反TLB的对应项不必同步,这是因为运行在现有CPU上可以使用同一个线性地址与不同的物理地址发生联系!
Linux中的分页
Linux从2.6开始使用四级分页:
-
页全局目录(Page Global Directory)
-
页上级目录(Page Upper Directory)
-
页中间目录(Page Middle Directory)
-
页表(Page Table)
页全局目录包含若干页上级目录的地址;
页上级目录又依次包含若干页中间目录的地址;
而页中间目录又包含若干页表的地址;
每一个页表项指向一个页框。
因此线性地址因此被分成五个部分,而每一部分的大小与具体的计算机体系结构有关。
页表类型定义pgd_t、pmd_t、pud_t和pte_t
Linux分别采用pgd_t
、pmd_t
、pud_t
和pte_t
四种数据结构来表示页全局目录项、页上级目录项、页中间目录项和页表项。这四种数据结构本质上都是无符号长整型unsigned long!
Linux为了更严格数据类型检查,将无符号长整型unsigned long分别封装成四种不同的页表项。如果不采用这种方法,那么一个无符号长整型数据可以传入任何一个与四种页表相关的函数或宏中,这将大大降低程序的健壮性。
pgprot_t是另一个64位(PAE激活时)或32位(PAE禁用时)的数据类型,它表示与一个单独表项相关的保护标志。
首先我们查看一下子这些类型是如何定义的
pteval_t,pmdval_t,pudval_t,pgdval_t
参照arch/x86/include/asm/pgtable_64_types.h
#ifndef __ASSEMBLY__ #include <linux/types.h> /** These are used to make use of C type-checking..*/ typedef unsigned long pteval_t; typedef unsigned long pmdval_t; typedef unsigned long pudval_t; typedef unsigned long pgdval_t; typedef unsigned long pgprotval_t; typedef struct { pteval_t pte; } pte_t; #endif /* !__ASSEMBLY__ */
pgd_t、pmd_t、pud_t和pte_t
参照 /arch/x86/include/asm/pgtable_types.h
typedef struct { pgdval_t pgd; } pgd_t; static inline pgd_t native_make_pgd(pgdval_t val) {return (pgd_t) { val }; } static inline pgdval_t native_pgd_val(pgd_t pgd) {return pgd.pgd; } static inline pgdval_t pgd_flags(pgd_t pgd) {return native_pgd_val(pgd) & PTE_FLAGS_MASK; } #if CONFIG_PGTABLE_LEVELS > 3 typedef struct { pudval_t pud; } pud_t; static inline pud_t native_make_pud(pmdval_t val) {return (pud_t) { val }; } static inline pudval_t native_pud_val(pud_t pud) {return pud.pud; } #else #include <asm-generic/pgtable-nopud.h> static inline pudval_t native_pud_val(pud_t pud) {return native_pgd_val(pud.pgd); } #endif #if CONFIG_PGTABLE_LEVELS > 2 typedef struct { pmdval_t pmd; } pmd_t; static inline pmd_t native_make_pmd(pmdval_t val) {return (pmd_t) { val }; } static inline pmdval_t native_pmd_val(pmd_t pmd) {return pmd.pmd; } #else #include <asm-generic/pgtable-nopmd.h> static inline pmdval_t native_pmd_val(pmd_t pmd) {return native_pgd_val(pmd.pud.pgd); } #endif static inline pudval_t pud_pfn_mask(pud_t pud) {if (native_pud_val(pud) & _PAGE_PSE)return PHYSICAL_PUD_PAGE_MASK;elsereturn PTE_PFN_MASK; } static inline pudval_t pud_flags_mask(pud_t pud) {return ~pud_pfn_mask(pud); } static inline pudval_t pud_flags(pud_t pud) {return native_pud_val(pud) & pud_flags_mask(pud); } static inline pmdval_t pmd_pfn_mask(pmd_t pmd) {if (native_pmd_val(pmd) & _PAGE_PSE)return PHYSICAL_PMD_PAGE_MASK;elsereturn PTE_PFN_MASK; } static inline pmdval_t pmd_flags_mask(pmd_t pmd) {return ~pmd_pfn_mask(pmd); } static inline pmdval_t pmd_flags(pmd_t pmd) {return native_pmd_val(pmd) & pmd_flags_mask(pmd); } static inline pte_t native_make_pte(pteval_t val) {return (pte_t) { .pte = val }; } static inline pteval_t native_pte_val(pte_t pte) {return pte.pte; } static inline pteval_t pte_flags(pte_t pte) {return native_pte_val(pte) & PTE_FLAGS_MASK; }
xxx_val和__xxx
参照/arch/x86/include/asm/pgtable.h
五个类型转换宏(_ pte、_ pmd、_ pud、_ pgd和__ pgprot)把一个无符号整数转换成所需的类型。
另外的五个类型转换宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。
#define pgd_val(x) native_pgd_val(x) #define __pgd(x) native_make_pgd(x) #ifndef __PAGETABLE_PUD_FOLDED #define pud_val(x) native_pud_val(x) #define __pud(x) native_make_pud(x) #endif #ifndef __PAGETABLE_PMD_FOLDED #define pmd_val(x) native_pmd_val(x) #define __pmd(x) native_make_pmd(x) #endif #define pte_val(x) native_pte_val(x) #define __pte(x) native_make_pte(x)
这里需要区别指向页表项的指针和页表项所代表的数据。以pgd_t类型为例子,如果已知一个pgd_t类型的指针pgd,那么通过pgd_val(*pgd)即可获得该页表项(也就是一个无符号长整型数据),这里利用了面向对象的思想。
页表描述宏
参照arch/x86/include/asm/pgtable_64
linux中使用下列宏简化了页表处理,对于每一级页表都使用有以下三个关键描述宏:
宏字段 | 描述 |
---|---|
XXX_SHIFT | 指定Offset字段的位数 |
XXX_SIZE | 页的大小 |
XXX_MASK | 用以屏蔽Offset字段的所有位。 |
我们的四级页表,对应的宏分别由PAGE,PMD,PUD,PGDIR
宏字段前缀 | 描述 |
---|---|
PGDIR | 页全局目录(Page Global Directory) |
PUD | 页上级目录(Page Upper Directory) |
PMD | 页中间目录(Page Middle Directory) |
PAGE | 页表(Page Table) |
PAGE宏–页表(Page Table)
字段 | 描述 |
---|---|
PAGE_SHIFT | 指定Offset字段的位数 |
PAGE_SIZE | 页的大小 |
PAGE_MASK | 用以屏蔽Offset字段的所有位。 |
定义如下,在/arch/x86/include/asm/page_types.h
文件中
/* PAGE_SHIFT determines the page size */#define PAGE_SHIFT 12#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)#define PAGE_MASK (~(PAGE_SIZE-1))
当用于80x86处理器时,PAGE_SHIFT返回的值为12。
由于页内所有地址都必须放在Offset字段, 因此80x86系统的页的大小PAGE_SIZE是2^12=4096
字节。
PAGE_MASK宏产生的值为0xfffff000,用以屏蔽Offset字段的所有位。
PMD-Page Middle Directory (页目录)
字段 | 描述 |
---|---|
PMD_SHIFT | 指定线性地址的Offset和Table字段的总位数;换句话说,是页中间目录项可以映射的区域大小的对数 |
PMD_SIZE | 用于计算由页中间目录的一个单独表项所映射的区域大小,也就是一个页表的大小 |
PMD_MASK | 用于屏蔽Offset字段与Table字段的所有位 |
当PAE 被禁用时,PMD_SHIFT 产生的值为22(来自Offset 的12 位加上来自Table 的10 位), PMD_SIZE 产生的值为222 或 4 MB, PMD_MASK产生的值为 0xffc00000。
相反,当PAE被激活时, PMD_SHIFT 产生的值为21 (来自Offset的12位加上来自Table的9位), PMD_SIZE 产生的值为2^21 或2 MB PMD_MASK产生的值为 0xffe00000。
大型页不使用最后一级页表,所以产生大型页尺寸的LARGE_PAGE_SIZE 宏等于PMD_SIZE(2PMD_SHIFT),而在大型页地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK。
PUD_SHIFT-页上级目录(Page Upper Directory)
字段 | 描述 |
---|---|
PUD_SHIFT | 确定页上级目录项能映射的区域大小的位数 |
PUD_SIZE | 用于计算页全局目录中的一个单独表项所能映射的区域大小。 |
PUD_MASK | 用于屏蔽Offset字段,Table字段,Middle Air字段和Upper Air字段的所有位 |
在80x86处理器上,PUD_SHIFT总是等价于PMD_SHIFT,而PUD_SIZE则等于4MB或2MB。
PGDIR_SHIFT-页全局目录(Page Global Directory)
字段 | 描述 |
---|---|
PGDIR_SHIFT | 确定页全局页目录项能映射的区域大小的位数 |
PGDIR_SIZE | 用于计算页全局目录中一个单独表项所能映射区域的大小 |
PGDIR_MASK | 用于屏蔽Offset, Table,Middle Air及Upper Air的所有位 |
当PAE 被禁止时, PGDIR_SHIFT 产生的值为22(与PMD_SHIFT 和PUD_SHIFT 产生的值相同), PGDIR_SIZE 产生的值为 222 或 4 MB, PGDIR_MASK 产生的值为 0xffc00000。
相反,当PAE被激活时, PGDIR_SHIFT 产生的值为30 (12 位Offset 加 9 位Table再加 9位 Middle Air), PGDIR_SIZE 产生的值为230 或 1 GB PGDIR_MASK产生的值为0xc0000000
PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD
用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024。当PAE被激活时,产生的值分别为512,512,1和4。
页表处理函数
内核还提供了许多宏和函数用于读或修改页表表项:
-
如果相应的表项值为0,那么,宏pte_none、pmd_none、pud_none和 pgd_none产生的值为1,否则产生的值为0。
-
宏pte_clear、pmd_clear、pud_clear和 pgd_clear清除相应页表的一个表项,由此禁止进程使用由该页表项映射的线性地址。ptep_get_and_clear( )函数清除一个页表项并返回前一个值。
-
set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值。set_pte_atomic与set_pte作用相同,但是当PAE被激活时它同样能保证64位的值能被原子地写入。
-
如果a和b两个页表项指向同一页并且指定相同访问优先级,pte_same(a,b)返回1,否则返回0。
-
如果页中间目录项指向一个大型页(2MB或4MB),pmd_large(e)返回1,否则返回0。
宏pmd_bad由函数使用并通过输入参数传递来检查页中间目录项。如果目录项指向一个不能使用的页表,也就是说,如果至少出现以下条件中的一个,则这个宏产生的值为1:
-
页不在主存中(Present标志被清除)。
-
页只允许读访问(Read/Write标志被清除)。
-
Acessed或者Dirty位被清除(对于每个现有的页表,Linux总是 强制设置这些标志)。
pud_bad宏和pgd_bad宏总是产生0。没有定义pte_bad宏,因为页表项引用一个不在主存中的页,一个不可写的页或一个根本无法访问的页都是合法的。
如果一个页表项的Present标志或者Page Size标志等于1,则pte_present宏产生的值为1,否则为0。
前面讲过页表项的Page Size标志对微处理器的分页部件来讲没有意义,然而,对于当前在主存中却又没有读、写或执行权限的页,内核将其Present和Page Size分别标记为0和1。
这样,任何试图对此类页的访问都会引起一个缺页异常,因为页的Present标志被清0,而内核可以通过检查Page Size的值来检测到产生异常并不是因为缺页。
如果相应表项的Present标志等于1,也就是说,如果对应的页或页表被装载入主存,pmd_present宏产生的值为1。pud_present宏和pgd_present宏产生的值总是1。
查询页表项中任意一个标志的当前值
下表中列出的函数用来查询页表项中任意一个标志的当前值;除了pte_file()外,其他函数只有在pte_present返回1的时候,才能正常返回页表项中任意一个标志。
函数名称 | 说明 |
---|---|
pte_user( ) | 读 User/Supervisor 标志 |
pte_read( ) | 读 User/Supervisor 标志(表示 80x86 处理器上的页不受读的保护) |
pte_write( ) | 读 Read/Write 标志 |
pte_exec( ) | 读 User/Supervisor 标志( 80x86 处理器上的页不受代码执行的保护) |
pte_dirty( ) | 读 Dirty 标志 |
pte_young( ) | 读 Accessed 标志 |
pte_file( ) | 读 Dirty 标志(当 Present 标志被清除而 Dirty 标志被设置时,页属于一个非线性磁盘文件映射) |
2.3.2 设置页表项中各标志的值
下表列出的另一组函数用于设置页表项中各标志的值
函数名称 | 说明 |
---|---|
mk_pte_huge( ) | 设置页表项中的 Page Size 和 Present 标志 |
pte_wrprotect( ) | 清除 Read/Write 标志 |
pte_rdprotect( ) | 清除 User/Supervisor 标志 |
pte_exprotect( ) | 清除 User/Supervisor 标志 |
pte_mkwrite( ) | 设置 Read/Write 标志 |
pte_mkread( ) | 设置 User/Supervisor 标志 |
pte_mkexec( ) | 设置 User/Supervisor 标志 |
pte_mkclean( ) | 清除 Dirty 标志 |
pte_mkdirty( ) | 设置 Dirty 标志 |
pte_mkold( ) | 清除 Accessed 标志(把此页标记为未访问) |
pte_mkyoung( ) | 设置 Accessed 标志(把此页标记为访问过) |
pte_modify(p,v) | 把页表项 p 的所有访问权限设置为指定的值 |
ptep_set_wrprotect() | 与 pte_wrprotect( ) 类似,但作用于指向页表项的指针 |
ptep_set_access_flags( ) | 如果 Dirty 标志被设置为 1 则将页的访问权设置为指定的值,并调用flush_tlb_page() 函数 |
ptep_mkdirty() | 与 pte_mkdirty( ) 类似,但作用于指向页表项的指针。 |
ptep_test_and_clear_dirty( ) | 与 pte_mkclean( ) 类似,但作用于指向页表项的指针并返回 Dirty 标志的旧值 |
ptep_test_and_clear_young( ) | 与 pte_mkold( ) 类似,但作用于指向页表项的指针并返回 Accessed标志的旧值 |
宏函数-把一个页地址和一组保护标志组合成页表项,或者执行相反的操作
现在,我们来讨论下表中列出的宏,它们把一个页地址和一组保护标志组合成页表项,或者执行相反的操作,从一个页表项中提取出页地址。请注意这其中的一些宏对页的引用是通过 “页描述符”的线性地址,而不是通过该页本身的线性地址。
宏名称 | 说明 |
---|---|
pgd_index(addr) | 找到线性地址 addr 对应的的目录项在页全局目录中的索引(相对位置) |
pgd_offset(mm, addr) | 接收内存描述符地址 mm 和线性地址 addr 作为参数。这个宏产生地址addr 在页全局目录中相应表项的线性地址;通过内存描述符 mm 内的一个指针可以找到这个页全局目录 |
pgd_offset_k(addr) | 产生主内核页全局目录中的某个项的线性地址,该项对应于地址 addr |
pgd_page(pgd) | 通过页全局目录项 pgd 产生页上级目录所在页框的页描述符地址。在两级或三级分页系统中,该宏等价于 pud_page() ,后者应用于页上级目录项 |
pud_offset(pgd, addr) | 参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址 |
pud_page(pud) | 通过页上级目录项 pud 产生相应的页中间目录的线性地址。在两级分页系统中,该宏等价于 pmd_page() ,后者应用于页中间目录项 |
pmd_index(addr) | 产生线性地址 addr 在页中间目录中所对应目录项的索引(相对位置) |
pmd_offset(pud, addr) | 接收指向页上级目录项的指针 pud 和线性地址 addr 作为参数。这个宏产生目录项 addr 在页中间目录中的偏移地址。在两级或三级分页系统中,它产生 pud ,即页全局目录项的地址 |
pmd_page(pmd) | 通过页中间目录项 pmd 产生相应页表的页描述符地址。在两级或三级分页系统中, pmd 实际上是页全局目录中的一项 |
mk_pte(p,prot) | 接收页描述符地址 p 和一组访问权限 prot 作为参数,并创建相应的页表项 |
pte_index(addr) | 产生线性地址 addr 对应的表项在页表中的索引(相对位置) |
pte_offset_kernel(dir,addr) | 线性地址 addr 在页中间目录 dir 中有一个对应的项,该宏就产生这个对应项,即页表的线性地址。另外,该宏只在主内核页表上使用 |
pte_offset_map(dir, addr) | 接收指向一个页中间目录项的指针 dir 和线性地址 addr 作为参数,它产生与线性地址 addr 相对应的页表项的线性地址。如果页表被保存在高端存储器中,那么内核建立一个临时内核映射,并用 pte_unmap 对它进行释放。 pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它们使用不同的临时内核映射 |
pte_page( x ) | 返回页表项 x 所引用页的描述符地址 |
pte_to_pgoff( pte ) | 从一个页表项的 pte 字段内容中提取出文件偏移量,这个偏移量对应着一个非线性文件内存映射所在的页 |
pgoff_to_pte(offset ) | 为非线性文件内存映射所在的页创建对应页表项的内容 |
简化页表项的创建和撤消
下面我们罗列最后一组函数来简化页表项的创建和撤消。当使用两级页表时,创建或删除一个页中间目录项是不重要的。如本节前部分所述,页中间目录仅含有一个指向下属页表的目录项。所以,页中间目录项只是页全局目录中的一项而已。然而当处理页表时,创建一个页表项可能很复杂,因为包含页表项的那个页表可能就不存在。在这样的情况下,有必要分配一个新页框,把它填写为 0 ,并把这个表项加入。
如果 PAE 被激活,内核使用三级页表。当内核创建一个新的页全局目录时,同时也分配四个相应的页中间目录;只有当父页全局目录被释放时,这四个页中间目录才得以释放。当使用两级或三级分页时,页上级目录项总是被映射为页全局目录中的一个单独项。与以往一样,下表中列出的函数描述是针对 80x86 构架的。
函数名称 | 说明 |
---|---|
pgd_alloc( mm ) | 分配一个新的页全局目录。如果 PAE 被激活,它还分配三个对应用户态线性地址的子页中间目录。参数 mm( 内存描述符的地址 )在 80x86 构架上被忽略 |
pgd_free( pgd) | 释放页全局目录中地址为 pgd 的项。如果 PAE 被激活,它还将释放用户态线性地址对应的三个页中间目录 |
pud_alloc(mm, pgd, addr) | 在两级或三级分页系统下,这个函数什么也不做:它仅仅返回页全局目录项 pgd 的线性地址 |
pud_free(x) | 在两级或三级分页系统下,这个宏什么也不做 |
pmd_alloc(mm, pud, addr) | 定义这个函数以使普通三级分页系统可以为线性地址 addr 分配一个新的页中间目录。如果 PAE 未被激活,这个函数只是返回输入参数 pud 的值,也就是说,返回页全局目录中目录项的地址。如果 PAE 被激活,该函数返回线性地址 addr 对应的页中间目录项的线性地址。参数 mm 被忽略 |
pmd_free(x) | 该函数什么也不做,因为页中间目录的分配和释放是随同它们的父全局目录一同进行的 |
pte_alloc_map(mm, pmd, addr) | 接收页中间目录项的地址 pmd 和线性地址 addr 作为参数,并返回与 addr 对应的页表项的地址。如果页中间目录项为空,该函数通过调用函数 pte_alloc_one( ) 分配一个新页表。如果分配了一个新页表, addr 对应的项就被创建,同时 User/Supervisor 标志被设置为 1 。如果页表被保存在高端内存,则内核建立一个临时内核映射,并用 pte_unmap 对它进行释放 |
pte_alloc_kernel(mm, pmd, addr) | 如果与地址 addr 相关的页中间目录项 pmd 为空,该函数分配一个新页表。然后返回与 addr 相关的页表项的线性地址。该函数仅被主内核页表使用 |
pte_free(pte) | 释放与页描述符指针 pte 相关的页表 |
pte_free_kernel(pte) | 等价于 pte_free( ) ,但由主内核页表使用 |
clear_page_range(mmu, start,end) | 从线性地址 start 到 end 通过反复释放页表和清除页中间目录项来清除进程页表的内容 |
处理硬件高速缓存与TLB
-
void flush_tlb_all(void)
最严格的刷新。在这个接口运行后,任何以前的页表修改都会对cpu可见。
这通常是在内核页表被改变时调用的,因为这种转换在本质上是“全局”的。
-
void flush_tlb_mm(struct mm_struct *mm)
这个接口从TLB中刷新整个用户地址空间。在运行后,这个接口必须确保 以前对地址空间‘mm’的任何页表修改对cpu来说是可见的。也就是说,在 运行后,TLB中不会有‘mm’的页表项。
这个接口被用来处理整个地址空间的页表操作,比如在fork和exec过程 中发生的事情。
-
void flush_tlb_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)
这里我们要从TLB中刷新一个特定范围的(用户)虚拟地址转换。在运行后, 这个接口必须确保以前对‘start’到‘end-1’范围内的地址空间‘vma->vm_mm’ 的任何页表修改对cpu来说是可见的。也就是说,在运行后,TLB中不会有 ‘mm’的页表项用于‘start’到‘end-1’范围内的虚拟地址。
“vma”是用于该区域的备份存储。主要是用于munmap()类型的操作。
提供这个接口是希望端口能够找到一个合适的有效方法来从TLB中删除多 个页面大小的转换,而不是让内核为每个可能被修改的页表项调用 flush_tlb_page(见下文)。
-
void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)
这一次我们需要从TLB中删除PAGE_SIZE大小的转换。‘vma’是Linux用来跟 踪进程的mmap区域的支持结构体,地址空间可以通过vma->vm_mm获得。另 外,可以通过测试(vma->vm_flags & VM_EXEC)来查看这个区域是否是 可执行的(因此在split-tlb类型的设置中可能在“指令TLB”中)。
在运行后,这个接口必须确保之前对用户虚拟地址“addr”的地址空间 “vma->vm_mm”的页表修改对cpu来说是可见的。也就是说,在运行后,TLB 中不会有虚拟地址‘addr’的‘vma->vm_mm’的页表项。
这主要是在故障处理时使用。
-
void update_mmu_cache(struct vm_area_struct *vma, unsigned long address, pte_t *ptep)
在每个缺页异常结束时,这个程序被调用,以告诉体系结构特定的代码,在 软件页表中,在地址空间“vma->vm_mm”的虚拟地址“地址”处,现在存在 一个翻译。
可以用它所选择的任何方式使用这个信息来进行移植。例如,它可以使用这 个事件来为软件管理的TLB配置预装TLB转换。目前sparc64移植就是这么干 的。
接下来,我们有缓存刷新接口。一般来说,当Linux将现有的虚拟->物理映射 改变为新的值时,其顺序将是以下形式之一:
1) flush_cache_mm(mm);change_all_page_tables_of(mm);flush_tlb_mm(mm); 2) flush_cache_range(vma, start, end);change_range_of_page_tables(mm, start, end);flush_tlb_range(vma, start, end); 3) flush_cache_page(vma, addr, pfn);set_pte(pte_pointer, new_pte_val);flush_tlb_page(vma, addr);
缓存级别的刷新将永远是第一位的,因为这允许我们正确处理那些缓存严格, 且在虚拟地址被从缓存中刷新时要求一个虚拟地址的虚拟->物理转换存在的系统。 HyperSparc cpu就是这样一个具有这种属性的cpu。
下面的缓存刷新程序只需要在特定的cpu需要的范围内处理缓存刷新。大多数 情况下,这些程序必须为cpu实现,这些cpu有虚拟索引的缓存,当虚拟->物 理转换被改变或移除时:必须被刷新。因此,例如,IA32处理器的物理索引的物理标记的缓存没有必要实现这些接口,因为这些缓存是完全同步的,并且不依赖于翻译信息。
下面逐个列出这些程序:
-
void flush_cache_mm(struct mm_struct *mm)
这个接口将整个用户地址空间从高速缓存中刷掉。也就是说,在运行后, 将没有与‘mm’相关的缓存行。
这个接口被用来处理整个地址空间的页表操作,比如在退出和执行过程 中发生的事情。
-
void flush_cache_dup_mm(struct mm_struct *mm)
这个接口将整个用户地址空间从高速缓存中刷新掉。也就是说,在运行 后,将没有与‘mm’相关的缓存行。
这个接口被用来处理整个地址空间的页表操作,比如在fork过程中发生 的事情。
这个选项与flush_cache_mm分开,以允许对VIPT缓存进行一些优化。
-
void flush_cache_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)
在这里,我们要从缓存中刷新一个特定范围的(用户)虚拟地址。运行 后,在“start”到“end-1”范围内的虚拟地址的“vma->vm_mm”的缓存中 将没有页表项。
“vma”是被用于该区域的备份存储。主要是用于munmap()类型的操作。
提供这个接口是希望端口能够找到一个合适的有效方法来从缓存中删 除多个页面大小的区域, 而不是让内核为每个可能被修改的页表项调 用 flush_cache_page (见下文)。
-
void flush_cache_page(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn)
这一次我们需要从缓存中删除一个PAGE_SIZE大小的区域。“vma”是 Linux用来跟踪进程的mmap区域的支持结构体,地址空间可以通过 vma->vm_mm获得。另外,我们可以通过测试(vma->vm_flags & VM_EXEC)来查看这个区域是否是可执行的(因此在“Harvard”类 型的缓存布局中可能是在“指令缓存”中)。
“pfn”表示“addr”所对应的物理页框(通过PAGE_SHIFT左移这个 值来获得物理地址)。正是这个映射应该从缓存中删除。
在运行之后,对于虚拟地址‘addr’的‘vma->vm_mm’,在缓存中不会 有任何页表项,它被翻译成‘pfn’。
这主要是在故障处理过程中使用。
-
void flush_cache_kmaps(void)
只有在平台使用高位内存的情况下才需要实现这个程序。它将在所有的 kmaps失效之前被调用。
运行后,内核虚拟地址范围PKMAP_ADDR(0)到PKMAP_ADDR(LAST_PKMAP) 的缓存中将没有页表项。
这个程序应该在asm/highmem.h中实现。
-
void flush_cache_vmap(unsigned long start, unsigned long end)
void flush_cache_vunmap(unsigned long start, unsigned long end)
在这里,在这两个接口中,我们从缓存中刷新一个特定范围的(内核) 虚拟地址。运行后,在“start”到“end-1”范围内的虚拟地址的内核地 址空间的缓存中不会有页表项。
这两个程序中的第一个是在vmap_range()安装了页表项之后调用的。 第二个是在vunmap_range()删除页表项之前调用的。
这是处理页表的一些API:
void copy_user_page(void *to, void *from, unsigned long addr, struct page *page)` `void clear_user_page(void *to, unsigned long addr, struct page *page)
这两个程序在用户匿名或COW页中存储数据。它允许一个端口有效地 避免用户空间和内核之间的D-cache别名问题。
例如,一个端口可以在复制过程中把“from”和“to”暂时映射到内核 的虚拟地址上。这两个页面的虚拟地址的选择方式是,内核的加载/存 储指令发生在虚拟地址上,而这些虚拟地址与用户的页面映射是相同 的“颜色”。例如,Sparc64就使用这种技术。
“addr”参数告诉了用户最终要映射这个页面的虚拟地址,“page”参 数给出了一个指向目标页结构体的指针。
如果D-cache别名不是问题,这两个程序可以简单地直接调用 memcpy/memset而不做其他事情。
void flush_dcache_page(struct page *page)
任何时候,当内核写到一个页面缓存页,或者内核要从一个页面缓存页中读出,并且这个页面的用户空间共享/可写映射可能存在时, 这个程序就会被调用。
这个程序只需要为有可能被映射到用户进程的地址空间的 页面缓存调用。因此,例如,处理页面缓存中vfs符号链 接的VFS层代码根本不需要调用这个接口。“内核写入页面缓存的页面”这句话的意思是,具体来说,内核执行存 储指令,在该页面的页面->虚拟映射处弄脏该页面的数据。在这里,通 过刷新的手段处理D-cache的别名是很重要的,以确保这些内核存储对 该页的用户空间映射是可见的。推论的情况也同样重要,如果有用户对这个文件有共享+可写的映射, 我们必须确保内核对这些页面的读取会看到用户所做的最新的存储。
如果D-cache别名不是一个问题,这个程序可以简单地定义为该架构上 的nop。在page->flags (PG_arch_1)中有一个位是“架构私有”。内核保证, 对于分页缓存的页面,当这样的页面第一次进入分页缓存时,它将清除这个位。这使得这些接口可以更有效地被实现。如果目前没有用户进程映射这个 页面,它允许我们“推迟”(也许是无限期)实际的刷新过程。请看 sparc64的flush_dcache_page和update_mmu_cache实现,以了解如 何做到这一点。
这个想法是,首先在flush_dcache_page()时,如果page->mapping->i_mmap 是一个空树,只需标记架构私有页标志位。之后,在update_mmu_cache() 中,会对这个标志位进行检查,如果设置了,就进行刷新,并清除标志位。通常很重要的是,如果你推迟刷新,实际的刷新发生在同一个 CPU上,因为它将cpu存储到页面上,使其变脏。同样,请看 sparc64关于如何处理这个问题的例子。
void flush_dcache_folio(struct folio *folio)
该函数的调用情形与flush_dcache_page()相同。它允许架构针对刷新整个 folio页面进行优化,而不是一次刷新一页。
void copy_to_user_page(struct vm_area_struct *vma, struct page *page, unsigned long user_vaddr, void *dst, void *src, int len)` `void copy_from_user_page(struct vm_area_struct *vma, struct page *page, unsigned long user_vaddr, void *dst, void *src, int len)
当内核需要复制任意的数据进出任意的用户页时(比如ptrace()),它将使 用这两个程序。
任何必要的缓存刷新或其他需要发生的一致性操作都应该在这里发生。如果 处理器的指令缓存没有对cpu存储进行窥探,那么你很可能需要为 copy_to_user_page()刷新指令缓存。
void flush_anon_page(struct vm_area_struct *vma, struct page *page, unsigned long vmaddr)
当内核需要访问一个匿名页的内容时,它会调用这个函数(目前只有 get_user_pages())。注意:flush_dcache_page()故意对匿名页不起作 用。默认的实现是nop(对于所有相干的架构应该保持这样)。对于不一致性 的架构,它应该刷新vmaddr处的页面缓存。
void flush_icache_range(unsigned long start, unsigned long end)
当内核存储到它将执行的地址中时(例如在加载模块时),这个函数被调用。
如果icache不对存储进行窥探,那么这个程序将需要对其进行刷新。
void flush_icache_page(struct vm_area_struct *vma, struct page *page)
flush_icache_page的所有功能都可以在flush_dcache_page和update_mmu_cache 中实现。在未来,我们希望能够完全删除这个接口。
最后一类API是用于I/O到内核内特意设置的别名地址范围。这种别名是通过使用 vmap/vmalloc API设置的。由于内核I/O是通过物理页进行的,I/O子系统假定用户 映射和内核偏移映射是唯一的别名。这对vmap别名来说是不正确的,所以内核中任何 试图对vmap区域进行I/O的东西都必须手动管理一致性。它必须在做I/O之前刷新vmap 范围,并在I/O返回后使其失效。
void flush_kernel_vmap_range(void *vaddr, int size)刷新vmap区域中指定的虚拟地址范围的内核缓存。这是为了确保内核在vmap范围 内修改的任何数据对物理页是可见的。这个设计是为了使这个区域可以安全地执 行I/O。注意,这个API并 没有 刷新该区域的偏移映射别名。
void invalidate_kernel_vmap_range(void *vaddr, int size) invalidates在vmap区域的一个给定的虚拟地址范围的缓存,这可以防止处理器在物理页的I/O 发生时通过投机性地读取数据而使缓存变脏。这只对读入vmap区域的数据是必要的。
Reference
物理地址扩展(PAE)分页机制 - 冷烟花 - 博客园 (cnblogs。com)
PAE 分页模式详解 - jack。chen - 博客园 (cnblogs。com)
Linux分页机制之分页机制的实现详解--Linux内存管理(八) - yooooooo - 博客园 (cnblogs.com)
Linux下的缓存和TLB刷新 — The Linux Kernel documentation