Post

[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를 관리하기 위한 구조체의 크기가 전달됩니다.

💡 즉, tcachemalloc 함수를 통해 주소를 받아와서 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 함수가 호출됩니다. 해당 함수에서 변경점을 간단하게 설명드리겠습니다.

  1. 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에서 가져오던 chunktcache_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]);
}
  1. 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 함수를 통해 할당하려는 chunktcache에 넣게 됩니다.

  1. 앞서 할당하지 못한 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
  1. 앞서 할당하지 못한 chunk 할당 - unsorted_bin

만약, unsorted_bin에서 할당하려는 크기와 같은 크기의 chunk를 찾게 됐을 때 실행되는 로직에서 다음과 같은 부분이 추가되었습니다. 위와 마찬가지로 tcache_put 함수를 통해 tcachechunk가 추가되고 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에 별다른 보호기법이 걸려있지 않은 것을 확인할 수 있습니다.

This post is licensed under CC BY 4.0 by the author.