一般人喜欢把堆和栈来做对比,网上资料也很多,这里我只分享一下我本人的理解。堆这个东西跟栈没有直接的关联,它只给程序员提供一个手工分配和释放的内存空间,仅此而已。
对于每个Unix进程来说,都拥有一个特殊的线性区,这个线性区就是所谓的堆(heap),堆用于满足进程的动态内存请求。内存描述符的start_brk与brk字段分别限定了这个区的开始地址和结束地址。
进程可以使用下面的C语言API来请求和释放动态内存:
malloc(size)
请求size个字节的动态内存。如果分配成功,就返回所分配内存单元第一个字节的线性地址。
calloc(n,size)
请求含有n个大小为size的元素的一个数组。如果分配成功,就把数组元素初始化为0,并返回第一个元素的线性地址。
realloc(ptr,size)
改变由前面的malloc()或calloc()分配的内存区字段的大小。
free(addr)
释放由malloc()或calloc()分配的起始地址为addr的线性区。
brk(addr)
直接修改堆的大小。addr参数指定current->mm->brk的新值,返回值是线性区新的结束地址(进程必须检查这个地址和所请求的地址值addr是否一致)。
sbrk(incr)
类似于brk(),不过其中的incr参数指定是增加还是减少以字节为单位的堆大小。
brk()函数和以上列出的函数有所不同,因为它是唯一以系统调用的方式实现的函数,而其他所有的函数都是使用brk()和mmap()系统调用实现的C语言库函数。
当用户态的进程调用brk()系统调用时,内核执行sys_brk(addr)函数。该函数首先验证addr参数是否位干进程代码所在的线性区。如
果
是,则立即返回,因为堆不能与进程代码所在的线性区重叠:
mm = current->mm;
down_write(&mm->mmap_sem);
if (addr < mm->end_code) {
out:
up_write(&mm->mmap_sem);
return mm->brk;
}
由于brk()系统调用作用于某一个非代码的线性区,它分配和释放完整的页
。因此,该函数把addr的值调整为PAGE_SIZE的倍数,然后把调整的结果与内存描述符的brk字段的值进行比较:
newbrk = (addr + 0xfff) & 0xfffff000;
oldbrk = (mm->brk + 0xfff) & 0xfffff000;
if (oldbrk == newbrk) {
mm->brk = addr;
goto out;
}
如果进程请求缩小堆,则sys_brk()调用do_munmap()函数完成这项任务,然后返回:
if (addr <= mm->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk))
mm->brk = addr;
goto out;
}
如果进程请求扩大堆,则sys_brk()首先检查是否允许进程这样做。如果进程企图分配在其跟制范围之外的内存,函数并不多分配内存,只简单地返回mm->brk的原有值:
rlim = current->signal->rlim[RLIMIT_DATA].rlim_cur;
if (rlim < RLIM_INFINITY && addr - mm->start_data > rlim)
goto out;
然后,函数检查扩大后的堆是否和进程的其他线性区相重叠,如果是,不做任何事情就返回:
if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
goto out;
如果一切都顺利,则调用do_brk()函数。如果它返回oldbrk,则分配成功且sys_brt()函数返回addr的值;否则,返回旧的mm->brk值:
if (do_brk(oldbrk, newbrk-oldbrk) == oldbrk)
mm->brk = addr;
goto out;
do_brk()函数实际上是仅处理匿名线性区的do_mmap()的简化版。可以认为它的调用等价于:
do_mmap(NULL, oldbrk, newbrk-oldbrk, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_FIXED|MAP_PRIVATE, 0)
当然,do_brk()比do_mmap()稍快,因为前者假定线性区不映射磁盘上的文件,从而避免了检查线性区对象的几个字段。