저번 글에서는 컴파일, 런타임, 로딩을 이해하는 시간을 가졌다. 이 세 용어를 확실하게 이해했다면 이번 글은 술술 읽으면서 넘길 수 있을 것이다. '메모리 계층 구조'부터 이 글까지는 '메모리 관리'파트를 이해하기 위한 단계라고 생각하면 된다. 우리가 앞으로 배울 내용을 한 문장으로 요약하면 아래와 같다. 참고로 메모리는 캐시부터 메인 메모리까지를 통칭하는데 이 글과 '메모리 파트'에서는 주로 메인 메모리로 쓰인다.
한정된 메모리 공간을 어떻게 하면 효율적으로 관리할 수 있을까?
CPU가 직접적으로 접근할 수 있는 저장 장치는 메모리다. 그러나 메모리 공간은 한정적이다. 여러 개의 프로그램을 실행해야 하는 운영체제는 협소한 메모리 공간을 최대한 효율적으로 관리해서 CPU가 필요한 데이터를 빨리 끌어올 수 있도록 하는 것이 목표다. CPU는 주소값을 통해 메모리에 접근하기 때문에 앞으로 주소 공간을 나타내는 그림을 자주 보게 될 것이다.
주소 공간(Address Space)
메모리의 주소 공간은 위와 같이 표현한다. CPU는 주소값을 통해 메모리에 접근할 수 있다. 위 예시에서 0번지 주소에 대응하는 명령어는 01001111 임을 알 수 있다. 그리고 메모리의 단위는 비트(bit)가 아니라 바이트(byte)다. 이를 꼭 기억하고 있기를 바란다. 즉, 위 그림에서 주소 한 칸은 1 byte의 데이터를 저장할 수 있다. 주소 공간이 2^32 라는 것은, 주소의 길이가 32비트라는 것이고, 이는 위 그림에서 저장할 수 있는 칸이 2^32개라는 뜻이다.
물리적 주소 (Physical Address)
우리는 메모리가 저장장치에 불과하다는 것을 잊으면 안 된다. 메모리의 기능은 '데이터를 저장하는 컨테이너' 그 이상 그 이하도 아니다. 단순히 크기가 굉~~장히 큰 배열로 생각해도 무방하다. 배열이기 때문에 인덱스 값을 가지는데, 이 인덱스 값을 '물리적 주소'라고 한다. 물리적 주소는 메모리 자체의 인덱스다. 위에 메인 메모리 그림에서 각 주소의 인덱스가 물리적 주소에 해당한다.
논리적 주소(Logical Address)
논리적 주소는 쉽게 말해서 CPU 입장에서의 메모리 주소, 또는 프로그램 실행 중에 CPU가 생성하는 주소다. 따라서 가상 주소라고도 한다. 논리적 주소를 사용하는 이유는 여러 가지가 있겠지만 나는 멀티 프로세싱 때문이라고 생각한다. 논리적 주소가 없다면 모든 프로세스가 물리적 주소에 직접 접근할 수 있게 된다. 이러면 아무런 안전 장치가 없기 때문에 프로세스끼리 주소 공간을 침범할 수 있는 문제가 발생한다.
같은 공간을 공유할 때 서로가 침범하지 않게 하려면 어떻게 해야할까? '여기까지가 나의 공간이야'라는 표시를 하면 된다. 자기가 사용하는 주소 공간을 표현하기 위해서는 시작 주소와 크기만 있으면 된다. 왜냐하면 '시작 주소 + 크기'가 끝 주소가 되기 때문에 프로세스의 시작 주소와 끝 주소 사이를 침범하는 상황이 발생하면 에러를 발생시키면 된다. 반대로 프로세스가 접근하고자 하는 주소가 시작 주소와 끝 주소 사이에 있으면 접근할 수 있다. 이렇게 모든 프로세스는 '시작 주소'와 '크기'를 나타내는 레지스터가 정의돼 있는데, 각각 'Base Register'와 'Limit Register'로 부른다.
이제 모든 프로세스는 Base Register로부터 물리적 주소에서의 시작 주소를 알 수 있기 때문에 항상 0번지 주소부터 시작해도 아무 문제가 없다. 따라서 새로운 프로세스가 생성되면 항상 0번지 주소부터 시작한다. 이를 논리적 주소라고 한다. 간단한 예를 한번 들어 보자. 논리적 주소가 5, base register 값이 1320면 물리적 주소는? 단순 덧셈 문제다. 정답은 1325!
프로세스가 물리적 주소에 접근하려면 논리적 주소를 물리적 주소로 Mapping시키는 과정이 필요한데, MMU(Memory Management Unit)가 이 역할을 한다. 주소 매핑 과정은 굉~~장히 빈번하게 일어나므로 MMU는 하드웨어적으로 구현되어 있다. MMU의 역할은 굉장히 간단하다. 아래 그림과 같이 그냥 논리적 주소에 base register 값을 더해주기만 하면 된다. Relocation Register 와 Base Register는 같은 것으로 생각하면 된다.
주소의 매핑은 '주소 할당'과 같다. 논리적 주소를 물리적 주소로 매핑시킨다는 것은 논리적 주소에게 새로운 물리적 주소를 '할당'한다는 뜻이다. 그리고 위와 같이 한 프로세스의 모든 논리적 주소에 동일한 base register를 더하는 주소 할당을 '연속 할당'이라고 한다. 모든 프로세스는 논리적 주소가 0번지부터 시작해서 차례대로 증가하기 때문에, 물리적 주소도 시작 주소만 다르지 연속적으로 배치되어 있다.
여기까지 잘 이해했다면, 주소 바인딩은 거저먹기다. 우선 주소 바인딩을 한 문장으로 설명하면 아래와 같다.
논리적 주소를 물리적 주소로 언제 Mapping 시킬 것인가?
이를 기억해놓자. 이제 아래와 같은 초간단 프로그램을 짰다고 가정해보자.
data = 10;
이는 data 메모리 번지에 10을 write하는 프로그램이다. 그런데, 실제로 CPU가 명령어를 수행하면 아래와 같이 된다.
store $98000 10 // $98000은 data의 논리적 주소
위에서 $98000은 논리적 주소를 뜻한다. 아까 주소 바인딩은 논리적 주소가 물리적 주소로 Mapping 되는 시점이라고 말했다. 위 예제에서 data의 물리적 주소는 compile time, load time, execution time에 결정될 수 있다. Relocation Register 값은 100000이라고 하자. 즉, 물리적 주소값은 198000이라는 뜻이다. 각각 어떻게 달라지는지 살펴보자.
1. Compile Time Binding
프로그램이 컴파일 될 때 주소가 매핑되는 경우다. 위 그림을 보면 메모리에 올라가기 전에 이미 물리적 주소가 결정됐다. 이건 말도 안 된다. 간단한 예를 들어 보자. 게임 회사에서 어떤 게임의 주소 바인딩을 compile time에 했다고 가정해보자. 바인딩 된 주소는 위 그림처럼 198000 ~ 1000000 이다. 앞의 16진수 표기는 생략하겠다. 이 게임을 설치한 사람들은 어떻게 될까? 물리적 주소에서 198000 ~ 1000000 사이가 무조건 비어있어야만 게임을 실행할 수 있게 된다. 왜냐하면 물리적 주소가 이미 정해졌기 때문이다. Compile time에 주소 바인딩을 한다는 것은 멀티 프로그래밍을 포기한다는 것과 같은 말이다. 약간 헷갈린다면 프로그램 내부에서 사용하는 주소와 물리적 주소가 같다는 것으로 이해해도 된다. 이렇게 되면 논리적 주소의 존재 이유가 없다.
그렇다면 compile time에 메모리 번지가 결정돼도 상관 없는 상황은 어떤 경우일까? 메모리에 오직 한 개의 프로그램만 올라가면 가능하다. 예를 들면 아두이노 같은 경우 내가 만든 프로그램만 돌아가기 때문에 compile time에 메모리 번지가 결정돼도 상관없다.
2. Load Time Binding
메모리에 로딩할 때 바꾸는 방법이다. 우선 논리적 주소와 물리적 주소를 분리시켰다는 점에서 Compile Time Binding 과는 다르게 멀티프로그래밍이 가능하다. Relocation register가 제 기능을 하기 때문에 프로세스의 주소가 메모리의 어디에나 위치할 수 있기 때문이다. 그런데 어떤 문제가 있을까? 프로그램 안에 있는 모든 주소를 전부 relocation 시켜야 하기 때문에 로딩할 때 시간이 오래 걸리는 문제가 발생한다. 또한 한 번 Relocation 되면 바뀌는 일이 없기 때문에 만약에 컴파일러가 주소를 특정할 수 없는 경우 전부 다시 Relocation 해야한다. 이게 무슨 상황이냐 하면, 프로세스의 크기가 커져서 불가피하게 다른 프로세스의 영역을 침범하게 될 경우 전체 코드를 복사해서 여유가 있는 공간에 붙여넣기 하는 상황을 말한다. 여러모로 오버헤드가 심하다. 그러면 남은 방법은? 과연 한 번에 다 Relocation을 할 필요가 있을까? 실행 중에 하나씩 Relocation을 하면 안 될까? 이 방법이 바로 Execution Time Binding이다.
3. Execution Time Binding
마지막 방법은 실행할 때 바꾸는 방법이다. 쉽게 말해서 code level 실행시 relocation을 해주는 것이다. code level이란 '코드 한줄'로 생각하면 된다. 위 그림의 경우 'data = 10;'이 실행될 때 비로소 논리적 주소를 물리적 주소로 바인딩 해주는 것을 확인할 수 있다. 그림에서 보이는 남색깔 동그라미는 '덧셈'을 뜻한다.
Load Time Binding vs Execution Time Binding
두 가지 방법이 헷갈릴 수 있다. 로드 타임은 메모리에 로딩할 때 주소 변환을 미리 다 해놓는 방법이고, 실행 타임은 프로그램을 실행해서 그 코드가 실행될 때마다 주소를 변환하는 방법이다. 로드 타임의 경우 한 번만 바꿔놓으면 그 다음부터는 해당 주소로 접근하면 되는데, 실행 타임은 변환 작업을 반복 수행해야 한다.
로드 타임의 경우 한 번에 변환을 실행하기 때문에 Relocation은 소프트웨어적으로 실행된다. 따라서 한 번 로딩할 때 시간이 오래 걸린다. 하지만 실행 타임은 Relocation이 매우 빈번하게 일어나기 때문에 하드웨어적인 도움이 필요하다. 이때 쓰는 것이 MMU다.
(추가) 32 bit CPU vs 64 bit CPU
이건 추가 설명인데, CPU마다 주소 체계가 다른 이유는 프로세서가 한 번에 처리할 수 있는 데이터의 크기가 다르기 때문이다. 64bit CPU와 32bit CPU의 차이는 뭘까? 전자는 프로세서가 한 번에 처리할 수 있는 데이터의 양이 64bit라는 뜻이고, 후자는 32bit라는 뜻이다. 이렇게 프로세서가 한 번에 처리할 수 있는 데이터의 양을 word라고 한다. 64 bit CPU의 경우 1 word의 크기는 8byte(=64bit)고, 메인 메모리에서 한 번에 8줄을 읽을 수 있다는 뜻이다. Word의 크기는 CPU의 데이터 단위라고 생각하면 된다. 또한 word의 크기는 CPU Regisger의 크기와 같다.
그러면 CPU Register 크기에 따라 할당할 수 있는 주소의 범위는 어떻게 될까? 결론부터 말하자면 32 bit CPU에서는 2^32 만큼의 논리적 주소를 할당할 수 있고, 64 bit CPU에서는 2^64 만큼의 논리적 주소를 할당할 수 있다. 이게 아마 많이 헷갈릴 것으로 생각된다. 한 번에 이해했다면 다행이지만, 만약 헷갈렸다면 '주소 공간'의 이해가 덜 됐다는 뜻이다. 아까 위에서 메모리 단위는 byte라고 했었다. 즉, 주소 공간에서 한 주소에 저장 가능한 데이터의 크기가 1 byte라는 뜻이다. 그런데 모든 주소는 bit로 표현된다. 예를 들어 할당 가능한 주소가 2^32개라면 32 bit로 모든 주소를 나타낼 수 있고, 1 byte짜리 칸이 2^32개 있다는 뜻이다. 여기서 아주 흥미로운 결과가 도출되는데, 아래의 식을 먼저 보자.
32 bit
2^32 byte = 4 GB
32bit CPU에서 최대로 할당 가능한 주소는 4GB을 넘지 못한다. 따라서 32bit CPU를 사용하고 있다면 최대 4GB의 메모리만 사용할 수 있다. 그 이상 사용해도 CPU가 접근할 수 없기 때문에 없는 것이나 마찬가지다. 그렇다면 64 bit의 크기는 얼마나 될까?
64 bit
2^64 byte = 16 EB
혹시 엑사 바이트라고 들어보았는지..? 어마어마한 크기다. 사실 아직까지는 어드레싱(Addressing) 범위를 16EB까지 사용할 일이 없다. 따라서 실제로는 하위 48bit만 사용한다고 한다. 그러면 48bit는 크기가 얼마나 되는데?
48 bit
2^48 byte = 256 TB
아마 테라 바이트는 많이 들어봤을 것이다. 하지만 그래도 크다. 256TB 램이라... 들어본 적 있는지..? 사실 이게 '가상 메모리'의 핵심 아이디어다. 가상 메모리는 나중에 다시 다루니깐 우선 가볍게 읽고 넘어가길 바란다.
가상 메모리(Virtual Memory) 아이디어
어떤 프로그램이 실행되려면 메모리에 적재되어야 한다. 그러면 램의 용량보다 훨씬 더 큰 프로그램은 실행시키지 못하는 것일까? 아주 유명한 게임인 GTA는 용량이 자그마치 80GB정도 된다. 그런데 유튜브에 검색만 해봐도 GTA 관련 영상이 많이 나오는 것을 볼 수 있다. 그러면 그 사람들은 전부 80GB 이상의 램을 사용하고 있는 것일까? 당연히 아니다.
해답은 CPU Register 크기에 있다. 아까 64 bit CPU는 256TB 범위의 논리적 주소를 할당할 수 있다고 했다. 논리적 주소가 강력한 이유는, 물리적 주소의 할당 범위보다 크기 때문이다. 우리는 용량이 아주 큰 SSD라는 놈이 있다는 것을 잊으면 안 된다. 가상 메모리는 쉽게 말해서 아래의 한 문장으로 설명할 수 있다.
SSD를 메모리처럼 사용하자~~
할당 주소의 범위가 램의 물리적 주소 범위를 넘어가면 SSD로 넘기자! 이게 가상 메모리의 핵심 아이디어다. CPU는 SSD에 직접 접근할 수 없지만 운영체제는 가능하다는거! 가상 메모리는 우선 이렇게만 이해하고 있자. 나중에 어떤 원리로 이게 가능한지 다시 알아볼 것이다.
오늘은 여기까지! 다음 글은 드디어 대망의 '메모리 관리' 파트다. 개인적으로 운영체제에서 가장 중요하다고 생각하는 부분이다. '메모리 계층 구조'부터 이 글까지 이해가 완료된 상태에서 보기를 바란다.
'CS > 운영체제' 카테고리의 다른 글
[ 운영체제 ] 메모리 관리2 - 페이지 테이블의 세 가지 구조 (0) | 2022.02.08 |
---|---|
[ 운영체제 ] 메모리 관리 1 - 페이징(Paging) (2) | 2022.02.04 |
[ 운영체제 ] 컴파일(Compile)과 링킹(Linking), 런타임(Runtime), 로딩(Loading) (0) | 2022.02.02 |
[ 운영체제 ] 메모리 계층 구조 (0) | 2022.02.01 |
[ 운영체제 ] Banker's Algorithm (0) | 2022.01.31 |