Linux页表机制分析.docx
《Linux页表机制分析.docx》由会员分享,可在线阅读,更多相关《Linux页表机制分析.docx(30页珍藏版)》请在冰豆网上搜索。
![Linux页表机制分析.docx](https://file1.bdocx.com/fileroot1/2022-12/10/58d5f7f0-d964-4882-a42b-bae021eca3d5/58d5f7f0-d964-4882-a42b-bae021eca3d51.gif)
Linux页表机制分析
12. 页表机制
12.1. 引言
在Linux系统中,存在以下三种地址:
∙逻辑地址:
它被包含在机器指令中用来指定一个操作数或一条指令的地址。
每一个逻辑地址都由一个段(Segment)和偏移量(Offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
∙线性地址(虚拟地址):
一个32位无符号整数,可以用来表示高达4GB的地址。
线性地址通常用十六进制数字表示,值的范围为[0x00000000,0xffffffff)。
∙物理地址:
用于内存芯片级内存单元寻址。
它们与从CPU的地址引脚发送到内存总线上的电信号相对应。
物理地址由32位或36位无符号整数表示。
内存控制单元(MMU)通过一种称为分段单元的硬件电路把一个逻辑地址转换成线性地址;称为分页单元的硬件电路把线性地址转换成一个物理地址。
有些MMU没有分页单元,或者禁止使能分页单元,比如x86的实模式,那么就只有分段单元,那么经过分段单元转换后的地址就是物理地址。
有些MMU没有分段单元,大多数RISC架构的CPU就是如此,此时段基址相当于0,而代码中的偏移地址就是线性地址,所有Linux下逻辑地址和线性地址是一致的。
如下图所示:
图 60. 地址转换
Linux中以非常有限的方式使用分段。
运行在用户态的所有Linux进程都使用一对相同的段来对指令和数据寻址,它们的段基址分别是__USER_CS和__USER_DS。
与此同时,运行在内核态的所有Linux进程(内核线程)都使用一对相同的段对指令和数据寻址,它们的段基址分别__KERNEL_CS和__KERNEL_DS。
分段可以给每个进程分配不同的线性地址空间,分页可以把同一线性地址空间映射到不同的物理空间。
与分段相比,Linux更喜欢分页方式,因为:
∙当所有进程使用相同的段寄存器值时,内存管理变得更简单,也就是说它们能共享同样的一簇线性地址。
∙为了兼容绝大多数的CPU,RISC体系架构对分段的支持很有限,比如ARM架构的CPU中的MMU单元通常只支持分页,而不支持分段。
分页使得不同的虚拟内存页可以转入同一物理页框。
于此同时分页机制可以实现对每个页面的访问控制,这是在平衡内存使用效率和地址转换效率之间做出的选择。
如果4G的虚拟空间,每一个字节都要使用一个数据结构来记录它的访问控制信息,那么显然是不可能的。
如果把4G的虚拟空间以4K(为什么是4K大小?
这是由于Linux中的可执行文件中的代码段和数据段等都是相对于4K对齐的)大小分割成若干个不同的页面,那么每个页面需要一个数据结构进行控制,只需要1M的内存来存储。
但是由于每一个用户进程都有自己的独立空间,所以每一个进程都需要一个1M的内存来存储页表信息,这依然是对系统内存的浪费,采用两级甚至多级分页是一种不错的解决方案。
另外有些处理器采用64位体系架构,此时两级也不合适了,所以Linux使用三级页表。
∙页全局目录(PageGlobalDirectory),即pgd,是多级页表的抽象最高层。
每一级的页表都处理不同大小的内存。
每项都指向一个更小目录的低级表,因此pgd就是一个页表目录。
当代码遍历这个结构时(有些驱动程序就要这样做),就称为是在遍历页表。
∙页中间目录(PageMiddleDirectory),即pmd,是页表的中间层。
在x86架构上,pmd在硬件中并不存在,但是在内核代码中它是与pgd合并在一起的。
∙页表条目(PageTableEntry),即pte,是页表的最低层,它直接处理页,该值包含某页的物理地址,还包含了说明该条目是否有效及相关页是否在物理内存中的位。
12.2. 一级页表
三级页表由不同的的数据结构表示,它们分别是pgd_t,pmd_t和pte_t。
注意到它们均被定义为unsignedlong类型,也即大小为4bytes,32bits。
arch/arm/include/asm/page.h
typedefunsignedlongpte_t;
typedefunsignedlongpmd_t;
typedefunsignedlongpgd_t[2];
typedefunsignedlongpgprot_t;
以下是页表操作相关的宏定义。
#definepte_val(x)(x)
#definepmd_val(x)(x)
#definepgd_val(x)((x)[0])
#definepgprot_val(x)(x)
#define__pte(x)(x)
#define__pmd(x)(x)
#define__pgprot(x)(x)
任何一个用户进程都有自己的页表,与此同时,内核本身就是一个名为init_task的0号进程,每一个进程都有一个mm_struct结构管理进程的内存空间,init_mm是内核的mm_struct。
在系统引导阶段,首先通过__create_page_tables在内核代码的起始处_stext向低地址方向预留16K,用于一级页表(主内存页表)的存放,每个进程的页表都通过mm_struct中的pgd描述符进行引用。
内核页表被定义在swapper_pg_dir。
arch/arm/kernel/init_task.c
#defineINIT_MM(name)\
{\
.mm_rb=RB_ROOT,\
.pgd=swapper_pg_dir,\
.mm_users=ATOMIC_INIT
(2),\
.mm_count=ATOMIC_INIT
(1),\
.mmap_sem=__RWSEM_INITIALIZER(name.mmap_sem),\
.page_table_lock=__SPIN_LOCK_UNLOCKED(name.page_table_lock),\
.mmlist=LIST_HEAD_INIT(name.mmlist),\
.cpu_vm_mask=CPU_MASK_ALL,\
}
structmm_structinit_mm=INIT_MM(init_mm);
swapper_pg_dir在head.S中被定义为PAGE_OFFSET向上偏移TEXT_OFFSET。
TEXT_OFFSET代表内核代码段的相对于PAGE_OFFSET的偏移。
KERNEL_RAM_VADDR的值与_stext的值相同,代表了内核代码的起始地址。
swapper_pg_dir为KERNEL_RAM_VADDR-0x4000,也即向低地址方向偏移了16K。
arch/arm/Makefile
textofs-y:
=0x00008000
......
TEXT_OFFSET:
=$(textofs-y)
特定系统架构的Makefile中通过textofs-y定义了内核起始代码相对于PAGE_OFFSET的偏移。
arch/arm/kernel/head.S
#defineKERNEL_RAM_VADDR(PAGE_OFFSET+TEXT_OFFSET)
......
.globlswapper_pg_dir
.equswapper_pg_dir,KERNEL_RAM_VADDR-0x4000
ARMLinux中的主内存页表,使用段表。
每个页表映射1M的内存大小,由于16K/4*1M=4G,这16K的主页表空间正好映射4G的虚拟空间。
内核页表机制在系统启动过程中的paging_init函数中使能,其中对内核主页表的初始化等操作均是通过init_mm.pgd的引用来进行的。
在系统执行paging_init之前,系统的地址空间如下图所示:
图 61. 内核RAM布局
图中的黄色部分就是内核0号进程的主页表。
arch/arm/mm/mmu.c
void__initpaging_init(structmeminfo*mi,structmachine_desc*mdesc)
{
void*zero_page;
build_mem_type_table();
sanity_check_meminfo(mi);
prepare_page_table(mi);
bootmem_init(mi);
devicemaps_init(mdesc);
top_pmd=pmd_off_k(0xffff0000);
zero_page=alloc_bootmem_low_pages(PAGE_SIZE);
memzero(zero_page,PAGE_SIZE);
empty_zero_page=virt_to_page(zero_page);
flush_dcache_page(empty_zero_page);
}
图 62. ARM内存主页表初始化
paging_init依次完成了以下工作:
∙调用prepare_page_table初始化虚拟地址[0,PAGE_OFFSET]和[mi->bank[0].start+mi->bank[0].size,VMALLOC_END]所对应的主页表项,所有表项均初始化为0。
这里保留了内核代码区,主页表区以及Bootmem机制中的位图映射区对应的主页表。
这是为了保证内核代码的执行以及对主页表区和位图区的访问。
如果只有一个内存bank,那么mi->bank[0].start+mi->bank[0].size的值和high_memory保持一致,它是当前bank进行物理内存一一映射后的虚拟地址。
对于一个内存为256M的系统来说,它只有一个bank,经过prepare_page_table处理后的内存如上图所示。
∙接着在bootmem_init函数将通过bootmem_init_node对每一个内存bank的页表进行值的填充。
bootmem_init_node将通过map_memory_bank间接调用create_mapping,最终由该函数创建页表。
∙通过devicemaps_init初始化设备I/O对应的相关页表。
∙最后创建0页表,并在Dcache中清空0页表的缓存信息。
图 63. 页表创建函数调用
12.3. ARM内存访问
当ARM要访问内存RAM时,MMU首先查找TLB中的虚拟地址表,如果ARM的结构支持分开的地址TLB和指令TLB,那么它用:
∙取指令使用指令TLB
∙其它的所有访问类别用数据TLB
指令TLB和数据TLB在ARMv6架构的MMU中被分别称为指令MicroTLB和数据MicroTLB。
如果没有命中MicroTLB,那么将查询主TLB,此时不区分指令和数据TLB。
如果TLB中没有虚拟地址的入口,则转换表遍历硬件从存在主存储器中的转换表中获取转换页表项,它包含了物理地址或者二级页表地址和访问权限,一旦取到,这些信息将被放在TLB中,它会放在一个没有使用的入口处或覆盖一个已有的入口。
一旦为存储器访问的TLB的入口被拿到,这些信息将被用于:
∙C(高速缓存)和B(缓冲)位被用来控制高速缓存和写缓冲,并决定是否高速缓存。
∙首先检查域位,然后检查访问权限位用来控制访问是否被允许。
如果不允许,则MMU将向ARM处理器发送一个存储器异常;否则访问将被允许进行。
∙对没有或者禁止高速缓存的系统(包括在没有高速缓存系统中的所有存储器访问),物理地址将被用作主存储器访问的地址。
图 64. 高速缓存的MMU存储器系统
12.4. ARMMMU页表
在ARMv6的MMU机制中,提供了两种格式的页表描述符:
∙兼容ARMv4和ARMv5MMU机制的页表描述符。
这种描述符可以对64K大页面和4K小页面再进一步细分为子页面。
∙ARMv6特有的MMU页表描述符,这种页表描述符内增加了额外的特定比特位:
Not-Global(nG),Shared(S),Execute-Never(XN)和扩展的访问控制位APX。
图 65. 向前兼容的一级页表描述符格式
图 66. 向前兼容的二级页表描述符格式
Linux使用ARMv6特有的MMU页表描述符格式,它们的标志位描述如下:
图 67. ARMv6一级页表描述符格式
表 20. ARMv6一级页表描述符比特位含义
位
标志
含义
b[1:
0]
类型
指示页表类型:
b00错误项;b11保留;b01粗页表,它指向二级页表基址。
b10:
1MB大小段页表(b[18]置0)或16M大小超级段页表(b[18]置1)
b[2]
B[a]
写缓冲使能[b]
b[4]
Execute-Never(XN)
禁止执行标志:
1,禁止执行;0:
可执行
b[5:
8]
域(domain)
指明所属16个域的哪个域,访问权限由CP15的c3寄存器据定
b[9]
P(ECCEnable)
ECC使能标志,1:
该页表映射区使能ECC校验[c]
b[10:
11]
AP(AccessPermissions)
访问权限位,具体见访问权限列表
b[12:
14]
TEX(TypeExtensionField)
扩展类型,与B,C标志协同控制内存访问类型
bit[15]
APX(AccessPermissionsExtensionBit)
扩展访问权限位
bit[16]
S(Shared)
共享访问
bit[17]
nG(Not-Global)
全局访问
bit[18]
0/1
段页表和超级段页表开关
bit[19]
NS
[a]高速缓存和写缓存的引入是基于如下事实,即处理器速度远远高于存储器访问速度;如果存储器访问成为系统性能的瓶颈,则处理器再快也是浪费,因为处理器需要耗费大量的时间在等待存储器上面。
高速缓存正是用来解决这个问题,它可以存储最近常用的代码和数据,以最快的速度提供给CPU处理(CPU访问Cache不需要等待)。
[b]SBZ意味置0,该位在粗页表中置0。
[c]ARM1176JZF-S处理器不支持该标志位。
Linux在ARM体系架构的Hardwarepagetable头文件中通过宏定义了这些位。
arch/arm/include/asm/pgtable-hwdef.h
/*
*Hardwarepagetabledefinitions.
*
*+Level1descriptor(PMD)
*-common
*/
#definePMD_TYPE_MASK(3<<0)//获取一级页表类型的掩码,它取bit[0:
1]
#definePMD_TYPE_FAULT(0<<0)//置bit[0:
1]为b00,错误项
#definePMD_TYPE_TABLE(1<<0)//置bit[0:
1]为b01,粗页表
#definePMD_TYPE_SECT(2<<0)//置bit[0:
1]为b10,段页表
#definePMD_BIT4(1<<4)//定义bit[4],禁止执行标志位
#definePMD_DOMAIN(x)((x)<<5)//获取域标志位b[5:
8]
#definePMD_PROTECTION(1<<9)//b[9]ECC使能标志
以上定义了一级页表的相关标志位。
Linux使用段页表作为一级页表,粗页表作为二级页表的基址页表。
段页表的标志位定义如下:
#definePMD_SECT_BUFFERABLE(1<<2)
#definePMD_SECT_CACHEABLE(1<<3)
#definePMD_SECT_XN(1<<4)/*v6*/
#definePMD_SECT_AP_WRITE(1<<10)
#definePMD_SECT_AP_READ(1<<11)
#definePMD_SECT_TEX(x)((x)<<12)/*v5*/
#definePMD_SECT_APX(1<<15)/*v6*/
#definePMD_SECT_S(1<<16)/*v6*/
#definePMD_SECT_nG(1<<17)/*v6*/
#definePMD_SECT_SUPER(1<<18)/*v6*/
图 68. ARMv6二级页表基址格式
二级页表相同标志位的含义与一级页表相同,这里不再单独列出。
注意它的b[1]为1时,b[0]表示禁止执行标志。
Linux对二级页表中的标志位定义如下:
/*
*+Level2descriptor(PTE)
*-common
*/
#definePTE_TYPE_MASK(3<<0)//获取二级页表类型的掩码,它取bit[0:
1]
#definePTE_TYPE_FAULT(0<<0)//置bit[0:
1]为b00,错误项
#definePTE_TYPE_LARGE(1<<0)//置bit[0:
1]为b01,大页表(64K)
#definePTE_TYPE_SMALL(2<<0)//置bit[0:
1]为b10,扩展小页表(4K)
#definePTE_TYPE_EXT(3<<0)//使能禁止执行标志的扩展小页表(4K)
#definePTE_BUFFERABLE(1<<2)//B标志
#definePTE_CACHEABLE(1<<3)//C标志
Linux二级页表使用扩展小页表,这样每个二级页表可以表示通常的1个页面大小(4K)。
Linux对二级页表标志位的定义如下:
/*
*-extendedsmallpage/tinypage
*/
#definePTE_EXT_XN(1<<0)/*v6*/
#definePTE_EXT_AP_MASK(3<<4)
#definePTE_EXT_AP0(1<<4)
#definePTE_EXT_AP1(2<<4)
#definePTE_EXT_AP_UNO_SRO(0<<4)
#definePTE_EXT_AP_UNO_SRW(PTE_EXT_AP0)
#definePTE_EXT_AP_URO_SRW(PTE_EXT_AP1)
#definePTE_EXT_AP_URW_SRW(PTE_EXT_AP1|PTE_EXT_AP0)
#definePTE_EXT_TEX(x)((x)<<6)/*v5*/
#definePTE_EXT_APX(1<<9)/*v6*/
#definePTE_EXT_COHERENT(1<<9)/*XScale3*/
#definePTE_EXT_SHARED(1<<10)/*v6*/
#definePTE_EXT_NG(1<<11)/*v6*/
以上两种页表转换机制由CP15协处理器的控制寄存器c1中的bit23来选择。
bit23为0时为第一种机制,否则为第二种。
在CPU初始化后该位的默认值为0。
Linux在系统引导时会设置MMU的控制寄存器的相关位,其中把bit23设置为1,所以Linux在ARMv6体系架构上采用的是ARMv6MMU页表转换机制。
arch/arm/mm/proc-v6.S
__v6_setup:
......
adrr5,v6_crval
ldmiar5,{r5,r6}
mrcp15,0,r0,c1,c0,0@readcontrolregister
bicr0,r0,r5@clearbitsthem
orrr0,r0,r6@setthem
movpc,lr@returntohead.S:
__ret
/*
*VXFIDLR
*.......EPUI..T.T4RVIZFRSBLDPWCAM
*rrrrrrrxxxx00101xxxxxxxxx111xxxx*011000111.00.1111101*/
.typev6_crval,#object
v6_crval:
crvalclear=0x01e0fb7f,mmuset=0x00c0387d,ucset=0x00c0187c
注意到v6_crval定义了三个常量,首先mrc指令读取c1到r0,然后清除clear常量指定的比特位,然后设置mmuset指定的比特位,其中bit23为1。
在movpc,lr跳转后将执行定义在head.S中的__enable_mmu函数,在进一步调节其它的比特位后最终将把r0中的值写回c1寄存器。
12.5. 页面访问控制
在谈到create_mapping之前,必须说明一下Linux是如何实现对页面的访问控制的。
它定义了一个类型为structmem_type的局部静态数组。
根据不同的映射类型,它定义了不同的访问权限,它通过md参数中的type成员传递给create_mapping。
arch/arm/include/asm/io.h
/*
*Architectureioremapimplementation.
*/
#defineMT_DEVICE0
#defineMT_DEVICE_NONSHARED1
#defineMT_DEVICE_CACHED2
#defineMT_DEVICE_WC3
arch/arm/include/asm/mach/map.h
/*types0-3aredefinedinasm/io.h*/
#defineMT_UNCACHED4
#defineMT_CACHECLEAN5
#defineMT_MINICLEAN6
#defineMT_LOW_VECTORS7
#defineMT_HIGH_VECTORS8
#defineMT_MEMORY9
#defineMT_ROM10
系统中定义了多个映射类型,最常用的是MT_MEMORY,它对应RAM;MT_DEVICE则对应了其他I/O设备,应用于ioremap;MT_ROM对应于ROM;MT_LOW_VECTORS对应0地址开始的向量;MT_HIGH_VECTORS对应高地址开始的向量,它有vector_base宏决定。
arch/arm/mm/mm.h
structmem_type{
unsignedintprot_pte;
unsignedintprot_l1;
unsignedintprot_sect;
unsignedintdomain;
};
尽管Linux在多数系统上实现或者模拟了3级页表,但是在ARMLinux上它只实现了主页表和两级页表。
主页表通过ARMCPU的段表实现,段表中的每个页表项管理1M的内存,虚拟地址只需要一次转换既可以得到物