malloc 从哪里得到的内存空间

引子

在计算机高级编程语言中,C 语言相对来说是一种低级语言,从某种意义上讲,C 语言就是现代的“汇编语言”。说 C 语言低级很大程度上是因为 C 程序员需要手动管理存储,具体反应在公认最难最容易出错的指针上。比如编写 C 程序时,经常会出现莫名奇妙的段错误,并且内存泄漏会在不知不觉的情况下发生,直到耗尽你的计算机内存资源为止。更危险的则是缓冲区溢出,使程序非常容易受到攻击。

发明 C++ 的一个目的是为了提升 C 语言的抽象能力,还有一个目的就是为了消除指针,但 C++ 显然没有做到这一点。Java 则继承了 C++ 的遗愿,彻底的消灭了指针。Java 等高级语言采用严格的内存管理,动态的垃圾回收等机制使得程序员不用去手动管理内存,不用和底层打交道。但 C 语言的地位仍是无法取代的。在必须和底层打交道的时候,就得使用 C 语言(有时候甚至要用汇编语言),比如现代操作系统都是用 C 语言编写的。另外,高级语言在引入高级特征的同时,效率上就会有所损失,在非常强调执行效率的地方,C 语言通常是首选。

动态存储器分配

大多是 C 程序在运行时会需要额外的存储,并且不能事先知道需要的存储大小,这时候使用一种动态存储分配器(dynamic memory allocator)。C 标准库提供了一个称为 malloc 的程序进行显式存储器分配,使用 free 函数来释放已分配内存,另外还有 calloc 和 realloc 两个函数。

#include<stdlib.h>

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 是从哪得到的内存空间了。

在写这篇文章时,笔者犯了一个错误,就是讨论的是 linux, 却在 windows 平台上做实验,cygwin 虽然是 linux 的模拟器,但最终调用的还是 windows API. 在同样的配置下,linux (32 位)平台上的到的结果都远大于 windows 平台,但所得到的最终结果并没错,即:glibc 实现的 malloc 同时使用 brk 和 mmap 两个系统调用获取内存,对于大块内存优先使用 mmap.其他 C 库则取决与其 malloc 函数的具体实现。之前看过一篇文章说,windows 的底层是符合 POSIX 的,求高人证实。

2012/3/18

常见的一种说法是,malloc 分配的空间是堆(heap)中的空间,即上图 brk 处开始的地址空间。堆的大小可以使用系统函数 sbrk 和 brk 进行扩展,我们做个实验看看堆最大能都达到多大。

#include<stdio.h>
#include<stdlib.h>

int main()
{
    int size = 0;
    while (sbrk(1 &lt;&lt; 20) != (void *)-1)
    size++;
    printf ("heap max size is %d MBn", size);

    return 0;
}

在我的电脑(2G 内存,Windows 系统,cygwin 编译运行)上运行得到的结果为:

heap max size is 383 MB

显然这远远少于我们的预期,如果是这样的话那内存利用效率也太低了。我们再用 malloc 函数进行一次实验。

#include<stdio.h>
#include<stdlib.h>

int main()
{
    int size = 0;
    void *p;
    while ((p = malloc(1 &lt;&lt; 20)))
    {
        size++;
        if (!(size % 100))
        printf("%dMBt 0x%.8xn", size, (unsigned int)p);
    }

    printf("nmax malloc memory size is %d MBn", size);

    return 0;
}

在我电脑上运行的结果为:

100MB 0x79420008
200MB 0x72280008
300MB 0x6b840008
400MB 0x64e00008
500MB 0x5ded0008
600MB 0x57490008
700MB 0x50a50008
800MB 0x4a010008
900MB 0x435d0008
1000MB 0x3cb90008
1100MB 0x36150008
1200MB 0x2f710008
1300MB 0x28cd0008
1400MB 0x22290008
1500MB 0x1b850008
1600MB 0x043e03d8
1700MB 0x0a7e06f8
1800MB 0x10be0a18
1900MB 0x16fe0d38

max malloc memory size is 1925 MB

这次 malloc 分配到了将近 2G 的存储空间,比较符合预期。那么 malloc 的存储空间还从哪里得到的呢?事实上,malloc 还使用 mmap 和 munmap 函数显式的分配存储器空间。我们做类似的实验。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/mman.h>

int main()
{
    int size = 0;
    while (mmap(NULL, 1 &lt;&lt; 20, PROT_READ, MAP_PRIVATE|MAP_ANON, 0, 0) != (void *)-1)
        size++;

    printf ("mmap max size is %d MBn", size);

    return 0;
}

运行结果为:

mmap max size is 1641 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 函数
#include<unistd.h>
#include<sys/mman.h>

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 可以将内存中的更改写回到磁盘。我写了一个简单的使用 mmap 打开文件的程序包,放在了 github 上,在这里

总结

虚拟存储器是现代计算机系统非常重要的概念之一,它总是默默的、自动的工作着。对于大多数程序员,特别是高级语言的程序员,并不十分需要了解这些概念。但对于每天和存储器打交道的 C 程序员,明白虚拟存储器的概念,知道它是怎么工作的将有非常大的帮助。

malloc 从哪里得到的内存空间》上有10条评论

  1. wowotou

    不同的操作系统对C库的实现也不同,cygwin也是windows下的模拟器,windows平台上的malloc应该不是调用brk和sbrk的。

    回复
    1. 海洋 文章作者

      多谢指教,写这篇文章犯了一个较大的错误,本来是讲linux,却在windows平台上做实验,忘了cygwin最终还是调用win api的。在linux上得到的一些结果和这个相差比较大。

      回复
    1. 海洋 文章作者

      我也好奇怪,后台源码编辑看着没问题的,可能是插件的原因吧,昨天看着好好的。我用的是SyntaxHighlighter Evolved,感觉不太好,有什么推荐的没?
      准备等这个空间到期后迁移到 github pages 上去。

      回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注