4.对区域的操作

内核提供了各种函数来操作进程的虚拟内存区域。在建立或删除映射时,创建和删除区域(以及查找用于新区域的适当的内存位置)是所需要的标准操作。如图4-10所示。

 

 

 

将虚拟地址关联到区域
通过虚拟地址,find_vma可以查找用户地址空间中结束地址在给定地址之后的第一个区域,即满足addr < vm_area_struct->vm_end条件的第一个区域。
<mm/mmap.c> 
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr) 
{ 
  struct vm_area_struct *vma = NULL; 
  if (mm) { 
    /* 首先检查缓存。 */ 
    /* (缓存命中率通常大约是35%。) */ 
    vma = mm->mmap_cache; 
    if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) { 
      struct rb_node * rb_node; 
      rb_node = mm->mm_rb.rb_node; 
      vma = NULL; 
      while (rb_node) { 
        struct vm_area_struct * vma_tmp; 
         vma_tmp = rb_entry(rb_node, 
        struct vm_area_struct, vm_rb); 
        if (vma_tmp->vm_end > addr) { 
          vma = vma_tmp; 
          if (vma_tmp->vm_start <= addr) 
              break; 
          rb_node = rb_node->rb_left; 
         } else 
          rb_node = rb_node->rb_right; 
        } 
     if (vma) 
       mm->mmap_cache = vma; 
    } 
  } 
  return vma; 
}
内核首先检查上次处理的区域(现在保存在mm->mmap_cache)中是否包含所需的地址,即是否该区域的结束地址在目标地址之后,而起始地址在目标地址之前。倘若如此,内核不会执行if语句,而是立即将指向该区域的指针返回。
否则必须逐步搜索红黑树。如果相关的区域结束地址大于目标地址而起始地址小于目标地址,内核就找到了一个适当的结点,可以退出while循环,否则,再继续搜索:
 如果当前区域结束地址大于目标地址,则从左子结点开始;
 如果当前区域的结束地址小于等于目标地址,则从右子结点开始。
如果找到适当的区域,则将其指针保存在mmap_cache中,因为下一次find_vma调用搜索同一个区域中邻近地址的可能性很高。
find_vma_intersection是另一个辅助函数,用于确认边界为start_addr和end_addr的区间是否完全包含在一个现存区域内部。它基于find_vma,很容易实现,如下所示:
<mm.h> 
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr) 
{ 
  struct vm_area_struct * vma = find_vma(mm,start_addr); 
  if (vma && end_addr <= vma->vm_start) 
    vma = NULL; 
  return vma; 
}
区域合并
在新区域被加到进程的地址空间时,内核会检查它是否可以与一个或多个现存区域合并,如图4-10所示。
vm_merge在可能的情况下,将一个新区域与周边区域合并。它需要很多参数。
mm/mmap.c 
struct vm_area_struct *vma_merge(struct mm_struct *mm, struct vm_area_struct *prev, unsigned long addr, unsigned long end, unsigned long vm_flags, 
struct anon_vma *anon_vma, struct file *file, pgoff_t pgoff, struct mempolicy *policy) { pgoff_t pglen = (end -addr) >> PAGE_SHIFT; struct vm_area_struct *area, *next; ...
mm是相关进程的地址空间实例,而prev是是紧接着新区域之前的区域。rb_parent是该区域在红黑查找树中的父结点。addr、end和vm_flags分别是新区域的开始地址、结束地址、标志。如果该区域属于一个文件映射,则file是一个指向表示该文件的file实例的指针。pgoff指定了映射在文件数据内的偏移量。
实现的技术细节非常简单。首先检查确定前一个区域的结束地址是否对应于新区域的起始地址。倘若如此,内核接下来必须检查两个区域,确认二者的标志和映射的文件相同,文件映射内部的偏移量符合连续区域的要求,两个区域内匿名映射相同或其中一个为NULL,而且两个区域彼此兼容。(如果两个文件映射在地址空间中连续,但在文件中不连续,亦无法合并)
通过can_vma_merge_after辅助函数完成检查。将区域与前一个区域合并的工作看起来如下所示:
mm/mmap.c 
  if (prev && prev->vm_end == addr && can_vma_merge_after(prev, vm_flags, 
       anon_vma, file, pgoff)) { 
...
如果可以,内核接下来检查后一个区域是否可以合并。
mm/mmap.c 
  /* 
  * OK,前一个可以合并。 现在我们可以合并后一个么?
  */ 
  if (next && end == next->vm_start &&  can_vma_merge_before(next, vm_flags, anon_vma, file, pgoff+pglen) && is_mergeable_anon_vma(prev->anon_vma, next->anon_vma)) { 
    vma_adjust(prev, prev->vm_start, next->vm_end, prev->vm_pgoff, NULL); 
  } else 
    vma_adjust(prev, prev->vm_start, end, prev->vm_pgoff, NULL); 
  return prev; 
}
与前一例相比,第一个差别是使用can_vma_merge_before来检查两个区域是否可以合并,替代了can_vma_merge_after。如果前一个和后一个区域都可以与当前区域合并,还必须确认前一个和后一个区域的匿名映射可以合并,然后才能创建包含这3个区域的一个单一区域。
插入区域
insert_vm_struct是内核用于插入新区域的标准函数。

 

 

首先调用find_vma_prepare,通过新区域的起始地址和涉及的地址空间(mm_struct),获取下列信息。
 前一个区域的vm_area_struct实例。
 (红黑树中)保存新区域结点的父结点。
 包含该区域自身的(红黑树)叶结点。
C语言中函数只允许返回一个值,这是常识,因此上述函数只返回了一个指向前一个区域的指针作为结果。剩余的信息通过指针参数提供。
查找到的信息足以使用vma_link将新区域合并到该进程现存的数据结构中。在经过一些准备工作之后,该函数将实际工作委托给__vma_link,后者执行3个插入操作,如代码流程图所示。
 __vma_link_list将新区域放置到进程管理区域的线性链表上。完成该工作,只需提供使用find_vma_prepare找到的前一个和后一个区域。①
 顾名思义,__vma_link_rb将新区域连接到红黑树的数据结构中。
__anon_vma_link将vm_area_struct实例添加到匿名映射的链表,上文讨论过。
最后,__vma_link_file将相关的address_space和映射(如果是文件映射)关联起来,并使用vma_prio_tree_insert将该区域添加到优先树中,对多个相同区域的处理如上所述。
创建区域
在向数据结构插入新的内存区域之前,内核必须确认虚拟地址空间中有足够的空闲空间,可用于给定长度的区域。该工作分配给get_unmapped_area辅助函数完成。
mm/mmap.c 
unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags)
根据进程虚拟地址空间的布局,会选择使用不同的映射函数。在这里我主要考虑大多数系统上采用的标准函数arch_get_unmapped_area。
arch_get_unmapped_area首先必须检查是否设置了MAP_FIXED标志,该标志表示映射将在固定地址创建。倘若如此,内核只会确保该地址满足对齐要求(按页),而且所要求的区间完全在可用地址空间内。
如果没有指定区域位置,内核将调用arch_get_unmapped_area在进程的虚似内存区中查找适当的可用区域。如果指定了一个特定的优先选用(与固定地址不同)地址,内核会检查该区域是否与现存区域重叠。如果不重叠,则将该地址作为目标返回。
mm/mmap.c 
unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags) 
{ 
  struct mm_struct *mm = current->mm;
...
  if (addr) { 
  addr = PAGE_ALIGN(addr); 
  vma = find_vma(mm, addr); 
  if (TASK_SIZE -len >= addr && (!vma || addr + len <= vma->vm_start)) 
    return addr; 
} 
...

否则,内核必须遍历进程中可用的区域,设法找到一个大小适当的空闲区域。这样做时,内核会检查是否可使用前一次扫描时缓存的区域。

mm/mmap.c 
  if (len > mm->cached_hole_size) { 
    start_addr = addr = mm->free_area_cache; 
  } else { 
    start_addr = addr = TASK_UNMAPPED_BASE; 
    mm->cached_hole_size = 0; 
} 
...
如果搜索持续到用户地址空间的末端(TASK_SIZE),仍然没有找到适当的区域,则内核返回一-ENOMEM错误。错误必须发送到用户空间,且由相关的应用程序来处理。该错误代码表示虚拟地址空间中可用内存不足,无法满足应用程序的请求。如果找到内存,则返回其起始处的虚拟地址。

否则,内核必须遍历进程中可用的区域,设法找到一个大小适当的空闲区域。这样做时,内核会检查是否可使用前一次扫描时缓存的区域。

mm/mmap.c 
if (len > mm->cached_hole_size) { 
  start_addr = addr = mm->free_area_cache; 
} else { 
  start_addr = addr = TASK_UNMAPPED_BASE; 
  mm->cached_hole_size = 0; 
} 
...
实际的遍历,或者开始于虚拟地址空间中最后一个“空洞”的地址,或者开始于全局的起始地址TASK_UNMAPPED_BASE。
mm/mmap.c 
full_search: 
  for (vma = find_vma(mm, addr); ; vma = vma->vm_next) { 
  /* At this point: (!vma || addr < vma->vm_end). */ 
    if (TASK_SIZE - len < addr) { 
       /* 
      * 开始一次新的搜索,以防错过某些空洞。
      */ 
      if (start_addr != TASK_UNMAPPED_BASE) { 
         addr = TASK_UNMAPPED_BASE; 
         start_addr = addr; 
         mm->cached_hole_size = 0; 
         goto full_search; 
       } 
       return -ENOMEM; 
    } 
    if (!vma || addr + len <= vma->vm_start) { 
     /* 
    * 记住我们停止搜索的位置:
    */ 
      mm->free_area_cache = addr + len; 
       return addr; 
    } 
    if (addr + mm->cached_hole_size < vma->vm_start) 
      mm->cached_hole_size = vma->vm_start - addr; 
      addr = vma->vm_end; 
    } 
}
如果搜索持续到用户地址空间的末端(TASK_SIZE),仍然没有找到适当的区域,则内核返回一个-ENOMEM错误。错误必须发送到用户空间,且由相关的应用程序来处理。该错误代码表示虚拟地址空间中可用内存不足,无法满足应用程序的请求。如果找到内存,则返回其起始处的虚拟地址。
prev
insert_vm_struct返回的值有前一个区域(prev)的指针,和后一个区域(prev->next)指针,后一个指针指向的vm_area_struct区间可能包括参数addr也可能不包含
__vma = find_vma_prepare(mm,vma->vm_start,&prev,&rb_link,&rb_parent);
    if (__vma && __vma->vm_start < vma->vm_end)
        return -ENOMEM;

find_vma则返回vm_area_struct的一个指针,指针指向的vm_area_struct区间可能包括参数addr也可能不包含

posted @ 2022-03-24 21:44  while(true);;  阅读(73)  评论(0编辑  收藏  举报