释放线性地址区间

来源:互联网 发布:淘宝卖家直播条件 编辑:程序博客网 时间:2024/05/16 07:45

内核使用do_munmap()函数从当前进程的地址空间中删除一个线性地址区间。

 

1 do_munmap()函数

 

该参数为:进程内存描述符的地址mm,地址区间的起始地址start和它的长度len。要删除的区间并不总是对应一个线性区,它或许是一个线性区的一部分,或许跨越两个或多个线性区。

 

该函数经过两个主要的阶段。第一阶段(第1一6步),扫描进程所拥有的线性区链表,并把包含在进程地址空间的线性地址区间中的所有线性区从链表中解除链接。第二阶段(第7~10步),更新进程的页表,并把第一阶段找到并标识出的线性区删除。函数利用稍后要说明的split_vma()和unmap_region()函数。do_munmap()执行下面的步骤:

 

int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
{
 unsigned long end;
 struct vm_area_struct *vma, *prev, *last;
 /* 1.参数值进行一些初步检查:如果线性地址区间所含的地址大于TASK_SIZE,
  * 如start不是4096的倍数,或者如果线性地址区间的长度为0,则函数返回一个错误代码-EINVAL。
  */
 if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE-start)
  return -EINVAL;

 if ((len = PAGE_ALIGN(len)) == 0)
  return -EINVAL;

 /* Find the first overlapping VMA */
 /* 2.定要删除的线性地址区间之后第一个线性区结构vma的位置(mpnt->end > start),如果有这样的线性区:*/
 vma = find_vma_prev(mm, start, &prev);
 if (!vma)
  return 0;
 /* we have  start < vma->vm_end 
  * 3.如果没有这样的线性区,也没有与线性地址区间重叠的线性区,就什么都不做,因为该线性地址区间上没有线性区:*/

 /* if it doesn't overlap, we have nothing.. */
 end = start + len;
 if (vma->vm_start >= end)
  return 0;

 /*
  * If we need to split any vma, do it now to save pain later.
  *
  * Note: mremap's move_vma VM_ACCOUNT handling assumes a partially
  * unmapped vm_area_struct will remain in use: so lower split_vma
  * places tmp vma above, and higher split_vma places tmp vma below.
  * 4.如果线性区的起始地址在线性区vma内,就调用split_vma()(在下面说明)把线性区vma划分成两个较小的区:
  * 一个区在线性地址区间外部,而另一个在区间内部。
  */
 if (start > vma->vm_start) {
  int error = split_vma(mm, vma, start, 0);
  if (error)
   return error;
  prev = vma;
  /* 更新局部变量prev,以前它存储的是指向线性区vma前面一个线性区的指针,
   * 现在要让它指向vma,即指向线性地址区间外部的那个新线性区。
   * 这样,prev仍然指向要删除的第一个线性区前面的那个线性区。
   */
 }

 /* Does it split the last one? */
 /* 5.如果线性地址区间的结束地址在一个线性区内部,
  * 就再次调用split_vma()把最后重叠的那个线性区划分成两个较小的区:
  * 一个区在线性地址区间内部,而另一个在区间外部。
  * (如果线性地址区间正好包含在某个线性区内部,就必须用用个较小的新线性区取代该线性区。
  * 当发生这种情况时,在第4步和第5步把该线性区分成三个较小的线性区:
  * 删除中间的那个线性区,而保留第一个和最后一个线性区。):*/
 last = find_vma(mm, end);
 if (last && end > last->vm_start) {
  int error = split_vma(mm, last, end, 1);
  if (error)
   return error;
 }
 /* 6.更新vma的值,使它指向线性地址区间的第一个线性区。
  * 如果prev为NULL,即没有上述线性区,就从mm->mmap获得第一个线性区的地址:*/
 vma = prev? prev->vm_next: mm->mmap;

 /*
  * Remove the vma's, and unmap the actual pages
  *
  * 7.调用detach_vmas_to_be_unmapped()从进程的线性地址空间中删除位于线性地址区间中的线性区。
  * 该函数本质上执行下面的流程:要删除的线性区的描述符放在一个排好序的链表中,
  * 局部变量mpnt指向该链表的头(实际上,这个链表只是进程初始线性区链表的一部分)。
 detach_vmas_to_be_unmapped(mm, vma, prev, end);
 /* 8.调用unmap_region()清除与线性地址区间对应的页表项并释放相应的页框(稍后讨论):*/
 unmap_region(mm, vma, prev, start, end);

 /* Fix up all other VM information
  * 9.释放在第7步建立链表时收集的线性区描述符:*/
 remove_vma_list(mm, vma);
 /* 10.返回0(成功)。 */
 return 0;
}

 

2 split_vma()函数

 

split_vma()函数是一个很重要的函数,它的功能是把与线性地址区间交叉的线性区划分成两个较小的区,一个在线性地址区间外部,另一个在区间的内部。该函数接收4个参数:内存描述符指针mm,线性区描述符指针vma(标识要被划分的线性区),表示区间与线性区之间交叉点的地址addr,以及表示区间与线性区之间交叉点在区间起始处还是结束处的标志new_below。我们来看看该函数的代码:

 

int split_vma(struct mm_struct * mm, struct vm_area_struct * vma,
       unsigned long addr, int new_below)
{
 struct mempolicy *pol;
 struct vm_area_struct *new;

 if (is_vm_hugetlb_page(vma) && (addr & ~HPAGE_MASK))
  return -EINVAL;

 if (mm->map_count >= sysctl_max_map_count)
  return -ENOMEM;
 /* 1.调用kmem_cache_alloc()获得一个新的线性区描述符vm_area_struct,
  * 并把它的地址存放在新的局部变量中new,如果没有可用的空闲空间,就返回-ENOMEM。
  */
 new = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
 if (!new)
  return -ENOMEM;

 /* most fields are the same, copy all, and then fixup */
 /* 2.用vma描述符的字段值初始化新描述符的字段。*/
 *new = *vma;

 if (new_below)
  /* 3.如果new_below标志等于1,说明线性地址区间的结束地址在vma线性区的内部,
  * 因此必须把新线性区放在vma线性区的前面,
  * 所以函数把字段new->vm_end和vma->vm_start都赋值为addr。
  */
  new->vm_end = addr;
 else {
  /* 4.相反,new_below为0,说明线性地址区间的起始地址在vma线性区的内部,
   * 因此必须把新线性区放在vma线性区之后,
   * 所以函数把new->vm_start和vma->vm_end字段都赋值为addr。
   */
  new->vm_start = addr;
  new->vm_pgoff += ((addr - vma->vm_start) >> PAGE_SHIFT);
 }

 pol = mpol_copy(vma_policy(vma));
 if (IS_ERR(pol)) {
  kmem_cache_free(vm_area_cachep, new);
  return PTR_ERR(pol);
 }
 vma_set_policy(new, pol);

 if (new->vm_file)
  get_file(new->vm_file);

 if (new->vm_ops && new->vm_ops->open)
  /* 5.如果定义了新线性区的open方法,函数就执行它。 */
  new->vm_ops->open(new);

 /* 6.根据new_below的值调用vma_adjust函数把新线性区描述符链接到线性区链表mm->mmap和红-黑树mm->mm_rb。
  * 此外,vma_adjust函数还要根据线性区vma的最新大小对红-黑树进行调整。 */
 if (new_below) {
  unsigned long old_end = vma->vm_end;

  vma_adjust(vma, addr, vma->vm_end, vma->vm_pgoff +
   ((addr - new->vm_start) >> PAGE_SHIFT), new);
  if (vma->vm_flags & VM_EXEC)
   arch_remove_exec_range(mm, old_end);
 } else
  vma_adjust(vma, vma->vm_start, addr, vma->vm_pgoff, new);

 return 0;
}

 

3 unmap_region()函数

 

unmap_region()函数遍历线性区链表并释放它们的页框。该函数作用于5个参数:内存描述符指针mm,指向第一个被删除线性区描述符的指针vma,指向进程链表中vma前面的线性区的指针prev(参见do_munmap()执行步骤中心的第2步和第4步),以及两个地址start和end,它们界定被删除线性地址区间的范围。
static void unmap_region(struct mm_struct *mm,
  struct vm_area_struct *vma, struct vm_area_struct *prev,
  unsigned long start, unsigned long end)
{
 struct vm_area_struct *next = prev? prev->vm_next: mm->mmap;
 struct mmu_gather *tlb;
 unsigned long nr_accounted = 0;

 lru_add_drain();
 tlb = tlb_gather_mmu(mm, 0);
 update_hiwater_rss(mm);
 unmap_vmas(&tlb, vma, start, end, &nr_accounted, NULL);
 vm_unacct_memory(nr_accounted);
 free_pgtables(&tlb, vma, prev? prev->vm_end: FIRST_USER_ADDRESS,
     next? next->vm_start: 0);
 tlb_finish_mmu(tlb, start, end);
}

 

函数各步骤解释:


1.调用lru_add_drain()(页面回收中会提到)。
2.调用tlb_gather_mmu()函数初始化每CPU变量mmu_gathers。mmu_gathers的值依赖于CPU体系结构:通常该变量应该存放成功更新进程页表项所需要的所有信息。在80x86体系结构中,tlb_gather_mmu()函数只是简单地把内存描述符指针mm的值赋给本地CPU的mmu_gathers变量。
3.把mmu_gathers变量的地址保存在局部变量tlb中。
4.调用unmap_vmas()扫描线性地址空间的所有页表项:如果只有一个有效CPU,函数就调用free_swap_and_cache()反复释放相应的页(页面回收中会提到);否则,函数就把相应页描述符的指针保存在局部变量mmu_gathers中。
5.调用free_pgtables(tlb, prev, start, end)回收在上一步已经清空的进程页表。
6.调用tlb_finish_mmu(tlb, start, end)结束unmap_region()函数的工作,tlb_finish_mmu(tlb,  start, end)依次执行下面的操作:


a.调用flush_tlb_mm()刷新TLB。
b.在多处理器系统中,调用 freepages_and_swap_cache()释放页框,这些页框的指针已经集中存放在mmu_gather数据结构中了。