YK
← Posts

[WEEK 11-12] 핀토스 - 가상메모리

2026-05-20

이번 주의 키워드는 Virtual Memory, Page Table, Swap in/out, Page Replacement이다.

이번 과제를 진행하면서 가장 힘들었던 점은 함수들이 아주 잘게 쪼개져 있는데, 각 함수가 정확히 어떤 책임을 지고 있는지 파악하고 구분하는 일이었다. 예를 들어 process_cleanup()을 호출해서 프로세스가 쓰던 공간 자원을 회수할 때 엮여있는 함수들의 흐름은 다음과 같다.

destroy나 kill 같은 비슷한 단어가 여러 함수에 섞여서 쓰이는데, 실제로는 각기 다른 역할을 맡고 있다.

여기서 가장 먼저 살펴본 건 supplemental_page_table_kill()이었다. 이름 그대로 단순하게 spt를 삭제해 주는 역할을 한다.

1void 2supplemental_page_table_kill (struct supplemental_page_table *spt UNUSED) { 3 RETURN_IF (spt == NULL); 4 hash_destroy(&spt->hash_table, spt_page_destroy); 5}

hash_destroy()는 내부적으로 hash_clear()를 부르면서 리스트 안의 모든 hash_elem을 보조 함수에 하나씩 던져준다. 즉, 보조 함수에서는 들어오는 hash_elem 하나(page 하나)에 대한 처리 로직만 짜면 된다. 그래서 spt_page_destroy()를 만들어 spt 안의 page들을 안정적으로 지울 수 있게 했다.

1static void 2spt_page_destroy (struct hash_elem *e, void *aux UNUSED) { 3 struct page *page = hash_entry (e, struct page, hash_elem); 4 vm_dealloc_page(page); 5}

여기서 고민이 좀 많았다. 원래 vm_dealloc_page()의 구조는 이랬다.

1void 2vm_dealloc_page (struct page *page) { 3 destroy (page); 4 free (page); 5}

그런데 page를 dealloc하는 전체적인 흐름을 보면 frame까지 같이 정리해 주는 게 더 자연스럽다. 주석에 수정하지 말라는 경고가 있긴 했지만, 이 함수를 부르는 곳 자체가 많지 않았다. 그리고 그 경고의 진짜 의도는 destroy를 호출한 뒤에 free를 해주는 큰 흐름을 깨지 말라는 뜻으로 보였다. 그래서 함수 자체를 직접 수정해서 흐름을 깔끔하게 맞췄다.

1void 2vm_dealloc_page (struct page *page) { 3 destroy (page); 4 vm_destroy_page_frame(page); 5 free (page); 6}

그리고 frame을 처리하는 로직은 vm_destroy_page_frame()으로 따로 분리했다.

1static void 2vm_destroy_page_frame (struct page *page) { 3 RETURN_IF (page == NULL || page->frame == NULL); 4 5 struct frame *frame = page->frame; 6 7 lock_acquire (&frame_lock); 8 if (clock_ptr == &frame->elem) 9 clock_ptr = list_next(&frame_table); 10 list_remove (&frame->elem); 11 lock_release (&frame_lock); 12 13 if (frame->kva != NULL) 14 palloc_free_page (frame->kva); 15 16 pml4_clear_page(frame->owner_thread->pml4, page->va); 17 frame->page = NULL; 18 frame->owner_thread = NULL; 19 free (frame); 20 21}

vm_get_victim() 안에서 clock 알고리즘을 쓰고 있기 때문에, 만약 지우려는 frame이 현재 clock ptr이 가리키는 대상이라면 포인터를 옆으로 한 칸 밀어주도록 했다. spt에서 지워지는 frame을 frame table에서도 꼬이지 않게 일관되게 지워주려면 꼭 필요한 처리다.

Pintos는 union을 써서 객체 지향을 흉내 내는데, 나도 supplemental_page_table_copy()를 구현할 때 copy 함수를 객체 지향 스타일로 빼서 진행했다. 결국 각 page 종류마다 copy할 때 해줘야 하는 행동이 다 다르니까, 각 객체가 자기만의 copy 함수를 들고 있는 게 자연스럽고 코드도 훨씬 깔끔해질 거라 생각해서 다음과 같이 진행했다.

1bool 2supplemental_page_table_copy (struct supplemental_page_table *dst UNUSED, 3 struct supplemental_page_table *src UNUSED) { 4 5 struct hash_iterator i; 6 hash_first (&i, &src->hash_table); 7 while (hash_next (&i)) 8 { 9 struct page *src_page = hash_entry (hash_cur (&i), struct page, hash_elem); 10 RETURN_FALSE_IF(!copy(dst, src_page)); 11 } 12 return true; 13}

운영체제 코드를 짜다 보면 예외 처리를 해줘야 할 일이 정말 많아서, 다음과 같은 매크로 함수들도 만들어 썼다.

1#define RETURN_IF(CONDITION) \ 2 do { if (CONDITION) return; } while (0) 3#define RETURN_VALUE_IF(CONDITION, VALUE) \ 4 do { if (CONDITION) return (VALUE); } while (0) 5#define RETURN_NULL_IF(CONDITION) \ 6 do { if (CONDITION) return NULL; } while (0) 7#define RETURN_FALSE_IF(CONDITION) \ 8 do { if (CONDITION) return false; } while (0)

그래서 각 page 종류별로 알맞은 copy 함수를 등록해 주는 방식으로 구조를 잡았다.

1static const struct page_operations uninit_ops = { 2 .swap_in = uninit_initialize, 3 .swap_out = NULL, 4 .destroy = uninit_destroy, 5 .copy = uninit_copy, 6 .type = VM_UNINIT, 7};

이렇게 함수들을 등록해 놓고, page 종류에 맞춰 세부 로직을 짰다.

1static bool 2uninit_copy (struct supplemental_page_table *dst, struct page *src_page){ 3 RETURN_FALSE_IF (src_page == NULL); 4 void *aux = src_page->uninit.aux; 5 if (aux) 6 { 7 struct lazy_load_arg *temp_aux 8 = (struct lazy_load_arg *)malloc (sizeof (struct lazy_load_arg)); 9 memcpy (temp_aux, src_page->uninit.aux, sizeof (struct lazy_load_arg)); 10 if (temp_aux->file) 11 { 12 temp_aux->file = file_reopen (temp_aux->file); 13 if (temp_aux->file == NULL) 14 { 15 free (temp_aux); 16 return false; 17 } 18 } 19 aux = temp_aux; 20 } 21 22 if (!vm_alloc_page_with_initializer ( 23 src_page->uninit.type, src_page->va, src_page->writable, 24 src_page->uninit.init, aux)) 25 return false; 26 return true; 27}
1static bool 2anon_copy (struct supplemental_page_table *dst, struct page *src_page){ 3 RETURN_FALSE_IF (src_page == NULL); 4 5 RETURN_FALSE_IF(!vm_alloc_page (page_get_type(src_page), src_page->va, src_page->writable)); 6 RETURN_FALSE_IF(!vm_claim_page (src_page->va)); 7 8 struct page *dst_page = spt_find_page (dst, src_page->va); 9 if (src_page->anon.swapped){ 10 lock_acquire(&swap_lock); 11 for (int i = 0; i < SWAP_SLOT; i++){ 12 disk_read(swap_disk, src_page->anon.swap_idx * 8 + i, (uint8_t *)dst_page->frame->kva + i * 512); 13 } 14 lock_release(&swap_lock); 15 }else{ 16 memcpy(dst_page->frame->kva, src_page->frame->kva, PGSIZE); 17 } 18 return true; 19}
1static bool 2file_copy (struct supplemental_page_table *dst, struct page *src_page){ 3 RETURN_FALSE_IF (src_page == NULL); 4 5 RETURN_FALSE_IF(!vm_alloc_page (page_get_type(src_page), src_page->va, src_page->writable)); 6 RETURN_FALSE_IF(!vm_claim_page (src_page->va)); 7 8 struct page *dst_page = spt_find_page (dst, src_page->va); 9 if (src_page->file.swapped){ 10 RETURN_FALSE_IF (file_read_at(src_page->file.file, src_page->frame->kva, src_page->file.page_read_bytes, src_page->file.ofs) != src_page->file.page_read_bytes); 11 }else{ 12 memcpy(dst_page->frame->kva, src_page->frame->kva, PGSIZE); 13 } 14 return true; 15}

코드를 짜면서 또 하나 크게 느낀 건, 함수로 넘어오는 매개변수가 정확히 뭘 뜻하는지, 그리고 어떤 값까지 들어올 수 있는지 유효 범위를 확실히 파악해야 한다는 점이었다. 이건 mmap을 구현할 때 가장 뼈저리게 느꼈는데, do_mmap의 매개변수 중 length가 뭘 의미하는지 착각하는 바람에 테스트 통과가 안 돼서 애를 먹었었다.

1void * 2do_mmap (void *addr, size_t length, int writable, 3 struct file *file, off_t offset) { 4 void *start_addr = addr; 5 struct file *reopened_file = file_reopen(file); 6 off_t file_size = file_length(reopened_file); 7 8 size_t f_read_bytes = length < file_size - offset ? length : file_size - offset; 9 while (f_read_bytes > 0){ 10 struct lazy_load_file_arg* aux = malloc(sizeof(struct lazy_load_file_arg)); 11 RETURN_VALUE_IF (aux == NULL, false); 12 13 size_t page_read_bytes = f_read_bytes < PGSIZE ? f_read_bytes : PGSIZE; 14 size_t page_zero_bytes = PGSIZE - page_read_bytes; 15 16 aux->file = reopened_file; 17 if (aux->file == NULL) { 18 free (aux); 19 return false; 20 } 21 aux->ofs = offset; 22 aux->page_read_bytes = page_read_bytes; 23 aux->page_zero_bytes = page_zero_bytes; 24 25 if (!vm_alloc_page_with_initializer (VM_FILE, addr, writable, 26 lazy_load_file, aux)) { 27 free(aux); 28 return false; 29 } 30 /* 31 * 다음 페이지로 진행한다. 32 */ 33 addr += PGSIZE; 34 offset += page_read_bytes; 35 f_read_bytes -= page_read_bytes; 36 } 37 38 return start_addr; 39}

결국 length는 사용자가 읽고 싶어 하는 길이를 뜻한다. 그래서 이걸 실제 filesize와 비교해서 제한해 주고, 남는 페이지 공간은 0으로 덮어버리는 것이 핵심이었다.