5.5.3 ptmalloc_lock_all(),ptmalloc_unlock_all(),ptmalloc_unlock_all2()
/* Magic value for the thread-specific arena pointer when malloc_atfork() is in use. */ #define ATFORK_ARENA_PTR ((Void_t*)-1) /* The following hooks are used while the `atfork' handling mechanism is active. */ static Void_t* malloc_atfork(size_t sz, const Void_t *caller) { Void_t *vptr = NULL; Void_t *victim; tsd_getspecific(arena_key, vptr); if(vptr == ATFORK_ARENA_PTR) { /* We are the only thread that may allocate at all. */ if(save_malloc_hook != malloc_check) { return _int_malloc(&main_arena, sz); } else { if(top_check()<0) return 0; victim = _int_malloc(&main_arena, sz+1); return mem2mem_check(victim, sz); } } else { /* Suspend the thread until the `atfork' handlers have completed. By that time, the hooks will have been reset as well, so that mALLOc() can be used again. */ (void)mutex_lock(&list_lock); (void)mutex_unlock(&list_lock); return public_mALLOc(sz); } }
当父进程中的某个线程使用 fork 的机制创建子线程时,如果进程中的线程需要分配内存,将使用 malloc_atfork() 函数分配内存。 malloc_atfork() 函数首先查看自己的线程私有实例中的分配区指针,如果该指针为 ATFORK_ARENA_PTR ,意味着本线程正在 fork 新线程,并锁住了全局锁 list_lock 和每个分配区,当前只有本线程可以分配内存,如果在 fork 线程前的分配函数不是处于 check 模式,直接调用内部分配函数 _int_malloc() 。否则在分配内存的同时做检查。如果线程私有实例中的指针不是 ATFORK_ARENA_PTR ,意味着当前线程只是常规线程,有另外的线程在 fork 子线程,当前线程只能等待 fork 子线程的线程完成分配,于是等待获得全局锁 list_lock ,如果获得全局锁成功,表示 fork 子线程的线程已经完成 fork 操作,当前线程可以分配内存了,于是是释放全局所 list_lock ,并调用 public_mALLOc() 分配内存。
static void free_atfork(Void_t* mem, const Void_t *caller) { Void_t *vptr = NULL; mstate ar_ptr; mchunkptr p; /* chunk corresponding to mem */ if (mem == 0) /* free(0) has no effect */ return; p = mem2chunk(mem); /* do not bother to replicate free_check here */ #if HAVE_MMAP if (chunk_is_mmapped(p)) /* release mmapped memory. */ { munmap_chunk(p); return; } #endif #ifdef ATOMIC_FASTBINS ar_ptr = arena_for_chunk(p); tsd_getspecific(arena_key, vptr); _int_free(ar_ptr, p, vptr == ATFORK_ARENA_PTR); #else ar_ptr = arena_for_chunk(p); tsd_getspecific(arena_key, vptr); if(vptr != ATFORK_ARENA_PTR) (void)mutex_lock(&ar_ptr->mutex); _int_free(ar_ptr, p); if(vptr != ATFORK_ARENA_PTR) (void)mutex_unlock(&ar_ptr->mutex); #endif }
当父进程中的某个线程使用 fork 的机制创建子线程时,如果进程中的线程需要释放内存,将使用 free_atfork() 函数释放内存。 free_atfork() 函数首先通过需 free 的内存块指针获得 chunk 的指针,如果该 chunk 是通过 mmap 分配的,调用 munmap() 释放该 chunk ,否则调用 _int_free() 函数释放内存。在调用 _int_free() 函数前,先根据 chunk 指针获得分配区指针,并读取本线程私用实例的指针,如果开启了 ATOMIC_FASTBINS 优化,这个优化使用了 lock-free 的技术优化 fastbins 中单向链表操作。如果没有开启了 ATOMIC_FASTBINS 优化,并且当前 线程没 有正在 fork 新子线程,则 对分配区加锁,然后调用 _int_free() 函数,然后对分配区解锁。而对于正在 fork 子线程的线程来说,是不需要对分配区加锁的,因为该线程已经对所有的分配区加锁了。
/* Counter for number of times the list is locked by the same thread. */ static unsigned int atfork_recursive_cntr; /* The following two functions are registered via thread_atfork() to make sure that the mutexes remain in a consistent state in the fork()ed version of a thread. Also adapt the malloc and free hooks temporarily, because the `atfork' handler mechanism may use malloc/free internally (e.g. in LinuxThreads). */ static void ptmalloc_lock_all (void) { mstate ar_ptr; if(__malloc_initialized < 1) return; if (mutex_trylock(&list_lock)) { Void_t *my_arena; tsd_getspecific(arena_key, my_arena); if (my_arena == ATFORK_ARENA_PTR) /* This is the same thread which already locks the global list. Just bump the counter. */ goto out; /* This thread has to wait its turn. */ (void)mutex_lock(&list_lock); } for(ar_ptr = &main_arena;;) { (void)mutex_lock(&ar_ptr->mutex); ar_ptr = ar_ptr->next; if(ar_ptr == &main_arena) break; } save_malloc_hook = __malloc_hook; save_free_hook = __free_hook; __malloc_hook = malloc_atfork; __free_hook = free_atfork; /* Only the current thread may perform malloc/free calls now. */ tsd_getspecific(arena_key, save_arena); tsd_setspecific(arena_key, ATFORK_ARENA_PTR); out: ++atfork_recursive_cntr; }
当父进程中的某个线程使用 fork 的机制创建子线程时,首先调用 ptmalloc_lock_all() 函数暂时对全局锁 list_lock 和所有的分配区加锁,从而保证分配区状态的一致性。 ptmalloc_lock_all() 函数首先检查 ptmalloc 是否已经初始化,如果没有初始化,退出,如果已经初始化,尝试对全局锁 list_lock 加锁,直到获得全局锁 list_lock ,接着对所有的分配区加锁,接着保存原有的分配释放函数,将 malloc_atfork() 和 free_atfork() 函数作为 fork 子线程期间所使用的内存分配释放函数,然后保存当前线程的私有实例中的原有分配区指针,将 ATFORK_ARENA_PTR 存放到当前线程的私有实例中,用于标识当前现在正在 fork 子线程。为了保证父线程 fork 多个子线程工作正常,也就是说当前线程需要 fork 多个子线程,当一个子线程已经创建,当前线程继续创建其它子线程时,发现当前线程已经对 list_lock 和所有分配区加锁,于是对全局变量 atfork_recursive_cntr 加 1 ,表示递归 fork 子线程的层数,保证父线程在 fork 子线程过程中,调用 ptmalloc_unlock_all() 函数加锁的次数与调用 ptmalloc_lock_all() 函数解锁的次数保持一致,同时也保证保证所有的子线程调用 ptmalloc_unlock_all() 函数加锁的次数与父线程调用 ptmalloc_lock_all() 函数解锁的次数保持一致,防止没有释放锁。
static void ptmalloc_unlock_all (void) { mstate ar_ptr; if(__malloc_initialized < 1) return; if (--atfork_recursive_cntr != 0) return; tsd_setspecific(arena_key, save_arena); __malloc_hook = save_malloc_hook; __free_hook = save_free_hook; for(ar_ptr = &main_arena;;) { (void)mutex_unlock(&ar_ptr->mutex); ar_ptr = ar_ptr->next; if(ar_ptr == &main_arena) break; } (void)mutex_unlock(&list_lock); }
当进程的某个线程完成 fork 子线程后,父线程和子线程都调用 ptmall_unlock_all() 函数释放全局锁 list_lock ,释放所有分配区的锁。 ptmall_unlock_all() 函数首先检查 ptmalloc 是否初始化,只有初始化后才能调用该函数,接着将全局变量 atfork_recursive_cntr 减 1 ,如果 atfork_recursive_cntr 为 0 ,才继续执行,这保证了递归 fork 子线程只会解锁一次。接着将当前线程的私有实例还原为原来的分配区, __malloc_hook 和 __free_hook 还原为由来的 hook 函数。然后遍历所有分配区,依次解锁每个分配区,最后解锁 list_lock 。
#ifdef __linux__ /* In NPTL, unlocking a mutex in the child process after a fork() is currently unsafe, whereas re-initializing it is safe and does not leak resources. Therefore, a special atfork handler is installed for the child. */ static void ptmalloc_unlock_all2 (void) { mstate ar_ptr; if(__malloc_initialized < 1) return; #if defined _LIBC || defined MALLOC_HOOKS tsd_setspecific(arena_key, save_arena); __malloc_hook = save_malloc_hook; __free_hook = save_free_hook; #endif #ifdef PER_THREAD free_list = NULL; #endif for(ar_ptr = &main_arena;;) { mutex_init(&ar_ptr->mutex); #ifdef PER_THREAD if (ar_ptr != save_arena) { ar_ptr->next_free = free_list; free_list = ar_ptr; } #endif ar_ptr = ar_ptr->next; if(ar_ptr == &main_arena) break; } mutex_init(&list_lock); atfork_recursive_cntr = 0; } #else #define ptmalloc_unlock_all2 ptmalloc_unlock_all #endif
函数ptmalloc_unlock_all2() 被 fork 出的子线程调用,在 Linux 系统中,子线程(进程) unlock 从父线程(进程)中继承的 mutex 不安全,会导致资源泄漏,但重新初始化 mutex 是安全的,所有增加了这个特殊版本用于 Linux 下的 atfork handler 。 ptmalloc_unlock_all2() 函数的处理流程跟 ptmalloc_unlock_all() 函数差不多,使用 mutex_init() 代替了 mutex_unlock() ,如果开启了 PER_THREAD 的优化,将从父线程中继承来的分配区加入到 free_list 中,对于子线程来说,无论全局变量 atfork_recursive_cntr 的值是多少,都将该值设置为 0 ,因为 ptmalloc_unlock_all2() 函数只会被子线程调用一次。