glibc内存分配基础

对于很多开发者来说,内存分配和回收似乎一直都是操作系统的事情。特别是现在大多高级语言都自带 GC, 让程序员这方面的意识会更加薄弱。可能有很大一部分人认为, 我要 100byte 的空间, 那么系统就只会给我 100byte。

1) 咦,怎么分配这么多内存?

下面先来看一段代码:

void *fn(void *args) {
    malloc(100); // 申请 100 byte
    sleep(128);  // 休息 128s,为了方便查看内存进程使用
    return NULL;
}

int main(int argc, char **argv) {
    num = 16; 
    pthread_t pids[num];
    for (i = 0; i < num; i++) {
        pthread_create(&pids[i], NULL, fn, NULL);
    }   
    for (i = 0; i < num; i++) {
        pthread_join(pids[i], NULL);
    }   
    return 0;
}

代码很简单: 启动 16 个线程,每个线程申请 100 byte。理论上内存使用应该是 100 byte * 16 = 1600 byte, 加上一些元数据也就 K 级别的内存。而我们在 Centos(64bit) 跑一下,发现内存使用的情况如下:

image

"咦,虚拟内存怎么到 1G 了,是我撸多眼花了么?"。 没看错,就是占用了 1G 的虚拟内存。

看一下虚拟内存的分布,发现里面多了一堆 64M 的内存分配,而且数量刚好等于线程数。

[linty@184 ~]$ cat /proc/15374/smaps |grep "65404"|wc -l
16

那么问题就来了,为什么每个线程都分配了一个 64M 呢。

2) Arena(分配区)

导致分配这么多内存的原因其实在之前的一篇文章(glibc-arena) 已经介绍。glibc 为了提高的多核场景的内存分配效率,当并发分配内存的时候,会产生多个内存分配区。每个分配区会每次向操作系统 "批发" 一片内存(64bit机器的大小是 64M), 然后再基于批发过来的内存上面分配内存,这个就是上面为什么会申请这么多 64M 内存块的原因。

这个问题并不是本文讨论的重点,只是为了引出操作系统分配内存方式在特定的场景会和我们想象的有很大的差别,进一步来细说 glibc 的内存分配方式。

3) sub-heap

每个进程内部只有一个主分配区以及可能会有多个非主分配区(non-arena), 主分配区分配虚拟内存可以使用 sbrk 以及 mmap, 而非主分配区是同 sub-heap(子堆) 来管理内存。

非主分配区每次会向操作系统申请一块固定大小的内存(64bit机器默认是64M),这个固定的堆大小就叫做 sub-heap。每次分配内存先从 sub-heap 里面分配,如果不够再从操作系统申请一个新的 sub-heap, 这些 sub-heap 通过链表连接起来。

image

4) 小块内存

glibc 的内存分配并不是我们所想的那样,申请 100byte 就从向操作系统申请 100byte, 释放就马上归还给操作系统,而是做了一层 "缓存", 避免频繁的触发系统调用而导致程序性能下降。

每个分配区都有一个 "缓存","缓存" 的几个基本组成部分是:

  1. fast bins
  2. unsorted bins
  3. small bins & large bins
  4. top chunk

fast bins 是用来加速小块分配。当分配或者释放小块内存(阀值默认是 64byte), 会优先从 fast bins 里面查找,如果没有再到其他的缓存区域查找。这个阀值可以通过 M_MXFAST 来设置,最大是 80byte.

unsorted bins 用来存放,大于 fast bins 阀值的内存块合并后的块,并在特定的时机会把这些内存块整理放到 small bins, large bins..

small bins & large bins 和 memcached 的 slabclass 角色类似,small bins 的各个 bin 的内存块是等长的,但 large bins 同一个 bin 里面的内存块并不是等长的。

small bins 每个bin 之间差是 8byte,最小的是 16byte, 接着是 24byte, 32byte... 最大是 512 byte

large bins 差依次为 64B、512B、4096B、32768B、262144B

top chunk 我们内存分配是从 sub-heap 分配出来,sub-heap 剩余未分配的部分就叫 top-chunk。内存分配会从上面那些 bin 优先分配,如果无法满足需求再通过 top-chunk 分配。

如果 top-chunk 空间不足,就会创建一个新的 sub-heap, 然后把新 sub-heap 的开始位置设置为 top-chunk.

image

5) 大块内存

上面说到内存分配是通过 sub-heap 来分配,而 sub-heap 是固定大小的空间,那万一我申请的内存比这个怎么办?

glibc 可以设置 MMAP_THRESHOLD ,当超过这个阀值内存分配不是走 sub-heap 来分配, 而是通过 mmap 来分配内存。释放内存的时候直接通过 unmmap 直接把内存归还给操作系统。

需要注意的点是, mmap 分配匿名内存页是会初始化这些内存页,这对性能会有一些损耗。不太适合不断的 mmap 和 unmmap。

6) END

内存分配并不是简单从操作系统申请一块内存,使用完直接归还,而是会有一些分配策略。

glibc 这种分配机制,在内存需要长期存储的场景(如 redis)也会产生一些问题,比如长期不释放小块内存,导致 top chunk 或者 sub-heap 无法收缩等等...

本文只介绍一些 glibc 内存分配的基础概念就足够长,如果加上代码分析基本就没法看,所以没有根据代码来分析内部一些实现,这个可以留着后面心情好的时候再来分析。

再见,朋友~