glibc中的malloc方法其实是映射到 __libc_malloc
的,其通过ASM的符号表的形式来实现
1 2 3 4 strong_alias (__libc_malloc, __malloc) strong_alias (__libc_malloc, malloc) define strong_alias(original, alias) .globl C_SYMBOL_NAME (alias) ASM_LINE_SEP .set C_SYMBOL_NAME (alias),C_SYMBOL_NAME (original)
1. malloc 整体流程 1 以下是祛除一些辅助功能后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void * __libc_malloc (size_t bytes) { mstate ar_ptr; void *victim; // 1.1 原子的形式调用钩子方法 __malloc_hook 用以初始化 ptmalloc void *(*hook) (size_t, const void *) = atomic_forced_read (__malloc_hook); if (__builtin_expect (hook != NULL, 0)) return (*hook)(bytes, RETURN_ADDRESS (0)); // 1.2 获取 或 创建 arena 并且会将arena 加锁(mutex 互斥锁)处理 arena_get (ar_ptr, bytes); // ptmalloc 核心逻辑 victim = _int_malloc (ar_ptr, bytes); /* Retry with another arena only if we were able to find a usable arena before. */ // 如果 ar_ptr 指定的 arena不为空 也就是说 arena_get 获取arena是成功的 但是 _int_malloc 方法返回的 victim 却为空 这里会重新获取一遍arena arena_get_retry if (!victim && ar_ptr != NULL) { LIBC_PROBE (memory_malloc_retry, 1, bytes); ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } // 解除arena的互斥锁 if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr->mutex); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || ar_ptr == arena_for_chunk (mem2chunk (victim))); // 返回内存地址给用户 return victim; }
__libc_malloc
整体流程分如下几步:
通过钩子函数 __malloc_hook
先初始化 ptmalloc
且只会初始化一次
通过 arena_get 获取当前线程的 arena 如果不存在当前线程的arena则创建一个arena返回 且 这一步还给arena加上互斥锁
调用核心逻辑 _int_malloc 查询chunk
如果arena获取成功 上一步却没找到chunk 则通过arena_get_retry 尝试再次分配
查询成功解锁arena 返回地址给用户
这里需要展开的函数有 __malloc_hook
arena_get
_int_malloc
其中_int_malloc
作为核心函数 代码量比较大 会放在下一章进行详细分析,这里主要分析 __malloc_hook
__malloc_hook
2. __malloc_hook
初始化ptmalloc 2.1 malloc初始化调用逻辑 malloc初始化调用的钩子方法 定义如下:
1 void *weak_variable (*__malloc_hook) (size_t __size, const void *) = malloc_hook_ini;
这里通过 weak_variable
进行修饰,为了方便用户对这个钩子方法的替换,这个钩子方法指定到了 函数 malloc_hook_ini,源码如下:
1 2 3 4 5 6 7 8 static void * malloc_hook_ini (size_t sz, const void *caller) { // 清空malloc钩子方法的绑定 __malloc_hook = NULL; // 调用初始化方法 ptmalloc_init (); // 从新走 __libc_malloc 方法 return __libc_malloc (sz); }
当函数执行进来则会将钩子函数的变量设置为 NULL 也是为了保证这个初始化函数只会被调用一次,初始化的主要代码还是在函数 ptmalloc_init
,初始化完毕还是会调用函数 __libc_malloc
2.2 ptmalloc_init 下面是初始化的代码,这里主要为了看逻辑过程 删除了一些配置相关的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static __thread mstate thread_arena attribute_tls_model_ie; int __malloc_initialized = -1; static void ptmalloc_init (void) { // 如果执行过或者正在执行中这个方法则直接返回 if (__malloc_initialized >= 0) return; __malloc_initialized = 0; #ifdef SHARED /* In case this libc copy is in a non-default namespace, never use brk. Likewise if dlopened from statically linked program. */ Dl_info di; struct link_map *l; if (_dl_open_hook != NULL || (_dl_addr (ptmalloc_init, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)) __morecore = __failing_morecore; #endif // thread_arena 记录当前线程的arena指针地址 ptmalloc_init 中指定的是主线程的arena thread_arena = &main_arena; // 这里对主线程的 arena 进行初始化 malloc_init_state (&main_arena); // 这里省略了一些配置项设置的代码 // 初始化执行完毕 将变量 __malloc_initialized 赋值为1 __malloc_initialized = 1; }
结合上面的逻辑可以看出来,初始化的线程一定是主线程,所以这里的当前线程就是 main_arena thread_arena = &main_arena
,然后会通过函数 malloc_init_state
初始化一些值。 此方法开始的时候设置 __malloc_initialized=0
结束的时候设置 __malloc_initialized=1
。主要的初始化代码在函数 malloc_init_state
需要注意下其中变量 thread_arena 的定义 static __thread mstate thread_arena attribute_tls_model_ie;
变量通过关键词 __thread
进行修饰,也就是说这个全局变量是针对每一个线程的,每一个线程都拥有一个这样的全局变量 且 与其他线程隔离。 也就是说 新的线程调用此方法的时候 其 thread_arena 为 NULL 这个会在获取 arena 的函数中用到。
2.3 malloc_init_state 对arena的初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #define NBINS 128 #define DEFAULT_MXFAST (64 * SIZE_SZ / 4) // 16 * 8 = 128 // static void malloc_init_state (mstate av) { int i; mbinptr bin; /* Establish circular links for normal bins */ // 初始化 arena的 bin 列表 bin的作用就是 一个缓冲区 方便查询 for (i = 1; i < NBINS; ++i) { bin = bin_at (av, i); bin->fd = bin->bk = bin; } #if MORECORE_CONTIGUOUS if (av != &main_arena) #endif // 主线程arena 设置 XXX set_noncontiguous (av); // 主线程arena设置 fastbin的最大值 if (av == &main_arena) set_max_fast (DEFAULT_MXFAST); atomic_store_relaxed (&av->have_fastchunks, false); av->top = initial_top (av); }
函数 malloc_init_state
做的初始化功能主要有 初始化 arena 的 bins、如果是主线程 设置fastbin的最大值 以及 初始化 top 先看top的初始化
1 2 #define initial_top(M) (unsorted_chunks (M)) #define unsorted_chunks(M) (bin_at (M, 1))
top的初始化最终调用的还是 宏 bin_at
不同的是 top 固定了其index为1 所以我们重点关注 宏 bin_at
1 2 3 4 5 6 7 #define bin_at(m, i) (mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) - offsetof (struct malloc_chunk, fd)) // 将其简化则如下 &bins[(i-1)*2] - 8*2 // bins 的定义 typedef struct malloc_chunk* mchunkptr; mchunkptr bins[NBINS * 2 - 2];
从上面的定义关系中其实可以得出 &bins[(i-1)*2]
得出的其实是一个地址 在x64上占8个字节,后面的 - 8*2
相当于在数组上向左移动两个位置 最后我们得到 i 与 实际得到的值的关系是 (i-2)*2
结合代码
1 2 3 4 5 for (i = 1; i < NBINS; ++i) { bin = bin_at(av, i); // 这里指向 的是 bins 数组索引 (i-2)*2 bin->fd = bin->bk = bin; // 这里再调用 bin->fd bin->bk 其实是在移动上一步得到的地址 通过 malloc_chunk 可以得出 fd 右位移2个位置 bk 右位移3个位置 这个时候就很容易理解为什么bin_at 的宏定义中要 ` - offsetof (struct malloc_chunk, fd)` 了 // 也就是说通过bin_at 得到偏移后的地址 通过 bin->fb 正好又偏移回来了 也就是说 i = 1 时 idx 0 是bin_at(1)->fd idx 1 是bin_at(1)->bk }
其实最终得到的 bins 的结构如下:
1 2 3 4 5 arr idx 0 1 2 3 4 5 .............. 249 250 251 252 253 254 +---------+---------+---------+----------------+---------+---------+---------+ | fd | bk | fd | bk | fd | bk | .............. | fd | bk | fd | bk | fd | bk | +---------+---------+---------+----------------+---------+---------+---------+ bin_at(1) bin_at(2) bin_at(3) bin_at(N) bin_at(125) bin_at(126) bin_at(127)
这样一个结构 将来是要承载 smallbin largebin unsortedbin 这三种bin链表的,初始化的时候并没有指向到一个malloc_chunk 地址 而是初始化了数组的地址 后面判断 一个bin是否为空链表的时候会用到这个进行判断
3. arena 的创建 arena创建这部分是整个malloc中比较重要的一部分了,在malloc代码通过宏 arena_get
实现,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #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) // 合并之后的代码 ptr = thread_arena; // 如果当前线程arena不为空 则给当前线程加互斥锁 if (ptr) __libc_lock_lock (ptr->mutex); // 当前线程arena为空 说明当前线程是刚刚创建 需要给当前线程创建arena else ptr = arena_get2 ((size), NULL);
所以这里代码的重点是 给当前线程创建 arena 也就是调用方法 arena_get2
3.1 arena_get2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static size_t narenas = 1; static mstate arena_get2 (size_t size, mstate avoid_arena) { mstate a; static size_t narenas_limit; // 读取free_list a = get_free_list (); if (a == NULL) { /* Nothing immediately available, so generate a new arena. */ // 这里是给静态变量 narenas_limit 进行初始化值 if (narenas_limit == 0) { // arena_max有值则使用arena_max的值 if (mp_.arena_max != 0) narenas_limit = mp_.arena_max; else if (narenas > mp_.arena_test) { int n = __get_nprocs (); // 读取CPU核数 if (n >= 1) narenas_limit = NARENAS_FROM_NCORES (n); // ((n) * (sizeof (long) == 4 ? 2 : 8)) -> cpu核数 * [2(x32)|8(x64)] else /* We have no information about the system. Assume two cores. */ narenas_limit = NARENAS_FROM_NCORES (2); } } repeat:; size_t n = narenas; 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); if (__glibc_unlikely (a == NULL)) catomic_decrement (&narenas); } else a = reused_arena (avoid_arena); } // 有空闲arena 则直接返回了 return a; }
这段代码主要有以下几个步骤
通过函数 get_free_list 获取arena 如果获取到则直接返回 获取不到则继续往下进行
通过内核数量计算出来arena数量最大值 narenas_limit = cpu核数 * 2/8 (x32/x64)
通过 narenas 与 narenas_limit 确定是通过 _int_new_arena
创建新的 arena 还是 通过 reused_arena 利用现有的arena
接下来要着重分析的是 get_free_list
reused_arena
_int_new_arena
这三个函数
3.2 get_free_list 从free_list查找arena 从 free_list 中获取一个合适的 arena,free_list 是指向链表头部的指针,这个链表是由 malloc_state中的属性 next_free
链接的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 // free_list 是一个malloc_state的指针 用以做成链表 typedef struct malloc_state *mstate; static mstate free_list; static mstate get_free_list (void) { // 当前线程arena mstate replaced_arena = thread_arena; mstate result = free_list; /*读取free_list 这个链表是从 fork调用 时候从父进程copy过来的,如果是在父进程中则free_list 一直为 NULL */ if (result != NULL) { __libc_lock_lock (free_list_lock); // 将free_list 指向的malloc_state 从 malloc_state的next_free 组成的链表中取出 result = free_list; if (result != NULL) { free_list = result->next_free; /* The arena will be attached to this thread. */ assert (result->attached_threads == 0); result->attached_threads = 1; detach_arena (replaced_arena); // 将当前线程指向的 malloc_state 中的属性 attached_threads 减一 因为下面要将当前线程换成从 free_list 中获取的arena } __libc_lock_unlock (free_list_lock); if (result != NULL) { LIBC_PROBE (memory_arena_reuse_free_list, 1, result); __libc_lock_lock (result->mutex); // 取出的arena作为当前线程的arena thread_arena = result; } } // 若free_list 为 NULL 则直接返回NULL return result; } // 如果 replaced_arena 不为空 则将其attached_threads 减一 static void detach_arena (mstate replaced_arena) { if (replaced_arena != NULL) { assert (replaced_arena->attached_threads > 0); /* The current implementation only detaches from main_arena in case of allocation failure. This means that it is likely not beneficial to put the arena on free_list even if the reference count reaches zero. */ --replaced_arena->attached_threads; } }
问题是 free_list 的数据是从哪里产生的呢,是从系统调用中的 fork
方法产生的,由于linux中线程之间是共享内存的 而进程之间是不共享的,使用 fork
创建新的进程的时候 系统会复制一份父进程的内存到子进程中。系统的 fork
方法经过层层调用 最终调用的函数是 __malloc_fork_unlock_child
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void __malloc_fork_unlock_child (void) { // ptmalloc_init 没有调用过 也就是说 ptmalloc没有初始化 if (__malloc_initialized < 1) return; /* Push all arenas to the free list, except thread_arena, which is attached to the current thread. */ __libc_lock_init (free_list_lock); if (thread_arena != NULL) thread_arena->attached_threads = 1; // 初始化 free_list 变量 free_list = NULL; // 从主线程arena开始 for (mstate ar_ptr = &main_arena;; ) { __libc_lock_init (ar_ptr->mutex); /* free_list 作为一个链表的头指针,通过循环 arean_list 将他们组织到 一个 free_list 指引的链表上 这些arena 都是从父进程中copy过来的 */ if (ar_ptr != thread_arena) { /* This arena is no longer attached to any thread. */ ar_ptr->attached_threads = 0; // 通过 free_list 将这些arena通过next_free 串联成链表 ar_ptr->next_free = free_list; free_list = ar_ptr; } // 移动到下一个 arena ar_ptr = ar_ptr->next; // 如果到了主线程arena 说明循环了一圈 结束循环 if (ar_ptr == &main_arena) break; } __libc_lock_init (list_lock); }
3.3 reused_arena 重用arena 这个方法是从当前进程中的 arena 列表中查询可用的arena 进行接下来的内存分配。其思路是 先通过main_arena 为起点循环遍历 arena 链表,对链表中的arena进行加锁尝试 如果失败则循环到下一个arena 如果成功 将当前线程的arena引用变量 thread_arena替换为当前的arena。 替换过程需要做一定的处理
替换前的thread_arena 的属性 attached_thread需要减一
将当前arena从free_list 中移除 将当前arena的attached_thread 加一
给 thread_arena赋值并返回 最后还会移动下 next_to_use 方便下次使用1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 static mstate reused_arena (mstate avoid_arena) { mstate result; /* FIXME: Access to next_to_use suffers from data races. */ // 初始化静态变量 next_to_use 为 &main_arena 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). */ result = next_to_use; // 从main_arena开始循环arena链表 直到循环一圈停止查询。在循环过程中 尝试给arena加锁 加锁成功则执行 out 代码片段,另外 __libc_lock_trylock 不会等待锁的 拿不到锁会立刻返回 do { if (!__libc_lock_trylock (result->mutex)) 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); /* Attach the arena to the current thread. */ out: { /* Update the arena thread attachment counters. */ mstate replaced_arena = thread_arena; __libc_lock_lock (free_list_lock); detach_arena (replaced_arena); // 将这个arena的attached_thread 减一 因为thread_arena 需要更换 // 将result从free_list 中去除 remove_from_free_list (result); // 新的thread_arean 加一 ++result->attached_threads; __libc_lock_unlock (free_list_lock); } LIBC_PROBE (memory_arena_reuse, 2, result, avoid_arena); // 替换 thread_arena thread_arena = result; // 将 next_to_use 移动到下一个 next_to_use = result->next; return result; } static void remove_from_free_list (mstate arena) { mstate *previous = &free_list; // 循环 free_list for (mstate p = free_list; p != NULL; p = p->next_free) { assert (p->attached_threads == 0); if (p == arena) { // 在free_list 的链表上找到参数 arena /* Remove the requested arena from the list. */ *previous = p->next_free; break; } else previous = &p->next_free; } }
3.4 _int_new_arena
新建arena 新建一个arena 每个arena都是有若干个 heap 组成的 heap 通过 new_heap
创建,然后就是要设置一些 malloc_state 的参数 重要的是 这里创建的 top 后面的内存分配会从top上切割产生,后面有了 bins 的缓存则会从缓存中获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 static mstate _int_new_arena (size_t size) { mstate a; heap_info *h; char *ptr; unsigned long misalign; // 创建 sub_heap 其实 new_heap 是申请内存 这里将 malloc_state top_pad heap_info 和 size 全包含进去 申请内存了 // 多申请 MALLOC_ALIGNMENT 这段内存的目的是 下面对齐的时候需要用到的 怕内存不够 h = new_heap (size + (sizeof (*h) + sizeof (*a) + MALLOC_ALIGNMENT), mp_.top_pad); if (!h) { // 创建失败 再次尝试创建 再不成功直接返回 h = new_heap (sizeof (*h) + sizeof (*a) + MALLOC_ALIGNMENT, mp_.top_pad); if (!h) return 0; } // 给heap设置 arena 指针 a = h->ar_ptr = (mstate) (h + 1); // 初始化 arena的bins malloc_init_state (a); a->attached_threads = 1; /*a->next = NULL;*/ a->system_mem = a->max_system_mem = h->size; /* Set up the top chunk, with proper alignment. */ // 新建的arena 初始化arena 初始化top且内存尺寸对齐 // 这里可以看到 当新建一个arena的时候其里面的chunk只有一个top chunk ptr = (char *) (a + 1); // misalign 是向下对齐多出来的长度 misalign = (unsigned long) chunk2mem (ptr) & MALLOC_ALIGN_MASK; // 先上对齐 if (misalign > 0) ptr += MALLOC_ALIGNMENT - misalign; // 设置 top chunk top (a) = (mchunkptr) ptr; set_head (top (a), (((char *) h + h->size) - ptr) | PREV_INUSE); LIBC_PROBE (memory_arena_new, 2, a, size); // 切换 thread_arena mstate replaced_arena = thread_arena; thread_arena = a; // 当前新的arena 加锁 添加到arena链表 __libc_lock_init (a->mutex); __libc_lock_lock (list_lock); // 添加arena到链表 a->next = main_arena.next; atomic_write_barrier (); main_arena.next = a; __libc_lock_unlock (list_lock); __libc_lock_lock (free_list_lock); detach_arena (replaced_arena); __libc_lock_unlock (free_list_lock); // arena增加互斥锁 __libc_lock_lock (a->mutex); return a; }
着重看下 在 new_heap
申请完内存后 在函数 _int_new_arena
中对这段内存的划分部分 以及top 部分
1 2 3 4 5 6 7 8 9 10 // h 指向 new_heap 申请到的内存地址起始位置 并且类型是被转成 heap_info 后返回的,这里 h+1 正好是到 h分配完类型 heap_info 成员占用内存后的位置 // 且通过类型转换 将后面的内存又划分出了一部分malloc_state 的内存 a = h->ar_ptr = (mstate) (h + 1); ptr = (char *) (a + 1); // 这里是做内存对齐的操作 misalign = (unsigned long) chunk2mem (ptr) & MALLOC_ALIGN_MASK; if (misalign > 0) ptr += MALLOC_ALIGNMENT - misalign; // 开始分配top部分 top (a) = (mchunkptr) ptr; set_head (top (a), (((char *) h + h->size) - ptr) | PREV_INUSE);
内存划分后的情况如下图所示: 对于top部分的处理 宏 进行展开分析
1 2 3 4 5 6 7 8 #define top(ar_ptr) ((ar_ptr)->top) #define set_head(p, s) ((p)->mchunk_size = (s)) // 对应代码展开如下 ptr = (char *) (a + 1); a->top = (mchunkptr) ptr; // 确定指针位置 // h + size - ptr 这就是剩下的空闲空间的长度 a->top->mchunk_size = (((char *) h + h->size) - ptr) | PREV_INUSE; // 设置 top chunk 的 mchunk_size 字段 设置chunk大小 以及 设置 prev_inuse 为已使用(其实这个时候 top chunk 前面并没有 chunk ,设置为已使用是为了后面chunk合并的时候 有一个结束条件)
3.5 new_heap
新建arena 的 sub_heap 每个arena 是 由多个行程链表的 heap组成的 称作 sub_heap 吧,其创建的整体逻辑如下注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 static heap_info * new_heap (size_t size, size_t top_pad) { // 获取系统内存管理的基本尺寸 一般为 4K 这个值可以在 glibc中的全局变量中找到 size_t pagesize = GLRO (dl_pagesize); // 4K char *p1, *p2; unsigned long ul; heap_info *h; /* 对size进行处理 如果size + top_pad 小于 HEAP_MIN_SIZE 则使用最小值 如果size + top_pad 小于或等于HEAP_MAX_SIZE 则size = size + top_pad 如果size 大于 HEAP_MAX_SIZE 则 return 0 */ if (size + top_pad < HEAP_MIN_SIZE) size = HEAP_MIN_SIZE; else if (size + top_pad <= HEAP_MAX_SIZE) size += top_pad; else if (size > HEAP_MAX_SIZE) return 0; else size = HEAP_MAX_SIZE; size = ALIGN_UP (size, pagesize); // 4K对齐 p2 = MAP_FAILED; // 如果上次分配内存计算出了下次内存分配的起始地址 这里合一直接分配了 减少一定的计算量与内核态的切换 if (aligned_heap_area) { // 直接基于aligned_heap_area作为起始地址分配 p2 = (char *) MMAP (aligned_heap_area, HEAP_MAX_SIZE, PROT_NONE, MAP_NORESERVE); // 清除了 aligned_heap_area = NULL; // 分配失败 返还申请的内存 if (p2 != MAP_FAILED && ((unsigned long) p2 & (HEAP_MAX_SIZE - 1))) { __munmap (p2, HEAP_MAX_SIZE); p2 = MAP_FAILED; } } /* 这里通过mmap申请内存,成功会返回申请的内存段的起始地址。且这里想要将内存地址的起始与结束的地址是与 HEAP_MAX_SIZE 对齐的 内存长度是 HEAP_MAX_SIZE。 这里会看到先申请了两倍 HEAP_MAX_SIZE 尺寸的内存,这是怕 当返回的地址不是内存对齐的,这个时候需要向后调整内存地址 使内存起始地址与 HEAP_MAX_SIZE 对齐 这里向后是因为前面的地址可能已经被其他程序申请,然后又要保持内存长度是 HEAP_MAX_SIZE ,这样前面申请的 HEAP_MAX_SIZE 长度内存就不够了,需要再次调用mmap 为了减少调用 这里申请了两倍的内存 然后将多余的通过 __munmap 还回去 */ if (p2 == MAP_FAILED) { // 申请 两倍 HEAP_MAX_SIZE 长度的内存 p1是内核返回的内存起始地址 p1 = (char *) MMAP (0, HEAP_MAX_SIZE << 1, PROT_NONE, MAP_NORESERVE); if (p1 != MAP_FAILED) { // 分配成功 对内存尺寸进行调整 使其在 HEAP_MAX_SIZE长度的内存段上 // p1 针对HEAP_MAX_SIZE向上对齐 p2 = (char *) (((unsigned long) p1 + (HEAP_MAX_SIZE - 1)) & ~(HEAP_MAX_SIZE - 1)); ul = p2 - p1;//由于是向上 所以是 p2 - p1,若 ul = 0 说明 mmap分配的起始地址恰好是HEAP_MAX_SIZE对齐的,不然就需要调整 if (ul) __munmap (p1, ul); // 返还起始地址向上偏移后多出来的地址 else aligned_heap_area = p2 + HEAP_MAX_SIZE; // 返还由于 两倍HEAP_MAX_SIZE 多出来的部分 只需要 HEAP_MAX_SIZE 长度的内存就好了 __munmap (p2 + HEAP_MAX_SIZE, HEAP_MAX_SIZE - ul); } else { /* Try to take the chance that an allocation of only HEAP_MAX_SIZE is already aligned. */ // 两倍内存申请不到,这里只好申请 HEAP_MAX_SIZE 内存 碰碰运气了 p2 = (char *) MMAP (0, HEAP_MAX_SIZE, PROT_NONE, MAP_NORESERVE); if (p2 == MAP_FAILED) return 0; // 分配失败 // 运气不好 返回的地址 并不能与HEAP_MAX_SIZE对齐 这里只好返回申请的内存 并结束 if ((unsigned long) p2 & (HEAP_MAX_SIZE - 1)) { __munmap (p2, HEAP_MAX_SIZE); return 0; } } } // 设置读写权限 if (__mprotect (p2, size, PROT_READ | PROT_WRITE) != 0) { __munmap (p2, HEAP_MAX_SIZE); return 0; } // 设置一些heap参数 返回这个申请的heap h = (heap_info *) p2; h->size = size; h->mprotect_size = size; LIBC_PROBE (memory_heap_new, 2, h, h->size); return h; }
先看参数处理部分,相关的宏定义如下:
1 2 3 #define HEAP_MIN_SIZE (32 * 1024) // 32K #define HEAP_MAX_SIZE (2 * DEFAULT_MMAP_THRESHOLD_MAX) // x64 上是64M #define DEFAULT_MMAP_THRESHOLD_MAX (4 * 1024 * 1024 * sizeof(long)) // 4 * [4|8] M
保持size的大小在 HEAP_MIN_SIZE 与 HEAP_MAX_SIZE 之间
然后是 size_t pagesize = GLRO (dl_pagesize);
展开宏
1 2 3 4 5 6 7 8 #define GLRO(name) _##name // _dl_pagesize /* Cached value of `getpagesize ()'. */ EXTERN size_t _dl_pagesize; size_t _dl_pagesize = EXEC_PAGESIZE; #define EXEC_PAGESIZE 4096 // 最终的形式是 size_t pagesize = EXEC_PAGESIZE;// 4096 byte 也就是 4k
然后是补齐的算法
1 2 3 // 补齐算法 size + size&(base-1) #define ALIGN_UP(base, size) ALIGN_DOWN ((base) + (size) - 1, (size)) #define ALIGN_DOWN(base, size) ((base) & -((__typeof__ (base)) (size))) // base & -size
这段主要是通过 mmap 申请内存 使用到的一个重要的方法是 mmap 函数原型 void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset)
和 int munmap(void* start, size_t length)
,mmap 分配内存 munmap 释放内存,这些操作都是要切到内核态执行的, |参数|例子|意义| |–|–|–| |void* start|NULL|内存分配开始的位置 写NULL 这个位置由内核决定| |size_t length|[number]|分配内存的长度| |int prot|PROT_EXEC:执行 PROT_READ:读 PROT_WRITE:写 PROT_NONE:不可访问|权限| |int flags||文件类型 需要与 open 的时候使用的配对儿| |int fd|-1|文件句柄 -1 是与匿名搭配使用| |off_t offset|0|写入文件的偏移量| 这里代码逻辑的设计也是为了减少内核函数的调用,先看其如何通过一系列宏调用系统函数的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #define MMAP_CALL(__nr, __addr, __len, __prot, __flags, __fd, __offset) INLINE_SYSCALL_CALL (__nr, __addr, __len, __prot, __flags, __fd, __offset) #define INLINE_SYSCALL_CALL(...) __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__) #define __INLINE_SYSCALL_DISP(b,...) __SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__) #define __INLINE_SYSCALL_NARGS(...) __INLINE_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,) #define __INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n // 以上宏展开情况 __SYSCALL_CONCAT ( __INLINE_SYSCALL, 1 )(__VA_ARGS__) #define __SYSCALL_CONCAT(a,b) __SYSCALL_CONCAT_X (a, b) #define __SYSCALL_CONCAT_X(a,b) a##b // 得到 __INLINE_SYSCALL1(__VA_ARGS__) #define __INLINE_SYSCALL1(name, a1) INLINE_SYSCALL (name, 1, a1) #define INLINE_SYSCALL(name, nr, args...) __syscall_##name (args) // 最终调用的内核方法是 __syscall_mmap(__addr, __len, __prot, __flags, __fd, __offset)
其中有一些宏在做 ##
进行字符拼接的时候 做了两层嵌套,其原因是 在 ##
连接的宏定义中 里面的字符不会在进行宏展开了,做一层嵌套的目的是 在前一层先做宏展开 然后在发给下一个宏做 ##
以此实现宏展开的需要。 最终调用的方法是 __syscall_mmap
然后是mmap分配内存的逻辑,先抽离出主要逻辑功能 主要分三个部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static char *aligned_heap_area; p2 = MAP_FAILED; if (aligned_heap_area) { p2 = (char *) MMAP (aligned_heap_area, HEAP_MAX_SIZE, PROT_NONE, MAP_NORESERVE); //....#1... } if (p2 == MAP_FAILED) { p1 = (char *) MMAP (0, HEAP_MAX_SIZE << 1, PROT_NONE, MAP_NORESERVE); if (p1 != MAP_FAILED) { //...#2... } else { p2 = (char *) MMAP (0, HEAP_MAX_SIZE, PROT_NONE, MAP_NORESERVE); // ...#3.... } }
如果存在 aligned_heap_area 则按照 aligned_heap_area 作为内存开始地址进行扩展(#1 处代码),否则按照两倍的 HEAP_MAX_SIZE 进行内存的申请(#2 处代码),申请失败的话 再次尝试申请 HEAP_MAX_SIZE 的内存(#3 处代码)。由于mmap 返回的是申请成功的内存的起始位置,这里分配内存 有一个需求是:如果将内存地址(虚拟地址) 按照 HEAP_MAX_SIZE 的长度进行分段 ,new_heap 分配的内存正好在这个段上 起始地址和结束地址都是 HEAP_MAX_SIZE 对齐的 且 长度是 HEAP_MAX_SIZE。 当内存申请成功后,这个时候是有申请内存的结尾地址的,将这个地址赋值给 aligned_heap_area 方便下次分配内存的时候 可以直接确定分配内存开始位置 见代码 #1 处,但是在这个过程中内存很有可能被其他线程或进程申请到,这个变量就没有用了 也就是 #1 处的申请内存的代码会失败, 这个场景可能比较有限 所以代码 #1 处使用完后立即就将这个变量赋值为NULL了,这个场景可能比较适用连续的大段内存分配吧。
重点是 #2 处的代码,他会尝试分配 两倍 HEAP_MAX_SIZE 的内存,可能会奇怪为什么分配两段。ptmalloc 在通过new_heap 申请内存的时候 他想要申请一段长度为 HEAD_MAX_SIZE 长度的内存,且这段内存的起始内存地址与结束地址是与 HEAD_MAX_SIZE 对齐的,也就是说 分配到的内存地址起始是 n * HEAD_MAX_SIZE
结束地址是 (n+1) * HEAD_MAX_SIZE
。 对于 mmap 函数 第一个参数是起始地址,一般会给 NULL 让内核决定起始地址,这样内核返回的地址很大情况不会是 HEAD_MAX_SIZE 的倍数,这个时候需要计算返回的内存地址的对齐地址,当然是需要向上对齐,因为前面的内存已经被其他线程占用了。如果我们申请的长度是 HEAD_MAX_SIZE ,如果返回的起始地址不对齐 就要再次申请一段内存补充整个内存长度为 HEAD_MAX_SIZE 但是当再次申请的时候有可能会被其他线程前先一步 这个时候补充的内存段与之前申请的内存段并不连续了,这样就很糟糕。所以ptmallloc在这里一次申请 2 * HEAD_MAX_SIZE
的内存 然后通过__munmap
函数将多余的内存返还给系统,通过这个思路来看代码就很好理解了,可以参考下图: 其中 p1 是mmap返回的地址 ,p1 以 HEAD_MAX_SIZE 向上对齐得到 p2 ,p2是我们想要的内存起始地址,同样需要将 p1 开始 长度 p2 - p1 的内存返还给系统,p2 - p1 也就是 ul。 同时还有尾部多余的内存需要返还给系统,我们最终需要的内存的结束内存地址是 p2 + HEAP_MAX_SIZE
这也是需要返还给系统的内存起始地址,然后是长度。申请两倍内存的结束地址是 p1 + 2 * HEAP_MAX_SIZE
得到尾部需要返还的内存长度是 (p1 + 2 * HEAP_MAX_SIZE) - (p2 + HEAP_MAX_SIZE)
,结合 p2 - p1 = ul
最终得到 HEAP_MAX_SIZE - ul
这也就是代码中的尾部返还系统内存的长度。
1 2 3 4 5 6 7 8 9 10 11 12 p1 = (char *) MMAP (0, HEAP_MAX_SIZE << 1, PROT_NONE, MAP_NORESERVE); if (p1 != MAP_FAILED) { // 成功分配两倍HEAP_MAX_SIZE长度的内存 p1 是内存起始地址 // p2 是 p1 以HEAP_MAX_SIZE向上对齐的地址 p2 = (char *) (((unsigned long) p1 + (HEAP_MAX_SIZE - 1)) & ~(HEAP_MAX_SIZE - 1)); ul = p2 - p1; // p1 需要向上补齐的长度 // 将头部多余的内存返还给系统 if (ul) __munmap (p1, ul); // 返回的内存地址正好是对齐的 这时给变量aligned_heap_area赋值 给下一个连续的内存申请用 可以直接确定内存起始位置 else aligned_heap_area = p2 + HEAP_MAX_SIZE; // 将尾部多余的内存返还给系统 __munmap (p2 + HEAP_MAX_SIZE, HEAP_MAX_SIZE - ul); }
然后是 #3 处的代码,这里的代码好理解,当上一步分配两倍HEAP_MAX_SIZE长度内存的时候失败的话,这里尝试申请HEAP_MAX_SIZE长度内存,看看返回的地址是否为 以HEAP_MAX_SIZE对齐的起始内存地址,这里纯粹是在碰运气的。
1 2 3 4 5 6 7 8 // 申请 HEAP_MAX_SIZE 长度内存 p2 = (char *) MMAP (0, HEAP_MAX_SIZE, PROT_NONE, MAP_NORESERVE); if (p2 == MAP_FAILED) return 0; // 分配失败 返回 // 分配的内存起始地址 p2 并不是以HEAP_MAX_SIZE对齐的 则返还内存 直接返回 if ((unsigned long) p2 & (HEAP_MAX_SIZE - 1)) { __munmap (p2, HEAP_MAX_SIZE); return 0; }
4. 内存的申请过程总结