ptmalloc源码分析 – 多线程争抢竞技场Arena的实现(04)

一、为何要引入Arena竞技场概念

二、主分配区和非主分配区的数据结构

三、获取分配区主函数arena_get

四、首次申请分配区的核心函数arena_get2

1、get_free_list 从空闲链表中获取一个分配区

2、_int_new_arena 初始化创建一个新的分配区

3、reused_arena 分配区满后重复利用一个分配区

一、为何要引入Arena竞技场概念

  • 每个进程有一个主分配区,也可以允许有多个非主分配区。
  • 主分配区可以使用brk和mmap来分配,而非主分配区只能使用mmap来映射内存块
  • 非主分配区的数量一旦增加,则不会减少。
  • 主分配区和非主分配区形成一个 环形链表进行管理。通过malloc_state->next来链接

我们可以看一下一个线程调用malloc的时候的流程以及分配区的状态:

  • 当一个线程使用malloc分配内存的时候,首选会检查该线程环境中是否已经存在一个分配区,如果存在,则对该分配区进行加锁,并使用该分配区进行内存分配
  • 如果分配失败,则遍历链表中获取的未加锁的分配区
  • 如果整个链表都没有未加锁的分配区,则ptmalloc开辟一个新的分配区,假如malloc_state->next全局队列,并该线程在改内存分区上进行分配
  • 当释放这块内存的时候,首先获取分配区的锁,然后释放内存,如果其他线程正在使用,则等待其他线程

通过主分配区和非主分配区,就可以解决多线程的冲突问题了。

二、主分配区和非主分配区的数据结构

/**
 * 全局malloc状态管理
 */
struct malloc_state
{
.......

  /* 分配区全局链表:分配区链表,主分配区放头部,新加入的分配区放main_arean.next 位置 Linked list */
  struct malloc_state *next;

  /* 分配区空闲链表 Linked list for free arenas.  Access to this field is serialized
     by free_list_lock in arena.c.  */
  struct malloc_state *next_free;

  /* freelist的状态,0-空闲 1-正在使用中,关联的线程数 Number of threads attached to this arena.  0 if the arena is on
     the free list.  Access to this field is serialized by
     free_list_lock in arena.c.  */
  INTERNAL_SIZE_T attached_threads;

 .....

};

malloc_state是分配区的数据结构,起到一个状态机的作用,记录分配区的重要信息。

  • next:通过next来链接分配区,其中主分配区放链表头部,新加入的分配区放main_arena.next
  • next_free:分配区的空闲链表,通过该链表来管理忙闲状态,解决对线程分配冲突情况
  • attached_threads:空闲链表的状态记录,0-空闲,n-正在使用中,关联的线程个数(一个分配区可以给多个线程使用)

三、获取分配区主函数arena_get

获取分配区的主函数是arena_get(arena.c文件中),该函数主要从 thread_arena(当前线程的私有变量),获取一个分配区。如果获取到了,则加锁,进行后续的操作;如果没有获取到,线程第一次获取分配区,则调用arena_get2函数进行分配区的初始化。

这里有两个重要的变量,贯穿整个分配区:

  • main_arena:全局变量。进程(主线程)第一次创建的时候,会生成主分配区,然后保存在main_arena全局变量中
  • thread_arena:线程私有变量。每个线程都会设置这么一个变量,该变量保存对应的分配区。如果是主线程,则thread_arena设置成main_arena。

在第一次pcmalloc_init的时候,就将thread_arena设置成main_arena,意味着进程的主线程对应主分配区,然后再对主分配区进行初始化操作。

/*
 * ptmalloc_init 初始化过程
 */
static void ptmalloc_init(void) {
    /**
     * 1. 判断是否已经初始化,如果初始化过了,则不再执行;
     * 2. 如果等于0,则正在初始化,如果等于1,则初始化完成
     */
    if (__malloc_initialized >= 0)
        return;

    __malloc_initialized = 0;

........

    /**
     * 1. main_arena为主分配区域
     * 2. malloc_init_state 初始化主分配区数据
     * 3. 将主线程的thread_arena值设置为main_arena
     */
    thread_arena = &main_arena;

    malloc_init_state(&main_arena);

......
    /* 初始化完毕,则设置为1 */
    __malloc_initialized = 1;
}

arena_get整个流程是这样的:

  • 先从私有变量中thread_arena尝试获取分配区,不同线程都会设置自己的分配区
  • 如果分配区存在,则加锁进行处理,直接返回当前分配区
  • 如果分配区不存在,则调用arena_get2函数,从空闲链表或者新创建分配区
  • thread_arena = &main_arena; 进程的主线程对应的是主分配区
  • 如果当前线程没有设置过分配区,则通过arena_get2进行分配区的申请
/**
 * 1. 先从私有变量中thread_arena尝试获取分配区,不同线程都会设置自己的分配区
 * 2. 如果分配区存在,则加锁进行处理,直接返回当前分配区
 * 3. 如果分配区不存在,则调用arena_get2函数,从空闲链表或者新创建分配区
 * 4. thread_arena = &main_arena;  进程的主线程对应的是主分配区
 * 5. 如果当前线程没有设置过分配区,则通过arena_get2进行分配区的申请
 */
#define arena_get(ptr, size) do { \
      ptr = thread_arena;                             \
      arena_lock (ptr, size);                             \
  } while (0)

#define arena_lock(ptr, size) do {                        \
      if (ptr)                                    \
        __libc_lock_lock (ptr->mutex);                        \
      else                                    \
        ptr = arena_get2 ((size), NULL);                      \
  } while (0)

四、首次申请分配区的核心函数arena_get2

如果线程是第一次申请分配区,这调用arena_get2函数,该函数也在arena.c文件中,该函数主要实现了三个功能:

  • get_free_list:从空闲链表中获取一个分配区,如果空闲链表中有该分配区,则直接使用,返回结果
  • _int_new_arena:去创建一个新的分配区,也就是一个malloc_state结构的对象,并且挂载到main_arena.next链表上面
  • reused_arena:如果分配区已经分配满了(分配区有个数上限),则需要循环等待其中一个分配区解锁

分配区个数:多少个分配区,根据系统来决定,一个进程最多能分配的arena个数在64位下是8 * core + 1,32位下是2 * core + 1个;arena 对于32位系统,数量最多为核心数量2倍,64位则最多为核心数量8倍,可以用来保证多线程的堆空间分配的高效性。

当arena满了之后就不再创建而是与其他arena共享一个arena,方法为依次给各个arena上锁(查看是否有其他线程正在使用该arena),如果上锁成功(没有其他线程正在使用),则使用该arena,之后一直使用这个arena,如果无法使用则阻塞等待。

/**
 * 获取一个分配区,如果有空闲的,则走空闲链表;没有则创建新的分配区;分配区满了,则等待释放
 */
static mstate arena_get2(size_t size, mstate avoid_arena) {
    mstate a;

    static size_t narenas_limit;

    /* 从空闲链表上获取一个mstate的分配区 */
    a = get_free_list();

    /* 如果空闲链表为NULL,则创建一个新的arean分配区 */
    if (a == NULL) {
        /* Nothing immediately available, so generate a new arena.  */
        /* 多少个分配区,根据系统来决定,一个进程最多能分配的arena个数在64位下是8 * core,32位下是2 * core个
         * arena 对于32位系统,数量最多为核心数量2倍,64位则最多为核心数量8倍,可以用来保证多线程的堆空间分配的高效性。
         * 主要存储了较高层次的一些信息。有一个main_arena,是由主线程创建的,thread_arena则为各线程创建的,
         * 当arena满了之后就不再创建而是与其他arena共享一个arena,方法为依次给各个arena上锁(查看是否有其他线程正在使用该arena),
         * 如果上锁成功(没有其他线程正在使用),则使用该arena,之后一直使用这个arena,如果无法使用则阻塞等待。
         *  */
        if (narenas_limit == 0) {
            if (mp_.arena_max != 0)
                narenas_limit = mp_.arena_max;
            else if (narenas > mp_.arena_test) {
                int n = __get_nprocs();

                if (n >= 1)
                    narenas_limit = NARENAS_FROM_NCORES(n);
                else
                    /* We have no information about the system.  Assume two
                     cores.  */
                    narenas_limit = NARENAS_FROM_NCORES(2); //默认是核数的两倍
            }
        }
        repeat: ;
        size_t n = narenas; //narenas=1
        /* NB: the following depends on the fact that (size_t)0 - 1 is a
         very large number and that the underflow is OK.  If arena_max
         is set the value of arena_test is irrelevant.  If arena_test
         is set but narenas is not yet larger or equal to arena_test
         narenas_limit is 0.  There is no possibility for narenas to
         be too big for the test to always fail since there is not
         enough address space to create that many arenas.  */
        /* */
        if (__glibc_unlikely(n <= narenas_limit - 1)) { if (catomic_compare_and_exchange_bool_acq(&narenas, n + 1, n)) goto repeat; a="_int_new_arena(size);" 创建一个新的分配区 (__glibc_unlikely(a="=" null)) catomic_decrement(&narenas); } else 复用默认分区 return a; }< code></=>

通过全局变量free_list保存空闲链表。如果空闲链表为空,则直接返回空的值,如果不为空,则调整free_list的变量值为free_list->next。将attached_threads的值设置成1,说明已经有线程绑定该分配区进行使用了。最后需要将thread_arena的线程私有变量,设置成分配区。

remove_from_free_list函数:主要是移除free_list,直接操作next_free的指针即可

/**
 * &#x4ECE;FreeList&#x4E0A;&#x83B7;&#x53D6;&#x4E00;&#x4E2A;&#x5206;&#x914D;&#x533A;
 */
/* Remove an arena from free_list.  */
static mstate get_free_list(void) {
    mstate replaced_arena = thread_arena; //&#x83B7;&#x53D6;&#x5F53;&#x524D;&#x7EBF;&#x7A0B;&#x5206;&#x914D;&#x533A;
    /* free_list &#x5168;&#x5C40;&#x53D8;&#x91CF; */
    mstate result = free_list; //&#x5F53;&#x524D;&#x7A7A;&#x95F2;&#x7684;&#x5206;&#x914D;&#x533A;
    if (result != NULL) {
        __libc_lock_lock(free_list_lock); //&#x52A0;&#x9501;
        result = free_list; //&#x518D;&#x6B21;&#x83B7;&#x53D6;free_list
        if (result != NULL) {
            free_list = result->next_free; //&#x79FB;&#x52A8;free_list

            /* The arena will be attached to this thread.  */
            assert(result->attached_threads == 0);
            result->attached_threads = 1; //&#x4FEE;&#x6539;&#x5206;&#x914D;&#x533A;&#x7684;&#x7EBF;&#x7A0B;&#x7ED1;&#x5B9A;&#x4E2A;&#x6570;

            detach_arena(replaced_arena);
        }
        __libc_lock_unlock(free_list_lock); //&#x89E3;&#x9664;&#x9501;

        /* &#x5206;&#x914D;&#x533A;&#x52A0;&#x9501;&#xFF0C;&#x5E76;&#x5C06;thread_arena&#x8BBE;&#x7F6E;&#x4E3A;result */
        if (result != NULL) {
            LIBC_PROBE(memory_arena_reuse_free_list, 1, result);
            __libc_lock_lock(result->mutex);
            thread_arena = result; //&#x5C06;&#x7EBF;&#x7A0B;&#x7684;&#x5206;&#x914D;&#x533A;&#x8BBE;&#x7F6E;&#x4E3A;result
        }
    }

    return result;
}

/* Remove the arena from the free list (if it is present).

 free_list_lock must have been acquired by the caller.

 &#x79FB;&#x52A8;&#x94FE;&#x8868;&#x5730;&#x5740;&#xFF0C;&#x79FB;&#x9664;free_list&#x4E0A;&#x7684;&#x5206;&#x914D;&#x533A;&#x7ED3;&#x6784;*/
static void remove_from_free_list(mstate arena) {
    mstate *previous = &free_list;
    for (mstate p = free_list; p != NULL; p = p->next_free) {
        assert(p->attached_threads == 0);
        if (p == arena) {
            /* Remove the requested arena from the list.  */
            *previous = p->next_free;
            break;
        } else
            previous = &p->next_free;
    }
}

_int_new_arena函数主要是创建一个新的分配区,该分配区主要是非主分配区类型。主分配区在ptmalloc_init中初始化,并且设置了全局变量main_arena的值。

  • 首先调用new_heap,该结构主要用来记录堆信息。new_heap只在非主分配区会使用,非主分配区一般都是通过MMAP向系统申请内存。非主分配区申请后,是不能被销毁的
  • 然后通过malloc_init_state函数,对分配区的状态机结构进行初始化。并设置attached_threads字段,关联的进程个数。将thread_arena的值设置为状态机结构
  • 最后,将新的分配区加入到全局链表上main_arena.next,新申请的分配区都会放入主分配区的下一个位置设置为1(表示有一个线程关联这个分配区)
/**
 * &#x521D;&#x59CB;&#x5316;&#x4E00;&#x4E2A;&#x65B0;&#x7684;&#x5206;&#x914D;&#x533A;arena
 * &#x8BE5;&#x51FD;&#x6570;&#x4E3B;&#x8981;&#x521B;&#x5EFA;&#xFF1A;&#x975E;&#x4E3B;&#x5206;&#x914D;&#x533A;
 * &#x4E3B;&#x5206;&#x914D;&#x533A;&#x5728;ptmalloc_init&#x4E2D;&#x521D;&#x59CB;&#x5316;&#xFF0C;&#x5E76;&#x4E14;&#x8BBE;&#x7F6E;&#x4E86;&#x5168;&#x5C40;&#x53D8;&#x91CF;main_arena&#x7684;&#x503C;
 */
static mstate _int_new_arena(size_t size) {
    mstate a;
    heap_info *h;
    char *ptr;
    unsigned long misalign;

    /* &#x5206;&#x914D;&#x4E00;&#x4E2A;heap_info&#xFF0C;&#x7528;&#x4E8E;&#x8BB0;&#x5F55;&#x5806;&#x7684;&#x4FE1;&#x606F;&#xFF0C;&#x975E;&#x4E3B;&#x5206;&#x914D;&#x533A;&#x4E00;&#x822C;&#x90FD;&#x662F;&#x901A;&#x8FC7;MMAP&#x5411;&#x7CFB;&#x7EDF;&#x7533;&#x8BF7;&#x5185;&#x5B58;&#xFF1B;&#x975E;&#x4E3B;&#x5206;&#x914D;&#x533A;&#x7533;&#x8BF7;&#x540E;&#xFF0C;&#x662F;&#x4E0D;&#x80FD;&#x88AB;&#x9500;&#x6BC1;&#x7684; */
    h = new_heap(size + (sizeof(*h) + sizeof(*a) + MALLOC_ALIGNMENT),
            mp_.top_pad);&#xE5;
    if (!h) {
        /* Maybe size is too large to fit in a single heap.  So, just try
         to create a minimally-sized arena and let _int_malloc() attempt
         to deal with the large request via mmap_chunk().  */
        h = new_heap(sizeof(*h) + sizeof(*a) + MALLOC_ALIGNMENT, mp_.top_pad);
        if (!h)
            return 0;
    }
    a = h->ar_ptr = (mstate)(h + 1); //heap_info->ar_ptr&#x7684;&#x503C;&#x8BBE;&#x7F6E;&#x6210;mstate&#x7684;&#x5206;&#x914D;&#x533A;&#x72B6;&#x6001;&#x673A;&#x7684;&#x6570;&#x636E;&#x7ED3;&#x6784;

    malloc_init_state(a); //&#x521D;&#x59CB;&#x5316;mstate
    a->attached_threads = 1; //&#x8BBE;&#x7F6E;&#x8FDB;&#x7A0B;&#x5173;&#x8054;&#x4E2A;&#x6570;
    /*a->next = NULL;*/
    a->system_mem = a->max_system_mem = h->size;

    /* Set up the top chunk, with proper alignment. */
    ptr = (char *) (a + 1);
    misalign = (unsigned long) chunk2mem(ptr) & MALLOC_ALIGN_MASK;
    if (misalign > 0)
        ptr += MALLOC_ALIGNMENT - misalign;
    top (a) = (mchunkptr) ptr;
    set_head(top(a), (((char *) h + h->size) - ptr) | PREV_INUSE);

    LIBC_PROBE(memory_arena_new, 2, a, size);
    mstate replaced_arena = thread_arena;
    thread_arena = a; //&#x5C06;&#x5F53;&#x524D;&#x7EBF;&#x7A0B;&#x8BBE;&#x7F6E;mstate
    __libc_lock_init(a->mutex); //&#x521D;&#x59CB;&#x5316;&#x5206;&#x914D;&#x533A;&#x9501;

    __libc_lock_lock(list_lock); //&#x52A0;&#x4E0A;&#x5206;&#x914D;&#x533A;&#x9501;

    /* &#x5C06;&#x65B0;&#x7684;&#x5206;&#x914D;&#x533A;&#x52A0;&#x5165;&#x5230;&#x5168;&#x5C40;&#x94FE;&#x8868;&#x4E0A;&#xFF0C;&#x65B0;&#x7533;&#x8BF7;&#x7684;&#x5206;&#x914D;&#x533A;&#x90FD;&#x4F1A;&#x653E;&#x5165;&#x4E3B;&#x5206;&#x914D;&#x533A;&#x7684;&#x4E0B;&#x4E00;&#x4E2A;&#x4F4D;&#x7F6E;*/
    /* Add the new arena to the global list.  */
    a->next = main_arena.next;
    /* FIXME: The barrier is an attempt to synchronize with read access
     in reused_arena, which does not acquire list_lock while
     traversing the list.  */
    atomic_write_barrier();
    main_arena.next = a;
    __libc_lock_unlock(list_lock);

    /* &#x8C03;&#x6574;attached_threads&#x72B6;&#x6001;*/
    __libc_lock_lock(free_list_lock);
    detach_arena(replaced_arena);
    __libc_lock_unlock(free_list_lock);

     __malloc_fork_lock_parent.  */

    __libc_lock_lock(a->mutex); //&#x89E3;&#x9664;&#x5206;&#x914D;&#x533A;&#x9501;

    return a;
}

/* Remove the arena from the free list (if it is present).

 free_list_lock must have been acquired by the caller.

 &#x79FB;&#x52A8;&#x94FE;&#x8868;&#x5730;&#x5740;&#xFF0C;&#x79FB;&#x9664;free_list&#x4E0A;&#x7684;&#x5206;&#x914D;&#x533A;&#x7ED3;&#x6784;*/
static void remove_from_free_list(mstate arena) {
    mstate *previous = &free_list;
    for (mstate p = free_list; p != NULL; p = p->next_free) {
        assert(p->attached_threads == 0);
        if (p == arena) {
            /* Remove the requested arena from the list.  */
            *previous = p->next_free;
            break;
        } else
            previous = &p->next_free;
    }
}

如果分配区全部处于忙碌中,则通过遍历方式,尝试没有加锁的分配区进行分配操作。如果得到一个没有加锁的分配区,则attached_threads关联的线程数,并将thread_arena设置到当前的分配区上。这样就实现了多线程环境下,分配区的重复利用。

/* Lock and return an arena that can be reused for memory allocation.

 Avoid AVOID_ARENA as we have already failed to allocate memory in
 it and it is currently locked.

 &#x5982;&#x679C;&#x5206;&#x914D;&#x533A;&#x5168;&#x90E8;&#x5904;&#x4E8E;&#x5FD9;&#x788C;&#x4E2D;&#xFF0C;&#x5219;&#x901A;&#x8FC7;&#x904D;&#x5386;&#x65B9;&#x5F0F;&#xFF0C;&#x5C1D;&#x8BD5;&#x6CA1;&#x6709;&#x52A0;&#x9501;&#x7684;&#x5206;&#x914D;&#x533A;&#x8FDB;&#x884C;&#x5206;&#x914D;&#x64CD;&#x4F5C;
 */
static mstate reused_arena(mstate avoid_arena) {
    mstate result;
    /* FIXME: Access to next_to_use suffers from data races.  */
    static mstate next_to_use;
    if (next_to_use == NULL)
        next_to_use = &main_arena;

    /* Iterate over all arenas (including those linked from
     free_list). &#x5FAA;&#x73AF;&#x904D;&#x5386;&#x6574;&#x4E2A;&#x5206;&#x914D;&#x533A;&#x94FE;&#x8868; */
    result = next_to_use;
    do {
        if (!__libc_lock_trylock(result->mutex)) //&#x5BFB;&#x627E;&#x4E00;&#x4E2A;&#x4E0D;&#x80FD;&#x9501;&#x5B9A;&#x7684;&#x5206;&#x914D;&#x533A;
            goto out;

        /* FIXME: This is a data race, see _int_new_arena.  */
        result = result->next;
    } while (result != next_to_use);

    /* Avoid AVOID_ARENA as we have already failed to allocate memory
     in that arena and it is currently locked.   */
    if (result == avoid_arena)
        result = result->next;

    /* No arena available without contention.  Wait for the next in line.  */
    LIBC_PROBE(memory_arena_reuse_wait, 3, &result->mutex, result, avoid_arena);
    __libc_lock_lock(result->mutex);

    /* &#x8DF3;&#x8F6C;&#x64CD;&#x4F5C; */
    out:
    /* Attach the arena to the current thread.  */
    {
        /* Update the arena thread attachment counters.   */
        mstate replaced_arena = thread_arena;
        __libc_lock_lock(free_list_lock); //&#x52A0;&#x9501;
        detach_arena(replaced_arena);

        /* We may have picked up an arena on the free list.  We need to
         preserve the invariant that no arena on the free list has a
         positive attached_threads counter (otherwise,
         arena_thread_freeres cannot use the counter to determine if the
         arena needs to be put on the free list).  We unconditionally
         remove the selected arena from the free list.  The caller of
         reused_arena checked the free list and observed it to be empty,
         so the list is very short.  */
        remove_from_free_list(result); //&#x4ECE;free list&#x79FB;&#x52A8;&#x9664;&#xFF0C;&#x591A;&#x4E2A;&#x7EBF;&#x7A0B;&#x5171;&#x7528;

        ++result->attached_threads; //&#x7EBF;&#x7A0B;&#x5F15;&#x7528;&#x6570;&#x91CF;+1

        __libc_lock_unlock(free_list_lock); //&#x89E3;&#x9501;
    }

    LIBC_PROBE(memory_arena_reuse, 2, result, avoid_arena);
    thread_arena = result;  //&#x8BBE;&#x7F6E;&#x7EBF;&#x7A0B;
    next_to_use = result->next; //&#x8C8C;&#x4F3C;&#x6CA1;&#x610F;&#x4E49;&#x7684;&#x4E00;&#x884C;&#x4EE3;&#x7801;

    return result;
}

非主分配区都是通过new_heap的方式,进行内存的申请和分配,下一章,我们重点讲解一下 heap_info堆信息的结构以及与分配区的关系

Original: https://blog.csdn.net/initphp/article/details/127750294
Author: 老码农zhuli
Title: ptmalloc源码分析 – 多线程争抢竞技场Arena的实现(04)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/660331/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球