2025.05.31

TIL(TODAY I LEARN)


  • 오늘한 내용 : PintOS - Project3: Virtual Memory - gitbook 정리 중! - Memory Management, Anonymous Page, Stack Growth

  • WEEK 12 : 정글 끝까지(PintOS) - Virtual Memory


Memory Management

페이지(Page) 구조

  • 프로그램이 사용하는 가상 메모리 4KB 단위를 페이지라고 부름.
  • struct page는 페이지 한 장에 관한 메타데이터를 담고 있음.
    struct page {
      const struct page_operations *operations;  // 페이지 타입별 함수 모음
      void *va;                                  // 사용자 가상 주소
      struct frame *frame;                       // 연결된 물리 프레임 정보
      
    	/* 페이지 종류별 추가 정보 */  
      union {
        struct uninit_page uninit;               // 아직 초기화되지 않은 페이지
        struct anon_page  anon;                  // 익명 메모리(스택·힙) 페이지
        struct file_page  file;                  // 파일에서 읽어올 페이지
        /* 기타 페이지 타입 추가 가능*/
      };
    };

2. 페이지 연산(Page Operations)

  • 각 페이지는 자신의 타입에 맞는 swap_in, swap_out, destroy 함수를 가짐.
  • struct page_operations 테이블에 함수 포인터로 등록되어 있어, 호출 시 적절한 구현이 실행.
    struct page_operations {
      bool (*swap_in)  (struct page *, void *);  // 스왑에서 꺼내 올 때
      bool (*swap_out) (struct page *);          // 스왑으로 내보낼 때
      void (*destroy)  (struct page *);          // 페이지 제거할 때
      enum vm_type type;                         // 파일용인지 익명용인지 표시
    };
    
  • 예컨대 destroy(page) 호출은 내부적으로 page->operations->destroy(page)를 실행합니다.

3. 보조 페이지 테이블(Supplemental PT)

  • 기본 페이지 테이블(PTE)만으로는 “이 페이지를 어떻게 읽어올지” 알 수 없으므로, 추가 정보를 저장하는 별도 테이블이 필요.
  • 담아야 할 정보 예시
    • 가상 주소(va)
    • 페이지 종류(익명, 파일, 초기화 안 됨)
    • 파일의 어디서 읽어 올지(오프셋) 또는 스왑 슬롯 번호
  • 구현해야 할 주요 함수
    1. supplemental_page_table_init(spt) – 프로세스 시작·복제 시 SPT 초기화
    2. spt_find_page(spt, va) – 가상 주소 va에 대응하는 struct page 검색
    3. spt_insert_page(spt, page) – 새 페이지를 SPT에 등록

4. 프레임(Frame) 관리

  • 물리 메모리 4KB 단위 : 프레임.
  • struct frame은 물리 프레임의 커널 가상 주소(kva)와, 이를 사용하는 page를 연결.
    struct frame {
      void *kva;        // 커널 가상 주소(실제 물리 메모리)
      struct page *page;// 매핑된 페이지
      /* 필요 시 추가 필드 가능*/
    };
  • 구현해야 할 주요 함수
    1. vm_get_frame()
      • palloc_get_page(PAL_USER)로 빈 프레임 확보
      • struct frame 구조체를 할당·초기화 후 반환
    2. vm_claim_page(page)
      • vm_get_frame()로 프레임을 받은 뒤
      • install_page(page->va, frame->kva, writable) 호출로 PTE에 매핑
    3. vm_do_claim_page(va)
      • spt_find_pagepage 조회 후 → vm_do_claim_page(page) 실행

정리

  1. 페이지 구조에 “내가 어떤 종류인지”와 “데이터 어디 있는지” 담고,
  2. 함수 포인터로 타입별 동작 정의해 놓고,
  3. SPT로 “가상 주소 → 페이지 정보” 관리,
  4. 프레임으로 실제 RAM 칸 빌리고,
  5. install_page로 CPU 페이지 테이블에도 매핑
  • “페이지를 어디서 가져와야 하나?”
    • SPT 보고 정보 꺼내고
    • vm_get_frame()로 빈 칸 확보 → 필요하면 스왑아웃
    • 데이터 로드 → install_page()

Anonymous Page

1. 익명 페이지(Anonymous Page) 개념

  • 정의: 파일이나 디바이스에 기반하지 않는 메모리 영역이다.
    • 실행 파일의 스택, 힙 영역 등에서 사용된다.
    • 이름이 없으므로 “익명(anonymous)”이라 부른다.
  • 주요 구조체
    • struct anon_page (include/vm/anon.h)
      struct anon_page {
        /* 필요한 경우 스왑 슬롯 번호, 추가 상태 정보 등을 여기에 저장 */
      };
    • struct page (include/vm/page.h) 내 anon 멤버
      
      struct page {
        const struct page_operations *operations;  // 페이지 타입별 함수 포인터
        void *va;                                  // 사용자 가상 주소
        struct frame *frame;                       // 연결된 물리 프레임 정보
        union {
          struct uninit_page uninit;               // 초기화되지 않은 페이지
          struct anon_page  anon;                  // 익명 페이지 정보
          struct file_page  file;                  // 파일 기반 페이지 정보
          /* (추가 페이지 타입) */
        };
      };
    • 익명 페이지를 생성·관리할 때 struct anon_page anon 필드에 필요한 데이터를 저장한다.

2. Lazy Loading 개념

  • 정의: 필요한 시점(페이지 폴트 발생)까지 실제 메모리 할당과 데이터 로드를 미루는 설계 방식이다.
    • 페이지 구조체(struct page)만 미리 생성해 둔다.
    • 실제 물리 프레임과 데이터 로드는 사용자가 해당 페이지에 처음 접근할 때(페이지 폴트) 수행한다.
  • 장점: 초기 실행 시 불필요한 메모리 로드를 줄여 오버헤드를 감소시킨다.

3. VM_UNINIT 페이지 타입

  • 모든 페이지(익명, 파일 기반 등)는 처음에 VM_UNINIT 타입으로 생성된다.
  • 관련 구조체
    • struct uninit_page (include/vm/uninit.h)
      struct uninit_page {
        /* 페이지 타입별 initializer 함수 포인터, aux 데이터 등 */
        vm_initializer *initializer;  // 초기화 시 호출할 함수
        void *aux;                    // 초기화에 필요한 추가 정보
      };
    • struct pageuninit 멤버
  • lifecyle
    1. vm_alloc_page_with_initializer 호출 → VM_UNINIT 페이지 생성
    2. 프로그램 실행 중 해당 페이지에 접근 → 페이지 폴트 발생
    3. uninit_initialize 호출 → 설정해 둔 initializer(anon_initializer 또는 file_backed_initializer) 실행 → 실제 페이지 초기화
    4. 이후 swap-in, swap-out, destroy 등 상태 전환

4. vm_alloc_page_with_initializer() 구현

bool vm_alloc_page_with_initializer(
    enum vm_type type,
    void *va,
    bool writable,
    vm_initializer *init,
    void *aux);
  • 역할
    1. type에 맞는 initializer 함수를 가져와 uninit_new 호출
    2. struct page 객체를 VM_UNINIT 상태로 생성하고, page->operations에 initializer·aux 정보 저장
    3. 프로세스의 보조 페이지 테이블(SPT)에 page 삽입
  • 구현 팁
    • VM_TYPE(type) 매크로를 사용해 type별 분기 처리
    • spt_insert_page 호출 전, 동일 va 존재 여부 확인

5. uninit_initialize() 구현

static bool uninit_initialize(struct page *page, void *kva);
  • 호출 시점: 페이지 폴트 처리 중, page->operations->initializer가 호출되기 전
  • 주요 작업
    1. page->operations 내부에 저장된 vm_initializer *aux 꺼내기
    2. initializer(page, type, kva) 호출
      • 익명 페이지(VM_ANON) → anon_initializer(page, type, kva)
      • 파일 기반 페이지(VM_FILE) → file_backed_initializer(page, type, kva)
    3. 반환값에 따라 초기화 성공/실패 판단

6. 익명 페이지 서브시스템 초기화 및 anon_initializer() 구현

6.1 vm_anon_init()

void vm_anon_init(void);
  • 역할: 익명 페이지 처리를 위한 전역 자료구조 초기화
  • 주요 작업 예시
    • 스왑 슬롯 관리용 비트맵 초기화
    • 익명 페이지 관련 락(lock) 초기화

6.2 anon_initializer()

bool anon_initializer(
    struct page *page,
    enum vm_type type,
    void *kva);
  • 호출 시점: 익명 페이지가 처음으로 페이지 폴트 날 때 (VM_UNINIT → VM_ANON 전환)
  • 주요 작업
    1. page->operations에 swap_in, swap_out, destroy 함수 포인터 설정

      page->operations->swap_in  = anon_swap_in;
      page->operations->swap_out = anon_swap_out;
      page->operations->destroy  = anon_destroy;
      page->operations->type     = VM_ANON;
    2. page->frame 설정: frame = vm_get_frame()로 새 프레임 할당 (페이지 폴트 처리 흐름에서 실제로 수행됨)

    3. anon_page 멤버에 필요 정보(스왑 슬롯 번호 등) 초기화

    4. 페이지 내용을 제로 초기화 (익명 페이지는 실행 시 초기화 필요)

    5. PTE에 install_page(page->va, kva, writable) 호출하여 매핑 완료


7. load_segment()lazy_load_segment() 구현

7.1 load_segment()

static bool load_segment(
    struct file *file,
    off_t ofs,
    uint8_t *upage,
    uint32_t read_bytes,
    uint32_t zero_bytes,
    bool writable);
  • 역할: 실행 파일의 특정 섹션을 메모리에 “등록(예약)”만 해두고, 실제 데이터 로드는 지연 처리
  • 흐름
    1. read_byteszero_bytes 계산
    2. 해당 페이지 범위별로 vm_alloc_page_with_initializer(VM_FILE, upage, writable, lazy_load_segment, aux) 호출
      • aux에는 (file, ofs, read_bytes) 정보를 담은 구조체 포인터 전달
    3. 반복문 돌며 upage += PGSIZE, read_bytes = max(read_bytes - PGSIZE, 0), ofs += PGSIZE

7.2 lazy_load_segment()

static bool lazy_load_segment(struct page *page, void *aux);
  • 호출 시점: 페이지 폴트 처리 중, 익명이나 파일 기반 페이지가 처음 로드될 때
  • aux 내용:
    struct load_info {
      struct file *file;    // 실행 파일 핸들
      off_t ofs;            // 파일 내 오프셋
      size_t read_bytes;    // 읽어야 할 바이트 수
      size_t zero_bytes;    // 0으로 채울 바이트 수
    };
  • 주요 작업
    1. auxstruct load_info *info = aux;로 캐스팅

    2. 파일에서 info->read_bytes만큼 kva로 읽어오기

      file_read_at(info->file, page->frame->kva, info->read_bytes, info->ofs);
    3. 남은 zero_bytes = PGSIZE - info->read_bytes만큼 memset(page->frame->kva + info->read_bytes, 0, zero_bytes)

    4. PTE는 이미 install_page()가 호출된 상태이므로 단순 데이터 로드만 수행


8. 스택 설정 수정 (setup_stack)

  • 기존 방법: 스택 페이지도 지연 로딩으로 처리
  • 변경 후 방법: 최초 스택 페이지는 즉시 할당 및 초기화
    1. setup_stack 함수 내에서 vm_alloc_page_with_initializer 대신 직접 vm_get_frame() 호출
    2. install_page(esp_base, frame->kva, true)로 PTE 매핑
    3. 커맨드 라인 인자 복사 등 스택 초기화 작업 수행
  • 추가 스택 페이지: 스택이 확장될 때마다 페이지 폴트가 발생 → lazy loading으로 처리

9. vm_try_handle_fault() 수정

  • 기존 흐름: 페이지 폴트 시 단순 SPT 조회 → 프레임 확보 → 데이터 로드 → PTE 설치
  • 수정 사항
    1. 페이지 폴트 발생 주소 fault_addr에서 페이지 경계 정렬된 va = pg_round_down(fault_addr) 계산
    2. page = spt_find_page(&current->spt, va) 호출 → 해당 struct page 가져옴
      • 없으면, 또는 권한 위반 시 exit(-1)
    3. page->operations->swap_in(page, frame_kva) 호출
      • 익명 페이지 → anon_swap_in
      • 파일 기반 페이지 → file_backed_swap_in
      • swap_in 내부에서 lazy_load_segment 또는 기존 데이터 로드 수행
    4. install_page(page->va, frame_kva, page->writable) 호출
    5. 정상 복귀 시 사용자 명령 재개

10. Supplemental Page Table 재방문

10.1 SPT 복사 (supplemental_page_table_copy)

bool supplemental_page_table_copy(
    struct supplemental_page_table *dst,
    struct supplemental_page_table *src);
  • 역할: fork() 시 부모 프로세스의 SPT를 자식에게 복사
  • 흐름
    1. src의 모든 struct page 순회
    2. 각각 페이지 타입(page->operations->type)에 따라 새 struct page *new_page 생성
      • vm_alloc_page_with_initializerVM_UNINIT 페이지 생성
      • vm_do_claim_page(new_page) 호출해 즉시 실제 페이지 할당 및 데이터 로드
      • 익명 페이지 → 스왑 슬롯에서 읽어오기
      • 파일 기반 페이지 → 파일에서 읽어오기
    3. spt_insert_page(&dst->spt, new_page) 호출
  • 반환: 복사 성공 시 true, 실패 시 false

10.2 SPT 해제 (supplemental_page_table_kill)

void supplemental_page_table_kill(struct supplemental_page_table *spt);
  • 역할: 프로세스 종료 시 SPT 자원 해제
  • 흐름
    1. SPT 내 모든 struct page 순회
    2. destroy(page) 호출 → 각 페이지 내부 리소스 해제
      • uninit_destroy (초기화되지 않은 페이지)
      • anon_destroy (익명 페이지)
      • file_backed_destroy (파일 페이지)
    3. struct page 자체는 호출자가 free

11. 페이지 정리 함수(Page Cleanup)

11.1 uninit_destroy() (vm/uninit.c)

static void uninit_destroy(struct page *page);
  • 호출 시점: 초기화되지 않은 상태로 남은 페이지 삭제 시
  • 주요 작업
    • page->uninit.initializerpage->uninit.aux 메모리 해제
    • 기타 초기화되지 않은 페이지 전용 자원 정리

11.2 anon_destroy() (vm/anon.c)

static void anon_destroy(struct page *page);
  • 호출 시점: 익명 페이지가 테이블에서 제거될 때
  • 주요 작업
    1. 해당 페이지가 스왑 슬롯에 있을 경우, 스왑 슬롯 해제(bitmap 업데이트)
    2. struct anon_page 내부 추가 메모리 해제(필요 시)
    3. struct page 자체는 호출자가 해제

최종 점검: 모든 요구 사항을 구현하면 프로젝트 2의 테스트(특히 fork 제외)가 통과되어야 한다.


Stack Growth

1. 개요

  • 프로젝트 2에서는 스택이 USER_STACK 주소 하나의 페이지(4KB)로 고정되어 있었다.
  • 이제 스택이 해당 영역을 넘어설 경우, 필요할 때마다 추가 페이지를 동적으로 할당하여 스택을 확장해야 한다.
  • 단, 모든 페이지 폴트가 스택 확장을 의미하는 것은 아니므로, “스택 접근”으로 보이는 경우에만 추가 페이지를 할당하는 휴리스틱을 구현해야 한다.

2. 스택 확장 판단 휴리스틱

  1. 스택 접근 범위 판단
    • 사용자 프로그램의 rsp(스택 포인터)보다 높은(주소 상으로는 낮은) 위치에 접근할 때를 스택 확장 후보로 간주한다.
    • 이를 통해 “단순 잘못된 메모리 접근”과 “스택을 늘려야 하는 경우”를 구분한다.
    • 주의: x86-64 PUSH 명령어는 실제로 스택 포인터를 조정하기 전에 메모리 권한을 검사하므로, rsp - 8 주소에서 페이지 폴트가 날 수 있다. 이 경우에도 스택 확장으로 처리해야 한다.
  2. 스택 크기 제한
    • 실제 운영체제들은 보통 8MB 정도로 스택 절대 크기를 제한한다.
    • 이번 과제에서는 스택 최대 크기를 1MB로 제한한다.
    • 스택 확장할 때마다 현재 스택 영역이 1MB를 넘어섰는지 확인하고, 이를 초과하면 확장 불가로 처리해야 한다.

3. 사용자 스택 포인터(rsp) 확보

  • 페이지 폴트 처리기에서 사용자 스택 포인터를 알아야 스택 확장 여부를 판단할 수 있다.
  • 문제점: 페이지 폴트가 커널 모드에서 발생하면 struct intr_frame *ff->rsp는 정의되지 않은 값일 수 있다.
  • 해결책:
    1. 초기 예외 진입 지점에서 사용자 모드→커널 모드 전환 시점에 rspstruct thread에 저장하도록 수정
      • 예시: userprog/exception.S 또는 인터럽트 엔트리 코드에서 f->rspthread_current()->rsp_saved 등에 복사
    2. 이후 페이지 폴트 처리(vm_try_handle_fault) 시 thread_current()->rsp_saved 값을 사용

4. vm_try_handle_fault() 수정

  1. 페이지 폴트 발생 시 호출 시그니처

    bool vm_try_handle_fault(struct intr_frame *f, void *addr, bool user, bool write, bool not_present);
  2. 기존 흐름

    • 보조 페이지 테이블(SPT) 조회 → 엔트리 없으면 종료
    • 프레임 확보 → 페이지 내용 로드 → PTE 설치
  3. 추가할 작업

    1. 스택 확장 후보 판정
      • addr가 페이지 테이블에 없어서 not_present == true로 들어온 경우,
      • addr이 사용자 스택 영역 범위에 해당하는지 확인
        1. addr(rsp_saved – STACK_GROWTH_THRESHOLD) 이상인지 검사
          • 예: threshold를 32바이트 정도로 잡아, rsp_saved - 32 <= addr < USER_STACK 이면 스택 확장 후보로 간주
        2. 현재 스택 최상위 주소(USER_STACK)에서 addr까지 크기를 계산하여 1MB 한도 내에 있는지 검사
      • 두 조건을 만족하면 스택 확장으로 처리하되, 그렇지 않으면 정상적인 페이지 폴트 절차 수행(또는 종료)
    2. 스택 확장 호출
      • 위 판정이 true일 때 vm_stack_growth(addr) 호출
      • vm_stack_growth(addr)가 성공하면 vm_try_handle_faulttrue 반환하여 사용자 명령을 재개
      • 스택 확장에서 실패(1MB 초과 등)하면 false 혹은 exit(-1) 처리

5. vm_stack_growth() 구현

void vm_stack_growth(void *addr);
  1. 입력
    • addr: 페이지 폴트를 일으킨 사용자 주소(가상 주소)
  2. 절차
    1. 페이지 경계 정렬

      void *page_va = pg_round_down(addr);
    2. 스택 최상위, 스택 바닥 주소 계산

      • 최상위(stack top): USER_STACK
      • 현재 스택 하한(stack bottom): STACK_BOTTOM = pg_round_down(f->rsp_saved) - 현재 할당된 페이지 수 * PGSIZE
        • SP를 저장해둔 값과 이미 할당한 페이지 수(또는 SPT 내 스택용 페이지 개수)로 구한다.
    3. 스택 크기 확인

      • 새로 할당될 주소(page_va)가 USER_STACK - 1MB <= page_va < USER_STACK 범위여야 함
      • 즉, (USER_STACK - page_va) <= 1MB 확인
    4. 페이지 할당

      1. vm_alloc_page_with_initializer(VM_ANON, page_va, true, anon_initializer, NULL) 호출하여 SPT에 익명 페이지 예약
      2. vm_do_claim_page() 호출하여 실제 프레임 할당 및 PTE 설치
      3. 할당에 실패하면 exit(-1)
    5. 반환

      • 성공 시 바로 vm_try_handle_fault로 돌아가 사용자 명령 재개
  3. 주의사항
    • 스택 확장 시 프레임과 SPT가 일관되게 반영되어야 한다.
    • 스택 확장용 페이지는 항상 익명 페이지(VM_ANON)이다.

6. 한도 관리

  • 스택 최대 크기 1MB 확보
    • USER_STACK 주소에서 1MB를 뺀 값이 스택 확장의 최저 한계 주소가 된다.
      void *stack_limit = USER_STACK - (1 << 20); // 1 << 20 = 1MB
    • addrpg_round_down(addr) 했을 때 page_va < stack_limit 이면 확장 불가
  • 스택 초기 페이지
    • USER_STACK - PGSIZE 하나는 setup_stack() 단계에서 미리 할당하여 초기 스택으로 사용
    • 이후 page_vaUSER_STACK - PGSIZE 이하로 내려갈 때마다 vm_stack_growth() 호출

7. 예시 흐름

  1. 사용자 코드가 push [rsp - 8] 수행
  2. CPU가 PTE 확인 → present = 0 → 페이지 폴트 발생 → vm_try_handle_fault 호출
  3. vm_try_handle_fault 내부:
    1. addr = rsp_saved - 8이 스택 영역 범위(USER_STACK - threshold <= addr)인지 판단
    2. 스택 크기가 1MB 미만인지 확인
    3. 만족하면 vm_stack_growth(addr) 호출 →
      • page_va = pg_round_down(addr)
      • SPT에 익명 페이지 예약 → 실제 프레임 할당 → PTE 설치
    4. 정상 처리되면 true 반환 → 사용자 명령 재개
  4. 사용자 스택이 1MB 한계에 도달하면 추가 확장 불가, exit(-1) 처리

위 절차대로 구현하면 “스택 접근 시 동적 페이지 할당 → 최대 1MB 제한” 테스트가 모두 통과


0개의 댓글