페이지 교체 알고리즘
FIFO·OPT·LRU·Clock·LFU의 교체 결정을 참조 문자열로 비교하고 페이지 폴트와 스래싱을 줄이는 기준을 익힙니다.
페이지 교체 알고리즘에서는 주소 변환, 테이블 갱신, 메모리/디스크 비용을 확인합니다.
FIFO는 들어온 순서, LRU는 최근 사용, Clock은 reference bit를 활용해 비용과 정확성 사이에서 타협한다.
- victim 후보
candidate victim 후보 메모리 부족
- FIFO/LRU/Clock
policy FIFO/LRU/Clock 교체 기준 선택
- write-back
dirty check write-back 수정 페이지 저장
- new page
install new page PTE와 TLB 갱신
물리 메모리가 부족할 때 어떤 페이지를 내보낼지 결정하는 것은 가상 메모리 성능의 핵심입니다.
잘못된 선택은 곧바로 페이지 폴트 증가로 이어지고, 극단적으로는 시스템 전체가 디스크 I/O에 매몰되어 멈추는 스래싱으로 이어집니다.
물리 메모리가 부족할 때 어떤 페이지를 내보낼지 결정하는 것은 가상 메모리 성능의 핵심입니다. 잘못된 선택은 곧바로 페이지 폴트 증가로 이어지고, 극단적으로는 시스템 전체가 디스크 I/O에 매몰되어 멈추는 스래싱으로 이어집니다.
- 참조 문자열 · frame 3개
· 0 · 1 · 2 · 0 · 3 · 0 · 4 같은 입력이어야 알고리즘을 공정하게 비교할 수 있다.
- FIFO
faults 들어온 순서만 사용
- LRU
faults 가장 오래 미사용 교체
- OPT
faults 미래를 아는 하한선
- 평가 기준
의 성능은 참조 문자열(Reference String)에 대한 페이지 폴트 횟수로 측정합니다. 스래싱
- FIFO 알고리즘
FIFO(First-In, First-Out)는 가장 먼저 메모리에 올라온 페이지를 가장 먼저 내보냅니다. 참조 문자열(Reference String)
- 최적 (OPT) 알고리즘
OPT(Optimal) 알고리즘은 앞으로 가장 오랫동안 사용되지 않을 페이지를 내보냅니다. 페이지 폴트 횟수
- 페이지 교체 알고리즘 기준
정확성 잘못된 교체 선택은 페이지 폴트를 늘리고, 심하면 시스템이 디스크 I/O에 매몰되는 스래싱으로 이어집니다. 비용 참조 문자열에서 발생한 페이지 폴트 횟수로 알고리즘 비용을 비교합니다. 확장성 구현 난도와 실제 근사 전략까지 함께 봐야 운영 비용을 판단할 수 있습니다. 예외 일반적으로 프레임 수가 많을수록 페이지 폴트가 줄어듭니다(단, FIFO는 예외).
- 정확성 잘못된 교체 선택
페이지 폴트를 늘리고, 심하면 시스템이 디스크 I/O에 매몰되는 스래싱으로 이어집니다.
- 비용 참조 문자열
발생한 페이지 폴트 횟수로 알고리즘 비용을 비교합니다.
- 확장성 구현 난도
실제 근사 전략까지 함께 봐야 운영 비용을 판단할 수 있습니다.
- 예외 일반적
프레임 수가 많을수록 페이지 폴트가 줄어듭니다(단, FIFO는 예외).
평가 기준
페이지 교체 알고리즘의 성능은 참조 문자열(Reference String)에 대한 페이지 폴트 횟수로 측정합니다.
참조 문자열은 프로세스가 접근하는 페이지 번호의 시퀀스입니다.
예: 참조 문자열 [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1], 프레임 수 3.
일반적으로 프레임 수가 많을수록 페이지 폴트가 줄어듭니다(단, FIFO는 예외).
FIFO 알고리즘
FIFO(First-In, First-Out)는 가장 먼저 메모리에 올라온 페이지를 가장 먼저 내보냅니다.
큐로 간단하게 구현할 수 있습니다.
from collections import deque
def fifo_simulate(pages, num_frames):
frames = deque(maxlen=num_frames)
frame_set = set()
page_faults = 0
for page in pages:
if page not in frame_set:
page_faults += 1
if len(frames) == num_frames:
evicted = frames.popleft()
frame_set.discard(evicted)
frames.append(page)
frame_set.add(page)
return page_faults
pages = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
print(f"FIFO 페이지 폴트 (3프레임): {fifo_simulate(pages, 3)}") # 15
print(f"FIFO 페이지 폴트 (4프레임): {fifo_simulate(pages, 4)}") # 10Belady의 모순 (Belady's Anomaly)
FIFO에는 기이한 현상이 있습니다.
프레임 수를 늘렸는데 오히려 페이지 폴트가 증가하는 경우가 발생합니다.
참조 문자열 [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5]에서:
- 3프레임: 9번 폴트
- 4프레임: 10번 폴트 (프레임을 늘렸는데 폴트 증가!)
직관에 완전히 어긋나는 결과입니다.
이 문제 때문에 FIFO는 실무에서 거의 사용되지 않습니다.
최적 (OPT) 알고리즘
OPT(Optimal) 알고리즘은 앞으로 가장 오랫동안 사용되지 않을 페이지를 내보냅니다.
모든 교체 알고리즘 중 페이지 폴트가 가장 적습니다.
Belady의 모순도 발생하지 않습니다.
하지만 미래의 접근 패턴을 알아야 하므로 실제로 구현할 수 없습니다.
다른 알고리즘의 성능을 비교하는 이론적 기준선(Baseline)으로만 사용됩니다.
def opt_simulate(pages, num_frames):
frames = []
page_faults = 0
for i, page in enumerate(pages):
if page not in frames:
page_faults += 1
if len(frames) == num_frames:
# 앞으로 가장 늦게 사용될 페이지를 교체
farthest = -1
victim = 0
for j, f in enumerate(frames):
try:
next_use = pages[i+1:].index(f)
except ValueError:
next_use = float('inf') # 다시 사용 안 됨
if next_use > farthest:
farthest = next_use
victim = j
frames[victim] = page
else:
frames.append(page)
return page_faults
pages = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
print(f"OPT 페이지 폴트 (3프레임): {opt_simulate(pages, 3)}") # 9LRU 알고리즘
LRU(Least Recently Used)는 가장 오래 전에 사용된 페이지를 내보냅니다.
과거의 접근 패턴으로 미래를 예측합니다.
"최근에 사용되지 않았으면 앞으로도 사용되지 않을 가능성이 높다"는 지역성 원리에 기반합니다.
LRU는 OPT에 가까운 성능을 보여주고, Belady의 모순도 발생하지 않습니다(스택 알고리즘이기 때문).
def lru_simulate(pages, num_frames):
frames = []
page_faults = 0
for page in pages:
if page in frames:
frames.remove(page)
frames.append(page) # 가장 최근 사용으로 이동
else:
page_faults += 1
if len(frames) >= num_frames:
frames.pop(0) # 가장 오래된 것 제거
frames.append(page)
return page_faults
pages = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
print(f"LRU 페이지 폴트 (3프레임): {lru_simulate(pages, 3)}") # 12LRU 구현의 어려움
완벽한 LRU는 모든 메모리 접근마다 순서 정보를 갱신해야 합니다.
타임스탬프 방식: 각 페이지 접근 시 현재 시각을 기록합니다.
교체 시 가장 작은 타임스탬프를 찾습니다.
모든 접근마다 메모리 쓰기가 필요하고, 교체 시 전체 탐색이 필요합니다.
스택(더블 링크드 리스트) 방식: 접근된 페이지를 리스트의 끝으로 옮깁니다.
교체 시 리스트 앞(가장 오래된)을 제거합니다.
리스트 갱신에 6개의 포인터 변경이 필요합니다.
하드웨어 지원 없이는 오버헤드가 크기 때문에, 실무에서는 LRU를 근사(Approximation)합니다.
근사 LRU: Clock 알고리즘 (Second Chance)
하드웨어가 제공하는 참조 비트(Reference Bit)를 활용합니다.
페이지가 접근되면 하드웨어가 참조 비트를 자동으로 1로 설정합니다.
이것은 추가 소프트웨어 오버헤드 없이 수행됩니다.
동작 방식
프레임들을 원형 리스트로 배열하고, 시계 바늘 포인터가 순회합니다.
- 교체가 필요하면 포인터 위치의 페이지를 확인합니다.
- 참조 비트 = 0이면: 이 페이지를 교체합니다.
- 참조 비트 = 1이면: 0으로 바꾸고(두 번째 기회 부여), 다음 페이지로 이동합니다.
- 모든 페이지가 참조 비트 1이면, 한 바퀴 돌면서 전부 0으로 바꾼 후 처음 만나는 페이지를 교체합니다.
최근에 접근된 페이지는 참조 비트가 1이므로 교체되지 않습니다.
LRU의 근사입니다.
Enhanced Clock (수정된 시계 알고리즘)
참조 비트(R)와 더티 비트(M)를 함께 고려하여 4가지 클래스로 분류합니다.
| 클래스 | (R, M) | 교체 우선순위 | 의미 |
|---|---|---|---|
| 0 | (0, 0) | 최우선 교체 | 최근 미사용, 수정 안 됨 |
| 1 | (0, 1) | 두 번째 | 최근 미사용, 수정됨 (디스크 쓰기 필요) |
| 2 | (1, 0) | 세 번째 | 최근 사용, 수정 안 됨 |
| 3 | (1, 1) | 최후 교체 | 최근 사용, 수정됨 |
클래스 0부터 찾아서 교체하면, 디스크 I/O(dirty page write back)를 최소화하면서 LRU를 근사합니다.
LFU 알고리즘
LFU(Least Frequently Used)는 사용 횟수가 가장 적은 페이지를 내보냅니다.
자주 사용된 페이지는 앞으로도 사용될 가능성이 높다는 가정입니다.
단점: 과거에 많이 사용되었지만 더 이상 사용되지 않는 페이지가 높은 카운트 때문에 오래 남습니다.
해결법으로 감쇠(Aging) 기법을 사용합니다 — 카운터를 주기적으로 오른쪽 시프트하여 오래된 접근의 가중치를 줄입니다.
반대로 MFU(Most Frequently Used)는 가장 많이 사용된 페이지를 내보냅니다.
적게 사용된 페이지는 방금 들어와서 아직 사용이 적은 것이라는 논리입니다.
실제로는 LFU, MFU 모두 OPT에 크게 근접하지 못합니다.
알고리즘 비교
| 알고리즘 | 폴트 수 | Belady 모순 | 구현 난이도 | 실무 사용 |
|---|---|---|---|---|
| FIFO | 많음 | 발생 | 쉬움 | 거의 안 씀 |
| OPT | 최소 | 없음 | 불가능 | 기준선만 |
| LRU | 적음 | 없음 | 하드웨어 의존 | 근사 형태로 |
| Clock | 적음 (LRU 근사) | 없음 | 보통 | 널리 사용 |
| LFU | 중간 | 없음 | 보통 | 드물게 |
스래싱 (Thrashing)
페이지 폴트가 극단적으로 많아져서 프로세스가 실제 작업보다 페이지 교체에 더 많은 시간을 쓰는 현상을 스래싱이라고 합니다.
발생 메커니즘
- 물리 메모리에 비해 동시 실행 프로세스가 너무 많습니다.
- 각 프로세스의 워킹 셋 합계가 물리 메모리를 초과합니다.
- 모든 프로세스가 끊임없이 페이지 폴트를 일으킵니다.
- CPU 이용률이 급락합니다 (I/O 대기 시간만 증가).
- OS의 스케줄러는 CPU 이용률이 낮으니 더 많은 프로세스를 실행하자고 판단합니다.
- 멀티프로그래밍 차수를 높이면 상황이 더 악화됩니다.
- 악순환으로 시스템이 거의 멈춥니다.
이 악순환은 CPU 자체의 부족보다 워킹 셋 합계가 물리 프레임 수를 넘는 순간에 시작됩니다.
CPU 이용률 하락은 원인이 아니라 페이지를 기다린 결과다. 이 신호를 여유 용량으로 읽으면 동시 실행을 늘려 상황이 악화된다.
- 1 실행 대상 증가
실행 대상 증가 프로세스당 프레임 몫이 줄어든다.
- 2 지역성 이탈
지역성 이탈 곧 쓸 활성 페이지까지 교체된다.
- 3 폴트·I/O 증가
폴트·I/O 증가 재적재·reclaim·swap 대기가 누적된다.
- 4 유용한 일 감소
유용한 일 감소 프로세스가 계산보다 메모리를 기다린다.
- 5 잘못된 증설
잘못된 증설 낮은 CPU만 보고 실행 대상을 더 넣는다.
워킹 셋 모델
워킹 셋(Working Set)은 시간 간격 동안 프로세스가 접근하는 페이지의 집합입니다.
가 너무 작으면 워킹 셋이 지역성을 반영하지 못합니다.
너무 크면 전체 프로그램을 포함합니다.
적절한 를 선택하면 프로세스의 활발한 메모리 사용 패턴을 포착합니다.
OS가 각 프로세스의 워킹 셋 크기 를 추적합니다.
전체 워킹 셋의 합(D = )이 총 프레임 수(m)를 초과하면 스래싱이 발생합니다.
이때 OS는 일부 프로세스를 중단(Suspend)시켜 D ≤ m을 유지합니다.
실무에서 스래싱 진단
# Linux에서 스래싱 징후 확인 (개념 코드)
import subprocess
# vmstat으로 페이지 인/아웃 확인
# si: swap in, so: swap out, us: user CPU, sy: system CPU
# si/so가 높고 us가 낮으면 스래싱 의심
result = subprocess.run(["vmstat", "1", "5"], capture_output=True, text=True)
print(result.stdout)
# 정상: si=0, so=0, us=70%, id=25%
# 스래싱: si=5000, so=3000, us=5%, id=2%, wa=90%실제 운영에서는 페이지 교체 횟수만 보지 말고, 워킹 셋 예산과 시스템 지표를 함께 묶어 판단합니다.
워킹 셋 합계가 물리 프레임 수를 넘으면 페이지 교체가 작업 실행을 밀어내고 I/O 대기가 지배합니다.
- 합산 워킹 셋이 기준선입니다
> m D는 각 프로세스의 WSS 합, m은 사용 가능한 프레임 수입니다.
- P1 WSS 40
활성 웹 서버
- P2 WSS 35
DB worker
- P3 WSS 30
batch job
- D=105 > m=96
스래싱 위험 · P3 suspend 후 D=75 ≤ 96으로 안정화
- si/so운영 신호
si/so 페이지가 메모리와 디스크 사이를 계속 오갑니다. wa CPU는 놀고 있지만 디스크 응답을 기다립니다. us 실제 계산보다 페이지 교체 시간이 더 커집니다.
- 자원 조치
일부 작업을 suspend해 D를 m 이하로 낮춥니다. 누수 종료와 cache pressure 완화로 프레임을 확보합니다. SSD 스왑은 시간을 벌지만 워킹 셋 초과를 해결하지 않습니다. 회수가 실패하면 점수 기반으로 종료 대상을 선택합니다.
- 프로세스 내림
일부 작업을 suspend해 D를 m 이하로 낮춥니다.
- 메모리 회수
누수 종료와 cache pressure 완화로 프레임을 확보합니다.
- 스왑은 완충
SSD 스왑은 시간을 벌지만 워킹 셋 초과를 해결하지 않습니다.
- OOM 최후 수단
회수가 실패하면 점수 기반으로 종료 대상을 선택합니다.
스래싱 해결: 메모리 추가, 프로세스 수 감소, 메모리 누수 프로세스 종료, 스왑 파티션 SSD로 변경(임시 방편).
Linux의 OOM Killer
모든 페이지 교체와 스왑도 메모리 부족을 해결하지 못하면, Linux 커널은 최후의 수단으로 OOM(Out Of Memory) Killer를 실행합니다.
각 프로세스의 oom_score를 기반으로 희생자를 선택하고 강제 종료합니다.
/proc/<pid>/oom_score로 점수를 확인하고, /proc/<pid>/oom_score_adj로 조절할 수 있습니다.
중요한 서비스는 oom_score_adj를 -1000으로 설정하여 OOM Killer로부터 보호합니다.
다음 장에서는 OS가 관리하는 또 다른 핵심 자원인 파일 시스템을 다루겠습니다.
페이지 교체 알고리즘은 빈 프레임이 없을 때 어떤 페이지를 내보낼지 정하는 정책입니다.
OPT는 미래를 알기 때문에 기준선이 되고, LRU와 Clock은 최근 접근 정보를 이용해 그 기준선을 현실적으로 근사합니다.
- 접근 패턴 관찰
참조 참조 문자열과 지역성으로 앞으로 쓸 페이지를 추정합니다.
- 희생 페이지 결정
선택 FIFO, LRU, Clock처럼 정책에 따라 내보낼 페이지를 고릅니다.
- 더티 페이지 처리
쓰기 수정된 페이지는 디스크에 기록한 뒤 프레임을 비웁니다.
- 새 페이지 적재
교체 필요한 페이지를 프레임에 넣고 페이지 테이블을 갱신합니다.
- 시험에서 구분할 성질
구현은 쉽지만 Belady의 모순이 발생할 수 있습니다. 지역성에 맞지만 완전 구현은 모든 접근 기록이 필요합니다. 참조 비트를 이용해 LRU를 낮은 비용으로 근사합니다.
- FIFO
구현은 쉽지만 Belady의 모순이 발생할 수 있습니다.
- LRU
지역성에 맞지만 완전 구현은 모든 접근 기록이 필요합니다.
- Clock
참조 비트를 이용해 LRU를 낮은 비용으로 근사합니다.
가상 메모리 정책은 단순히 더 빠른지로 끝나지 않는다. 평균 접근 시간, TLB hit율, page fault 비용, 단편화와 보호 수준을 함께 봐야 한다.
- TLB hit 98%
0.98 × 100ns = 98ns 대부분의 빠른 접근
- TLB miss 1.999%
0.01999 × 500ns ≈ 10ns page table walk
- Major fault 0.001%
0.00001 × 8ms = 80µs 확률은 작아도 비용이 지배
- EAT ≈ 80.1µs
hit율만 보지 말고 fault율과 I/O 지연을 반드시 함께 본다.
EAT = hitRatio * hitCost + missRatio * missCost + faultRate * faultCost