들어가며
가상 메모리는 OS의 주 역할이자 현대 컴퓨터에서 가장 중요한 메커니즘이다.
가상 메모리란?
메인 메모리의 추상화로 각 프로세스에 하나의 크고 통합된, 사적인 주소 공간을 제공하는 것을 의미.
그렇다면 사적인 주소 공간 제공이 왜 중요한 걸까??
가상 메모리가 없으면 어떻게 되는 걸까?
먼저, 알아야 할 것이 있는데 바로 물리 주소라는 개념이다. 컴퓨터 시스템의 메인 메모리는 M개의 연속적인 바이트 크기 셀의 배열로 구성된다. 각 바이트는 고유의 물리 주소(Physical Address)를 가진다. 그렇다면 우린 자연스럽게 물리 주소를 통해 메인 메모리에 접근하여 데이터를 꺼낼 수 있다. 이를 물리 주소 방식 이라고 한다.
물리 주소 방식의 문제점
만약 물리 주소 방식만 사용한다면, 우린 프로그램을 개발할 때 현재 어떤 물리 주소 공간을 사용할 수 있는지 이를 바꿔 말하면 메모리에 어느 곳이 비었는지를 직접 지정해줘야 한다. 당연하게도 프로세스(프로그램)은 여러 개이므로 빈 공간을 가리키는 물리 주소는 계속해서 바뀔 것이고 이 주소를 하드 코딩하는 방식으로 데이터를 주입해야 한다. 즉, 개발자(사용자)가 매우 힘들어진다. 간단한 변수 하나 선언조차 엄청나게 복잡한 과정을 거쳐야 된다.
그래서 등장한 가상 메모리
물리 주소가 문제였던 건, 결국 주소가 계속 바뀐다는 것이다. 이를 해결하기 위해 프로세스에겐 모두 같은 크기의 주소 공간을 제공하면 해결됀다. 이게 바로 가상 주소(상대 주소)이다. OS 및 CPU(MMU)에게 가상 주소를 물리 주소로 번역하도록 만들어 놓고 개발자(사용자)는 메인 메모리 하나만 쓴다고 생각하고 가상 주소대로 처리하면 그만이다. 사용자가 매우 편해지는 결과가 나타난다. 이게 바로 메모리의 추상화이다.
캐시 역할을 하는 가상 메모리
메인 메모리는 결국 일종의 캐시이다. 그러면 가상 메모리는 이를 어떻게 실현하고 있을까?
여기서 등장하는 개념이 바로 페이지이다. 페이지는 운영체제가 규정해 놓은 사이즈 블록의 단위로 크기가 고정되어 있는 메모리 블록이다. 가상 메모리를 블록 단위로 분할함으로써 최대한 메모리 공간을 활용하고자 하는 것이다.
이를 쉽게 이해할려면 게임을 예시로 들 수 있다. 최근 게임을 보면 100GB 넘게 용량이 크다. 그런데 우리가 사용하는 컴퓨터의 메인 메모리인 RAM의 용량은 16, 32, 64GB를 많이 사용한다. 그러면 100GB 넘는 게임을 어떻게 메인 메모리에 올려 컴퓨터는 돌릴 수 있는 걸까? 여기서 페이지가 쓰인다. 결국 우리는 게임을 하면서 모든 게임의 기능들을 사용하지 않는다. 일부분만 사용할 뿐이다. 이는 지금 사용하는 코드 부분만 메인 메모리에 올리면 된다는 뜻이다. 그 코드 부분이라는 것이 전체 뭉텅이로 올라가는 것이 아니고 페이지 크기 단위로 잘게 잘라서 메인 메모리에 올린다. 왜냐하면 프로세스(프로그램)은 지금 여러 개를 수행해야 한다. 프로세스가 실행되고 종료되는 과정 속에서 메모리의 빈 공간이 불연속적으로 발생한다. 최대한 메인 메모리에 욱여넣기 위해 페이지라는 레고 블록으로 잘라서 넣는 것이다. 이러한 기법을 요구 페이징이라고 한다.
페이지 테이블
어느 프로세스의 데이터들을 담고 있는 페이지들은 메인 메모리에 불연속적으로 올라가 있다. 프로세스를 종료한다던가 그 페이지가 필요하는 상황이 온다면 어떻게 페이지를 가져올 수 있을까? 이를 위해 페이지 테이블이라는 일종의 자료 구조를 만들어 해결한다. 페이지 테이블은 페이지 테이블 엔트리(PTE)의 배열이라고 볼 수 있다. 기본적인 PTE는 안에 유효 비트와 PPN(Physical page number)이라는 물리 페이지 번호가 들어있다. 이 페이지 테이블은 각 프로세스마다 존재한다.
페이지 테이블 엔트리란?
엔트리는 특정 목적으로 사용하는 자료 구조이다. 배열, 구조체, 리스트 등이 들어갈 수 있다. 페이지 테이블 엔트리는 페이지 테이블 배열 한 칸을 구성하는 자료 구조 형식을 의미한다. 그렇기에 페이지 테이블은 페이지 테이블 엔트리의 배열이라고 볼 수 있다.
페이지 적중
메모리 관리 유닛(MMU)은 CPU가 전달한 가상 메모리 주소를 토대로 페이지 테이블에 그 주소가 있는지 확인한다. 이는 유효 비트가 1인지 0인지를 파악해서 알아낼 수 있다. 1이면 있다는 말이고 0이면 없다는 말이다. 페이지 테이블에 찾고자 하는 PTE가 존재한다면 이는 페이지 적중이라고 부른다.
페이지 적중 시엔 MMU는 PPN과 가상 주소에 있던 Offset을 결합해 물리 주소를 만들어 내고(번역하고) 메인 메모리에게 해당 물리 주소의 데이터를 요구하고 전달받는 식으로 흘러간다.
페이지 폴트
페이지 폴트라는 것은 페이지 테이블에 지금 찾고자 하는 PTE가 없다는 것을 의미한다. 그러면 운영체제는 오류 핸들러를 불러서 이를 처리하게 하는데, 이 오류 핸들러는 다음과 같은 작업을 수행 한다.
1. 물리 메모리 내의 교체할 페이지를 결정하고, 만일 이 페이지가 수정되었다면 디스크에 페이지를 저장한다.
2. CPU가 요청한 페이지를 디스크에서 찾아 메모리에 올리고 페이지 테이블을 갱신한다.
3. 오류 핸들러는 처음의 프로세스로 돌아가고, 오류를 발생했던 CPU 인스트럭션은 재시작된다. CPU는 문제를 일으킨 가상 주소를 다시 MMU로 전송하여 결과적으로 메인 메모리에 있는 페이지를 꺼내오게 된다.
추가로 페이지 폴트가 생길 때, 물리 메모리 내의 교체할 페이지를 결정하는 알고리즘을 페이지 교체 알고리즘이라고 부른다. 여기엔 수많은 알고리즘이 있기에 따로 찾아보는 것을 추천한다. (대학 전공 시험 문제 단골이다..)
TLB를 이용한 주소 번역 속도 개선
앞서 봤다시피 MMU가 가상 주소를 물리 주소로 번역하기 위해 페이지 테이블을 참조한다. 하지만 페이지 폴트라는 것은 디스크에 접근하는 연산이다. 따라서 큰 비용을 요구한다. (디스크 접근 속도는 캐시에 비하면 매우 매우 느리다)
페이지 폴트를 줄이기 위해서 등장한 것이 TLB이다. TLB는 번역 참조 버퍼(Translation Lookaside Buffer)의 줄임말로 페이지 테이블에 있던 PTE를 캐싱하는 용도로 사용한다. 그렇게 MMU는 주소 번역을 위해 다음과 같은 작업 과정을 거치게 된다.
1. CPU는 가상 주소를 생성한다.
2. MMU는 적당한 PTE를 TLB로부터 선입한다 (TLB에 찾고자 하는 PTE가 존재하는지 확인하고 있으면)
3. MMU는 가상 주소를 물리 주소로 번역하고, 그것을 캐시/메인 메모리에 전송한다.
4. 캐시/메인 메모리는 요청한 데이터 워드를 CPU로 리턴한다.
TLB 또한 캐시이기 때문에 적중과 폴트가 존재한다. 위 과정은 적중일 때를 나타내는데 만약 폴트라면 페이지 테이블에 PTE가 있는지 확인하는 방식으로 기존과 동일한 과정을 거치게 된다. 결국 이러한 과정은 페이지 폴트를 최대한 안 일어나게 하고자 함에 있다.
참고
컴퓨터 시스템 Randal E. Bryan , David R. O'Hallaron 저자(글) · 김형신 번역