Linux内核页表映射分页机制原理

深入理解Linux内核页表映射分页机制原理

一、寻址机制

​ 最初8086处理器使用的是实地址,后来Intel为解决地址宽度不足的问题从而引入分段机制,再后来为进一步保护数据又引入分页机制,从而衍生出MMU、CRn等寄存器和物理单元,演变为至今的分段加分页的寻址系统

在这里插入图片描述

1.1 段式寻址

​ 当起初8086寻址范围64K太小,于是Intel将其扩展到1MB,即20位地址宽度。为此Intel发明了一种巧妙的方法,即分段。在CPU中设置了四个段寄存器:CS、DS、SS、ES,用于访问指令、数据、堆栈和其他。将内存对应划分为多段,用段寄存器配合偏移量来完成寻址。

1.1.1 实模式:

8086处理器上写汇编语言时,访问一个内存的两步:1.将段地址写入DS寄存器,将偏移量写入BX寄存器。2.使用[DS:BX]组合完成寻址,这就是段式寻址。此时DS:BX的组合称为逻辑地址,经过分段单元的硬件电路转化为线性地址。

缺点:

​ 这种寻址方式存在安全风险,任何进程都能访问所有地址空间(实模式)

1.1.2 保护模式:

在这里插入图片描述

  • 保护模式下段寄存器中不再是段地址,而是一个段选择子
  • 逻辑地址由16位段选择符和32位偏移量组成,段选择符存放段寄存器中。有六个段寄存器,分别是cs,ss,ds,es,fs和gs。每个段选择符有一个TI位表示是哪个描述符表,RPL位表示访问权限,13位索引号字段表示是段描述符表中的哪一个。
  • 有两类段描述符表:GDT和LDT;GDT的地址和大小再gdtr控制寄存器定义,LDT在ldtr控制寄存器中定义
  • 此时访问一个地址,先将该地址所在段的段选择子放入段寄存器,根据索引字段找到段描述符,找到段基址,再加上偏移量,就转换成了线性地址;在这个过程中,通过段访问权限,可以控制进程无法访问非法地址
    在这里插入图片描述

1.2 页式寻址

1.2.1 分页机制作用
  • 将线性地址转换成物理地址
  • 将大小不同的大内存段拆分为大小相等的小内存块
1.2.2 一级页表

​ 32位地址表示4GB空间,CPU采用的页大小定为4KB,那么4GB地址空间被划分为4GB/4KB=1M个页;那么4GB地址空间可以将32位地址分为高低两部分;虚拟地址高20位用来索引一个页,低12位用来页内寻址。

在这里插入图片描述

线性地址转换为物理地址

​ 线性地址高20位作为页表项的索引,每个页表项占用4字节大小,故高20位的索引乘以4后才是该页表项相对于页表物理地址的字节偏移量。用CR3寄存器中的页表物理地址加上此偏移量便是该页表项的物理地址,从该页表项中得到映射的物理地址,然后再用线性地址的低12位与该物理页地址相加,所得地址之和便是最终要访问的物理地址。
在这里插入图片描述

1.2.3 二级页表
  • 将每个页表的物理地址存放在页目录表中都以页目录项(PDE)的形式存储,页目录项大小同页表项一样都为4KB,PDE用来描述一个物理页的物理地址

  • 页目录表和所有页表都放在物理内存中
    在这里插入图片描述

  • 页目录项和页表项

    页目录项和页表项都是4字节大小,用来存储物理页地址。具体结构如下图所示:
    在这里插入图片描述

二级页表的地址转换原理

二级页表地址转换原理是将32位虚拟地址拆分为高10位、中间10位、低12位三部分;

  • 高10位作为页表的索引,用于在页目录表中定为一个页目录项PDE,页目录项中有页表的物理地址,也就是定位到了某个页表
  • 中间10位作为物理页的索引,用于在页表内定位到某个页表项篇TE,页表项中有分配的物理页地址,也就是定位到了某个物理页
  • 低12位作为页内偏移量用于在已经定位到的物理页寻址

在这里插入图片描述

1.2.4 启动分页机制的3个步骤:

1)准备好页目录表及页表

2)将页表地址写入控制寄存器CR3

3)寄存器CR0的PG位置1

二、分页管理代码分析

2.1 X86架构下的分页管理分析

以下是内核Linux5.6.4版本四级分页模型介绍:

2.1.1 PGDIR_SHIFT及相关的宏
  • 表示线性地址中的offset字段,Table字段,Middle Dir字段和Upper Dir 字段,PGDIR_SIZE用于计算页全局目录中一个表项能映射区域的大小。PGDIR_MASK用于屏蔽线性地址中Middle Dir字段、Table字段和offset字段所在位。
  • 在四级分页模型中,PGDIR_SHIFT占据39位,即9位页上级目录、9位页中间目录、9位页表和12位偏移。页全局目录同样占线性地址的9位,因此PTRS_PER_PGD(表示的是PGD对应的页表中有多少个表项)为512。
arch/x86/include/asm/pgtable_64_types.h

#define PGDIR_SHIFT 39

#define PTRS_PER_PGD 512

#define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT)

#define PGDIR_MASK (~(PGDIR_SIZE - 1))

pgd_offset
该函数返回线性地址address在页全局目录中对应表项的线性地址。mm为指向一个内存描述符的指针,address为要转换的线性地址。该宏最终返回address在页全局目录中相应表项的线性地址。

#define pgd_index(address)	(((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
#define pgd_offset(mm, address)	((mm)->pgd+pgd_index(address))

2.1.2 PUD_SHIFT及相关的宏
  • 表示线性地址中offset字段、Table字段和Middle Dir字段的位数。PUD_SIZE用于计算页上级目录一个表项映射的区域大小,PUD_MASK用于屏蔽线性地址中Middle Dir字段、Table字段和offset字段所在位。
  • 在64位系统四级分页模型下,PUD_SHIFT的大小为30,包括12位的offset字段、9位Table字段和9位Middle Dir字段。由于页上级目录在线性地址中占9位,因此页上级目录的表项数为512。

arch/x86/include/asm/pgtable_64_types.h

#define PUD_SHIFT 30

#define PTRS_PER_PUD 512

#define PUD_SIZE        (_AC(1, UL) << PUD_SHIFT)

#define PUD_MASK        (~(PUD_SIZE - 1))

pud_offset

该函数与pgd_offset类似,最终得到address对应的页上级目录项的线性地址。

#define pud_offset(dir,addr) \

	((pud_t *) pgd_page_vaddr(*(dir)) + (((addr) >> PUD_SHIFT) & (PTRS_PER_PUD - 1)))

#endif
2.1.3 PMD_SHIFT及相关宏
  • 表示线性地址中offset字段和Table字段的位数,2的PMD_SHIFT次幂表示一个页中间目录项可以映射的内存区域大小。PMD_SIZE用于计算这个区域的大小,PMD_MASK用来屏蔽offset字段和Table字段的所有位。PTRS_PER_PMD表示页中间目录中表项的个数。
  • 在64位系统中,Linux采用四级分页模型。线性地址包含页全局目录、页上级目录、页中间目录、页表和偏移量五部分。在这两种模型中PMD_SHIFT占21位,即包括Table字段的9位和offset字段的12位。PTRS_PER_PMD的值为512,即2的9次幂,表示页中间目录包含的表项个数。
#define PMD_SHIFT 21

#define PTRS_PER_PMD 512

#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT)

#define PMD_MASK (~(PMD_SIZE - 1))

pmd_offset

该函数返回address在页中间目录中对应表项的线性地址。

2.1.4 PAGE_SHIFT及相关宏
  • 表示线性地址offset字段的位数。该宏的值被定义为12位,即页的大小为4KB。与它对应的宏有PAGE_SIZE,它返回一个页的大小;PAGE_MASK用来屏蔽offset字段,其值为oxfffff000。PTRS_PER_PTE表明页表在线性地址中占据9位。
  • 通过上面的分析可知,在x86-64架构下64位的线性地址被划分为五部分,每部分占据的位数分别为9,9,9,9,12,实际上只用了64位中的48位。对于四级页表而言,级别从高到底每级页表中表项的个数为512,512,512,512。

2.2 ARM架构下的分页管理分析

2.2.1 虚拟地址到物理地址的转换

ARMv8中,Kernel Space的页表基地址存放在TTBR1_EL1寄存器中,User Space页表基地址存放在TTBR0_EL0寄存器中,其中内核地址空间的高位为全1,(0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF),用户地址空间的高位为全0,(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF)

结合有效虚拟地址位, 页面大小,页表的级数,可以组合成不同的页表映射方式。以下以内核配置为:39位有效位,4KB大小页面,3级页表来介绍

在这里插入图片描述

  1. 虚拟地址[63:39]用于区分内核空间与用户空间,从而选择不同的TTBRn寄存器来获取Level 1页表基地址;
  2. 虚拟地址[38:30]放置Level 1页表中的索引,从而找到对应的描述符地址并获取描述符内容,根据描述符中的内容获取Level 2页表基地址;
  3. 虚拟地址[29:21]Level 2页表中的索引,从而找到对应的描述符地址并获取描述符内容,根据描述符中的内容获取Level 3页表基地址;
  4. 虚拟地址[20:12]Level 3页表中的索引,从而找到对应的描述符地址并获取描述符内容,根据描述符中的内容获取物理地址的高36位,以4K地址对齐;
  5. 虚拟地址[11:0]放置的是物理地址的偏移,结合获取的物理地址高位,最终得到物理地址。
2.2.2 Linux页表映射

内核中关于页表的操作如图所示:
在这里插入图片描述

代码路径:

arch/arm64/include/asm/pgtable-types.h:定义pgd_t, pud_t, pmd_t, pte_t等类型;
arch/arm64/include/asm/pgtable-prot.h:针对页表中entry中的权限内容设置;
arch/arm64/include/asm/pgtable-hwdef.h:主要包括虚拟地址中PGD/PMD/PUD等的划分,这个与虚拟地址的有效位及分页大小有关,此外还包括硬件页表的定义, TCR寄存器中的设置等;
arch/arm64/include/asm/pgtable.h:页表设置相关;

在这些代码中可以看到,

  • CONFIG_PGTABLE_LEVELS=4时:pgd-->pud-->pmd-->pte;
  • CONFIG_PGTABLE_LEVELS=3时,没有PUD页表:pgd(pud)-->pmd-->pte;
  • CONFIG_PGTABLE_LEVELS=2时,没有PUDPMD页表:pgd(pud, pmd)-->pte

常用的宏定义
在这里插入图片描述

页表处理

/*描述各级页表中的页表项*/
typedef struct { pteval_t pte; } pte_t;
typedef struct { pmdval_t pmd; } pmd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pgdval_t pgd; } pgd_t;

/*  将页表项类型转换成无符号类型 */
#define pte_val(x)	((x).pte)
#define pmd_val(x)	((x).pmd)
#define pud_val(x)	((x).pud)
#define pgd_val(x)	((x).pgd)

/*  将无符号类型转换成页表项类型 */
#define __pte(x)	((pte_t) { (x) } )
#define __pmd(x)	((pmd_t) { (x) } )
#define __pud(x)	((pud_t) { (x) } )
#define __pgd(x)	((pgd_t) { (x) } )

/* 获取页表项的索引值 */
#define pgd_index(addr)		(((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#define pud_index(addr)		(((addr) >> PUD_SHIFT) & (PTRS_PER_PUD - 1))
#define pmd_index(addr)		(((addr) >> PMD_SHIFT) & (PTRS_PER_PMD - 1))
#define pte_index(addr)		(((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))

/*  获取页表中entry的偏移值 */
#define pgd_offset(mm, addr)	(pgd_offset_raw((mm)->pgd, (addr)))
#define pgd_offset_k(addr)	pgd_offset(&init_mm, addr)
#define pud_offset_phys(dir, addr)	(pgd_page_paddr(*(dir)) + pud_index(addr) * sizeof(pud_t))
#define pud_offset(dir, addr)		((pud_t *)__va(pud_offset_phys((dir), (addr))))
#define pmd_offset_phys(dir, addr)	(pud_page_paddr(*(dir)) + pmd_index(addr) * sizeof(pmd_t))
#define pmd_offset(dir, addr)		((pmd_t *)__va(pmd_offset_phys((dir), (addr))))
#define pte_offset_phys(dir,addr)	(pmd_page_paddr(READ_ONCE(*(dir))) + pte_index(addr) * sizeof(pte_t))
#define pte_offset_kernel(dir,addr)	((pte_t *)__va(pte_offset_phys((dir), (addr))))

2.2.3 head.S中的页表映射

下面来介绍页表的创建过程,代码路径:arch/arm64/kernel/head.S

head.S中,创建页表相关的有三个宏:

  1. create_pgd_entry
/*
 * Macro to populate the PGD (and possibily PUD) for the corresponding
 * block entry in the next level (tbl) for the given virtual address.
 *
 * Preserves:	tbl, next, virt
 * Corrupts:	tmp1, tmp2
 */
	.macro	create_pgd_entry, tbl, virt, tmp1, tmp2
	create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
#if SWAPPER_PGTABLE_LEVELS > 3
	create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2
#endif
#if SWAPPER_PGTABLE_LEVELS > 2
	create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
#endif
	.endm

上述函数主要是调用create_table_entry,由于SWAPPER_PGTABLES配置为3,因此相当于创建了pgd和pmd两级页表,此处需要注意一点,create_table_entry函数执行后,tbl参数会自动加上PAGE_SIZE,也就是说pgd和pmd两级页表是物理连续的。

  1. create_block_map
/*
 * Macro to populate block entries in the page table for the start..end
 * virtual range (inclusive).
 *
 * Preserves:	tbl, flags
 * Corrupts:	phys, start, end, pstate
 */
	.macro	create_block_map, tbl, flags, phys, start, end
	lsr	\phys, \phys, #SWAPPER_BLOCK_SHIFT
	lsr	\start, \start, #SWAPPER_BLOCK_SHIFT
	and	\start, \start, #PTRS_PER_PTE - 1	// table index
	orr	\phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT	// table entry
	lsr	\end, \end, #SWAPPER_BLOCK_SHIFT
	and	\end, \end, #PTRS_PER_PTE - 1		// table end index
9999:	str	\phys, [\tbl, \start, lsl #3]		// store the entry
	add	\start, \start, #1			// next entry
	add	\phys, \phys, #SWAPPER_BLOCK_SIZE		// next block
	cmp	\start, \end
	b.ls	9999b
	.endm

  1. create_table_entry
/*
 * Macro to create a table entry to the next page.
 *
 *	tbl:	page table address
 *	virt:	virtual address
 *	shift:	#imm page table shift
 *	ptrs:	#imm pointers per table page
 *
 * Preserves:	virt
 * Corrupts:	tmp1, tmp2
 * Returns:	tbl -> next level table page address
 */
	.macro	create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
	lsr	\tmp1, \virt, #\shift
	and	\tmp1, \tmp1, #\ptrs - 1	// table index
	add	\tmp2, \tbl, #PAGE_SIZE
	orr	\tmp2, \tmp2, #PMD_TYPE_TABLE	// address of next table and entry type
	str	\tmp2, [\tbl, \tmp1, lsl #3]
	add	\tbl, \tbl, #PAGE_SIZE		// next level table page
	.endm

上述三个函数创建页表项,并且返回下一个Level的页表地址

在这里插入图片描述

三、动手实践

基于上面的分析,编写内核模块,获取一个线性地址对应的物理地址

首先写一个测试程序获取其虚拟地址

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
	char *p = NULL;
	p = malloc(10);
	printf("address = 0x%x\n",p);
	while(1);
	return 0;
}

以下是内核模块的整个代码:

#include  <linux/module.h> 
#include <linux/kernel.h> 
#include <linux/init.h> 
#include <linux/sched.h> 
#include <linux/pid.h> 
#include <linux/mm.h> 
#include <asm/pgtable.h> 
#include <asm/page.h> 

MODULE_AUTHOR("wang.com");
MODULE_DESCRIPTION("vitual address to physics address");

static int pid; 
static unsigned long va; 

module_param(pid,int,0644); //从命令行传递参数(变量,类型,权限)
module_param(va,ulong,0644); //va表示的是虚拟地址

static int find_pgd_init(void) 
{ 
        unsigned long pa = 0; //pa表示的物理地址
        struct task_struct *pcb_tmp = NULL; 
        pgd_t *pgd_tmp = NULL; 
        pud_t *pud_tmp = NULL; 
        pmd_t *pmd_tmp = NULL; 
        pte_t *pte_tmp = NULL; 

        printk(KERN_INFO"PAGE_OFFSET = 0x%lx\n",PAGE_OFFSET);  //页表中有多少个项
		/*pud和pmd等等  在线性地址中占据多少位*/
        printk(KERN_INFO"PGDIR_SHIFT = %d\n",PGDIR_SHIFT); 
		//注意:在32位系统中  PGD和PUD是相同的
        printk(KERN_INFO"PUD_SHIFT = %d\n",PUD_SHIFT); 
        printk(KERN_INFO"PMD_SHIFT = %d\n",PMD_SHIFT); 
        printk(KERN_INFO"PAGE_SHIFT = %d\n",PAGE_SHIFT); 

        printk(KERN_INFO"PTRS_PER_PGD = %d\n",PTRS_PER_PGD); //每个PGD里面有多少个ptrs
        printk(KERN_INFO"PTRS_PER_PUD = %d\n",PTRS_PER_PUD); 
        printk(KERN_INFO"PTRS_PER_PMD = %d\n",PTRS_PER_PMD); //PMD中有多少个项
        printk(KERN_INFO"PTRS_PER_PTE = %d\n",PTRS_PER_PTE); 

        printk(KERN_INFO"PAGE_MASK = 0x%lx\n",PAGE_MASK); //页的掩码

	struct pid *p = NULL;
	p = find_vpid(pid); //通过进程的pid号数字找到struct pid的结构体
	pcb_tmp = pid_task(p,PIDTYPE_PID); //通过pid的结构体找到进程的task  struct
        printk(KERN_INFO"pgd = 0x%p\n",pcb_tmp->mm->pgd); 
               // 判断给出的地址va是否合法(va&lt;vm_end)
  	if(!find_vma(pcb_tmp->mm,va)){ 
                printk(KERN_INFO"virt_addr 0x%lx not available.\n",va); 
                return 0; 
        } 
        pgd_tmp = pgd_offset(pcb_tmp->mm,va);  //返回线性地址va,在页全局目录中对应表项的线性地址
        printk(KERN_INFO"pgd_tmp = 0x%p\n",pgd_tmp); 
		//pgd_val获得pgd_tmp所指的页全局目录项
		//pgd_val是将pgd_tmp中的值打印出来
        printk(KERN_INFO"pgd_val(*pgd_tmp) = 0x%lx\n",pgd_val(*pgd_tmp)); 
        if(pgd_none(*pgd_tmp)){  //判断pgd有没有映射
                printk(KERN_INFO"Not mapped in pgd.\n");         
                return 0; 
        } 
        pud_tmp = pud_offset(pgd_tmp,va); //返回va对应的页上级目录项的线性地址
        printk(KERN_INFO"pud_tmp = 0x%p\n",pud_tmp); 
        printk(KERN_INFO"pud_val(*pud_tmp) = 0x%lx\n",pud_val(*pud_tmp)); 
        if(pud_none(*pud_tmp)){ 
                printk(KERN_INFO"Not mapped in pud.\n"); 
                return 0; 
        } 
        pmd_tmp = pmd_offset(pud_tmp,va); //返回va在页中间目录中对应表项的线性地址
        printk(KERN_INFO"pmd_tmp = 0x%p\n",pmd_tmp); 
        printk(KERN_INFO"pmd_val(*pmd_tmp) = 0x%lx\n",pmd_val(*pmd_tmp)); 
        if(pmd_none(*pmd_tmp)){ 
                printk(KERN_INFO"Not mapped in pmd.\n"); 
                return 0; 
        } 
        //在这里,把原来的pte_offset_map()改成了pte_offset_kernel
        pte_tmp = pte_offset_kernel(pmd_tmp,va);  //pte指的是  找到表

        printk(KERN_INFO"pte_tmp = 0x%p\n",pte_tmp); 
        printk(KERN_INFO"pte_val(*pte_tmp) = 0x%lx\n",pte_val(*pte_tmp)); 
        if(pte_none(*pte_tmp)){ //判断有没有映射
                printk(KERN_INFO"Not mapped in pte.\n"); 
                return 0; 
        } 
        if(!pte_present(*pte_tmp)){ 
                printk(KERN_INFO"pte not in RAM.\n"); 
                return 0; 
        } 
        pa = (pte_val(*pte_tmp) & PAGE_MASK) ;//物理地址的计算方法
        printk(KERN_INFO"virt_addr 0x%lx in RAM Page is 0x%lx .\n",va,pa); 
        //printk(KERN_INFO"contect in 0x%lx is 0x%lx\n",pa,*(unsigned long *)((char *)pa + PAGE_OFFSET)); 
                                                       
        return 0; 

} 

static void __exit  find_pgd_exit(void) 
{ 
        printk(KERN_INFO"Goodbye!\n"); 

} 

module_init(find_pgd_init); 
module_exit(find_pgd_exit);

MODULE_LICENSE("GPL"); 


Makefile

# If KERNELRELEASE is defined, we've been invoked from the
# # kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
	obj-m := lab3.o
#         # Otherwise we were called directly from the command
# line; invoke the kernel build system.
else        
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build
        PWD := $(shell pwd)
    
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif


clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.order *.symvers *.unsigned


  • 执行命令:insmod lab3.ko pid=2630 va=0xa87010
  • 通过dmesg查看打印的信息:
    在这里插入图片描述

可以看到相关的宏,以及线性地址对应的物理地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值