分析完linux内存管理的基本概念与实现之后,就可以接着分析用户空间与内核空间的交互操作了。Brk系统调用属于那种常用但是“可见度”不高的操作,常用于用户空间堆的管理(请参阅本站的<<Uclibc中的malloc机制分析>>一文)。
Brk在用户空间的接口为int brk(void *end_data_segment)。它通过系统调用进入内核空间。在内核的相应接口为sys_brk().
闲言少叙,言归正传。转入相应的代码。同以往一样,linux内核代码版本为2.6.21
//sys_brk:用来扩大或者缩小进程的数据段边界,brk为新的数据段边界).
asmlinkage unsigned long sys_brk(unsigned long brk)
{
unsigned long rlim, retval;
unsigned long newbrk, oldbrk;
struct mm_struct *mm = current->mm;
down_write(&mm->mmap_sem);
//参数有效性判断。
//代码段非法访问,
if (brk < mm->end_code)
goto out;
//页框对齐
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
//如果新边界与旧边界相等,不用进行空间的伸缩操作,直接赋值即可
if (oldbrk == newbrk)
goto set_brk;
//如果新边界比现在的边界要小,那说明要执行收缩操作
//缩短堆
if (brk <= mm->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk))
goto set_brk;
goto out;
}
//运行到这里的话,说明要执行的是数据段的伸展操作
//不能超过数据段上限
rlim = current->rlim[RLIMIT_DATA].rlim_cur;
if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim)
goto out;
/* Check against existing mmap mappings. */
//伸展空间已经有映射了
if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
goto out;
/* Ok, looks good - let it rip. */
//执行伸长操作
if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
goto out;
set_brk:
mm->brk = brk;
out:
retval = mm->brk;
up_write(&mm->mmap_sem);
return retval;
}
Brk系统调用分为两种情况,一种是收缩数据区,一种是伸长操作。我们分为两种情况来分析
二:用户空间的收缩
从上面的代码我们可以看出。用户空间的收缩操作相应的接口是:do_munmap()。代码如下:
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
{
unsigned long end;
struct vm_area_struct *mpnt, *prev, *last;
if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE-start)
return -EINVAL;
if ((len = PAGE_ALIGN(len)) == 0)
return -EINVAL;
//找到第一个结束地址大于start的VMA。Prev是前一个VMA
mpnt = find_vma_prev(mm, start, &prev);
if (!mpnt)
return 0;
//在没有定义CONFIG_HUGETLB_PAGE条件下,is_vm_hugetlb_page()为0
//略过这段代码
if (is_vm_hugetlb_page(mpnt)) {
int ret = is_aligned_hugepage_range(start, len);
if (ret)
return ret;
}
//现在的堆尾点不可能落在空洞里
//start:新的边界地址。Len:收缩的长度。Start+len即为旧的边界地址。
//所以 start+len肯定是属于进程的线性地址
end = start + len;
if (mpnt->vm_start >= end)
return 0;
//如果start大于mpnt的起始地址,就会把mpnt一分为二
if (start > mpnt->vm_start) {
if (split_vma(mm, mpnt, start, 0))
return -ENOMEM;
prev = mpnt;
}
//找到最后的一个vma
last = find_vma(mm, end);
//把最后一个线性区一分为二的情况
if (last && end > last->vm_start) {
if (split_vma(mm, last, end, 1))
return -ENOMEM;
}
mpnt = prev? prev->vm_next: mm->mmap;
//将mpnt对的区间vma从进程描述符组中删除
detach_vmas_to_be_unmapped(mm, mpnt, prev, end);
spin_lock(&mm->page_table_lock);
//更新页表项,释放页框
unmap_region(mm, mpnt, prev, start, end);
spin_unlock(&mm->page_table_lock);
//到现在为止,所有要释放的vma都挂在mpnt上。Unmap_vma_list为对要删除的vma链的处理
unmap_vma_list(mm, mpnt);
return 0;
}
为了弄清楚收缩的整个过程,有必要详细的分析一下函数所调用的各个子函数。
Split_vma:将一个vma劈为成两个:
//参数含义:
//mm:进程的内存描述符 vma:要劈分的vma addr:为界线地址 new_below:为0时,vma为下一半 为1时,//vma为上一半
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;
//如果进程的vma总数超过了限制值
if (mm->map_count >= sysctl_max_map_count)
return -ENOMEM;
//新申请一个vma
new = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!new)
return -ENOMEM;
//将新的vma赋值为旧的vma,使其两者相等
*new = *vma;
//new_below为1的时候,vma为上一半,对应的new为下一半
if (new_below)
new->vm_end = addr;
else {
//new_below为0时,vma为下一半,new为上一半
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);
//如果定义了open操作
if (new->vm_ops && new->vm_ops->open)
new->vm_ops->open(new);
//经过前面的初始化之后,再由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;
}
转入vma_adjust():
void vma_adjust(struct vm_area_struct *vma, unsigned long start,
unsigned long end, pgoff_t pgoff, struct vm_area_struct *insert)
{
……
//调整vma的起始边界和结束边界
vma->vm_start = start;
vma->vm_end = end;
vma->vm_pgoff = pgoff;
……
//将新的vma,插入到进程的vma链
__insert_vm_struct(mm, insert);
……
}
第二个要为析的函数是:detach_vmas_to_be_unmapped()
它主要是将要删除的vma链到一起,同时将要删除的vma从mm中脱链
//参数说明:
/*
Mm: 进程的内存描述符
Vma:要删除的起始vma
Prev:vma的前一个vma区
End:结束地址
*/
static void
detach_vmas_to_be_unmapped(struct mm_struct *mm, struct vm_area_struct *vma,
struct vm_area_struct *prev, unsigned long end)
{
struct vm_area_struct **insertion_point;
struct vm_area_struct *tail_vma = NULL;
insertion_point = (prev ? &prev->vm_next : &mm->mmap);
do {
//从红黑对中释放掉vma
rb_erase(&vma->vm_rb, &mm->mm_rb);
//更新vma计数
mm->map_count--;
tail_vma = vma;
vma = vma->vm_next;
} while (vma && vma->vm_start < end);
//将要删除的vma从链表中脱落
*insertion_point = vma;
//最后无素后向指针置NULL
tail_vma->vm_next = NULL;
//由于进行了删除操作。Mmap_cache失效了,置NULL
mm->mmap_cache = NULL; /* Kill the cache. */
}
接下来要分析的调用函数是unmap_vma_list()
它主要对删除的vma链进行处理。具体代码如下示:
//参数说明:
//mm:进程的内存描述符
//mpnt:要删除的链表的头节点
static void unmap_vma_list(struct mm_struct *mm,
struct vm_area_struct *mpnt)
{
//遍历链表的每个元素,然后对每一个vma,进行unmap_vma处理
do {
struct vm_area_struct *next = mpnt->vm_next;
unmap_vma(mm, mpnt);
mpnt = next;
} while (mpnt != NULL);
//debug 用,忽略
validate_mm(mm);
}
转向unmap_vma():
static void unmap_vma(struct mm_struct *mm, struct vm_area_struct *area)
{
size_t len = area->vm_end - area->vm_start;
//更新mm的total_vm
area->vm_mm->total_vm -= len >> PAGE_SHIFT;
if (area->vm_flags & VM_LOCKED)
area->vm_mm->locked_vm -= len >> PAGE_SHIFT;
vm_stat_unaccount(area);
area->vm_mm->unmap_area(area);
remove_vm_struct(area);
}
在remove_vm_struct中:
static void remove_vm_struct(struct vm_area_struct *vma)
{
……
//将vma描述符释放
kmem_cache_free(vm_area_cachep, vma);
}