가상 메모리의 백스테이지
지금까지 우리는 페이징과 TLB를 통해 '메모리 가상화' 개념에 대해 알아왔다. 하지만 여기엔 치명적인 물리적 제약이 있다. 바로 RAM 용량의 한계다. 내가 실행하려는 프로그램들의 합이 32GB인데, 내 컴퓨터의 물리 메모리가 16GB뿐이라면?
OS는 이 불가능한 상황을 해결하기 위해 메모리 계층 구조의 더 아래쪽, 즉 디스크(Disk)를 빌려오기로 했다. 당장 쓰지 않는 페이지를 디스크의 창고(Swap Space)로 잠시 치워두는 것이다. 오늘은 이 가혹한 물리적 한계를 극복하기 위한 스와핑(Swapping)의 메커니즘과 그 비용에 대해 알아 본다.
1. 스왑 공간 (Swap Space)
스와핑의 핵심은 간단하다. 물리 메모리에 다 들어가지 못하는 페이지들을 저장하기 위해 디스크(HDD/SSD)의 일정 영역을 스왑 공간(Swap Space)으로 예약해두는 것이다. OS는 메모리에 있는 페이지를 페이지 단위(Page-sized unit)로 이 공간에 읽고 쓴다.
| Swap Space |
이로써 프로세스는 물리 메모리 크기에 구애받지 않고, OS가 제공하는 광활한 가상 주소 공간을 마음껏 사용할 수 있게 된다.
2. 메커니즘: 페이지 폴트 (Page Fault)
그렇다면 CPU는 내가 요청한 데이터가 메모리에 있는지, 아니면 스왑 공간(디스크)에 있는지 어떻게 알까? 여기서 다시 한번 PTE (Page Table Entry)가 중요한 역할을 한다. (PTE 개념은 Paging 글을 참고하자)
Present Bit (존재 비트)
PTE에는 Present Bit라는 플래그가 추가된다.
| Present Bit |
1: 해당 페이지가 물리 메모리(DRAM)에 있다 (Page Hit).0: 해당 페이지가 물리 메모리에 없고 디스크에 있다.
페이지 폴트 처리 과정 (Page Fault Control Flow)
만약 프로그램이 Present Bit가 0인 페이지에 접근하려 하면 어떤 일이 벌어질까? 하드웨어는 이를 처리할 수 없어 페이지 폴트(Page Fault)라는 예외(Exception)를 발생시키고, OS의 페이지 폴트 핸들러(Page-Fault Handler)가 깨어난다. 처리 과정은 다음과 같다.
참조(Reference): CPU가 가상 주소를 참조했는데, PTE의 Present Bit가 0이다.
트랩(Trap): 하드웨어가 OS에게 제어권을 넘긴다(Page Fault).
검색(Check Storage): OS는 해당 페이지가 스왑 공간의 어디에 있는지 확인한다.
가져오기(Swap In): 디스크에서 해당 페이지를 읽어 물리 메모리의 빈 프레임(Free Frame)에 올린다. (만약 빈 공간이 없다면, 누군가를 쫓아내야 한다. 이는 아래에서 다룰 '교체 정책'의 영역이다.)
업데이트(Reset Page Table): PTE의 PFN을 업데이트하고, Present Bit를 1로 설정한다.
재실행(Retry): 중단되었던 명령어를 다시 실행한다. 이번에는 Page Hit가 발생한다.
3. 비용: 디스크는 너무 느리다
스와핑은 "무한한 메모리"라는 환상을 주지만, 그 대가는 혹독하다. 디스크는 메모리(DRAM)보다 약 10,000배 이상 느리기 때문이다.
유효 접근 시간 (Effective Access Time, EAT)
스와핑이 성능에 미치는 영향을 수식으로 보면 충격적이다.
($p$: 페이지 폴트 확률, $T_{mem}$: 메모리 접근 시간, $T_{disk}$: 디스크 접근 시간)
예를 들어, 메모리 접근이 200ns, 디스크 접근이 8ms(8,000,000ns)라고 가정하자. 만약 1,000번 중 1번만 페이지 폴트가 발생해도($p=0.001$), 평균 접근 시간은 8.2us가 된다. 이는 원래 속도보다 40배나 느려지는 결과다.
성능 저하를 10% 이내로 막으려면, 페이지 폴트는 400,000번의 메모리 접근 중 단 1번만 허용되어야 한다. 즉, 스와핑은 가능하면 일어나지 말아야 한다.
4. 최적화 방법: 스왑 데몬과 워킹
OS는 스왑 비용을 줄이고 시스템 멜트다운을 방지하기 위해 몇 가지 똑똑한 전략을 사용한다.
스왑 데몬 (Swap Daemon): OS는 메모리가 꽉 찰 때까지 기다렸다가 부랴부랴 비우지 않는다. 대신 High Watermark(HW)와 Low Watermark(LW)라는 기준선을 두고, 여유 메모리가 LW 밑으로 떨어지면 백그라운드 스레드(Swap Daemon)가 깨어나 미리미리 페이지를 회수(Reclaim)하여 여유 공간을 확보해 둔다.
워킹 셋 (Working Set) 알고리즘: 만약 실행 중인 프로세스들의 메모리 요구량이 물리 메모리를 초과하면, 페이지를 넣고 빼느라 바쁜 쓰레싱(Thrashing) 현상이 발생할 수 있다. 이를 막기 위해 OS는 프로세스가 원활히 수행되기 위해 필요한 최소한의 페이지 집합, 즉 작업 집합(Working Set)을 파악한다. 이 공간이 확보되지 않으면 아예 프로세스를 실행하지 않고 대기시켜 전체 시스템의 붕괴를 예방한다.
| Thrashing |
5. 그렇다면 누구를 희생시킬까?
지난 기록에서 우리는 스와핑(Swapping)을 통해 물리 메모리의 한계를 극복하는 메커니즘을 살펴보았다. 디스크라는 거대한 창고를 빌려 쓰는 것까지는 좋았으나, 이제 더 본질적인 문제가 남았다. 창고(디스크)에서 새 물건을 가져오려는데 방(메모리)이 꽉 차 있다면, "방에 있는 누구를 내보내야 하는가?"
이 결정은 매우 중요하다. 만약 방금 쫓아낸 페이지를 1초 뒤에 다시 찾는다면, 우리는 또다시 끔찍하게 느린 디스크에 접근해야 하기 때문이다. 바로 이어서 페이지 교체 알고리즘(Replacement Policy)을 정리해 본다.
6. 목표: 미래를 맞혀라 (Optimal Policy)
교체 알고리즘의 목표는 명확하다. 캐시 미스(Page Fault)를 최소화하는 것이다. 이를 달성하기 위한 가장 이상적인 전략은 무엇일까?
The Optimal Replacement Policy
이론적으로 가장 완벽한 방법은 "앞으로 가장 오랫동안 사용되지 않을 페이지를 쫓아내는 것"이다.
예를 들어, 페이지 A는 1초 뒤에 필요하고 B는 100초 뒤에 필요하다면, 당연히 B를 내보내야 한다.
하지만 이것은 불가능하다. OS는 미래를 볼 수 있는 수정구슬을 가지고 있지 않기 때문이다. 따라서 Optimal Policy는 실제 구현할 수 있는 알고리즘이 아니라, 다른 알고리즘들의 성능을 평가하는 비교 기준(Benchmark)으로만 사용된다.
| Optimal Policy Tracing |
7. 단순한 접근: FIFO와 Random
미래를 알 수 없다면, 가장 단순한 규칙을 적용해보면 어떨까?
FIFO (First-In, First-Out)
가장 먼저 들어온 페이지를 가장 먼저 내보낸다. 구현이 매우 쉽다.
하지만 치명적인 문제가 있다. Belady's Anomaly라고 불리는 현상인데, "오래 머물러있었다"는 점이 "앞으로 사용되지 않을 가능성이 높다"는 논리가 성립하지 않아 발생한다. 이로 인해 상식적으로 메모리 크기(프레임 수)를 늘려주면 성능이 좋아져야 하는데, FIFO에서는 오히려 페이지 폴트가 늘어나는 기이한 현상이 발생하기도 한다.
| FIFO Tracing |
Random (무작위)
그냥 아무거나 찍어서 내보낸다. 운이 좋으면 최적(Optimal)에 가까울 수도 있지만, 운이 나쁘면 중요한 페이지를 계속 내쫓아 성능이 곤두박질친다. 시스템의 성능을 '운'에 맡길 수는 없다.
| Random Tracing |
| Random Performance |
8. 역사를 통해 배우다: LRU (Least Recently Used)
미래를 알 수 없다면, 과거(History)를 보면 된다. 이것이 현대 교체 알고리즘의 핵심 철학이다.
LRU의 논리
지역성(Locality)의 원리에 따르면, "최근에 사용된 페이지는 곧 다시 사용될 확률이 높다."
그렇다면 반대로, "가장 오랫동안 사용되지 않은(Least Recently Used) 페이지는 앞으로도 사용되지 않을 것이다"라고 가정할 수 있다.
| LRU Tracing |
LRU는 워크로드에 따라 다르지만, 일반적으로 Optimal에 근접하는 훌륭한 성능을 보여준다. 하지만 문제는 구현 비용(Cost)이다.
가장 오래된 녀석을 찾으려면, 메모리에 접근할 때마다 타임스탬프를 찍거나 리스트의 순서를 바꿔줘야 한다.
메모리 접근은 초당 수억 번 일어나는데, 그때마다 이런 부가 작업을 하는 것은 하드웨어적으로 불가능에 가깝다.
4. 현실적인 방법: 시계 알고리즘 (Clock Algorithm)
완벽한 LRU는 너무 비싸다. 그래서 OS는 LRU를 흉내 내는(Approximating) 가성비 좋은 방법을 찾아냈다. 바로 시계 알고리즘(Clock Algorithm)이다.
동작 원리
하드웨어의 지원을 받아 각 페이지에 Reference Bit(참조 비트)를 하나 둔다.
페이지들이 원형(시계)으로 배치되어 있고, '시계바늘(Hand)'이 페이지를 가리킨다.
누군가를 쫓아내야 할 때, 시계바늘이 돌면서 검사한다.
Reference Bit == 1: "어? 최근에 썼네?" -> 비트를 0으로 초기화하고 기회를 준 뒤 다음 페이지로 넘어간다.
Reference Bit == 0: "최근에 안 썼구나!" -> 이 페이지를 희생자(Victim)로 선정한다.
이 방식은 LRU처럼 완벽하게 순서를 세우진 않지만, "최근에 한 번이라도 쓰였는지"를 판단하여 자주 쓰이는 페이지를 살려둔다. 성능은 LRU에 버금가면서 구현 비용은 훨씬 저렴하다.
| Performance in 80-20 Workload |
5. 또 하나의 고려사항: 더러운 페이지 (Dirty Page)
누구를 쫓아낼지 결정할 때 고려해야 할 것이 하나 더 있다. 바로 페이지의 오염 여부(Dirty Bit)다.
Clean Page: 디스크에서 읽어온 후 수정되지 않은 페이지. 그냥 버리면 된다(덮어쓰기 불필요).
Dirty Page: 메모리에서 내용이 수정된 페이지. 쫓아내려면 변경된 내용을 디스크에 다시 써야(Write-back) 한다.
디스크 쓰기 작업은 매우 비싸다. 따라서 교체 알고리즘은 가능하면 Clean Page를 우선적으로 쫓아내고, Dirty Page는 최대한 메모리에 데리고 있으려 한다. (다음과 같이 쓰기 작업들을 메모리에 모아두었다가 한번에 처리한다.)
마치며
스와핑은 결국 "디스크의 공간을 빌려 메모리 부족을 해결하되, 접근 속도(시간)을 양보하는 트레이드오프(trade-off)"다.
이 거래가 손해가 되지 않으려면, 우리는 정말로 자주 쓰지 않는 페이지들만 골라서 디스크로 보내야 한다. 즉, "누구를 쫓아낼 것인가(Victim Selection)"가 시스템 성능의 핵심 열쇠가 되었다.
지금까지 가상 메모리의 개념부터 페이징, TLB, 그리고 스와핑까지 긴 여정을 달려왔다. 이 지식들은 단순히 OS를 이해하는 것을 넘어, 대규모 시스템의 성능 병목을 진단하고 최적화하는 데 중요한 통찰력을 제공할 것이다.
추천글:
[운영체제] Operating System 전체 포스팅 모음집
[운영체제] 가상 메모리(Virtual Memory) | 운영체제는 어떻게 프로세스에게 독점 공간을 제공할까?
[운영체제] 페이징(Paging) | Page Table, Copy-on-Write, TLB
[운영체제] Advanced Page Table | Multi-level Page Table, Hybrid approach, Inverted Page Table, Huge Page