动态存储器分配
大多是 C 程序在运行时会需要额外的存储,并且不能事先知道需要的存储大小,这时候使用一种动态存储分配器(dynamic memory allocator)。C 标准库提供了一个称为 malloc 的程序进行显式存储器分配,使用 free 函数来释放已分配内存,另外还有 calloc 和 realloc 两个函数。
void * malloc (size_t size);
void * calloc (size_t num, size_t size);
void * realloc(void * ptr, size_t size);
void free (void * ptr);
malloc 返回指向 size 个字节的存储块的指针;calloc 返回指向 num * size 个存储器块的指针,即分配 num 个 size 大小的连续存储块,并且存储器初始化为 0. 注意: malloc 并不保证得到存储块初始化为 0;realloc 用在当 malloc 分配的存储块大小不够时,分配更大的块,并将数据复制到新的块。以上三个函数在没有多余的存储可以分配时则都返回 NULL 指针。free 函数则释放 ptr 指针指向的存储器块。
虚拟存储器
虚拟存储器是现代计算机系统中对内存的一个抽象概念,它是由硬件和软件协同工作,提供给每个进程一个大的、一致的、私有的地址空间。简单的说,对一个 n 位的计算机系统,虚拟存储器被组织成存放在磁盘上的,2 ** n 个连续字节大小的数组的连续的地址空间,使用内存作为高速缓存。它为每个进程提供了一个一致的地址空间,从而简化了存储器的管理,并且它保护了每个进程的地址空间不被其他进程破坏(注意这里存储器与内存概念上的区别)。
比如对于 32 位的 Linux 系统,虚拟存储器空间为 2 ^ 32 即 4G, 即进程的寻址空间位 4G, 其中前 3G 划分给用户使用,后 1G 留给操作系统使用。操作系统将用户 3G 的空间划分成了数个 存储器区域,一个区域就是已经分配过的虚拟存储器上连续的一段空间。比如对于进程来说,进程的代码数据区域总是开始与 0x08048000 处向上增长;堆则在接着代码和数据上面;共享库总是从 0x40000000 处开始向上增长;进程桟则总是从 0xbfffffff 处开始向下递减。
这样链接器在生成可执行文件时,不需要知道数据运行时存放的地址,只需按照约定的方式生成虚拟地址,大大的简化了链接的过程。另外,虚拟地址机制还简化了内存共享、存储器分配和程序加载过程。虚拟地址则在运行时,由 CPU 中的内存管理单元(MMU)翻译为物理地址,即数据的实际地址。
malloc 从哪里得到的内存空间
现在我们可以讨论 malloc 是从哪得到的内存空间了。
glibc 实现的 malloc 同时使用 brk 和 mmap 两个系统调用获取内存,对于大块内存优先使用 mmap.其他 C 库则取决与其 malloc 函数的具体实现。
常见的一种说法是,malloc 分配的空间是堆(heap)中的空间,即上图 brk 处开始的地址空间。堆的大小可以使用系统函数 sbrk 和 brk (unistd.h包含)进行扩展,我们做个实验看看堆最大能都达到多大。
int main()
{
int size = 0;
while (sbrk(1 << 20) != (void *)-1)
size++;
printf ("heap max size is %d MB\n", size);
return 0;
}
运行得到的结果为:
heap max size is 243 MB
显然这远远少于我们的预期,如果是这样的话那内存利用效率也太低了。我们再用 malloc 函数进行一次实验。
int main()
{
int size = 0;
void *p;
while ((p = malloc(1 << 20)))
{
size++;
if (!(size % 100))
printf("%dMB\t 0x%.8x\n", size, (unsigned int)p);
}
printf("max malloc memory size is %d MB\n", size);
return 0;
}
在我电脑上运行的结果为:
这次 malloc 分配到了将近 3G 的存储空间,比较符合预期。那么 malloc 的存储空间还从哪里得到的呢?事实上,malloc 还使用 mmap 和 munmap (#include
int main()
{
int size = 0;
while (mmap(NULL, 1 << 20, PROT_READ, MAP_PRIVATE|MAP_ANON,
0, 0) != (void *)-1)
size++;
printf ("mmap max size is %d MB\n", size);
return 0;
}
运行结果为:
mmap max size is 2767 MB
mmap 的最大空间和 sbrk 最大地址空间相加和 malloc 最大空间很接近,考虑到内存大小的限制,说明 malloc 是同时使用两者的。
结合上次实验和虚拟存储器区域地址分布,我们发现对于 glibc 的 malloc 的实现,其首先分配的空间是从高地址向低地址发展,分布在共享存储区域和进程桟区域之间。这说明 malloc 首先使用的是 mmap 分配的存储器区域(最起码对于大小为 1MB 的块是如此),并且 mmap 分配的存储器区域是先从高处后从低处的。malloc 后分配的空间地址是从低地址,大约是堆开始的地方,向上增长。
在 glibc 源文件 malloc.c 中,有下面一段话:
/* …
Rather than using a static threshold for the brk/mmap tradeoff,
we are now using a simple dynamic one. The goal is still to avoid
fragmentation. The old goals we kept are
1) try to get the long lived large allocations to use mmap()
2) really large allocations should always use mmap()
and we’re adding now:
3) transient allocations should use brk() to avoid forcing the kernel
having to zero memory over and over again
… */
翻译一下大概是这个意思:
争取 在分配 生命周期比较长的大内存块 时使用mmap()
非常大的分配应该总是使用mmap()
动态分配应该使用brk()避免强迫内核不断的清零内存
mmap 函数
void *mmap (void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
mmap 函数是 UNIX 的一个系统函数,其功能是要求内核创建一个新的虚拟存储器区域(想想我们前面讨论过的存储器区域概念),最好是从地址 start 开始的一个区域,并将文件描述符 fd 指定的文件对象的一个连续的组块映射到这个新的区域。munmap 则删除虚拟存储器的区域。上面使用的
mmap(NULL, 1 << 20, PROT_READ, MAP_PRIVATE|MAP_ANON, 0, 0)
调用,fd 为 0,将创建一个新的包含 1MB 的只读、私有、请求二进制零的虚拟存储器区域。
mmap 内存映射文件
mmap 的另一个作用就是把一个文件的一部分映射到内存中,这样可以想操作内存一样操作文件中的内容,方便 随机读和改写文件中的内容。当flag 为 MAP_SHARED 时调用 mmap 可以将内存中的更改 写回到磁盘
虚拟存储器是现代计算机系统非常重要的概念之一,它总是默默的、自动的工作着。对于大多数程序员,特别是高级语言的程序员,并不十分需要了解这些概念。但对于每天和存储器打交道的 C 程序员,明白虚拟存储器的概念,知道它是怎么工作的将有非常大的帮助。