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也可能不包含