Week 11. 정글 끝까지(PintOS) - Virtual Memory

2026. 6. 6. 03:19·Krafton-Jungle/WIL

이번 주 목표

이번 주의 핵심은 "운영체제가 메모리 부족을 어떻게 감추는지 직접 만들어보기"이다.

지난 주가 사용자 프로그램이 커널 위에서 실행되고 시스템 콜로 자원을 요청하는 흐름이었다면, 이번 주는 그 프로그램이 사용하는 메모리를 운영체제가 어떻게 관리하는지 따라간다. 사용자 입장에서는 넓은 가상 주소 공간을 자유롭게 쓰는 것처럼 보이지만, 커널은 뒤에서 페이지를 가져오고 내보내며 그 착시를 만들어낸다.

구체적으로는 Supplemental Page Table(SPT), Frame Table, Lazy Loading, Page Fault Handling, Anonymous Page, Stack Growth, Memory Mapped Files, Swap In/Out을 구현한다.
이번 주는 그 중 lazy_load_segment()까지 구현을 완료했다.

 


 

어디까지, 어떻게 시도했는가?

🤝 우리 팀의 협업 방식

이번 주는 Phase 1과 Phase 2로 계획을 세웠다.

코딩에 들어가기 전에 팀원 각자가 공부한 내용을 먼저 발표하는 시간을 가졌다. 큰 흐름은 대부분이 비슷하게 이해하고 있었지만, 세부적인 부분에서 팀원마다 조금씩 다른 부분이 있었다. 서로 다른 지식이 모이면서 개념이 더 입체적으로 잡혔다.

Phase 1 (5/11~5/14) 은 4명이 함께였다. SPT, Frame Table, Uninit + Page Fault Handler까지 공통 뼈대를 먼저 만들고 main에 merge하는 방식이었다. 모든 팀원이 같은 자료구조를 써야 이후 충돌이 줄어들기 때문이다.

Phase 2 (5/15~) 는 A팀/B팀으로 나눠서 병렬로 진행하려 했다.
그런데 실제로는 시간이 빠듯해서 팀 분리보다 각자 구현을 먼저 진행하는 방식이 됐다. 역할을 나눠서 병렬로 진행하는 것이 계획이었지만, 시간 압박 속에서 자연스럽게 각자 할 수 있는 만큼 구현하고, 코어타임에 모여 서로 코드를 비교하는 흐름으로 바뀌었다.

merge는 서로의 코드를 함께 보면서 어떤 구현이 더 나은지 회의한 뒤 결정했다. 단순히 "누구 코드를 main에 올릴지" 정하는 게 아니라, 각자가 다르게 접근한 부분을 같이 읽으면서 왜 그렇게 짰는지 설명하는 과정이 있었다.

팀 전체로는 lazy_load_segment 이후로도 진도가 나갔지만, 각자가 완전히 이해하고 스스로 구현까지 완료한 범위는 lazy_load_segment()까지였다.

🗺️ 전체 흐름 먼저 잡기

이번 주는 코드보다 그림을 먼저 그렸다. VM 흐름은 크게 세 가지 루트로 나뉜다.

1. 초기화 루트
   main() → exception_init() → vm_init()

2. 페이지 등록 루트 (코드 로딩)
   initd() → process_exec() → load() → load_segment()
   → vm_alloc_page_with_initializer() → uninit_new() → spt_insert_page()
   (RAM엔 없음, SPT에만 등록 — Lazy Loading)

3. 실제 로딩 루트 (Page Fault)
   page_fault() → vm_try_handle_fault() → spt_find_page()
   → vm_do_claim_page() → swap_in() → uninit_initialize()
   → lazy_load_segment() → RAM에 올라옴

이 세 루트를 먼저 잡고 들어가니 어디에 무엇을 구현해야 하는지 보였다.

🏗️ 5/11 설계 회의

Phase 1 첫날, 코드를 치기 전에 설계 회의를 먼저 했다. 핵심 결정 사항은 세 가지였다.

첫째, 자료구조 결정. SPT는 해시테이블, Frame Table은 리스트로 결정했다. 처음에는 "배열로 세그먼트를 먼저 찾고, 그 안에서 페이지를 찾는" O(n\*m) 방식도 제안됐는데, SPT는 page fault 때마다 spt_find_page(spt, va)로 VA 하나를 빠르게 찾는 것이 핵심이라 해시테이블의 평균 O(1)이 훨씬 유리하다는 쪽으로 합의했다. PintOS가 이미 lib/kernel/hash.c를 제공하니 가져다 쓰면 됐다. Frame Table은 삽입/삭제가 빈번하고 순회가 주 목적이라 리스트가 더 자연스럽다고 판단했다.

둘째, struct page 추가 멤버. struct hash_elem hash_elem, bool writable은 필수였다. 구현하면서 struct thread *owner와 bool accessed도 추가했다. 위치는 union 위에 넣어야 한다. union은 타입별 데이터를 담는 공간이라 건드리면 안 되고, 우리 멤버는 모든 타입 공통이기 때문이다.

셋째, SPT 방식. Page 기반 vs Segment 기반을 비교한 뒤, SPT 자체는 Page 기반(VA → page 1:1)을 유지하되 mmap을 위해 file_page 안에 segment 정보를 추가 보관하는 Page + Segment 혼합 방식을 택했다. Segment 기반으로 가면 spt_find_page(spt, va) 인터페이스 자체를 바꿔야 해서 팀 프로젝트에서 리스크가 너무 컸다.

📋 SPT & Frame 구현

Phase 1의 핵심이었다.

SPT는 해시테이블 필드 이름을 page_hash로 두고 구현했다.

supplemental_page_table_init() → hash_init(&spt->page_hash, page_hash, page_less, NULL)
spt_find_page(spt, va)         → pg_round_down(va) → hash_find() → hash_entry() → page 반환
spt_insert_page(spt, page)     → hash_insert()

spt_find_page()에는 NULL 체크와 커널 주소 체크를 ASSERT 대신 if문으로 처리했다. 어디서 호출될지 모르기 때문에 방어적으로 작성했다. pg_round_down(va)는 안에서 해주는 게 맞다고 판단했다.

vm_alloc_page_with_initializer()는 malloc -> uninit_new() -> page->writable = writable 순서로 구현했다. uninit_new()가 *page 전체를 초기화하므로 호출 후에 writable을 설정해야 한다는 점이 중요했다.

Frame 관련 함수는 이렇다.

vm_get_frame()       : palloc_get_page(PAL_USER) → frame malloc → 실패 시 PANIC("todo")
vm_do_claim_page()   : vm_get_frame() → page↔frame 연결 → pml4_set_page() → swap_in()
vm_claim_page(va)    : spt_find_page() → vm_do_claim_page() 호출

아직 eviction은 구현하지 않아서, 빈 frame이 없으면 일단 PANIC("todo")로 처리해두었다.

💤 Lazy Loading

load_segment() (VM 모드)는 파일 내용을 바로 읽지 않고, 읽어야 할 정보를 load_info 구조체에 담아 SPT에 등록만 한다.

struct load_info {
    struct file *file;     // file_reopen()으로 복제한 파일 객체
    off_t offset;          // 파일 내 읽기 위치
    size_t page_read_bytes;
    size_t page_zero_bytes;
};

한 가지 중요한 점은 file 포인터를 그대로 저장하는 게 아니라 file_reopen(file)로 복제한다는 것이다. 원본 파일이 닫히거나 seek 위치가 바뀌어도 lazy_load_segment가 나중에 올바른 위치에서 읽을 수 있게 하기 위해서다.

page fault가 발생하면 lazy_load_segment()가 호출된다.

static bool lazy_load_segment(struct page *page, void *aux) {
    struct load_info *load_info = aux;
    uint8_t *kva = page->frame->kva;

    if (file_read_at(load_info->file, kva,
                     load_info->page_read_bytes,
                     load_info->offset) == load_info->page_read_bytes) {
        memset(kva + load_info->page_read_bytes, 0, load_info->page_zero_bytes);
        success = true;
    }
    free(load_info);
    return success;
}

처음엔 file_seek() + file_read()를 쓰려 했는데, 팀원이 file_read_at() 을 쓰면 seek 없이 offset을 직접 지정할 수 있다는 걸 알려줘서 바꿨다. 훨씬 깔끔했다. 이와 비슷하게 팀원 중 하나는 SPT 구조체를 직접 구현했다가, 나중에 PintOS hash.c에 이미 hash_init이 제공된다는 걸 뒤늦게 발견하기도 했다. API를 먼저 훑어보는 습관이 필요하다는 걸 팀 전체가 이번에 반복해서 느꼈다.

setup_stack()도 기존 palloc 직접 호출에서 VM 시스템을 거치도록 수정했다.

static bool setup_stack(struct intr_frame *if_) {
    void *stack_bottom = (void *)(((uint8_t *)USER_STACK) - PGSIZE);
    if (vm_alloc_page(VM_ANON, stack_bottom, true)) {
        success = vm_claim_page(stack_bottom);
        if (success)
            if_->rsp = USER_STACK;
    }
    return success;
}

스택은 Lazy가 아니라 vm_claim_page()까지 즉시 호출해서 바로 Frame을 할당한다.

🔥 vm_try_handle_fault()

Page fault handler를 3가지 케이스로 나눠서 구현했다.

bool vm_try_handle_fault(...) {
    // PT에 매핑이 있었는데 fault → write-protection 위반 → false
    if (not_present == false) return false;

    struct page *found_page = spt_find_page(spt, addr);

    if (found_page != NULL) {
        // write 시도인데 읽기 전용 페이지 → false
        if (found_page->writable != write) return false;
        return vm_do_claim_page(found_page);
    } else {
        // SPT에 없을 때 → Stack Growth 조건 확인
        if (!is_certified_stackgrowth()) return false;
        if (user != true) return false;
        vm_stack_growth(addr);
    }
}

not_present == false는 page table에 매핑은 있는데 fault가 난 경우, 즉 write-protection 위반이다. 이 경우는 바로 false를 반환한다. found_page->writable != write 체크도 추가했다.

is_certified_stackgrowth()도 항상 true를 반환하는 임시 함수로 두었다.

🔌 page_operations: 같은 호출, 다른 실행

이번 주 발표에서는 내가 PintOS의 page operations — 페이지 타입별 동작 분리 구조를 맡았다.

VM_UNINIT, VM_ANON, VM_FILE 세 가지 페이지 타입이 있고, 타입마다 동작이 다른데 VM 코드는 swap_in(page, kva)처럼 공통된 방식으로 호출한다. page->operations가 실제 함수를 결정한다.

UNINIT → uninit_initialize()
ANON   → anon_swap_in()
FILE   → file_backed_swap_in()

VM_UNINIT은 단순한 임시 상태가 아니라 lazy loading의 핵심 장치다. 처음 페이지를 만들 때 UNINIT으로 등록하고, 첫 접근 시 page fault → vm_do_claim_page() → swap_in() → uninit_initialize()를 통해 실제 타입으로 전환된다.

발표를 준비하면서 코드를 다시 따라가니 "page fault가 오류가 아니라 lazy loading의 트리거"라는 게 명확하게 보였다. 그리고 한 가지 궁금한 점이 생겼다. page fault를 처리하고 나면 커널은 어떻게 원래 명령어로 돌아가 다시 실행하는가. do_iret()으로 인터럽트 프레임을 복원하면서 faulted instruction으로 되돌아가는 구조라는 건 알았지만, 그 흐름을 코드로 직접 따라가지는 못했다. 다음에 짚어봐야 할 부분이다.

🔄 Swap In/Out 흐름 이해

직접 구현은 Phase 2 몫이지만, 팀이 함께 흐름을 정리해두었다.

Swap In은 page 내용을 frame에 채우는 과정이다. 타입에 따라 출처가 다르다.

VM_FILE  : file_read_at(file, kva, read_bytes, offset) → memset 0채우기
VM_ANON  : swap slot 있음? → swap에서 PGSIZE 전체 읽기
           swap slot 없음? → frame 0으로 초기화 (처음 올라온 ANON page)

VM_ANON의 swap slot 없는 경우가 처음엔 의아했는데, 스택이나 힙처럼 파일 backing이 없는 페이지는 처음 올라올 때 그냥 0으로 채우면 된다는 걸 알고 나서 이해됐다.

Swap Out은 victim page를 frame에서 내보내는 과정이다.

VM_FILE  : pml4_is_dirty? → Y → file_write_at으로 write-back
                          → N → 그냥 버림 (다음에 파일에서 다시 읽으면 됨)
VM_ANON  : swap 영역에 PGSIZE 전체 저장 → page->anon.slot 기록
공통 정리 : pml4_clear_page(page->va) → page->frame = NULL → frame 재사용 가능

VM_FILE은 dirty할 때만 write-back하면 된다는 게 핵심이다. 파일이 backing store이기 때문에, clean한 경우엔 그냥 버려도 나중에 파일에서 다시 읽어오면 된다. VM_ANON은 파일이 없으므로 반드시 swap 영역에 저장해야 한다.

이 차이를 이해하고 나서야 VM_FILE과 VM_ANON의 설계 이유가 더 명확하게 보였다. 단순히 "파일이냐 아니냐"의 구분이 아니라, 데이터의 원본이 어디 있느냐에 따라 eviction 전략 전체가 달라지는 구조였다.

 


 

이번 주 아쉬웠던 점

vm_stack_growth()와 supplemental_page_table_copy()를 이번 주에 완성하지 못한 것이 가장 아쉬웠다. 내 코드에서도 vm_try_handle_fault() 안에서 호출하는 구조이기 때문에 흐름을 완전히 이해하지 못한 채 합치게 된 느낌이 들었다.

file_read_at()을 뒤늦게 알게 된 것도 아쉬웠다. 처음엔 file_seek + file_read로 짜다가 팀원 코드 리뷰 때서야 더 나은 방법이 있다는 걸 알았다. 코드를 치기 전에 PintOS API를 더 훑어봤다면 처음부터 쓸 수 있었을 것이다.

vm_get_frame()에서 eviction이 필요한 경우를 PANIC("todo")로 막아뒀다. Swap In/Out 흐름은 이해했지만 구현은 Phase 2 몫이다. 이 부분이 남아 있는 한 메모리 부족 상황에서 커널이 그냥 죽는다.

그리고 이번 주에 팀 전체에서 반복적으로 느낀 게 있었다. file_read_at(), hash_init(), pml4_is_dirty() 같은 PintOS 내장 함수들을 구현을 시작하고 나서야 발견하는 패턴이 계속 반복됐다. 구현 전에 관련 API를 먼저 훑어보는 습관을 들여야겠다.

 


 

다음 주 계획

다음 주는 PintOS를 떠나 신경망 직접 구현 과제로 넘어간다.

PyTorch, TensorFlow 같은 프레임워크 없이 NumPy만으로 신경망의 핵심 구성 요소를 직접 만들고, MNIST 손글씨 숫자 분류기를 구현하는 것이 목표다. 최종 목표는 테스트 정확도 97% 이상이다.

핵심은 Forward → Loss → Backward → Update 흐름을 직접 연결해보는 것이다. ReLU, Softmax, Affine, BatchNorm, Dropout 같은 레이어를 하나씩 구현하고, 각 단계의 테스트를 통과시키는 순서로 진행한다.

ReLU / Softmax → Affine → cross_entropy_loss
→ SGD / Adam → NeuralNetwork → BatchNorm / Dropout → train / evaluate

이번 PintOS처럼 처음부터 전체 흐름을 그려두고 각 구성 요소가 어디서 어떻게 연결되는지 먼저 파악한 뒤 코드로 들어가고 싶다. 정확도를 맞히는 것보다 팀원 모두가 forward에서 backward까지 흐름을 설명할 수 있는 상태가 먼저다.

 


 

11주차 팀원 블로그

https://velog.io/@dolggul2/WIL-week-11-12

 

[WIL] week 11-12

wil 11-12

velog.io

https://velog.io/@sjinbk/Pintos-Project-3%EC%97%90%EC%84%9C-%EB%A7%88%EC%A3%BC%ED%95%9C-%EC%A0%95%EC%A0%81%EB%B6%84%EC%84%9D%EA%B3%BC-cflow-%EC%82%AC%EC%9A%A9%EB%B2%95

 

Pintos Project 3에서 마주한 정적분석과 cflow 사용법

Pintos 주차가 진행됨에 따라 vm/, userprog/, threads/에 흩어진 파일들이 페이지 폴트(page fault) 하나로 연결되어 있었고, vm_try_handle_fault() 같은 함수가 어디서 호출되고 어떤 흐름으로 이어지는지 정확

velog.io

https://velog.io/@dnjsdn818/posts

 

dnjsdn818 (김원우) / 작성글 - velog

 

velog.io

 

반응형

'Krafton-Jungle > WIL' 카테고리의 다른 글

Week 10. 정글 끝까지(PintOS) - User Programs  (0) 2026.05.21
Week 9. 정글 끝까지(PintOS) - Threads  (0) 2026.05.02
Week 8. 탐험 준비  (1) 2026.04.23
Week 7. 탐험 준비  (0) 2026.04.16
Week 6. 탐험 준비  (0) 2026.04.09
'Krafton-Jungle/WIL' 카테고리의 다른 글
  • Week 10. 정글 끝까지(PintOS) - User Programs
  • Week 9. 정글 끝까지(PintOS) - Threads
  • Week 8. 탐험 준비
  • Week 7. 탐험 준비
Jio_ni
Jio_ni
안녕하세요!! 개발 공부를 하고 있는 뽀시래기 강지현입니다!!
  • Jio_ni
    지현이의 개발 블로그
    Jio_ni
  • 전체
    오늘
    어제
    • 분류 전체보기 (240)
      • LG AI (4)
      • About (0)
      • ELITE HACKER bootcamp (12)
        • Pre.web (12)
        • Main.web (0)
      • Study (31)
        • Python (13)
        • C언어 (0)
        • Java (0)
        • HTML (8)
        • CSS (0)
        • Linux (0)
        • Web hacking (0)
        • git (4)
        • 혼공학습단 (1)
        • 유니티 (1)
        • 코딩 자율학습단 (4)
      • Krafton-Jungle (36)
        • WIL (12)
        • 개인 공부 기록용 - Python (11)
        • 백준 (4)
        • LeetCode (1)
        • 개인 공부 기록용 - C (8)
      • Project (0)
      • 코딩테스트 (153)
        • CodeUp (76)
        • 프로그래머스 (20)
        • 백준 (47)
        • SWEA (10)
      • 자격증 (1)
      • Book (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ADsP 자격증 합격 후기
    ADsP 벼락치기
    크래프톤 정글
    Python
    개발자
    에세이
    CodeUp
    전공자
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
Jio_ni
Week 11. 정글 끝까지(PintOS) - Virtual Memory
상단으로

티스토리툴바