zl程序教程

您现在的位置是:首页 >  系统

当前栏目

Linux 页表管理(二)

Linux 管理 页表
2023-09-11 14:18:25 时间

前言

在这篇文章简单的描述了x86_64页表相关知识:Linux 页表管理(一),接下来主要描述页表的一些数据结构和相应的函数操作。

一、页表相关数据结构

以4级页表为例:
示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

1.1 页表项

Linux使用c语言中的结构体来表示页表项,但通常情况下页表项都只有一个成员,分别采用pgd_t、pmd_t、pud_t和pte_t四种数据结构来表示页全局目录项、页上级目录项、页中间目录项和页表项。这四种 数据结构本质上都是无符号长整型unsigned long。

每一个页表项是8个字节,用于存储物理地址和相应的访问权限。

一个页表页占用物理内存中的一个物理页:4KB,4KB/8 = 512,因此一个页表页有512页表项:512 = 2^9。因此虚拟地址中对应的每一级页表的索引都是9位。

页全局目录项包含若干个页上级目录项的物理地址,页上级目录项依次包含若干个页中间目录项的物理地址,页中间目录项包含若干个页表项的物理地址。每一个页表项指向一个物理页框。一个物理页框为4KB = 5^12。因此虚拟地址的页内偏移是12位。

页全局目录项只有一个页表页,占用一个物理内存中一个物理页(4KB),其物理地址放在CR3寄存器中。其余的每级页表都有若干个离散的页表页。

因此线性地址被分为五个部分(四级页表):9、9、9、9、12(页内偏移)。

使用struct而不是unsigned long是用来确保页表项的相关内容只能由相关的辅助函数进行读取或写入,而绝不能直接访问。同时用struct也方便扩展结构体成员。

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;
typedef struct pgprot { pgprotval_t pgprot; } pgprot_t;

pgprot_t是一个unsigned long的数据类型,它表示与一个单独表项相关的保护标志,用来存储pte的保护位,一般用来与pte低12位进行比较和设置其值。

// linux-3.10.1/arch/x86/include/asm/pgtable_64_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;

#define PAGETABLE_LEVELS	4
// linux-3.10.1/arch/x86/include/asm/pgtable_types.h

typedef struct pgprot { pgprotval_t pgprot; } pgprot_t;

typedef struct { pgdval_t pgd; } pgd_t;

//write val to pgd_t
static inline pgd_t native_make_pgd(pgdval_t val)
{
	return (pgd_t) { val };
}

//read pgd_t pgd:pgdval_t
static inline pgdval_t native_pgd_val(pgd_t pgd)
{
	return pgd.pgd;
}

#if PAGETABLE_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;
}

#if PAGETABLE_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;
}

1.2 线性地址字段

上面说到线性地址被分为五个部分:9、9、9、9、12。

// linux-3.10.1/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))
// linux-3.10.1/arch/x86/include/asm/pgtable_64_types.h

/*
 * 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

#define PMD_SIZE	(_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK	(~(PMD_SIZE - 1))
#define PUD_SIZE	(_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK	(~(PUD_SIZE - 1))
#define PGDIR_SIZE	(_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK	(~(PGDIR_SIZE - 1))

XXX_SHIFT指定字段的位数
XXX_SIZE指定的一个表项所能映射的大小
XXX_MASK用来屏蔽指定字段的位数
PTRS_PER_XXX指定一个表中表项的个数

以 PAGE宏–页表(PTE)为例:
PAGE_SHIFT表示线性地址字段的offset字段的位数,12位,由于一个页内所有的地址都要能放在offset字段中,所以页的大小是2^12 = 4096字节,也就是4KB。
PAGE_SIZE页的大小,4KB。
PAGE_MASK用来屏蔽OFFSET字段,也就是屏蔽后12位:0xfffffffffffff000

PTRS_PER_PTE用来表示pte页表项的个数:2^9 = 512

其余的依次类推,如图所示:
在这里插入图片描述

在这里插入图片描述

其中PGD是一个pgt_t类型数组。
其中PUD是一个put_t类型数组。
其中PMD是一个pmt_t类型数组。
其中PTE是一个pte_t类型数组。
PGD为顶级页表,只有一个页表页,一个页表页占用物理内存的一个物理页(4K),也就是PGD大小为4K,页表基地址寄存器存储的就是该页的物理地址(CR3寄存器)。其余每一级页表:PUD、PMD、PTE都有若干个离散的页表页,每个页表页也是占用一个物理页,大小为4K。

linux中每个进程有它自己的PGD( Page Global Directory),它是一个物理页,并包含一个pgd_t数组。
PGD(PGD只有一个页表页4K)、PUDs、PMDs、PTEs。

二、页表函数处理

2.1 类型转换宏

pgd_val将 结构体 pgd_t 转换为 无符号整形pgdval_t
__pgd将 无符号整形pgdval_t 转换为 结构体 pgd_t

// linux-3.10.1/arch/x86/include/asm/pgtable.h

#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)

2.2 访问页表

pgd_offset根据struct mm_struct *mm 和线性地址(用户空间虚拟地址)获取pgd_t * pgd。
pgd_offset_k根据address(该地址是内核空间虚拟地址)获取内核页表的pgd_t * pgd:swapper_pg_dir。

// linux-3.10.1/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))

/*
 * 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))

pud_offset参数为指向页全局目录项的指针 pgd 和线性地址 address。这个函数产生页上级目录中目录项 address 对应的线性地址,即:pud_t。

// linux-3.10.1/arch/x86/include/asm/pgtable.h

/* to find an entry in a page-table-directory. */
static inline unsigned long pud_index(unsigned long address)
{
	return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}

static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)
{
	return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);

pmd_offset接收指向页上级目录项的指针 pud 和线性地址 address 作为参数。这个函数产生目录项 addr 在页中间目录中的偏移地址,即:pmd_t 。

// linux-3.10.1/arch/x86/include/asm/pgtable.h

static inline unsigned long pud_page_vaddr(pud_t pud)
{
	return (unsigned long)__va((unsigned long)pud_val(pud) & PTE_PFN_MASK);
}

/*
 * 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);
}

/* 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);
}

pte_offset_kernel,线性地址address 在页中间目录 dir 中有一个对应的项,该函数就产生这个对应项,即页表(PTE)的线性地址,物理页的起始地址,pte_t。另外,该宏只在主内核页表上使用。

// linux-3.10.1/arch/x86/include/asm/pgtable.h

static inline unsigned long pmd_page_vaddr(pmd_t pmd)
{
	return (unsigned long)__va(pmd_val(pmd) & PTE_PFN_MASK);
}

/*
 * 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);
}

static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
	return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}

其中主内核页表,在内核中其实就是一段内存,存放在主内核页全局目录init_mm.pgd(swapper_pg_dir)中,硬件并不直接使用。

// linux-3.10.1/mm/init-mm.c

struct mm_struct init_mm = {
	.mm_rb		= RB_ROOT,
	.pgd		= swapper_pg_dir,
	.mm_users	= ATOMIC_INIT(2),
	.mm_count	= ATOMIC_INIT(1),
	.mmap_sem	= __RWSEM_INITIALIZER(init_mm.mmap_sem),
	.page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
	.mmlist		= LIST_HEAD_INIT(init_mm.mmlist),
	INIT_MM_CONTEXT(init_mm)
};

pte_offset_map接收指向一个页中间目录项的指针 dir 和线性地址 address 作为参数,它产生与线性地址 address 相对应的页表项的线性地址,即页表(PTE)的线性地址,物理页的起始地址

/* x86-64 always has all page tables mapped. */
#define pte_offset_map(dir, address) pte_offset_kernel((dir), (address))

pgd_offset、pud_offset、pmd_offset、和pte_offset_kernel()分别用于从虚拟(线性)地址中提取查找PGD、PUD、 PMD和PTE表所需的index。
在这里插入图片描述

2.3 分配释放相关函数

内核加载一个进程后,需要为该进程创建属于它的页表。

// linux-3.10.1/kernel/fork.c

static inline int mm_alloc_pgd(struct mm_struct *mm)
{
	mm->pgd = pgd_alloc(mm);
	if (unlikely(!mm->pgd))
		return -ENOMEM;
	return 0;
}

static inline void mm_free_pgd(struct mm_struct *mm)
{
	pgd_free(mm, mm->pgd);
}

pgd_alloc(),pud_alloc(),pmd_alloc()和pte_alloc_kernel()分别用于创建PGD,PUD,PMD和PTE。

2.3.1 pgd_alloc

__get_free_pages返回的是虚拟空间地址,page_address用于将物理页struct page *page转化为虚拟地址。

// linux-3.10.1/mm/page_alloc.c

/*
 * Common helper functions.
 */
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
	struct page *page;

	/*
	 * __get_free_pages() returns a 32-bit address, which cannot represent
	 * a highmem page
	 */
	VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);

	page = alloc_pages(gfp_mask, order);
	if (!page)
		return 0;
	return (unsigned long) page_address(page);
}
EXPORT_SYMBOL(__get_free_pages);

pgd_ctor拷贝了对于 swapper_pg_dir 的引用。swapper_pg_dir 是内核页表的最顶级的全局页目录。
一个进程的虚拟地址空间包含用户态和内核态两部分。为了从虚拟地址空间映射到物理页面,页表也分为用户地址空间的页表和内核页表。在内核里面,映射靠内核页表,这里内核页表会拷贝一份到进程的页表。
x86_64体系架构只提供一个页表基地址寄存器CR3,内核不使用单独的页表,而是把自己映射到应用程序的高地址部分,因此在系统调用是不会切换页表。(这里没有考虑内核页表隔离的情况)。
关于内核页表隔离,请参考:内核页表隔离 (KPTI) 详解

如果没有KPTI,每当执行用户空间代码(应用程序)时,Linux会在其分页表中保留整个内核内存的映射,并保护其访问。这样做的优点是当应用程序向内核发送系统调用或收到中断时,内核页表始终存在,可以避免绝大多数上下文切换相关的开销(TLB刷新、页表交换等)。

// linux-3.10.1/arch/x86/include/asm/pgtable.h
#define KERNEL_PGD_BOUNDARY	pgd_index(PAGE_OFFSET)

/*
 * clone_pgd_range(pgd_t *dst, pgd_t *src, int count);
 *
 *  dst - pointer to pgd range anwhere on a pgd page
 *  src - ""
 *  count - the number of pgds to copy.
 *
 * dst and src can be on the same page, but the range must not overlap,
 * and must not cross a page boundary.
 */
static inline void clone_pgd_range(pgd_t *dst, pgd_t *src, int count)
{
       memcpy(dst, src, count * sizeof(pgd_t));
}

// linux-3.10.1/arch/x86/mm/pgtable.c

static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
{
	/* If the pgd points to a shared pagetable level (either the
	   ptes in non-PAE, or shared PMD in PAE), then just copy the
	   references from swapper_pg_dir. */
	if (PAGETABLE_LEVELS == 2 ||
	    (PAGETABLE_LEVELS == 3 && SHARED_KERNEL_PMD) ||
	    PAGETABLE_LEVELS == 4) {
		clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,
				swapper_pg_dir + KERNEL_PGD_BOUNDARY,
				KERNEL_PGD_PTRS);
	}

	/* list required to sync kernel mapping updates */
	if (!SHARED_KERNEL_PMD) {
		pgd_set_mm(pgd, mm);
		pgd_list_add(pgd);
	}
}

struct mm_struct里面存的都是虚拟地址。

// linux-3.10.1/arch/x86/mm/pgtable.c

pgd_t *pgd_alloc(struct mm_struct *mm)
{
	pgd_t *pgd;
	pmd_t *pmds[PREALLOCATED_PMDS];

	pgd = (pgd_t *)__get_free_page(PGALLOC_GFP);

	if (pgd == NULL)
		goto out;

	mm->pgd = pgd;

	......
	
	pgd_ctor(mm, pgd);
	......
	
	return pgd;
}

2.3.2 pud_alloc和pmd_alloc

// linux-3.10.1/include/linux/mm.h

/*
 * The following ifdef needed to get the 4level-fixup.h header to work.
 * Remove it when 4level-fixup.h has been removed.
 */
#if defined(CONFIG_MMU) && !defined(__ARCH_HAS_4LEVEL_HACK)
static inline pud_t *pud_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
{
	return (unlikely(pgd_none(*pgd)) && __pud_alloc(mm, pgd, address))?
		NULL: pud_offset(pgd, address);
}

static inline pmd_t *pmd_alloc(struct mm_struct *mm, pud_t *pud, unsigned long address)
{
	return (unlikely(pud_none(*pud)) && __pmd_alloc(mm, pud, address))?
		NULL: pmd_offset(pud, address);
}
#endif /* CONFIG_MMU && !__ARCH_HAS_4LEVEL_HACK */

2.3.3 pte_alloc_map

pte_alloc_map()接收页中间目录项的地址 pmd 和线性地址 address作为参数,并返回与 address对应的页表项的地址。如果页中间目录项为空,该函数通过调用函数 pte_alloc_one( ) 分配一个新页表页,也就是一个物理页。如果分配了一个新页表页, address对应的项就被创建,同时 User/Supervisor 标志被设置为 1

// linux-3.10.1/arch/x86/mm/pgtable.c

gfp_t __userpte_alloc_gfp = PGALLOC_GFP | PGALLOC_USER_GFP;

pgtable_t pte_alloc_one(struct mm_struct *mm, unsigned long address)
{
	struct page *pte;

	pte = alloc_pages(__userpte_alloc_gfp, 0);
	if (pte)
		pgtable_page_ctor(pte);
	return pte;
}

// linux-3.10.1/arch/x86/include/asm/pgtable_types.h
typedef struct page *pgtable_t;

// linux-3.10.1/mm/memory.c
int __pte_alloc(struct mm_struct *mm, struct vm_area_struct *vma,
		pmd_t *pmd, unsigned long address)
{
	pgtable_t new = pte_alloc_one(mm, address);
	......
	return 0;
}

// linux-3.10.1/include/linux/mm.h
#define pte_alloc_map(mm, vma, pmd, address)				\
	((unlikely(pmd_none(*(pmd))) && __pte_alloc(mm, vma,	\
							pmd, address))?	\
	 NULL: pte_offset_map(pmd, address))

2.3.4 pte_alloc_kernel

pte_alloc_kernel如果与地址 address相关的页中间目录项 pmd 为空,该函数分配一个新页表。然后返回与 address相关的页表项的线性地址。该函数仅被主内核页表使用。

// linux-3.10.1/arch/x86/mm/pgtable.c
pte_t *pte_alloc_one_kernel(struct mm_struct *mm, unsigned long address)
{
	return (pte_t *)__get_free_page(PGALLOC_GFP);
}

// linux-3.10.1/mm/memory.c

int __pte_alloc_kernel(pmd_t *pmd, unsigned long address)
{
	pte_t *new = pte_alloc_one_kernel(&init_mm, address);
	......
	return 0;
}

// linux-3.10.1/include/linux/mm.h

#define pte_alloc_kernel(pmd, address)			\
	((unlikely(pmd_none(*(pmd))) && __pte_alloc_kernel(pmd, address))? \
		NULL: pte_offset_kernel(pmd, address))

2.3.5 quicklist

页表本身也是放在内存中的, 其本质上是由若干物理pages组成的,分配物理内存本身就是耗时的,加之在分配过程中需要关中断,而页表的创建和销毁又是很频繁的操作,而且现在的页表多达四级甚至五级,所以整个过程的系统开销较大。

为了加速页表页的分派,采用percup变量来存储一个struct quicklist,把要提供给页表创建的物理pages放到一组叫"quicklist"的链表中,以后销毁页表释放的页就往这个quicklist里送,创建页表需要的页就直接从这个quicklist里找,其实就是一个专用的freelist,比去整个物理内存的free pages list分配要快。

关于percpu变量请参考:Linux per-cpu

// linux-3.10.1/include/linux/quicklist.h

#ifdef CONFIG_QUICKLIST

struct quicklist {
	void *page;
	int nr_pages;
};

DECLARE_PER_CPU(struct quicklist, quicklist)[CONFIG_NR_QUICK];

/*
 * The two key functions quicklist_alloc and quicklist_free are inline so
 * that they may be custom compiled for the platform.
 * Specifying a NULL ctor can remove constructor support. Specifying
 * a constant quicklist allows the determination of the exact address
 * in the per cpu area.
 *
 * The fast patch in quicklist_alloc touched only a per cpu cacheline and
 * the first cacheline of the page itself. There is minmal overhead involved.
 */
static inline void *quicklist_alloc(int nr, gfp_t flags, void (*ctor)(void *))
{
	struct quicklist *q;
	void **p = NULL;

	q =&get_cpu_var(quicklist)[nr];
	p = q->page;
	if (likely(p)) {
		q->page = p[0];
		p[0] = NULL;
		q->nr_pages--;
	}
	put_cpu_var(quicklist);
	if (likely(p))
		return p;

	p = (void *)__get_free_page(flags | __GFP_ZERO);
	if (ctor && p)
		ctor(p);
	return p;
}

2.3.6 pgd_none

如果相应的表项值为0,那么,宏pte_none、pmd_none、pud_none和 pgd_none产生的值为1,否则产生的值为0。

// linux-3.10.1/arch/x86/include/asm/pgtable_types.h
static inline pgdval_t native_pgd_val(pgd_t pgd)
{
	return pgd.pgd;
}


// linux-3.10.1/arch/x86/include/asm/pgtable.h
static inline int pgd_none(pgd_t pgd)
{
	return !native_pgd_val(pgd);
}
// linux-3.10.1/arch/x86/include/asm/pgtable_types.h
static inline pmdval_t native_pmd_val(pmd_t pmd)
{
	return pmd.pmd;
}

// linux-3.10.1/arch/x86/include/asm/pgtable.h
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;
}
// linux-3.10.1/arch/x86/include/asm/pgtable_types.h
static inline pudval_t native_pud_val(pud_t pud)
{
	return pud.pud;
}

// linux-3.10.1/arch/x86/include/asm/pgtable.h
#if PAGETABLE_LEVELS > 2
static inline int pud_none(pud_t pud)
{
	return native_pud_val(pud) == 0;
}
 linux-3.10.1/arch/x86/include/asm/pgtable.h
static inline int pte_none(pte_t pte)
{
	return !pte.pte;
}

2.3.7 mk_pte

如果建立了新的映射关系,则需要调用mk_pte()创建一个新的页表项,因为页表描述符由物理页面号和权限控制位组成,所以新建页表项的工作主要就是填充这两部分内容,组装完成就可以调用set_pte()插入到对应的页表中去了。


// linux-3.10.1/arch/x86/include/asm/pgtable.h
static inline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)
{
	return __pte(((phys_addr_t)page_nr << PAGE_SHIFT) |
		     massage_pgprot(pgprot));
}

/*
 * Conversion functions: convert a page and protection to a page entry,
 * and a page entry and page directory to the page they refer to.
 *
 * (Currently stuck as a macro because of indirect forward reference
 * to linux/mm.h:page_to_nid())
 */
#define mk_pte(page, pgprot)   pfn_pte(page_to_pfn(page), (pgprot))
// linux-3.10.1/arch/x86/include/asm/pgtable_64.h
static inline void native_set_pte(pte_t *ptep, pte_t pte)
{
	*ptep = pte;
}

#define set_pte(ptep, pte)		native_set_pte(ptep, pte)

set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值。

三、地址转换过程

  1. 从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。
    第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出,即页上级页目录的物理基地址。

  2. 从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。
    第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。

  3. 从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。
    第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。

  4. 从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。
    第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。

  5. 从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。
    第五次读取内存得到最终要访问的数据。

在这里插入图片描述

每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单 位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。

参考资料

Linux 3.10.1

深入理解Linux内核
深入Linux内核架构
现代操作系统原理与实现
https://www.cnblogs.com/linhaostudy/p/10038100.html
https://www.cnblogs.com/muahao/p/10297852.html
https://zhuanlan.zhihu.com/p/67813716