Operating Systems : Three Easy Pieces를 보고 번역 및 정리한 내용들입니다.
메모리의 측면에서 초기의 컴퓨터들은 사용자에게 추상화를 많이 제공하지는 않았다. 이때 OS는 그저 메모리의 0번 주소부터 시작하는 루틴들의 집합이었고, 나머지의 메모리는 하나의 실행 중인 프로그램(즉 프로세스)이 사용했다.
시간이 지나고 컴퓨터들이 비싸짐에 따라 사람들은 이를 좀 더 효율적으로 사용하기 시작했다. 여러 프로세스들이 특정 시간에 실행되기를 기다리고 OS가 그 프로세스들을 전환시키는 멀티프로그래밍의 시대가 시작한 것이다. 이에 따라 CPU 이용률(utilization) 이 크게 증가했는데, 이런 효율성의 증가는 특히 컴퓨터가 수 십만에서 수 백만 달러를 하던 시대에는 더 중요했다.
곧 사람들은 더 많은 컴퓨터들을 필요로 했고, 시분할(time sharing) 의 시대가 시작했다. 구체적으로는, 많은 사람들, 특히 프로그래머들이 배치 컴퓨팅의 한계를 느꼈기 때문이다. 동시에 컴퓨터를 사용하는 많은 사용자들이 현재 실행되고 있는 각자의 작업들로부터 적당한 시간 내의 응답을 받을 수 있기를 희망하게 되면서 상호작용성의 개념도 중요해졌다.
시분할을 구현하는 한 가지 방법은 한 프로세스를 모든 메모리에 대한 전적인 접근 권한을 주면서 짧게 실행한 후, 어떤 종류의 디스크에 모든 상태를 저장하며 멈추고, 다른 프로세스의 상태를 로드하고, 또 잠시 실행시키는 것이다.
하지만 이런 접근법은 메모리가 커짐에 따라 너무 느려진다는 큰 단점을 가지고 있다. 레지스터 레벨에서 상태를 저장하고 복원하는 것은 빠르지만, 메모리의 모든 내용들을 디스크에 저장하는 건 성능 상 너무 비효율적이기 때문이다. 이를 위해 선택한 해결법은 OS가 시분할을 효율적으로 구현할 수 있도록 여러 프로세스들을 메모리에 두고 서로 전환할 수 있게 하는 것이다.
예를 들어 세 프로세스 A, B, C가 있다고 하자. 이들은 각각 물리 메모리의 일부를 차지하며, 단일 CPU를 사용한다고 할 때, OS는 그 프로세스들 중 하나를 선택해 실행하고 나머지들은 큐에서 대기 상태로 있게 한다.
시분할이 널리 사용되면서, 메모리에 동시에 올라가있는 프로세스 사이의 보호가 중요한 이슈가 됐다. 한 프로세스가 다른 프로세스의 메모리를 읽거나 쓰게 해서는 안 된다는 것이다.
이를 위해 OS는 물리 메모리를 사용하기 쉽도록 추상화해야 했다. 이 추상화를 주소 공간(address space) 라 부르는데, 이것이 실행 중인 프로그램이 시스템 메모리를 보는 방식이다. 이 근본적인 OS 메모리 추상화 기법은 메모리가 어떻게 가상화되는지를 이해하기 위한 핵심이 된다.
프로세스의 주소 공간은 실행 중인 프로그램의 모든 메모리 상태를 포함한다. 예를 들어 프로그램의 코드는 메모리의 어딘가에서 살아있어야 하므로 주소 공간에 위치한다. 프로그램은 자신이 함수 호출 체인의 어디에 있는지 알기 위해, 지역 변수를 할당하고 파라미터를 전달하고 다른 루틴들로 값을 반환하거나 그것들로부터 값을 반환받기 위해서 스택(stack)을 사용하며, C에서의 malloc()
이나 C++, 자바의 new
와 같이 동적으로 할당되는 메모리를 위해 힙(heap)도 필요로 한다. 물론 이 외에도 여러 가지들이 있기는 하지만, 지금은 코드, 스택, 힙의 세 구성 요소만 신경쓰도록 하자.
프로그램 코드는 주소 공간의 최상단에 위치한다. 코드는 정적이므로 프로그램이 실행된다고 해서 더 많은 공간을 요구하거나 하지는 않는다.
스택과 힙은 프로그램이 실행됨에 따라 늘거나 주는 영역들이다. 이 영역들은 각각 주소 공간의 양 반대 끝에 위치시켜 서로 다른 방향으로 자랄 수 있게 한다. 보통 힙은 코드 영역 바로 뒤에 위치시켜 아래로(즉 주소가 증가하는 방향으로) 자라게 하고, 스택은 맨 아래에서 시작해 위로 자라게 한다. 다만 이러한 배치는 관례일 뿐이기에 다르게 조정될 수 있고, 특히 여러 스레드들이 주소 공간에 있을 때에는 유효하지 않다.
물론 여기서의 위치는 실제 물리 메모리에서의 위치를 말하는 게 아니라, OS가 실행 중인 프로그램에 제공하는 추상화의 측면에서 말하는 것이다. 프로그램은 실제 물리 메모리의 주소 0에서부터 시작하는 것이 아니라 다른 어떤 물리 주소들에 올라가있다. 따라서 다음과 같은 문제가 발생한다.
어떻게 OS는 하나의 물리적인 메모리를 사용하면서도, 여러 프로세스들에게 자기 자신만의 거대한 주소 공간을 사용하고 있다는 환상을 제공할 수 있을까?
이렇게 OS가 환상을 제공할 때, 우리는 "OS가 메모리를 가상화하고 있다."라고 말한다. 왜냐하면 실행 중인 프로그램은 스스로가 상당히 큰 주소 공간을 가지고 메모리의 특정 주소에 위치해있다고 때문이다. 실제로는 다르지만 말이다.
그렇다면 OS가 가지고 있는 메모리 가상화의 목표들에는 어떤 것들이 있을까?
가상 메모리 시스템의 주요 목표 중 하나는 투명성이다. OS는 프로그램이 메모리가 가상화되었다는 사실을 모르는 듯이, 자신이 고유한 물리 메모리를 가지고 있다는 듯이 행동할 수 있게 해야한다.
가상 메모리의 다른 목표는 효율성이다. OS는 가상화를 시공간적으로 가능한 한 효율적으로 구현해야 한다. 시간적으로 효율적인 가상화를 구현하기 위해, OS는 TLB와 같은 하드웨어의 도움에 의존한다.
가상 메모리의 마지막 목표는 보호다. OS는 프로세스들을 OS 자신을 포함한 다른 프로세스로부터 보호해야 한다. 프로세스는 탑재, 저장, 명령어 페치 등을 수행할 때, 다른 프로세스나 OS의 메모리 내용에 접근하거나 영향을 줄 수 없어야 한다. 보호는 프로세스들이 고립(isolation) 의 특성을 가질 수 있게 한다. 오류가 있거나 악의적인 다른 프로세스들로부터 스스로를 안전하게 고립시켜 실행한다는 의미에서 말이다.
다음 챕터부터는 메모리 가상화에 필요한 하드웨어와 OS의 기본적인 메커니즘을 공부하고, 가용 공간들을 관리하고 공간이 부족할 때 메모리 페이지들을 쫓아내는 방법 등 OS에서 조우할 정책들을 공부하게 될 것이다.
가상 메모리 시스템은 각 프로세스에 스스로가 크고, 성긴, 고유의 주소 공간을 가지고 있다는 환상을 제공한다. 각 가상 주소 공간은 프로그램의 모든 명령어, 데이터들을 포함하며, 가상 주소를 통해 프로그램에서 참조된다. OS는 하드웨어의 도움을 받아 이렇게 참조된 가상 주소를 물리 주소로 변환한다. OS는 이 서비스를 많은 프로세스들에 한 번에 제공할 수 있다.