[OS] 가상기억장치와 주소 변환 기법 - 13

2026. 4. 6. 11:17운영체제(OS)

가상기억장치와 주소 변환 기법 — 페이징, 세그먼테이션, 그리고 현대 OS

가상기억장치(Virtual Memory)의 개념과 주소 변환 기법인 페이징, 세그먼테이션, 혼합 기법까지, 그리고 현대 OS에서 실제로 어떤 방식이 쓰이는지까지 정리한다.

💡 핵심: 가상기억장치는 물리 메모리 크기와 무관하게 큰 주소 공간을 프로세스에 제공하는 추상화다. 주소 변환 기법(페이징/세그먼테이션)이 논리 주소를 실제 물리 주소로 바꿔준다.

1. 가상기억장치 — 왜 필요한가

컴퓨터 시스템에서 실행되는 모든 프로그램은 반드시 RAM1에 올라와야 CPU가 처리할 수 있다. 그런데 프로그램이 RAM 용량보다 크거나, 여러 프로그램을 동시에 띄우면 어떻게 될까.

❌ 가상기억장치 없을 때
RAM 8칸 중 꽉 참
슬롯 1
게임 A (앞부분)
슬롯 2
게임 A (뒷부분)
슬롯 3
브라우저
슬롯 4
브라우저
슬롯 5
IDE
슬롯 6
IDE
슬롯 7
← 새 프로그램?
슬롯 8
← 공간 없음!
새 프로그램을 실행하거나,
현재 RAM보다 큰 프로그램은
아예 실행 불가
✅ 가상기억장치 있을 때
지금 당장 필요한 것만 RAM에
슬롯 1
게임 (현재 장면)
슬롯 2
브라우저 (현재 탭)
슬롯 3
IDE (현재 파일)
슬롯 4
새 프로그램 일부
슬롯 5
비어있음
슬롯 6~8
← HDD/SSD 대기
안 쓰는 부분은 디스크에 대기
필요할 때만 RAM으로 올라옴
더 많은 프로그램 동시 실행 가능

가상기억장치(Virtual Memory)는 이 문제를 해결하는 핵심 구조다. 보조기억장치(SSD/HDD)의 일부를 마치 RAM처럼 사용해서, 지금 당장 필요한 부분만 RAM에 올리고 나머지는 디스크에 두는 방식이다. 프로그램 입장에서는 자신이 메모리를 전부 가진 것처럼 실행되고, OS가 뒤에서 실제 RAM과 디스크 사이를 조율한다.

프로세스의 가상 주소 공간

모든 프로세스는 OS로부터 자신만의 독립적인 가상 주소 공간을 받는다. 실제로는 여러 프로그램이 RAM을 나눠 쓰고 있지만, 각 프로세스는 자기 혼자 쓰는 것처럼 보이는 큰 공간을 가진다. 이 공간은 보통 아래처럼 구역이 나뉜다.

높은 주소 ▲
스택 — 함수 호출, 지역 변수 (↓ 아래로 성장) 가변
← 비어있는 공간 (가상으로만 존재) →
힙 — new/malloc 동적 할당 (↑ 위로 성장) 가변
데이터 — 전역 변수, 정적 변수 고정
코드 — 실행할 프로그램 코드 고정
낮은 주소 ▼

스택과 힙 사이의 빈 공간은 가상 주소 공간에만 존재한다. 실제 RAM에는 지금 쓰고 있는 부분만 올라온다. 이 논리적인 가상 주소를 실제 RAM 주소로 바꿔주는 게 바로 주소 변환(Address Mapping)이고, 이를 어떻게 구현하느냐에 따라 페이징과 세그먼테이션으로 나뉜다.

2. 페이징(Paging) 기법

페이징은 가장 널리 쓰이는 주소 변환 방식이다. 가상 주소 공간과 물리 RAM을 모두 같은 크기의 블록으로 잘라서 관리한다. 이 블록을 가상 쪽에서는 페이지(Page), RAM 쪽에서는 프레임(Frame)이라 부른다. 보통 한 블록이 4KB다.

핵심 아이디어는 간단하다. 프로그램의 각 페이지가 RAM 어디에 있든 상관없이, 빈 프레임이면 아무 데나 넣는다. 연속된 공간이 필요 없으니 외부 단편화2가 사라진다.

페이지 맵 테이블 — 어느 프레임에 있는지 기억하는 지도

페이지들이 RAM 여기저기 흩어져 들어가다 보면, "내 프로그램의 3번 페이지가 지금 RAM 몇 번 프레임에 있지?"를 기억해야 한다. 그 역할을 하는 것이 페이지 맵 테이블이다. 각 프로세스마다 하나씩 있고, OS가 관리한다.

예시: 게임 프로그램이 실행 중이다. 총 6개 페이지 중 4개만 RAM에 있고, 2개는 아직 디스크에 있다.
CPU가 페이지 2번에 접근하려 한다.
1
CPU가 가상 주소를 만들어낸다 → "페이지 2번, 그 안의 50번째 바이트"
2
페이지 맵 테이블에서 페이지 2번 행을 확인 → "RAM 7번 프레임에 있음, 유효"
3
실제 주소 계산: 7번 프레임 시작점 + 50바이트 → RAM 직접 접근 ✅
페이지 맵 테이블 (게임 프로그램)
페이지 번호
RAM 프레임
상태
0번 페이지
3번 프레임
RAM에 있음 ✅
1번 페이지
1번 프레임
RAM에 있음 ✅
2번 페이지 ← 지금 접근
7번 프레임
RAM에 있음 ✅
3번 페이지
5번 프레임
RAM에 있음 ✅
4번 페이지
디스크 대기 ❌
5번 페이지
디스크 대기 ❌

RAM 어느 프레임에 들어가도 상관없이 테이블로 추적하기 때문에, 연속된 공간이 필요 없다. 디스크에 있는 페이지에 접근하면 페이지 부재(Page Fault)가 발생해 OS가 개입한다.

페이지 부재(Page Fault) — 디스크에 있는 페이지에 접근하면

페이지 맵 테이블에서 "디스크 대기" 상태인 페이지에 접근하면 하드웨어가 OS에 신호를 보낸다. OS가 개입해서 디스크에서 해당 페이지를 RAM으로 가져오는 과정이 진행된다.

1
CPU가 4번 페이지 접근 시도 → 테이블 확인 → "디스크에 있음" → OS에 신호 (트랩)
2
OS가 현재 실행 중이던 작업 상태를 잠시 저장
3
RAM에 빈 프레임 없으면 → 덜 쓰는 페이지를 디스크로 밀어냄 (희생 페이지 선택)
4
디스크에서 4번 페이지를 빈 프레임으로 복사 → 페이지 맵 테이블 업데이트
5
저장해뒀던 작업 상태 복원 → 아까 실패한 명령을 처음부터 재실행 → 성공
⚠️ 성능 비용이 크다: 일반 RAM 접근은 거의 즉시지만, 디스크(SSD)에서 페이지를 가져오면 수백~수천 배 느리다. 페이지 부재가 자주 발생하면 게임이 버벅이고 앱이 느려진다. 현대 OS는 앞으로 쓸 것 같은 페이지를 미리 가져오는 방식으로 이를 줄인다.

3. 세그먼테이션(Segmentation) 기법

페이징이 "크기를 무조건 같게 잘라서 관리"라면, 세그먼테이션은 프로그램을 의미 있는 단위(코드, 데이터, 스택 등)로 나눠서 관리하는 방식이다. 각 영역의 크기가 다르고, 그 크기 그대로 메모리에 할당된다.

예를 들어 코드 영역은 600바이트, 데이터 영역은 400바이트, 스택은 200바이트 이런 식으로, 실제 크기에 딱 맞게 들어간다. 페이지처럼 잘게 쪼개지 않으니 내부 낭비(내부 단편화)가 없다. 대신 크기가 제각각이라 RAM 여기저기에 빈 자투리 공간이 생길 수 있다(외부 단편화).

세그먼테이션 주소 변환 흐름
프로그램이 주소 요청
"코드 구역, 50번째 바이트"
세그먼트 테이블 조회
코드 구역이 RAM 어디서 시작?
시작점 + 50바이트
= 실제 RAM 주소
요청이 구역 크기 초과?
→ 에러 발생, 다른 구역 침범 방지
세그먼테이션은 구역 경계를 넘는 접근을 하드웨어 수준에서 막아준다 — 이게 메모리 보호의 핵심
페이징세그먼테이션
분할 단위고정 크기 (4KB)가변 크기 (실제 크기)
내부 낭비있음 (마지막 페이지)없음
외부 낭비없음있음 (자투리 공간)
현대 사용✅ 주력⚠️ 거의 안 씀

4. 세그먼테이션-페이징 혼합 기법

두 방식의 장점을 합치자는 아이디어다. 프로그램을 먼저 논리 구역(세그먼트)으로 나누고, 그 각 구역을 다시 페이지로 잘게 쪼개는 방식이다. 사용자가 볼 때는 코드/데이터/스택 같은 논리 구역으로 보이고, 실제 메모리에서는 페이지 단위로 관리된다.

가상 주소
어느 구역?
(세그먼트 테이블)
그 구역의 몇 번 페이지?
(페이지 테이블)
실제 RAM 주소

주소 변환을 두 번 거치다 보니 구조가 복잡해진다. 그래서 현대에는 이 방식보다 페이징 단일 방식이 훨씬 널리 쓰인다.

5. 현대 OS는 어떻게 하는가

교재에서 세 기법을 동등하게 다루지만, 현대 OS에서 실제로 쓰이는 건 사실상 페이징 하나다.

🕰 1980~90년대 — 세그먼테이션도 쓰던 시절
👨‍💻 과거의 OS 설계
"코드 구역, 데이터 구역, 스택 구역을 세그먼트로 구분하고, 그 안을 페이징으로 관리하자. 구역마다 접근 권한도 다르게 걸 수 있고 좋지 않나?"
🤔 "세그먼트 테이블 전환이 너무 느리고, 64비트 주소 공간이면 굳이 구역으로 나눌 이유가 없지 않나?"
✅ 현대 64비트 OS — Linux, Windows, macOS
💻 현대의 OS 커널
"세그먼테이션은 사실상 제거. 구역 구분 없이 전체 주소를 하나로 보는 Flat Memory Model을 쓰고, 주소 변환은 페이징만으로 처리한다. 접근 권한(읽기/쓰기/실행)은 페이지 단위로 설정하면 충분하다."

실제 RAM 접근 흐름 — 4단계 주소 변환

현대 64비트 시스템에서 하나의 메모리 접근이 일어날 때 내부에서는 이런 흐름이 진행된다. 교재의 단순한 1단계 테이블 조회보다 단계가 더 많지만, 덕분에 훨씬 큰 주소 공간을 효율적으로 관리할 수 있다.

현대 OS에서 가상 주소 → 실제 RAM 주소 변환 흐름
CPU가
가상 주소 생성
TLB 확인
최근 변환 캐시
캐시 히트?
즉시 완료 ✅
↓ 캐시 미스 시
1단계 테이블
2단계 테이블
3단계 테이블
4단계 테이블
실제 RAM 주소
캐시(TLB)가 있으면 테이블 4번 뒤지지 않아도 된다. 자주 쓰는 주소는 캐시에서 바로 해결되므로 실제 성능은 생각보다 빠르다.

Demand Paging — 현대의 핵심 전략

현대 OS는 메모리 할당을 최대한 미룬다. C#에서 new를 하거나, C에서 malloc()을 해도 실제 RAM은 바로 안 준다. 가상 주소 공간만 예약해두고, 실제로 그 주소를 처음 읽거나 쓸 때 그제서야 RAM 프레임을 할당한다. 이때 발생하는 page fault가 바로 그 시점이다.

new 호출
(100MB 요청)
가상 주소만
예약 (즉시)
실제 접근 시
Page Fault
RAM 할당
(그때서야)

100MB를 요청해도 실제로 10MB만 쓰면 RAM은 10MB만 소모된다. 그래서 여러 프로그램을 동시에 띄워도 RAM이 버티는 것이다.

6. Unity 개발자 입장에서 보면

🎮 Unity와 가상기억장치

Unity 앱이 실행되면 OS는 Unity 프로세스에 독립적인 가상 주소 공간을 제공한다. C# 객체들이 올라가는 Managed Heap도 이 가상 주소 공간 안에 있다. 여기서 페이징 개념이 직접 연결된다.

  • Boehm GC는 non-compacting: Unity의 GC는 안 쓰는 객체를 수집해도 남은 객체들을 이동시키지 않는다. 빈 슬롯이 흩어진 채로 남는 게 힙 내부 단편화다. 큰 배열을 할당하려는데 연속 공간이 없으면 힙이 늘어난다.
  • 가상 주소 공간은 OS에 안 돌려줌: Managed Heap이 한 번 커지면 GC가 돌아도 그 가상 주소 공간은 OS에 반환되지 않는다. Profiler에서 Reserved가 줄지 않는 이유다.
  • NativeArray는 연속 물리 메모리: NativeArray<T>는 페이지 경계에 정렬된 연속 메모리를 쓴다. GC 없이 캐시 효율도 높아서 Job System과 궁합이 좋다.
// Managed Heap — GC 관리, non-compacting 단편화 가능
var enemies = new List<Enemy>();

// NativeArray — 연속 물리 메모리, GC 없음, 캐시 친화적
using var positions = new NativeArray<float3>(1000, Allocator.Persistent);

📎 용어 설명

  1. RAM (Random Access Memory) — 주기억장치. CPU가 직접 접근하는 메모리. 전원을 끄면 내용이 사라진다.
  2. 외부 단편화 — 빈 공간이 충분한데 연속되지 않아서 큰 프로그램을 넣지 못하는 현상. 퍼즐 조각처럼 조각조각 흩어진 빈 공간.
가상기억장치 페이징 세그먼테이션 Page Fault 운영체제 Demand Paging Unity