size_t형의 bytes(동적할당 크기)를 인자로 받아 void형 포인터 victim(할당된 힙 영역의 주소)를 반환한다.
부분을 나눠 자세하게 살펴보자.
mstate ar_ptr;
void *victim;
_Static_assert (PTRDIFF_MAX <= SIZE_MAX / 2,
"PTRDIFF_MAX is not more than half of SIZE_MAX");
if (!__malloc_initialized)
ptmalloc_init ();
우선 mstate형 ar_ptr변수와 void형 포인터 victim이 선언된다.
mstate는 malloc_state구조체에 대한 포인터형이다.
typedef struct malloc_state *mstate;
그리고 victime은 malloc이 종료된 다음 반환될 변수라는 점을 기억하자.
그리고 그 다음 _Static_assert함수는 컴파일 타임에 조건을 검사하는 함수이므로 일단 넘어가자,
다음으로는 malloc이 초기화되었는지를 나타내는 __malloc_initialized가 flase이면 ptmalloc_init ()를 실행해 초기화를 진행한다.
/* Already initialized? */
static bool __malloc_initialized = false;
//https://elixir.bootlin.com/glibc/latest/source/malloc/arena.c#L261
static void
ptmalloc_init (void)
{
if (__malloc_initialized)
return;
__malloc_initialized = true;
#if USE_TCACHE
tcache_key_initialize ();
#endif
.....(중략)
우선 malloc_initialized의 초기값은 flase이며 ptmalloc_init() 내부 루틴에 의해 이 함수가 한번이라도 실행되면 malloc_initialized를 true로 만들기 때문에 무조건 초기화(=ptmalloc_init의 실행)는 맨 처음 한 번 이루어질 것이다.
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes = checked_request2size (bytes);
if (tbytes == 0)
{
__set_errno (ENOMEM);
return NULL;
}
size_t tc_idx = csize2tidx (tbytes);
MAYBE_INIT_TCACHE ();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
&& tcache != NULL
&& tcache->counts[tc_idx] > 0)
{
victim = tcache_get (tc_idx);
return tag_new_usable (victim);
}
DIAG_POP_NEEDS_COMMENT;
#endif
#if USE_TCACHE~#endif에 해당하는 위 부분은 tcache가 도입되며 추가된 부분이라고 한다.
즉, tcache에 관한 부분이다.
우선 size_t형의 tbytes라는 변수에 checked_request2size (bytes)의 반환 값을 넣어준다.
checked_request2size는 입력 받은 bytes가 PTRDIFF_MAX
보다 크다면 바로 0을, MINSIZE
보다 작으면 MINSIZE
를, 그 외에는 제대로 된 size의 값을 반환한다. 따라서 만약 요청된 bytes가 정해진 값보다 크다면 아래의 if문이 실행되어 에러 넘버가 설정되고 널 값이 반환되면서 malloc이 종료될 것이다. 그 외의 경우에는 tbytes에 적당한 size값이 대입 된 채로 계속 진행될 것이다.
다음으로는 tc_idx라는 변수에 csize2tidx (tbytes)의 반환값(=tbytes에 해당하는 tcache인덱스 값)을 넣는다.
다음으로는 MAYBE_INIT_TCACHE ()를 수행하여 tcache에 대한 초기화를 수행한다 (tcache_perthread_struct 구조체를 동적할당, 구조체 포인터 변수 tcache에 해당 주소 저장)
이는 tcache의 값이 NULL일 때만 실행되므로 가장 첫 번째의 malloc수행에만 초기화가 진행될 것임.
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;
//tcache를 관리하는 구조체
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
static __thread tcache_perthread_struct *tcache = NULL;
<함수 내부 코드>//https://elixir.bootlin.com/glibc/latest/source/malloc/malloc.c#L3264
# define MAYBE_INIT_TCACHE() \
if (__glibc_unlikely (tcache == NULL)) \
tcache_init();
static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);
if (tcache_shutting_down)
return;
arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
if (!victim && ar_ptr != NULL)
{
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}
if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);
/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */
if (victim)
{
tcache = (tcache_perthread_struct *) victim;
memset (tcache, 0, sizeof (tcache_perthread_struct));
}
}
그리고 아래의 세 가지 조건을 만족하면 tcache_get함수를 실행시켜 해당 idx에 해당하는 tcache 청크를 재 할당한다.
TCACHE_MAX_BINS
의 값보다 작다면(tcache 내에서 유효한 인덱스라면)또한, 청크를 재할당하기 위한 매커니즘은 다음과 같다.
victim = tcache_get (tc_idx); ⇒ 해당 idx에 해당하는 tcache list의 청크 주소를 반환함 + tcache_perthread_struct의 해당 idx의 count를 1 줄이고 반환한(재할당할)청크의 key값을 널로 바꿈.
tcache_get (size_t tc_idx)
{
return tcache_get_n (tc_idx, & tcache->entries[tc_idx]);
}
tcache_get_n (size_t tc_idx, tcache_entry **ep)
{
tcache_entry *e;
if (ep == &(tcache->entries[tc_idx]))
e = *ep;
else
e = REVEAL_PTR (*ep);
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
if (ep == &(tcache->entries[tc_idx]))
*ep = REVEAL_PTR (e->next);
else
*ep = PROTECT_PTR (ep, REVEAL_PTR (e->next));
--(tcache->counts[tc_idx]);
e->key = 0;
return (void *) e;
}
tag_new_usable (void *ptr)
{
if (__glibc_unlikely (mtag_enabled) && ptr)
{
mchunkptr cp = mem2chunk(ptr);
ptr = __libc_mtag_tag_region (__libc_mtag_new_tag (ptr), memsize (cp));
}
return ptr;
}
만약 여기서 tcache의 재할당이 이루어진다면, __libc_malloc은 여기서 victim을 return하며 아예 종료되어버린다.
만약 이루어지지 않는다면 함수는 계속 진행된다.
if (SINGLE_THREAD_P)
{
victim = tag_new_usable (_int_malloc (&main_arena, bytes));
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
&main_arena == arena_for_chunk (mem2chunk (victim)));
return victim;
}
tcache의 재할당이 이루어지지 않았을 때 실행되는 부분들이다.
해당 if문 내부는 단일 스레드일 때 실행된다. (SINGLE_THREAD_P)
그리고 main_arena의 주소와 bytes를 인자로 _int_malloc을 호출하고 반환 값을 victim에 넣는다.(실질적인 동적 할당이 일어나는 부분)
다음으로 다음 조건들을 확인하고(정상적으로 할당이 되었는지 확인) victim을 반환하고, 함수를 종료한다.
check for mmap()'ed chunk
단일 스레드인 경우 여기서 __libc_malloc이 종료된다.
arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
/* Retry with another arena only if we were able to find a usable arena
before. */
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);
}
if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);
victim = tag_new_usable (victim);
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
ar_ptr == arena_for_chunk (mem2chunk (victim)));
return victim;
여기서부터는 단일 스레드 상황이 아닐 때 실행되는 부분일 것이다.
우선 알맞은 arena의 주소를 malloc_state포인터인 ar_ptr에 저장하고,
_int_malloc으로 ar_ptr기반의 동적할당을 해 victim에 할당된 주소를 저장한다.
다음으로 victim과 ar_ptr이 널이 아닐 때(즉, 제대로 된 동적할당이 이루어졌을 때)
usable arena가 존재하는 경우 해당 arena에 대해 malloc을 retry한다.
이후 assert로 동적할당이 제대로 이루어졌는지 확인하고
victim을 malloc의 반환값으로 반환하고 함수를 종료한다.
전반적으로 초기화 ⇒ tcache확인 ⇒ 싱글 스레드 할당 ⇒ 멀티스레드 할당 의 순서로 이루어진다.
만약 tcache에 재사용할 bin이 존재하지 않는 이상, 실질적으로 동적할당이 일어나는 부분은 _int_malloc인 듯 하다. 이 함수를 살펴볼 필요가 있다.