[운영체제] Main Memory (1) - Address Binding, Continuous Memory Allocation
이번 글에서는 메인 메모리에 대해서 공부해보도록 하겠습니다. 메모리는 어떤 값을 저장하는 1차원 배열이라고 볼 수 있습니다. CPU는 주소 값을 통해 메모리의 특정 위치에 접근할 수 있습니다. 프로세스가 하나뿐일 때는 그 프로세스가 메모리를 모두 사용해도 되지만, 프로세스가 두 개 이상일 경우 이 메모리 자원들을 어떻게 나누어서 쓸 것인가는 중요한 이슈입니다. 우선은 메모리 주소의 할당부터 하나씩 차근차근 공부해 나가 보도록 하겠습니다.
1. 주소 할당 (Address Binding)
프로그램은 원래 디스크에 저장되어 있습니다. 이 프로그램이 실행되기 위해서는 메인 메모리로 올라와서 프로세스가 되어야 합니다. 그리고 프로세스는 실행될 때 메모리에 적재해 놓은 명령어와 데이터에 접근하여 사용하게 됩니다. 그리고 이 프로세스가 종료되면 해당 프로세스가 사용하던 메모리 공간은 가용(available) 공간이 되어 다른 프로세스가 사용할 수 있게 됩니다.
프로그램이 이렇게 메모리로 올라올 때 메모리의 어느 부분을 사용할 지를 결정을 해 주어야 합니다. 이것이 바로 주소 할당에 관한 문제인데, 즉 메모리 접근을 위한 물리적인 주소를 결정해주는 것입니다.
이는 이 주소 공간이 바인딩(binding)되는 시점에 따라 3가지 방법으로 나누어집니다. 이해를 돕기 위해 프로그램의 처리 과정을 확인하면서 읽어보시면 좋을 것 같습니다.
1. Compile Time Binding (컴파일 시간 바인딩)
컴파일 시간 바인딩은 말 그대로 컴파일이 실행될 때 주소가 이미 결정된 다는 것입니다. 즉 컴파일 과정에서 어셈블리 코드로 변경될 때 절대 코드(absolute code)가 생성되어, 정확한 주소의 값이 결정됩니다. 그래서 만일 주소를 바꿔주고 싶다면 다시 컴파일을 해야 합니다.
2. Load Time Binding (적재 시간 바인딩)
프로세스가 메모리 내에 어디로 올라오게 될지를 컴파일 중에 알지 못하면, 컴파일러는 우선 재배치 가능 코드(relocatable code)를 생성하게 됩니다. 그리고 실제 메모리의 위치는 프로그램이 메인 메모리에 실제로 적재(load)될 때 결정되게 됩니다. 그리고 만일 메모리의 위치를 바꾸어야 한다면, 사용자 코드를 다시 적재하기만 하면 됩니다.
3. Execution Time Binding (실행 시간 바인딩)
마지막으로 실행 시간 바인딩은 프로그램이 실제로 실행될 때 메모리 주소가 동적으로 결정되는 것을 말합니다. 이 방법을 사용하기 위해서는 MMU라고 하는 특별한 하드웨어 장치가 필요하며, 대부분의 운영 체제에서 이 실행 시간 바인딩을 사용합니다.
2. Logical vs. Physical Address Space
그렇다면 논리적 주소와 물리적 주소의 차이는 무엇일까요? 논리적 주소는 CPU가 생성하는 주소, 물리적 주소는 메모리가 취급하는 주소라고 생각하면 가장 쉬울 것 같습니다. 여기서 논리적 주소는 가상 주소(virtual address)라고 불리기도 합니다.
두 주소는 컴파일 시간 바인딩과 적재 시간 바인딩에서는 서로 같은 값을 가집니다. 하지만 실행 시간 바인딩 기법에서는 서로 다른 값을 가지게 됩니다. 실행 시간에 이 논리적 주소를 물리적 주소로 바꿔주는 것이 바로 MMU입니다.
MMU(메모리 관리기, Memory Management Unit)는 실행 시간에 논리적 주소를 물리적 주소로 바꿔주는 하드웨어 장치입니다. 가장 단순한 MMU 기법을 예로 들자면, 재배치 레지스터(Relocation Register)에 있는 값 만큼을 CPU에서 받은 논리적 주소에 더해줘서 최종적인 물리적인 주소를 만들게 됩니다. 아래의 그림을 참고하면 좋을 것 같습니다.
3. 연속 메모리 할당 (Continuous Memory Allocation)
이제 메모리를 어떻게 할당하는 지에 대해서 공부해보겠습니다. 가장 쉬운 메모리 할당 방법이 바로 연속 메모리 할당입니다. 우선 메모리의 일부를 Kernel에게 할당해주고, 남은 메모리들을 프로세스들에게 할당해주는 방법입니다. 따라서 하나의 프로세스는 메모리에서 하나의 연속된 메모리 섹션만을 할당받습니다.
이 시스템에서는 두 개의 레지스터가 필요합니다. 하나는 리미트 레지스터(Limit Register)이고 하나는 앞에서 배운 재배치 레지스터입니다. 재배치 레지스터는 프로세스에 할당된 물리적 주소의 가장 작은 값을 저장하고 있으며, 리미트 레지스터는 할당된 메모리의 크기에 대한 값을 가지고 있어서, 리미트 레지스터의 값보다 큰 논리적 주소는 메모리에 접근할 수 없습니다. 따라서 할당된 메모리의 범위 내의 값을 가지는 논리적 주소 값들은 리미트 레지스터를 통과해 재배치 레지스터의 값과 합쳐져서 물리적 주소를 만들어냅니다.
당연히 이 리미트 레지스터와 재배치 레지스터가 가지고 있는 값은 각각의 프로세스마다 달라져야겠죠? 그래서 Context Switch가 일어날 때 예전에 앞에서 잠깐 공부했었던 Dispatcher라는 친구가 레지스터들의 값을 바꿔주게 됩니다.
4. 연속 메모리 할당의 종류
연속 메모리 할당도 할당 형식에 따라 두 가지로 분류할 수 있습니다.
첫 번째로 Fixed-partition scheme입니다. 여기서는 각 프로세스들에게 할당해주는 각각의 파티션들의 사이즈를 고정해서 주는 것입니다. 즉 어떤 프로세스에게 할당을 해주든 1GB면 1GB, 512MB면 512MB와 같은 형식으로 모두 똑같은 사이즈로 할당을 해주게 됩니다. 그리고 하나의 프로세스는 하나의 파티션만 할당받을 수 있게 됩니다. 이러한 시스템에서는 멀티프로그래밍에서 한 번에 실행될 수 있는 프로세스의 개수가 이 메모리의 파티션의 총 개수로 인해서 정해지게 됩니다. 현재는 이 방법은 더 이상 사용하지 않습니다.
두 번째는 Variable-partition scheme입니다. 파티션들은 모두 다른 크기의 사이즈를 가지며, 운영체제는 어떤 파티션이 사용되고 있고 비어있는지를 알려주는 테이블을 가지고 있어야 합니다. 여기서 Hole이라는 개념이 등장하게 되는데, Hole이란 variable-partition scheme에서 메모리 내에 아직 할당되지 않은 부분들을 말합니다.
새로운 프로세스가 도착하면, 운영체제는 남은 Hole 중에서 적절한 크기의 Hole을 골라 프로세스에게 할당해 줍니다. 너무 큰 Hole을 할당해주면 Hole이 반으로 갈라질 수도 있고, 효율적인 메모리 사용이 힘들 수도 있습니다. 물론 프로세스가 종료되면, 메모리는 반납되어 다시 Hole이 되게 됩니다. 반납된 부분과 다른 Hole이 바로 붙어있다면 둘은 합쳐져 큰 하나의 Hole이 되어 다른 프로세스를 기다리게 됩니다. 그래서 어떤 Hole을 어떤 프로세스에게 할당해주는 지를 정해주는 것은 메모리 효율을 위해 굉장히 중요한 부분입니다. 여기에는 3가지 방법이 존재합니다.
1. First fit: 프로세스에게 충분한 크기의 Hole을 찾으면 바로 그곳을 할당시켜준다.
2. Best fit: 남아있는 Hole을 모두 살펴본 뒤 그중에 프로세스에게 맞는 Hole 중 가장 작은 Hole을 할당시켜준다.
3. Worst fit: 남아있는 Hole을 모두 살펴본 뒤 그 중에 가장 큰 Hole을 할당해준다
5. Fragmentation
마지막으로 살펴볼 개념은 Fragmentation입니다. Fragmentation은 쉽게 말해 메모리에서 현재 사용되지 못한 부분을 말하며, Externel Fragmentation은 Hole들이 다 합친다면 충분한 크기를 가지지만. 너무 작게 여러 곳에 분포해서, 프로세스에게 할당될 만큼 충분히 크지 않아서 사용되지 못하는 부분을, 그리고 Internel Fragmentation은 Fixed-partition scheme과 같은 상황에서 프로세스에게 필요 이상으로 큰 파티션이 할당되어, 할당되었지만 사용되지 않는 부분을 뜻합니다.
오늘은 메모리에 대해서 살펴보았습니다. 다음 글에서는 메모리의 중요한 개념인 Paging에 대해 알아보도록 하겠습니다. 읽어주셔서 감사합니다.