
이번 포스팅에서는 PintOS Project 3 가상 메모리 구현을 시작해보자 !
PintOS 기본 코드에는 유저 프로세스가 실행될 때,
물리 메모리(palloc)에만 의존한 단순한 메모리 모델이 존재함 !
하지만 현실의 OS에서는 다음과 같은 기능들이 필요하다
지연 로딩(Lazy loading) : 실제로 접근하기 전까지는 물리 메모리를 할당하지 않는다 !
익명 페이지 관리(Anonymous Page) : 파일과 무관한 일반 데이터 페이지를 관리
스왑 공간 분리 (Swap space) : 물리 메모리가 부족할 때 디스크로 페이지를 내보내고 다시 불러오기 !
스택 자동 확장(Stack Growth) : 스택이 확장될 경우 새로운 페이지를 자동으로 확보
이번 포스팅에서는 Memory Management 전반부와 Anonymous Page 관리 기능의 구현 과정
을 같이 알아가보자 !
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로 복제하여, 자식 프로세스가 동일한 주소 공간을 가질 수 있도록 준비하는 함수.
src 테이블 순회struct hash_iterator i;
hash_first(&i, &src->spt_hash);
while (hash_next(&i)) {
...
}
src SPT에 있는 모든 struct page를 순회한다
각 페이지 정보를 dst에 복사할 준비
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에 매핑이 시점에서 자식 페이지는 이미 접근 가능한 상태가 됨
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);
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 | 쓰기 가능 여부 |
init | Lazy initializer 함수 포인터 |
aux | initializer에 전달할 부가 정보 |
if (spt_find_page (spt, upage) == NULL) {
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)
uninit_new()로 Uninit 페이지 초기화uninit_new(p, upage, init, type, aux, page_initializer);
p->writable = writable;
내부 상태는 Uninit 상태지만, lazy-load 시 필요한 정보를 다 세팅
init은 지연 로딩할 때 실제 로직을 수행할 함수이다
aux는 파일 오프셋, 길이, 읽기 정보 등 context 정보를 담는다
return spt_insert_page(spt, p);
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가 발생했는지 여부 |
if (addr == NULL || is_kernel_vaddr(addr))
return false;
NULL이거나 커널 주소라면 false를 반환한다.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);
}
페이지가 아예 존재하지 않아서 생긴 페이지 폴트만 처리
해당 가상 주소의 페이지를 SPT에서 찾음
존재하고, 쓰기 권한도 유효하다면 → vm_do_claim_page() 호출하여 프레임 확보 + 매핑 + swap-in
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 부분에 대해서 알아보도록 하자 !