本文针对ARM32处理器进行说明。
内核页表: 即书上说的主内核页表,在内核中其实就是一段内存,存放在主内核页全局目录init_mm.pgd(swapper_pg_dir)
中,硬件并不直接使用。
进程页表: 每个进程自己的页表,放在进程自身的页目录task_struct.pgd
中。
进程创建,frok
时会拷贝内核页表到当前进程页表中。
调用关系:
do_dork->copy_process->copy_mm->copy_mmp->mm_init->mm_alloc_pgd->pgd_alloc
最后调用的pgd_alloc
跟arch
相关,不同的架构处理方式也不一致,arm32只用到TTBR0
去设置页表基地址。内核又和进程共享地址,所以需要拷贝init
进程页表项内核空间到新创建进程中。
pgd_t *pgd_alloc(struct mm_struct *mm) { pgd_t *new_pgd, *init_pgd; pud_t *new_pud, *init_pud; pmd_t *new_pmd, *init_pmd; pte_t *new_pte, *init_pte; new_pgd = __pgd_alloc(); if (!new_pgd) goto no_pgd; memset(new_pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t)); (1) /* * Copy over the kernel and IO PGD entries */ init_pgd = pgd_offset_k(0); memcpy(new_pgd + USER_PTRS_PER_PGD, init_pgd + USER_PTRS_PER_PGD, (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t)); (2) clean_dcache_area(new_pgd, PTRS_PER_PGD * sizeof(pgd_t)); #ifdef CONFIG_ARM_LPAE /* * Allocate PMD table for modules and pkmap mappings. */ new_pud = pud_alloc(mm, new_pgd + pgd_index(MODULES_VADDR), MODULES_VADDR); if (!new_pud) goto no_pud; new_pmd = pmd_alloc(mm, new_pud, 0); if (!new_pmd) goto no_pmd; #endif if (!vectors_high()) { /* * On ARM, first page must always be allocated since it * contains the machine vectors. The vectors are always high * with LPAE. */ new_pud = pud_alloc(mm, new_pgd, 0); if (!new_pud) goto no_pud; new_pmd = pmd_alloc(mm, new_pud, 0); if (!new_pmd) goto no_pmd; new_pte = pte_alloc_map(mm, NULL, new_pmd, 0); if (!new_pte) goto no_pte; init_pud = pud_offset(init_pgd, 0); init_pmd = pmd_offset(init_pud, 0); init_pte = pte_offset_map(init_pmd, 0); set_pte_ext(new_pte + 0, init_pte[0], 0); set_pte_ext(new_pte + 1, init_pte[1], 0); pte_unmap(init_pte); pte_unmap(new_pte); } return new_pgd; no_pte: pmd_free(mm, new_pmd); mm_dec_nr_pmds(mm); no_pmd: pud_free(mm, new_pud); no_pud: __pgd_free(new_pgd); no_pgd: return NULL; }
1)清零用户空间一级页表
2)拷贝init
进程pgd
内核空间页表到当前一级页表中。因为一级页表一样,所以指向的二级页表都是同一份。
在vmalloc
、vmap
亦或者ioremap
时,内核空间页表会进程调整。
以vmalloc
和vmap
为例,进行地址映射时,会调用map_vm_area
,最终调用到vmap_pte_range
static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end, pgprot_t prot, struct page **pages, int *nr) { pte_t *pte; /* * nr is a running index into the array which helps higher level * callers keep track of where we're up to. */ pte = pte_alloc_kernel(pmd, addr); (1) if (!pte) return -ENOMEM; do { struct page *page = pages[*nr]; if (WARN_ON(!pte_none(*pte))) return -EBUSY; if (WARN_ON(!page)) return -ENOMEM; set_pte_at(&init_mm, addr, pte, mk_pte(page, prot)); /* 设置pte页表项内容 */ (*nr)++; } while (pte++, addr += PAGE_SIZE, addr != end); return 0; }
判断pmd
页表项是否为空(对于arm32来说,二级映射,只有pte和pgd,即pmd
就是pgd
)。两种情况,第一种情况,pmd
为空,需要重新申请pte
页表项,因为单次申请需要page
对齐,即单次申请2MB虚拟地址空间的二级页表,所以当进行映射时,可能用不完。第二种情况就是,上次申请的二级页表没有映射完,就可以直接拿来用。
第一种情况下
#define pte_alloc_kernel(pmd, address) \ ((unlikely(pmd_none(*(pmd))) && __pte_alloc_kernel(pmd, address))? \ NULL: pte_offset_kernel(pmd, address)) int __pte_alloc_kernel(pmd_t *pmd, unsigned long address) { pte_t *new = pte_alloc_one_kernel(&init_mm, address); if (!new) return -ENOMEM; smp_wmb(); /* See comment in __pte_alloc */ spin_lock(&init_mm.page_table_lock); if (likely(pmd_none(*pmd))) { /* Has another populated it ? */ pmd_populate_kernel(&init_mm, pmd, new); (1) new = NULL; } else VM_BUG_ON(pmd_trans_splitting(*pmd)); spin_unlock(&init_mm.page_table_lock); if (new) pte_free_kernel(&init_mm, new); return 0; }
1)将pmd
内容设置为新申请的pte
页表项,注意,这里设置的是init_mm
,并没有同步到其他进程,包括当前进程。
上文描述的,内核空间新映射虚拟地址时,仅仅设置了init_mm
的pgd(pmd)
一级页表,并没有同步所有进程。那么,当进程需要范围这段虚拟地址时,硬件需要根据页表翻译虚拟地址,不就访问非法地址了?
在这个问题上,我思考了很久,然后也找了很久内核代码都百思不得其解。后面才惊奇的发现,用的缺页中断操作。(感慨自己见识短浅了,看的一些书上也确实没有提到这个)
因为是一级页表缺失,所以会跳转到段地址翻译错误接口:
do_translation_fault
(这个函数也是架构相关的)
static int __kprobes do_translation_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs) { unsigned int index; pgd_t *pgd, *pgd_k; pud_t *pud, *pud_k; pmd_t *pmd, *pmd_k; if (addr < TASK_SIZE) return do_page_fault(addr, fsr, regs); (1) if (user_mode(regs)) goto bad_area; index = pgd_index(addr); pgd = cpu_get_pgd() + index; (2) pgd_k = init_mm.pgd + index; (3) if (pgd_none(*pgd_k)) (4) goto bad_area; if (!pgd_present(*pgd)) set_pgd(pgd, *pgd_k); pud = pud_offset(pgd, addr); pud_k = pud_offset(pgd_k, addr); if (pud_none(*pud_k)) goto bad_area; if (!pud_present(*pud)) set_pud(pud, *pud_k); pmd = pmd_offset(pud, addr); pmd_k = pmd_offset(pud_k, addr); #ifdef CONFIG_ARM_LPAE /* * Only one hardware entry per PMD with LPAE. */ index = 0; #else /* * On ARM one Linux PGD entry contains two hardware entries (see page * tables layout in pgtable.h). We normally guarantee that we always * fill both L1 entries. But create_mapping() doesn't follow the rule. * It can create inidividual L1 entries, so here we have to call * pmd_none() check for the entry really corresponded to address, not * for the first of pair. */ index = (addr >> SECTION_SHIFT) & 1; #endif if (pmd_none(pmd_k[index])) goto bad_area; copy_pmd(pmd, pmd_k); (5) return 0; bad_area: do_bad_area(addr, fsr, regs); return 0; }
1)地址在用户空间,跳转到page_fault
2)获取当前进程错误地址页表
3)获取init进程当前地址页表
4)如果init进程当前地址页表项也为空,那么这个地址确实是非法地址。
5)拷贝内核对应页表到进程中。
#define copy_pmd(pmdpd,pmdps) \ do { \ pmdpd[0] = pmdps[0]; \ pmdpd[1] = pmdps[1]; \ flush_pmd_entry(pmdpd); \ } while (0)
ARM32位处理器,由于无法通过TTBR0、TTBR1同时设置内核页表项地址和用户空间页表项地址,所以采用创建进程时拷贝内核空间页表的方式来实现共享内核空间的操作。但是内核空间的虚拟地址也是在不停的变换的,如果映射一段地址就去主动更新所有进程页表的内核部分,显然是相当不划算的。一个进程映射的地址,其他进程很大概率是不会用到的。所以采用缺页中断的方式,在需要使用时,去拷贝页表。init_mm
的pgd
,则保存了完整的内核空间页表。