文章目录
3.4 翻译和设置页表条目
下面这组宏用来地址映射(地址及page到PTE)以及单个的页表条目的设置,ptep_get_and_clear用来保护和修改页表条目或物理页
名称 | 功能 |
---|---|
mk_pte() | 通过 struct page 和 保护位,联合生成一个pte_t 的页表项 |
mk_pte_phys() | 类似mk_pte,不同的是使用的页面物理地址作为输入参数 |
pte_page | 通过pte_t页表项获得memmap中对应的struct page 地址 |
pmd_page | 通过pmd_t页表项获得memmap中pte所在的struct page 地址 |
set_pte | 将 mk_pte返回的pte_t 表项内容设置到进程的pte页表项中 |
pte_clear | 清除pte页表中一个页表项;set_pte的逆操作 |
ptep_get_and_clear | 返回并清除页表中的一个条目pte_t |
page <–>pfn 转换
PHYS_OFFSET : 内存的起始物理地址
PHYS_PFN_OFFSET: 内存的起始物理地址对应的页号
/*
* PFNs are used to describe any physical page; this means
* PFN 0 == physical address 0.
*
* This is the PFN of the first RAM page in the kernel
* direct-mapped view. We assume this is the first page
* of RAM in the mem_map as well.
*/
#define PHYS_PFN_OFFSET (PHYS_OFFSET >> PAGE_SHIFT)
#define page_to_pfn(page) (((page) - mem_map) + PHYS_PFN_OFFSET)
#define pfn_to_page(pfn) ((mem_map + (pfn)) - PHYS_PFN_OFFSET)
PTE页表项的设置
从下面的函数可以看出pte 页表项中指向的为包含用户数据页面的物理地址,其他
static inline pte_t mk_pte_phys(unsigned long physpage, pgprot_t pgprot)
{
pte_t pte;
pte_val(pte) = physpage | pgprot_val(pgprot);
return pte;
}
3.5 分配和释放页表
页表,如之前说的,就是一个物理页,里面存放的是多个页表条目的数组
释放和分配页表是一个相对耗费资源的操作,一个是时间消耗,另外在物理页分配以及分配和删除三级页表的任意一个时都需要关闭中断,可想这是一个高频的操作,所以要越快越好。
因此我们将存放页表的页面缓存在一些称为quicklists 的链表上,每种架构的缓存实现方式有所不同,但原理都是一样的。例如,不是所有的硬件平台都缓存PGDs,因为其分配和释放动作只发生在进程创建和退出时,由于进程创建和退出本来就是个开销很高的操作,所以分配和释放页面的开销可以忽略了,下面一组函数用来分配和释放各级页表
名称 | 功能 |
---|---|
pgd_alloc | 分配pgd页表 |
pgd_free | 释放pgd页表 |
pmd_alloc | 分配pmd页表 |
pmd_free | 释放pmd页表 |
pte_alloc | 分配pte页表 |
pte_free | 释放pte页表 |
不同平台的缓存实现不一样,介绍其中一种后进先出(LIFO)方式,概括来说,内核使用三个链表pgd_quicklist,pmd_quicklist,pte_quicklist 分别缓存空闲的页表,当释放页表时,将其插入到链表头中,而分配时,从表头弹出,通常页表保存的页表条目是下级页表指针或者物理页指针,当空闲页表缓存到链表中,其首个条目将指向下一个空闲页表对象,并且对缓存链表进行计数
下面这组接口就是通过缓存快速分配页表
名称 | 功能 |
---|---|
get_pgd_fast | 快速分配pgd页表 |
pmd_alloc_one_fast | 快速分配pmd页表 |
pte_alloc_one_fast | 快速分配pte页表 |
如果缓存链表空,那么只有通过物理页分配器来分配,对应的接口如下
名称 | 功能 |
---|---|
get_pgd_slow | 通过物理页分配器分配pgd页表 |
pmd_alloc_one | 通过物理页分配器分配pmd页表 |
pte_alloc_one | 通过物理页分配器分配pte页表 |
显然,这种方式可能会导致缓存大量的页表,所以需要一种控制机制,监测其数量,内核分别设置了高低两种限位。当进入空闲任务,或者进入clear_page_tables函数(该函数可能释放大量的页表,导致缓存超限)中调用check_pgt_cache函数,当缓存超过限制时,释放部分页直到满足低位标准。 |
pgd_alloc
get_pgd_slow
- 从页分配器上分配4页(2的2次幂)即16K 给PGD 页面
- 清空用户页表
- 获取当前PGD第一页
- 如果向量表从0开始,则复制第一页到新的PGD页
- copy 当前的内核及IO对应的页表项到新的PGD 页表
- 强制写入cache,保持cache一致性
/*
* need to get a 16k page for level 1
*/
pgd_t *get_pgd_slow(struct mm_struct *mm)
{
pgd_t *new_pgd, *init_pgd;
pmd_t *new_pmd, *init_pmd;
pte_t *new_pte, *init_pte;
new_pgd = (pgd_t *)__get_free_pages(GFP_KERNEL, 2);//1
if (!new_pgd)
goto no_pgd;
memzero(new_pgd, FIRST_KERNEL_PGD_NR * sizeof(pgd_t));// 2
init_pgd = pgd_offset_k(0); //3
//[4
if (vectors_base() == 0) {
init_pmd = pmd_offset(init_pgd, 0);
init_pte = pte_offset(init_pmd, 0);
/*
* This lock is here just to satisfy pmd_alloc and pte_lock
*/
spin_lock(&mm->page_table_lock);
/*
* On ARM, first page must always be allocated since it
* contains the machine vectors.
*/
new_pmd = pmd_alloc(mm, new_pgd, 0);
if (!new_pmd)
goto no_pmd;
new_pte = pte_alloc(mm, new_pmd, 0);
if (!new_pte)
goto no_pte;
set_pte(new_pte, *init_pte);
spin_unlock(&mm->page_table_lock);
}//]
/*
* Copy over the kernel and IO PGD entries
*/
memcpy(new_pgd + FIRST_KERNEL_PGD_NR, init_pgd + FIRST_KERNEL_PGD_NR,
(PTRS_PER_PGD - FIRST_KERNEL_PGD_NR) * sizeof(pgd_t));//5
/*
* FIXME: this should not be necessary
*/
clean_cache_area(new_pgd, PTRS_PER_PGD * sizeof(pgd_t));//6
return new_pgd;
no_pte:
spin_unlock(&mm->page_table_lock);
pmd_free(new_pmd);
free_pages((unsigned long)new_pgd, 2);
return NULL;
no_pmd:
spin_unlock(&mm->page_table_lock);
free_pages((unsigned long)new_pgd, 2);
return NULL;
no_pgd:
return NULL;
}
3.6 内核页表
当系统启动时,还无法使用内存分页机制,因为页表还未初始化,不同架构的页表初始化不一样,我们只讨论x86,页表初始化分为两个阶段,bootstrap中只初始化8MiB1内存空间的页表用来启动页表机制。第二阶段初始化剩余的页表
3.6.1 bootstrap
arch/i386/kernel/head.S 中的汇编函数startup_32()负责建立分页机制,vmlinuz 内核代码的编译基址是PAGE_OFFSET + 1MiB(arch/i386/vmlinuz.lds 中指定,见下图),
但事实上内核从0x00100000地址加载,第一个megabyte 被bios 用于与其他的硬件通讯,因此跳过,head.s 中的引导代码在分页机制建立前,直接将线性地址减去PAGE_OFFSET就是其实际物理地址。即先建立 8MiB物理内存的线性地址映射,偏移是PAGE_OFFSET。
将swapper_pg_dir(即一级页表)通过链接器放到 0x00101000地址,然后先建立pg0,pg1两个页的页表项,如果处理器支持PSE模式,那么每页大小就是4Mib,否则每页4KiB,如下图 ,页目录的前两项 和 从768 开始的两个项都指向的pg0/1,这样做,无论使用物理地址1-9MiB或者使用虚拟地址 PAGE OFFSET+1MiB — PAGE OFFSET+9MiB都映射到了1-9m 的物理地址空间。
页表建好后,通过设置cr0 寄存器某个位得以打开分页机制,接着通过跳转到正确的地址(使用虚拟地址),以确保EIP (指令指针寄存器)的正确。
3.6.2 第二阶段
paging_init函数负责剩余页表的初始化,x86 中该函数调用关系如图3.4.
首先调用pagetable_init()初始化ZONE DMA and ZONE NORMAL物理内存, ZONE HIGHMEM 高端内存不能直接引用,而是临时建立的,通过调用boot memory allocater(see Chapter 5)为内核的所有的pgd_t分配一个页面,即PGD页表,如果设置了PSE标志位,将使用 4MiB TLB项 替代4KiB ,如果未设置,则为每个pmd_t 分配一个PTE 页表,如果cpu支持PGE,全局页面标志将设置,以对所有进程可见。
pagetable_init() 调用 fixrange_init() 初始化固定映射区,以映射从虚拟空间的尾部即起始于FIXADDR_START的空间,用于APIC,以及在 FIX_KMAP_BEGIN 到 FIX_KMAP_END 之间的原子映射,最后,fixrange_init 初始化高端内存映射中kmap()所需的页表项,在pagetable_init()返回后,内核空间的页表已经初始化完成,此时将静态PGD载入CR3,启动页表机制。接下来,调用kmap_init初始化带有PAGE_KERNEL标志的每个PTE,最终调用 zone_sizes_init(),初始化node,zone,page结构。
详见 内核页表初始化源码分析
1 MiB =2^20BYTE ↩︎