 |
|
자유 리스트 기반 메모리 관리자
일반적인 프로그램에서는 자주 요청하는 메모리 블록 크기가 대개 몇 가지로 한정되어 있다. 이
절에서 구현할 메모리 관리자는 이러한 경험적 법칙을 활용한다. 메모리 관리자는 빈번히 사용하는
크기마다 자유 블록 주소 리스트를 관리하는데, 이를 기술 용어로 자유
리스트(free-list)라고 한다. 예를 들어 8, 16, 24, 32, 48, 64
바이트를 자주 요청하는 프로그램이 있다면, 메모리 관리자는 각 바이트 별로 자유 리스트 6개를
관리한다. 비트맵 메모리 관리자와 비슷하게, 메모리 관리자는 초반에 운영체제로부터 커다란
메모리 덩어리를 확보한다. 그런 다음, 메모리 관리자 내부에서 메모리 덩어리를 개별 리스트로
나누어 관리한다. 프로그램이 'm' 바이트를 요청하면, 메모리 관리자는 이 값을 'm'에 가장
가까운 블록 크기 'n'으로 변환한다. 그런 다음, 블록 크기가 'n'인 리스트에서 자유
블록을 찾아서 할당한 다음 프로그램에 돌려준다.
보호 바이트 간략 소개
'n' 바이트 블록은 객체만이 아니라 몇 가지 메타 정보도 저장한다. 각 블록은 끝 부분에
보호 바이트(guard byte)라는 추가 바이트를 포함한다. 보호 바이트는 힙 메모리에서
사용하는 개념이다. 일반적으로,
memcpy
와
memset
함수는 원래 할당 받은 메모리 범위를 벗어나서 힙 메모리를 손상시킬 수 있다. 그래서 많은
컴파일러는 메모리 할당 시 블록 앞뒤에 특수 문자, 즉 보호 바이트를 추가한다. 그런 다음,
해당 블록을 참조할 때 앞뒤에 특수 문자가 있는지 확인한다. 블록 앞뒤에 특수 문자가 없으면
프로그램이 어느 순간에 값을 덮어썼다고 가정한다. 즉 힙 메모리가 더 이상 정상 상태가
아니며 손상되었다고 가정한다. 이는 프로그래머를 괴롭히는 까다로운 메모리 버그를 찾아내는 좋은
방법이다. 다음 예제에서는 네 바이트 중 두 바이트를 보호 바이트로 사용한다. 세 번째
바이트는 객체 크기를 저장하고, 마지막 바이트는 블록 상태(자유/할당)를 나타내는 플래그를
저장한다.
자유 리스트 메모리 관리자 구현
앞서 설명했듯이, 메모리 관리자는 블록 크기 별로 블록 포인터 리스트를 여러 개 관리한다.
또한 운영체제에서 확보한 메모리 덩어리를
memoryPool
로 관리한다.
MemoryManager
소멸자는 풀에 든 메모리 덩어리 전체를 해제하여 메모리 누수를 막는다.
Listing 17
은 메모리 관리자 자료 구조다.
Listing 17. 자유 리스트 메모리 관리자를 위한 MemoryManager 클래스
정의
class MemoryManager : public IMemoryManager
{
private:
VoidPtrList Byte8PtrList;
VoidPtrList Byte16PtrList;
VoidPtrList Byte24PtrList;
VoidPtrList Byte32PtrList;
VoidPtrList Byte40PtrList;
std::vector<void*> MemoryPoolList;
. . . . . . . . . //helper routines may go in here
public:
MemoryManager( ) {}
~MemoryManager( ) {}
void* allocate(size_t);
void free(void*);
};
|
예제 프로그램은 크기가 각각 16, 28, 32바이트인 세 가지 클래스를 사용한다. 보호
바이트까지 고려하면 24, 32, 40바이트 블록이 필요하다. 즉
Byte24PtrList
,
Byte32PtrList
,
Byte40PtrList
가 가장 자주 쓰이므로 코드 역시 세 리스트를 위주로 구현한다. 그러나 설계가 유연하므로 다른
블록 크기를 추가하기도 쉽다.
각 클래스에서
new
와
delete
연산을 중복 정의한다. 각
allocate
/
free
연산은 메모리 관리자 함수를 호출하여 메모리를 할당하고 해제한다. 메모리를 할당하고 해제하는
함수는 아래서 상세히 설명한다. 초기 객체 수는 1024개로 잡는다. 또한 예제에서 사용할
클래스 크기는 상수로 미리 정의한다.
Listing 18
을 참조한다.
Listing 18. 메모리 관리자가 사용할 상수 정의
const int JOB_SCHEDULER_SIZE = sizeof(JobScheduler);
const int COMPLEX_SIZE = sizeof(Complex);
const int COORDINATE_SIZE = sizeof(Coordinate);
const int POOL_SIZE = 1024; //number of elements in a single pool
//can be chosen based on application requirements
const int MAX_BLOCK_SIZE = 36; //depending on the application it may change
//In above case it came as 36
|
위 상수는 메모리 관리자가
allocate
와
free
함수에서 사용한다.
메모리 할당
allocate 함수는 블록 크기를 지정한 인수 size를 보고 어느 자유 리스트에서 블록을
할당할지 결정한다. 그런 다음, 해당 리스트에 남아있는 자유 블록이 있는지 찾는다. 자유
리스트는 블록 자체가 아니라 블록 포인터만 저장한다는 사실에 주의한다(블록 자체는
MemoryPoolList
에 속한다).
자유 리스트가 비어 있다면, 운영체제에서 추가로 메모리 덩어리를 요청한 후
InitialiseByte24List
,
InitialiseByte32List
,
InitialiseByte40List
함수 중 하나로 초기화한다.
자유 블록이 있으면, 해당 블록을 할당 상태로 표시하고 보호 바이트를 설정한 후 블록 주소를
반환한다. 구현은
Listing 19
를 참조한다.
Listing 19. MemoryManager::allocate 정의
void* MemoryManager::allocate(size_t size)
{
void* base = 0;
switch(size)
{
case JOB_SCHEDULER_SIZE :
{
if(Byte32PtrList.empty())
{
base = new char [32 * POOL_SIZE];
MemoryPoolList.push_back(base);
InitialiseByte32List(base);
}
void* blockPtr = Byte32PtrList.front();
*((static_cast<char*>(blockPtr)) + 30) = 32; //size of block set
*((static_cast<char*>(blockPtr)) + 31) = 0; //block is no longer free
Byte32PtrList.pop_front();
return blockPtr;
}
case COORDINATE_SIZE :
{
if(Byte40PtrList.empty())
{
base = new char [40 * POOL_SIZE];
MemoryPoolList.push_back(base);
InitialiseByte40List(base);
}
void* blockPtr = Byte40PtrList.front();
*((static_cast<char*>(blockPtr)) + 38) = 40; //size of block set
*((static_cast<char*>(blockPtr)) + 39) = 0; //block is no longer free
Byte40PtrList.pop_front();
return blockPtr;
}
case COMPLEX_SIZE :
{
if(Byte24PtrList.empty())
{
base = new char [24 * POOL_SIZE];
MemoryPoolList.push_back(base);
InitialiseByte24List(base);
}
void* blockPtr = Byte24PtrList.front();
*((static_cast<char*>(blockPtr)) + 22) = 24; //size of block set
*((static_cast<char*>(blockPtr)) + 23) = 0; //block is no longer free
Byte24PtrList.pop_front();
return blockPtr;
}
default : break;
}
return 0;
}
|
메모리 해제
free
함수는 해제할 블록 주소를 받는다. 주어진 블록 주소로 블록 끝에 있는 보호 바이트를 찾은 후
블록 크기를 읽는다. 블록 크기는 마지막 바이트 바로 앞 바이트에 들어 있다. 크기를 얻은
후에는 객체를 자유 상태로 설정하고 (마지막 바이트를 1로 설정하고) 해당하는 자유 리스트를
찾는다. 객체는 삭제 단계 끝 부분에 자유 리스트로 들어가게 된다. 구현은
Listing 20
을 참조한다.
Listing 20. MemoryManager::free 정의
void MemoryManager::free(void* object)
{
char* init = static_cast<char*>(object);
while(1)
{
int count = 0;
while(*init != static_cast<char>(0xde))
//this loop shall never iterate more than
{ // MAX_BLOCK_SIZE times and hence is O(1)
init++;
if(count > MAX_BLOCK_SIZE)
{
printf ("runtime heap memory corruption near %d", object);
exit(1);
}
count++;
}
if(*(++init) == static_cast<char>(0xad)) // we have hit the guard bytes
break; // from the outer while
}
init++;
int blockSize = static_cast<int>(*init);
switch(blockSize)
{
case 24: Byte24PtrList.push_front(object); break;
case 32: Byte32PtrList.push_front(object); break;
case 40: Byte40PtrList.push_front(object); break;
default: break;
}
init++;
*init = 1; // set free/available byte
}
|
자유 리스트 메모리 관리자 전체 코드는
다운로드
절에서 참조한다. 코드는 실제로 동작하는 자유 리스트 메모리 관리자 구현과 위에서 설명한
목록을 모두 포함한다.
장점과 단점
위에서 설계한 메모리 관리자에는 몇 가지 장점이 있다.
- 크기가 다양한 메모리 블록을 깔끔하게 처리한다.
-
설계가 유연하며, 프로그램 요구사항에 따라 자유 리스트 크기를 결정하도록 확장할 수
있다.
-
보호 바이트를 사용하여 메타 정보를 저장한다. 객체마다 저장되는 메타 정보는 여러 모로
유용한데, 특히 프로그램 실행 도중 힙 메모리 손상을 감지할 수 있다.
위 설계는 성능이 높아진다는 장점이 있는 반면 메모리를 많이 사용한다는 단점도 있다.
|