[Pintos] Virtual Memory 구현하기 Part1: Lazy Load 방식으로 프로그램 실행하기 (Memory Management, Anonymous Page, Stack Growth)

2025. 6. 6. 20:44·크래프톤 정글/Code 정글(C언어)

Virtual Memory 구현하기 Part1: Lazy Load 방식으로 프로그램 실행하기 (Memory Management, Anonymous Page, Stack Growth) 

Pintos Project3 Virtual Memory의 경우, Project1, 2에 비해 난이도가 상당히 높아졌는데요. 그럼에도 불구하고 선배 기수 분들이 정리를 잘 해주셔서 난관에 부딪혔을 때 참고해서 헤쳐나갈 수 있었습니다. 다만 블로그에 잘못된 코드가 올라가 있는 경우가 있어 적절히 수정하면서 적용해 나가야 했던 점이 아쉬웠는데, 이런 점을 없애보자는 취지로 구현 과정만 다이렉트로 정리해 보려고 합니다.

 

※ 주의: 제 컴퓨터 기준에서는, All Pass가 뜨는 것을 확인했으나, 다른 조원들의 컴퓨터에서 page-merge.* 테스트가 간헐적으로 실패하는 것을 확인했습니다. 따라서 그대로 적용하기보다는, 참고만 하시는 것을 추천드립니다.

 

Memory Management

(1) SPT(Supplemental Page Table) 인터페이스 추가하기 (vm.h)

해시 모듈 참조 추가하기

#include <hash.h>	// hash 함수 모듈

기존 spt 구조체에 hash 추가하기

struct supplemental_page_table {
	struct hash spt_hash;	// spt 해시 구조체
};

기존 page 구조체에 hash_elem 추가하기

/* Your implementation 아래에 추가 */
struct hash_elem hash_elem;

 

(2) supplemental_page_table_init 구현하기 (vm.c)

/* 보조 페이지 테이블 spt를 해시 테이블로 초기화 */
void
supplemental_page_table_init (struct supplemental_page_table *spt UNUSED) {
	// page_hash: 해시 함수, page_less: 비교 함수 사용
	hash_init(&spt->spt_hash, page_hash, page_less, NULL);
}

 

(3) 헬퍼 함수: page_hash, page_less 구현하기 (vm.c)

헬퍼 함수 선언 추가하기

/* vm.c 위쪽에 함수 선언 추가 */
uint64_t page_hash(const struct hash_elem *e, void *aux);
bool page_less(const struct hash_elem *a, const struct hash_elem *b, void *aux);

page_hash 구현하기

/* page의 va를 기준으로 해시 값을 계산 */
uint64_t 
page_hash(const struct hash_elem *p_, void *aux UNUSED) {
    // 해시 요소에서 struct page 포인터 추출
    const struct page *p = hash_entry(p_, struct page, hash_elem);

    // va 값을 바이트 단위로 해싱하여 해시값 반환
    return hash_bytes(&p->va, sizeof p->va);
}

page_less 구현하기

/* 두 page의 va를 비교하여 정렬 순서를 결정 (va 기준 오름차순) */
bool
page_less(const struct hash_elem *a_,
           const struct hash_elem *b_, void *aux UNUSED) {
    // 해시 요소에서 struct page 포인터 추출
    const struct page *a = hash_entry(a_, struct page, hash_elem);
    const struct page *b = hash_entry(b_, struct page, hash_elem);

    // va 기준으로 작은 쪽이 먼저 오도록 비교
    return a->va < b->va;
}

 

(4) spt_find_page 구현하기 (vm.c)

/* SPT에서 va에 해당하는 page 구조체를 찾아 반환 */
struct page *
spt_find_page (struct supplemental_page_table *spt UNUSED, void *va UNUSED) {
	// 해시 탐색용 임시 페이지
	struct page page;

	// va를 페이지 기준 주소로 정렬
    page.va = pg_round_down(va);

	// 해당 주소에 대응하는 page를 해시 테이블에서 탐색
    struct hash_elem *e = hash_find(&spt->spt_hash, &page.hash_elem);

	// 찾으면 page 반환, 없으면 NULL
    return e != NULL ? hash_entry(e, struct page, hash_elem) : NULL;
}

 

(5) spt_insert_page 구현하기 (vm.c)

/* spt에 page를 삽입. 이미 존재하면 실패 */
bool
spt_insert_page (struct supplemental_page_table *spt UNUSED,
		struct page *page UNUSED) {
	// 삽입 성공 시 NULL 반환 → true, 실패(중복) 시 false
	return hash_insert(&spt->spt_hash, &page->hash_elem) == NULL;
}

 

(6) spt_remove_page 구현하기 (vm.c)

/* spt에서 page를 제거하고 메모리 해제 */
void
spt_remove_page (struct supplemental_page_table *spt, struct page *page) {
	// 해시 테이블에서 page 제거
	hash_delete(&spt->spt_hash, &page->hash_elem);

	// page 메모리 해제
	vm_dealloc_page(page);
}

 

(7) Frame Table 인터페이스 추가

frame table용 전역 변수 선언하기 (vm.c)

struct list frame_table;	// 현재 할당된 frame 추적, 관리용 리스트
struct lock frame_lock;		// frame table 접근 동기화를 위한 전역 락

frame table용 전역 변수 초기화하기 (vm.c)

// vm_init의 #endif 아래 맨 밑에 추가
list_init(&frame_table);
lock_init(&frame_lock);

기존 frame 구조체에 list_elem 추가하기 (vm.h)

// kva, page 필드 아랫줄에 추가
struct list_elem frame_elem;

 

(8) vm_get_frame 구현하기 (vm.c)

/* 새로운 frame을 할당하거나 필요 시 교체하여 반환 */
static struct frame *
vm_get_frame (void) {
	// frame 구조체 동적 할당
	struct frame *frame = (struct frame *)malloc(sizeof(struct frame));
	ASSERT (frame != NULL);

	// 사용자 영역용 물리 페이지 할당 (0으로 초기화)
	frame->kva = palloc_get_page(PAL_USER | PAL_ZERO);  

	// 할당 실패 시, frame을 하나 선택해 교체
    if (frame->kva == NULL) {
        frame = vm_evict_frame();
	} else {
        // frame 테이블에 추가
        lock_acquire(&frame_lock);
        list_push_back(&frame_table, &frame->frame_elem);
		lock_release(&frame_lock);
	}

	// 초기에는 매핑된 page 없음
    frame->page = NULL;

	ASSERT (frame->page == NULL);
	return frame;
}

 

(9) vm_claim_page 구현하기 (vm.c)

/* 주어진 va에 해당하는 page를 찾아 물리 메모리에 매핑 */
bool
vm_claim_page (void *va UNUSED) {
	// SPT에서 va에 해당하는 page 찾기
	struct page *page = spt_find_page(&thread_current()->spt, va);

	// 없다면 실패
    if (page == NULL)
        return false;

	// page를 실제 물리 메모리에 매핑
	return vm_do_claim_page(page);
}

 

(10) vm_do_claim_page 구현하기 (vm.c)

/* page에 frame을 할당하고 물리 메모리에 매핑 및 데이터 로드 */
static bool
vm_do_claim_page (struct page *page) {
	// 새 frame 할당
	struct frame *frame = vm_get_frame();

	/* 연결 설정 */
	frame->page = page;
	page->frame = frame;

	// 가상 주소(page->va)와 물리 주소(frame->kva) 매핑
    if (!pml4_set_page(thread_current()->pml4, page->va, frame->kva, page->writable))
        return false;

	// 디스크나 swap 영역에서 실제 데이터 로드
	return swap_in(page, frame->kva);
}

mmu 모듈 참조 추가하기 (vm.c)

#include "threads/mmu.h"	// pml4 함수 모듈

기존 page 구조체에 writable 추가하기 (vm.h)

// hash_elem 아랫줄에 추가
bool writable;

 

 

Anonymous Page

(1) vm_alloc_page_with_initializer 구현하기 (vm.c)

/* 초기화 함수와 함께 새로운 가상 페이지를 생성하고 SPT에 삽입 */
bool
vm_alloc_page_with_initializer (enum vm_type type, void *upage, bool writable,
		vm_initializer *init, void *aux) {

	ASSERT (VM_TYPE(type) != VM_UNINIT);

	struct supplemental_page_table *spt = &thread_current ()->spt;

	/* 이미 매핑된 페이지가 없는 경우에만 진행 */
	if (spt_find_page(spt, upage) == NULL) {

		// 새 page 구조체 동적 할당
		struct page *page = malloc(sizeof(struct page));
		if (!page)
			return false;

		// 타입별 초기화 함수 선택
		typedef bool (*initializer_by_type)(struct page *, enum vm_type, void *);
		initializer_by_type initializer = NULL;

		switch (VM_TYPE(type)) {
		case VM_ANON:
			initializer = anon_initializer;
			break;
		case VM_FILE:
			initializer = file_backed_initializer;
			break;
		}

		// uninit page 생성 및 초기화 정보 등록
		uninit_new(page, upage, init, type, aux, initializer);
		page->writable = writable;

		// SPT에 삽입 후 성공 여부 반환
		return spt_insert_page(spt, page);
	}
}

 

(2) lazy_load_arg 구조체 추가하기 (vm.h)

struct lazy_load_arg {
	struct file *file;
	off_t ofs;
	uint32_t read_bytes;
	uint32_t zero_bytes;
};

 

(3) load_segment 구현하기 (VM전용, process.c)

/* 파일의 segment를 lazy loading 방식으로 메모리에 매핑 */
static bool
load_segment(struct file *file, off_t ofs, uint8_t *upage,
			 uint32_t read_bytes, uint32_t zero_bytes, bool writable) {
	// 총 크기는 페이지 단위로 정렬되어야 함
	ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
	// 가상 주소는 페이지 기준 정렬이어야 함
	ASSERT(pg_ofs(upage) == 0);
	// 파일 오프셋도 페이지 단위여야 함
	ASSERT(ofs % PGSIZE == 0);

	while (read_bytes > 0 || zero_bytes > 0)
	{
		// 이번 페이지에 읽을 바이트 수
		size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
		// 나머지는 0으로 채움
		size_t page_zero_bytes = PGSIZE - page_read_bytes;

		// lazy_load_segment에 전달할 인자 구조체 할당 및 설정
		struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)malloc(sizeof(struct lazy_load_arg));
		lazy_load_arg->file = file;
		lazy_load_arg->ofs = ofs;
		lazy_load_arg->read_bytes = page_read_bytes;
		lazy_load_arg->zero_bytes = page_zero_bytes;

		// lazy loading용 uninit 페이지 할당
		if (!vm_alloc_page_with_initializer(VM_ANON, upage,
											writable, lazy_load_segment, lazy_load_arg))
			return false;

		// 다음 페이지로 이동
		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		upage += PGSIZE;
		ofs += page_read_bytes;
	}
	return true;
}

 

(4) lazy_load_segment 구현하기 (VM전용, process.c)

/* page fault 시 호출되어 파일 내용을 물리 메모리에 로드 */
bool
lazy_load_segment(struct page *page, void *aux) {
	// 파일 읽기 정보가 담긴 구조체로 변환
	struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)aux;

	// 파일 오프셋 설정
	file_seek(lazy_load_arg->file, lazy_load_arg->ofs);

	// 파일에서 read_bytes만큼 읽어 page의 물리 메모리(kva)에 저장
	if (file_read(lazy_load_arg->file, page->frame->kva, lazy_load_arg->read_bytes) 
		!= (int)(lazy_load_arg->read_bytes))
	{
		// 실패 시 물리 페이지 해제 후 false 반환
		palloc_free_page(page->frame->kva);
		return false;
	}

	// 나머지 영역은 0으로 초기화
	memset(page->frame->kva + lazy_load_arg->read_bytes, 0, lazy_load_arg->zero_bytes);

	return true;
}

 

(5) setup_stack 구현하기 (VM전용, process.c)

/* 사용자 스택 할당 및 초기 rsp 설정 */
static bool
setup_stack(struct intr_frame *if_) {
	bool success = false;

	// 스택의 첫 페이지 주소 (USER_STACK에서 한 페이지 아래)
	void *stack_bottom = (void *)(((uint8_t *)USER_STACK) - PGSIZE);

	// 스택 페이지를 SPT에 등록
	if (vm_alloc_page(VM_ANON | VM_MARKER_0, stack_bottom, 1))
	{
		// 물리 프레임을 실제로 할당
		success = vm_claim_page(stack_bottom);
		if (success)
			// rsp를 스택 최상단(USER_STACK)으로 설정
			if_->rsp = USER_STACK;
	}
	return success;
}

기존 thread 구조체에 stack_pointer 추가하기 (thread.h)

/* thread.h - thread 구조체의 spt 아래에 추가 */
void *stack_pointer;

 

(6) vm_try_handle_fault 구현하기 (vm.c)

/* 페이지 폴트 발생 시, 유효한 접근이면 페이지를 메모리에 매핑 */
bool
vm_try_handle_fault(struct intr_frame *f UNUSED, void *addr UNUSED,
		bool user UNUSED, bool write UNUSED, bool not_present UNUSED) {
	struct supplemental_page_table *spt UNUSED = &thread_current()->spt;

	// 유효하지 않은 주소(커널 영역 or NULL) 접근은 처리 불가
	if (addr == NULL || is_kernel_vaddr(addr))
		return false;

	// 접근한 페이지가 존재하지 않아 발생한 폴트인 경우
	if (not_present) {
		// SPT에서 해당 주소에 대한 페이지 정보 탐색
		struct page *page = spt_find_page(spt, addr);
		if (page == NULL)
			return false;

		// 쓰기 권한 없는 페이지에 대한 쓰기 접근은 처리 불가
		if (write && !page->writable)
			return false;

		// 페이지 실제 물리 메모리에 매핑
		return vm_do_claim_page(page);
	}
	return false;
}

 

(7) supplemental_page_table_copy 구현하기 (vm.c)

/* src SPT의 내용을 dst SPT로 복사 */
bool
supplemental_page_table_copy(struct supplemental_page_table *dst UNUSED,
		struct supplemental_page_table *src UNUSED) {
	
	// src SPT 해시 테이블을 순회할 반복자 초기화
	struct hash_iterator i;
	hash_first(&i, &src->spt_hash);

	while (hash_next(&i)) {
		// 현재 항목의 page 정보 가져오기
		struct page *src_page = hash_entry(hash_cur(&i), struct page, hash_elem);
		enum vm_type src_type = src_page->operations->type;

		if (src_type == VM_UNINIT) {
			// lazy load용 uninit 페이지는 메타데이터만 복사
			vm_alloc_page_with_initializer(
				src_page->uninit.type,
				src_page->va,
				src_page->writable,
				src_page->uninit.init,
				src_page->uninit.aux);
		} else {
			// 이미 초기화된 페이지는 새로 할당하고 데이터 복사
			if (vm_alloc_page(src_type, src_page->va, src_page->writable) &&
				vm_claim_page(src_page->va)) {
				struct page *dst_page = spt_find_page(dst, src_page->va);
				memcpy(dst_page->frame->kva, src_page->frame->kva, PGSIZE);
			}
		}
	}
	return true;
}

 

(8) supplemental_page_table_kill 구현하기 (vm.c)

/* SPT의 모든 페이지를 제거하고 메모리 해제 */
void
supplemental_page_table_kill(struct supplemental_page_table *spt UNUSED) {
	// 해시 테이블 전체 삭제, 각 항목은 hash_page_destroy로 정리
	hash_clear(&spt->spt_hash, hash_page_destroy);
}

 

(9) 헬퍼 함수: hash_page_destroy 구현하기 (vm.c)

헬퍼 함수 선언 추가하기

// vm.c 위쪽에 함수 선언 추가
void hash_page_destroy(struct hash_elem *e, void *aux);

hash_page_destroy 구현하기

/* 해시 테이블에서 페이지 제거 시 호출되는 정리 함수 */
void 
hash_page_destroy(struct hash_elem *e, void *aux) {
    // hash_elem에서 page 구조체 포인터 추출
    struct page *page = hash_entry(e, struct page, hash_elem);

    // 페이지 내부 자원 해제 (frame, swap 등)
    destroy(page);

    // page 구조체 자체 메모리 해제
    free(page);
}

 

(10) check_page(사용자 주소 검증 함수) 수정하기 (validate.c)

참고로 validate.c는 기본 Pintos-kaist 디렉터리에는 존재하지 않는데요. 사용자 주소 검증을 위한 함수들을 모아놓기 위해 따로 만든 파일입니다. 다른 레퍼런스를 참고한다면, synch.c의 check_addr 함수 등을 변경해야 합니다.

/* uaddr가 유저 영역 주소이며 매핑된 페이지가 존재하는지 확인 */
static bool
check_page(const void *uaddr) {
	// NULL이거나 커널 영역 주소면 잘못된 접근
    if (uaddr == NULL || !is_user_vaddr(uaddr)) {
        return false;
    }

#ifdef VM
    // VM이 활성화된 경우, SPT에서 페이지 존재 여부 확인
    struct thread *curr = thread_current();
    struct page *page = spt_find_page(&curr->spt, uaddr);

    // 존재하면 true
    if (page != NULL) return true;

    return false;

#else
    // VM 미사용 시, 직접 page table에서 매핑 여부 확인
    return pml4_get_page(thread_current()->pml4, uaddr) != NULL;
#endif
}

 

(11) page fault 조건 수정하기 (exception.c)

#ifdef VM
	// 페이지 폴트 처리 실패 시 → 프로세스 종료
	if (vm_try_handle_fault (f, fault_addr, user, write, not_present)) {
		return;
	} else {
		sys_exit(-1);
	}
#endif

여기까지 구현이 끝나면, Page Fault로 인해 실행부터 막혔던 프로그램들이 정상적으로 실행되면서, 기본적인 테스트 수행이 가능하게 됩니다.

 

 

Stack Growth

(1) vm_try_handle_fault 수정하기 (vm.c)

/* 페이지 폴트 발생 시, SPT 또는 스택 확장을 통해 처리 시도 */
bool
vm_try_handle_fault(struct intr_frame *f, void *fault_addr,
                    bool user, bool write, bool not_present) {
    struct thread *t = thread_current();
    struct supplemental_page_table *spt = &t->spt;

    // NULL이거나 커널 주소면 잘못된 접근 → 처리 실패
    if (fault_addr == NULL || is_kernel_vaddr(fault_addr))
        return false;

    // SPT에서 fault_addr에 해당하는 페이지 검색
    struct page *page = spt_find_page(spt, fault_addr);

    // 접근이 존재하는 페이지에 대한 쓰기일 경우 → 처리 불가
    if (!not_present && write)
        return false;

    // 해당 페이지가 존재하면 처리 시도
    if (page != NULL) {
        // 쓰기 불가능한 페이지에 쓰기 접근 → 실패
        if (write && !page->writable)
            return false;

        // 실제 페이지 할당 및 매핑 시도
        return vm_do_claim_page(page);
    }

    // SPT에 없는 경우: 스택 확장 가능한 상황인지 확인
    void *rsp = user ? f->rsp : t->stack_pointer;
    bool can_grow =
        fault_addr >= STACK_LIMIT &&       // 최소 스택 크기 제한
        fault_addr < USER_STACK &&         // 유저 영역 내
        fault_addr >= rsp - 32;            // rsp 기준 접근 가능 범위

    // 조건 만족 시 스택 확장
    if (can_grow)
        return vm_stack_growth(fault_addr);

    // 그 외의 경우 처리 실패
    return false;
}

vm_try_handle_fault용 매크로 추가하기

/* 스택은 최대 1 MiB, USER_STACK 기준 아래로 확장할 수 있다. */
#define STACK_LIMIT (USER_STACK - (1 << 20))

/* 최대 몇 바이트까지 rsp 아래 접근을 스택 성장으로 허용할지. 
   8 바이트(단일 push) + 여유를 주고 싶다면 32로 늘릴 수 있다. */
#define STACK_GROW_GAP 32

 

(2) vm_stack_growth 구현하기 (vm.c)

기존 void로 반환값이 없었지만, bool 값을 반환하도록 변경해줍니다.

/* fault 주소를 기준으로 스택 영역을 한 페이지 확장 */
static bool
vm_stack_growth(void *addr UNUSED) {
    bool success = false;

	// 페이지 단위로 정렬
	addr = pg_round_down(addr);

    // 새 스택 페이지를 SPT에 등록
    if (vm_alloc_page(VM_ANON | VM_MARKER_0, addr, true)) {
        // 실제 물리 메모리 할당 및 매핑
        success = vm_claim_page(addr);

        if (success) {
			return true;
        }
    }
	return false;
}

 

(3) sys_read 실패 조건 추가하기 (syscall.c)

// sys_read의 validate_ptr 아랫줄에 추가
#ifdef VM
    // buffer가 SPT에 등록되어 있고, 쓰기 불가능한 페이지인 경우 → 비정상 종료
    struct page *page = spt_find_page(&thread_current()->spt, buffer);
    if (page && !page->writable)
        sys_exit(-1);
#endif

 

(4) syscall_handler에서 스레드 rsp 갱신 처리하기 (syscall.c)

// syscall_handler 시작 부분에 추가
#ifdef VM
    // 현재 스레드의 커널 스택 포인터를 유저 컨텍스트의 rsp로 저장
    thread_current()->stack_pointer = f->rsp;
#endif

 

(5) check_page(사용자 주소 검증 함수) 수정하기 (validate.c)

추가로 스택 확장 조건에 해당하는지에 대해 검사합니다.

// if (page != NULL) return true; 와 return false; 사이에 추가
// 그 외에는 스택 확장 조건인지 확인
void *rsp = curr->stack_pointer;
if ((uaddr >= rsp - 8 || uaddr >= rsp) && uaddr <= USER_STACK) {
    return true;
}

 

 

저작자표시 비영리 변경금지 (새창열림)

'크래프톤 정글 > Code 정글(C언어)' 카테고리의 다른 글

[Pintos] tests 디렉터리의 모든 txt파일 CRLF 없애기  (0) 2025.06.09
[Pintos] Virtual Memory 구현하기 Part2: 파일과 페이지 공유 및 교체하기 (Memory Mapped Files, Swap In/Out, Copy On Write)  (0) 2025.06.06
[Pintos] OSTEP 기반 Virtual Memory 배경지식 정리: 왜 VM이 필요한가  (0) 2025.05.31
[Pintos] Virtual Memory Layout 정리  (0) 2025.05.30
[Pintos] Virtual Memory 전체적인 큰 그림 그리기  (1) 2025.05.30
'크래프톤 정글/Code 정글(C언어)' 카테고리의 다른 글
  • [Pintos] tests 디렉터리의 모든 txt파일 CRLF 없애기
  • [Pintos] Virtual Memory 구현하기 Part2: 파일과 페이지 공유 및 교체하기 (Memory Mapped Files, Swap In/Out, Copy On Write)
  • [Pintos] OSTEP 기반 Virtual Memory 배경지식 정리: 왜 VM이 필요한가
  • [Pintos] Virtual Memory Layout 정리
그냥사람_
그냥사람_
IT 관련 포스팅을 합니다. 크래프톤 정글 8기 정경호
  • 그냥사람_
    그냥코딩
    그냥사람_
  • 전체
    오늘
    어제
    • 글 전체보기
      • 크래프톤 정글
        • 로드 투 정글(입학시험)
        • CS기초(키워드, 개념정리)
        • 컴퓨터구조(CSAPP)
        • Code 정글(C언어)
        • Equipped in 정글(나만무)
        • 마이 정글(WIL, 에세이)
      • 자료구조&알고리즘
        • 자료구조
        • 알고리즘
      • 일상
  • 블로그 메뉴

    • 홈
  • 링크

    • Github
  • hELLO· Designed By정상우.v4.10.3
그냥사람_
[Pintos] Virtual Memory 구현하기 Part1: Lazy Load 방식으로 프로그램 실행하기 (Memory Management, Anonymous Page, Stack Growth)
상단으로

티스토리툴바