<컴퓨터 구조에 대한 네 번째 이야기>
이제 책의 마지막 챕터까지 오게 되었습니다.
이번 글에서는 컴퓨터 구조에 대한 마지막 이야기를 다루게 됩니다.
그리고 앞으로 작성하게 되는 글은 메모리 관리와 I/O(입출력) 등에 대해 다룰 예정입니다.
[공부했던 것을 되짚어보며]
어느덧 책의 마지막 챕터까지 오면서 다뤘던 내용들에 대해서 되짚어볼까 합니다.
항상 매 챕터마다 처음에는 컴퓨터 구조와 운영체제에 관련된 이야기로 시작을 했었습니다.
그리고 챕터 별로 항상 굵직하게 다루는 주제들이 따로 있었고요.
첫 번째 챕터에서는 컴퓨터 구조 중에서도 컴퓨터의 주요 구성 요소는 무엇인가에 대해서 다뤘었습니다.
그리고 아스키 코드와 유니코드의 차이, 32bit와 64bit 환경에서의 프로그래밍 방법에 대한 공부를 했습니다.
두 번째 챕터에서부터 좀 어렵지만 중요한 개념들이 나오기 시작했습니다.
컴퓨터 구조 부분에서는 관련하여 레지스터를 설계했습니다.
그리고 기본적인 사칙연산 명령어와 LOAD/STORE 명령어를 설계하는 과정을 거쳤습니다.
또한 여기서 공부했던 개념 중 커널 오브젝트와 핸들, 핸들 테이블과 같은 중요한 것들이 있었습니다.
프로세스와 프로세스 간 통신(IPC)에 대해서 공부했고, 프로세스의 상태에 대한 것들을 학습했었죠.
OS의 스케줄러가 어떤 일을 하는지와 어떻게 일을 하는지(알고리즘)에 대해서도 공부했었습니다.
세 번째 챕터에서는 쓰레드와 관련한 내용들이 주를 이뤘습니다.
여기서 다뤘던 컴퓨터 구조와 OS 이야기에서는 CPU에 의해 함수 호출을 다뤘습니다.
그 과정에서 레지스터와 스택 프레임, 명령어의 순차적인 실행과 함께 함수 호출 규약에 대해서 공부를 했습니다.
그 뒤의 내용은 쓰레드에 대한 개념적인 내용부터 생성과 소멸, 동기화 기법과 쓰레드 풀까지.
쓰레드로 시작해서 쓰레드로 끝났다고 봐도 무방할 정도네요.
이제 마지막 챕터에 들어오게 되었습니다.
앞에서 배웠던 내용들과는 또 새로운 내용들과 다양한 주제를 다루게 될 것입니다.
저도 책의 앞부분은 집중해서 읽다가 뒤에 가서는 좀 풀리는 경향이 없잖아 있습니다.
그러다보니 항상 후반부의 내용은 잘 기억을 못하거나 애매하게 알고 있는 경우들도 많았고요.
그렇지만 여기서 다루게 될 주제들도 중요한 내용이므로 끝까지 집중해서 내용을 정리하도록 하겠습니다.
[메모리 계층(Memory Hierarchy)]
[메모리의 범위와 종류]
우리가 보통 컴퓨터의 스펙을 이야기하면서 "메모리 몇 기가 정도 된다"라는 말을 많이 합니다.
보통 이렇게 말하면 메인 메모리로 사용되는 램(RAM)을 주로 생각하죠.
근데 사실 메인 메모리로 사용되는 램 이외에도 메모리라고 불릴 수 있는 것들은 아주 많습니다.
컴퓨터를 구성하는 요소 중에서 임시/영구적으로 저장 기능을 조금이라도 가지고 있으면 메모리라고 볼 수 있습니다.
실제로 메모리라고 불리울 수 있는 것들을 한 번 나열해볼까 합니다.
1) 메인 메모리
우리가 흔히 알고 있는 램(RAM)입니다.
엄밀히 따지면 D-RAM이라고 하는 것이 맞겠네요.
그리고 더 엄밀하게 따지고 들어가자면 사실 "메인 메모리 != 램" 이라는 것도 알아두세요.
메인 메모리 역할을 꼭 램이 해야되는 것은 아니거든요.
그런데 현재 사용하는 대부분의 컴퓨터에서 메인 메모리의 역할은 램이 하고 있습니다.
막 어디가서 눈치없이 "사실 램을 메인 메모리라고 하는 것은 어쩌고저쩌고" 이러지는 마시고요.
저도 그냥 "메인 메모리 == 램" 이라고 할겁니다.
그게 편하거든요.
그리고 뒤에서 설명하게 될 가상 메모리 개념을 공부하면서 더 명확해질겁니다.
2) 레지스터
CPU 안에 있던 그 친구 맞습니다.
마찬가지로 이 친구도 메모리입니다.
CPU 안에 내장되어 있고, 연산을 위한 '저장' 기능을 제공하고 있습니다.
3) 캐시(Cache)
게임할 때 많으면 좋은 것
캐시 메모리입니다.
이제 컴퓨터를 사면서 CPU도 중요하게 생각하다보니 캐시 메모리도 메인 메모리만큼이나 알 사람들은 알고 있죠.
메인 메모리로 사용되는 D-RAM보다 더 빠른 S-RAM을 사용합니다.
그리고 이 녀석은 메인 메모리와 CPU의 사이에서 중간 저장소 역할을 수행합니다.
요즘에는 캐시가 CPU에 기본적으로 내장되어서 나오다보니 레지스터랑 비슷하다고 생각할 수도 있습니다.
사실 캐시 메모리는 CPU의 일부로 존재하던 메모리 개념이 아닙니다.
정확히는 CPU에 가까이 있는 메모리라고 보는 것이 맞습니다.
CPU 내부에 내장되어 있는 메모리는 위에 있는 레지스터라는 친구입니다.
단지 CPU를 설계하는 과정에서 캐시 메모리도 같이 넣어서 만드는 것 뿐입니다.
4) 하드디스크
마찬가지로 하드디스크도 메모리입니다.
거의 대부분이 "이것도 메모리인가? 저장소 아니었음?" 이렇게 생각하실 수도 있습니다.
메모리 맞습니다.
SD카드나 플로피 디스켓(이게 뭔줄 아신다면...), CD-ROM과 같은 I/O 장치도 메모리라고 할 수 있습니다.
[메모리의 계층 구조(Memory Hierarchy)]
프로그램이 실행되는 동안에 메모리가 하는 일은 데이터의 입/출력입니다.
그래서 모든 메모리가 동일하게 데이터의 입/출력이라는 역할을 수행합니다.
바꿔서 말하면 레지스터가 됐건, 하드 디스크가 됐건 다 똑같다는 말입니다.
단지 차이점이라면 CPU와 얼마나 멀리 떨어져있느냐에 따라서 결정됩니다.
우리가 알고 있다시피 레지스터는 CPU안에 내장되어 있으므로 CPU와 제일 가깝습니다.
반대로 하드디스크는 CPU와 가장 멀리 떨어져있고요.
그리고 CPU에 가까운 메모리일수록 속도가 빠르고, 멀리 있을수록 느립니다.
예를 들면 CPU가 레지스터에서 데이터를 가져오는 것은 별 다른 절차가 필요없습니다.
그런데 CPU가 메인 메모리에 접근을 하려면 버스(BUS) 인터페이스를 통해서 접근해야 하는 절차가 있죠.
어딘가를 걸쳤다가 가기 때문에 그만큼 느릴 수 밖에 없습니다.
"그럼 제일 좋은 레지스터만 메모리로 갖다 쓰면 되지 뭐한다고 이것저것 섞어서 쓴답니까?"
그게 됐으면 진즉에 했을겁니다.
근데 여러분들도 아시겠지만 지금 그렇게 된 컴퓨터는 없습니다.
기술도 기술이거니와, 돈이 문제입니다.
현실적으로 안된다는 것이죠.
CPU 근처로 가는 메모리일수록 빠른 것도 많지만 기술적인 문제도 있고, 돈도 많이 듭니다.
그래서 하드 1테라 늘리는거랑 메인 메모리 16기가 하나 사는거를 비교해보면 어디가 더 싸게 먹히던가요?
굳이 안 찾아보셔도 금방 아실겁니다.
일단 정리하자면 메모리는 다음과 같은 피라미드 계층 구조를 갖고 있습니다.
보다시피 CPU에 가장 가까이 있는 레지스터가 맨 위에 있습니다.
그 다음은 L1, L2 캐시(요즘에는 L3 캐시도 기본인거 같습니다), 메인 메모리, 하드디스크 순입니다.
그림처럼 아래로 갈수록 면적이 커지는데, 이는 용량을 뜻합니다.
그래서 가장 아래에 있는 하드디스크는 용량이 큰 대신, 가장 속도가 느리기도 합니다.
이제 이 피라미드 계층의 메모리 구조는 어떻게 동작을 하는가?
우리가 하드 디스크에 저장되어 있는 프로그램을 실행시킨다고 칩시다.
그러면 하드 디스크에서 프로그램이 실행되면서 메인 메모리로 올라갑니다.
그리고 메인 메모리에 있는 데이터 일부는 실행을 하기 위해 캐시로 올라갑니다.
마찬가지로 캐시에 있는 데이터 중에서 연산에 필요한 데이터를 레지스터로 보내게 됩니다.
이 피라미드 구조는 자신보다 아래에 있는 메모리를 캐시하기 위한 캐시 구조로 이뤄져있다고도 볼 수 있습니다.
이 말이 무슨 말인가 이해가 잘 안될 수도 있을겁니다.
그래서 이번에는 역순으로 동작을 해보겠습니다.
연산에 필요한 데이터를 레지스터에서 찾지 못하면? 캐시한테 연산에 필요한 데이터가 있는지 확인합니다.
그런데 여기서도 없다고 하면? 메인 메모리에도 이게 있나 물어봅니다.
여기마저도 없다고 하면 하드 디스크까지 가서 데이터를 읽어오게 됩니다.
데이터를 찾는 내내 찾던 데이터가 하나도 없어서 밑바닥까지 내려갔다 오게 되었습니다.
그런데 중간의 어딘가에 찾던 데이터가 있었다고 한다면?
하드디스크까지 갈 일은 없었겠죠.
그래서 캐시를 사용하는 것이고, 피라미드 구조가 캐시 구조로 이뤄졌다고 하는 것입니다.
실제로 메인 메모리를 제외한 캐시 메모리에는 연산에 필요한 데이터가 존재할 확률은 90% 이상입니다.
그래서 메인 메모리에 데이터가 있게 된다 그러면 밑바닥까지 내려갈 일은 거의 없게 되는겁니다.
[캐시(Cache)와 캐시 알고리즘]
앞에서 메모리 이야기를 하다가 캐시(Cache) 이야기로 넘어오게 되었습니다.
이전 주제에서도 말했다시피, 레지스터만 크게 놓고 사용한다면 다른 메모리는 필요조차도 없을겁니다.
그리고 결론은 알고 있겠지만 이는 현실적으로 불가능합니다.
그렇기 때문에 메모리를 계층 구조로 나누고 캐시하는 구조를 만들게 된 것이고요.
이처럼 캐시 메모리는 컴퓨터의 성능을 향상시키는 데에 있어서 지대한 역할을 수행하게 됩니다.
[컴퓨터 프로그램의 일반적인 특성]
우선 캐시가 도입되게 된 배경부터 알아보고 가겠습니다.
옛날에 석학들이 메모리의 문제를 해결하기 위해 많은 노력을 했습니다.
그러던 중 컴퓨터 프로그램을 유심히 관찰하다보니 프로그램에 공통적인 특성이 있는 것을 알게 되었습니다.
바로 Locality(지역성 또는 국부성)이라는 특성이었습니다. ('참조의 지역성'으로도 알려져 있습니다.)
그래서 이런 공통적인 소프트웨어의 특성을 연구하고 분석했죠.
연구 결과는 "하드웨어 구조적으로 캐시 메모리라는 것을 두게 되면 성능이 향상될 것이다"는 결론이 나온 것입니다.
그 결론은 실제로 성능 향상이라는 결과로 이어지게 됐고요.
이제 간단한 코드를 하나 예시로 들어보려고 합니다.
아마 아시는 분들은 아시는 굉장히 간단한 정렬 알고리즘인 '버블 정렬'의 코드입니다.
이 코드를 통해서 프로그램이 가지고 있는 일반적인 특성에 대해서 알아보겠습니다.
#define ARR_LEN 5
void bubblesort(int srcArr[], int n)
{
int i, j, temp;
for(i = 0; i < n; i++)
{
for(j=0; j < n - i - 1; j++)
{
temp = srcArr[j];
srcArr[j] = srcArr[j+1];
srcArr[j+1] = temp;
}
}
}
코드 자체는 어려운 코드가 아니니까 별도의 설명은 하지 않겠습니다.
우선 함수 내부적으로 알고리즘을 구현하면서 사용된 변수 i, j, temp가 있습니다.
굳이 이 함수만이 아니라고 해도 대부분 함수에서는 특정 연산을 위해 지역변수를 선언하게 됩니다.
이런 지역변수의 특성은 선언 또는 초기화 이후에 다양한 값으로 변경되는 것입니다.
또한 값을 얻기 위해서 참조도 빈번하게 일어나고요.
위의 코드만 예로 들더라도 i, j, temp에 굉장히 자주 접근하는 것을 볼 수 있습니다.
이와 같은 특성을 'Temporal Locality(시간 지역성)'이라고 합니다.
프로그램 실행 시 최근에 접근이 이뤄진 주소의 메모리 영역은 자주 접근하게 된다는 프로그램 특성을 말합니다.
쉽게 말해서 같은 메모리에 반복적으로 접근할 일이 많다는 것입니다.
다음은 srcArr[j] = srcArr[j+1]이라는 부분을 보겠습니다.
보다시피 배열이고, 여기서는 j의 값이 변합니다.
j값은 0부터 시작해서 배열의 크기만큼 증가할 것이고요.
여기서 j값이 1 증가할 때마다 접근하는 메모리의 주소값은 4바이트씩 증가하게 됩니다.
이와 같은 특성을 가리켜 'Spacial Locality(공간 지역성)'이라고 합니다.
프로그램 실행 시 접근하는 메모리 영역은 이미 접근이 이루어진 영역의 근처일 확률이 높다는 프로그램 특성을 말합니다.
그래서 내가 0x12번지를 통해서 메모리를 접근했다 가정해봅시다.
그러면 다음에 접근할 메모리는 이 주소에서 멀리 떨어지지 않을 것(주변 어딘가에 접근한다)이라는겁니다.
이와 같은 두 가지의 특성을 통해 캐시라는 개념이 도입이 되었고, 실제로 메모리 접근 성능 향상에 큰 영향을 줬습니다.
[캐시 알고리즘]
캐시가 도입된 이유도 알았고, 이제 캐시가 어떻게 반영되는 것인지 그 알고리즘에 대해서 한 번 알아볼까 합니다.
그냥 대충 "자주 사용하는 주소의 데이터를 캐시에 넣는다"라고 이해하고 말아도 그만이긴 합니다.
그래도 기왕 알거라면 제대로 알고 가는 것이 더 좋지 않겠습니까?
이 글을 읽으시는 시점에서 이미 선택지는 없습니다.
어차피 알고 가는 것은 끝까지 알고 가도록 합시다.
위 그림을 가지고 예를 들어가며 설명하겠습니다.
우선 ALU가 연산하는 과정에서 필요한 데이터가 있는 경우에는 데이터를 레지스터로 옮겨야 합니다.
그래서 가장 먼저 L1 캐시가 데이터를 가지고 있는지 확인해봅니다.
만약 있다면 캐시 힛(Cache Hit)이 발생했다고 합니다.
그리고 찾은 데이터를 레지스터로 이동합니다.
반대로 찾지 못했다면 캐시 미스(Cache Miss)가 발생했다고 합니다.
이어서 L2 캐시에서 데이터를 찾아보게 됩니다.
여기서 찾았다면 아까와 마찬가지로 캐시 힛이 발생하여 L2 캐시에서 데이터를 가져오면 됩니다.
그런데 여기서도 못찾았다? 그러면 캐시 미스로 인해서 메인 메모리까지 가는 것입니다.
이제 데이터가 이동하는 것을 보겠습니다.
캐시는 기본적으로 데이터를 블록 단위로 전송하게 됩니다.
만약 L1 캐시에서 데이터를 찾지 못했는데, L2 캐시에서 데이터를 찾을 경우를 예로 들어보겠습니다.
0x0100 번지의 데이터를 조회하려고 했는데 이를 L2 캐시에서 찾게되면 L2 캐시는 데이터를 L1 캐시로 올려보냅니다.
이 때 올려보내는 데이터는 0x0100 번지의 데이터를 포함한 하나의 블록으로 전달이 됩니다.
이는 실제로 필요한 데이터만 올려보내는 것이 아니라 그 주변의 데이터도 같이 블록으로 묶어서 보내는 것이죠.
여기서 Spatial Locality의 특성이 적용되는 것입니다.
앞서 CPU가 캐시에 데이터를 요청할 때 데이터를 가지고 있을 확률이 90% 이상인 것도 특성을 적용했기 때문입니다.
이제 그림을 좀 더 자세히 보겠습니다.
메모리 계층의 아래로 갈수록 위로 올려보내는 데이터의 블록이 크다는 것을 알 수 있습니다.
왜 블록 사이즈가 아래의 계층으로 갈수록 커질까요?
그 이유는 아래에 존재하는 메모리일수록 접근 횟수를 줄이기 위함입니다.
이 또한 Spatial Locality의 특성을 고려한 부분이라고 볼 수 있겠습니다.
이제 마지막으로 캐시 미스가 발생할 때 고려할 부분을 다뤄보겠습니다.
프로그램이 실행되는 동안에는 하드디스크를 제외한 모든 메모리 부분은 항상 채워져있습니다.
특히 L1, L2 캐시는 항상 블록을 꽉 채워놓고 있어야 합니다.
그래야 캐시 미스가 날 확률을 줄일 수 있기 때문입니다.
그런데도 만약 캐시 미스가 발생했다고 하면?
새로운 블록을 받아야 하는 상황인데 캐시는 꽉 차있는 상태입니다.
결국 기존에 저장된 블록을 밀어내고 새로운 블록을 받아야 합니다.
이 때 사용하는 블록 교체 알고리즘은 캐시 교체 정책에 따라서 달라지게 됩니다.
보편적으로는 LRU(Least-Recently Used) 알고리즘을 사용하는데, 최근에 가장 적게 사용된 블록을 교체하게 됩니다.
물론 실제로는 캐시 정책에 따라서 약간씩의 차이가 있다는 것 정도는 알아두시면 될 것 같습니다.
[캐시 프렌들리 코드(Cache Friendly Code)]
이제 캐시에 대해 대략적으로 이해가 되었습니다.
그런데 우리가 지금까지 코드를 작성하면서 캐시를 고려하면서 작성했던 적은 없었습니다.
실제로 캐시의 도움을 많이 받을 수 있도록 구현한 코드를 '캐시 프렌들리 코드'라고 합니다.
되게 어려울 것 같은 개념이지만 정말 간단한 예시만 가지고도 이해할 수 있습니다.
// 1번
for (int i = 0 ; i < 3; i++)
for (int j = 0 ; j < 3; j++)
total += arr[j][i];
// 2번
for (int i = 0 ; i < 3; i++)
for (int j = 0 ; j < 3; j++)
total += arr[i][j];
보다시피 정말 간단한 코드입니다.
여러분이 보셨을 때에는 어느쪽 코드가 캐시 프렌들리 코드라고 생각이 드시나요?
정답은 2번입니다.
1번의 경우에는 Temporal Locality는 만족을 하고 있습니다.
하지만 2번과 비교했을 때 Spatial Locality는 만족을 시키지 못하고 있죠.
1번과 2번의 메모리 구조를 그림으로 그려서 비교해보시면 왜 그런지 바로 이해가 되실겁니다.
[가상 메모리(Virtual Memory)]
이제 좀 어려운 부분입니다.
저도 이 부분을 이해하는 것이 좀 어려웠습니다.
글로 정리하기까지 세 번은 본 것 같네요.
그렇지만 확실히 중요한 부분이기 때문에 이해를 하고 넘어가야 합니다.
64bit 시스템을 기준으로 다음의 질문에 대한 답을 어떻게 내릴 수 있을까요?
"실제 메모리는 2GB밖에 안되는데 어떻게 프로세스에 8GB가 할당이 될 수 있는가?"
가상 메모리에 대한 개념을 이해하고 나면 이 질문에 답을 할 수 있어야 합니다.
[물리 주소(Physical Address)]
우선은 물리 주소에 대해서 이야기를 해보겠습니다.
물리 주소는 말 그대로 진짜 RAM의 메모리 주소값을 말합니다.
위에서 예로 든 2GB의 메모리를 예로 들어보겠습니다.
그러면 CPU에서 접근가능한 메모리 영역은 $0 ~ 2 * 2^{10} * 2^{10} * 2^{10} - 1$번지까지가 됩니다.
그래서 접근 가능한 메모리 영역은 실제 물리적인 메인 메모리의 주소 범위에 해당하게 됩니다.
이처럼 주소를 할당하는 방식을 '물리적 주소 지정(Physical Addressing)'이라고 합니다.
이 방식의 특징은 메인 메모리의 크기에 따라서 지정 가능한 주소의 범위가 결정되는 것입니다.
하지만 이 방식은 우리가 가지고 있는 질문에 대한 해답을 내리기에 충분한 방식이 아닙니다.
CPU 입장에서는 접근 가능한 주소의 범위가 2GB 범위로 지정이 됩니다.
이는 바꿔서 말하면 프로그래머도 할당할 수 있는 주소 범위가 제한적이라는 것입니다.
물리 주소 방식은 쉽게 이해할 수 있는 메모리 구조지만, 우리가 찾는 답은 아닙니다.
[가상 주소(Virtual Address) 시스템 - (1)]
64비트 시스템에서 프로세스 생성 시 최소 8GB의 메모리를 할당 받을 수 있습니다.
이 경우에는 4GB는 유저 영역, 4GB는 커널 영역이 되겠죠.
그런데 앞에서 우리가 가지고 있는 메모리는 2GB라고 했으니, 할당 받기에는 어림도 없는 크기입니다.
그래서 8GB는 실제로 존재하지 않는 가상의 주소라고 생각해볼 수 있습니다.
이처럼 가상의 주소를 지정하는 방식을 '가상 주소 지정(Virtual Addressing)'이라고 합니다.
그리고 가상 주소 지정을 통해서 할당받게 되는 8GB를 '가상 주소 공간(Virtual Address Space)'라고 합니다.
이제 가상 주소를 쓸 수 있다는 것을 알았습니다.
우리가 가지고 있는 질문에 대한 답을 찾을 수 있는 실마리를 얻은 것입니다.
일단 우리가 가지고 있는 메모리는 2GB, 나머지 6GB를 어디선가 가져오긴 해야됩니다.
이걸 가져올 곳이 있습니다.
바로 '하드디스크'라는 놈한테서 가져오면 됩니다.
앞에서도 말했지만 하드디스크도 결국 메모리라고 했습니다.
단지 메인 메모리에 비해서 속도가 느리다는 점을 빼면 얘도 충분히 메모리로서 기능을 할 수 있습니다.
요즘에는 아무리 못해도 1TB는 사용하니까 하드디스크가 1TB라고 해보겠습니다.
그럼 여기에 8GB의 프로세스 몇 개를 할당해준다 해도 큰 문제는 없게 됩니다.
대신, 이 두 가지를 고려할 필요가 있습니다.
[첫 번째: 선 할당으로 인한 부담]
일단 프로세스가 8GB라고는 했는데, 프로세스가 생길 때마다 진짜로 8GB씩 할당한다면 이것도 문제입니다.
예를 들어 우리가 콘솔에서 "Hello World"를 출력하는 프로세스를 하나 생성하는데 여기에 8GB가 할당된다?
기절초풍할 일입니다.
[두 번째: 느린 속도의 개선 필요성]
일단 메인 메모리와 비교하면 하드디스크는 당연히 느립니다.
다른건 몰라도 속도에 있어서 문제가 생기면 안되겠죠.
우리가 뮤직 플레이어를 사용해서 음악을 듣고 있다고 가정해봅시다.
그런데 음악의 재생이 느려질때 이렇게 생각하진 않습니다.
'이건 하드디스크에 있는 가상 메모리를 읽고 있어서 그렇구나'
애당초 이런 일을 겪으신 분은 없으실겁니다.
이제부터 이 두 문제를 해결하는 것이 곧 가상 주소 체계를 이해하는 과정이 될 것입니다.
첫 번째 문제인 선 할당으로 인한 부담에 대한 문제부터 해결하겠습니다.
우선 가상 주소 체계를 사용할 시스템에 대해서 다음과 같은 가정을 하고 가겠습니다.
1) 16비트 시스템, 주소의 갯수는 $2^6 * 2^{10}$개이므로 0~64K-1까지 주소 지정이 가능
2) 프로세스별로 64KB 메모리가 할당, 가상 메모리 방식으로 할당
3) 메인 메모리는 16KB, 램 용량이 16KB
우선 메인 메모리의 4배나 되는 프로세스를 할당해야 되는 상황입니다.
실제 메모리는 16KB로, 물리 주소 체계 기준으로 0~16K-1까지만 할당이 가능합니다.
그래서 가상 메모리 방식을 이용해서 할당해야 하는 상황입니다.
이 문제를 해결하기 위해서는 다음과 같은 구조를 고안하게 됐습니다.
이는 프로그램이 실제로 64KB를 전부 사용할 확률이 꽤 높다는 것을 근거로 합니다.
위의 그림을 보시면 MMU(Memory Management Unit)라는 놈이 있습니다.
이 놈이 하는 일은 16KB밖에 없는 메모리를 CPU에게는 64KB가 존재하는 것처럼 제어하는 역할을 합니다.
그림에서는 편의상 CPU와 MMU가 따로 있는 것처럼 보이게 뒀습니다.
실제로는 CPU와 함께 하나로 묶여있는(Packaging)되어 있는 하드웨어적인 장치입니다.
이제 MMU라는 놈이 CPU의 요청을 어떻게 처리하는지 보겠습니다.
먼저 위에 있는 CPU가 1K번지부터 20바이트를 할당해달라고 요청을 합니다.
그러면 MMU는 메인 메모리에서 사용되고 있지 않은 블록을 하나 골라서 할당을 합니다.
그리고 이어서 36K번지부터 20바이트를 할당해달라는 요청을 받게 됩니다.
앞의 과정과 마찬가지로 MMU는 메인 메모리에서 사용되고 있지 않은 블록을 하나 골라서 또 할당을 합니다.
여기서 할당하는 크기는 기껏해봐야 20바이트인데, 할당은 4KB를 할당했습니다.
이는 앞서 소개했던 Spatial Locality의 특성을 근거로 하여 할당의 단위를 4K로 잡고 블록으로 준 것입니다.
조만간 해당 메모리의 근처에서 접근하고 참조할 것이기 때문이죠.
이러한 블록의 크기는 절대적인 것이 아니며 시스템마다 달라질 수 있는 기준입니다.
그리고 이와 같은 블록을 하드웨어 측면에서는 '페이지 프레임(Page Frame)'이라고 합니다.
소프트웨어의 측면에서는 '페이지(Page)'라고 합니다.
여기서 페이지 프레임은 실제 메인 메모리 블록을 의미하며, 페이지는 가상 메모리 블록을 말합니다.
그리고 페이지 프레임과 페이지의 크기는 동일합니다.
위 그림은 MMU에 의해서 가상 메모리와 실제 메모리가 매핑(Mapping)되어있는 것을 보여주고 있습니다.
실제로 MMU는 CPU에 서비스를 제공하기 위해서 위 그림과 같은 정보를 유지하게 됩니다.
이제 메모리를 할당하는 과정이 아닌 CPU가 요청한 주소의 데이터를 MMU가 해석해 나가는 과정을 보겠습니다.
페이지의 크기를 4K로 했기 때문에 페이지의 갯수는 16개가 됩니다.
그래서 페이지0은 0K-4K, 페이지1은 4K-8K, 페이지 15는 60K-64K의 메모리 주소를 가리키게 됩니다.
페이지 테이블의 키(Key)는 페이지의 숫자를 사용합니다.
그리고 페이지 테이블의 값(Value)는 해당 페이지가 존재하는 페이지 프레임의 시작 주소입니다.
이와 같이 테이블을 구성하게 되면 가상 주소를 물리 주소로 변환하는 것이 가능합니다.
위 그림의 예시는 CPU가 10진수로 57354번지에 데이터를 저장하라고 MMU에게 요청한 상황입니다.
그러면 MMU는 가상 주소 57354에 해당하는 물리 주소를 찾아야 합니다.
57354는 56K + 1을 가리키므로 14번째 페이지 프레임에 해당하는 것을 알 수 있습니다.
여기서 MMU가 페이지 프레임을 찾는 것은 상위 4비트만 가지고도 충분히 구분을 할 수 있습니다.
그래서 페이지 테이블을 통해 14번째 프레임의 시작 번지를 확인하고 하위 12비트를 참조하여 값을 저장합니다.
만약 4바이트의 int형 정수라면 000000000001을 시작으로 000000000100 까지 걸쳐서 저장됩니다.
[가상 주소(Virtual Address) 시스템 - (2)]
앞에서는 가상 주소와 물리 주소를 매핑하는 MMU와 가상 주소를 물리 주소로 변환하는 과정까지 확인했습니다.
사실상 가장 주소 체계에 대해서는 설명할 것은 다 끝났습니다.
그런데 아직 남은 문제가 하나 있죠.
"하드디스크의 느린 속도를 어떻게 개선할 것인가?"
앞서 말했던 것처럼 부족한 메모리를 하드디스크에서 땡겨와서 쓰기로 했습니다.
그래서 스왑 파일(Swap file)이라는 개념을 도입하여 하드디스크의 일부를 메인 메모리처럼 끌어다 쓰게 됩니다.
메모리의 부족한 문제는 해결이 되었지만, 속도에 대한 부분은 해결이 되지 않은 상황입니다.
여기서 캐시의 개념을 하드디스크와 램 사이에 접목하는 것입니다.
실제로 하드디스크에 실질적인 메모리 공간은 모두 할당을 합니다.
그리고 프로그램의 Locality라는 특성을 고려한다면 블럭 단위로 필요한 부분만 메인 메모리에 올리면 되는 것입니다.
현재 메모리가 꽉 차있는 상태에서 새로운 메모리를 할당해야 하는 상황이 왔다고 가정해보겠습니다.
그러면 시스템이 정의한 메모리 관리 기법(아마도 LRU)에 의해서 특정 메모리 블록이 하드디스크에 저장됩니다.
그리고 그 블록이 빠진 자리에 새로운 메모리가 할당이 됩니다.
이번에는 디스크에 저장했던 8-12K 영역에 접근을 하게 되면 어떻게 될까요?
다시 꺼내와야 합니다.
그래서 이전과 마찬가지로 특정 메모리 블록을 하드디스크에 저장하고, 그 블록이 빠진 자리에 다시 할당되면 됩니다.
위의 두 그림을 통해서 추가로 확인해야 할 점이 있습니다.
램과 하드디스크 사이의 데이터 이동 기본 단위는 페이지 프레임의 크기와 같다는 것입니다.
앞서 문제가 되었던 부분들은 다 해결이 되었습니다.
이제 진짜 마지막으로 둘 이상의 프로세스가 있을 때 가상 메모리는 어떻게 관리되는지 알아보겠습니다.
하드디스크는 스왑 파일이라는 개념을 이용해서 부족한 메모리 공간을 대체하는 역할을 수행합니다.
그럼 프로세스가 둘 이상이면 어떻게 될까요?
프로세스 별로 공간이 할당되는데 각각의 스왑 파일이 하드디스크에 별도로 저장이 됩니다.
그리고 실행 중인 프로세스에 따라서 메인 메모리는 스왑 파일에 맞춰 메모리 공간을 구성하게 됩니다.
위 그림처럼 프로세스 A가 실행을 멈추고, 프로세스 B를 실행한다고 해봅시다.
그러면 프로세스 A를 실행시키기 위한 데이터는 스왑 파일로 저장하게 됩니다.
그리고 프로세스 B를 실행시키기 위한 데이터가 들어있는 프로세스 B의 스왑 파일을 메인 메모리로 가져오게 됩니다.
이와 같은 과정을 반복적으로 수행하면서 프로세스는 각각 8GB씩 메모리를 할당받아가며 실행을 이어가는 것입니다.