IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
메인 컨텐츠로 가기

한국 developerWorks  >  리눅스  >

리눅스 슬랩 할당자 분석

리눅스 메모리 관리 기법 배우기

developerWorks
문서 옵션

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.

영어원문

영어원문


제안 및 의견
피드백

난이도 : 중급

M. Tim Jones, 컨설턴트 엔지니어, Emulex Corp.

옮긴이: 박재호 이해영 dwkorea@kr.ibm.com

2008 년 4 월 15 일

좋은 운영체제 성능은 어느 정도 효율적인 메모리 자원 관리 능력에 달려있습니다. 과거에는 힙 메모리 관리가 일반적이었지만, 단편화와 메모리 수집으로 인해 성능 저하가 일어났습니다. 오늘날 리눅스(Linux ®) 커널은 솔라리스에서 시작해 임베디드 시스템에도 오래동안 적용된, 메모리를 크기에 기반을 둔 객체로 취급하는 방법을 사용합니다. 이 기사는 슬랩 할당자 이면에 숨겨진 사상을 살펴보고 인터페이스와 활용 방안을 검토합니다.

동적 메모리 관리

메모리 관리 목표는 다양한 목적을 위해 여러 사람이 동적으로 메모리를 공유하도록 만드는 방법을 제공하는 데 있다. 메모리 관리 방법은 다음과 같은 양쪽 측면을 모두 고려해야 한다.

  • 메모리 관리에 필요한 시간을 최소로 줄인다.
  • 일반적인 사용에 적합한 메모리 양을 최대로 늘인다(메모리 관리 부하를 최소로 줄인다).

메모리 관리는 궁극적으로는 의자뺏기 경기다. 관리에 메모리를 덜 쓰는 알고리즘을 개발하면 가용 메모리 관리에 시간을 더 많이 쓰게 된다. 메모리를 효율적으로 관리하는 알고리즘을 개발할 수도 있지만 이럴 경우 더 많은 메모리를 사용한다. 결국 특정 응용 프로그램을 위한 요구 사항은 시간과 공간 사이에서 절충점을 찾아내도록 만든다.

초기 메모리 관리자는 힙 기반 할당 전략을 사용했다. 이 방법을 사용하면 (힙으로 부르는) 큰 메모리 블록을 사용해 사용자가 정의한 목적에 맞춰 메모리를 제공한다. 사용자가 메모리 블록을 요구할 때 크기를 지정한다. 힙 매니저는 (특정 알고리즘을 사용해) 가용 메모리를 살펴 블록을 반환한다. 이런 탐색에 사용되는 몇몇 알고리즘으로 (힙 내부에서 요청을 만족하는 처음으로 찾아낸 블록을 반환하는) 최초 적합과 (힙 내부에서 요청을 가장 잘 만족하는 블록을 반환하는) 최적 적합이 있다. 사용자가 메모리 사용을 끝내면 다시 힙으로 반환한다.

이런 힙 기반 할당 전략에서 발생하는 근본적인 문제는 단편화다. 메모리 블록을 할당할 때 시시각각 다른 순서로 반환된다. 이렇게 되면 힙에 구멍이 남으며, 자유 메모리를 효율적으로 관리하기 위한 시간이 늘어난다. 이런 알고리즘은 (필요할 때 할당하는 방식으로) 메모리 효율성을 추구하지만 힙 관리에 더 많은 시간을 요구한다.

또 다른 접근 방법으로 버디 메모리 할당으로 부르는 좀더 빠른 메모리 기법이 있는데, 메모리를 2의 배수로 나눠놓은 다음에 메모리 최적 적합 기법을 사용해 메모리 요청을 처리하려고 시도한다. 사용자가 메모리를 반환할 때 버디 블록을 점검해 인접한 영역 중에서 반환된 메모리가 없는지 살펴본다. 만일 인접 영역에 반환된 메모리가 존재하면 단편화를 최소로 줄이기 위해 블록을 결합한다. 이런 알고리즘은 시간 측면에서 좀더 효율적이지만 최적 적합 기법을 사용하므로 메모리 낭비가 심하다.

이 기사는 리눅스 커널 메모리 관리와 특히 슬랩 할당자에서 제공하는 메커니즘에 초점을 맞춘다.

슬랩 캐시

리눅스에서 사용하는 슬랩 할당자는 썬OS 운영체제를 위해 Jeff Bonwick이 처음으로 만든 알고리즘에 기반한다. Jeff가 만든 할당자는 객체 캐시를 중심으로 동작한다. 커널 내부에서 파일 기술자나 기타 공통 구조체를 위한 객체 집합을 위해 메모리를 미리 많이 할당해 놓는다. Jeff는 커널에서 정규 객체를 초기화하는 데 들어가는 시간이 할당과 해제에 필요한 시간을 초과한다는 사실을 발견했다. Jeff는 메모리를 전역 풀로 되돌려서 해제하는 대신 의도한 목적에 맞춰 초기화한 상태로 메모리를 유지해야 한다고 결론지었다. 예를 들어, 뮤텍스용 메모리를 할당했다면 뮤텍스 초기화 함수( mutex_init )는 뮤텍스를 위해 메모리를 처음 할당할 때 한번만 수행하면 된다. 연속적인 메모리 할당이 이런 초기화 작업을 수행하지 않아도 되는 이유는 직전에 호출된 할당 해제와 제거 함수를 통해 이미 초기화 상태로 바뀌었기 때문이다.

시간과 공간 모두를 효율적으로 만드는 메모리 할당자를 만들기 위해 리눅스 슬랩 할당자는 이런저런 아이디어를 활용했다.

그림 1은 슬랩 구조체를 상위 단계에서 보여준다. 최상위 단계에 cache_chain 이 있는데 슬랩 캐시를 연결 리스트로 묶는다. 이는 최적 적합 알고리즘에 유용한데, (목록을 순회하면서) 요구하는 할당 크기에 가장 근접한 캐시를 찾아주기 때문이다. cache_chain 의 각 구성 요소는 (캐시라고 부르는) kmem_cache 구조체 참조다. 이는 관리를 위해 크기가 정해진 객체를 담은 풀을 정의한다.


그림 1. 슬랩 할당자를 구성하는 주요 구조
그림 1. 슬랩 할당자를 구성하는 주요 구조

각 캐시는 슬랩 목록을 포함하는데 (일반적으로 페이지에 들어가는) 연속적인 메모리 블록이다. 슬랩 종류는 다음과 같은 세 가지다.

slabs_full
완전히 할당된 슬랩
slabs_partial
일부 할당된 슬랩
slabs_empty
비어있거나 객체를 할당하지 않은 슬랩

slabs_empty 리스트에 있는 슬랩은 주요 수확 대상이다. 슬랩이 사용한 메모리는 다른 목적을 위해 운영체제에 반환되는 과정을 거친다.

슬랩 리스트에 있는 각 슬랩은 연속적인 메모리 블록이며(하나 이상 연속된 페이지) 객체로 쪼개진다. 이런 객체는 특별한 캐시에서 할당되고 할당이 해제되는 기본 구성 요소다. 슬랩은 슬랩 할당자가 할당하는 최소 단위이므로 더 많은 공간이 필요하다면 슬랩 단위로 확장되어야 한다. 일반적으로 다양한 객체가 슬랩 단위로 할당된다.

객체가 슬랩에서 할당되고 할당이 해제될 때 개별 슬랩은 슬랩 리스트에서 이동할 수 있다. 예를 들어, 모든 객체가 슬랩 내부에서 사용되었을 경우 slabs_partial 리스트에서 slabs_full 리스트로 이동한다. 슬랩이 꽉차고 객체 할당이 해제될 때, slabs_full 리스트에서 slabs_partial 리스트로 이동한다. 모든 객체 할당이 해제될 때 slabs_partial 리스트에서 slabs_empty 리스트로 이동한다.

슬랩 이면에 숨겨진 동기

슬랩 캐시 할당자는 전통적인 메모리 관리 기법과 비교해 여러 가지 장점을 제공한다. 첫째로, 일반적으로 커널 운영 과정에서 시스템 생명 주기 동안에 여러 차례에 걸친 작은 객체 할당이 필요하다. 슬랩 캐시 할당자는 비슷한 크기의 객체를 캐시하는 방법을 통해 일반적으로 일어나는 단편화 문제를 회피한다. 슬랩 할당자는 또한 공통 객체 초기화를 지원하므로 동일한 목적으로 사용하는 객체를 반복적으로 초기화하는 상황을 방지한다. 마지막으로 슬랩 할당자는 하드웨어 캐시 정렬과 컬러링을 지원하므로, 동일한 캐시 라인를 차지하도록 다른 캐시에 객체를 배열하므로 시스템 활용도를 높이고 성능을 개선한다.




위로


API 함수

이제 새로운 슬랩 캐시를 생성하기 위한 API를 소개할 차례다. 캐시에 메모리를 추가하고, 캐시를 제거하고, 캐시에 객체를 할당하고, 할당을 해제하는 함수를 정리했다.

첫 단계는 슬랩 캐시 구조체 생성이며 다음과 같은 방법으로 정적으로 만들 수 있다.

struct struct kmem_cache *my_cachep;
			

슬랩 캐시를 위한 리눅스 원시 코드
슬랩 캐시 원시 코드는 ./linux/mm/slab.c에서 찾을 수 있다. kmem_cache 구조체는 ./linux/mm/slab.c에 정의되어 있다. 이 기사에서는 리눅스 커널 2.6.21에 구현된 내용에 초점을 맞춘다.

이 참조 값은 생성, 삭제, 할당과 같은 작업을 위해 다른 캐시 함수가 사용한다. kmem_cache 구조체는 슬랩을 관리하기 위해 CPU 단위로 필요한 자료, (proc 파일 시스템으로 접근 가능한) 조율값 집합, 통계, 기타 필요한 구성 요소를 포함한다.

kmem_cache_create

커널 함수인 kmem_cache_create 은 새로운 캐시를 생성한다. 커널 초기화 시점이나 커널 모듈이 처음 메모리에 올라올 때 수행된다. 서식은 다음과 같다.

struct kmem_cache *
kmem_cache_create( const char *name, size_t size, size_t align,
                       unsigned long flags;
                       void (*ctor)(void*, struct kmem_cache *, unsigned long),
                       void (*dtor)(void*, struct kmem_cache *, unsigned long));
			

name 매개변수는 캐시 이름을 정의하며, proc 파일 시스템(/proc/slabinfo)에서 캐시를 식별하기 위한 목적으로 사용한다. size 매개변수는 이 캐시를 위해 생성될 객체 크기를 지정하며, align 매개변수는 각 객체에 대한 정렬 값을 지정한다. flags 매개변수는 캐시를 활성화하는 옵션을 지정한다. 각 플래그를 표 1에 정리했다.


표 1. kmem_cache_create를 위한 옵션 일부 목록(플래그로 명시)
옵션설명
SLAB_RED_ZONE버퍼 넘침을 점검하기 위해 객체 헤더와 트레일러에 특수한 표식을 집어 넣는다.
SLAB_POISON (캐시가 소유한 객체를 외부에서 수정했는지 검사하기 위해) 캐시에서 객체를 감시하기 위해 알려진 패턴으로 슬랩을 채운다.
SLAB_HWCACHE_ALIGN 이 캐시에 있는 객체가 하드웨어 캐시 라인에 맞춰 정렬되어야 함을 지시한다.

ctordtor 매개변수는 부가적인 객체 생성자와 소멸자를 정의한다. 생성자와 소멸자는 사용자가 제공하는 콜백 함수다. 새로운 객체를 캐시에서 할당할 때 생성자를 통해 초기화할 수 있다.

캐시 생성 후 참조값은 kmem_cache_create 함수가 반환한다. 이 함수는 캐시에 메모리를 할당하지 않음에 주목하자. 그 대신 캐시에서 객체를 할당하려는 시도가 일어날 때(초기에는 비어있다), refill 연산을 통해 메모리를 할당한다. 모든 객체를 다 썼을 때나 캐시에 메모리를 추가할 때도 같은 연산을 이용한다.

kmem_cache_destroy

커널 함수인 kmem_cache_destroy 는 캐시를 소멸시킬 때 사용한다. 이 함수는 메모리에서 내려올 때 커널 모듈이 호출한다. 이 함수 호출 전에 캐시는 비어있어야 한다.

void kmem_cache_destroy( struct kmem_cache *cachep );
			

kmem_cache_alloc

이름을 붙인 캐시에서 객체를 할당하려면 kmem_cache_alloc 함수를 사용한다. 호출한 쪽에서 객체를 할당하기 위한 캐시와 플래그 집합을 제공해야 한다.

void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
			

이 함수는 캐시에서 객체를 반환한다. 캐시가 현재 비어있다면 이 함수는 메모리를 캐시에 추가하기 위해 cache_alloc_refill 을 호출할지도 모른다. kmem_cache_alloc 을 위한 flag 옵션은 kmalloc 과 동일하다. 표 2에 flag 옵션 일부 목록을 정리해놓았다.


표 2. kmem_cache_alloc과 kmalloc 커널 함수를 위한 플래그 옵션
플래그설명
GFP_USER커널 RAM을 위한 메모리 할당(잠들지도 모른다)
GFP_KERNEL커널 RAM을 위한 메모리 할당(잠들지도 모른다).
GFP_ATOMIC이 호출은 잠들기를 허용하지 않는다(인터럽트 처리기에 유용하다).
GFP_HIGHUSER상위 메모리에서 할당한다.

NUMA를 위한 슬랩 할당
NUMA(Non-Uniform Memory Access) 아키텍처에서 사용하는 특정 노드를 위한 할당 함수는 kmem_cache_alloc_node 다.

kmem_cache_zalloc

커널 함수인 kmem_cache_zallockmem_cache_alloc 과 비슷하다. 단 호출한 쪽으로 객체를 되돌리기 전에 정리하기 위해 객체에 memset 을 수행한다는 점이 다르다.

kmem_cache_free

객체를 슬랩으로 돌려놓기 위해 kmem_cache_free 를 사용한다. 호출한 쪽에서 캐시 참조와 해제할 객체를 제공한다.

void kmem_cache_free( struct kmem_cache *cachep, void *objp );
			

kmalloc and kfree

커널에서 가장 흔히 사용하는 메모리 관리 함수는 kmallockfree 함수다. 두 함수 서식은 다음과 같이 정의되어 있다.

void *kmalloc( size_t size, int flags );
void kfree( const void *objp );
			

kmalloc 에서 유일한 매개변수는 할당할 객체 요청 크기와 플래그 집합이다( 표 2 에서 부분 목록을 참조하자). 하지만 kmallockfree 는 직전에 정의한 함수와 마찬가지로 슬랩 캐시를 활용한다. 객체를 할당할 구체적인 슬랩 캐시에 이름을 붙이는 대신, kmalloc 함수는 크기 제약을 충족하는 항목을 찾기 위해 가용 캐시를 순회하며 살핀다. 발견하면, ( __kmem_cache_alloc 을 활용해) 객체를 할당한다. kfree 로 객체를 해제하려면 객체를 할당한 캐시는 virt_to_cache 를 호출해서 찾아내야 한다. 이 함수는 캐시 참조를 반환하는데, 객체를 해제하기 위해 __cache_free 함수를 호출할 때 사용하게 된다.

일반적인 객체 할당
슬랩 소스 내부적으로 kmem_find_general_cachep 라고 이름 붙인 함수를 제공해서 필요한 객체 크기에 가장 적합한 슬랩 캐시를 찾는 캐시 탐색을 수행하도록 만든다.

기타 함수

슬랩 캐시 API는 다른 유용한 몇몇 함수를 제공한다. kmem_cache_size 함수는 이 캐시가 다루는 객체 크기를 반환한다. 또한 (캐시 생성 시점에 정의된) 주어진 캐시 이름을 얻기 위해 kmem_cache_name 을 호출한다. 캐시는 캐시에서 자유 슬랩을 놓아주는 방법으로 크기가 줄어든다. 이렇게 하려면 kmem_cache_shrink 함수를 호출한다. (수확을 위한) 이런 행위는 ( kswapd 를 통해) 커널이 주기에 맞춰 자동으로 수행한다.

unsigned int kmem_cache_size( struct kmem_cache *cachep );
const char *kmem_cache_name( struct kmem_cache *cachep );
int kmem_cache_shrink( struct kmem_cache *cachep );
			




위로


슬랩 캐시 활용 예

다음 코드 조각은 새로운 슬랩 캐시를 생성하고 캐시에서 객체를 할당/할당 해제하고, 캐시를 제거하는 예를 보여준다. 가장 먼저 kmem_cache 객체를 정의해 초기화해야 한다(Listing 1 참조). 이렇게 만들어진 특별한 캐시는 32바이트 객체를 포함하며, 하드웨어 캐시에 정렬되어 있다(플래그 매개변수로 SLAB_HWCACHE_ALIGN 을 정의했다).


Listing 1. 새로운 슬랩 캐시 생성
                
static struct kmem_cache *my_cachep;

static void init_my_cache( void )
{

   my_cachep = kmem_cache_create( 
                  "my_cache",            /* Name */
                  32,                    /* Object Size */
                  0,                     /* Alignment */
                  SLAB_HWCACHE_ALIGN,    /* Flags */
                  NULL, NULL );          /* Constructor/Deconstructor */

   return;
}
			

할당된 슬랩 캐시를 사용해 객체를 할당할 수 있다. Listing 2는 이 캐시에서 객체를 할당하고 할당을 해제하는 예다. 또한 기타 함수 두 개를 사용하는 방법도 보여준다.


Listing 2. 객체 할당과 할당 해제
                
int slab_test( void )
{
  void *object;

  printk( "Cache name is %s\n", kmem_cache_name( my_cachep ) );
  printk( "Cache object size is %d\n", kmem_cache_size( my_cachep ) );

  object = kmem_cache_alloc( my_cachep, GFP_KERNEL );

  if (object) {

    kmem_cache_free( my_cachep, object );

  }

  return 0;
}
			

마지막으로 Listing 3은 슬랩 캐시를 제거하는 예다. 제거 연산을 수행하는 도중에 호출하는 쪽에서 캐시로부터 객체를 할당하려는 시도가 없음을 확인해야 한다.


Listing 3. 슬랩 캐시 제거
                
static void remove_my_cache( void )
{

  if (my_cachep) kmem_cache_destroy( my_cachep );

  return;
}
			




위로


슬랩에 대한 proc 인터페이스

proc 파일 시스템은 시스템에서 활성화된 모든 슬랩 캐시를 감시하는 단순한 방법을 제공한다. /proc/slabinfo라는 파일은 사용자 영역에서 접근이 가능한 몇몇 조율값을 추가로 제공하는 동시에 모든 슬랩 캐시에 대한 세부 정보를 출력한다. 현재 slabinfo 버전은 헤더를 제공해 사람이 좀더 읽기 편하도록 출력한다. 시스템에 있는 각 슬랩 캐시, 객체 숫자, 활성 객체 숫자, 슬랩 당 객체와 페이지 정보, 객체 크기를 제공한다. 조율값 집합과 슬랩 자료도 제공한다.

특정 슬랩 캐시를 조율하려면, 슬랩 캐시 이름과 문자열로 만들어진 세 가지 조율값 매개변수를 /proc/slabinfo 파일로 echo 하면 된다. 다음 예제는 shared_factor를 그대로 놓아둔 채 limit와 batchcount를 증가하는 방법을 보여준다(형식은 "캐시_이름 limit batchcount shared_factor"다).

# echo "my_cache 128 64 8" > /proc/slabinfo
            

limit 필드는 각 CPU에 캐시될 최대 객체 개수를 지정한다. batchcount 필드는 캐시가 빌 때 CPU 캐시 단위로 전송될 전역 캐시 객체 최대 개수를 지정한다. shared 매개변수는 SMP 시스템에서 공유 행동 양식을 지정한다.

슬랩 캐시를 위한 proc 파일 시스템에 접근하려면 관리자 권한이 있어야 함에 주의하자.




위로


SLOB 할당자

작은 임베디드 시스템을 위해 SLOB이라는 슬랩 흉내내기 층이 존재한다. 이 슬랩 대체품은 작은 임베디드 리눅스 시스템에서 유리하지만, 메모리를 512KB 정도 소모하는데다 단편화와 확장성 결여라는 문제가 있다. CONFIG_SLAB 을 비활성화하면, 커널은 SLOB 할당자로 돌아간다. 세부 사항은 참고자료 절을 참조하자.




위로


한걸음 더 나가면

슬랩 캐시 할당자를 위한 원시 코드는 리눅스 커널 중에서 가장 가독성이 높은 부문에 속한다. 함수 호출에 존재하는 외부 간접 참조는 일반적으로 아주 직관적이고 주석이 잘 달려 있다. 슬랩 캐시 할당자에 대해 더 많은 지식이 필요하다면 메커니즘을 설명한 최신 문서에서 시작하기 바란다. 참고자료 절에서 슬랩 캐시 할당자를 기술하는 몇몇 자료를 제공하지만, 불행하게도 대부분 현재 2.6 구현에는 뒤쳐진 상황이다.



참고자료

교육

제품 및 기술 얻기
  • DB2®, Lotus®, Rational®, Tivoli®, WebSphere® 같은 최신 IBM 평가판 소프트웨어를 포함하는 두 장짜리 DVD 세트인 SEK for Linux 를 주문하자.

  • IBM 평가판 소프트웨어 : developerWorks에서 직접 내려 받아 다음번 리눅스 프로젝트에 활용하자.


토론


필자소개

M. Tim Jones 사진

M. Tim Jones는 임베디드 소프트웨어 아키텍트이자 GNU/Linux Application Programming, AI Application Programming, BSD Sockets Programming from a Multilanguage Perspective의 저자이기도 한다. Jones의 공학 배경은 정지 위성을 위한 커널 개발에서 시작해 임베디드 시스템 아키텍처와 네트워크 프로토콜 개발에 이르기까지 다양한 분야를 아우른다. Jones는 콜로라도 주, 롱몬트 소재 Emulex 사에서 컨설턴트 엔지니어로 활약한다.




기사에 대한 평가


보다 나은 서비스를 제공하기 위함이오니 잠시 짬을 내어 이 양식을 제출하여 주십시오.



 


 


 


이 문서 북마킹 하기

mar.gar.in mar.gar.in naver naver eolin eolin del.icio.us del.icio.us





위로


DB2, Lotus, Rational, Tivoli, and WebSphere are trademarks of IBM Corporation in the United States, other countries, or both. Linux is a trademark of Linus Torvalds in the United States, other countries, or both. 기타 회사, 제품, 및 서비스명은 다른 상표나 서비스 마크일 수 있습니다.

developerWorks 콘텐트를 다른 사이트에 전재하기:
developerWorks 콘텐트에 대한 저작권은 IBM에 있습니다. IBM의 서면 허가나 원본 저자의 허락이 없이는 전재를 금합니다. 저희 콘텐트를 전재하시려면 IBM developerWorks 담당자 에게 문의하십시오.
    IBM 소개 개인정보 보호정책 문의