[glibc 2.26] ptmalloc2 분석(2)
개요
이전 포스터에 이어서 tcache가 도입된 glibc 2.26
버전에 대해서 분석하고자 합니다. 이전 포스터를 읽고 보시는 것을 추천드립니다!
tcache
해당 버전에서 tcache
개념이 도입되었습니다. __libc_malloc
함수를 보면 다음과 같은 로직이 추가되었습니다.
1
2
3
4
5
6
7
8
9
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
#define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
#define MAYBE_INIT_TCACHE() \
if (__glibc_unlikely (tcache == NULL)) \
tcache_init();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes = request2size (bytes);
size_t tc_idx = csize2tidx (tbytes);
MAYBE_INIT_TCACHE ();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
tcache 초기화
우선 아래의 부분에서 각 변수들이 초기화되게 됩니다. 해당 부분은 _int_malloc
에서 봤던 로직과 유사하기 때문에 생략하겠습니다.
1
2
size_t tbytes = request2size (bytes);
size_t tc_idx = csize2tidx (tbytes);
다음으로 tcache를 초기화하는 매크로 함수가 호출됩니다. 해당 함수는 tcache
변수가 NULL
이면 tcache_init
함수를 실행하게 되고 해당 함수는 아래와 같습니다. _int_malloc
함수를 통해 주소를 받아온 뒤 tcache
변수를 초기화하고 있습니다.
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
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));
}
}
위의 로직에서 _int_malloc
함수에 전달되는 크기는 tcache
를 관리하기 위한 구조체의 크기가 전달됩니다.
💡 즉,
tcache
는malloc
함수를 통해 주소를 받아와서heap
영역에 있는tcache_perthread_struct
구조체에 의해서 관리되는 것을 알 수 있습니다.
_int_malloc
함수에도 많은 로직이 추가되었지만 tcache
가 초기화 된 이후에 작동하기 때문에 나중에 설명드리겠습니다.
tcache 할당
tcache
가 초기화 되었다면 아래의 조건문을 만족하는 경우에 tcache_get
함수가 호출됩니다.
1
2
3
4
5
6
7
8
9
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
여기서 조건문을 보면 tc_idx
변수의 값이 tcache
의 최대 bin
개수보다 작고 tcache→entries[tc_idx]
의 값이 NULL
이 아닐경우 tcache_get
함수가 호출되게 됩니다. 해당 함수는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}
위의 로직과 같이 주소와 bin
의 최대 개수의 검증만 하고 바로 주소를 반환하는 것을 확인할 수 있습니다.
💡 이렇게 로직을 추가함으로써 malloc 함수의 효율성을 향상시킬 수 있습니다.
_int_malloc 함수 변경점
만약 tcache
할당 단계에서 if문을 만족못하는 경우 기존과 같이 _int_malloc
함수가 호출됩니다. 해당 함수에서 변경점을 간단하게 설명드리겠습니다.
fastbin
할당에서 변경점
첫번째로 fastbin
에서 할당할 때 아래와 같은 로직이 추가되었습니다.
1
2
3
4
5
6
7
8
9
#define REMOVE_FB(fb, victim, pp) \
do \
{ \
victim = pp; \
if (victim == NULL) \
break; \
} \
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) \
!= victim); \
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (pp = *fb) != NULL)
{
REMOVE_FB (fb, tc_victim, pp);
if (tc_victim != 0)
{
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
로직을 살펴보면 tcache
초기화가 진행되었고 tcache bin
의 여유가 있으면 조건문을 만족하여 실행되게 됩니다. 이후, tcache의 여유가 있기 때문에 fastbin
에서 가져오던 chunk
를 tcache_put
함수를 통해 tcache
에 넣게 됩니다. 해당 함수는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
smallbin
할당에서 변경점
fastbin
과 마찬가지로 tcache
가 초기화되었고 tcache bin
의 여유가 있으면 조건문을 만족하여 다음의 로직을 실행합니다.
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
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
해당 로직도 tcache_put
함수를 통해 할당하려는 chunk
를 tcache
에 넣게 됩니다.
- 앞서 할당하지 못한
chunk
할당 직전
_int_malloc
함수에서 fastbin
, smallbin
에서 할당하지 못했을 경우 나중에 사용되기 위한 변수들이 초기화 됩니다.
1
2
3
4
5
6
7
8
9
#if USE_TCACHE
INTERNAL_SIZE_T tcache_nb = 0;
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
tcache_nb = nb;
int return_cached = 0;
tcache_unsorted_count = 0;
#endif
- 앞서 할당하지 못한
chunk
할당 -unsorted_bin
만약, unsorted_bin
에서 할당하려는 크기와 같은 크기의 chunk
를 찾게 됐을 때 실행되는 로직에서 다음과 같은 부분이 추가되었습니다. 위와 마찬가지로 tcache_put
함수를 통해 tcache
에 chunk
가 추가되고 continue
되는 것을 확인할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#if USE_TCACHE
/* Fill cache first, return to user only if cache fills.
We may return one of these chunks later. */
if (tcache_nb
&& tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (victim, tc_idx);
return_cached = 1;
continue;
}
else
{
#endif
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
#if USE_TCACHE
}
#endif
또한, unsorted_bin
에서 할당되지 않았을 경우 계속해서 반복문이 돌게되는데 해당 부분에도 다음과 같이 추가되었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#if USE_TCACHE
/* If we've processed as many chunks as we're allowed while
filling the cache, return one of the cached ones. */
++tcache_unsorted_count;
if (return_cached
&& mp_.tcache_unsorted_limit > 0
&& tcache_unsorted_count > mp_.tcache_unsorted_limit)
{
return tcache_get (tc_idx);
}
#endif
#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;
}
#if USE_TCACHE
/* If all the small chunks we found ended up cached, return one now. */
if (return_cached)
{
return tcache_get (tc_idx);
}
#endif
해당 로직은 이전에 tcache_put
함수를 통해 tcache
에 추가되었을때 return_cached
변수의 값이 1로 변하게 되는데 이 경우 작동하게 되어 tcache_get
함수를 통해 주소가 반환되게 됩니다.
_int_free
우선, tcache
와 관련하여 __libc_free
함수의 변경사항이 없기 때문에 바로 _int_free
함수의 수정사항에 대해서 코드의 순서대로 분석하겠습니다. 아래의 로직만 추가되었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache
&& tc_idx < mp_.tcache_bins
&& tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
#endif
기존의 free
과정에서 fastbin
인지 확인하기 전에 먼저 tcache
가 초기화되었고 tcache
bin에 여유가 있다면 tcache_put
함수를 통해 tcache
를 추가하고 바로 종료합니다.
결론
tcache
는 메모리 관리를 효율적으로 하기 위해 나타난 개념으로 처음 개념이 도입되었기 때문에 tcache
에 별다른 보호기법이 걸려있지 않은 것을 확인할 수 있습니다.