파일 기반 페이지란?
파일 기반 페이지(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 포인터를 순환시켜 다음 호출 때는 이전 위치부터 탐색하여 공정성을 보장합니다.
'PintOS > Project 3 : VIRTUAL MEMORY' 카테고리의 다른 글
| [PintOS] Project 3 : VM - Swap In/out(anon) (2) | 2025.06.11 |
|---|---|
| [PintOS] Project 3 : VM - Memory Mapped Files - File-Backed 페이지 구성(with lazy loading) (0) | 2025.06.11 |
| [PintOS] Project 3 : VM - Memory Mapped Files - mmap & munmap 구현(with lazy loading) (0) | 2025.06.11 |
| [PintOS] Project 3 : VM - Stack Growth (7) | 2025.06.09 |
| [PintOS] Project 3 : VM - Anonymous Page (1) | 2025.06.07 |