uClinux의 malloc이 다른 이유
RenameThisPage MallocInUClinux 등과 같은 이름으로 바꾸기를 제안합니다.
DifferentMallocUnderUCLinux를 제안합니다. 원제는 #title로 쓸 수 있을 겁니다. --kz
uClinux 의 malloc 이 다른 이유
먼저 uClinux 에서는 가상 메모리 (VM - Virtual Memory) 시스템을 사용하지 않는다. 이것은 당신이 이미 실행중인 프로세스에 대해서 임의로 메모리를 추가할 수 없다는 것을 의미한다. VM 이 보통 MMU (Memory Management Unit)라 불리우는 처리 장치를 이용해 구현되므로 당신은 uClinux 의 세계를 여행하는 동안 NOMMU 라는 단어를 자주 접하게 될 것이다. 매우 고수준(high-level)에서 MMU 나 VM 이 없는 것이 malloc 에 어떠한 영향을 미치는지에 대해서 알아보도록 하자.
VM 상에서는 모든 프로세스는 (비록 가상 주소이기는 하지만) 동일한 주소공간에서 실행되며 가상 메모리 시스템은 이 영역이 실제로 어느 물리 메모리에 매핑되는지를 관리한다. 게다가 프로세스가 보는 가상 메모리는 연속적인 것 일지라도 실제 물리 메모리 상에서는 동떨어진 영역에 흩어져 있을 수도 있다. 그 중에는 아마 디스크 상에 스왑 (swap) 되어 있는 것도 있을 것이다.
VM이 없다면 각각의 프로세스는 반드시 그들이 실행될 수 있는 메모리 영역 안에 위치해야 한다. 단순하게 생각해보면, 이 영역은 연속적인 공간이어야 하며 이 영역의 위아래로 다른 프로세스가 위치하고 있을 수 있으므로 일반적으로 메모리 영역을 확장할 수 없다.
원래의 주제로 돌아가보면, malloc 은 일반적으로 프로세스의 주소 공간의 크기를 확장하거나 변경하는 시스템 콜인 sbrk/brk 를 이용하여 구현된다. 그리고 malloc 라이브러리는 프로세스를 대신하여 sbrk() 에 의해 얻어진 여분의 메모리를 관리한다. 이 여분의 공간이 없어지면 다시 sbrk() 를 호출하여 더 많은 공간을 확보하게 되고, 또한 brk() 를 이용하여 메모리의 크기를 줄일 수도 있지만 대부분의 malloc 구현은 이러한 일은 하지 않는다. sbrk() 는 프로세스 메모리 영역의 끝부분에 메모리를 추가하는 일을 하고 (프로세스 크기 증가), brk() 는 프로세스 메모리 영역의 끝부분을 시작 부분의 가까운 임의의 영역으로 옮기거나 (프로세스 크기 감소) 뒤로 확장하는 일을 한다 (프로세스 크기 증가). sbrk()/brk() 의 표준적인 행동은 프로세스의 크기를 확장/감소 시키는 것이다.
uClinux 상에서는 프로세스의 크기를 증가시킬 수 없기 때문에, malloc 이 동작하기 위해서는 저수준(low-level)에서 몇가지 변화가 필요하게 되었다.
이에 관련된 많은 방법들이 있다. 대표적인 몇가지를 살펴보면:
2번은 그 나름의 장점을 가지고 있다. 이 기능을 이용하면 프로세스가 사용할 수 있는 메모리의 양을 제한할 수 있다 (장점이 될 수 있다). 하지만 힙 영역이 단지 일시적인 용도로만 사용되는 경우에도 항상 할당된다는 것을 의미한다. 힙 영역은 프로세스가 생성될 때 할당되어져 있으므로 sbrk/brk 는 표준적인 행동을 취할 수 있고 일반적인 malloc 라이브러리가 사용될 수 있다.
마지막으로 3번을 살펴보자. 여기에는 단점이 존재한다. 잘못된 프로세스 하나가 모든 메모리 영역을 다 차지해 버릴 수 있다. 시스템의 메모리 풀에서 메모리를 할당받는 것은 프로세스 메모리 영역의 마지막 부분에 대해 연산을 하는 sbrk/brk 와 호환되지 않는다. 따라서 기존의 malloc 구현으로는 불가능하고 새로운 구현이 필요하게 된다. 이 방식의 장점은 실제로 필요한 만큼의 메모리 만이 사용된다는 것이다. 사용된 메모리는 바로 커널의 전역 메모리 풀로 반환될 수 있으며 이 메모리 영역을 관리하는 기존의 커널 할당자를 이용하여 malloc 을 구현할 수 있다.
현재 uClinux 에서는 3번째 방식이 사용되고 있다. 가장 단순한 malloc 구현에서는 커널의 이용가능한 메모리 풀에서 메모리를 얻기 위해 mmap 을 호출하고 반환하기 위해서 munmap 을 호출한다. 이것으로 매우 작은 malloc 구현이 가능하다 (단지 1개의 시스템 콜 만이 사용된다).
이러한 단순한 malloc 구현에서는 다음과 같은 문제점이 발생된다:
커널 메모리 할당자는 최대 할당 크기를 충분히 늘일 수 있도록 수정되었다. 몇몇 경우에서 이것은 커널 설정 옵션을 조정하는 것만으로 가능하다. 이것은 더 큰 메모리 할당을 가능하게 하므로 큰 응용 프로그램을 실행할 수 있도록 한다.
uClinux 에는 새로운 커널 메모리 할당자가 추가되어 더이상 2의 배수 단위 할당이 필요치 않게 되었고 메모리 할당 시의 낭비가 많이 제거되었다. 이 할당자는 일반적으로 Kmalloc2 라고 부르며 NOMMU 환경에서 메모리 할당의 오버헤드를 상당히 감소시켰고 다른 태스크에서 사용할 수 있는 메모리의 양을 증가시켰다.
기존의 커널 메모리 할당자는 오직 2의 배수 단위로 메모리를 할당하였다. 예를 들어 12000 바이트가 필요한 경우에는 16KB 를 할당받게 되어 나머지 4KB 에 대해서는 사용할 수가 없었다. 이것은 응용 프로그램을 시작시킬 때 매우 낭비가 심해진다. 예를 들어 응용 프로그램의 크기가 130KB 라면 이 프로그램이 실행되기 위해선 실제로 256KB 가 필요하게 된다.
Kmalloc2 는 1 페이지 크기 (4KB) 의 요청까지는 2의 배수 단위로 할당하는 정책을 그대로 사용한다. 하지만 1 페이지 크기 이상의 요청에 대해서는 가장 가까운 페이지 단위로 조정하여 할당하도록 한다. 앞의 예제에서라면 130KB 의 응용 프로그램에 대해서 132KB 의 메모리가 할당된다. 기존의 커널 메모리 할당자에 비해 124KB 를 절약한 셈이다.
Kmalloc2 는 또한 단편화된 메모리를 피하는 기능을 포함한다. 2KB 혹은 그 이하의 할당에 대해서는 메모리 영역의 끝에서부터 아래로 내려오며 처리하고, 큰 할당에 대해서는 메모리의 시작 부분에서부터 위로 올라오며 처리한다. 이렇게 함으로써 네트워크 버퍼와 같은 일시적인 할당에 의해 메모리가 단편화되어 커다란 응용 프로그램이 실행되지 않는 상황을 막아준다.
Kmalloc2 가 완벽한 것은 아니다. 비록 기존의 커널 메모리 할당자가 더 많은 메모리 단편화를 만들어 낸다고는 하지만 Kmalloc2 에서도 메모리 단편화가 꽤 일어날 수 있다. Kmalloc2 는 uClinux 가 동작하는 임베디드 환경 - 주로 (상대적으로) 고정된 그룹의 오랫 동안 실행되는 응용 프로그램들이 존재하는 환경 - 에서 실제적으로 잘 동작한다.
이제까지 메모리 할당에 대한 커널 수준의 옵션에 대해서 약간 살펴보았다, 이제는 사용자 프로그램에서 사용할 수 있는 옵션에 대해서 살펴보기로 한다. 앞서 살펴본 몇가지 제한 사항들이 있기 때문에 사용자 프로그램에서 사용할 수 있는 솔루션은 많지가 않다. 그들은 각자 자신의 역할들을 명확히 수행한다.
먼저 "libc" 의 선택에 대한 문제가 있다. -- 이것은 다른 토픽으로 다뤄질 것이다. "malloc" 의 선택은 보통 당신이 사용하는 "libc" 에 의존한다: uC-libc 와 uClibc 가 있다. 둘 다 모두 단순한 메모리 할당자인 malloc-simple 을 제공한다.
malloc-simple 은 실제적인 메모리 요청에 대한 처리를 커널에서 수행하도록 mmap 과 munmap 을 사용한다. malloc-simple 의 구현은 매우 단순하고 코드의 크기도 무시할 만큼 작으므로 응용 프로그램에서 이를 포함하는 데 드는 비용은 매우 적다.
malloc-simple 의 단점은 - 앞서 살펴본대로 - 각각의 할당에 사용되는 약 56 바이트의 커널 오버헤드이다. 만약 응용 프로그램이 작은 크기의 메모리 할당을 많이 수행하는 경우, 메모리 사용량은 매우 커질 것이다. 예를 들어 당신이 짠 프로그램이 10 바이트 크기의 메모리를 1000번 할당받는 경우, 필요한 전체 메모리 양은 10000 바이트가 된다. 하지만 56 바이트의 오버헤드로 인해 실제로 할당되는 전체 메모리는 66000 바이트가 되며, 실제로 필요한 양보다 560% 나 많은 오버헤드를 감수해야 한다. Zebra 라는 라우팅 데몬은 시작될 때 각각의 명령 (command) 에 대한 자료 구조를 할당받기 때문에 - Zebra 에는 매우 많은 수의 명령/키워드가 존재한다 - 이러한 빈번한 작은 할당의 문제에 심각한 피해를 보는 대표적인 예이다.
uC-libc 는 libsmalloc 라고 하는 Zebra 와 같이 빈번한 작은 할당 문제를 겪는 프로그램에 특화된 버전의 malloc 을 제공한다. 이 버전은 malloc-simple 의 외부 코드로 통합되어 졌으므로 더이상 uClinux 에서
![]() uClibc 는 다음과 같은 몇가지 선택안을 제공한다:
일반적으로 더 복잡한 malloc 의 구현에서는 더 빠른 메모리 할당과 작은 할당에 대한 효율적인 처리가 이루어 지지만 uClinux 환경에서 실행되는 작은 응용 프로그램들에 포함되기 에는 부담스러운 코드 크기를 가진다. malloc 구현에 대한 몇가지 선택안이 존재한다. 어떤 것을 사용해야 할까? malloc-simple 은 일반적으로 사용하기에 좋은 기본 malloc 구현이다. 그런 다음에는 각 응용 프로그램에 알맞은 malloc 을 선택할 수 있다. 위에서 살펴본 대로 당신이 하고자 하는 목적에 따라 약간의 제한이 있을 수 있다.
부득이 하게도, 당신이 어떤 할당자를 선택했든 결국엔 모든 메모리가 바닥나고 말 것이다. 새로운 사용자가 공통적으로 접하게 되는 문제점으로는 "missing memory" 문제가 있다. 시스템은 많은 양의 사용 가능한 메모리를 가지고 있어도 사용자의 응용 프로그램에서 X 라는 크기의 버퍼를 할당받지 못하는 현상이다. 이것은 메모리 단편화에서 오는 문제로서 현재에는 가능한 해결책이 없는 실정이다. uClinux 환경에는 VM 이 없기 때문에 메모리 단편화 없이 모든 메모리를 고루 사용하는 것은 거의 불가능해 보인다. 예제를 하나 살펴보도록 하자. 시스템에는 500KB 의 사용 가능한 메모리가 있고 당신은 100KB 의 메모리를 할당받으려고 하는 상황이다. 당연히 이것이 가능한 일이라고 생각될 것이다. 하지만 메모리를 할당하기 위해서는 100KB 의 연속적인 공간이 있어야 한다는 것을 기억하자. 메모리 맵을 아래와 같이 표현해 보도록 하겠다. 하나의 문자는 약 20KB 의 메모리를 나타낸다. X 문자는 메모리가 할당되었거나 사용자의 응용 프로그램 혹은 커널에서 이미 사용 중인 영역을 나타낸다.
0 100 200 300 400 500 600 700 800 900 1000 -+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-- |XXXXX|XXXXX|---XX|--X--|-X---|XX---|-X---|-XX--|-X---|XXXXX| 위에서 볼 수 있듯이 시스템의 전체 사용 가능한 메모리 양은 500KB 이다. 하지만 그중에서 가장 큰 연속적인 영역의 크기는 오직 80KB 뿐이다. 이러한 상황이 될 수 있는 가능성은 많이 존재한다. 어떤 프로그램에서 메모리를 할당받은 후에 메모리의 중간 부분을 제외한 다른 부분들만 해제하는 경우라면 이러한 문제가 쉽게 발생될 수 있다. 일시적으로 실행되는 프로그램들 또한 메모리의 할당에 영향을 줄 수 있다.
종종 듣게되는 질문으로 왜 메모리의 단편화를 제거할 수 없느냐? 라는 질문이 있다. 문제는 uClinux 에는 VM 이 존재하지 않고 프로그램에서 사용되는 메모리 영역을 옮길 수 없다는 것이다. 일반적으로 프로그램에서는 할당된 메모리 영역 내의 주소에 대한 참조를 가지고 있으며 VM 이 없는 환경에서는 메모리가 항상 올바른 주소에 존재하도록 해야 한다. 만약 임의로 해당 메모리 영역을 다른 곳으로 옮겨 버린다면 프로그램은 정상적으로 동작되지 않을 (crash) 것이다. uClinux 상에서 이러한 상황에 대한 해결책은 없다. uClinux 의 응용 프로그램 개발자들은 항상 이러한 문제점을 인식하고 작은 메모리 블럭들을 활용하도록 노력해야 한다.
uClinux 환경에서의 메모리 할당은 위에서 살펴 보았듯이 일반적인 리눅스 환경에서의 메모리 할당과 비슷하다. 하지만 uClinux 만의 특징과 단점들을 포함한다. 메모리 할당에 관련되어 다음으로 진행되어야 할 작업은 의심할 여지없이 공유 라이브러리 구현일 것이다? (Further progress will no doubt be made on the memory allocation front now that uClinux is enjoying its first shared library implementations) 이것이 가능해 져서 malloc 의 구현이 공유 라이브러리에 존재하면 malloc 의 구현을 가능한 한 작게 만들어야 한다는 제한 사항이 적어진다. 그러므로 크기는 더 커져도, 메모리 단편화 문제와 할당시의 오버헤드 문제를 감소시킬 수 있는 효율적인 사용자 레벨의 malloc 구현이 가능해 질 것이다.
몇가지 질문에 대해 답변해 준 Phil Wilshire
![]() 참고문헌 (http://www.uclinux.org/pub/uClinux/dist 에서 다운로드 받을 수 있는 uClinux 배포판의 소스)
커널 할당자와 mmap 구현:
|