【推荐阅读】
尽管可见度不高,brk也许是最常使用的系统调用了,用户进程通过它向内核申请空间。人们常常并不意识到在调用brk,原因在于很少有人会直接使用系统调用brk向系统申请空间,而总是通过像malloc一类的C语言库函数(或语言成分,如C++中的new)间接地调用brk。如果把malloc想象成零售,brk则是批发。库函数malloc为用户进程(malloc本身就是该进程的一部分)维持一个小仓库,当进程需要使用更多的内存空间时就向小仓库要,小仓库中存量不足时就通过brk向内核批发。
前面讲过,每个进程拥有3GB字节的用户虚存空间。但是,这并不意味着用户进程在这3GB字节的范围里可以任意使用,因为虚存空间最终得映射到某个物理存储空间(内存或磁盘空间),才真正可以使用,而这种映射的建立和管理则由内核处理。所谓向内核申请一块空间,是指请求内核分配一块虚存空间和相应的若干物理页面,并建立起映射关系。由于每个进程的虚存空间都很大(3GB),而实际需要使用的又很小,内核不可能在创建进程时就为整个虚存空间都分配好相应的物理空间并建立映射,而只能是需要用多少才分配多少。
那么,内核怎样管理每个进程的3G字节虚存空间呢?粗略地说,用户程序经过编译、链接形成的映像文件中有一个代码段和一个数据段(包括data段和bss段),其中代码段在下,数据段在上。数据段中包括了所有静态分配的数据空间,包括全局变量和说明为static的局部变量。这些空间是进程所必须的基本要求,所以内核在建立一个进程的运行映象时就分配好些空间,包括虚存地址区间和物理页面,并建立好二者间的映射。除此之外,堆栈空间安置在虚存空间的顶部,运行时由顶向下延伸;代码段和数据段则在底部(注意,不要与X86系统结构中由段寄存器建立的代码段及数据段相混淆);在运行时并不向上伸展。而从数据段的顶部end_data到堆栈段地址的下沿这个中间区域则是一个巨大的空洞,这就是可以在运行时动态分配的空间。最初,这个动态分配空间是从进程的end_data开始的,这个地址为内核和进程所共知。以后,每次动态分配一块内存,这个边界就往上推进一段距离,同时内核和进程都要记下当前的边界在哪里。在进程这一边由malloc或类似的库函数管理,而在内核则将当前的边界记录在进程的mm_struct结构中。具体地说,mm_struct结构中有一个成分brk,表示动态分配区当前的底部。当一个进程需要分配内存时,将要求的大小与其当前的动态分配区底部边界相加,所得的就是所要求的的新边界,也就是brk调用时的参数brk。当内核能满足要求时,系统调用brk返回0,此后新旧两个边界之间的虚存地址就都可以使用了。当内核发现无法满足要求(例如物理空间已经分配完),或者发现新的边界已经过于逼近设于顶部的堆栈时,就拒绝分配而返回-1。
系统调用brk在内核中的实现为sys_brk,其代码在mm/mmap.c中,这个函数既可以用来分配空间,即把动态分配区底部的边界往上推;也可以用来释放,即归还空间。因此,它的代码也大致上可以分成两部分。我们先读第一部分:
sys_brk
/*
* sys_brk() for the most part doesn't need the global kernel
* lock, except when an application is doing something nasty
* like trying to un-brk an area that has already been mapped
* to a regular file. in this case, the unmapping will need
* to invoke file system routines that need the global lock.
*/
asmlinkage unsigned long sys_brk(unsigned long brk)
{
unsigned long rlim, retval;
unsigned long newbrk, oldbrk;
struct mm_struct *mm = current->mm;
down(&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;
/* Always allow shrinking brk. */
if (brk <= mm->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk))
goto set_brk;
goto out;
}
参数brk表示所要求的新边界,这个边界不能低于代码段的终点,并且必须与页面大小对齐。如果新边界低于老边界,那就不是申请分配空间,而是释放空间,所以通过do_munmap解除一部分区间的映射,这是个重要的函数。其代码如下:
sys_brk=>do_munmap
/* Munmap is split into 2 main parts -- this part which finds
* what needs doing, and the areas themselves, which do the
* work. This now handles partial unmappings.
* Jeremy Fitzhardine <[email]jeremy@sw.oz.au[/email]>
*/
int do_munmap(struct mm_struct *mm, unsigned long addr, size_t len)
{
struct vm_area_struct *mpnt, *prev, **npp, *free, *extra;
if ((addr & ~PAGE_MASK) || addr > TASK_SIZE || len > TASK_SIZE-addr)
return -EINVAL;
if ((len = PAGE_ALIGN(len)) == 0)
return -EINVAL;
/* Check if this memory area is ok - put it on the temporary
* list if so.. The checks here are pretty simple --
* every area affected in some way (by any overlap) is put
* on the list. If nothing is put on, nothing is affected.
*/
mpnt = find_vma_prev(mm, addr, &prev);
if (!mpnt)
return 0;
/* we have addr < mpnt->vm_end */
if (mpnt->vm_start >= addr+len)
return 0;
/* If we'll make "hole", check the vm areas limit */
if ((mpnt->vm_start < addr && mpnt->vm_end > addr+len)
&& mm->map_count >= MAX_MAP_COUNT)
return -ENOMEM;
函数find_vma_prev的作用于以前在linux内存管理-几个重要的数据结构和函数博客中读过的find_vma基本相同,它扫描当前进程用户空间的vm_area_struct结构链表或AVL树,试图找到结束地址高于address的第一个区间,如果找到,则函数返回该区间的vm_area_struct结构指针。不同的是,它同时还通过参数prev返回其前一区间结构的指针。等一下我们就将看到为什么需要这个指针。如果返回的指针为0,或者该区间的起始地址也高于addr+len,那就表示想要解除映射的那部分空间原来就没有映射,所以直接返回0,。如果这部分空间落在某个区间的中间,则在解除这部分空间的映射以后会造成一个空洞而使原来的区间一分为二。可是,一个进程可以拥有的虚存区间的数量是有限制的,所以若这个数量达到了上限MAX_MAP_COUNT,就不再允许这样的操作。
sys_brk=>do_munmap
/*
* We may need one additional vma to fix up the mappings ...
* and this is the last chance for an easy error exit.
*/
extra = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!extra)
return -ENOMEM;
npp = (prev ? &prev->vm_next : &mm->mmap);
free = NULL;
spin_lock(&mm->page_table_lock);
for ( ; mpnt && mpnt->vm_start < addr+len; mpnt = *npp) {
*npp = mpnt->vm_next;
mpnt->vm_next = free;
free = mpnt;
if (mm->mmap_avl)
avl_remove(mpnt, &mm->mmap_avl);
}
mm->mmap_cache = NULL; /* Kill the cache. */
spin_unlock(&mm->page_table_lock);
由于解除一部分空间的映射有可能使原来的区间一分为二,所以这里先分配好一个空白的vm_area_struct结构extra。另一方面,要解除映射的那部分空间也有可能跨越好几个区间,所以通过一个for循环把所有涉及的区间都转移到一个临时队列free中,如果建立了AVL树,则也要把这些区间的vm_area_struct结构从AVL树中删除。以前讲过