
2025.05.31
오늘한 내용 : PintOS - Project3: Virtual Memory - gitbook 정리 중! - Memory Management, Anonymous Page, Stack Growth
WEEK 12 : 정글 끝까지(PintOS) - Virtual Memory
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; // 파일에서 읽어올 페이지
/* 기타 페이지 타입 추가 가능*/
};
};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)를 실행합니다.supplemental_page_table_init(spt) – 프로세스 시작·복제 시 SPT 초기화spt_find_page(spt, va) – 가상 주소 va에 대응하는 struct page 검색spt_insert_page(spt, page) – 새 페이지를 SPT에 등록struct frame은 물리 프레임의 커널 가상 주소(kva)와, 이를 사용하는 page를 연결.struct frame {
void *kva; // 커널 가상 주소(실제 물리 메모리)
struct page *page;// 매핑된 페이지
/* 필요 시 추가 필드 가능*/
};vm_get_frame()palloc_get_page(PAL_USER)로 빈 프레임 확보struct frame 구조체를 할당·초기화 후 반환vm_claim_page(page)vm_get_frame()로 프레임을 받은 뒤install_page(page->va, frame->kva, writable) 호출로 PTE에 매핑vm_do_claim_page(va)spt_find_page로 page 조회 후 → vm_do_claim_page(page) 실행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 필드에 필요한 데이터를 저장한다.struct page)만 미리 생성해 둔다.VM_UNINIT 타입으로 생성된다.struct uninit_page (include/vm/uninit.h)struct uninit_page {
/* 페이지 타입별 initializer 함수 포인터, aux 데이터 등 */
vm_initializer *initializer; // 초기화 시 호출할 함수
void *aux; // 초기화에 필요한 추가 정보
};struct page 내 uninit 멤버vm_alloc_page_with_initializer 호출 → VM_UNINIT 페이지 생성uninit_initialize 호출 → 설정해 둔 initializer(anon_initializer 또는 file_backed_initializer) 실행 → 실제 페이지 초기화swap-in, swap-out, destroy 등 상태 전환vm_alloc_page_with_initializer() 구현bool vm_alloc_page_with_initializer(
enum vm_type type,
void *va,
bool writable,
vm_initializer *init,
void *aux);
type에 맞는 initializer 함수를 가져와 uninit_new 호출struct page 객체를 VM_UNINIT 상태로 생성하고, page->operations에 initializer·aux 정보 저장page 삽입VM_TYPE(type) 매크로를 사용해 type별 분기 처리spt_insert_page 호출 전, 동일 va 존재 여부 확인uninit_initialize() 구현static bool uninit_initialize(struct page *page, void *kva);
page->operations->initializer가 호출되기 전page->operations 내부에 저장된 vm_initializer *와 aux 꺼내기initializer(page, type, kva) 호출VM_ANON) → anon_initializer(page, type, kva)VM_FILE) → file_backed_initializer(page, type, kva)anon_initializer() 구현vm_anon_init()void vm_anon_init(void);
anon_initializer()bool anon_initializer(
struct page *page,
enum vm_type type,
void *kva);
VM_UNINIT → VM_ANON 전환)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;
page->frame 설정: frame = vm_get_frame()로 새 프레임 할당 (페이지 폴트 처리 흐름에서 실제로 수행됨)
anon_page 멤버에 필요 정보(스왑 슬롯 번호 등) 초기화
페이지 내용을 제로 초기화 (익명 페이지는 실행 시 초기화 필요)
PTE에 install_page(page->va, kva, writable) 호출하여 매핑 완료
load_segment() 및 lazy_load_segment() 구현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);
read_bytes와 zero_bytes 계산vm_alloc_page_with_initializer(VM_FILE, upage, writable, lazy_load_segment, aux) 호출aux에는 (file, ofs, read_bytes) 정보를 담은 구조체 포인터 전달upage += PGSIZE, read_bytes = max(read_bytes - PGSIZE, 0), ofs += PGSIZElazy_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으로 채울 바이트 수
};aux를 struct load_info *info = aux;로 캐스팅
파일에서 info->read_bytes만큼 kva로 읽어오기
file_read_at(info->file, page->frame->kva, info->read_bytes, info->ofs);
남은 zero_bytes = PGSIZE - info->read_bytes만큼 memset(page->frame->kva + info->read_bytes, 0, zero_bytes)
PTE는 이미 install_page()가 호출된 상태이므로 단순 데이터 로드만 수행
setup_stack)setup_stack 함수 내에서 vm_alloc_page_with_initializer 대신 직접 vm_get_frame() 호출install_page(esp_base, frame->kva, true)로 PTE 매핑vm_try_handle_fault() 수정fault_addr에서 페이지 경계 정렬된 va = pg_round_down(fault_addr) 계산page = spt_find_page(¤t->spt, va) 호출 → 해당 struct page 가져옴exit(-1)page->operations->swap_in(page, frame_kva) 호출anon_swap_infile_backed_swap_inswap_in 내부에서 lazy_load_segment 또는 기존 데이터 로드 수행install_page(page->va, frame_kva, page->writable) 호출supplemental_page_table_copy)bool supplemental_page_table_copy(
struct supplemental_page_table *dst,
struct supplemental_page_table *src);
fork() 시 부모 프로세스의 SPT를 자식에게 복사src의 모든 struct page 순회page->operations->type)에 따라 새 struct page *new_page 생성vm_alloc_page_with_initializer로 VM_UNINIT 페이지 생성vm_do_claim_page(new_page) 호출해 즉시 실제 페이지 할당 및 데이터 로드spt_insert_page(&dst->spt, new_page) 호출true, 실패 시 falsesupplemental_page_table_kill)void supplemental_page_table_kill(struct supplemental_page_table *spt);
struct page 순회destroy(page) 호출 → 각 페이지 내부 리소스 해제uninit_destroy (초기화되지 않은 페이지)anon_destroy (익명 페이지)file_backed_destroy (파일 페이지)struct page 자체는 호출자가 freeuninit_destroy() (vm/uninit.c)static void uninit_destroy(struct page *page);
page->uninit.initializer와 page->uninit.aux 메모리 해제anon_destroy() (vm/anon.c)static void anon_destroy(struct page *page);
struct anon_page 내부 추가 메모리 해제(필요 시)struct page 자체는 호출자가 해제최종 점검: 모든 요구 사항을 구현하면 프로젝트 2의 테스트(특히 fork 제외)가 통과되어야 한다.
USER_STACK 주소 하나의 페이지(4KB)로 고정되어 있었다.rsp(스택 포인터)보다 높은(주소 상으로는 낮은) 위치에 접근할 때를 스택 확장 후보로 간주한다.PUSH 명령어는 실제로 스택 포인터를 조정하기 전에 메모리 권한을 검사하므로, rsp - 8 주소에서 페이지 폴트가 날 수 있다. 이 경우에도 스택 확장으로 처리해야 한다.rsp) 확보struct intr_frame *f의 f->rsp는 정의되지 않은 값일 수 있다.rsp를 struct thread에 저장하도록 수정userprog/exception.S 또는 인터럽트 엔트리 코드에서 f->rsp를 thread_current()->rsp_saved 등에 복사vm_try_handle_fault) 시 thread_current()->rsp_saved 값을 사용vm_try_handle_fault() 수정페이지 폴트 발생 시 호출 시그니처
bool vm_try_handle_fault(struct intr_frame *f, void *addr, bool user, bool write, bool not_present);
기존 흐름
추가할 작업
addr가 페이지 테이블에 없어서 not_present == true로 들어온 경우,addr이 사용자 스택 영역 범위에 해당하는지 확인addr이 (rsp_saved – STACK_GROWTH_THRESHOLD) 이상인지 검사rsp_saved - 32 <= addr < USER_STACK 이면 스택 확장 후보로 간주USER_STACK)에서 addr까지 크기를 계산하여 1MB 한도 내에 있는지 검사true일 때 vm_stack_growth(addr) 호출vm_stack_growth(addr)가 성공하면 vm_try_handle_fault는 true 반환하여 사용자 명령을 재개false 혹은 exit(-1) 처리vm_stack_growth() 구현void vm_stack_growth(void *addr);
addr: 페이지 폴트를 일으킨 사용자 주소(가상 주소)페이지 경계 정렬
void *page_va = pg_round_down(addr);
스택 최상위, 스택 바닥 주소 계산
USER_STACKSTACK_BOTTOM = pg_round_down(f->rsp_saved) - 현재 할당된 페이지 수 * PGSIZE스택 크기 확인
page_va)가 USER_STACK - 1MB <= page_va < USER_STACK 범위여야 함(USER_STACK - page_va) <= 1MB 확인페이지 할당
vm_alloc_page_with_initializer(VM_ANON, page_va, true, anon_initializer, NULL) 호출하여 SPT에 익명 페이지 예약vm_do_claim_page() 호출하여 실제 프레임 할당 및 PTE 설치exit(-1)반환
vm_try_handle_fault로 돌아가 사용자 명령 재개VM_ANON)이다.USER_STACK 주소에서 1MB를 뺀 값이 스택 확장의 최저 한계 주소가 된다.void *stack_limit = USER_STACK - (1 << 20); // 1 << 20 = 1MBaddr를 pg_round_down(addr) 했을 때 page_va < stack_limit 이면 확장 불가USER_STACK - PGSIZE 하나는 setup_stack() 단계에서 미리 할당하여 초기 스택으로 사용page_va가 USER_STACK - PGSIZE 이하로 내려갈 때마다 vm_stack_growth() 호출push [rsp - 8] 수행PTE 확인 → present = 0 → 페이지 폴트 발생 → vm_try_handle_fault 호출vm_try_handle_fault 내부:addr = rsp_saved - 8이 스택 영역 범위(USER_STACK - threshold <= addr)인지 판단vm_stack_growth(addr) 호출 →page_va = pg_round_down(addr)true 반환 → 사용자 명령 재개exit(-1) 처리위 절차대로 구현하면 “스택 접근 시 동적 페이지 할당 → 최대 1MB 제한” 테스트가 모두 통과