Linux内存管理--高端内存映射与非连续内存分配

系统 Linux
返回页框线性地址的页分配函数对于高端内存是无效的,因为高端内存不会自动的映射到某个线性地址。内核可以采用三种方式来使用高端物理内存:永久内核映射,临时内核映射和非连续内存分配。

对于32位的机器来说,高于896的物理内存在内核中属于高端内存,并没有对内存做一一的映射,系统保留了128M的线性地址空间来临时映射这些高于896M的高端物理内存,该线性地址为3G+768m~4G。返回页框线性地址的页分配函数对于高端内存是无效的,因为高端内存不会自动的映射到某个线性地址。例如__get_free_pages(GFP_HIGH_MEM,0)函数分配高端内存页框时,返回的是NULL;内核可以采用三种方式来使用高端物理内存:***内核映射,临时内核映射和非连续内存分配。建立***内核映射可能会阻塞当前进程的执行,这发生在没有高端内存没有空闲的页表项来做映射的情况下,因此在中断等不能阻塞的代码中不要使用***内核映射。临时内核映射不会发生阻塞的情况,但必须保证没有其他的内核路径在使用同样的临时内核映射。


一、***内存映射

***内核映射使用的是内核主页表中的一个专门的页表,其地址存放在pkmap_page_table中,页表的页表项由宏LAST_PKMAP产生,页表中包含512或者1024项。

该页表映射的线性地址从PKMAP_BASE开始,pkmap_count数组包含了LAST_PKMAP个计数器,pkmap_page_table页表中的每项都有对应一个计数值:

计数器为0:对应的页表项是空闲可用的。

计数器为1:对应的页表项没有映射任何高端内存,但是它不能够使用,因为自从***一次使用以来,其相应的TLB尚未被刷新。

计数器为n:有多个内核成分使用该页表项所对应的页框。

源码分析:

  1. void fastcall *kmap_high(struct page *page) 
  2. unsigned long vaddr; 
  3.  
  4.  
  5. spin_lock(&kmap_lock); 
  6. //page->virtual记录了页框对应的线性地址 
  7. vaddr = (unsigned long)page_address(page); 
  8. //若页框未被映射过,分配新的空闲页表项 
  9. if (!vaddr) 
  10. vaddr = map_new_virtual(page); 
  11. //若是刚分配到了空闲页表项的话,在map_new_virtual()中其count 
  12. //值被设置为了1,在这里再次++ 
  13. pkmap_count[PKMAP_NR(vaddr)]++; 
  14. BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2); 
  15. spin_unlock(&kmap_lock); 
  16. return (void*) vaddr; 
  17. static inline unsigned long map_new_virtual(struct page *page) 
  18. unsigned long vaddr; 
  19. int count; 
  20.  
  21.  
  22. start: 
  23. count = LAST_PKMAP; 
  24. //寻找一个空的页表项 
  25. for (;;) { 
  26. //从上一次找到的空闲页表项的位置开始寻找 
  27. last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK; 
  28. if (!last_pkmap_nr) { 
  29. flush_all_zero_pkmaps(); 
  30. count = LAST_PKMAP; 
  31. //找到一个未用的空闲页表项 
  32. if (!pkmap_count[last_pkmap_nr]) 
  33. break;  /* Found a usable entry */ 
  34. //count变为0的话,意味着当前没有空闲的页表项 
  35. if (--count) 
  36. continue
  37. //没有找到空闲的页表项,将当前进程加入到等待队列,进行调度,直到 
  38. //有空闲的页表项或者该页面被别人映射 
  39. DECLARE_WAITQUEUE(wait, current); 
  40.  
  41.  
  42. __set_current_state(TASK_UNINTERRUPTIBLE); 
  43. add_wait_queue(&pkmap_map_wait, &wait); 
  44. spin_unlock(&kmap_lock); 
  45. schedule(); 
  46. remove_wait_queue(&pkmap_map_wait, &wait); 
  47. spin_lock(&kmap_lock); 
  48. //有可能在该进程睡眠期间,有其它进程对该页面做了内存映射 
  49. if (page_address(page)) 
  50. return (unsigned long)page_address(page); 
  51.  
  52.  
  53. /* Re-start */ 
  54. goto start; 
  55. //得到对应页表项对应的线性地址 
  56. vaddr = PKMAP_ADDR(last_pkmap_nr); 
  57. //设置对应的页表项 
  58. set_pte_at(&init_mm, vaddr, 
  59.   &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); 
  60. //设置***内存映射数组的值 
  61. pkmap_count[last_pkmap_nr] = 1; 
  62. //将page->virtual的值设为vaddr,ok 
  63. set_page_address(page, (void *)vaddr); 
  64.  
  65.  
  66. return vaddr; 

二、临时内核映射

临时内核映射比较简单,在内核中,为每个cpu都保存了一组页表项,每个页表项由一个特定的内核成分使用,需要注意的是,不同的内核控制路径不应该同时使用一个页表项,这样的话,会使后一个内核控制路径将前一个内核控制路径设置页表项给冲掉。

建立临时内核映射使用kmap_atomic()函数。

  1. void *__kmap_atomic(struct page *page, enum km_type type) 
  2. enum fixed_addresses idx; 
  3. unsigned long vaddr; 
  4.  
  5.  
  6. //禁止内核抢占,以预防不同内核控制路径使用同一页表项 
  7. inc_preempt_count(); 
  8. //非高端内存,不用进行高端内存映射 
  9. if (!PageHighMem(page)) 
  10. return page_address(page); 
  11. //得到使用的页表项的下表索引 
  12. idx = type + KM_TYPE_NR*smp_processor_id(); 
  13. //得到相关页表项的线性地址 
  14. vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx); 
  15. //设置对应的页表项 
  16. set_pte(kmap_pte-idx, mk_pte(page, kmap_prot)); 
  17. local_flush_tlb_one((unsigned long)vaddr); 
  18.  
  19.  
  20. return (void*) vaddr; 

三、非连续内存分配

下图显示了如何使用高于0xc0000000线性地址的线性地址空间:

  1. 内存区的开始部分包含的是对前896MB的RAM进行映射的线性地址,直接映射的物理内存的末尾的线性地址保存在high_memory变量中。
  2. 内存区的结尾位置包含的是固定映射的线性地址。
  3. 从PKMAP_BASE开始,是用于高端内存***映射的线性地址。
  4. 其余的线性地址用于非连续内存区,在物理内存映射和***个内存区间有一个8M的安全区,用于捕捉对内存的越界访问,同样道理,插入其它4KB大小的内存区来隔离非连续内存区。

非连续内存区描述符数据结构:

  1. struct vm_struct { 
  2. void     *addr;//内存区***个内存单元的线性地址 
  3. unsigned long    size;//内存区的大小加上4K,4K是用来检查越界的内存 
  4. unsigned long     flags;//非连续内存的类型,VM_ALLOC表示使用vmalloc分配的内存,VM_MAP表示使用vmap分配的内存, 
  5.      //VM_IOREMAP表示用ioremap()分配的内存 
  6. struct page  **pages;//非连续内存的的物理页数组 
  7. unsigned int     nr_pages;//非连续内存的物理页的个数 
  8. unsigned long    phys_addr; 
  9. struct vm_struct    *next;//用来将各个非连续内存描述符串联起来 
  10. }; 

1、分配非连续的内存区

分配函数主要是vmalloc(),vmap(),vmalloc()会去调用__vmalloc_node()函数:

  1. void *__vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot, 
  2. int node) 
  3. struct vm_struct *area; 
  4. //size要对其为4K的整数倍,因为非连续内存区域是将各个物理页进行映射 
  5. size = PAGE_ALIGN(size); 
  6. if (!size || (size >> PAGE_SHIFT) > num_physpages) 
  7. return NULL; 
  8. //找到一块空闲的线性地址区域,用来映射该非连续内存 
  9. area = get_vm_area_node(size, VM_ALLOC, node); 
  10. if (!area) 
  11. return NULL; 
  12.  
  13.  
  14. return __vmalloc_area_node(area, gfp_mask, prot, node); 
  15.  
  16.  
  17. void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask, 
  18. pgprot_t prot, int node) 
  19. struct page **pages; 
  20. unsigned int nr_pages, array_size, i; 
  21. //计算要映射的物理页数 
  22. nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT; 
  23. //计算vm_struct中pages数组的数组元素个数 
  24. array_size = (nr_pages * sizeof(struct page *)); 
  25. //记录下物理页面的数目 
  26. area->nr_pages = nr_pages; 
  27. //为vm_struct中的pages数组分配内存 
  28. if (array_size > PAGE_SIZE) { 
  29. pages = __vmalloc_node(array_size, gfp_mask, PAGE_KERNEL, node); 
  30. area->flags |= VM_VPAGES; 
  31. else 
  32. pages = kmalloc_node(array_size, (gfp_mask & ~__GFP_HIGHMEM), node); 
  33. area->pages = pages; 
  34. if (!area->pages) { 
  35. remove_vm_area(area->addr); 
  36. kfree(area); 
  37. return NULL; 
  38. memset(area->pages, 0, array_size); 
  39. //为非连续内存进行页面的分配,每次分配一个页面,将其页框指针记录在pages数组中 
  40. for (i = 0; i < area->nr_pages; i++) { 
  41. if (node < 0) 
  42. area->pages[i] = alloc_page(gfp_mask); 
  43. else 
  44. area->pages[i] = alloc_pages_node(node, gfp_mask, 0); 
  45. if (unlikely(!area->pages[i])) { 
  46. /* Successfully allocated i pages, free them in __vunmap() */ 
  47. area->nr_pages = i; 
  48. goto fail; 
  49. //将各个物理页框映射到分配好的空闲线性区里面去 
  50. if (map_vm_area(area, prot, &pages)) 
  51. goto fail; 
  52. return area->addr; 
  53.  
  54.  
  55. fail: 
  56. vfree(area->addr); 
  57. return NULL; 

__vmalloc_node()并不触及当前进程的页表,因此当内核态进程访问非连续内存区时,会发生缺页异常,因为对应的进程的相应地址对应的页表项为空。当缺页异常发生时,异常处理程序会到内核主页表(init_mm.pgd页全局目录)中去查看是否有对应的页表项,有的话,就会修改当前进程的页表项,并继续进程的执行。

2、释放非连续的内存区

  1. void vfree(void *addr) 
  2. BUG_ON(in_interrupt()); 
  3. __vunmap(addr, 1); 
  4. void __vunmap(void *addr, int deallocate_pages) 
  5. struct vm_struct *area; 
  6.  
  7.  
  8. if (!addr) 
  9. return
  10. //释放的地址应该是4k的整数倍 
  11. if ((PAGE_SIZE-1) & (unsigned long)addr) { 
  12. printk(KERN_ERR "Trying to vfree() bad address (%p)\n", addr); 
  13. WARN_ON(1); 
  14. return
  15. //移除对应的vm_area数据描述符,解除对各个物理页面的页面映射项 
  16. area = remove_vm_area(addr); 
  17. if (unlikely(!area)) { 
  18. printk(KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n"
  19. addr); 
  20. WARN_ON(1); 
  21. return
  22.  
  23.  
  24. debug_check_no_locks_freed(addr, area->size); 
  25. //需要向伙伴系统归还非连续的物理页 
  26. if (deallocate_pages) { 
  27. int i; 
  28. //将各个物理页面归还给伙伴系统 
  29. for (i = 0; i < area->nr_pages; i++) { 
  30. BUG_ON(!area->pages[i]); 
  31. __free_page(area->pages[i]); 
  32.  
  33.  
  34. if (area->flags & VM_VPAGES) 
  35. vfree(area->pages); 
  36. else 
  37. kfree(area->pages); 
  38.  
  39.  
  40. kfree(area); 
  41. return

与vmalloc()一样,该函数修改的是主内核页全局目录和它的页表表项,内核永远不会回收页全局,页上级,页中间目录,也不会回收页表,而进程的页表会指向这些表项。这样的话,假设一个内核进程访问已经释放的非连续内存,最终就会访问到已经被清空的页表表项,从而引发缺页异常,这就是一个错误。

责任编辑:奔跑的冰淇淋 来源: ChinaUnix博客
相关推荐

2009-10-19 09:45:06

linux内存存管理

2023-10-18 13:31:00

Linux内存

2018-05-18 09:07:43

Linux内核内存

2013-10-12 11:15:09

Linux运维内存管理

2011-12-20 10:43:21

Java

2013-10-11 17:32:18

Linux运维内存管理

2009-06-03 15:52:34

堆内存栈内存Java内存分配

2021-07-14 10:00:32

Python内存测量

2021-10-15 08:51:09

Linux内存 Kmalloc

2021-04-27 13:56:49

内存.映射地址

2022-03-07 10:54:34

内存Linux

2023-09-05 09:36:19

2009-06-16 11:11:07

Java内存管理Java内存泄漏

2018-07-23 09:26:08

iOS内存优化

2010-09-25 14:12:50

Java内存分配

2017-05-18 16:30:29

Linux内存管理

2023-02-02 09:38:37

VMSTAT命令内存

2021-02-28 13:22:54

Java内存代码

2013-03-28 09:55:37

Java对象

2022-08-08 08:31:00

Linux内存管理
点赞
收藏

51CTO技术栈公众号