컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
arch · 07

가상 메모리

페이징OS메모리

가상 메모리(Virtual Memory)는 프로그램이 실제 물리 메모리보다 훨씬 큰 주소 공간을 사용할 수 있게 해주는 기법입니다. OS가 페이지 단위로 물리 메모리와 디스크를 투명하게 관리합니다.

핵심 개념

가상 주소 공간

각 프로세스는 독립된 가상 주소 공간을 가집니다. 실제 물리 메모리 위치와 무관하게 하나의 연속된 주소 범위를 쓰는 것처럼 보입니다(실제로는 일부 구간만 매핑됩니다).

핵심 개념

페이지 (Page)

가상 주소 공간을 고정 크기(보통 4KB)로 나눈 단위입니다. 물리 메모리의 같은 크기 단위인 프레임(Frame)에 매핑됩니다.

핵심 개념

페이지 테이블

가상 페이지 번호(VPN)를 물리 프레임 번호(PFN)로 변환하는 테이블입니다. 각 프로세스마다 별도로 존재합니다.

핵심 개념

TLB

Translation Lookaside Buffer. 페이지 테이블 조회 비용을 줄이기 위한 하드웨어 캐시입니다. 최근 변환 결과를 저장합니다.

핵심 개념

페이지 폴트

접근한 페이지가 물리 메모리에 없거나 접근 권한이 맞지 않을 때 발생합니다. OS가 원인을 확인하고, 정상 접근이면 페이지를 읽어 오거나(메이저 폴트) 빈 페이지를 할당하거나(마이너 폴트) 복사해서(Copy-on-Write) 처리합니다.

핵심 개념

스왑 (Swap)

물리 메모리가 부족할 때 쓰지 않는 페이지를 내보내 두는 디스크 공간(스왑 영역)입니다. 내보내는 동작을 스왑 아웃, 다시 읽어 오는 동작을 스왑 인이라고 합니다. 파일에서 온 변경 없는 페이지는 스왑 영역이 아니라 원본 파일에서 다시 읽습니다.

주소 변환 흐름 — 가상 주소를 VPN(페이지 번호)과 Offset(페이지 내 위치)으로 분리 후 TLB 조회 → (미스 시) 페이지 테이블 조회 → 물리 주소 = PFN과 Offset을 이어 붙인 값
TLB 히트율 — 지역성 덕분에 대체로 매우 높습니다(강의 자료에서 흔히 제시하는 값은 미스율 0.01~2% 수준이며 구조마다 다릅니다). TLB 미스가 나면 페이지 테이블을 걸어 내려가야 해서 대략 10~100 사이클의 추가 비용이 듭니다(구조마다 다릅니다).
페이지 교체 알고리즘 — 물리 메모리가 꽉 찼을 때 어떤 페이지를 내보낼지 결정합니다. LRU, FIFO, Clock 알고리즘 등이 사용됩니다.
메모리 보호 — 각 프로세스가 독립된 가상 주소 공간을 가지므로 일반 접근 경로로는 한 프로세스가 다른 프로세스의 메모리에 접근할 수 없습니다(공유하도록 명시한 영역이나 운영체제가 허용한 디버깅 같은 경우는 예외입니다).
Thrashing — 페이지 폴트가 너무 자주 발생해 시스템이 실제 작업보다 페이지 교체에 더 많은 시간을 쓰는 현상입니다.

가상 메모리가 필요했던 이유

가상 메모리가 없던 시절에는 프로그램이 물리 주소를 직접 다뤘기 때문에 세 가지 문제가 있었습니다. 프로그램이 물리 메모리보다 크면 실행할 수 없었고, 여러 프로그램을 함께 올리면 서로의 메모리를 덮어쓸 수 있었으며, 프로그램을 어느 주소에 올릴지 미리 정해 두어야 했습니다. 가상 메모리는 프로그램마다 자기만의 연속된 주소 공간을 주고, 실제 물리 위치는 운영체제와 하드웨어(MMU)가 뒤에서 대응시켜서 이 문제를 한꺼번에 풀었습니다. 1960년대 초 영국 맨체스터 대학의 Atlas 컴퓨터가 이 방식을 처음 구현한 시스템 중 하나로 알려져 있습니다.

페이지 폴트가 처리되는 순서

1. 접근 — CPU가 가상 주소에 접근합니다. TLB에 변환 결과가 없으면 페이지 테이블을 확인합니다.
2. 폴트 발생 — 페이지 테이블 항목의 유효(present) 비트가 0이거나 접근 권한이 맞지 않으면 CPU가 예외를 일으키고 제어가 운영체제로 넘어갑니다.
3. 접근 검사 — 운영체제는 이 접근이 허용된 주소인지 확인합니다. 허용되지 않은 주소이면 프로세스에 오류를 알립니다(리눅스에서는 SIGSEGV 시그널).
4. 프레임 확보 — 비어 있는 프레임을 찾습니다. 없으면 페이지 교체 알고리즘으로 내보낼 페이지를 고르고, 내용이 바뀐 페이지는 디스크에 먼저 기록합니다.
5. 적재 — 디스크(스왑 영역이나 파일)에서 필요한 페이지를 읽어 프레임에 올리고 페이지 테이블을 갱신합니다.
6. 재실행 — 폴트가 났던 명령어를 처음부터 다시 실행합니다. 프로그램은 폴트가 있었다는 사실을 알지 못합니다.

자주 하는 오해

가상 메모리는 디스크를 RAM처럼 쓰는 기능이다 — 스왑은 가상 메모리의 한 부분일 뿐입니다. 핵심은 프로세스마다 독립된 주소 공간을 주고 주소를 변환하고 보호하는 것이며, 스왑 영역이 없는 시스템에서도 이 기능은 동작합니다.
가상 주소 공간의 크기는 설치된 RAM 크기와 같다 — 주소 공간의 크기는 CPU 구조가 정합니다. x86-64의 4단계 페이지 테이블은 48비트 가상 주소를 쓰고, 리눅스는 이를 사용자 영역과 커널 영역으로 128TiB씩 나눕니다. RAM이 16GB여도 프로세스가 볼 수 있는 주소 공간은 그보다 훨씬 큽니다.
페이지 폴트는 오류다 — 정상 동작입니다. 리눅스는 디스크 읽기가 필요 없는 마이너 폴트(예: 처음 접근하는 메모리에 0으로 채운 페이지를 할당)와 디스크 읽기가 필요한 메이저 폴트로 구분합니다. 프로그램이 SIGSEGV로 종료되는 것은 보통 허용되지 않은 주소에 접근하거나 권한을 위반했을 때입니다(파일 매핑 범위를 벗어나면 SIGBUS 등 다른 경우도 있습니다).
TLB는 데이터 캐시의 일부다 — TLB는 가상 주소를 물리 주소로 바꾼 결과를 저장하는 별도의 캐시이고, 데이터 캐시는 메모리 내용을 저장합니다. 프로세스를 전환하면 주소 대응이 달라지므로 TLB를 비우거나, 항목에 프로세스 식별자(x86에서는 PCID)를 붙여 구분하기도 합니다.

면접에서 이렇게 물어요

면접 질문

단일 단계 페이지 테이블은 왜 문제인가요?

32비트 주소와 4KB 페이지에서는 페이지가 2의 20제곱(약 100만)개이므로, 항목이 4바이트이면 프로세스마다 4MB 테이블이 필요합니다. 대부분의 주소 공간은 쓰지 않는데도 테이블이 통째로 필요하므로, 쓰는 영역의 하위 테이블만 만드는 다단계 페이지 테이블을 씁니다. x86-64는 기본 4단계(5단계 확장도 있습니다)이고 각 테이블은 8바이트 항목 512개(4KB)입니다.

면접 질문

요구 페이징(Demand Paging)이란 무엇인가요?

프로세스를 시작할 때 모든 페이지를 올리지 않고, 실제로 접근하는 순간 페이지 폴트로 필요한 페이지만 올리는 방식입니다. 쓰지 않는 부분은 물리 메모리를 차지하지 않아 시작이 빠르고 메모리를 아낄 수 있습니다.

면접 질문

스래싱은 왜 생기고 어떻게 줄이나요?

실행 중인 프로세스들이 자주 쓰는 페이지의 합(작업 집합, working set)이 물리 메모리보다 커서 페이지 폴트가 계속 나는 현상입니다. 동시에 실행하는 프로세스 수를 줄이거나 메모리를 늘려서 작업 집합이 들어갈 수 있게 합니다.

면접 질문

fork 뒤 부모와 자식의 메모리는 어떻게 되나요?

Copy-on-Write 방식이라 페이지를 바로 복사하지 않고, 쓰기 시 복사하도록 표시한 채 공유합니다. 어느 한쪽이 쓰려고 하면 페이지 폴트가 나고, 그때 해당 페이지만 복사합니다. 그래서 fork 직후에는 데이터 페이지를 복사하지 않아 비용이 작습니다(페이지 테이블은 복제됩니다).

실제 시스템에서는

리눅스 /proc/<pid>/maps — 프로세스의 가상 주소 영역(코드, 힙, 스택, 공유 라이브러리)과 읽기·쓰기·실행 권한을 한 줄씩 보여줍니다.
/usr/bin/time -v (GNU time) — 프로그램을 실행하고 마이너 폴트와 메이저 폴트 횟수를 출력합니다. 메이저 폴트가 많으면 디스크 읽기 때문에 느려졌다는 단서가 됩니다.
Windows — 프로세스마다 독립된 가상 주소 공간을 주고, 물리 메모리가 부족하면 일부 내용을 디스크의 페이징 파일로 내보냅니다.

함께 보면 좋은 토픽

메모리 계층 구조 — 레지스터부터 디스크까지, 가상 메모리가 어느 계층 사이를 잇는지 봅니다.
캐시 메모리 — TLB와 비슷하게 최근 결과를 가까이 두는 캐시의 동작을 봅니다.
가상 메모리(운영체제) — 운영체제 쪽에서 페이지 교체와 스래싱을 어떻게 다루는지 봅니다.
메모리 관리 — 페이징과 세그멘테이션 등 메모리 할당 방식을 비교합니다.

참고 자료

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)의 역할은?

페이지 테이블 조회를 매번 하면 느려서 생긴 캐시입니다.
TLB는 최근에 변환한 가상주소-물리주소 매핑을 캐싱해, 페이지 테이블을 매번 메모리에서 조회하지 않고 빠르게 주소 변환을 할 수 있게 합니다.

Q2

Page Fault(페이지 폴트)가 발생하는 경우는?

페이지가 아직 메모리에 올라오지 않은 상태입니다.
Page Fault는 프로세스가 필요로 하는 페이지가 물리 메모리에 없어, OS가 디스크(스왑)에서 그 페이지를 가져와야 할 때 발생합니다.

Q3

TLB Hit과 TLB Miss 중 더 빠른 것은?

TLB는 캐시이므로 히트가 더 빠릅니다.
TLB Hit은 캐싱된 변환 결과를 바로 쓰므로 빠르고, TLB Miss는 페이지 테이블을 직접 조회해야 해서 상대적으로 느립니다.