서론


프로그래밍을 계속해서 공부하다보면 virtual memory를 접하게 됩니다. 저도 처음 접했을 때는 아 어떻게 가상으로 메모리를 쓰나보다, 그러면 뭔가 좋은가보다하고 넘어갔던 기억이 있습니다.

이 글에서부터 시작해 여러 글을 통해 그렇게 넘어갔던 virtual memory의 모든 것에 대해 알아보려고 합니다. 우선 이 글에서는 virtual memory가 어떤 방식으로 발달해왔는지 알아보겠습니다.

목표


우선 간단하게 Virtual memory를 사용해서 달성하고자 하는 목표들에 대해서 알아보죠.

  1. Transparency
  2. Efficiency
  3. Protection

이렇게 세가지가 있는데요, Transparency는 프로세스는 메모리가 공유되고 있는지 알지 못하는 것을 의미하고, Efficiency는 fragmentation이라는 것을 줄여서 효율적으로 메모리를 사용하는 것을 의미하고, Protection은 말 그대로 프로세스와 OS를 다른 프로세스로부터 보호하는 것을 의미합니다.

앞으로 이 목표들이 달성되는 것을 천천히 살펴보시죠.

Virtual Memory


그럼 virtual Memory란 무엇인지부터 간단하게 알아볼까요? Virtual memory가 없다면 프로세스는 코드 영역, 데이터 영역 등을 곧바로 RAM에 올릴 것입니다. 그리고 CPU가 RAM을 이용해서 뭔가 실행을 하겠죠.

반면 virtual memory가 있다면 프로세스에게는 virtual address space가 할당이 되고, 런타임에 virtual address space에서 physicl address로의 변환을 거쳐 실제 메모리에 데이터들이 올라가게 됩니다.

그런데 virtual address space는 굉장히 큽니다. 64비트 프로세스는 64비트 사이즈의 주소공간을 가질 수 있죠. 64비트 사이즈라는게 어느 정도일까요? 총 2^64개만큼의 주소를 가질 수 있는 것이니 감도 안오게 큰 사이즈이죠.

그런데 그에 반해 8gb RAM 정도를 생각해봐도 2^30이 1gb이니 2^33크기의 주소밖에 가지지 못합니다. 그러니까 저런 가상 주소 공간을 전부다 RAM에 매핑할 수 있는 것은 아닙니다. 선별적으로 매핑을 하죠. 이 이야기는 추후에 다루겠습니다.

이런 virtual memory를 구현하는데에는 크게 두 가지 방법이 있습니다. static relocationdynamic relocation입니다. 하나씩 알아보겠습니다.

Static Relocation


static relocation이란 소프트웨어를 이용한 방법으로 운영체제가 각 프로그램을 메모리에 로드하기 전에 주소값들을 다 다시써서 메모리에 올리는 것입니다. 그렇게되면 프로그램이 0번지를 기준으로 저장되어있던 주소가 메모리에 올라가면서 100번지를 기준으로 바뀌고 그렇게 되겠죠.

아래의 그림이 그 예시입니다.

이런 간단한 방식은 하드웨어의 지원이 따로 필요없이 소프트웨어적으로 처리한다는 장점이 있습니다만 딱봐도 단점이 있어보이죠.

처음에 소개해드렸던 virtual memory의 목표인 protection을 하나도 달성하지 못합니다. 그리고 한번 메모리에 로드하고 나면 다른 데로 움직이기가 너무 어렵기때문에 external fragmentiation이 발생할 수 있습니다.

Dynamic relocation


앞선 방법을 개선하여 이제 OS가 relocation을 하는 것이 아닌 하드웨어가 relocation을 하게됩니다. 그러므로 하드웨어에 의해 protection이 이루어지게 됩니다.

이런 Dynamic relocation을 구현한 세 가지 방법이 있습니다.

  1. Fixed or variable partitions
  2. Segmentation
  3. Paging

partition 방법부터 알아보죠

Fixed Partitions

프로세스의 virtual address와 특정 주소값을 합하여 실제 주소로 가게됩니다. 그리고 그 실제 주소는 고정된 사이즈의 partition으로 나누어져 있습니다. Base register값 같은 경우 프로세스마다 다르므로 PCB같은 곳에 저장합니다.

이 방식은 간단하고, base register값만 바꾸면되므로 context swtiching이 빠르게 일어나지만 고정된 크기의 partition으로 나누어져있기때문에 internal fragmentation으로 이어지게됩니다.

그래서 이를 개선하기 위해 파티션 사이즈를 나누고 파티션마다 큐를 구현해 둔 후 first fit/best fit으로 프로세스를 파티션에 할당하게 됩니다.

Variable Partitions

Variable Partition은 레지스터를 하나 더 쓰게 됩니다. 새로 추가된 레지스터는 limit register로, 파티션의 크기를 제한합니다.

이 방식은 전 방식의 internal fragmentation을 막을 수 있곘죠.

그렇지만 여전한 문제가 있습니다. 바로 프로세스가 쭉 연속적으로 저장되어야한다는 거고, external fragmentation이 발생할 수 있습니다. 또한 sharing 하는 측면에서 약점이 있죠.

Segmentation

Segmentation 방법은 address space를 logicla segment들도 나누는 방법입니다. 이 segment들이 각각 code, data, stack, heap인 것이죠.

그래서 한 프로세스의 여러 세그먼트들을 굳이 메로리 상에서 한 군데에 배치할 필요도 없고, 각 세그먼트들을 따로 관리할 수도 있습니다.

각 세그먼트는 두가지 방법으로 구분이 가능하게 할 수 있습니다.

하나는 Explicit approach로, virtual address의 맨 앞 두자리를 segment number로 삼습니다.

다른 하나는 ‘Implicit approach’로, type of memory reference로 segment를 구분합니다. 그러면 PC-based addressing이 일어나면 code segment라고 판단하는 식인 것이죠.

Explicit approach로 구현된 segmentation 방법을 설명드리겠습니다.

아래와 같이 virtual address의 첫부분을 통해 어떤 segment인지 구분하고, offset을 통해 그 segment의 특정 위치로 접근하게 되는 식입니다.

Segmentation은 좋은 방법인 듯 보이지만 오늘날 쓰이느 방법은 아니며 역시 장단점이 있습니다.

Segmentation은 segment를 공유하기 좋고, 보호하기 좋고, 메모리를 sparse하게 써서 stack과 heap을 그때그때 늘릴 수 있습니다.

그러나 각 segment는 역시 연속적으로 할당되어야하고, 커다란 segment table이 계속 메모리에 있어야합니다. 또한 Cross-segment address 문제가 발생할 수 있습니다.

그래서 등장한 것이 바로 paging 방법인데, 이는 다음 글에서 소개해드리겠습니다.