본문으로 건너뛰기

안동민 개발노트

본문 시작

요구 페이징과 페이지 폴트

필요한 페이지만 적재하는 요구 페이징의 지역성을 이해하고 페이지 폴트 처리와 Copy-on-Write 흐름을 추적합니다.

가상 메모리의 핵심 아이디어는, 프로세스의 모든 페이지를 미리 메모리에 올리지 않아도 된다는 것입니다.

실제로 필요한 페이지만 메모리에 올리면 됩니다.

이것이 요구 페이징(Demand Paging)이며, 현대 OS 가상 메모리의 기반 전략입니다.

필요한 페이지는 접근될 때 메모리에 올라온다

처음부터 모든 페이지를 적재하지 않고, 실제 접근이 일어난 페이지만 가져온다. valid bit가 0이면 페이지 폴트 처리 경로로 이동한다.

  1. 접근 페이지 테이블 확인 가상 주소

    어느 페이지를 가리키는지 찾는다.

  2. 판정 valid bit 검사 1이면

    메모리 접근, 0이면 폴트 처리로 간다.

  3. 적재 디스크

    페이지 읽기 빈 프레임을 찾거나 교체 대상을 고른 뒤 페이지를 올린다.

  4. 재시도 테이블 갱신 후 명령

    재실행 이제 valid bit가 1이므로 같은 접근이 성공한다.

개념좋은 점비용
지연 적재실제로 쓰는 페이지만 메모리에 올린다.첫 접근에서 페이지 폴트가 발생할 수 있다.
지역성자주 쓰는 근처 페이지만 유지해도 효율이 좋다.지역성이 깨지면 폴트가 급증한다.
작업 집합최근 필요한 페이지 묶음을 기준으로 메모리를 배분한다.너무 작으면 스래싱으로 성능이 무너진다.

요구 페이징의 원리

왜 모든 페이지를 올리지 않는가

프로세스의 코드와 데이터를 전부 메모리에 올리면:

  • 실행 시작이 느려집니다 (수 MB~수 GB를 디스크에서 읽어야 함).
  • 에러 처리 코드, 드물게 실행되는 함수, 대규모 데이터 배열의 일부분 등 실제로 사용되지 않는 부분까지 메모리를 차지합니다.
  • 동시에 실행할 수 있는 프로세스 수가 줄어듭니다.

요구 페이징은 이 문제를 해결합니다.

프로세스가 시작될 때 아무 페이지도 올리지 않을 수 있습니다(순수 요구 페이징).

프로세스가 특정 페이지에 접근하면, 그때 해당 페이지를 디스크에서 메모리로 읽어옵니다.

지역성이 동작하는 이유

게으른(Lazy) 전략이 효율적인 이유는 지역성의 원리(Principle of Locality) 때문입니다.

시간적 지역성: 최근 접근한 메모리 위치를 가까운 미래에 다시 접근할 확률이 높습니다.

루프 변수, 함수 내 지역 변수가 대표적입니다.

공간적 지역성: 접근한 메모리 위치 근처의 메모리를 곧 접근할 확률이 높습니다.

배열 순차 접근, 인접 명령어 실행이 대표적입니다.

이러한 지역성 덕분에, 프로세스는 어느 시점에서든 전체 주소 공간의 극히 일부만 활발하게 사용합니다.

이 활발한 부분을 워킹 셋(Working Set)이라고 합니다.

워킹 셋만 메모리에 있으면 프로세스는 거의 페이지 폴트 없이 실행됩니다.

페이지 테이블의 유효 비트

페이지 테이블 엔트리에 유효 비트(Valid Bit)가 있습니다.

  • 유효 = 1: 해당 페이지가 물리 메모리(프레임)에 존재합니다.
  • 유효 = 0: 해당 페이지가 디스크에 있거나 아직 할당되지 않았습니다.

페이지 폴트 처리 과정

요구 페이징과 페이지 폴트에서는 주소 변환, 테이블 갱신, 메모리/디스크 비용을 확인합니다.

페이지 폴트는 빠진 페이지를 가져온 뒤 같은 명령을 다시 실행한다

CPU trap으로 커널에 들어가고, OS가 backing store에서 페이지를 읽어 PTE를 갱신한 뒤 fault가 난 명령을 재시도한다.

  1. invalid PTE

    trap invalid PTE 커널 page fault handler

  2. disk/swap/file

    locate disk/swap/file 페이지 위치 확인

  3. frame 할당

    load frame 할당 I/O 완료 대기

  4. PTE valid

    resume PTE valid fault 명령 재실행

CPU가 유효 비트 0인 페이지에 접근하면 페이지 폴트(Page Fault) 트랩이 발생합니다.

단계별 처리:

  1. 트랩 발생: CPU가 페이지 폴트 인터럽트를 발생시키고, OS의 페이지 폴트 핸들러에 제어를 넘깁니다.

  2. 주소 유효성 확인: OS가 프로세스의 주소 공간 정보(PCB, VMA 리스트)를 확인합니다. 유효 범위 밖의 접근이면 세그먼테이션 폴트로 프로세스를 종료합니다. 유효 범위 내라면 다음 단계로 진행합니다.

  3. 빈 프레임 확보: 프리 프레임 리스트에서 빈 프레임을 가져옵니다. 빈 프레임이 없으면 페이지 교체 알고리즘으로 기존 페이지를 내보냅니다(다음 절에서 설명).

  4. 디스크 I/O: 해당 페이지를 디스크(스왑 영역 또는 실행 파일)에서 빈 프레임으로 읽어옵니다. 이 I/O 동안 프로세스는 대기(block) 상태로 전환되고, CPU는 다른 프로세스를 실행합니다.

  5. 페이지 테이블 업데이트: I/O가 완료되면, 페이지 테이블 엔트리를 업데이트합니다 — 유효 비트를 1로, 프레임 번호를 기록합니다.

  6. 명령어 재실행: 페이지 폴트를 일으킨 명령어를 처음부터 다시 실행합니다. 이번에는 유효 비트가 1이므로 정상적으로 진행됩니다.

페이지 폴트의 성능 영향

페이지 폴트 처리 시간을 tpft_{pf}, 일반 메모리 접근 시간을 tt, 페이지 폴트 확률을 pp라고 하면:

EAT=(1p)×t+p×tpfEAT = (1 - p) \times t + p \times t_{pf}

t=100nst = 100\text{ns}, tpf=8mst_{pf} = 8\text{ms} (SSD 기준), p=0.001p = 0.001 (1000번에 1번)이면:

EAT=0.999×100+0.001×8000000=99.9+8000=8099.9nsEAT = 0.999 \times 100 + 0.001 \times 8000000 = 99.9 + 8000 = 8099.9\text{ns}

페이지 폴트 확률이 0.1%에 불과해도 성능이 80배 저하됩니다!

페이지 폴트율을 극도로 낮추는 것이 가상 메모리 성능의 핵심입니다.

페이지 폴트를 해석할 때는 원인별 비용과 복구 가능 여부를 함께 보아야 합니다.

페이지 폴트는 원인별로 비용과 처리 상태가 달라진다

같은 트랩이라도 빈 프레임 할당, 파일 읽기, COW 복사, 잘못된 접근은 서로 다른 경로로 분기됩니다.

  1. 낮은 폴트율이 성능 조건입니다

    EAT 일반 접근은 ns 단위지만 major fault는 ms 단위라서 작은 확률도 평균 시간을 크게 끌어올립니다.

  2. 주소가 VMA 안에 있는가?

    프로세스에 허용된 가상 주소 범위 확인

  3. 권한·PTE 상태 확인

    미적재면 minor/major/COW 경로로 복구

  4. 복구 불가 fault

    SIGSEGV 등으로 프로세스에 오류 전달

  5. minor
    프레임만 연결

    minor 0 페이지나 공유 페이지처럼 디스크 읽기 없이 PTE를 갱신합니다.

  6. major
    디스크에서 적재

    major 스왑 또는 실행 파일에서 읽는 동안 프로세스는 block 상태가 됩니다.

  7. COW
    쓰기 때 복사

    COW 읽기 전용 공유 페이지에 쓰면 새 프레임을 만들고 권한을 바꿉니다.

  8. fault
    프로세스 종료

    fault 주소 범위 밖이거나 권한 회복이 불가능하면 예외로 처리합니다.

  9. 프레임 확보 판단

    즉시 할당하고 PTE valid 비트를 1로 바꿉니다. 디스크 쓰기 없이 프레임을 회수해 새 페이지를 적재합니다. page out 후 재사용하므로 I/O 대기 시간이 추가됩니다.

  10. free frame 있음

    즉시 할당하고 PTE valid 비트를 1로 바꿉니다.

  11. clean victim

    디스크 쓰기 없이 프레임을 회수해 새 페이지를 적재합니다.

  12. dirty victim

    page out 후 재사용하므로 I/O 대기 시간이 추가됩니다.

page_fault_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>

int main() {
    struct rusage before, after;
    getrusage(RUSAGE_SELF, &before);

    /* 대량 메모리 할당 및 접근 — 페이지 폴트 유발 */
    size_t size = 100 * 1024 * 1024;  /* 100MB */
    char *buf = malloc(size);
    for (size_t i = 0; i < size; i += 4096) {
        buf[i] = 1;  /* 각 페이지의 첫 바이트에 접근 */
    }

    getrusage(RUSAGE_SELF, &after);
    long minor = after.ru_minflt - before.ru_minflt;
    long major = after.ru_majflt - before.ru_majflt;
    printf("Minor page faults: %ld\n", minor);  /* ~25600 */
    printf("Major page faults: %ld\n", major);   /* 0 (메모리 충분시) */

    free(buf);
    return 0;
}

Minor page fault: 디스크 I/O 없이 해결되는 페이지 폴트.

예: 0으로 초기화된 빈 프레임을 할당하거나, 이미 메모리에 있는 공유 페이지를 매핑.

Major page fault: 디스크에서 페이지를 읽어야 하는 페이지 폴트.

수 밀리초 소요.

성능에 심각한 영향.


Copy-on-Write

fork 직후에는 공유하고 쓰는 순간에만 복사한다

부모와 자식은 같은 물리 페이지를 read-only로 가리키다가 어느 한쪽이 쓰기를 시도하면 fault가 발생하고 그때 새 페이지를 만든다.

  1. share page

    fork share page PTE read-only

  2. same frame

    read same frame 복사 없음

  3. COW trigger

    write fault COW trigger 커널이 새 frame 할당

  4. private page

    after copy private page 쓴 쪽만 새 값 보유

fork() 시스템 콜로 자식 프로세스를 만들면, 부모의 전체 주소 공간을 복사해야 합니다.

4GB 주소 공간을 복사하면 엄청난 시간과 메모리가 소요됩니다.

Copy-on-Write(COW)는 이 문제를 우아하게 해결합니다.

  1. fork() 직후: 부모와 자식이 같은 물리 페이지를 공유합니다.
  2. 공유된 페이지는 읽기 전용으로 표시됩니다.
  3. 둘 중 하나가 페이지에 쓰기를 시도하면, 보호 위반 트랩이 발생합니다.
  4. OS가 해당 페이지만 복사(Copy)하고, 쓰기를 허용합니다.
  5. 수정이 발생한 페이지만 복사되므로 불필요한 복사를 완전히 피합니다.
cow_example.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main() {
    int *data = malloc(sizeof(int));
    *data = 100;

    pid_t pid = fork();
    /* fork 직후: parent와 child가 data 페이지를 공유 (COW)
       data가 있는 물리 프레임은 하나, 양쪽 페이지 테이블이 같은
       프레임을 가리킴. 읽기 전용으로 표시됨. */

    if (pid == 0) {
        /* 자식: 쓰기 시도 → 보호 위반 트랩 → OS가 페이지 복사 */
        *data = 200;
        printf("Child: %d\n", *data);  /* 200 (복사된 페이지) */
        free(data);
        _exit(0);
    } else {
        wait(NULL);
        printf("Parent: %d\n", *data);  /* 100 (원본 유지) */
        free(data);
    }
    return 0;
}

많은 프로세스가 fork() 후 바로 exec()를 호출하여 새 프로그램을 실행합니다.

exec()는 주소 공간을 완전히 교체하므로, COW 덕분에 fork()에서 복사된 페이지가 제로입니다.

Linux에서는 fork() 대안으로 COW를 개선한 vfork() 도 있습니다.

vfork()는 부모를 일시 중단하고 자식이 부모의 주소 공간을 직접 사용합니다.

exec() 직전까지만 사용하므로 페이지 테이블 복사조차 필요 없습니다.


빈 프레임이 없다면 — 페이지 교체의 필요성

물리 메모리가 가득 차서 빈 프레임이 없는 상태에서 페이지 폴트가 발생하면, 기존에 메모리에 있던 페이지 중 하나를 내보내야(Evict) 합니다.

내보내는 페이지가 수정되었다면(더티 비트 = 1), 디스크에 써야 하므로 추가 I/O가 발생합니다(Page out).

수정되지 않은 깨끗한 페이지(더티 비트 = 0)는 디스크에 이미 원본이 있으므로, 그냥 프레임을 반환하면 됩니다.

따라서 더티 비트의 값에 따라 페이지 교체의 비용이 크게 달라집니다.

어떤 페이지를 내보낼지 결정하는 것이 페이지 교체 알고리즘이며, 이것이 가상 메모리 성능의 핵심입니다.

잘못된 페이지를 내보내면 곧바로 다시 필요해져서 또 페이지 폴트가 발생합니다.

메모리 매핑 파일 (Memory-Mapped Files)

페이지 폴트 메커니즘의 응용으로, 메모리 매핑 파일이 있습니다.

mmap() 시스템 콜로 파일을 프로세스의 주소 공간에 직접 매핑합니다.

파일의 내용이 메모리처럼 접근 가능해지며, 실제 데이터는 페이지 폴트를 통해 자동으로 로드됩니다.

mmap_example.c
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    int fd = open("data.bin", O_RDONLY);
    /* 파일을 메모리에 매핑 */
    char *mapped = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);

    /* 메모리처럼 접근 — 첫 접근 시 page fault → OS가 파일에서 읽음 */
    printf("첫 번째 바이트: %c\n", mapped[0]);

    munmap(mapped, 4096);
    return 0;
}

다음 절에서 다양한 페이지 교체 알고리즘과 그 성능 비교를 살펴보겠습니다.


페이지 폴트는 오류처럼 보이지만, 요구 페이징에서는 필요한 페이지를 늦게 올리는 정상 경로이기도 합니다.

페이지 폴트 처리 단계와 비용

CPU가 유효하지 않은 페이지 테이블 항목을 만나면 커널이 접근 권한과 주소를 확인하고 필요한 페이지를 가져옵니다.

  1. 1
    커널 진입

    Trap MMU가 페이지 부재를 감지하면 커널의 예외 처리로 들어갑니다.

  2. 2
    주소와 권한 확인

    검사 참조가 합법인지, 읽기와 쓰기 권한이 맞는지 확인합니다.

  3. 3
    빈 프레임 확보

    적재 필요하면 희생 페이지를 내보내고 디스크에서 페이지를 읽습니다.

  4. 4
    명령 재실행

    재개 페이지 테이블을 갱신한 뒤 중단된 명령을 다시 실행합니다.

  5. 5
    성능을 읽는 두 종류

    디스크 I/O 없이 빈 프레임 할당이나 공유 페이지 매핑으로 해결됩니다. 디스크에서 페이지를 읽어야 하므로 밀리초 단위 지연이 생깁니다. 쓰기 시점에 공유 페이지를 복사해 fork 비용을 뒤로 미룹니다.

  6. 6
    Minor

    디스크 I/O 없이 빈 프레임 할당이나 공유 페이지 매핑으로 해결됩니다.

  7. 7
    Major

    디스크에서 페이지를 읽어야 하므로 밀리초 단위 지연이 생깁니다.

  8. 8
    COW

    쓰기 시점에 공유 페이지를 복사해 fork 비용을 뒤로 미룹니다.

페이지 폴트 분기와 프레임 확보

페이지 폴트는 “아직 안 올린 페이지”와 “접근하면 안 되는 주소”를 커널이 구분하는 과정입니다. 유효성 검사 뒤 frame 확보와 디스크 I/O가 이어집니다.

  1. 1
    02

    validate PTE

  2. 2
    03

    allocate / read

  3. 3
    04

    restart

  4. 4
    빈 frame이 있는가?

    있으면 즉시 page in

  5. 5
    희생 page 선택

    clean이면 즉시 재사용 · dirty이면 먼저 write-back

  6. 6
    PTE·TLB 갱신

    valid=1로 바꾸고 faulting instruction 재실행

  7. 7
    trap CPU

    faulting address와 접근 종류를 남기고 커널 핸들러로 들어간다.

  8. 8
    validate PTE 주소 범위, present

    bit, R/W/X 권한을 확인해 demand paging과 보호 위반을 나눈다.

  9. 9
    allocate / read 빈 frame

    찾거나 희생 페이지를 고른 뒤 swap/file에서 데이터를 읽는다.

  10. 10
    restart PTE

    TLB를 갱신한 뒤 같은 명령을 재실행하거나 SIGSEGV로 종료한다.

아래 다이어그램은 페이지 폴트 후 디스크 로드와 페이지 테이블 갱신이 이어지는 과정을 핵심 질문과 판단 순서로 정리한 것입니다.