이미 테스트는 통과한지 오래이지만... 도저히 포스팅할 엄두가 나지 않아서 이제야 작성한다.
vm 프로젝트의 extra 과제인데, 기존에 작성한 코드들을 많이 수정해야해서 애를 먹었다. 역시 코드를 작성하는것보다 수정하는 일이 훨씬 어려운 것 같다.
copy-on-write가 무엇인가?
우리가 이전에 작성한 spt-copy는 카피해온 페이지들을 즉시 초기화시켜주고, 데이터를 복사하는 등 자식과 부모가 서로 다른 프레임을 참조하도록 만들었다. 하지만 잘 생각해보면 read만 할 거면 굳이 같은 데이터를 가진 다른 프레임을 서로 참조할 필요가 있을까? 그냥 같은 프레임을 부모와 자식이 같이 참조하도록 해도 될 것 같다. 그러다가 write가 발생해서 일관성이 깨질 것 같으면, 그제서야 다른 프레임 하나를 할당해주고 내용을 복사하여 그 프레임에 write 해주도록 하면 되잖아??

이를 그림으로 나타내면 위와 같이 만들어질 것이다. fork가 일어나서 서로 같은 객체를 가리키고, 처음에 이 객체는 같은 물리 프레임 내에 존재할 것이다. 하지만 write가 발생하면 그 페이지만 따로 복사한 이후 쓰기 작업이 일어나게 된다. 이러한 방식으로 오버헤드를 크게 줄일 수 있고, 물리 주소 공간도 절약할 수 있다.
구현 드가자 ~
먼저 과제 설명서를 보자.

우리가 구현해야 될 부분을 정리해보면
- 복사할 때 그 페이지의 쓰기 권한을 제거하여 페이지 폴트가 의도적으로 일어나도록 해야함
- 페이지 폴트를 처리할 때 copy-on-write를 위한 페이지 폴트인지 확인 가능해야함
- cow 페이지 폴트일 때 프레임을 복사한다.
그리고 우리가 구현 시에 생각해야할 예외 상황은
- 프레임을 공유 중일 때 자식이 죽는 경우
- 공유 중인 프레임이 스왑 아웃될 때
- 부모가 먼저 write를 해버리는 경우
하지만 cow-simple 테스트 케이스에서 2번이나 3번 같은 경우는 발생하지 않는다. 물론 두가지를 고려해서 구현하는 것이 정석이겠지만, 복잡도가 너무 증가하므로 나는 두 경우를 제외하고 구현하였다.
본격적인 구현에 앞서, frame 구조체에 필드 하나를 추가해주자. 이 프레임을 참조하는 페이지가 몇개인지 나타내어주는 필드가 필요하다. 이 참조 카운트가 0이 될 때만 이 프레임을 진정으로 삭제해줘야 한다.
struct frame
{
...
/* extra-cow */
int ref_cnt;
...
}
더해서 이제 프레임을 생성할 때 이 필드도 초기화해주어야 한다.
static struct frame *
vm_get_frame(void)
{
...
frame->ref_cnt = 1;
...
}
이제 어디를 수정해야 할까? 기존에는 spt_copy에서 복사 이후에 바로 vm_claim_page를 호출하여 페이지를 초기화시켜줬다. 이제는 같은 프레임을 참조해야하므로 별도의 로직이 필요할 것이다. 그러면 spt_copy 시에 vm_claim_page 대신 사용할 함수를 하나 만들어주자
vm_copy_claim_page
bool vm_copy_claim_page(void *va, struct page *parent, struct supplemental_page_table *parent_spt)
{
struct page *page = spt_find_page(&thread_current()->spt, va);
if (page == NULL)
return false;
void *temp = page->operations->swap_in;
struct frame *frame = parent->frame;
frame->ref_cnt++;
if (frame->ref_cnt == 1)
return true;
/* Set links */
page->frame = frame;
if (!pml4_set_page(thread_current()->pml4, page->va, frame->kva, false))
return false;
return swap_in(page, frame->kva);
}
사실 로직 자체는 vm_claim_page와 vm_do_claim_page를 합친 것에 불과하다. 하나 다른 점은 vm_get_frame을 호출하여 프레임을 할당해주는 대신, 부모의 프레임을 가져와 할당해주고, 프레임의 참조 횟수를 1 증가시켜준다는 것이다.
그리고 pml4_set_page를 통해 PTE에 이 페이지에 write를 하려고 하면 페이지 폴트가 일어나도록 PTE_W를 false로 만들어주자. 위에서 보았던 1번 구현은 완성했다.
다음은 2번 구현, write 권한 페이지 폴트를 처리할 때 이 폴트가 cow인지 확인해야 한다.
bool vm_try_handle_fault(struct intr_frame *f UNUSED, void *addr UNUSED,
bool user UNUSED, bool write UNUSED, bool not_present UNUSED)
{
...
if (write == true && page->writable && page->frame != NULL)
return vm_handle_wp(page);
...
여기서 인자로 받는 write는 이 addr에 write 작업이 발생했는지 확인하는 인자고, page->writable은 이 페이지에 쓰기가 가능한지를 확인하는 인자다. 예를 들어 code나 text 세그먼트는 당연히 page->writabel이 false이다.
따라서 쓰기를 하려고 했는데, page의 writable이 true 였다면, 이 PTE의 W비트가 false라는 것을 의미하고, 이는 일부러 이 PTE의 쓰기 비트를 false로 설정해두었다는 것을 의미한다. 거기에 더해서 이 page의 frame이 참조되어있다면 cow 폴트라는 것을 인지하고 다음 로직을 호출한다.
이제 3번을 구현할 때다.
vm_handle_wp
static bool
vm_handle_wp(struct page *page UNUSED)
{
if (page == NULL)
{
return false;
}
struct frame *copy_frame = page->frame;
if (copy_frame->ref_cnt > 1)
{
struct frame *frame = vm_get_frame();
page->frame = frame;
memcpy(frame->kva, copy_frame->kva, PGSIZE);
copy_frame->ref_cnt--;
if (!pml4_set_page(thread_current()->pml4, page->va, frame->kva, true))
return false;
}
else
{
copy_frame->page = page;
if (!pml4_set_page(thread_current()->pml4, page->va, copy_frame->kva, true))
return false;
}
return true;
}
먼저 인자로 받은 페이지의 프레임의 참조 카운트를 확인한다. 1보다 크다면 같은 프레임을 공유 중이라는 뜻이므로, 다른 프레임을 할당받고 그 주소에다가 공유하던 프레임의 내용을 복사해주고, 참조 카운트를 1 감소시킨다. 그 다음 cow 폴트가 발생하지 않도록 pml4_set_page로 write 권한을 true로 만들어주자.
1보다 크지 않다면, 이 프레임을 참조하는건 자신 뿐이므로 복사할 필요 없이 프레임의 페이지를 자신으로 매핑해주고 cow 폴트가 발생하지 않도록 write 권한을 true로 만들어주면 된다.
이제 구현은 모두 완성했다. 하지만 이대로 만들면 fork 관련 테스트에서 펑펑 터지는 현상을 볼 수 있을 것이다.

예외 상황이 3가지 있었는데, 테스트 케이스에서 2,3번은 고려하지 않는 것 같았다. 그럼 1번을 보자. 공유 프레임을 사용 중일 때 자식 프로세스가 죽으면서 process_cleanup을 호출하여 자원을 해제하는 상황이 있다. 그러면 자신이 참조하던 공유 프레임을 palloc_free_page로 해제해버리는 대 참사가 난다. 따라서 이 공유 프레임을 참조하는 페이지가 없을 때만 free해주는 검사가 필요하다.
먼저 process_cleanup에는 이 프로세스의 pml4를 순회하며 매핑되어있는 물리 프레임을 모두 해제해주는 로직이 있다. 그 말은 pml4_clear_page를 해 주지 않으면 pml4_destroy를 통해 다 해제가 된다는 소리다. 따라서 각 페이지의 destroy 함수에서 pml4_clear_page를 통해 pml4에서의 공유 프레임과의 매핑을 끊어줘야 한다. 하지만 매핑을 무작정 다 끊어주면 이 프레임은 해제가 되지 않아 메모리 누수가 발생하게 된다. 따라서 참조 카운트를 확인하고, 0이라면 직접 palloc_free_page를 해줘야 한다.
먼저 vm_delloc_page에 코드 한줄 추가해주자. 이 함수는 건드리지 말라고 했지만... 귀찮으니까 걍 수정하자. 뭘 어렵게 살아 ~
void vm_dealloc_page(struct page *page)
{
if (page->frame)
page->frame->ref_cnt--;
destroy(page);
free(page);
}
이 함수를 호출했다는 뜻이 page 자체를 없애준다는 뜻이니까 이 페이지에 매핑된 프레임의 참조 카운트를 1 감소시켜주면 된다.
그리고 이 함수를 통해 호출되는 함수가 두가지 있다. anon_destroy와 file_backed_destroy
file_backed_destroy / anon_destroy
static void
file_backed_destroy(struct page *page)
{
...
// 해당 페이지가 물리 프레임에 매핑되어 있으면
if (page->frame != NULL && page->frame->ref_cnt < 1)
{
// 물리 페이지를 해제하고, frame 구조체도 동적 메모리 해제
palloc_free_page(page->frame->kva);
free(page->frame);
page->frame = NULL;
}
pml4_clear_page(curr->pml4, page->va);
}
static void
anon_destroy(struct page *page)
{
...
// 현재 스레드의 pml4에서 이 페이지에 대한 매핑을 제거 (VA -> PA 연결 해제)
pml4_clear_page(thread_current()->pml4, page->va);
// 스왑 아웃된 적이 없거나, 이미 스왑에서 복구되어 유효하지 않은 스왑 슬롯이면 아무 작업도 하지 않음
// swap_idx < 0이면 해당 페이지는 스왑 슬롯을 점유하고 있지 않음
if (anon_page->swap_idx < 0)
{
return; // 추가 작업 없이 종료
}
// 해당 페이지가 물리 프레임에 매핑되어 있으면
if (page->frame != NULL && page->frame->ref_cnt < 1)
{
// 물리 페이지를 해제하고, frame 구조체도 동적 메모리 해제
palloc_free_page(page->frame->kva);
free(page->frame);
page->frame = NULL;
}
...
}
이렇게 참조 카운트를 검사하고, 1보다 작다면 (delloc에서 1을 감소시켰으니까) palloc_free_page를 호출해주자!!
여기까지 완성하면?

이렇게 모두 통과했다는 메세지를 볼수 있다.

pintos 재밌다 헤헤
'크래프톤 정글' 카테고리의 다른 글
| [pintOS] FAT 구현기 (0) | 2025.06.10 |
|---|---|
| [pintOS] 파일 시스템 찍먹하기 (4) | 2025.06.08 |
| [pintOS] swap-in/out 구현기 (5) | 2025.06.05 |
| [pintOS] mmap 구현기 (0) | 2025.06.05 |
| [pintOS] vm 트러블슈팅 (1) | 2025.06.04 |