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


    目录

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

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

    三、获取分配区主函数arena_get

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

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

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

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


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


    《ptmalloc源码分析 - 分配区状态机malloc_state(02)》中,我们介绍过ptmalloc为了解决多线程同时并发争抢的时候,会分为主分配区非主分配区

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

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

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

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

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


    1. /**
    2. * 全局malloc状态管理
    3. */
    4. struct malloc_state
    5. {
    6. .......
    7. /* 分配区全局链表:分配区链表,主分配区放头部,新加入的分配区放main_arean.next 位置 Linked list */
    8. struct malloc_state *next;
    9. /* 分配区空闲链表 Linked list for free arenas. Access to this field is serialized
    10. by free_list_lock in arena.c. */
    11. struct malloc_state *next_free;
    12. /* freelist的状态,0-空闲 1-正在使用中,关联的线程数 Number of threads attached to this arena. 0 if the arena is on
    13. the free list. Access to this field is serialized by
    14. free_list_lock in arena.c. */
    15. INTERNAL_SIZE_T attached_threads;
    16. .....
    17. };

    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,意味着进程的主线程对应主分配区,然后再对主分配区进行初始化操作。

    1. /*
    2. * ptmalloc_init 初始化过程
    3. */
    4. static void ptmalloc_init(void) {
    5. /**
    6. * 1. 判断是否已经初始化,如果初始化过了,则不再执行;
    7. * 2. 如果等于0,则正在初始化,如果等于1,则初始化完成
    8. */
    9. if (__malloc_initialized >= 0)
    10. return;
    11. __malloc_initialized = 0;
    12. ........
    13. /**
    14. * 1. main_arena为主分配区域
    15. * 2. malloc_init_state 初始化主分配区数据
    16. * 3. 将主线程的thread_arena值设置为main_arena
    17. */
    18. thread_arena = &main_arena;
    19. malloc_init_state(&main_arena);
    20. ......
    21. /* 初始化完毕,则设置为1 */
    22. __malloc_initialized = 1;
    23. }

    arena_get整个流程是这样的:

    • 先从私有变量中thread_arena尝试获取分配区,不同线程都会设置自己的分配区

    • 如果分配区存在,则加锁进行处理,直接返回当前分配区

    • 如果分配区不存在,则调用arena_get2函数,从空闲链表或者新创建分配区

    • thread_arena = &main_arena;  进程的主线程对应的是主分配区

    • 如果当前线程没有设置过分配区,则通过arena_get2进行分配区的申请

    1. /**
    2. * 1. 先从私有变量中thread_arena尝试获取分配区,不同线程都会设置自己的分配区
    3. * 2. 如果分配区存在,则加锁进行处理,直接返回当前分配区
    4. * 3. 如果分配区不存在,则调用arena_get2函数,从空闲链表或者新创建分配区
    5. * 4. thread_arena = &main_arena; 进程的主线程对应的是主分配区
    6. * 5. 如果当前线程没有设置过分配区,则通过arena_get2进行分配区的申请
    7. */
    8. #define arena_get(ptr, size) do { \
    9. ptr = thread_arena; \
    10. arena_lock (ptr, size); \
    11. } while (0)
    12. #define arena_lock(ptr, size) do { \
    13. if (ptr) \
    14. __libc_lock_lock (ptr->mutex); \
    15. else \
    16. ptr = arena_get2 ((size), NULL); \
    17. } 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,如果无法使用则阻塞等待。

    1. /**
    2. * 获取一个分配区,如果有空闲的,则走空闲链表;没有则创建新的分配区;分配区满了,则等待释放
    3. */
    4. static mstate arena_get2(size_t size, mstate avoid_arena) {
    5. mstate a;
    6. static size_t narenas_limit;
    7. /* 从空闲链表上获取一个mstate的分配区 */
    8. a = get_free_list();
    9. /* 如果空闲链表为NULL,则创建一个新的arean分配区 */
    10. if (a == NULL) {
    11. /* Nothing immediately available, so generate a new arena. */
    12. /* 多少个分配区,根据系统来决定,一个进程最多能分配的arena个数在64位下是8 * core,32位下是2 * core个
    13. * arena 对于32位系统,数量最多为核心数量2倍,64位则最多为核心数量8倍,可以用来保证多线程的堆空间分配的高效性。
    14. * 主要存储了较高层次的一些信息。有一个main_arena,是由主线程创建的,thread_arena则为各线程创建的,
    15. * 当arena满了之后就不再创建而是与其他arena共享一个arena,方法为依次给各个arena上锁(查看是否有其他线程正在使用该arena),
    16. * 如果上锁成功(没有其他线程正在使用),则使用该arena,之后一直使用这个arena,如果无法使用则阻塞等待。
    17. * */
    18. if (narenas_limit == 0) {
    19. if (mp_.arena_max != 0)
    20. narenas_limit = mp_.arena_max;
    21. else if (narenas > mp_.arena_test) {
    22. int n = __get_nprocs();
    23. if (n >= 1)
    24. narenas_limit = NARENAS_FROM_NCORES(n);
    25. else
    26. /* We have no information about the system. Assume two
    27. cores. */
    28. narenas_limit = NARENAS_FROM_NCORES(2); //默认是核数的两倍
    29. }
    30. }
    31. repeat: ;
    32. size_t n = narenas; //narenas=1
    33. /* NB: the following depends on the fact that (size_t)0 - 1 is a
    34. very large number and that the underflow is OK. If arena_max
    35. is set the value of arena_test is irrelevant. If arena_test
    36. is set but narenas is not yet larger or equal to arena_test
    37. narenas_limit is 0. There is no possibility for narenas to
    38. be too big for the test to always fail since there is not
    39. enough address space to create that many arenas. */
    40. /* */
    41. if (__glibc_unlikely(n <= narenas_limit - 1)) {
    42. if (catomic_compare_and_exchange_bool_acq(&narenas, n + 1, n))
    43. goto repeat;
    44. a = _int_new_arena(size); //创建一个新的分配区
    45. if (__glibc_unlikely(a == NULL))
    46. catomic_decrement(&narenas);
    47. } else
    48. a = reused_arena(avoid_arena); //复用默认分区
    49. }
    50. return a;
    51. }

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


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

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

    1. /**
    2. * 从FreeList上获取一个分配区
    3. */
    4. /* Remove an arena from free_list. */
    5. static mstate get_free_list(void) {
    6. mstate replaced_arena = thread_arena; //获取当前线程分配区
    7. /* free_list 全局变量 */
    8. mstate result = free_list; //当前空闲的分配区
    9. if (result != NULL) {
    10. __libc_lock_lock(free_list_lock); //加锁
    11. result = free_list; //再次获取free_list
    12. if (result != NULL) {
    13. free_list = result->next_free; //移动free_list
    14. /* The arena will be attached to this thread. */
    15. assert(result->attached_threads == 0);
    16. result->attached_threads = 1; //修改分配区的线程绑定个数
    17. detach_arena(replaced_arena);
    18. }
    19. __libc_lock_unlock(free_list_lock); //解除锁
    20. /* 分配区加锁,并将thread_arena设置为result */
    21. if (result != NULL) {
    22. LIBC_PROBE(memory_arena_reuse_free_list, 1, result);
    23. __libc_lock_lock(result->mutex);
    24. thread_arena = result; //将线程的分配区设置为result
    25. }
    26. }
    27. return result;
    28. }
    29. /* Remove the arena from the free list (if it is present).
    30. free_list_lock must have been acquired by the caller.
    31. 移动链表地址,移除free_list上的分配区结构*/
    32. static void remove_from_free_list(mstate arena) {
    33. mstate *previous = &free_list;
    34. for (mstate p = free_list; p != NULL; p = p->next_free) {
    35. assert(p->attached_threads == 0);
    36. if (p == arena) {
    37. /* Remove the requested arena from the list. */
    38. *previous = p->next_free;
    39. break;
    40. } else
    41. previous = &p->next_free;
    42. }
    43. }

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


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

    • 首先调用new_heap,该结构主要用来记录堆信息。new_heap只在非主分配区会使用,非主分配区一般都是通过MMAP向系统申请内存。非主分配区申请后,是不能被销毁的
    • 然后通过malloc_init_state函数,对分配区的状态机结构进行初始化。并设置attached_threads字段,关联的进程个数。将thread_arena的值设置为状态机结构
    • 最后,将新的分配区加入到全局链表上main_arena.next,新申请的分配区都会放入主分配区的下一个位置设置为1(表示有一个线程关联这个分配区)
    1. /**
    2. * 初始化一个新的分配区arena
    3. * 该函数主要创建:非主分配区
    4. * 主分配区在ptmalloc_init中初始化,并且设置了全局变量main_arena的值
    5. */
    6. static mstate _int_new_arena(size_t size) {
    7. mstate a;
    8. heap_info *h;
    9. char *ptr;
    10. unsigned long misalign;
    11. /* 分配一个heap_info,用于记录堆的信息,非主分配区一般都是通过MMAP向系统申请内存;非主分配区申请后,是不能被销毁的 */
    12. h = new_heap(size + (sizeof(*h) + sizeof(*a) + MALLOC_ALIGNMENT),
    13. mp_.top_pad);å
    14. if (!h) {
    15. /* Maybe size is too large to fit in a single heap. So, just try
    16. to create a minimally-sized arena and let _int_malloc() attempt
    17. to deal with the large request via mmap_chunk(). */
    18. h = new_heap(sizeof(*h) + sizeof(*a) + MALLOC_ALIGNMENT, mp_.top_pad);
    19. if (!h)
    20. return 0;
    21. }
    22. a = h->ar_ptr = (mstate)(h + 1); //heap_info->ar_ptr的值设置成mstate的分配区状态机的数据结构
    23. malloc_init_state(a); //初始化mstate
    24. a->attached_threads = 1; //设置进程关联个数
    25. /*a->next = NULL;*/
    26. a->system_mem = a->max_system_mem = h->size;
    27. /* Set up the top chunk, with proper alignment. */
    28. ptr = (char *) (a + 1);
    29. misalign = (unsigned long) chunk2mem(ptr) & MALLOC_ALIGN_MASK;
    30. if (misalign > 0)
    31. ptr += MALLOC_ALIGNMENT - misalign;
    32. top (a) = (mchunkptr) ptr;
    33. set_head(top(a), (((char *) h + h->size) - ptr) | PREV_INUSE);
    34. LIBC_PROBE(memory_arena_new, 2, a, size);
    35. mstate replaced_arena = thread_arena;
    36. thread_arena = a; //将当前线程设置mstate
    37. __libc_lock_init(a->mutex); //初始化分配区锁
    38. __libc_lock_lock(list_lock); //加上分配区锁
    39. /* 将新的分配区加入到全局链表上,新申请的分配区都会放入主分配区的下一个位置*/
    40. /* Add the new arena to the global list. */
    41. a->next = main_arena.next;
    42. /* FIXME: The barrier is an attempt to synchronize with read access
    43. in reused_arena, which does not acquire list_lock while
    44. traversing the list. */
    45. atomic_write_barrier();
    46. main_arena.next = a;
    47. __libc_lock_unlock(list_lock);
    48. /* 调整attached_threads状态*/
    49. __libc_lock_lock(free_list_lock);
    50. detach_arena(replaced_arena);
    51. __libc_lock_unlock(free_list_lock);
    52. __malloc_fork_lock_parent. */
    53. __libc_lock_lock(a->mutex); //解除分配区锁
    54. return a;
    55. }
    56. /* Remove the arena from the free list (if it is present).
    57. free_list_lock must have been acquired by the caller.
    58. 移动链表地址,移除free_list上的分配区结构*/
    59. static void remove_from_free_list(mstate arena) {
    60. mstate *previous = &free_list;
    61. for (mstate p = free_list; p != NULL; p = p->next_free) {
    62. assert(p->attached_threads == 0);
    63. if (p == arena) {
    64. /* Remove the requested arena from the list. */
    65. *previous = p->next_free;
    66. break;
    67. } else
    68. previous = &p->next_free;
    69. }
    70. }

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


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

    1. /* Lock and return an arena that can be reused for memory allocation.
    2. Avoid AVOID_ARENA as we have already failed to allocate memory in
    3. it and it is currently locked.
    4. 如果分配区全部处于忙碌中,则通过遍历方式,尝试没有加锁的分配区进行分配操作
    5. */
    6. static mstate reused_arena(mstate avoid_arena) {
    7. mstate result;
    8. /* FIXME: Access to next_to_use suffers from data races. */
    9. static mstate next_to_use;
    10. if (next_to_use == NULL)
    11. next_to_use = &main_arena;
    12. /* Iterate over all arenas (including those linked from
    13. free_list). 循环遍历整个分配区链表 */
    14. result = next_to_use;
    15. do {
    16. if (!__libc_lock_trylock(result->mutex)) //寻找一个不能锁定的分配区
    17. goto out;
    18. /* FIXME: This is a data race, see _int_new_arena. */
    19. result = result->next;
    20. } while (result != next_to_use);
    21. /* Avoid AVOID_ARENA as we have already failed to allocate memory
    22. in that arena and it is currently locked. */
    23. if (result == avoid_arena)
    24. result = result->next;
    25. /* No arena available without contention. Wait for the next in line. */
    26. LIBC_PROBE(memory_arena_reuse_wait, 3, &result->mutex, result, avoid_arena);
    27. __libc_lock_lock(result->mutex);
    28. /* 跳转操作 */
    29. out:
    30. /* Attach the arena to the current thread. */
    31. {
    32. /* Update the arena thread attachment counters. */
    33. mstate replaced_arena = thread_arena;
    34. __libc_lock_lock(free_list_lock); //加锁
    35. detach_arena(replaced_arena);
    36. /* We may have picked up an arena on the free list. We need to
    37. preserve the invariant that no arena on the free list has a
    38. positive attached_threads counter (otherwise,
    39. arena_thread_freeres cannot use the counter to determine if the
    40. arena needs to be put on the free list). We unconditionally
    41. remove the selected arena from the free list. The caller of
    42. reused_arena checked the free list and observed it to be empty,
    43. so the list is very short. */
    44. remove_from_free_list(result); //free list移动除,多个线程共用
    45. ++result->attached_threads; //线程引用数量+1
    46. __libc_lock_unlock(free_list_lock); //解锁
    47. }
    48. LIBC_PROBE(memory_arena_reuse, 2, result, avoid_arena);
    49. thread_arena = result; //设置线程
    50. next_to_use = result->next; //貌似没意义的一行代码
    51. return result;
    52. }

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

  • 相关阅读:
    <Linux>进程地址空间
    阿里云OSS云存储简介 与 基本概念
    设计模式:设计模式概述
    JAVA工程师面试专题-《消息队列》篇
    从书本《皮囊》摘录的几个句子
    代码质量与安全 | 想在发布竞赛中胜出?Sonar来帮你
    Spring常见的注解
    Java生成Jar包方法
    【SSM】Spring系列——IoC 控制反转
    哪款蓝牙耳机打电话好用?打电话用的蓝牙耳机推荐
  • 原文地址:https://blog.csdn.net/initphp/article/details/127750294