系统调用brk()的作用是扩展进程的“堆”,在其实现代码里,最后会调到do_brk()函数来完成,do_brk()函数在载入elf文件时也会调用。
do_brk()函数的声明如下:
[c]
unsigned long do_brk(unsigned long addr, unsigned long len)
[/c]
就像在上面的注释中所提到的,do_brk()是一个简化版的do_mmap(),因为在这里只需要考虑匿名映射,与文件无关。do_brk()有两个参数,addr是要扩展到的目标区域的开始地址,len是目标区域的长度。
与mm/mmap.c文件中其它大多数函数的一样,do_brk()函数所做的是处理vm area,目标是在这个函数完成的时候,在进程空间中有一个匿名vm area能映射到[addr, addr len)这段区域。下面是主要代码的分析:
[c]
error = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED);
if (error & ~PAGE_MASK)
return error;
[/c]
调用get_unmapped_area()函数在当前进程的用户空间中获得一个未映射区间的起始地址。PAGE_MASK的值为0xFFFFF000,因此,如果 (error & ~PAGE_MASK)为非0,说明addr最低12位非0,addr就不是一个有效的地址,就以这个地址作为返回值;否则,addr就是一个有效的地址(最低12位为0)
[c]
vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
if (vma && vma->vm_start < addr len) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
[/c]
调用find_vma_prepare()查找是不是已经存在某个vma覆盖了地址addr,如果是的话,这个已经存在的vma可能只是覆盖了部分[addr, addr len)区域,也可能覆盖了整个区域。这时就需要调用do_munmap()来把被覆盖地部分清除。已有的vma可能会被切分,与[addr, addr len)区域有覆盖关系的部分会被清除,在此以外的部分将被保留。
至此,已经可以保证,区域[addr, addr len)是空白了,没有任何vma覆盖这一区域。
接下来,首先尝试看能不能把前面的一个vma做一个扩展,扩展到addr len处,这是通过vma_merge()来进行的(vma_merge()中会通过调用vma_adjust()来调整前一个vma的大小):
[c]
vma = vma_merge(mm, prev, addr, addr len, flags,
NULL, NULL, pgoff, NULL);
if (vma)
goto out;
[/c]
如果没有成功的话,那么就只能新建一个vma,并把它连接到所有vma的链表和红黑树中去:
[c]
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
vm_unacct_memory(len >> PAGE_SHIFT);
return -ENOMEM;
}
INIT_LIST_HEAD(&vma->anon_vma_chain);
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr len;
vma->vm_pgoff = pgoff;
vma->vm_flags = flags;
vma->vm_page_prot = vm_get_page_prot(flags);
vma_link(mm, vma, prev, rb_link, rb_parent);
[/c]
到这里,代码的主要部分已经结束,区域[addr, addr len)已经被映射到了某一个vma中。
这里顺便再接着上一篇讲elf_load_binary()函数的话题说一下在那里对do_brk()的调用。代码如下:
[c]
elf_load_binary():
......
retval = set_brk(elf_bss, elf_brk);
......
static int set_brk(unsigned long start, unsigned long end):
......
addr = do_brk(start, end - start);
......
[/c]
这一处调用发生的场景,是ELF文件中的所有可装载段都已经解析完,进程中各区域(代码区、数据区、BSS区,堆区)的地址都已经计算完成。elf_bss和elf_brk就分别是BSS和堆的起始地址。而因为BSS与堆是前后相连的,所以它们之间的距离正好是BSS区的大小。do_brk()的两个参数正是BSS区的起始地址和长度,原来这里是在用do_brk()来映射BSS区。