1、 进程可以寻址 4G,其中 03G 为用户态,3G4G 为内核态。如果内存不超过 1G 那么最后这 1G 线性空间足够映射物理内存了,如果物理内存大于 1G,为了使内核空间的 1G 线性地址可以访问到大于1G 的物理内存,把物理内存分为两部分,0896MB 的进行直接内存映射,也就是说存在一个线性关系:virtual address = physical address + PAGE_OFFSET,这里的 PAGE_OFFSET 为 3G。还剩下一个 128MB 的空间,这个空间作为一个窗口动态进行映射,这样就可以访问大于 1G 的内存,但是同一时刻内核空间还是只有 1G 的线性地址,只是不同
2、时刻可以映射到不同的地方。综上,大于896MB 的物理内存就是高端内存,内核引入高端内存这个概念是为了通过 128MB 这个窗口访问大于 1G 的物理内存。上图是内核空间 1G 线性地址的布局,直接映射区为PAGE_OFFSETPAGE_OFFSET+ 896MB,直接映射的物理地址末尾对应的线性地址保存在 high_memory 变量中。直接映射区后边有一个 8MB 的保护区,目的是用来“ 捕获“ 对内存的越界访问。然后是非连续内存区,范围从 VMALLOC_STARTVMALLOC_END,出于同样的原因,每个非连续内存区之间隔着 4KB。永久内核映射区从PKMAP_BASE 开始,大小为
3、 2MB(启动 PAE)或 4MB。后边是固定映射区,范围是 FIXADDR_STARTFIXADDR_TOP,至于临时内核映射区是固定内核映射区里的一部分,在后边会做详细解析。下边来详细介绍高端内存的三种访问方式:非连续内存区访问,永久内核映射,临时内核映射。非连续内存区访问:非连续内存区访问会使用一个 vm_struct 结构来描述每个非连续内存区:view plainprint?1. struct vm_struct 2. void *addr; /内存区内第一个内存单元的线性地址 3. unsigned long size; /内存区的大小加 4096(内存区之间的安全区间的大小) 4
4、. unsigned long flags; /非连续的内存区映射的内存类型 5. struct page *pages; /指向nr_pages 数组的指针,该数组由指向页描述符的指针组成 6. unsigned int nr_pages; /内存区填充的页的个数 7. unsigned long phys_addr; /该字段为 0,除非内存已被创建来映射一个硬件设备的 I/O 共享内存 8. struct vm_struct *next; /指向下一个vm_struct 结构的指针 9. ; struct vm_struct void unsigned long unsigned lon
5、g struct page unsigned int vm_struct 与 vmalloc()分配的非连续线性区有如下关系:下边来看非连续内存区的分配,分配调用了 vmalloc()函数:view plainprint?1. void *vmalloc(unsigned long size) 2. 3. return _vmalloc(size, GFP_KERNEL | _GFP_HIGHMEM, PAGE_KERNEL); 4. void *vmalloc(unsigned long sizreturn _vmalloc(size, Gflags 标志中设置了从 high memory
6、分配。view plainprint?1. void *_vmalloc(unsigned long size, int gfp_mask, pgprot_t prot) 2. 3. struct vm_struct *area; 4. struct page *pages; 5. unsigned int nr_pages, array_size, i; 6. /*需要分配的大小页对齐*/ 7. size = PAGE_ALIGN(size); 8. /*需要分配的大小不能为 0,也不能大于物理页的总数量*/ 9. if (!size | (size PAGE_SHIFT) num_phys
7、pages) 10. return NULL; 11. /*找到一个线性区,并获得 vm_struct 描述符,描述符的flags 字段被初始化为 VM_ALLOC 标志,该标志意味着通过使用 vmalloc()函数*/ 12. area = get_vm_area(size, VM_ALLOC); 13. if (!area) 14. return NULL; 15. /*需要分配页的数量*/ 16. nr_pages = size PAGE_SHIFT; 17. /*数组的大小*/ 18. array_size = (nr_pages * sizeof(struct page *); 19
8、. 20. area-nr_pages = nr_pages; 21. /* Please note that the recursion is strictly bounded. */ 22. /*如果数组的大小大于页的大小就从非连续内存区分配,否则从 kmalloc 分配*/ 23. if (array_size PAGE_SIZE) 24. pages = _vmalloc(array_size, gfp_mask, PAGE_KERNEL); 25. else 26. pages = kmalloc(array_size, (gfp_mask 27. area-pages = page
9、s; 28. /*如果这个数组没有分配到内存,就释放 vm_struct 描述符,返回*/ 29. if (!area-pages) 30. remove_vm_area(area-addr); 31. kfree(area); 32. return NULL; 33. 34. /*清空*/ 35. memset(area-pages, 0, array_size); 36. /*从高端内存非配每一个物理页,将描述符的 pages 字段的每一项指向得到的物理页的 page 结构*/ 37. for (i = 0; i nr_pages; i+) 38. area-pagesi = alloc_
10、page(gfp_mask); 39. if (unlikely(!area-pagesi) 40. /* Successfully allocated i pages, freethem in _vunmap() */ 41. area-nr_pages = i; 42. goto fail; 43. 44. 45. /*建立页表与物理页之间的映射,一级一级的很复杂*/ 46. if (map_vm_area(area, prot, 48. return area-addr; 49. 50.fail: 51. vfree(area-addr); 52. return NULL; 53. vo
11、id *_vmalloc(unsigned long sstruct vm_struct *area; struct page *pages;unsigned int nr_pages, /*需 要 分 配 的 大 小 页 对 齐 */调用 get_vm_area()分配描述符和获得线性地址空间,get_vm_area()函数在线性地址 VMALLOC_START 和VMALLOC_END 之间查找一个空闲区域。步骤如下:1. 调用 kmalloc()为 vm_struct 类型的新描述符获得一个内存区。2. 为写得到 vmlist_lock()锁,并扫描类型为 vm_struct 的描述符链
12、表来查找线性地址一个空闲区域,至少覆盖 size + 4096 个地址。 3. 如果存在这样一个区间,函数就初始化描述符的字段,释放vmlist_lock 锁,并以返回这个非连续内存区的起始地址而结束。4. 否则,get_vm_area()释放先前得到的描述符,释放 vmlist_lock,然后返回 NULL。调用 map_vm_area()建立页表与物理页之间的映射:view plainprint?1. int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page *pages) 2. 3. unsigned long
13、address = (unsigned long) area-addr; 4. unsigned long end = address + (area-size-PAGE_SIZE); 5. unsigned long next; 6. pgd_t *pgd; 7. int err = 0; 8. int i; 9. /*得到主内核页表中 pgd 中的相应项的线性地址*/ 10. pgd = pgd_offset_k(address); 11. /*获得主内核页表自旋锁*/ 12. spin_lock( 13. for (i = pgd_index(address); i end) 23. n
14、ext = end; 24. /*建立这个页上级目录所对应的所有页中间目录*/ 25. if (map_area_pud(pud, address, next, prot, pages) 26. err = -ENOMEM; 27. break; 28. 29. 30. address = next; 31. pgd+; 32. 33. 34. spin_unlock( 35. flush_cache_vmap(unsigned long) area-addr, end); 36. return err; 37. int map_vm_area(struct vm_strucunsigned
15、long address =unsigned long end = addunsigned long next;pgd_t *pgd;map_area_pud 也就是反复调用 map_area_pmd 来填充各级页目录,页表。map_area_pte()的主循环如下:view plainprint?1. do 2. struct page *page = *pages; 3. WARN_ON(!pte_none(*pte); 4. if (!page) 5. return -ENOMEM; 6. 7. set_pte(pte, mk_pte(page, prot); 8. address +=
16、 PAGE_SIZE; 9. pte+; 10. (*pages)+; 11. while (address end); do struct page *paWARN_ON(!pte_noif (!page)return 调用 set_pte 设置将相应页的页描述符地址设置到相应的页表项。非连续内存区的释放:view plainprint?1. #define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot)2. #define pfn_pte(pfn, prot) _pte(pfn) PAGE_SHIFT) | pgprot_v
17、al(prot) 3. #define set_pte(pteptr, pteval) (*(pteptr) = pteval) #define mk_pte(page, pgprot) pf#define pfn_pte(pfn, prot) _pt#define set_pte(pteptr, pteval)需要注意的是,map_vm_area()并不触及当前进程的页表。非连续内存区的释放:调用 vfree()函数:view plainprint?1. void vfree(void *addr) 2. 3. BUG_ON(in_interrupt(); 4. _vunmap(addr,
18、1); 5. void vfree(void *addr)BUG_ON(in_interrupt();_vunmap(addr, 1);_vunmap()调用 remove_vm_area(),执行与 map_vm_area()相反的操作,最终调用到了 unmap_area_pte():view plainprint?1. do 2. pte_t page; 3. page = ptep_get_and_clear(pte); 4. address += PAGE_SIZE; 5. pte+; 6. if (pte_none(page) 7. continue; 8. if (pte_pres
19、ent(page) 9. continue; 10. printk(KERN_CRIT “Whee Swapped out page in kernel page tablen“); 11. while (address end); do pte_t page;page = ptep_get_and_cleaddress += PAGEpte+;if (pte_none(pa这里调用 ptep_get_and_clear()宏将 pte指向的页表项设为 0。注意,在调用 vmalloc()时建立的映射是在物理内存和主内核页表之间的,并没有涉及到进程的页表。当内核态的进程访问非连续内存区时,缺页
20、发生, 因为该内存区所对应的进程页表中的表项为空。然而,缺页处理程序要检查这个缺页线性地址是否在主内核页表中。一旦处理程序发现一个主内核页表含有这个线性 地址的非空项,就把它的值拷贝到相应的进程页表项中,并恢复进程的正常执行。在调用vfree 时,与 vmalloc()一样,内核修改主内核页全局目录和 它的子页表中相应的项,但是映射第 4 个 GB 的进程页表的项保持不变。unmap_area_pte()函数只是清除页表中的项(不回收页表本身)。进程对 已释放非连续内存区的进一步访问必将由于空的页表项而触发缺页异常。永久内核映射:永久内核映射使用主内核页表中一个专门的页表,其地址存放在 pkm
21、ap_page_table 变量中,页表中的表项数由 LAST_PKMAP 宏产生,因此 内核一次访问 2MB(启动 PAE)或 4MB 的高端内存。该页表映射的线性地址从 PKMAP_BASE 开始,pkmap_count 数组包含 LAST_PKMAP 个计数器,pkmap_page_table 页表中每一项都有一个。计数器为 0对应的页表项没有映射任何高端内存页框,并且是可用的。计数器为 1对应的页表项没有映射任何高端内存页框,但是它不能使用,因为此从他最后一次使用以来,其相应的 TLB 表项还未被刷新。计数器为 2相应的页表项映射一个高端内存页框,这意味着正好有 n-1 个内核成分在使
22、用这个页框。view plainprint?1. struct page_address_map 2. struct page *page; 3. void *virtual; 4. struct list_head list; 5. ; 6. 7. static struct page_address_slot 8. struct list_head lh; /* List of page_address_maps */ 9. spinlock_t lock; /* Protect this buckets list */ 10. _cacheline_aligned_in_smp page
23、_address_htable1PA_HASH_ORDER; struct page_address_map struct page *page;void *virtual;struct list_head list;view plainprint?1. static struct page_address_slot *page_slot(struct page *page) 2. 3. return 4. static struct page_address_slotreturn 4. if (!PageHighMem(page) 5. return page_address(page);
24、6. return kmap_high(page); 7. void *kmap(struct page *page)might_sleep();if (!PageHighMem(page)return page_addreturn kmap_high(page);kmap 函数只是一个前端,用于确定指定的页是否确实在高端内存域中。首先判断是不是高端内存,如果不是高端内存就调用 page_address 直接返 回 page 对应的线性地址,0896MB 的是直接映射,也就是在 kernel 初始化的时候映射已经建立好了,之后直接访问就行了。而高端内存需要自己建 立映射,然后才能访问。如果是高
25、端内存则将工作委托给 kmap_high:view plainprint?1. void fastcall *kmap_high(struct page *page) 2. 3. unsigned long vaddr; 4. 5. /* 6. * For highmem pages, we cant trust “virtual“ until 7. * after we have the lock. 8. * 9. * We cannot call this from interrupts, as it may block 10. */ 11. spin_lock( 12. vaddr =
26、(unsigned long)page_address(page); 13. if (!vaddr) 14. vaddr = map_new_virtual(page); 15. pkmap_countPKMAP_NR(vaddr)+; 16. if (pkmap_countPKMAP_NR(vaddr) 2) 17. BUG(); 18. spin_unlock( 19. return (void*) vaddr; 20. void fastcall *kmap_high(structunsigned long vaddr;/* * For highmem pages, w首先获得需要映射的
27、 page 对应的线性地址,从 page_address_htable 中进行查找,如果已经映射过了肯定不为空,如果没有映射过则执行 map_new_virtual 进行映射。注意这里进行的 pkmap_count 加一操作,后边会提到。view plainprint?1. static inline unsigned long map_new_virtual(struct page *page) 2. 3. unsigned long vaddr; 4. int count; 5. 6. start: 7. count = LAST_PKMAP; 8. /* Find an empty en
28、try */ 9. for (;) 10. last_pkmap_nr = (last_pkmap_nr + 1) 11. if (!last_pkmap_nr) 12. flush_all_zero_pkmaps(); 13. count = LAST_PKMAP; 14. 15. if (!pkmap_countlast_pkmap_nr) 16. break; /* Found a usable entry */ 17. if (-count) 18. continue; 19. 20. /* 21. * Sleep for somebody else to unmap their en
29、tries 22. */ 23. 24. DECLARE_WAITQUEUE(wait, current); 25. 26. _set_current_state(TASK_UNINTERRUPTIBLE); 27. add_wait_queue( 28. spin_unlock( 29. schedule(); 30. remove_wait_queue( 31. spin_lock( 32. 33. /* Somebody else might have mapped it while we slept */ 34. if (page_address(page) 35. return (u
30、nsigned long)page_address(page); 36. 37. /* Re-start */ 38. goto start; 39. 40. 41. vaddr = PKMAP_ADDR(last_pkmap_nr); 42. set_pte( 43. 44. kmap_countlast_pkmap_nr = 1; 45. set_page_address(page, (void *)vaddr); 46. 47. return vaddr; 48. static inline unsigned long mapunsigned long vaddr;int count;s
31、tart:内核将上次使用过的页表项的索引保存在 last_pkmap_nr 变量中,避免了重复查找。如果找到计数器为0 的,则获得这个页表项对应的页表的线性地 址,填充相应的页表项,计数器为 1。这里产生一个问题,刚才不是说为 1 代表“对应的页表项没有映射任何高端内存页框,但是它不能使用,因为此从他最后一次 使用以来,其相应的 TLB 表项还未被刷新。”,确实是这样,看看调用 map_new_virtual 函数的 kmap_high 函数,在这里对 pkmap_count进行了再次加一。view plainprint?1. void kunmap(struct page *page) 2.
32、 3. if (in_interrupt() 4. BUG(); 5. if (!PageHighMem(page) 6. return; 7. kunmap_high(page); 8. void kunmap(struct page *page)if (in_interrupt()BUG();if (!PageHighMem(page)return;kmap 会导致进程阻塞,所以永久内核映射不能运行在中断处理程序中和可延迟函数的内部。所以这里kunmap 首先判断是不是在中断上下文中,如果不在中 断上下文,但在在高端内存中,则将工作委托 kunmap_high。如果有计数器为 1 的表项,
33、而且有等待的进程,则唤醒进程。如果有进程被阻塞那么肯定没 有计数器为 0 的表项了,所以从 map_new_virtual 中 start 标号的地方开始执行,应该会把表项遍历一圈,此时肯定会遇到 last_pkmap_nr 为0 的情况,此时就可以调用 flush_all_zero_pkmaps()函数来寻找计数器为 1 的表项,将其清零,解除映 射,并刷新 TLB。可以看到 kunmap并没有解除映射,并刷新 tlb,这里仅仅是将表项的计数器减一。为什么要这样做呢?看一下 kmap_high 函数 中,判断了一次是否已经映射了,如果映射了就把表项的引用计数器加一,也就是说如果调用过kunm
34、ap 使计数为 1 了,此时又变成 2,又可以进行访问了,不 用重新填充页表,刷新 tlb 等。view plainprint?1. void fastcall kunmap_high(struct page *page) 2. 3. unsigned long vaddr; 4. unsigned long nr; 5. int need_wakeup; 6. 7. spin_lock( 8. vaddr = (unsigned long)page_address(page); 9. if (!vaddr) 10. BUG(); 11. nr = PKMAP_NR(vaddr); 12. 1
35、3. /* 14. * A count must never go down to zero 15. * without a TLB flush! 16. */ 17. need_wakeup = 0; 18. switch (-pkmap_countnr) 19. case 0: 20. BUG(); 21. case 1: 22. need_wakeup = waitqueue_active( 23. 24. spin_unlock( 25. 26. /* do wake-up, if needed, race-free outside of the spin lock */ 27. if (need_wakeup) 28. wake_up( 29. void fastcall kunmap_high(strucunsigned long vaddr;unsigned long nr;int need_wakeup;分享到: 上