OSTEP 기반 Virtual Memory 배경지식 정리: 왜 VM이 필요한가
무언가에 대한 코드를 구현하기에 앞서 우리가 항상 생각해야 하는 부분은 바로 도대체 문제가 무엇인지, 그래서 이걸 해결하기 위한 방법으로 우리가 무언가를 구현하게 되었음을 아는 것인데요. 이는 VM에서도 다르지 않습니다. 이를 위해 이번 글에서 OSTEP을 참고하여 VM의 기반이 되는 배경지식들을 정리하고 하나의 흐름으로 엮어 봅니다.
주소 공간과 멀티프로그래밍의 등장
멀티프로그래밍 및 시분할(Time-Sharing) 시대가 도래하면서 사용자들은 여러 프로그램을 동시에 실행할 수 있게 되었는데요. 이는 여러 프로그램을 메모리에 올려놓고, 하나가 대기 중일 때 다른 프로그램을 실행하는 방식을 통해 CPU 사용률을 극대화함으로써 구현되었습니다. 그런데 여러 프로그램이 메모리 상에 동시에 존재하게 되면서, 다른 프로세스가 내 영역을 읽거나 쓰는 것을 막는 보호(Protection)가 중요한 문제로 떠올랐습니다.
이를 해결하기 위해 운영체제가 만든 사용하기 쉬운 메모리 개념이 바로 주소 공간(Address Space)입니다. 주소 공간을 실행 프로그램의 모든 메모리 상태(코드, 데이터, 스택, 힙 영역 등)을 갖고 있지요.
그리고 각 프로세스가 이러한 독점적인 주소 공간을 가지고 있는 것처럼 보이게 하는 것이 메모리 가상화, 즉 가상 메모리(Virtual Memory, VM)입니다.
Virtual Memory의 목표
- 투명성(Transparency): 프로세스가 가상 메모리의 존재를 인지하지 못하게 해야 함
- 프로그램은 자신이 전용 물리 메모리를 독점적으로 소유한 것처럼 행동해야 함
- 이를 위한 모든 작업은 백그라운드에서 운영체제와 하드웨어가 힘을 합쳐 수행
- 효율성(Efficiency): 가상화가 시간, 공간 측면에서 효율적이도록 해야 함
- 시간적으로 프로그램이 너무 느리게 실행되서는 안됨
- 공간적으로 가상화 지원을 위해 너무 많은 메모리를 사용하면 안됨
- 보호(Protection): 프로세스-프로세스 또는 프로세스-커널 간 침범 및 충돌을 방지해야 함
- 운영체제는 프로세스를 다른 프로세스로부터 보호하고, 운영체제 자신도 프로세스로부터 보호해야 함
- 자신 주소 공간 밖의 어느 것도 접근할 수 없는 고립(Isolate) 상태여야 함
주소 변환의 원리: 어떻게 효율적이고 유연하게 메모리를 가상화하는가?
주소 변환(Address Translation)은 명령어 반입, 탑재, 저장 등에 사용되는 가상 주소를 정보가 실제로 존재하는 물리 주소로 변환하는 것인데요. 이를 통해 운영체제는 프로세스의 모든 메모리 접근을 제어할 수 있게 되고, 주소 접근이 항상 주소 공간의 범위 내에서 이루어지도록 보호되는 것을 보장합니다.
초기에는 베이스 레지스터(Base Register)와 한계 레지스터(Limit Register)를 사용하여 유효 주소 범위를 지정함으로써 간단한 주소 변환 및 보호가 구현되었습니다. 하지만 이 방식은 현대의 페이징 기반 주소 변환 방식에 비해 유연성이 부족하다는 문제가 있었습니다.
페이징: 페이지를 사용하여 어떻게 메모리를 가상화할 수 있을까?
페이징(Paging)은 공간을 동일한 크기의 조각으로 분할하는 것입니다. 여기에는 두 가지 장점이 있습니다.
- 유연성: 프로세스 주소 공간 사용 방식과 관계없이 효율적인 주소 공간 개념 지원 가능
- 단순함: 메모리의 빈 공간 관리가 단순해짐
페이지 테이블(Page Table)은 각 프로세스 주소 공간의 가상 페이지에 대한 주소 변환 정보를 저장하고 있습니다. 페이지 테이블은 다양한 종류의 비트를 통해 각 페이지의 상태를 관리합니다.
- Valid Bit: 특정 주소 변환의 유효 여부를 나타냄 (미사용 페이지 구분)
- Protection Bit: 페이지가 읽기, 쓰기, 실행이 가능한지 여부
- Present Bit: 해당 페이지가 현재 물리 메모리에 존재하는지 여부
- Dirty Bit: 메모리 반입 후 페이지 변경 여부
- Reference(Accessed) Bit: 해당 페이지가 접근되었는지 여부
페이징은 페이지 테이블을 통해 위에 언급한 강력한 기능들을 제공하지만, 동시에 시공간적인 문제점도 발생시킵니다.
- 모든 메모리 참조에 대해 먼저 페이지 테이블에서 변환 정보를 반입해야 하므로, 추가적인 메모리 참조가 필요해진다.
- 이는 프로세스를 매우 느리게 만듦
- 페이지 테이블 자체가 각 프로세스마다 필요하므로, 너무 많은 메모리를 차지하게 된다.
이러한 페이징이 가진 문제를 해결하기 위해 하드웨어의 도움을 받게 되는데, 이를 도와주는 컴퓨터의 주소 변환 전담 하드웨어가 바로 MMU(Memory Management Unit)입니다.
이 장치는 TLB(Translation Lookaside Buffer)라는 고속 캐시 메모리를 내장하고 있는데, 이를 참조하여 가상 주소-물리 주소 간의 변환을 효율적으로 처리합니다. 페이지 테이블 참조는 결국 메모리 접근이기 때문에 CPU보다 확연하게 느릴 수밖에 없고, 이를 위해 TLB가 주소 변환 작업의 캐시 역할을 하게 됩니다.
스와핑, 물리 메모리 이상의 공간을 사용하기 위해서 어떻게 해야 할까?
프로세스에게 더 큰 주소 공간을 제공해야 하는 이유는 편리함과 사용 용이성을 위해서인데요. 이는 디스크(보조기억장치)의 일부분을 메모리 스왑 공간으로 할당한 뒤, 이 공간을 마치 논리적인 메모리처럼 사용함으로써 구현됩니다.
이를 통해 프로세스 실행 중 자료구조들을 위한 충분한 메모리 공간이 있는지 걱정할 필요가 없게 되고, 멀티프로그래밍의 등장과 함께 많은 프로세스들의 페이지를 물리 메모리에 전부 저장하는 것이 불가능하여 발생하는 문제를 해결할 수 있게 됩니다.
곧, 각 프로세스에 곧 저마다의 큰 가상 메모리가 있는 것 같은 환상을 부여함으로써 아무리 큰 프로그램이더라도, 아무리 많은 프로그램이더라도 문제없이 실행할 수 있게 합니다.
물리 메모리에 존재하지 않는 유효 페이지를 접근하는 행위인 페이지 폴트(Page Fault)가 발생하는 경우, 운영체제는 페이지 폴트 핸들러를 호출해 이를 해결하게 되는데요. 이 과정에서 빈 물리 메모리를 찾아 디스크에서 데이터를 로딩하고 해당 가상 페이지와 매핑해 줍니다.
이때 빈 프레임이 없다면 기존 프레임들 중 Victim(희생될) 프레임을 찾아 Eviction(퇴출)한 뒤, 이를 비운 다음 사용하게 됩니다. 이때 희생될 프레임의 데이터가 보관할 필요가 있는 경우, 디스크에 마련해 놓은 스왑 공간에 들어가게 됩니다(Swap-Out). 그리고 해당 데이터가 다시 필요하게 되는 경우 다시 메모리에 로딩되지요(Swap-In).
이러한 방식은 과거에 사용되던 스와핑(Swapping) 기법의 발전된 형태입니다. 스와핑은 전체 프로세스를 디스크와 메모리 사이에 통째로 옮겨 실행 공간을 확보하는 방식이었으나, 현대의 시스템에서는 필요한 페이지만 메모리로 불러오는 요구 페이징(Demand Paging) 기법이 사용되고 있습니다.