지난 포스팅에서 우리는 Memory Management와 관련한 API들에 대해 알아보았다. 이때, 이러한 API들의 실제 구현에 있어서, Free한 메모리 공간을 어떻게 처리할 것인가는 상당히 중요한 문제이다. 좀 더 구체적으로 말하면, Dynamic Memory Allocation 시, Free Space의 관리를 어떻게 할 것이냐가 핵심이란 것이다.
금일 포스팅에선 이 OS의 'Free Space Management'에 대해 알아볼 것이다. 그렇다. SP에서 다루었던 Dynamic Memory Allocation 개념의 복습 시간이라 보면 된다. SP에선 이 개념을 User System Program 입장에서 바라보았다면, 이번엔 OS 관점에서 이 개념을 학습할 것이다.
Dynamic Memory Allocation에서 Free Space를 다루는 가장 기본적이고 핵심인 기능은 Splitting과 Coalescing이다. 아래를 보자.
여담) Heap Management 시 위와 같이 Free List를 운용한다. 그 운용 방식이 무엇이든 간에!
(과거 SP 포스팅 참고)
~> 과거 SP에선, Implicit(or Explicit or Segregated) Free List 구현 간에 위와 같이 Bidirectional Coalescing을 적용할 수 있음을 알아본 바 있다. (여기선, Header와 Footer는 신경쓰지 말도록!)
한편, Memory Management에서 Tracking도 상당히 중요하다. Allocated Region, Freed Region을 각각 Tracking해야한다. Tracking을 통해 현재 Memory 상태를 알아야, 이후의 Memory를 관리할 수 있기 때문이다.
그러한 Tracking을 위해선 아래 그림과 같이 Allocated Block에 특정 정보를 담는 Header를 마련해야한다.
Header Block을 추가로 두고, 해당 Block에 Extra Information을 저장한다.
malloc이 반환한 위치는 ptr이 가리키는 위치(Block의 실제 데이터가 시작하는 위치)이고, 이 Allocated Block의 Header는 ptr이 가리키는 위치에서 8Bytes 위쪽에 마련된다.
Actual Chunk Size of malloc(N) = N + Header Size 8
이러한 Header의 용도는 다음과 같은 예시 상황에서 명확히 나타난다.
"free Function은 Size Parameter가 없는데 free할 Size를 어떻게 알까?"
~> Target Block의 Header에서 Size 정보를 추출해서 알 수 있다.
"free Function에선 넘겨받은 Block이 Allocated인지 어떻게 알까?"
~> Target Block의 Header에서 magic Number를 추출해 알 수 있다. 일종의 Is_Allocated_Flag인 것이다.
void free(void *ptr) {
header_t *header = (void*)ptr – sizeof(header_t); // Header 추출
// malloc의 반환 포인터를 받은 ptr은 Allocated Block의 Payload 시작 위치
// 를 가리키므로, 그 위치에서 8Bytes 위쪽으로 Pointer Arithmetic을 해야함!
…
assert(header->magic==1234567); // 정상적인 Free인지를 확인할 수 있음.
…
}
~> Block Header의 magic Number가 1234567이면 Allocated Block이다!!! ★
(본 시리즈 참조 교재 상에서)
한편, Free Region에 대한 Tracking도 중요하다. 초기 Heap은 하나의 커다란 Free Region이고, 당연히 Free List엔 단 하나의 Entry만 있을 것이다.
Free Region이 Memory Allocation 요청으로 인해 여러 개의 작은 Free Region으로 분리되고, 이들은 당연히 서로 인접하지 않을 수 있다.
Free List를 구축하여 Free Region들을 Tracking한다! (ex. Linked List)
typedef struct _node_t {
int size;
struct _node_t *next; // 다음의 '인접하지 않은' Free Block을 가리킨다.
} node_t; // Coalescing 때문에 항상 'Not Adjacent'임. ★★
즉, Allocated Block의 Header엔 Size, magic Number가, Free Block엔 Size, Next Pointer가 있는 것이다. ★★★
예를 들어보자. 초기 Heap이 mmap( ) System Call을 통해 커다란 공간으로 구축되었다고 하자.
/* OS initializes the heap with mmap() */
// mmap()은 Free Space의 Chunk에 대한 Pointer를 반환한다.
node_t *head = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
head->size = 4096 - sizeof(node_t); // Header Size를 제외한다!
head->next = NULL; // 초기 Next Pointer 설정
이때, Tracking을 위해 상기한 Freed Region Format에 맞게 Header를 구축한다.
Header는 8Bytes이므로, Size 정보엔 4096-8 = 4088Bytes가 들어간다. ★
Next Pointer는 일단 없으므로 NULL로 초기화한다.
이때, 위의 상황에서 User Program은 OS에게 100Bytes 공간을 Heap에 할당해달라고 Request하는데, 이를 총 3번 요청하였다. 아래 그림과 같이 말이다.
User Program이 100Bytes를 달라고 요구하면, 실제 Heap에는 Header를 포함해 108Bytes가 할당된다.
이 상태에서 두 번의 같은 Request를 반복하면, 3988 - 108x2 = 3772Bytes가 남고, 역시나 Header를 고려하면, 최종적으로 (only one) Free Space의 Size는 3764가 된다.
자, 이제 Free를 해보자. Allocation을 받아들인 Pointer Variable을 순서대로 ptr1, ptr2, ptr3라 하고,
ptr2->ptr1->ptr3의 순서대로 free( ) Function을 호출해보자.
(Heap의 시작 주소가 16KB = 16384번지라고 가정한다 ★)
~> Newly Freed Block의 앞 뒤는 모두 Allocated Block으로, Coalescing이 필요 없다(일단, 잠시 Coalescing은 고려하지 말자). 이 Block의 Next Pointer는 기존 Free List의 Head에 위치했던 '큰 블록'을 가리키도록 만들어주자.
~> 이어서 ptr1이 가리키는 Allocated Block이 Free된다. ptr1이 가리키는 위치는 16392번지이다. malloc의 반환 주소는 Payload의 시작 위치이기 때문이다. ★★
~~> 허나 Free를 위해선 Payload가 아니라, Header를 알아야 한다. 따라서, ptr1이 가리키는 위치에서 8Bytes 앞으로 이동한 후, 그곳에서 작업을 해준다. ★★
===> Freed Block에선 Payload의 위치가 아니라 해당 Block의 시작 위치가 중요하기 때문에, Next Pointer는 '기존의 큰 블록'의 Payload 위치가 아니라, 'Block의 시작 위치'를 가리키도록 만들어준다. 여기선, 16708번지가 되겠다. ★
-> 이러한 Free Mechanism을 고려해봤을 때, 우리는 다음과 같이 free( ) Function을 설계할 수 있을 것이다.
/* 현재 예시의 OS는 Free List의 Freed Block 삽입 간에
'맨 앞에 삽입' Policy를 따른다고 가정! */
void free(*ptr) {
void* temp = head; // '기존의 head가 가리키는 Block의 시작주소'를 임시 보관
head = ptr - 8; // head는 이제 '넘겨 받은 Block의 Header 주소'를 가리킴!
head->next = temp; // 새 head는 '기존의 head'를 Next Block으로 취급한다!
}
// Heap은 위로 확장하기 때문에 ptr - 8인 것이다. ★★★
동일한 원리를 토대로 free(ptr3)까지 수행하면 Heap의 상태는 아래와 같아진다. Next Pointer는 Next Freed Block의 시작 주소를 가리키고, free의 Parameter인 ptr은 Allocated Block의 Payload 시작 위치를 가리키고 있음을 반드시 기억하자. ★
~> 하지만, 이게 끝은 아니다. Coalescing이 필요하다. Coalescing은 여러 가지 방식으로 할 수 있는데, 하나는 Free할 때마다 당장 Coalescing하지 않고, 특정 시간이 지날 때만 한 번에 Coalescing하는 방식이 있을 수 있고, 다른 하나는 매번 Free할 때마다 Coalescing하는 방식이 있을 수 있다.
=> 우리는 전자를 채택했다고 가정하면, 특정 시간 소요 후 다음과 같이 Coalescing이 일어나 최초의 Heap 상태로 돌아가게 될 것이다.
여담) Coalescing은 External Fragmentation을 해결하기 위한 Method 중 하나이다. External Fragmentation은 추후 다시 설명할 것이지만, 간단히 말하면, 합쳤을 땐 Request를 Cover할 수 있는데, 안 합치면 Cover하지 못하는 그런 상황을 의미한다.
여담) 반대 개념인 Internal Fragmentation은 Block 내부에서의 희생 공간을 의미한다. Header나 Footer같은 것 말이다.
여담) 이러한 내/외부 단편화 문제를 해결하기 위한 다양한 Heap 관리 방법론이 존재한다. SP에서 다루었던 그것들을 포함해서 말이다! 하지만, 기억나는가? 내/외부 단편화는 서로 Trade-Off 관계에 있기 때문에, 둘 모두를 만족시키는 방법론을 찾기란 쉬운 일이 아니다. 내부 단편화를 줄이면 공간 효율은 높아지지만 Searching이 오래 걸리는,, 그런 느낌이다. (과거에 다 설명했다)
한편, Memory Allocation 시 Free List에서 적절한 Free Region을 어떻게 찾을 것이냐라는 Issue도 존재한다. 이 역시 마찬가지로 SP에서 자세히 다루었던 바 있다. 다시 한 번 복습해보자. 아래와 같이 크게 4개의 Approach가 있다.
~> Workload에 따라 달라지겠지만, 일반적으로 Best Fit과 First Fit이 가장 우수하다고 알려진다.
지금까지 우리는 Linked List 기반의 Heap Management에 대해 알아보고 있다. 구체적 명칭은 언급하진 않았지만 SP에서 소개했던 Implicit List, Explicit List 방식을 배운 것이라 보면 된다. 허나, 이러한 (Single) Linked List 기반의 Approach들은 다음과 같은 문제점들이 존재한다.
External Fragmentation이 높아질 가능성이 있다. ★
Same Size의 Request, Popular Size의 Request의 처리에 그닥 효율적이지 않다. ★
이러한 단점을 극복하기 위해 고안된 방법이 바로 Segregated List 방식이다.
Segregated List : 특정 Size(주로 Popular Size)의 Request들에 대해 미리 Allocated된 Memory Chunk들을 특정 자료구조 형태로 준비해놓는 방식 ★
굳이 '자주 Request되는 Size'들에 대해 매번 할당하고 해제하는 일을 반복하지 말고, 미리 그에 대한 공간을 마련해두자는 것이다. ★★
다른 Size의 Request에 대해선 기존의 방식을 사용한다.
Slab Allocator
Segregated List 기반 Approach이다.
Slab : Kernel Page의 집합 ★
각 Kernel Page들은 똑같은 크기의 Object들로 구성된다. ★
이러한 Slab들이 모여 Cache를 이룬다. ★
즉, 'Objects -> KernelPages -> Slabs -> Caches -> SlabAllocator'
Allocate memory & Construct the object!
큰 판대기인 Slab에 Kernel Page들을 모아놓는다.
그러한 Slab들을 모여서 Cache를 이룬다.
정해진(대중적인) 크기의 자료구조를 Allocation할 때는, 굳이 새로 생성/해제하지 말고, 그냥 이렇게 미리 Kernel에 마련된 공간을 사용한다는 것이다. ★
~> 이러한 Segregated List는 실제 구현 시에는 Multiple Linked Lists가 있는 형태로 구현될 수 있겠다. SP에서 수행했던 malloc-lab Project를 참고하도록 하자.
본 포스팅의 마지막 개념은 Buddy Allocator로, 최근에 가장 많이 쓰이는 Memory Allocation 방식이다.
Buddy Allocator
Free Memory를 2^n 사이즈의 Big Space로 설정한다.
Memory Allocation Request가 오면 2^n 크기의 Free Memory를 Recursive하게 계속해서 반으로 나눈다.
ex) 예를 들어보자. 전체 Free Space의 크기가 K, 즉, 1024라 하자. 이때, 10Bytes에 대한 Memory Allocation Request가 왔다.
1024
512 / 512
256 / 256 / 512
128 / 128 / 256 / 512
64 / 64 / 128 / 256 / 512
32 / 32 / 64 / 128 / 256 / 512
~> 이렇게 Division이 이뤄진다. 왜 32까지만 나누었냐면, 보통 이러한 Buddy Allocator는 Minimum Threshold를 두기 때문이다. 너무 많이 나뉘는 것을 방지하기 위해 말이다.
~~> 따라서, 10Bytes Request에 대해 32Bytes의 Block이 할당된다!
===> 이러한 '하한'이 있기에, 1Byte만 요청하더라도 32Bytes의 Block이 할당될 것이다.
왜 'Buddy' Allocator라 불리는가?
Buffer(Free Space)를 반으로 쪼갤 때 생기는 같은 크기의 Block들을 'Buddy(친구)'라고 부르기 때문이다. 사이즈가 같으니 친구인 것이다!
이러한 Buddy의 상태를 계속해서 체크해가며 할당할지 말지를 결정한다.
Allocated Block이 Free되면, 해당 Block의 Buddy가 Freed인지 확인하고, Freed일 경우 하나의 Block으로 Coalesce한다. 그리고, 이를 Recursive하게 반복한다! 멈출때까지! ★
주로 이러한 Buddy Allocator의 구현에는 Bitmap이 사용된다.
또한, 각 Size의 Buffer(Free Space)에 대해 각각 Free List를 운용한다.
Buddy Allocator Implementation : Bitmap + Multiple Free Lists
이제 예제를 통해 Buddy Allocator를 이해해보자. 위의 상황과 똑같은 상황을 상정한다. 초기 Buffer Size는 1024이고, 하한은 32이다.
1024 / 32 = 2^10 / 2^5 = 2^5
Free List는 32부터 512까지의 Size에 대해 각각 존재한다. (1024는 초기값이고, 아무 할당이 없는 상태를 의미하므로 굳이 Free List를 만들 필요가 없음)
그렇다. 이번엔 그 우측의 64Bytes 할당 공간을 Release하자. 해당 Block의 우측에는 똑같은 64Bytes 크기의 Freed Buddy가 존재한다.
Coalesce한다.
이러한 'Buddy Checking'에 Bitmap이 사용되는 것이다. 현재 Freed Block의 Size만큼만 Bitmap을 양 옆으로 탐색해, Bit Value가 0이면 Coalesce하는 것이다. ★
~> Buddy Allocator는 이런 논리로 진행된다. ★
이러한 Buddy Allocator는 Linux Kernel에서 사용하는 Memory Allocating 방식이다. Heap 할당에서도 사용되고, 굳이 Heap 뿐만이 아니라 '연속적인 메모리에 대한 할당/해제'가 필요한 부분에선 어디든지 적용되는 개념이다. ★★★
Buddy Allocator의 특징
2^n꼴의 Block만 취급하므로 Internal Fragmentation이 높다.
Bitmap을 사용하기 때문에 Buddy를 찾기가 쉽다. (효율적이다)
Buddy Allocator의 장점
Buddy Allocator의 단점
금일 포스팅은 여기까지이다. 다음 포스팅에선 Address Translation을 다룰 것이다!