가상 메모리
가상 메모리(Virtual Memory)는 프로그램이 실제 물리 메모리보다 훨씬 큰 주소 공간을 사용할 수 있게 해주는 기법입니다. OS가 페이지 단위로 물리 메모리와 디스크를 투명하게 관리합니다.
핵심 개념
가상 주소 공간
각 프로세스는 독립된 가상 주소 공간을 가집니다. 실제 물리 메모리 위치와 무관하게 하나의 연속된 주소 범위를 쓰는 것처럼 보입니다(실제로는 일부 구간만 매핑됩니다).
핵심 개념
페이지 (Page)
가상 주소 공간을 고정 크기(보통 4KB)로 나눈 단위입니다. 물리 메모리의 같은 크기 단위인 프레임(Frame)에 매핑됩니다.
핵심 개념
페이지 테이블
가상 페이지 번호(VPN)를 물리 프레임 번호(PFN)로 변환하는 테이블입니다. 각 프로세스마다 별도로 존재합니다.
핵심 개념
TLB
Translation Lookaside Buffer. 페이지 테이블 조회 비용을 줄이기 위한 하드웨어 캐시입니다. 최근 변환 결과를 저장합니다.
핵심 개념
페이지 폴트
접근한 페이지가 물리 메모리에 없거나 접근 권한이 맞지 않을 때 발생합니다. OS가 원인을 확인하고, 정상 접근이면 페이지를 읽어 오거나(메이저 폴트) 빈 페이지를 할당하거나(마이너 폴트) 복사해서(Copy-on-Write) 처리합니다.
핵심 개념
스왑 (Swap)
물리 메모리가 부족할 때 쓰지 않는 페이지를 내보내 두는 디스크 공간(스왑 영역)입니다. 내보내는 동작을 스왑 아웃, 다시 읽어 오는 동작을 스왑 인이라고 합니다. 파일에서 온 변경 없는 페이지는 스왑 영역이 아니라 원본 파일에서 다시 읽습니다.
가상 메모리가 필요했던 이유
가상 메모리가 없던 시절에는 프로그램이 물리 주소를 직접 다뤘기 때문에 세 가지 문제가 있었습니다. 프로그램이 물리 메모리보다 크면 실행할 수 없었고, 여러 프로그램을 함께 올리면 서로의 메모리를 덮어쓸 수 있었으며, 프로그램을 어느 주소에 올릴지 미리 정해 두어야 했습니다. 가상 메모리는 프로그램마다 자기만의 연속된 주소 공간을 주고, 실제 물리 위치는 운영체제와 하드웨어(MMU)가 뒤에서 대응시켜서 이 문제를 한꺼번에 풀었습니다. 1960년대 초 영국 맨체스터 대학의 Atlas 컴퓨터가 이 방식을 처음 구현한 시스템 중 하나로 알려져 있습니다.
페이지 폴트가 처리되는 순서
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
단일 단계 페이지 테이블은 왜 문제인가요?
32비트 주소와 4KB 페이지에서는 페이지가 2의 20제곱(약 100만)개이므로, 항목이 4바이트이면 프로세스마다 4MB 테이블이 필요합니다. 대부분의 주소 공간은 쓰지 않는데도 테이블이 통째로 필요하므로, 쓰는 영역의 하위 테이블만 만드는 다단계 페이지 테이블을 씁니다. x86-64는 기본 4단계(5단계 확장도 있습니다)이고 각 테이블은 8바이트 항목 512개(4KB)입니다.
면접 질문
요구 페이징(Demand Paging)이란 무엇인가요?
프로세스를 시작할 때 모든 페이지를 올리지 않고, 실제로 접근하는 순간 페이지 폴트로 필요한 페이지만 올리는 방식입니다. 쓰지 않는 부분은 물리 메모리를 차지하지 않아 시작이 빠르고 메모리를 아낄 수 있습니다.
면접 질문
스래싱은 왜 생기고 어떻게 줄이나요?
실행 중인 프로세스들이 자주 쓰는 페이지의 합(작업 집합, working set)이 물리 메모리보다 커서 페이지 폴트가 계속 나는 현상입니다. 동시에 실행하는 프로세스 수를 줄이거나 메모리를 늘려서 작업 집합이 들어갈 수 있게 합니다.
면접 질문
fork 뒤 부모와 자식의 메모리는 어떻게 되나요?
Copy-on-Write 방식이라 페이지를 바로 복사하지 않고, 쓰기 시 복사하도록 표시한 채 공유합니다. 어느 한쪽이 쓰려고 하면 페이지 폴트가 나고, 그때 해당 페이지만 복사합니다. 그래서 fork 직후에는 데이터 페이지를 복사하지 않아 비용이 작습니다(페이지 테이블은 복제됩니다).
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
C — 가상 주소 변환 (페이지 테이블 조회)
// 페이지 크기: 4KB = 4096 bytes #define PAGE_SIZE 4096 #define PAGE_BITS 12 // log2(4096) = 12 #define VPN_MASK 0xFFFFF000 #define OFFSET_MASK 0x00000FFF typedef struct { unsigned int pfn : 20; // 물리 프레임 번호 (20비트) unsigned int valid : 1; // 유효 비트 (물리 메모리에 있으면 1) unsigned int dirty : 1; // 수정 여부 (Write-Back용) unsigned int ref : 1; // 참조 비트 (LRU 교체 판단용) } PageEntry; PageEntry page_table[1048576]; // 2^20개 엔트리 // 가상 주소 → 물리 주소 변환 long translate(unsigned int vaddr) { unsigned int vpn = (vaddr & VPN_MASK) >> PAGE_BITS; unsigned int offset = vaddr & OFFSET_MASK; PageEntry *entry = &page_table[vpn]; if (!entry->valid) { // Page Fault — OS가 처리 handle_page_fault(vpn); } entry->ref = 1; // 참조 비트 설정 return (((long)entry->pfn) << PAGE_BITS) | offset; } // 물리 주소 = PFN << 12 | Offset // 예: VPN=5, Offset=0x1A0 → PA = (5 << 12) | 0x1A0 = 0x501A0
가상 주소 32비트 중 상위 20비트가 VPN(페이지 번호), 하위 12비트가 Offset입니다. 페이지 테이블에서 VPN에 해당하는 엔트리를 찾아 PFN을 얻고, 물리 주소 = PFN × 4096 + Offset 으로 계산합니다. 엔트리가 2²⁰개 × 4바이트 = 4MB이므로 프로세스마다 4MB의 페이지 테이블이 필요합니다. 이 오버헤드를 줄이기 위해 실제 OS는 2단계 이상의 다단계 페이지 테이블을 사용합니다.
Java — TLB 시뮬레이션 (LinkedHashMap 기반)
import java.util.LinkedHashMap; import java.util.Map; /** * TLB 시뮬레이터 — 최근 사용 기반(LRU) 교체 * 용량: 16엔트리 (교육용 단순화) * 실제 x86 L1 DTLB: 64~128 엔트리 (Pentium 4: 64, Core 2 Duo: 128, * Alder Lake Golden Cove: 96 — Intel SDM Vol.3A Ch.11) */ public class TLBSimulator { private static final int TLB_SIZE = 16; // accessOrder=true: get() 호출 시 해당 항목을 MRU로 이동 (LRU 자동 관리) private final Map<Integer, Integer> tlb = new LinkedHashMap<>( TLB_SIZE, 0.75f, true ) { protected boolean removeEldestEntry(Map.Entry<Integer,Integer> e) { return size() > TLB_SIZE; // 용량 초과 시 LRU 항목 자동 제거 } }; private int[] pageTable; // 페이지 테이블 (VPN → PFN) private int tlbHits, tlbMisses; public TLBSimulator(int[] pageTable) { this.pageTable = pageTable; } /** 가상 주소 → 물리 주소 변환 */ public int translate(int vaddr) { int vpn = vaddr >> 12; int offset = vaddr & 0xFFF; // TLB 조회 Integer pfn = tlb.get(vpn); if (pfn != null) { tlbHits++; System.out.printf("TLB HIT VPN:%d → PFN:%d%n", vpn, pfn); } else { // TLB Miss — 페이지 테이블 조회 후 TLB 갱신 tlbMisses++; pfn = pageTable[vpn]; tlb.put(vpn, pfn); // TLB에 캐싱 System.out.printf("TLB MISS VPN:%d → PFN:%d (PT 조회)%n", vpn, pfn); } return (pfn << 12) | offset; // 물리 주소 } public void printStats() { int total = tlbHits + tlbMisses; System.out.printf("TLB 히트율: %.1f%%%n", tlbHits * 100.0 / total); } }
TLB는 VPN → PFN 변환 결과를 캐싱해 페이지 테이블 조회 비용을 줄입니다. 이 코드의 단일 레벨 페이지 테이블 기준으로 히트 시 메모리 접근 1회, 미스 시 페이지 테이블(메모리) 접근 후 TLB 갱신으로 총 2회 접근합니다. 실제 x86-64의 4단계 페이지 테이블(PML4→PDPT→PD→PT)에서는 TLB 미스 시 하드웨어 페이지 워크로 최대 4+1 = 5회 메모리 접근이 발생합니다(Intel SDM Vol.3A Ch.4). LinkedHashMap의 accessOrder로 LRU 교체를 자동 구현합니다.
Q1
TLB(Translation Lookaside Buffer)의 역할은?
Q2
Page Fault(페이지 폴트)가 발생하는 경우는?
Q3
TLB Hit과 TLB Miss 중 더 빠른 것은?