2025.06.01

TIL(TODAY I LEARN)


  • 오늘한 내용 : PintOS - Project3: Virtual Memory - gitbook 정리 중! - Memory Mapped Files, Swap In/Out, Copy-on-Write(Extra), FAQ

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


Memory Mapped Files 정리

1. 개요

메모리 매핑 파일(Memory Mapped Files)은 익명 페이지와 달리 파일을 그대로 백업 저장소(backing store)로 사용하는 방식이다.

  • 프로그램의 가상 주소 영역에 파일 내용을 그대로 매핑해 두고, 페이지 폴트가 발생하면 파일로부터 데이터를 읽어와 물리 페이지에 올린다.
  • 변경이 일어나면 스왑이 아닌 원본 파일에 곧바로 반영(혹은 쓰기 직전 반영)한다.

2. mmap/munmap 시스템콜 구현

2.1 mmap()

void *mmap(void *addr, size_t length, int writable, int fd, off_t offset);
  • 역할:
    • fd로 열린 파일의 offset 바이트부터 length 바이트를 가상 주소 addr부터 연속 페이지로 매핑
    • 파일 크기가 length보다 작으면 마지막 페이지에서 파일 끝 이후 바이트는 0으로 채움
    • 매핑 성공 시 매핑된 가상 주소 반환, 실패 시 NULL 반환
  • 실패 조건:
    1. addr가 페이지 정렬(4KB)되지 않음 → 실패
    2. length == 0 → 실패
    3. 파일 크기가 0바이트 → 실패
    4. fd가 콘솔 입출력(stdin/stdout) → 실패
    5. 이미 매핑된 영역, 스택, 및 실행 파일 로드 시 사용된 영역과 겹침 → 실패
    6. addr == NULL → 실패(간단화를 위해 자동 주소 배정 미지원)
  • 구현 방법:
    1. do_mmap() (vm/file.c) 호출
      • 별도 파일 핸들(file_reopen(fd)) 복제 후 사용
      • length 바이트를 페이지 단위로 나누어 각 페이지마다 vm_alloc_page_with_initializer(VM_FILE, va, writable, file_backed_initializer, aux) 호출
      • 이때 auxfile, offset, read_bytes, zero_bytes 정보 포함
    2. do_mmap()이 정상 완료되면 addr 반환, 아니면 NULL 반환

2.2 munmap()

void munmap(void *addr);
  • 역할:
    • 이전 mmap으로 매핑된 가상 주소 addr 영역을 해제(unmap)
    • 해제 대상 페이지 중 변경된 내용을 파일에 써야 하는 경우 즉시(또는 즉시에 준하는 시점에) 파일에 반영
    • 페이지를 프로세스의 보조 페이지 테이블(SPT)에서 제거하고, 물리 프레임·PTE도 정리
  • 호출 조건:
    1. addr는 이전 mmap()이 반환한 정확한 가상 주소여야 함
    2. 이미 언맵된 주소이거나 잘못된 주소면 무시하거나 오류 처리
  • 구현 방법:
    1. do_munmap() (vm/file.c) 호출
      • 매핑된 페이지 목록을 순회하며
        • page->operations->swap_out(page) 또는 file_backed_destroy(page)로 파일에 쓰기 수행
        • PTE에서 페이지 제거, 물리 프레임 해제, SPT에서 페이지 엔트리 삭제
      • 매핑과 연관된 파일 핸들 닫기

3. Lazy Loading

  • 익명 페이지와 동일하게, 메모리 매핑 페이지도 처음엔 물리 프레임 할당 없이 SPT에만 struct page 객체를 생성한다.
  • 페이지 폴트 발생 시점에 file_backed_initializer가 호출되어 파일로부터 페이지 내용을 읽어 물리 프레임에 올린다.
  • 핵심 원리:
    1. vm_alloc_page_with_initializer(VM_FILE, va, writable, file_backed_initializer, aux)struct page만 생성
    2. 페이지 폴트 → swap_in 실행 → lazy_load_segment와 유사하게 파일에서 read_bytes만큼 읽어오고, 나머지 zero_bytes 부분은 0으로 채움
    3. PTE에 매핑된 상태에서 데이터 로드만 처리
    4. 이후 메모리 변경 시 dirty 비트를 확인하여 swap_out 또는 file_backed_destroy에서 파일에 덮어씀

4. 필수 함수 및 구조체

4.1 vm_file_init() (vm/vm.c)

void vm_file_init(void);
  • 역할: 파일 기반 페이지 서브시스템 초기화
    • 전역 자료구조(예: 매핑 테이블) 초기화
    • 파일 매핑 관리용 락(lock) 초기화

4.2 file_backed_initializer() (vm/vm.c)

bool file_backed_initializer(struct page *page, enum vm_type type, void *kva);
  • 호출 시점: mmap 이후 첫 페이지 폴트 또는 SPT 복사·fork 시
  • 주요 작업:
    1. page->operations에 swap_in, swap_out, destroy 함수 포인터 등록

      page->operations->swap_in  = file_backed_swap_in;
      page->operations->swap_out = file_backed_swap_out;
      page->operations->destroy  = file_backed_destroy;
      page->operations->type     = VM_FILE;
    2. struct file_page 멤버에 파일 핸들, 읽기 오프셋, 읽어야 할 바이트 수, 쓰기 권한 등 저장

    3. 물리 프레임은 아직 할당하지 않음 (lazy loading)

4.3 file_backed_destroy() (vm/vm.c)

static void file_backed_destroy(struct page *page);
  • 호출 시점:
    • munmap() 수행 중
    • 프로세스 종료 시 SPT 정리 단계
  • 주요 작업:
    1. 페이지가 dirty 상태이면, 파일 핸들로 덮어쓰기 (file_write_at)
    2. 열린 파일 핸들 닫기
    3. 물리 프레임 해제, PTE 제거
    4. SPT에서 페이지 엔트리 삭제

5. 구현 흐름 요약

  1. vm_file_init() 호출하여 파일 기반 VM 서브시스템 초기화
  2. mmap() 시스템콜
    • 사용자 호출 → do_mmap() 실행
    • 파일 복제(file_reopen(fd)) → 페이지 영역 수 계산
    • vm_alloc_page_with_initializer(VM_FILE, va, writable, file_backed_initializer, aux) 반복 호출
    • 매핑 성공 시 addr 반환, 에러 시 NULL 반환
  3. 페이지 폴트 발생 시
    • vm_try_handle_fault() 내에서 spt_find_page()page->operations->swap_in(page, page->frame->kva)
    • file_backed_swap_in() 호출 → 파일에서 read_bytes 읽어와 frame->kva 채움, 남은 부분 0으로 초기화
    • PTE는 이미 install_page()로 매핑 완료
  4. 메모리 변경 발생 시
    • 페이지가 dirty 비트 세팅 → file_backed_swap_out() 또는 file_backed_destroy()에서 파일에 반영
  5. munmap() 시스템콜
    • do_munmap() 실행
    • 매핑된 페이지별 file_backed_destroy(page) 호출 및 SPT/PTE/프레임 정리
    • 매핑된 파일 핸들 모두 닫기

위 단계를 구현하면 메모리 매핑 파일 관련 기능이 정상 동작하며, 페이지 폴트마다 필요한 데이터가 파일에서 지연 로딩 되어 메모리에 올라오고, unmap 시에는 변경 내용이 파일에 반영


Swap In/Out 정리

1. 개요

프로그래밍 실행 중 물리 메모리가 부족하면, 현재 사용되지 않는 페이지를 디스크로 내보내(swap out) 메모리를 확보할 수 있다. 이후 해당 페이지에 접근 요청이 들어오면 디스크에서 다시 읽어 들여(swap in) 원래 상태로 복원한다. 이 과정을 통해 물리 메모리를 최대한 활용할 수 있다.


2. struct page_operations와 함수 포인터

  • 각 페이지는 struct page_operations 안에 swap_in, swap_out, destroy 함수 포인터를 갖고 있다.
  • 스왑 동작은 직접 호출하는 것이 아니라, page->operations->swap_in(page, kva)처럼 함수 포인터를 통해 실행된다.
  • 익명 페이지와 파일 기반 페이지 각각에 맞는 swap_in/swap_out 함수를 구현해야 한다.

3. 익명 페이지(Anonymous Page) 스왑 처리

3.1 vm_anon_init()

void vm_anon_init(void);
  • 역할: 익명 페이지 스왑용 디스크(스왑 영역) 초기화
  • 구현할 일
    1. 스왑 디스크 장치 등록
    2. 스왑 슬롯 관리용 자료구조(예: 비트맵) 초기화
      • 스왑 슬롯은 페이지 크기(4KB) 단위로 관리
      • 비트맵에서 0은 “빈 슬롯”, 1은 “사용 중” 표시

3.2 anon_initializer()

bool anon_initializer(struct page *page, enum vm_type type, void *kva);**역할**: 익명 페이지를 처음 생성할 때 호출
  • 구현할 일
    1. 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. struct anon_page 안에 스왑 슬롯 인덱스를 저장할 수 있도록 멤버 추가

      • 예: size_t swap_slot;
      • 아직 스왑되지 않은 초기 상태에 대해서는 특별 값(e.g. SWAP_NOTHING)으로 표시
    3. 물리 프레임은 할당하지 않으므로 page->frame = NULL 상태 유지

3.3 anon_swap_out()

static bool anon_swap_out(struct page *page);
  • 역할: 익명 페이지를 스왑 디스크로 내보내기
  • 절차
    1. 빈 스왑 슬롯 찾기
      • 스왑 비트맵에서 첫 번째 0(bit)을 찾아 해당 슬롯 번호 얻기
      • 없으면 커널 패닉(panic)
    2. 물리 메모리에 올라와 있는 페이지 내용(page->frame->kva)을 스왑 디스크의 해당 슬롯에 복사
      • swap_disk_write(slot, page->frame->kva)와 유사한 함수 사용
    3. struct anon_pageswap_slot = slot 저장
    4. PTE(page table entry)에서 해당 페이지 “비유효(not present)”로 표시
      • pagedir_clear_page(thread->pagedir, page->va)나 유사 함수 호출
    5. 물리 프레임 해제
      • palloc_free_page(page->frame->kva)
      • page->frame = NULL
    6. 스왑 비트맵에서 해당 슬롯을 1로 표시(사용 중)
  • 반환값
    • 스왑 성공 시 true, 오류(panic 제외) 시 false

3.4 anon_swap_in()

static bool anon_swap_in(struct page *page, void *kva);
  • 역할: 스왑 디스크에서 익명 페이지를 메모리로 복원
  • 절차
    1. page->anon.swap_slot 값 읽기
      • 값이 SWAP_NOTHING이면 스왑된 적 없는 새 페이지이므로 제로 초기화
    2. 빈 프레임 확보
      • kva 파라미터는 이미 할당된 물리 프레임의 커널 가상 주소
      • 만약 kva == NULL이라면 vm_get_frame()으로 새로운 프레임 확보
    3. 스왑 디스크의 swap_slot 위치에서 메모리로 읽기
      • swap_disk_read(slot, kva)와 유사한 함수 사용
    4. PTE에 다시 매핑
      • install_page(page->va, kva, true)
    5. 스왑 비트맵에서 해당 슬롯 0으로 해제(빈 슬롯)
    6. page->anon.swap_slot = SWAP_NOTHING로 초기화
  • 반환값
    • 스왑인 성공 시 true, 아니면 false

4. 파일 매핑 페이지(File-Mapped Page) 스왑 처리

4.1 개념

  • 파일 기반 페이지는 원본 파일 자체를 백업 저장소로 사용
    • 페이지가 처음 매핑되면, 물리 프레임에 아직 읽어오지 않은 상태라 “lazy loading” 시점에 파일 내용 그대로 메모리로 읽어온다.
    • 메모리상에서 해당 페이지를 수정(쓰기)하기 전까지는 실제 파일에 아무 변화가 없다. 이때는 파일이 원본 데이터를 보관하는 백업 저장소가 된다.
    • 페이지가 스왑 아웃되거나 munmap() 호출 시점에, 메모리에서 수정된 내용이 있으면 실제 파일에 덮어쓰기(write-back)하여 파일을 최신 상태로 유지한다.
    • 따라서 쓰기 전에는 파일 내용이 변경되지 않기 때문에, 파일 자체를 안전한 백업 저장소로 사용할 수 있다.
  • 스왑 아웃 시: 변경된 내용(dirty)만 파일에 덮어쓰고, 메모리에서 페이지 제거
  • 스왑 인 시: 파일에서 지정된 오프셋 부분만 읽어 메모리에 올림

4.2 file_backed_swap_out()

static bool file_backed_swap_out(struct page *page);
  • 역할: 파일 매핑 페이지를 파일에 다시 쓰고 메모리에서 해제
  • 절차
    1. Dirty 비트 검사
      • PTE의 dirty 비트가 꺼져 있으면(내용 변경 없음) → 파일에 쓰지 않고 넘어감
    2. 변경된 경우 file_write_at으로 파일의 지정 오프셋에 페이지 내용을 덮어쓰기
      • file_write_at(page->file.file, page->frame->kva, page->file.read_bytes, page->file.offset)
    3. PTE에서 페이지 “비유효(not present)” 표시
      • pagedir_clear_page(thread->pagedir, page->va)
    4. 물리 프레임 해제
      • palloc_free_page(page->frame->kva)
      • page->frame = NULL
    5. PTE dirty 비트 초기화
      • pagedir_set_dirty(thread->pagedir, page->va, false)
  • 반환값
    • 성공 시 true, 실패 시 false

4.3 file_backed_swap_in()

static bool file_backed_swap_in(struct page *page, void *kva);
  • 역할: 파일에서 페이지 데이터를 읽어 메모리에 올리기
  • 절차
    1. 빈 프레임 확보
      • kva 파라미터로 전달된 커널 가상 주소 또는 vm_get_frame() 호출
    2. 파일에서 page->file.read_bytes만큼 kva로 읽어오기
      • file_read_at(page->file.file, kva, page->file.read_bytes, page->file.offset)
    3. 남은 zero_bytes = PGSIZE - page->file.read_bytes 만큼 제로 초기화
      • memset(kva + page->file.read_bytes, 0, zero_bytes)
    4. PTE에 다시 매핑
      • install_page(page->va, kva, page->file.writable)
    5. PTE dirty 비트 초기화
      • pagedir_set_dirty(thread->pagedir, page->va, false)
  • 반환값
    • 성공 시 true, 실패 시 false

5. 구현 흐름 요약

  1. 시스템 초기화
    • vm_anon_init() 호출 → 스왑 슬롯 관리 비트맵 생성
    • vm_file_init() 호출 → 파일 매핑 페이지 관련 자료구조 초기화
  2. 페이지 초기화 단계
    • 익명 페이지: anon_initializer()swap_in/out/destroy 함수 포인터 설정, swap_slot 초기화
    • 파일 페이지: file_backed_initializer()로 함수 포인터 설정, 파일 핸들·오프셋 저장
  3. 페이지 폴트 시
    • vm_try_handle_fault() 호출 → page->operations->swap_in(page, kva) 실행 → 익명/파일 페이지에 맞는 swap_in 수행
  4. 스왑 아웃 조건
    • 물리 프레임이 부족해 vm_get_frame()에서 실패할 때
    • 이미 할당된 프레임 중 희생자(victim)로 선택된 페이지에 대해 page->operations->swap_out(page) 호출
  5. 프로세스 종료 혹은 언맵 시
    • file_backed_destroy() 혹은 anon_destroy() 호출
    • 남아 있는 페이지가 스왑 또는 파일 기반이면, 필요 시 최종 덮어쓰기 후 메모리 정리

위 과정을 구현하면, 익명 페이지와 파일 매핑 페이지 모두 필요할 때 디스크로 내보내고, 다시 접근 시 복원하는 스왑 메커니즘이 완성


Copy-on-Write 구현 정리(Extra)

1. 개념

  • Copy-on-Write(COW)는 포크(fork) 시 부모와 자식이 같은 물리 페이지를 공유하고, 실제로 수정이 일어날 때에만 페이지를 복사하는 기법이다.
  • 이를 통해 포크 시점에는 실제 복사 작업을 지연(Deferred Copy)하여 속도를 최적화할 수 있다.
  • 부모와 자식 프로세스 간에 읽기 작업만 일어나는 동안 동일한 물리 페이지를 그대로 사용하다가, 어느 한쪽에서 쓰기 시도가 감지되면 그 시점에 물리 페이지를 복제하여 독립된 사본을 만든다.

2. COW 적용 대상 페이지

  1. 익명 페이지(Anonymous Page)
    • 주로 스택(heap) 등의 동적 메모리 영역
    • 복사 시 물리 페이지를 직접 복제해야 함
  2. 파일 기반 페이지(File-Backed Page)
    • mmap으로 매핑된 파일 영역
    • 읽기 전용으로 공유 가능하며, 쓰기 시점에 COW 동작 또는 파일 백업 적용
    • 여기서는 파일 기반 페이지도 공유 대상으로 처리해야 함

3. 필요한 자료구조 및 수정 사항

  1. 프레임(Frame) 참조 카운트 추가
    • 현재 물리 페이지가 몇 번 참조되고 있는지를 세는 ref_count 필드를 프레임 구조체에 추가한다.
    • 구조 예시 (include/vm/vm.h):
      struct frame {
        void *kva;                /* 커널 가상 주소(실제 물리 페이지) */
        struct page *page;        /* 대표 페이지(struct page)를 가리킴 */
        int ref_count;            /* 현재 이 프레임을 참조하는 페이지 수 */
        /* 필요 시 lock 추가 가능 */
      };
    • 새 프레임 할당 시 ref_count = 1로 초기화
    • 부모·자식이 동일한 프레임을 가리키면 ref_count++
  2. 페이지 초기화(initializer) 변경
    • 익명/파일 기반 페이지를 생성할 때, 쓰기 보호(Write-Protect) 상태로 설정
    • install_page(va, frame_kva, writable=false)로 PTE에 매핑하여 쓰기 시도를 페이지 폴트로 유도
    • 기존 익명·파일 초기화 함수(anon_initializer, file_backed_initializer)에서 writable 인자를 강제로 false로 변경하거나, COW 전용 별도 초기화 함수를 사용할 수 있다.
  3. 페이지 폴트 핸들러 수정
    • 페이지 폴트 시 vm_try_handle_fault()가 호출되면,
      1. SPT에서 page 찾기 → struct page *p

      2. user 영역·not_present 여부 검사 후, 쓰기 폴트(write fault)인지 확인

        bool is_write_fault = (write && !pagedir_is_writable(cur->pagedir, va));
      3. 쓰기 폴트(write fault)이고, COW 대상 페이지라면

        • frame = p->frame;
        • frame->ref_count > 1인지 확인
          • 1인 경우(공유 중) → COW 수행
            1. 새로운 물리 프레임 new_frame = vm_get_frame();
            2. memcpy(new_frame->kva, frame->kva, PGSIZE);
            3. frame->ref_count--, new_frame->ref_count = 1
            4. p->frame = new_frame
            5. install_page(p->va, new_frame->kva, true); // 쓰기 허용
          • ==1인 경우(혼자 단독 사용 중) → 기존 swap_in/install_page로 변경만 하면 됨
      4. 읽기 폴트(read fault)이거나 쓰기 폴트가 아니면서 not_present면 기존 로직—스왑인(swap_in) 또는 lazy-load—수행

  4. fork 시 SPT 복사 변경
    • 기존 프로젝트 3 supplemental_page_table_copy()는 새 페이지마다 vm_do_claim_page()로 실제 페이지를 복제해 왔다.
    • COW를 적용하기 위해, 부모 페이지를 그대로 참조만 추가하고 물리 페이지를 복제하지 않는다. 즉:
      bool supplemental_page_table_copy(struct supplemental_page_table *dst,
                                        struct supplemental_page_table *src) {
        // src SPT의 모든 struct page 순회
        for each struct page *p_src in src->spt {
          // 1) 새 struct page *p_dst 할당
          struct page *p_dst = malloc(sizeof *p_dst);
          // 2) p_dst->va, p_dst->operations, p_dst->aux = p_src->aux 등 복제
          *p_dst = *p_src; // 얕은 복사 후 필요한 필드 수정
          // 3) p_dst->frame = p_src->frame;
          // 4) frame->ref_count++;
          p_src->frame->ref_count++;
          // 5) PTE 복제: 부모 페이지 테이블에서 va에 매핑된 프레임 번호 읽어 와
          //    install_page_in_child(child_pagedir, p_dst->va, p_dst->frame->kva, false);
          //    (쓰기 금지 상태로 복제)
          install_page(dst->pagedir, p_dst->va, p_dst->frame->kva, false);
          // 6) dst SPT에 p_dst 삽입
          spt_insert_page(dst, p_dst);
        }
        return true;
      }
    • 이렇게 하면 부모·자식 모두 쓰기 금지된(toggled writable=false) 상태로 동일 물리 페이지를 가리킨다.

4. 상세 동작 흐름

4.1 부모-자식 fork 시점

  1. 부모 프로세스
    • SPT에 있는 모든 페이지가 쓰기 보호 상태인지 확인.
    • (기존 익명/파일 페이지는 이미 lazy loading 시점에 쓰기 보호로 등록해 두었음)
  2. 자식 프로세스
    • 부모의 각 페이지마다 새 struct page 복사본 생성
    • frame = 부모가 가리키는 frame 연결
    • frame->ref_count++
    • 자식 페이지 테이블에 install_page(child_pagedir, va, frame_kva, false) 호출 → 읽기만 허용, 쓰기 금지
    • SPT에는 새로 만든 struct page 삽입

요약: 부모와 자식은 같은 물리 페이지를 읽기 전용으로 공유한다.


4.2 쓰기 시도 시 페이지 폴트

  1. 사용자 쓰기 명령
    • 예: mov [rsp], eax 등으로 쓰기 시도 → MMU가 PTE에서 writable = false 감지 → 페이지 폴트 발생
  2. vm_try_handle_fault() 진입
    1. vapg_round_down(fault_addr)로 페이지 단위 정렬

    2. p = spt_find_page(&cur->spt, va)struct page 조회

    3. 쓰기 폴트 감지:

      if (write && !pagedir_is_writable(cur->pagedir, va)) {
          // COW candidate
      }
    4. COW 수행 조건 검사

      • p->operations->type == VM_ANON 또는 VM_FILE (공유 가능한 페이지)
      • frame = p->frame; frame->ref_count > 1 (현재 물리 페이지를 2개 이상이 공유 중)
    5. COW 실행

      1. 새 프레임 new_frame = vm_get_frame();
      2. memcpy(new_frame->kva, frame->kva, PGSIZE);
      3. frame->ref_count--; new_frame->ref_count = 1;
      4. p->frame = new_frame;
      5. install_page(cur->pagedir, p->va, new_frame->kva, true);
        • 쓰기 권한을 다시 부여하여 이후 쓰기 폴트 방지
      6. 페이지 폴트 처리 완료 → 사용자 명령 재개
    6. COW 불필요 조건

      • frame->ref_count == 1인 경우: 현재 페이지를 오직 이 프로세스만 사용 중이므로 그냥 install_page(..., true)로 쓰기 권한만 회복
      • 또는 읽기 폴트이거나 단순한 not_present(swap-in, lazy-load) 케이스: 기존 swap_in, lazy_load_segment 로직 실행

5. 구현 단계 요약

  1. 프레임 구조 수정
    • struct frameint ref_count 추가
    • vm_get_frame()에서 새 프레임 생성 시 ref_count = 1 초기화
  2. 페이지 초기화 시 쓰기 보호
    • anon_initializer(), file_backed_initializer() 호출 시,
      install_page(cur->pagedir, va, frame_kva, false);
      와 같이 쓰기 금지로 PTE를 생성
    • 또는 나중에 fork 복사 단계에서 자식 쪽 PTE writable=false
  3. fork 시 SPT 복사 로직 변경
    • 기존 supplemental_page_table_copy()에서 물리 복사(vm_do_claim_page) 대신,
      • p_dst->frame = p_src->frame; p_src->frame->ref_count++;
      • install_page(child_pagedir, va, frame_kva, false)
    • 두 프로세스 모두 쓰기 금지 상태로 만든다
  4. 페이지 폴트 핸들러 수정 (vm_try_handle_fault)
    1. 쓰기 폴트(write fault)인지 먼저 검사
    2. 쓰기 폴트 & COW 대상(p->frame->ref_count > 1)이면 COW 분기 수행
      • 프레임 복제, ref_count 조정, PTE writable=true 복구
    3. COW 불필요하거나 읽기 폴트라면 기존 swap-in / lazy-load 흐름 계속 실행
  5. 프로세스 종료 및 SPT 해제
    • supplemental_page_table_kill() 실행 시,
      • 각 페이지 destroy(page) 호출 → 익명 또는 파일 기반 소멸자에서 frame->ref_count--
      • 만약 ref_count == 0이 되면 물리 페이지 해제(palloc_free_page)

6. 예외 및 주의사항

  1. 파일 기반 페이지 공유
    • mmap된 파일 영역도 COW 대상이 될 수 있다.
    • 두 프로세스가 같은 파일 페이지를 공유 중 쓰기 시도하면 위와 동일한 COW 로직을 수행한다.
    • 단순한 “읽기 전용” 공유가 아니라 “쓰기 시 COW” 형태여야 함.
  2. Dirty 비트 관리
    • PTE가 “쓰기 금지”인 상태에서 쓰기 시도 → 페이지 폴트 → COW → 쓰기 권한 복구
    • 이후 페이지에서 write가 일어나면 PTE dirty 비트도 세팅되어야 하며, 파일 기반 페이지라면 ungback write-back 시 이 비트를 확인해야 한다.
  3. 스왑 아웃 시 COW
    • COW 페이지도 스왑 대상이 될 수 있다.
    • 스왑 아웃 직후 물리 프레임을 곧바로 해제할 수는 없으므로,
      • ref_count == 1인 경우 프레임을 스왑해야 하며,
      • ref_count > 1인 경우 COW 단계에서 이미 공유 중인 페이지가 있으므로,
        • 프레임 복제 없이 “공유 중이던 한쪽이 스왑” 방식으로 처리하도록 고민할 수 있다.
    • 단순 구현을 위해, 스왑 아웃할 때 ref_count > 1이면 즉시 COW 수행 후 스왑하도록 설계해도 무방하다.
  4. 페이지 교체 알고리즘과 COW 상호작용
    • 스왑 아웃 후보(eviction target)를 선택할 때,
      • COW 공유 중인 페이지(ref_count > 1)는 순수한 스왑 아웃 대상이 아니다.
      • 오히려 공유 중 페이지는 “읽기 전용”(writable=false) 상태로 남겨 두고, 필요 시 COW가 먼저 이뤄지도록 설계해야 한다.

7. 마무리

  • COW 도입 결과
    • 포크 시 부모 자식 모두 같은 페이지를 읽기 전용으로 즉시 공유 → 포크 속도 대폭 향상
    • 쓰기 시점에만 실제 복사 발생 → 메모리 사용 최적화
  • 구현 점검
    1. fork 후 부모/자식 페이지가 동일 물리 페이지 공유, PTE 쓰기 금지 확인
    2. 쓰기 시도 → COW 로직 정상 동작하여 복제 후 쓰기 허용 확인
    3. COW 페이지 소멸 시 ref_count 감소 및 최종 해제 정상 동작
    4. 파일 기반 페이지 공유/쓰기 시 COW 동작 확인

위 과정을 모두 구현하면 Pintos에서 Copy-on-Write 기능이 정상 작동하며, 포크 성능이 크게 개선된다.


FAQ 정리

  1. 프로젝트 2가 제대로 동작해야 프로젝트 3을 구현할 수 있나?

    예. 프로젝트 2가 올바르게 통과되어야 프로젝트 3 작업을 시작할 수 있다.

  2. 페이지 폴트 처리를 완료한 뒤 어떻게 프로세스를 재개하나?

    page_fault() 함수에서 반환하면 현재 사용자 프로세스가 바로 재개된다. 예외 발생 지점의 명령어를 다시 시도한다.

  3. 스택 포인터 위쪽 주소에서 페이지 폴트가 발생하는 이유는 무엇인가?

    스택 확장 테스트에서, 유저 프로그램은 현재 스택 포인터보다 위쪽(주소 상으로 낮은)에 있는 주소를 액세스할 수 있다.

    • x86-64의 PUSH 명령어는 스택 포인터를 먼저 감소시키지 않고 메모리 권한을 검사한다.
    • 따라서 rsp - 8 위치가 아직 매핑되지 않았더라도, 쓰기 권한 확인을 먼저 하며 페이지 폴트가 날 수 있다.
  4. 가상 메모리 시스템에서 데이터 세그먼트(변수 영역) 확장 기능이 필요한가?

    아니다. 데이터 세그먼트 크기는 링커가 결정한다. 현재 Pintos에는 동적 할당 기능이 없으므로, 데이터 세그먼트 확장은 지원하지 않아도 된다. (메모리 맵 파일을 이용해 유저 레벨에서 흉내낼 수는 있다.)

  5. 왜 페이지 프레임을 할당할 때 PAL_USER를 사용해야 하나?

    • PAL_USER를 주면 palloc_get_page()가 사용자 풀(user pool)에서 메모리를 할당한다.
    • 사용자 풀 부족 시 페이지 폴트(스왑)를 수행하더라도 커널 풀은 건드리지 않으므로, 커널 기능이 계속 안정적으로 동작한다.
    • 반면, 커널 풀을 모두 소진하면 커널 함수들이 메모리 할당에 실패하며 여러 오류가 발생할 수 있다.
    • 필요하다면 palloc_get_page() 위에 별도의 할당자 레이어를 얹어도 되지만, 최종적으로는 PAL_USER를 사용해야 한다.
    • 또한 커널 실행 시 ul 옵션으로 사용자 풀 크기를 제한해, 다양한 사용자 메모리 크기로 VM 구현을 테스트할 수 있다.

0개의 댓글