가상 메모리는 밥 벌어 먹고 사는데 정말 잘 공부해야겠다고 느낀다.
기본 개념
메모리를 관리 할 때는 실제 메모리에 담겨있는 내용이 있고 또 그 메모리에 접근 할 주소로 나누어진다. 우리는 주소를 논할 것인데, 이 주소는 물리 주소와 가상 주소를 구분하는데에서 출발한다.
- 물리 주소는 컴퓨터의 램에 있는 실제 주소를 말한다. 하드웨어적으로 정해진 진짜 주소이다.
- 가상 주소는 CPU나 프로그램이 사용하는 논리적인 주소이다.
게임과 연결시켜 이야기를 해보겠다. 게임을 실행하면 운영체제는 프로세스라는 독립된 실행 단위를 만든다. 운영체제는 이 프로세스에게 PC에 장착된 램을 온전히 다 쓰는 것처럼 행하도록 가상 주소 공간을 배정한다.
게임의 코드 내에서 변수 선언이나 객체를 생성 할 때, 이 내용들은 가상 주소 어딘가에 할당이 이루어진다.
- 유니티의 경우 제일 흔한 오류인 NullReferenceExepction은 종종 0x00000000 같은 주소를 참조하려 할 때 발생한다. 이 주소가 바로 물리주소가 아닌 프로세스가 갖는 가상 주소이다.
이 처럼 프로그램은 가상의 지도를 보고 실행되고, 실제 물리 메모리는 운영체제가 주관으로 관리하고 있다.
앞의 챕터 글에서 간략히 다루었지만 가상 주소에서 물리 주소의 변환은 CPU와 MMU(메모리 관리 장치)가 변환을 수행한다. 구체적으로,
- CPU : 가상 주소 0x1000에 위치한 데이터 요청
- MMU : 이 요청을 받아, 운영체제가 사전에 작성해둔 페이지 테이블을 참조한다.
- 가상 주소 0x1000에 해당하는 내용은 물리 주소 0x8000에 있음을 확인한다고 하면, 실제 물리 메모리의 주소로 접근하여 데이터를 가져오게 된다.
이 과정에서 CPU(내지 프로그램)은 가상의 주소만 알고, 실제 변환은 MMU가 운영체제의 지휘 아래 수행하는 협업이다.
필요성
모든 프로세스는 각자 자신만의 독립된 가상 주소 공간을 받는다. 왜 이런 번거로운 변환이 필요 할까?
- 보호 : 프로세스끼리 서로의 공간을 침범 할 수 없도록 격리한다.
- 만약 게임의 오류로 인해 음악 듣겠다고 켜둔 크롬에 영향이 가면 안될 것이다. 이 내용이 달성 가능한 것은 가상 메모리가 있기 때문이다. (물론 램 100% 점유 내지 다른 이유로는 그럴 수 있다)
- 간결한 관리 : 모든 프로세스는 가상 메모리 시스템 덕분에 정해진 위치에서 코드 실행을 시작한다고 가정 할 수 있다. 링커와 로더가 프로그램을 만들고 실행하는데 이러한 가정을 하고 시작하는 것이 실행 난이도를 크게 감소 시킨다.
- 운영체제는 실제 물리 메모리의 공간이 흩어져있더라도 이것을 연속적인 가상 주소 공간처럼 보이게 할 수 있다.
- 이 내용에 시비를 걸었는데, 그 결과를 하단에 후술한다.
핵심 기능
‘가상’ 이라는 이름이 붙은 근본적인 이유가 되는 기능을 논해보겠다.
게임을 어찌어찌 만들었는데 필요한 걸 전부 로드하면 메모리가 20GB씩 필요하다고 하자. 근데 물리 RAM은 16GB에 불과하다. 여기서 우리가 한번씩 C드라이브에서 구경했을법한 pagefile.sys나 swap 파티션의 존재 의의가 나온다.
페이징이라고 불리는 이것은 물리 RAM의 가용공간이 없는 경우 당장 사용하지 않는 메모리 페이지를 선택한다음 디스크에 내보낸다. 이제 비어있는 공간에서 새로 요구한 데이터 중 일부를 다시 적재한다.
어떤 프로그램을 진행하면서 1-2초간 멈칫하는 현상을 경험했다면 그 동안 PC가 페이징을 수행했을 여지가 크다. 페이징은 Page Fault로 인해 발생하는데, 디스크까지 내려가는 시점에서 속도가 확 내려가는 포인트가 있기 때문이다.
이 모든 것을 실현하는 핵심인 주소 변환의 구체적인 과정을 살펴보자. 가상 메모리는 페이지라는 고정된 크기의 조각으로 관리된다.
- 가상 메모리는 가상 페이지에 대응, 물리 메모리는 물리 페이지에 대응한다. 이 매핑 정보를 저장하는 자료구조의 이름이 페이지 테이블이다.
이 페이지 테이블도 결국 램에 저장되어있다. MMU가 램에 접근해서 페이지 테이블을 한번 열람하고, 페이지 테이블 내의 정보를 통해 실제 물리 주소에 접근함으로써 실제 데이터를 얻는데는 두 번의 램 접근이 필요했다. 모든 메모리 접근이 이 모양이 되면 프로그램 성능에는 상당히 치명적이 된다.
TLB (Translation Lookaside Buffer)
변환, 옆에 두고 훔쳐보는, 임시 저장소
이것도 Page Fault과 유사하게 다음 접근에 대한 캐싱을 위해 만들어졌다. Page Fault와 TLB Miss가 대응하기 때문이다. TLB에 정보가 없는 경우 TLB Miss가 일어나고 그 이후에 페이지 테이블 참조가 일어난다.
MMU와 TLB 덕분에 주소 변환이 매우 빠르다는 것을 알았지만, 64비트 시스템해서 2^64의 가상 주소 공간은 정말 크다. 이런 거대한 페이지 테이블을 실제 RAM에 저장하기 위해선 요구 페이징(Demand Paging, 어떻게 번역이 이렇게..)을 이용한다.
필요 할 때만 불러온다는 아이디어에 더해, 정말 큰 테이블을 통째로 적재하는게 아니라 이 테이블을 트리 구조로 여러 단계로 나눈다. 이게 멀티 레벨 페이지 테이블이다.
- 1단계 테이블은 보다 큰 범위의 주소 영역을 가리킨다.
- 2단계 테이블도 1단계 만큼은 아니지만 제법 큰 범위의 주소 영역을 가리킨다.
- 1단계가 나라였다면 2단계는 시, 도, 구 정도로 생각 할 수 있다.
필요 할 때만 불러온다는 아이디어는 여기서 이용된다 : 특정한 메모리 영역을 사용하지 않는다면, 해당 하는 페이지 테이블은 할당하지 않는다. 이렇게 해서 실제 사용하는 영역의 페이지 테이블만 RAM에 유지 할 수 있다.
멀티 레벨 페이지 테이블 덕분에 메모리 절약 문제를 해결했지만 속도면에서 다소 손해를 본다.
TLB Miss로 인한 페이지 테이블 참조 필요로 인해 접근하게 되면, 이 나눠진 테이블 당 새로운 접근 횟수가 카운트 된다. 그래서 2단계 테이블이 있다면 3번이 소요된다. 그래도 TLB가 있기 때문에 다음 접근은 한 번의 램 접근 (어쨌든 다시 두 번)으로 줄어들었다.
Page Fault
Page Fault 여부는 MMU가 띄우는 것이고, 발생하면 CPU를 멈추고 커널에게 제어권을 넘긴다. 이후 페이지 폴트 핸들러 실행을 통해 예외 처리가 이루어진다.
- 디스크 I/O 시작 : 디스크에서 필요한 데이터를 찾아 RAM으로 읽어오도록 디스크 컨트롤러에게 명령한다.
- CPU 휴식 : 디스크가 데이터를 읽어오는 동안 CPU를 마냥 놀릴 수 없으니, 커널은 기존의 진행하던 프로세스를 잠시 대기 상태로 만든다.
- 다른 일 하기 : 그 사이에 CPU가 다른 일을 하도록 문맥 전환이 수행된다.
- 작업 복귀 : 마침내 디스크 작업이 끝나면 커널은 해당 페이지를 RAM의 빈 곳에 적재하고, 페이지 테이블에 있는 데이터 위치를 수정한다.
커널은 대기 중이었던 프로세스를 다시 실행 상태로 바꾸고, 기존에 실패했던 바로 그 명령을 다시 진행한다.
mmap
보다 용량이 큰 내용들을 가져오려면 mmap을 사용한다. 간략한 과정을 살펴보면 :
- 매핑 : OS는 가상 주소 0xA부터 0xC까지의 영역은 보조 디스크 내 field.bin 파일의 0 - 1000 바이트 내용과 연결된다고 페이지 테이블에 표시만 해둔다.
- 페이지 폴트 : 게임 코드가 가상 주소에 접근한다. 페이지 폴트가 발생한다.
- 커널 개입 : 이 페이지는 field.bin에 연결되어있음을 확인하고, 원본 파일을 가져와 RAM에 적재한다.
Flag Bits
페이지 테이블의 단위 내용에는 플래그가 몇 개 있는데, 이 내용들을 소개한다.
Dirty Bit (수정 여부 판단 비트)
CPU가 특정 페이지에 쓰기 작업을 수행하면 MMU 하드웨어는 이 비트를 0에서 1로 자동으로 설정한다. OS가 특정 페이지를 RAM에서 쫓아내야 하는 상황이 되면 이 비트를 확인한다.
- 0이라면 : 페이지가 수정된적이 없으니 추가 작업을 수행하지 않고 RAM에서 비운다.
- 1이라면 : 페이지가 수정된적이 있기 때문에 이 데이터를 반드시 저장해야한다.
Present Bit (존재 여부 확인 비트)
MMU가 주소 변환을 시도 할 때 현재 페이지가 RAM에 있는지, 디스크에 있는지를 알려주는 플래그 비트이다.
- 1이라면 : RAM에 존재하기 때문에 MMU는 페이지 테이블 항목에서 물리 주소를 가져와 반환 할 수 있다.
- 0이라면 : 페이지 폴트를 통해 CPU 중지 → OS 커널 호출 → 커널 주관의 페이지 폴트 핸들러 호출이 일어난다.
Accessed Bit (접근 여부 확인 비트) & 페이지 교체 정책
페이지 폴트가 일어나는 구체적인 과정을 알아보았는데, 실제로 어떤 페이지를 RAM에서 빼야하는지 정하는 기준이 있다. LRU 정책으로, 가장 오랫동안 참조 되지 않은 페이지를 제거 대상으로 삼는 방식이다.
하지만 진짜 LRU 완성은 모든 메모리 접근을 추적해서 마지막 사용 시간을 알아야하는데, 이러면 배보다 배꼽이 커지는 수준의 오버헤드가 일어날 것이다. 현대 OS는 LRU 비스무리한 것을 사용한다.
Accessed Bit는 여기서 사용된다. 시계 알고리즘 내지 Second-Chance 알고리즘이라 불리는 이 내용은
원형 큐처럼 존재하는 모든 페이지 프레임을 순회하며 Accessed Bit만을 확인한다.
- 1이라면 : 최근 사용된 상황으로, 값을 0으로 전환하고 다음 페이지를 확인한다.
- 0이라면 : 대상이 0으로 전환된 이후 다시 사용된적이 없다는게 되기 때문에, 이 페이지는 대상자가 되어 RAM에서 이탈하게 된다.
가설 논하기
운영체제는 실제 물리 메모리가 흩어져 있더라도, 연속적인 가상 주소 공간처럼 보이게 할 수 있다고 했다.
근데 어쩌다가 이렇게 흘러들어왔을까? 프로세스 당 연속적으로 배치되게 끔 하는 기술은 이미 시도 되었을 것 같다.
가상 메모리와 페이징 기법 등장 전까지는 실제로 사용되었던 메모리 관리 방식이었다.
연속 메모리 할당이라고 불리는 이 기술은 프로세스가 실행 될 때 필요한 만큼의 연속된 물리 메모리 덩어리를 통째로 할당하곤 했다.
- 고정 크기 분할 : RAM을 고정된 크기로 쪼개놓고, 프로세스가 들어오면 맞는 크기의 파티션에 할당한다.
- 가변 크기 분할 : 프로세스가 요청하는 RAM 크기에 따라 할당한다.
트레이드오프? 실제 사용에 적절하지 못했다. 구체적으로..
외부 단편화 :
상황 [||||| 5MB] | [|||||||||| 10MB] | [|||||||| 8MB] | [ 7MB 여유]
에서 10MB가 작업을 마치고 종료되었다.
상황 [||||| 5MB] | [ 10MB 여유] | [|||||||| 8MB] | [ 7MB 여유]
이제 RAM은 17MB의 여유가 있다. 하지만 12MB의 프로세스는 실행 될 수 없다. 연속되어 있지 못하기 때문이다.
이처럼 사용 중인 메모리 블록에 빈 공간이 흩어져 총량이 여유로움에도 사용 할 수 없는 현상을 외부 단편화라 한다.
압축 :
앞의 문제를 해결하기 위한 방법인 압축은 OS가 Stop-the-world 상태로 프로세스 전체 중지를 때린다음,
[||||| 5MB] | ****[ 10MB 여유] | [|||||||| 8MB] | [ 7MB 여유] 를
[||||| 5MB] | ****[|||||||| 8MB] | [ 7MB 여유] | [ 10MB 여유] 로 전환한다.
이젠 12MB의 프로세스를 받을 수 있다. 근데, 이렇게 앞으로 밀어 붙이는 작업은 수 기가바이트에 달하는 메모리를 통째로 복사(memcpy)하는 작업이다. 이건 정말 많이 느리고, 말 그대로 모든 프로세스는 정지한다. 엄청난 성능저하라는 것이다.
논외 : 내부 단편화
고정 크기 분할 상황에서 일어날 수 있는 일로, 4MB 배정에 3MB 프로세스를 배정하면 남은 1MB는 속할지언정, 어디에도 사용되지 못한다.
그럼 압축 비용만 해결하면 어느정도 해결이 된다. 천문학적인 돈을 쓸 수 있다면, memcpy를 1 클럭 사이클에 뚝딱 끝내던가, RAM에 어디가 어느만큼 비어있는지에 대한 단편화를 담당할 칩도 고려 할 수 있다.
하지만 이런 내용들이 학습 곡선이 다소 가파르게 되었지만 페이징 기법을 통해 유사한 효과를 낼 수 있게 되었다.
페이징
앞에서 이야기 했던 압축은 RAM에서 RAM으로 조각을 모으는 행위에 가까웠는데, 페이징은 좀 다르다. 페이징은 데이터를 직접 옮기는 대신 페이지 테이블을 수정함으로써 연속적 공간을 달성한다.
페이징 방식에서 10MB가 필요해지면, RAM의 어디에 있든 비어있는 4KB 페이지 2560개를 찾아다가 배정하게 된다. 핵심은 이 흩어진 2560개의 물리 프레임을 연속적인 공간으로 보이게 하기 위한 작업인 것인데, 이게 페이지 테이블 수정으로써 가능하게 된다.
'컴퓨터 이론 > CS:APP' 카테고리의 다른 글
| 오늘만 푸는 초강력 가상 메모리 프롬프트 [CSAPP Chap 9] (1) | 2025.10.23 |
|---|---|
| 예외처리 #3 [CSAPP Chap 8] (0) | 2025.10.20 |
| 예외처리 #2 [CSAPP Chap 8] (0) | 2025.10.20 |
| 예외처리 #1 [CSAPP Chap 8] (0) | 2025.10.20 |
| 링커 #2 [CSAPP Chap 7] (0) | 2025.10.20 |