趣谈Linux操作系统学习笔记-内存管理(26讲)-- 内核的映射机制
问题:
1. 内核态内存映射函数 vmalloc、kmap_atomic 是如何工作的;
2. 内核态页表是放在哪里的,如何工作的?
3. swapper_pg_dir 是怎么回事;
4. 出现了内核态缺页异常应该怎么办?
内核页表
注意:和用户态页表不同,在系统初始化的时候,我们就要创建内核页表了
从内核页表的根 swapper_pg_dir 开始找线索,在 arch/x86/include/asm/pgtable_64.h 中就能找到它的定义
1 extern pud_t level3_kernel_pgt[512]; 2 extern pud_t level3_ident_pgt[512]; 3 extern pmd_t level2_kernel_pgt[512]; 4 extern pmd_t level2_fixmap_pgt[512]; 5 extern pmd_t level2_ident_pgt[512]; 6 extern pte_t level1_fixmap_pgt[512]; 7 extern pgd_t init_top_pgt[]; 8 9 10 #define swapper_pg_dir init_top_pgt
swapper_pg_dir 指向内核最顶级的目录 pgd,同时出现的还有几个页表目录。
64 位系统的虚拟地址空间的布局:
其中 XXX_ident_pgt 对应的是直接映射区
XXX_kernel_pgt 对应的是内核代码区
XXX_fixmap_pgt 对应的是固定映射区
内核页表的顶级目录 init_top_pgt,
init_top_pgt 有三项:
第一项:
指向的是 level3_ident_pgt,也即直接映射区页表的三级目录
为什么要减去 __START_KERNEL_map 呢?
因为 level3_ident_pgt 是定义在内核代码里的,写代码的时候,写的都是虚拟地址,谁写代码的时候也不知道将来加载的物理地址是多少
因为 level3_ident_pgt 是在虚拟地址的内核代码段里的,而 __START_KERNEL_map 正是虚拟地址空间的内核代码段的起始地址,level3_ident_pgt 减去 __START_KERNEL_map 才是物理地址
第二项:PGD_PAGE_OFFSET
__PAGE_OFFSET_BASE 是虚拟地址空间里面内核的起始地址。第二项也指向 level3_ident_pgt,直接映射区
第三项:PGD_START_KERNEL
__START_KERNEL_map 是虚拟地址空间里面内核代码段的起始地址。第三项指向 level3_kernel_pgt,内核代码区
vmalloc 和 kmap_atomic 原理
在虚拟地址空间里面,有个 vmalloc 区域,从 VMALLOC_START 开始到 VMALLOC_END,可以用于映射一段物理内存。
1 /** 2 * vmalloc - allocate virtually contiguous memory 3 * @size: allocation size 4 * Allocate enough pages to cover @size from the page level 5 * allocator and map them into contiguous kernel virtual space. 6 * 7 * For tight control over page level allocator and protection flags 8 * use __vmalloc() instead. 9 */ 10 void *vmalloc(unsigned long size) 11 { 12 return __vmalloc_node_flags(size, NUMA_NO_NODE, 13 GFP_KERNEL); 14 } 15 16 17 static void *__vmalloc_node(unsigned long size, unsigned long align, 18 gfp_t gfp_mask, pgprot_t prot, 19 int node, const void *caller) 20 { 21 return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END, 22 gfp_mask, prot, 0, node, caller); 23 }
kmap_atomic 的实现:
1 void *kmap_atomic_prot(struct page *page, pgprot_t prot) 2 { 3 ...... 4 if (!PageHighMem(page)) 5 return page_address(page); 6 ...... 7 vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx); 8 set_pte(kmap_pte-idx, mk_pte(page, prot)); 9 ...... 10 return (void *)vaddr; 11 } 12 13 14 void *kmap_atomic(struct page *page) 15 { 16 return kmap_atomic_prot(page, kmap_prot); 17 } 18 19 20 static __always_inline void *lowmem_page_address(const struct page *page) 21 { 22 return page_to_virt(page); 23 } 24 25 26 #define page_to_virt(x) __va(PFN_PHYS(page_to_pfn(x)
1. 如果是 32 位有高端地址的,就需要调用 set_pte 通过内核页表进行临时映射;
2. 如果是 64 位没有高端地址的,就调用 page_address,里面会调用 lowmem_page_address。
3. 其实低端内存的映射,会直接使用 __va 进行临时映射。
内核态缺页异常
kmap_atomic 发现,没有页表的时候,就直接创建页表进行映射了。而 vmalloc 没有,它只分配了内核的虚拟地址。所以,访问它的时候,会产生缺页异常
内核态的缺页异常还是会调用 do_page_fault,这个函数前面分析了已经,
说一下vmalloc_fault:
1 /* 2 * 32-bit: 3 * 4 * Handle a fault on the vmalloc or module mapping area 5 */ 6 static noinline int vmalloc_fault(unsigned long address) 7 { 8 unsigned long pgd_paddr; 9 pmd_t *pmd_k; 10 pte_t *pte_k; 11 12 13 /* Make sure we are in vmalloc area: */ 14 if (!(address >= VMALLOC_START && address < VMALLOC_END)) 15 return -1; 16 17 18 /* 19 * Synchronize this task's top level page-table 20 * with the 'reference' page table. 21 * 22 * Do _not_ use "current" here. We might be inside 23 * an interrupt in the middle of a task switch.. 24 */ 25 pgd_paddr = read_cr3_pa(); 26 pmd_k = vmalloc_sync_one(__va(pgd_paddr), address); 27 if (!pmd_k) 28 return -1; 29 30 31 pte_k = pte_offset_kernel(pmd_k, address); 32 if (!pte_present(*pte_k)) 33 return -1; 34 35 36 return 0
总结:
1、物理内存管理
物理内存根据 NUMA 架构分节点。每个节点里面再分区域。每个区域里面再分页。
物理页面通过伙伴系统进行分配。分配的物理页面要变成虚拟地址让上层可以访问,kswapd 可以根据物理页面的使用情况对页面进行换入换出。
2、内存分配内核态
对于内核态,kmalloc 在分配大内存的时候,以及 vmalloc 分配不连续物理页的时候,直接使用伙伴系统,分配后转换为虚拟地址,访问的时候需要通过内核页表进行映射
对于 kmem_cache 以及 kmalloc 分配小内存,则使用 slub 分配器,将伙伴系统分配出来的大块内存切成一小块一小块进行分配。
kemem_cache和kmalloc的部分不会被换出、因为用这两个函数分配的内存多用于保存内核关键的数据结构,内核中vmalloc分配的部分会被换出,因而当访问的时候、发现不再
就会调用do_page_fault
3、内存分配用户态
对于用户态的内存分配,或者直接调用 mmap 系统调用分配,或者调用 malloc。调用 malloc 的时候,如果分配小的内存,就用 sys_brk 系统调用;如果分配大的内存,还是用 sys_mmap 系统调用。
正常情况下,用户态的内存都是可以换出的,因而一旦发现内存中不存在,就会调用 do_page_fault
4、内存分配体系总图