PintOS/Project 3 : VIRTUAL MEMORY

[PintOS] Project 3 : VM - Swap In/out(File-Mapped Page)

넌뭐가그렇게중요해 2025. 6. 11. 21:47

파일 기반 페이지란?

파일 기반 페이지(File-backed Page)는 mmap() 등을 통해 파일을 메모리에 직접 매핑한 페이지입니다. 일반적인 anonymous 페이지와 달리, 이 페이지들의 백업 저장소는 파일입니다.
즉, 페이지를 퇴출(evict)할 때 swap 디스크로 보내는 것이 아니라 파일에 다시 쓰기(write back) 해야 합니다.


Swap In / Swap Out 개요

함수 설명
file_backed_swap_in() 파일에서 내용을 읽어와 페이지를 메모리로 다시 불러오는 함수
file_backed_swap_out() 페이지의 내용을 다시 파일에 기록하는 함수(dirty 상태일 때만)

전체 Control Flow

메모리 부족
   ↓
vm_get_frame() 호출 → palloc 실패
   ↓
vm_evict_frame() → victim 선정
   ↓
victim이 file-mapped → file_backed_swap_out() 호출
   ↓
victim 프레임 회수하여 재사용

그리고 나중에 해당 페이지가 다시 접근되면,

page fault
   ↓
page fault handler → swap-in 필요
   ↓
file_backed_swap_in() 호출 → 파일에서 데이터 복원

1. file_backed_swap_in() - 파일 기반 페이지를 메모리에 로드하는 함수

//.../vm/file.c
/* 파일에서 내용을 읽어와 페이지를 스왑인합니다. */
static bool
file_backed_swap_in(struct page *page, void *kva)
{
	// file_page는 file-backed 페이지에 대한 메타데이터를 담고 있는 구조체
	struct file_page *file_page UNUSED = &page->file;
	// swap_in을 위한 버퍼
	// void *buffer[PGSIZE];
	/** TODO: 파일에서 정보를 읽어와 kva에 복사하세요
	 * aux에 저장된 백업 정보를 사용하세요
	 * file_open과 read를 사용하면 될 것 같아요
	 * 파일 시스템 동기화가 필요할수도 있어요
	 * 필요시 file_backed_initializer를 수정하세요
	 */
	// mmap 시 등록된 파일 객체 포인터
	struct file *file = file_page->file;
	// mmap 시 설정된 파일 내 offset(해당 페이지가 파일의 어디서부터 읽어야 하는지 나타냄)
	off_t offset = file_page->offset;
	// 실제로 파일에서 읽어야 할 바이트 수
	size_t read_byte = file_page->read_byte;

	// 파일에서 데이터를 읽어와 kva(페이지가 매핑된 커널 가상 주소)에 저장
	if (file_read_at(file, kva, read_byte, offset) != (off_t)read_byte)
	{
		// 읽은 바이트 수가 기대치와 다르면 오류 처리
		return false;
	}

	// 파일에서 읽어오지 못한 나머지 페이지 영역을 0으로 초기화
	memset(kva + read_byte, 0, page->file.zero_byte);

	return true;
}

1.1. file_backed_swap_in() 핵심 요약

  • file_page 구조체에는 mmap 된 파일 포인터와 파일 내 오프셋, 읽을 바이트 수 정보가 저장되어 있습니다.
  • file_read_at() 함수를 통해 해당 위치에서 데이터를 읽고 페이지 프레임(kva)에 복사합니다.
  • 읽어온 바이트 수 이후의 남은 공간은 memset()으로 0으로 채워줍니다.

1.2. file_backed_swap_in()을 이렇게 구현한 이유

  • mmap 된 파일 기반 페이지는 파일에 있는 내용을 페이지에 직접 매핑하기 때문에 file_read_at()으로 읽어오는 방식이 적절합니다.
  • 읽어온 바이트보다 페이지 크기가 클 수 있으므로 남은 부분은 0으로 명시적으로 초기화해야 예측 가능한 메모리 동작을 보장할 수 있습니다.
  • 스왑 인은 유저 프로세스 실행 시 필요 페이지가 메모리에 없는 경우 발생하므로 반드시 실패 없이 로딩되어야 합니다.

2. file_backed_swap_out() - 파일 기반 페이지를 디스크로 저장하는 함수

/* 페이지의 내용을 파일에 기록(writeback)하여 스왑아웃합니다. */
static bool
file_backed_swap_out(struct page *page)
{
	// file_page는 file-backed 페이지에 대한 메타데이터를 담고 있는 구조체
	struct file_page *file_page UNUSED = &page->file;
	/** TODO: dirty bit 확인해서 write back
	 * pml4_is_dirty를 사용해서 dirty bit를 확인하세요
	 * write back을 할 때는 aux에 저장된 파일 정보를 사용
	 * file_write를 사용하면 될 것 같아요
	 * dirty_bit 초기화 (pml4_set_dirty)
	 */
	struct thread *curr = thread_current();
	bool dirty_bit = pml4_is_dirty(curr->pml4, page->va);

	// dirty bit가 true이면, 즉 메모리에서 수정된 경우
	if (dirty_bit == true)
	{
		// 공유 자원 접근 → 락 걸고 접근
		lock_acquire(&filesys_lock);
		if (file_write_at(file_page->file,		// mmap된 파일 객체
						  page->frame->kva,		// 페이지의 실제 물리 주소
						  file_page->read_byte, // 실제로 파일에 기록할 바이트 수
						  file_page->offset)	// 파일 내 시작 위치
			!= (off_t)file_page->read_byte)
		{
			// write 실패하면 lock 해제 해야겠지?
			lock_release(&filesys_lock);
			return false;
		}
		// 파일 쓰기 완료 후 락 해제
		lock_release(&filesys_lock);

		// 더티 비트 클리어(쓰기 완!)
		pml4_set_dirty(curr->pml4, page->va, false);
	}
	// 초기화는 victim에서
	page->frame->page = NULL;
	page->frame = NULL;

	return true;
}

2.1. file_backed_swap_out() 핵심 요약

  • pml4_is_dirty()로 해당 페이지가 수정되었는지(dirty) 확인합니다.
  • dirty 상태인 경우, 파일에 변경된 내용을 file_write_at()으로 기록합니다.
  • 쓰기 후에는 pml4_set_dirty()로 dirty 비트를 초기화합니다.
  • 페이지가 프레임과의 연결을 끊으며 완전히 스왑아웃됩니다.

2.2. file_backed_swap_out()을 이렇게 구현한 이유

  • 파일 기반 페이지는 원본 파일의 내용을 참조하므로 수정된 경우에만 writeback이 필요합니다. 따라서 dirty check는 필수입니다.
  • 파일 시스템은 공유 자원이므로 동시 접근을 막기 위해 filesys_lock을 반드시 획득하고 해제해야 합니다.
  • pml4_set_dirty()를 통해 dirty 상태를 클리어해야 동일 페이지가 다시 swap in 될 때 불필요한 writeback을 방지할 수 있습니다.
  • page->frame을 NULL로 지정함으로써 프레임과 연결을 끊고, 물리 메모리 반환 준비를 마칩니다.

3. vm_get_frame() - 사용자 프로세스를 위한 프레임 할당 함수

이걸 다시 쓴 이유도 앞에서는 그냥 대충 설명했지만 지금 get_evict_frame으로 희생자 프레임을 가져온다.

//.../vm/vm.c
/* palloc()을 사용하여 프레임을 할당합니다.
 * 사용 가능한 페이지가 없으면 페이지를 교체(evict)하여 반환합니다.
 * 이 함수는 항상 유효한 주소를 반환합니다. 즉, 사용자 풀 메모리가 가득 차면,
 * 이 함수는 프레임을 교체하여 사용 가능한 메모리 공간을 확보합니다.*/
static struct frame *
vm_get_frame(void)
{ 
   /* 1. 새로운 frame 구조체를 메모리에 할당
      이는 물리 페이지의 메타데이터를 저장할 공간
      반드시 free해라 뒤지기싫으면.. 
   */
   struct frame *frame = malloc(sizeof(struct frame));
   // 예외 처리 → 할당 실패시 시스템 중단
   ASSERT(frame != NULL);  

   /* 2. 실제 물리 페이지(4KB) 할당 시도 
      PAL_USER : 사용자 프로세스용 메모리 풀에서 할당
      PAL_ZERO : 할당된 페이지를 0으로 초기화
   */
   frame->kva = palloc_get_page(PAL_USER | PAL_ZERO);

   /* 3. 메모리 부족 상황 처리
      사용 가능한 물리 페이지가 없는 경우 처리
   */
   if (frame->kva == NULL)
   {  
      /* 3.1. 희생자(victim) 프레인 선택 및 교체
         vm_evict_frame()은 페이지 교체 알고리즘(처음: FIFO, 지금: Clock)을 사용하여
         교체할 프레임을 선택, 해당 페이지를 디스코로 내보냅니다.
      */
      struct frame *victim1 = vm_evict_frame();

      // 예외 처리 → 교체 실패시 시스템 중단
      ASSERT(victim1 != NULL);

      /* 3.2. 희생자의 물리 페이지를 재활용
         교체된 프레임의 물리 주소를 새로운 프레임이 사용
      */
      frame->kva = victim1->kva; // victim의 물리 페이지를 재활용

      /* 3.3. 희생자 프레임 구조체 해제
         물리 페이지는 재활용하지만, 메타데이터는 새로 만듦
      */
      free(victim1);
   }

   /* 4. 새로운 프레임 초기화
      아직 어떤 가상 페이지와도 연결되지 않은 상태
   */
   frame->page = NULL;                 // 연결된 가상 페이지 X
   frame->ref_cnt = 1;                 // 참조 카운터 초기화(COW extra 과제 용)
   
   /* 5. 프레임 테이블에 등록
      시스템이 이 프레임을 추적할 수 있도록 전역 프레임 테이블에 추가
   */
   frame_table_insert(&frame->elem);   

   /* 예외 처리 → 프레임이 올바르게 초기화 되었나? */
   ASSERT(frame->page == NULL);

   /* 6. 할당된 프레임 반환
      이제 이 프레임은 가상 페이지와 연결된 준비가 완료되었습니다.
   */
   return frame;
}

3.1. vm_get_frame() 핵심 요약

  • vm_get_frame()은 사용자 메모리 풀에서 새로운 프레임(물리 페이지)을 할당하는 함수입니다.
  • 만약 할당 가능한 물리 페이지가 없다면, 페이지 교체 알고리즘(예: Clock)을 이용해 희생자 프레임을 선택(evict)하고 해당 페이지를 재활용합니다.
  • 이 함수는 항상 유효한 물리 주소(kva)를 반환해야 하므로, 교체 실패 시에도 예외 없이 처리되도록 설계되어 있습니다.
  • 새로 할당한 프레임은 아직 어떤 가상 페이지와도 연결되지 않았으며, 프레임 테이블에 등록하여 관리됩니다.
  • Copy-on-Write(COW) 구현을 위해 참조 카운터(ref_cnt)를 1로 초기화합니다.
  • 핵심 역할: 사용자 공간의 물리 페이지 확보 + 부족 시 교체 로직 실행 + 프레임 메타데이터 관리

3.2. vm_get_frame() 이렇게 구현한 이유

 

  • 물리 메모리 부족 대응
    palloc_get_page()가 실패하면 바로 vm_evict_frame()을 통해 페이지 교체 알고리즘 (예: Clock)을 수행하여 victim을 제거하고 공간을 확보합니다. 이를 통해 시스템이 항상 프레임을 확보할 수 있습니다.
  • frame 구조체는 재사용하지 않고 새로 생성
    victim의 struct frame은 free 하고 새 frame을 생성함으로써, victim에 대한 모든 메타데이터를 새롭게 초기화할 수 있어 일관성을 유지할 수 있습니다.
  • 전역 프레임 테이블 관리
    frame_table_insert()를 통해 새로운 프레임을 시스템 전역 테이블에 등록하면, 나중에 eviction 시 추적이 쉬워지고, 전체 메모리 상태를 관리하기 수월해집니다.
  • COW(Write-On-Copy)를 고려한 참조 카운터 초기화
    frame->ref_cnt = 1은 Copy-on-Write 구현을 위해 필요합니다. 이후 여러 프로세스가 공유하는 페이지를 구현할 때 핵심이 됩니다.

4. vm_evict_frame() - 페이지 교체 알고리즘을 통해 프레임 확보

//.../vm/vm.c
/* 한 페이지를 교체(evict)하고 해당 프레임을 반환합니다.
 * 에러가 발생하면 NULL을 반환합니다.*/
static struct frame *
vm_evict_frame(void)
{  
   /* 희생(victim) 프레임을 선택 */
   struct frame *victim = vm_get_victim();
   
   // 예외 처리 : victim이 없을 경우 
   if (victim == NULL)
      return NULL;
   /* TODO: swap out the victim and return the evicted frame. */

   // victim 프레임이 매핑하고 있는 페이지 정보를 얻어옴
   struct page *victim_page = victim->page;

   // 예외 처리 : victim 프레임이 매핑하고 있는 페이지가 NULL이면 잘못된 상태
   if (victim_page == NULL)
      return NULL;
   /** TODO: 여기서 swap_out 매크로를 호출??
    *   pml4_clear_page를 아마 사용?? (잘 모름)
    */

   // 선택된 victim 페이지를 스왑 디스트로 보냄
   if (!swap_out(victim_page))
      return NULL;

   /* 현재 프로세스의 pml4에서 해당 페이지의 매핑을 제거함
      즉, 사용자 가상 주소(victim_page -> va)가 더 이상 물리 메모리를 가르키지 않도록 함 
   */
   pml4_clear_page(thread_current()->pml4, victim_page->va);
   
   // 프레임 리스트에서 victim 프레임 제거 → 다시 frame_get_page() 등에서 재할당될 수 있음
   list_remove(&victim->elem);

   // 스왑 아웃 및 클리어 작업 이 끝난 프레임 반환
   return victim;
}

 

4.1. vm_evict_frame() 핵심 요약

  • vm_get_victim()으로 교체 대상 프레임을 선택합니다.
  • victim 프레임이 매핑한 페이지를 찾아 swap_out()으로 스왑 디스크에 저장합니다.
  • PML4에서 해당 가상 주소와 물리 주소의 매핑을 제거합니다 (pml4_clear_page()).
  • 프레임 테이블에서 victim 프레임을 제거합니다 (list_remove()).
  • 스왑 아웃이 완료된 프레임을 반환합니다.

4.2. vm_evict_frame()을 이렇게 구현한 이유

  • 사용자 메모리 풀에 빈 공간이 없을 경우, 기존 페이지를 내보내고 그 프레임을 재사용해야 합니다.
  • victim 페이지는 반드시 page!= NULL이어야 정상적인 상태이며, 그렇지 않으면 잘못된 매핑이므로 예외 처리합니다.
  • 스왑 아웃 후 pml4_clear_page()를 호출해야 사용자 가상 주소가 더 이상 해당 물리 메모리를 가리키지 않도록 하여 페이지 폴트를 유도할 수 있습니다.
  • 프레임 테이블에서 제거함으로써 시스템이 더 이상 해당 프레임을 추적하지 않도록 합니다.

5. vm_get_victim() - Clock 알고리즘을 사용한 페이지 교체 대상 선택 함수

//.../vm/vm.c
static struct frame *vm_get_victim(void)
{
   struct list_elem *clock_now;
   struct frame *victim;

   ASSERT(victim != NULL);
   if(clock_start == NULL || clock_start == list_end(&frame_table))
      clock_start = list_begin(&frame_table);

   clock_now = clock_start;
   do{
      victim = list_entry(clock_now,struct frame, elem);

      bool success = pml4_is_accessed(thread_current()->pml4,victim->page->va);
      if(success == false){
         clock_start = list_next(clock_now);
         return victim;
      }


      pml4_set_accessed(thread_current()->pml4,victim->page->va, false);
      clock_now = list_next(clock_now);
      if(clock_now == list_end(&frame_table))
         clock_now = list_begin(&frame_table);
   } while(clock_now != clock_start);

   victim = list_entry(clock_now,struct frame, elem);
   clock_start = list_next(clock_now);

   return victim;
}

5.1. vm_get_victim() 핵심 요약

  • clock_start가 NULL이거나 리스트 끝에 도달했으면 리스트 시작점으로 초기화합니다.
  • 현재 clock_start부터 리스트를 순회하며 다음을 반복합니다:
    • 현재 프레임의 페이지가 접근(accessed)된 적이 있는지 pml4_is_accessed()로 확인합니다.
    • 접근 비트가 false이면 해당 프레임을 희생자로 선택해 반환합니다.
    • 접근 비트가 true이면 접근 비트를 false로 초기화(pml4_set_accessed())하고 다음 프레임으로 이동합니다.
  • 한 바퀴를 돌았는데 접근 비트가 모두 true였다면, 현재 프레임을 희생자로 선택해 반환합니다.
  • 반환 직전, clock_start는 다음 프레임 위치로 갱신합니다.

5.2. vm_get_victim()을 이렇게 구현한 이유

  • Clock 알고리즘은 페이지의 최근 접근 여부(access bit)를 기준으로 페이지 교체 대상을 효율적으로 결정합니다.
  • 접근 비트가 설정된 페이지는 아직 사용 중일 가능성이 높으므로 접근 비트를 초기화하고 넘어가 다음 프레임을 검사합니다.
  • 한 바퀴 순회 후에도 접근 비트가 모두 true인 경우, 가장 최근에 검사된 프레임을 희생자로 선택합니다.
  • clock_start 포인터를 순환시켜 다음 호출 때는 이전 위치부터 탐색하여 공정성을 보장합니다.