许多系统程序需要为动态数据结构(比如链表、二叉树)分配额外内存,此类数据结构的大小由运行时所获取的信息决定。
进程可以通过增加堆的大小来分配内存,所谓堆是一段长度可变的连续虚拟内存,始于进程的未初始化数据段末尾,随着内存的分配和释放而增减。通常将堆的当前内存边界称为"program break"
调整 program break:brk()和 sbrk()
改变堆的大小(即分配或释放内存),其实就像命令内核改变进程的program break位置一样。最初,program break正好位于未初始化数据段末尾之后(如上图所示,与&end位置相同)
在program break的位置抬升后,程序可以访问新分配区域内的任何内存地址,而此时物理内存页尚未分配。内核在进程首次试图访问这些虚拟内存地址时自动分配新的物理内存页
传统的 UNIX 系统提供了两个操纵 program break 的系统调用:brk()和 sbrk(),在 Linux 中依然可用。虽然代码中很少直接使用这些系统调用,但了解它们有助于弄清内存分配的工作过程
NAME
brk, sbrk - 更改数据段大小
SYNOPSIS
#include <unistd.h>
int brk(void *addr);
void *sbrk(intptr_t increment);
glibc的功能测试宏要求 (see feature_test_macros(7)):
brk(), sbrk():
Since glibc 2.12:
_BSD_SOURCE || _SVID_SOURCE ||
(_XOPEN_SOURCE >= 500 ||
_XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED) &&
!(_POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600)
Before glibc 2.12:
_BSD_SOURCE || _SVID_SOURCE || _XOPEN_SOURCE >= 500 || _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED
DESCRIPTION
brk()和sbrk()更改program break的位置, 它定义进程数据段的结束位置(即program break 是未初始化数据段结束后的第一个位置). 增加program break的作用是为进程分配内存; 减少break将释放内存.
brk()将数据段的结尾设置为addr指定的值,当该值合理时,系统有足够的内存,并且进程不超过其最大数据大小 (see setrlimit(2)).
sbrk() 将程序的数据空间增量字节。以0为增量调用sbrk() 可用于查找program break的当前位置
RETURN VALUE
成功时,brk()返回0. 如果出错,则返回-1,并将errno设置为ENOMEM
成功时,sbrk()返回上一个program break(如果增加了break,则此值是指向新分配的内存的起点的指针)。 发生错误时,将返回(void *)-1,并将errno设置为ENOMEM。
系统调用brk()会将program break设置为参数end_data_segment所指定的位置。由于虚拟内存以页为单位进行分配,end_data_segment实际会四舍五入到下一个内存页的边界处。
当试图将 program break 设置为一个低于其初始值(即低于&end)的位置时,有可能会导致无法预知的行为,例如,当程序试图访问的数据位于初始化或未初始化数据段中当前尚不存在的部分时,就会引发分段内存访问错误(segmentation fault)(SIGSEGV 信号)。program break可以设定的精确上限取决于一系列因素,这包括进程中对数据段大小的资源限制(RLIMIT_DATA),以及内存映射、共享内存段、共享库的位置
调用sbrk()将 program break 在原有地址上增加从参数 increment 传入的大小。若调用成功,sbrk()返回前一个 program break 的地址。
调用 sbrk(0)将返回 program break 的当前位置,对其不做改变。在意图跟踪堆的大小,或是监视内存分配函数包的行为时,可能会用到这一用法
每个进程可访问的虚拟内存空间为3G,但在程序编译时,不可能也没必要为程序分配这么大的空间,只分配并不大的数据段空间,程序中动态分配的空间就是从这一块分配的。如果这块空间不够,malloc函数族(realloc,calloc等)就调用sbrk函数将数据段的下界移动,sbrk函数在内核的管理下将虚拟地址空间映射到内存,供malloc函数使用。