【笔记】glibc、maclloc理解

127 阅读8分钟

1.堆内存管理机制

  • ptmalloc2 – glibc

    gcc -o mthread mthread.c -lpthread

    cat /proc/[线程id]/maps

After malloc and before free in thread/* Per thread arena example. */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* threadFunc(void* arg) {
        printf("Before malloc in thread 1\n");
        getchar();
        char* addr = (char*) malloc(1000);
        printf("After malloc and before free in thread 1\n");
        getchar();
        free(addr);
        printf("After free in thread 1\n");
        getchar();
}

int main() {
        pthread_t t1;
        void* s;
        int ret;
        char* addr;

        printf("Welcome to per thread arena example::%d\n",getpid());
        printf("Before malloc in main thread\n");
        getchar();
        addr = (char*) malloc(1000);
        printf("After malloc and before free in main thread\n");
        getchar();
        free(addr);
        printf("After free in main thread\n");
        getchar();
        ret = pthread_create(&t1, NULL, threadFunc, NULL);
        if(ret)
        {
                printf("Thread creation error\n");
                return -1;
        }
        ret = pthread_join(t1, &s);
        if(ret)
        {
                printf("Thread join error\n");
                return -1;
        }
        return 0;
}

1.Before malloc in main thread :

没有 heap segement的

2.After malloc in main thread:

brk系统调用实现。分配堆栈在数据段之上。

3.After free in main thread:

堆空间没释放。由maclloc管理,chunk添加到main arenas的bin。glibc malloc会先尝试从bins中找到一个满足要求的chunk,如果没有才会向操作系统申请新的堆空间。

4.Before malloc in thread1:

thread1调用malloc之前,并没有heap segement,但是thread1的栈已经分配完毕。

5.After malloc in thread1:

thread1的heap segment已经分配完毕。同时从这个区域的起始地址可以看出,它并不是通过brk分配的,而是通过mmap分配,因为它的区域为b7500000-b7600000共1MB,并不是同程序的data segment相邻。同时,我们还能看出在这1MB中,根据内存属性分为了2部分:0xb7500000-0xb7520000共132KB大小的空间是可读可写属性;后面的是不可读写属性。原来,这里只有可读写的132KB空间才是thread1的堆空间,即thread1 arena。

2.Arena理解

1.Arena数量限制

For 32 bit systems: Number of arena = 2 * number of cores. For 64 bit systems: Number of arena = 8 * number of cores.

2.多Arean的管理

单核心pc装了32位操作系统,运行多线程app。4线程(1主线程+3用户线程)。

线程个数>arena个数,此时glibc malloc确保4个线程共享3(2*核心数+1=3)个arena。

当main thread首次调用malloc的时候,glibc malloc会直接分配一个main arena,不需要任何附加条件。

当user1和user2 thread首次malloc的时候,glibc malloc会为每个线程创建一个新的thread arena。此时thread和arena是一对一的。

当user3 thread首次maclloc的时候,出现问题了。此时glibc malloc能维护的arena个数已经达到上限,无法在此为user3 thread创建arena,所以需要复用已经分配好的arena。

1)glibc malloc循环遍历所有可用的arenas,尝试对可用的arena加锁,如果成功lock,就返回

2)如果没找到可用的arena,会把user3 thread的malloc阻塞,直到有可用的arena为止

3)如果user3 thread再次调用malloc,会尝试使用最近访问的arena,可用直接返回,不可用阻塞线程。

3.多线程堆管理

1.三种数据结构:

  • heap_info:Heap Header

    一个thread arena(不包含mainc thread)可以包含多个heaps,每个heap都有他自己的header。当heap不够用时,malloc会通过mmap申请新的堆空间,新的堆空间会被添加到当前thread arena中。

    typedef struct _heap_info
    {
      mstate ar_ptr; /* Arena for this heap. */
      struct _heap_info *prev; /* Previous heap. */
      size_t size;   /* Current size in bytes. */
      size_t mprotect_size; /* Size in bytes that has been mprotected
                               PROT_READ|PROT_WRITE.  */
      /* Make sure the following data is properly aligned, particularly
         that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
         MALLOC_ALIGNMENT. */
      char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
    } heap_info;
    
  • malloc_state:Arena Header

    每个thread只含有一个Arena Header。包含bins、top chunk、最后一个remainder chunk等。

    struct malloc_state
    {
      /* Serialize access.  */
      mutex_t mutex;
    
      /* Flags (formerly in max_fast).  */
      int flags;
    
      /* Fastbins */
      mfastbinptr fastbinsY[NFASTBINS];
    
      /* Base of the topmost chunk -- not otherwise kept in a bin */
      mchunkptr top;
    
      /* The remainder from the most recent split of a small request */
      mchunkptr last_remainder;
    
      /* Normal bins packed as described above */
      mchunkptr bins[NBINS * 2 - 2];
    
      /* Bitmap of bins */
      unsigned int binmap[BINMAPSIZE];
    
      /* Linked list */
      struct malloc_state *next;
    
      /* Linked list for free arenas.  */
      struct malloc_state *next_free;
    
      /* Memory allocated from the system in this arena.  */
      INTERNAL_SIZE_T system_mem;
      INTERNAL_SIZE_T max_system_mem;
    };
    
  • malloc_chunk:Chunk Header

    一个heap分成多个chunk,当用户调用malloc传递size参数时候,会根据size调整每个chunk的大小。

    struct malloc_chunk {
    
      INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
      INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */
    
      struct malloc_chunk* fd;         /* double links -- used only if free. */
      struct malloc_chunk* bk;
    
      /* Only used for large blocks: pointer to next larger size.  */
      struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
      struct malloc_chunk* bk_nextsize;
    };
    

不同于thread arena,main arena的arena header并不是sbrk heap segment的一部分,而是一个全局变量!因此它属于libc.so的data segment。

2.heap segment与arena关系

只有一个heap segment的main arena和thread arena的内存分布图:

image.png

一个thread arena中含有多个heap segments的情况:

image.png

thread arena只含有一个malloc_state,有两个heap_info。由于heap segements通过mmap分配,两者在内存分布上不相邻,为了便于管理,libc malloc会把第二个heap_info的prev分配给第一个heap_info的ar_ptr(结构体起始位置)上,第一个heap_info的ar_ptr指向malloc_state,形成一个单链表。

Chunk理解

glibc malloc会把整个堆内存空间分成连续的,大小不一定的chunk,所以chunk就是最小操作单位。总过分为4种chunk:
  1. Allocated chunk

  2. Free chunk

  3. Top chunk

  4. Last Remainder chunk

    简单来说就是两种,一种已经分配给用户使用的chunk,另一种未使用的chunk。

    在里面特定位置的某些标识符来区分。

    核心目的:高效分配和回收chunk,所以就有不同的算法。

    隐式链表:

    把一些边界信息(标识各个块的边界,以及已分配块和空闲块)作为chunk的一部分,嵌入到chunk内部。
    

image.png image.png

    每个chunk的大小必须为8的整倍数,所以chunk size的后3位是无效的,为了充分利用内存,堆管理器利用这3bit作为chunk标识位,比如0bit标记该chunk是否已经被分配。

allocated chunkd的padding部分主要是用于内存对其的

把整个堆内存组织成一个连续的已分配或者未分配的序列,就是隐式链表,内存结构如下:

截图

该链表隐式得由每个chunk的size字段连接起来,在分配的时候遍历整个堆内存的chunk,分析每个chunk的size字段找到合适的chunk。缺点就是内存回收时效率太低,没办法进行相邻多个free chunk的合并。只切割不合并会产生内存碎片,所以进化了一下,变成了带边界的chunk合并。

进化-带边界标记的合并技术

每个chunk的最后加了个Footer,就是该chunk header的副本。每个chunk的Footer都在bk的header前4字节,通过footer,很容易找到fd chunk的起始位置和分配状态,好合并了。

但是,每个chunk都包含一个header和footer,如果app频繁进行小内存申请和释放,会造成大量性能损耗。同时,考虑到只有对free chunk合并的时候采用footer,对allocated chunk不需要。可以优化一下:把fd chunk的已分配/空闲的标识位存在当前chunk的size字段first or second bit上,可以通过当前chunk的size字段找到fd chunk为free chunk。

超进化-支持多线程

需要新的标识位来标识当前chunk是否属于thread arena,以及该chunk是mmap来的还是brk来的。

PREV_INUSE(P): 表示前一个chunk是否为allocated。

IS_MMAPPED(M):表示当前chunk是否是通过mmap系统调用产生的。

NON_MAIN_ARENA(N):表示当前chunk是否是thread arena。

Top Chunk

当一个chunk处于arena最高地址的时候,就叫top chunk。

不属于任何bin。

当系统当前的free chunk都无法满足用户请求的内存大小的时候,这个top chunk才会分配给用户使用。

if(top chunk size > user apply){

top chunk 一分为2:

1)user apply size

2)new top chunk

}else{

扩展new heap->

1)在main arena通过sbrk扩展heap

2)在thread arena通过mmap扩展heap

}

Last Remainder Chunk

当用户请求的是一个small chunk,且该请求无法被small bin、unsorted bin满足的时候,就通过binmaps遍历bin查找最合适的chunk,如果该chunk有剩余部分的话,就将该剩余部分变成一个新的chunk加入到unsorted bin中,另外,再将该新的chunk变成新的last remainder chunk。

此类型的chunk用于提高连续malloc(small chunk)的效率,主要是提高内存分配的局部性。

当用户请求一个small chunk,且该请求无法被small bin满足,那么就转而交由unsorted bin处理。同时,假设当前unsorted bin中只有一个chunk的话——就是last remainder chunk,那么就将该chunk分成两部分:前者分配给用户,剩下的部分放到unsorted bin中,并成为新的last remainder chunk。这样就保证了连续malloc(small chunk)中,各个small chunk在内存分布中是相邻的,即提高了内存分配的局部性。