[PintOS] Project 3 : VM - Memory Management & Anonymous Page

CorinBeom·2025년 6월 1일

PintOS

목록 보기
10/19
post-thumbnail

이번 포스팅에서는 PintOS Project 3 가상 메모리 구현을 시작해보자 !

구현 목표

PintOS 기본 코드에는 유저 프로세스가 실행될 때,
물리 메모리(palloc)에만 의존한 단순한 메모리 모델이 존재함 !
하지만 현실의 OS에서는 다음과 같은 기능들이 필요하다

  • 지연 로딩(Lazy loading) : 실제로 접근하기 전까지는 물리 메모리를 할당하지 않는다 !

  • 익명 페이지 관리(Anonymous Page) : 파일과 무관한 일반 데이터 페이지를 관리

  • 스왑 공간 분리 (Swap space) : 물리 메모리가 부족할 때 디스크로 페이지를 내보내고 다시 불러오기 !

  • 스택 자동 확장(Stack Growth) : 스택이 확장될 경우 새로운 페이지를 자동으로 확보

이번 포스팅에서는 Memory Management 전반부와 Anonymous Page 관리 기능의 구현 과정
을 같이 알아가보자 !

참고로 이번 포스팅에서는 vm.c에 구현하는 코드만 다룰 예정이다.


supplemental_page_table_init() - 보조 페이지 테이블 초기화

PintOS에서는 가상 주소 공간을 관리하기 위해 Supplemental Page Table (SPT)이라는 구조를 도입.
이는 커널의 pml4 페이지 테이블과는 별개로,
유저 가상 주소마다 추가 메타데이터(페이지 타입, 백업 위치 등)를 저장하는 자료구조.

우리는 SPT를 hash table로 구현하며, 각 struct page는 va(가상 주소)를 키로 사용한다.

void
supplemental_page_table_init (struct supplemental_page_table *spt UNUSED) {
	hash_init(&spt->spt_hash, page_hash, page_less, NULL);
}
  • spt->spt_hash : 페이지 정보를 저장할 해시 테이블

  • page_hash : 해시 값을 계산할 함수

  • page_less : 정렬 및 비교 시 사용할 함수

unsigned page_hash(const struct hash_elem *p_, void *aux UNUSED)

unsigned page_hash(const struct hash_elem *p_, void *aux UNUSED) {
	const struct page *p = hash_entry(p_, struct page, hash_elem);
	return hash_bytes(&p->va, sizeof p->va);
}
  • SPT는 가상 주소(va)를 키로 사용함

  • 이 함수는 page->kva의 바이트를 기반으로 해시 값을 생성

  • 해시 테이블에서 빠르게 페이지를 검색 가능하게 해줌

bool page_less(const struct hash_elem *a_, const struct hash_elem *b_, void *aux UNUSED)

bool page_less(const struct hash_elem *a_, const struct hash_elem *b_, void *aux UNUSED) {
	const struct page *a = hash_entry(a_, struct page, hash_elem);
	const struct page *b = hash_entry(b_, struct page, hash_elem);

	return a->va < b->va;
}
  • struct page의 가상 주소 (va)를 비교

  • hash_replace()나 내부 정렬 시에 사용됨

  • 이 비교는 순서성을 보장해 충돌 처리나 동등성 판별에 유용하다 !


supplemental_page_table_copy() -SPT 복사

fork() 시스템 콜을 구현할 때, 부모 프로세스의 가상 메모리 상태(SPT)를 자식에게 복사해야 한다.
이 함수는 src 보조 페이지 테이블을 dst로 복제하여, 자식 프로세스가 동일한 주소 공간을 가질 수 있도록 준비하는 함수.

1. src 테이블 순회

struct hash_iterator i;
hash_first(&i, &src->spt_hash);
while (hash_next(&i)) {
	...
}
  • src SPT에 있는 모든 struct page를 순회한다

  • 각 페이지 정보를 dst에 복사할 준비

2. 페이지 타입 분기

enum vm_type type = src_page->operations->type;
void *upage = src_page->va;
bool writable = src_page->writable;

각 페이지에 대해 다음 두 가지로 분기한다 :

case 1 : Uninitialized Page (지연 로딩 대기 중)

if (type == VM_UNINIT) {
	vm_initializer *init = src_page->uninit.init;
	void *aux = src_page->uninit.aux;

	vm_alloc_page_with_initializer(VM_ANON, upage, writable, init, aux);
	continue;
}
  • 부모는 아직 해당 페이지를 지연 로딩으로 미초기화(uninit) 상태로 두었음

  • 자식도 동일한 방식으로 Uninit 상태로 복사

  • 즉시 로딩할 필요 없음 → continue

case 2 : 이미 초기화된 페이지

if (!vm_alloc_page_with_initializer(type, upage, writable, NULL, NULL))
	return false;

if (!vm_claim_page(upage))
	return false;
  • vm_alloc_page_with_initializer()로 페이지를 예약

  • vm_claim_page()를 호출해 즉시 물리 프레임 할당 및 PML4에 매핑

이 시점에서 자식 페이지는 이미 접근 가능한 상태가 됨

3. 실제 데이터 복사

struct page *dst_page = spt_find_page(dst, upage);
memcpy(dst_page->frame->kva, src_page->frame->kva, PGSIZE);
  • 부모의 kva 내용을 자식 페이지의 프레임에 복사

  • 이를 통해 유저가 malloc()한 데이터 등도 정확히 복제됨

주의 !

  • 페이지 타입이 VM_UNINIT인 경우는 실제 데이터가 없는 상태이므로, 복사 없이 예약만 하면 됨

  • claim_page()memcpy()를 해야 실제 데이터를 갖는 페이지도 정확히 동작


vm_claim_page() & vm_do_claim_page() - 페이지 확보

이 함수들은 유저 프로그램이 특정 가상 주소에 접근했을 때,
그 주소에 매핑된 페이지를 물리 메모리에 확보하고(MMU 등록 포함),
필요하다면 디스크 또는 lazy data로부터 내용을 불러오는 역할을 수행한다.

보통 이 함수들은 page fault 핸들링 과정에서 호출됨

vm_claim_page()

struct page *page = spt_find_page(&thread_current()->spt, va);
if (page == NULL)
	return false;
return vm_do_claim_page(page);
  • 인자로 받은 va(가상 주소)에 해당하는 페이지를 SPT에서 탐색

  • 존재하지 않으면 실패(false)

  • 존재할 경우 → vm_do_claim_page()로 실제 확보 절차 진행


vm_do_claim_page()

struct frame *frame = vm_get_frame();
frame->page = page;
page->frame = frame;
  • 페이지를 담을 빈 물리 프레임 하나 가져옴

  • 해당 프레임과 페이지를 서로 연결

pml4_set_page(current->pml4, page->va, frame->kva, page->writable);
  • 현재 스레드의 페이지 테이블(PML4)에
    va → kva (가상주소 → 커널 주소)의 MMU 매핑 엔트리 생성

  • writable 여부도 함께 설정

이 과정에서 비로소 해당 페이지에 대한 접근이 가능해짐

return swap_in(page, frame->kva);
  • 해당 페이지의 실제 데이터가 존재할 경우,
    이를 디스크(swap), 파일, lazy load source 등에서 프레임으로 로딩

vm_alloc_page_with_initializer() - 지연 초기화용 페이지 할당

이 함수는 아직 실제로 물리 메모리를 할당하지 않고,
"필요할 때 초기화(lazy load)"되도록 설정된 페이지를 SPT에 등록한다.
보통 ELF 실행 파일을 로드할 때나 mmap()으로 파일 매핑할 때 호출됨

즉, 이 함수는 페이지를 예약만 해두는 역할이며,
실제로 메모리를 확보하거나 내용을 적재하는 건 vm_claim_page()에서 수행된다 !

먼저 이 함수의 인자에 대해서 알아보자

bool vm_alloc_page_with_initializer(enum vm_type type, void *upage,
	bool writable, vm_initializer *init, void *aux)
인자설명
type페이지의 타입 (예: VM_ANON, VM_FILE)
upage유저 가상 주소
writable쓰기 가능 여부
initLazy initializer 함수 포인터
auxinitializer에 전달할 부가 정보

1. 이미 존재하는지 확인

if (spt_find_page (spt, upage) == NULL) {
  • 해당 주소의 페이지가 이미 등록되어 있다면 중복 등록은 하지 않음 → 실패 반환

2. struct page 할당 + 페이지 타입 별 초기화 함수 설정

struct page *p = (struct page *)malloc(sizeof(struct page));
bool (*page_initializer)(struct page *, enum vm_type, void *);

switch (VM_TYPE(type)) {
	case VM_ANON:
		page_initializer = anon_initializer;
		break;
	case VM_FILE:
		page_initializer = file_backed_initializer;
		break;
}
  • 페이지 구조체를 동적 할당

  • 페이지 타입에 따라 초기화 함수 결정
    (예: VM_ANON → anon_initializer / VM_FILE → file_backed_initializer)

3. uninit_new()로 Uninit 페이지 초기화

uninit_new(p, upage, init, type, aux, page_initializer);
p->writable = writable;
  • 내부 상태는 Uninit 상태지만, lazy-load 시 필요한 정보를 다 세팅

  • init은 지연 로딩할 때 실제 로직을 수행할 함수이다

  • aux는 파일 오프셋, 길이, 읽기 정보 등 context 정보를 담는다

4. SPT에 삽입

return spt_insert_page(spt, p);
  • 최종적으로 SPT에 삽입하고 성공 여부를 반환한다.

spt_find_page() & spt_insert_page() - SPT 탐색 및 삽입

Supplemental Page Table (SPT)struct page들을 해시 테이블로 관리하는 구조이며,
이 두 함수는 각각 다음 역할을 수행한다:

  • spt_find_page() → 가상 주소로 등록된 페이지를 찾기

  • spt_insert_page() → 새로운 페이지를 보조 테이블에 삽입하기

이 함수들은 vm_claim_page(), vm_alloc_page_with_initializer()
모든 메모리 할당/요청 경로에서 공통적으로 호출됨.

spt_find_page()

struct page *
spt_find_page (struct supplemental_page_table *spt, void *va) {
	struct page p;
	struct hash_elem *e;

	p.va = pg_round_down(va); // 가상 주소를 페이지 단위로 정렬
	e = hash_find(&spt->spt_hash, &p.hash_elem);
	return e != NULL ? hash_entry(e, struct page, hash_elem) : NULL;
}
  • 입력된 va는 실제 접근 주소일 수 있으므로 pg_round_down()을 통해 페이지 시작 주소로 정렬

  • 페이지의 키 값은 struct page.va 이므로, 임시 구조체 va만 설정하고 hash_elem으로 검색

  • 찾으면 hash_entry()struct page 포인터를 역으로 얻어서 반환한다.

spt_insert_page()

bool
spt_insert_page (struct supplemental_page_table *spt, struct page *page) {
	return hash_insert(&spt->spt_hash, &page->hash_elem) == NULL ? true : false;
}
  • page->hash_elem 을 SPT 테이블에 삽입한다

  • hash_insert()는 중복된 키가 존재하면 기존 요소를 반환하므로, NULL일 때만 성공으로 처리

  • 중복 삽입을 방지한다.


vm_get_page() - 유저용 물리 프레임 확보

가상 페이지(struct page)가 실제 메모리에서 사용할 수 있도록 하려면,
물리 프레임(struct frame)을 하나 할당받아 연결해야 함.

이 함수는:

  • 유저 영역 물리 페이지(palloc)를 하나 할당받고

  • 이를 담는 struct frame을 만들어 반환함

아직은 frame_table(프레임 목록)이나 eviction 로직은 포함되지 않은 단순 버전

void *kva = palloc_get_page(PAL_USER);
if (kva == NULL)
	PANIC("todo");
  • 유저 영역용 물리 페이지 1장을 요청

  • 실패 시 커널 패닉 발생 (나중에 여기를 eviction trigger 지점으로 바꾸게 된다 )

frame = (struct frame *)malloc(sizeof(struct frame));
frame->kva = kva;
frame->page = NULL;
  • kva를 담는 프레임 구조체 생성

  • 초기 상태에서는 어떤 struct page와도 연결되어 있지 않음 (frame->page = NULL)

ASSERT (frame != NULL);
ASSERT (frame->page == NULL);
return frame;
  • 메모리 확보 확인용 ASSERT

  • 구조체 반환


vm_try_handle_fault() - 페이지 폴트 처리 시도

유저 프로그램이 접근한 주소에 대해 페이지 폴트가 발생했을 때,
이 함수는 그 상황이 정상적인 가상 메모리 상황인지 판단하고,
필요하다면 해당 페이지를 물리 메모리에 로딩하여 문제를 해결한다.

즉, "이 접근은 유효한가? → 유효하다면 페이지를 메모리에 올려줘" 역할.

이 함수도 인자가 많다. 함수의 시그니처를 먼저 알아보자

bool vm_try_handle_fault(struct intr_frame *f, void *addr,
                         bool user, bool write, bool not_present)
인자설명
f인터럽트 프레임 (사용하지 않음)
addr접근한 주소 (fault address)
user유저 모드 접근인지 여부
write쓰기 접근인지 여부
not_present페이지가 없어서 fault가 발생했는지 여부

1. 주소 유효성 검사

if (addr == NULL || is_kernel_vaddr(addr))
	return false;
  • 접근 주소가 NULL이거나 커널 주소라면 false를 반환한다.

2. not_present 케이스만 처리

if (not_present) {
	page = spt_find_page(spt, addr);
	if (page == NULL)
		return false;

	if (write == 1 && page->writable == 0)
		return false;

	return vm_do_claim_page(page);
}
  • 페이지가 아예 존재하지 않아서 생긴 페이지 폴트만 처리

    • 예: lazy loading, swap-in, stack growth 등
  • 해당 가상 주소의 페이지를 SPT에서 찾음

  • 존재하고, 쓰기 권한도 유효하다면 → vm_do_claim_page() 호출하여 프레임 확보 + 매핑 + swap-in

3. 그 외 상황은 처리 불가

return false;
  • 페이지는 존재하지만 Protection Fault (쓰기 권한 없음 등) → 실패

  • 페이지는 있지만 다른 이유로 못 처리하는 경우도 포함


supplemental_page_table_kill() - SPT 정리 및 자원 해제

프로세스가 종료되면 해당 프로세스가 소유한 모든 가상 메모리 자원(SPT)도 반드시 해제되어야 한다.
이 함수는 보조 페이지 테이블에 등록된 모든 struct page를 순회하며:

  • 자원 회수 (프레임, 파일, 스왑 공간 등)
  • page 구조체 자체 메모리 해제

를 수행한다.

supplemental_page_table_kill()

void
supplemental_page_table_kill (struct supplemental_page_table *spt) {
	hash_clear(&spt->spt_hash, clear_hash_page);
}
  • hash_clear()를 통해 SPT 내부의 모든 hash_elem을 제거

  • 각 요소 삭제 시마다 clear_hash_page() 호출

clear_hash_page()

void clear_page_hash(struct hash_elem *h, void *aux) {
	struct page *page = hash_entry(h, struct page, hash_elem);
	destroy(page);
	free(page);
}
  • hash_elem을 통해 struct page *를 얻음

  • 페이지 타입에 따라 알맞은 destroy() 함수 호출

    • 예: anon_destroy() → swap 해제
    • file_backed_destroy() → mmap 파일 write-back 등
  • 마지막으로 struct page 메모리 해제\


vm.c의 Memory Management 전반부와 Anonymous Page 관리 기능의 구현 과정을 알아보았다 !
다음 포스팅에서는 기존에 작성했던 process.c와 syscall.c 부분에 대해서 알아보도록 하자 !

profile
Before Sunrise

0개의 댓글