合并内存区域
Linux的内存管理模块以虚拟内存区域为单位管理进程的虚拟内存,一个进程的所有内存区域分别以链表和树形结构进行组织。当一个新建的区域加入进程时,内核会试图将这个新区域与已存在的区域进行合并。区域合并的首要条件就是检查新区域之前的prve区域终止地址是否与新区域起始地址重合,或新区域的结束地址是否与其之后的next区域起始地址重合;接着再检查将要合并的区域是否有相同的标志。如果合并区域均映射了磁盘文件,则还要检查其映射文件是否相同,以及文件内的偏移量是否连续。
上述提及的一系列条件检查是在vma_merge()中完成的,根据新区域和它前后两个内存区域之间的位置关系,区域合并分为8种情况。由于合并条件的复杂性,该函数包含好几个参数,分别对上述说明的合并条件进行检测。
mm描述要添加新区域进程的内存空间,prev指向当前区域之前的一个内存区域,addr表示新区域的起始地址,end为新区域的结束地址,vm_flags表示该区域的标志。如果该新区域映射了一个磁盘文件,则file结构表示该文件,pgoff表示该文件映射的偏移量。
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)
合并函数首先通过起始地址和终止地址计算新区域的长度,接下来判断新区域是否设置了VM_SPECIAL,这个标志指定了该区域不能和其他区域合并,因此立即返回NULL。
pgoff_t pglen = (end - addr) >> PAGE_SHIFT;
struct vm_area_struct *area, *next;
int err;
if (vm_flags & VM_SPECIAL)
return NULL;
if (prev)
next = prev->vm_next;
else
next = mm->mmap;
area = next;
if (next && next->vm_end == end) /* cases 6, 7, 8 */
next = next->vm_next;
接着,通过prev获得下一个内存区域的描述符next,如图1。如果新区域的终止地址与next区域的终止地址重合,则next再向前移动一个区域,如图2。为了便于说明,在图2所示的情况下,next‘表示紧邻prev的那个区域,而next表示紧邻next‘的区域。图中每个不同的内存区域都使用不同的颜色标示。
接下来开始真正的合并工作,合并分为两大类,第一大类为新区域的起始地址和prev区域的终止地址重合,第二种情况为新区域的终止地址和next区域的起始地址重合。
我们首先分析第一种情况,即便addr和prev的终止地址重合还不足以将其合并,还应通过can_vma_merge_after()判断两者的标志和映射文件等是否相同。如果都满足条件,那么合并此时就应该可以开始了。不过合并函数力求每次合并程度达到最大化,它再继续检查end是否恰好与next区域的起始地址重合。
在这样的判断条件下,会出现5种不同的合并情况(分别为case1,6,2,5,7)。每种合并情况最终都会调用vma_adjust(),它通过修改vma结构中的字段对区域进行适当调整,也就是说真正的合并是在这个函数中完成的。可以看出vma_merge()本质上是一个“分流”函数,它将区域合并细化,根据不同的合并情况向vma_adjust()传递不同的参数。
if (prev && prev->vm_end == addr &&
mpol_equal(vma_policy(prev), policy) &&
can_vma_merge_after(prev, vm_flags,
anon_vma, file, pgoff)) {
if (next && end == next->vm_start &&
mpol_equal(policy, vma_policy(next)) &&
can_vma_merge_before(next, vm_flags,
anon_vma, file, pgoff+pglen) &&
is_mergeable_anon_vma(prev->anon_vma,
next->anon_vma)) {
/* cases 1, 6 */
err = vma_adjust(prev, prev->vm_start,
next->vm_end, prev->vm_pgoff, NULL);
} else /* cases 2, 5, 7 */
err = vma_adjust(prev, prev->vm_start,
end, prev->vm_pgoff, NULL);
if (err)
return NULL;
return prev;
}
从上面的分析以及源码可以看出,case1和case6既满足addr与prev终止地址重合,又满足end与next起始地址重合,但是他们的next(如图1,2)却指向不同的区域。case1可参考下图,它实际上是填充了prev和next之间的“空洞”,也就是说三个区域合为一个区域,vma_adjust()会删除next区域同时扩大prev区域。
case6也可以看作是填充prev和next区域之间的空洞,不过它会将next‘区域进行“覆盖”。通过代码可以发现,next‘和next区域事实上是连续的,不过由于其他原因,比如标志不同,造成它们是两个不同的区域。尽管地址连续,但是组织的时候仍然通过链表链接。这里为了定量的表示区域之间的关系,省去了next‘和next之间的链接箭头。
如果end与next的起始地址不重合,那么会出现case2,case5,case7三种情况。这三种情况的差异会在vma_adjust()中被进一步区分,而在当前函数中,它们被看作是一种情况,即addr与prev终止地址重合而end与next起始地址不重合。
如果end小于next区域的起始地址,则为case2。
如果end大于next区域的起始地址,则为case5。在这种情况下,next会被一分为二,一部分加入prev,而另一部分则继续保留在原始区域中。
case7也是一种扩大prev的情况,它会将next‘覆盖,而next则保持不变。
当上述情况都不符合时,进入第二大类合并,即end与next的起始地址重合。在这种情形下涉及三种合并模型,它们的图示分别如下所示。
if (next && end == next->vm_start &&
mpol_equal(policy, vma_policy(next)) &&
can_vma_merge_before(next, vm_flags,
anon_vma, file, pgoff+pglen)) {
if (prev && addr < prev->vm_end) /* case 4 */
err = vma_adjust(prev, prev->vm_start,
addr, prev->vm_pgoff, NULL);
else /* cases 3, 8 */
err = vma_adjust(area, addr, next->vm_end,
next->vm_pgoff - pglen, NULL);
if (err)
return NULL;
return area;
}
return NULL;
如果addr大于prev的终止地址,则属于case4。这种情况下缩小prev,扩充next。
如果addr小于prev区域的终止地址,则属于case3。这种情况下prev不做改变,扩充next。
case8与case3比较类似,不过它会覆盖已存在的next‘。
从源码的条件判断语句可以看出,上述8种合并情况可以规划为四类,通过向vma_adjust()传递不同的参数可以为每种情况调节内存区域。