2026. 4. 6. 11:31ㆍ운영체제(OS)
페이지 교체 알고리즘, 워킹셋, 스래싱 — RAM 관리가 무너지면 어떻게 되는가
이전 글에서 페이지 부재(Page Fault)가 발생하면 OS가 디스크에서 페이지를 가져온다고 했다. 그런데 RAM이 꽉 찼을 때 어떤 페이지를 내쫓을지, 그리고 그게 계속 잘못되면 시스템 전체가 어떻게 멈추는지까지 이어서 정리한다.
1. 페이지 교체 알고리즘 — 누구를 내쫓을까
RAM이 꽉 찬 상태에서 새 페이지를 가져와야 할 때, OS는 기존 페이지 중 하나를 디스크로 밀어내야 한다. 이때 "어떤 페이지를 골라서 내쫓을지" 결정하는 방법이 페이지 교체 알고리즘이다. 잘못 고르면 방금 내쫓은 페이지를 다시 불러오는 일이 반복되고, 그게 누적되면 시스템이 버벅인다.
문제는 미래를 알아야 한다는 것. 실제 구현이 불가능해서 다른 알고리즘의 성능 비교 기준으로만 쓴다.
단점: 오래됐다고 안 쓰는 게 아니다. RAM을 늘렸는데 오히려 페이지 부재가 더 늘어나는 벨레이디의 모순이 발생하기도 한다.
현실적으로 구현하려면 각 페이지마다 사용 시각을 기록해야 해서 하드웨어 지원이 필요하고 오버헤드가 있다.
문제: 실행 초기에 집중적으로 쓴 페이지가 이후엔 안 쓰여도 높은 카운트를 유지해 남아있고, 방금 올라온 새 페이지가 먼저 쫓겨나는 역설이 생긴다.
LRU보다 오버헤드가 훨씬 적어 현실적인 타협안이다.
자주 쓰는 페이지는 계속 기회를 받아 살아남는 구조다.
FIFO vs LRU — 같은 상황에서 어떻게 다른가
요청 순서가 4 → 3 → 2 → 1 → 4 → 3 → 5 → 4 → 3 → 2 → 1 → 5이고 RAM에 페이지 3개를 담을 수 있다고 하면, 두 알고리즘의 차이를 직접 볼 수 있다.
| 알고리즘 | 기준 | 현실 사용 | 단점 |
|---|---|---|---|
| OPT | 미래에 가장 안 쓸 것 | ❌ 불가능 | 미래를 알 수 없음 |
| FIFO | 가장 먼저 들어온 것 | ⚠️ 단순한 환경 | 벨레이디의 모순 |
| LRU | 가장 오래 전에 쓴 것 | ✅ 근사 구현 | 하드웨어 오버헤드 |
| LFU | 사용 횟수 가장 적은 것 | ⚠️ 제한적 | 초기 집중 사용 왜곡 |
| NUR | 참조/변경 비트 조합 | ✅ 경량 LRU 대안 | 정밀도 낮음 |
| SCR | FIFO + 2차 기회 | ✅ 실제 OS 사용 | FIFO 기반 한계 |
2. 지역성(Locality) — 왜 LRU가 잘 통하는가
LRU가 현실에서 잘 동작하는 이유가 있다. 프로그램은 메모리를 무작위로 접근하지 않는다. 어떤 코드나 데이터에 접근하면, 그 주변을 계속 접근하는 경향이 있다. 이것을 메모리의 구역성(Locality)이라고 한다.
한 번 쓴 변수나 함수는 반복해서 쓰이는 경우가 많다.
i, 자주 호출되는 함수, 캐시에 올라간 게임 오브젝트 데이터
코드는 순서대로 실행되고, 배열은 연속으로 읽힌다.
이 구역성 덕분에 가상기억장치가 성립한다. 프로그램 전체를 RAM에 올리지 않아도, 지금 쓰는 근처만 올려두면 대부분의 접근이 RAM 안에서 해결된다. 캐시 메모리도 같은 원리로 작동한다. 구역성이 높을수록 페이지 부재가 적고, 낮을수록(무작위 접근이 많을수록) 페이지 부재가 잦아진다.
3. 워킹셋(Working Set) — 지금 이 프로세스가 필요한 최소한
지역성을 좀 더 실용적으로 표현한 개념이 워킹셋이다. 한 프로세스가 일정 시간 동안 자주 접근하는 페이지들의 집합을 워킹셋이라고 한다. 게임으로 비유하면 지금 플레이 중인 맵과 캐릭터 데이터 — 당장 필요한 것들의 묶음이다.
워킹셋이 바뀌었는데 교체가 안 되면 → 죽은 페이지가 자리 차지 → 역시 문제
워킹셋의 핵심은 각 프로세스에 워킹셋만큼의 프레임은 반드시 보장해줘야 한다는 것이다. 그 아래로 내려가면 페이지 부재가 폭발적으로 늘어난다. 반대로 여유 있게 줄 수 없는 상황에서 프로세스가 너무 많이 동시에 실행되면 전체 시스템이 무너진다 — 이게 스래싱이다.
4. 스래싱(Thrashing) — 교체만 하다가 실제 일을 못 하는 상태
스래싱은 페이지 교체가 너무 자주 발생해서 실제 작업보다 페이지 불러오는 시간이 더 많아지는 현상이다. CPU는 쉬지 않고 돌아가는 것처럼 보이지만, 실제로는 페이지 부재 처리와 디스크 I/O만 반복하느라 아무것도 못 하는 상태다.
프로그램이 늘어날수록 CPU 활용도도 같이 올라가다가, 어느 시점을 넘으면 급격히 추락한다. 이 지점이 스래싱 포인트다. 프로그램이 많아질수록 각 프로세스에 돌아가는 RAM 프레임이 줄어들고, 워킹셋을 담기 부족해지면서 페이지 부재가 폭발한다.
페이지 부재가 가끔 발생하지만 빠르게 해결된다.
CPU는 실제 계산을 하느라 바쁘다.
A가 B 페이지를 내쫓으면, B가 C를 내쫓고, C가 A를 내쫓는 연쇄가 반복된다.
CPU 사용률 수치는 높지만 실제 일은 거의 0.
스래싱 대응 — OS가 할 수 있는 것
스래싱이 발생하면 OS는 프로세스 수를 줄이는 방향으로 대응한다. 일부 프로세스를 통째로 디스크에 내리고(스왑 아웃), 남은 프로세스들이 충분한 프레임을 확보할 수 있게 한다. 또한 프리페이징(Pre-paging)으로 워킹셋에 들어갈 것 같은 페이지를 미리 올려 초기 페이지 부재를 줄이기도 한다.
5. Unity 개발자 입장에서 보면
스래싱과 워킹셋 개념은 OS 수준에서만 일어나는 게 아니다. Unity 개발에서도 같은 패턴이 반복된다.
- GC 스파이크 = 작은 스래싱: Managed Heap에서 GC가 너무 자주 돌면 실제 게임 로직 실행이 밀린다. 페이지 교체 연쇄처럼, GC가 메모리 정리하느라 프레임 처리를 못 하는 구조다.
new를 매 프레임 남발하면 이 현상이 심해진다. - 에셋 로딩의 워킹셋: 지금 플레이 중인 씬에서 필요한 텍스처, 메시, 사운드가 워킹셋이다. 이게 VRAM + RAM을 초과하면 계속 에셋을 로드/언로드하는 스래싱이 발생한다.
Addressables로 씬 단위 워킹셋을 관리하는 게 정석이다. - 구역성을 활용하는 NativeArray:
NativeArray<T>는 연속 메모리라 공간 구역성이 높다. Burst + Job System과 조합하면 캐시 히트율이 높아지고 실질적인 처리량이 올라간다. 반면 흩어진 참조형 객체들은 공간 구역성이 낮아 캐시 미스가 잦다.
// ❌ 매 프레임 할당 — GC 스파이크 유발 (작은 스래싱)
void Update() {
var list = new List<Enemy>(); // 매 프레임 GC 압박
}
// ✅ 구역성 활용 — 연속 메모리, 캐시 친화
using var positions = new NativeArray<float3>(count, Allocator.TempJob);
// 한 번에 쭉 처리 → 공간 구역성 높음 → 빠름
📎 용어 설명
- 벨레이디의 모순(Belady's Anomaly) — FIFO에서 프레임 수를 늘렸는데 오히려 페이지 부재가 증가하는 역설적 현상. LRU에서는 발생하지 않는다.
- 참조 비트(Reference Bit) — 각 페이지에 붙는 플래그. 최근에 접근되면 1, OS가 주기적으로 0으로 초기화. NUR/SCR이 이를 활용한다.
- 프리페이징(Pre-paging) — 프로세스 시작 시 워킹셋에 들어갈 것 같은 페이지를 미리 RAM에 올려두는 기법. 초기 페이지 부재 폭탄을 완화한다.
'운영체제(OS)' 카테고리의 다른 글
| [OS] 파일 시스템(File System) - 16 (0) | 2026.04.10 |
|---|---|
| [OS] 하드디스크 구조와 디스크 스케줄링 알고리즘 - 15 (0) | 2026.04.07 |
| [OS] 가상기억장치와 주소 변환 기법 - 13 (0) | 2026.04.06 |
| [OS] 메모리 단편화(Fragmentation)와 배치 전략 - 12 (0) | 2026.04.03 |
| [OS] 기억장치 개요와 주기억장치 할당 기법 - 11 (0) | 2026.03.27 |