운영체제는 제한된 물리 메모리로 많은 프로세스를 동시에 실행해야 한다.
이때 메모리가 부족하면, 사용하지 않는 페이지를 디스크에 임시로 저장하고 필요할 때 다시 불러오는 방식이 사용되며,
이를 Swap이라 부른다.
PintOS의 가상 메모리 시스템에서도 Swap 기능은 중요한 구성 요소이다.
이 글에서는 PintOS에서 Swap 기능을 직접 구현한 과정을 설계 → 구현 흐름 → 주요 함수 설명 순서로 정리해보겠다.
swap_disk: 디스크 핸들러
swap_slot_list: 사용 가능한 swap 슬롯들 (프레임 하나에 해당하는 디스크 섹터 묶음)
swap_slot.page_list: 하나의 프레임을 공유하는 페이지들
vm_get_frame()에서 프레임이 부족하면 vm_evict_frame() → swap_out() 호출
스왑 영역이 꽉 차면 어떻게 해야 할까? (일단 PintOS 과제에선 무제한으로 가정해도 무방)
- 동기화:
swap_lock을 사용한 동시 접근 제어
anon.cvoid vm_anon_init(void)
{
// (1) 스왑 디스크 설정 (디스크 컨트롤러 1번, 디스크 1번)
swap_disk = disk_get(1, 1); // pintos에서는 하드코딩된 디스크 번호 사용
// (2) swap slot 리스트 및 락 초기화
list_init(&swap_slot_list); // 사용 가능한 슬롯을 리스트 형태로 관리
lock_init(&swap_lock); // swap-in/out 중 동기화 필요
// (3) 디스크 전체를 SLOT_SIZE(=PGSIZE / DISK_SECTOR_SIZE) 단위로 분할
for (int i = 0; i < disk_size(swap_disk); i += SLOT_SIZE)
{
// 새 swap_slot 구조체 할당 및 초기화
struct swap_slot *slot = malloc(sizeof(struct swap_slot));
list_init(&slot->page_list); // 해당 슬롯에 연결된 페이지들 목록
slot->start_sector = i; // 이 슬롯의 시작 섹터 번호
// 전역 swap_slot_list에 추가
list_push_back(&swap_slot_list, &slot->slot_elem);
}
// (4) 모든 sector를 0으로 초기화할 수 있는 zero buffer 준비
memset(zero_set, 0, PGSIZE); // disk_write 시 zero-fill에 사용
}
/* 스왑 디스크에서 데이터를 읽어와 메모리로 복구 (anonymous 페이지 swap-in)
*
* [역할]
* - 스왑 슬롯에 저장된 데이터를 하나의 프레임으로 복구
* - 이 프레임을 공유하던 모든 페이지에 대해 pml4 매핑 복원
* - 스왑 슬롯은 복구 이후 재사용 가능하도록 반환
*
* @param page : 복구 대상 페이지 (공유 프레임 기준)
* @param kva : 매핑할 커널 가상 주소 (unused, frame->kva 사용)
* @return true if success
*/
static bool anon_swap_out(struct page *page)
{
struct anon_page *anon_page = &page->anon;
struct frame *frame = page->frame;
// (1) 비어 있는 swap 슬롯을 하나 꺼내 현재 페이지에 할당
anon_page->slot = list_entry(list_pop_front(&swap_slot_list), struct swap_slot, slot_elem);
// (2) 해당 프레임에 연결된 모든 페이지를 순회하며 swap-out
while (!list_empty(&frame->page_list))
{
struct page *out_page = list_entry(list_pop_front(&frame->page_list), struct page, out_elem);
// (3) 프레임 참조 수 감소
frame->cnt_page -= 1;
// (4) 스왑 슬롯에 해당 페이지 정보 저장
list_push_back(&anon_page->slot->page_list, &out_page->out_elem);
out_page->anon.slot = anon_page->slot;
// (5) 페이지 내용을 스왑 디스크에 sector 단위로 저장
for (int i = 0; i < SLOT_SIZE; i++)
{
disk_write(swap_disk,
out_page->anon.slot->start_sector + i,
out_page->va + DISK_SECTOR_SIZE * i);
}
// (6) 현재 프로세스의 pml4에서 이 페이지에 대한 매핑 제거
pml4_clear_page(out_page->pml4, out_page->va);
}
return true;
}
/* 스왑 디스크에서 데이터를 읽어와 메모리로 복구 (anonymous 페이지 swap-in)
*
* [역할]
* - 스왑 슬롯에 저장된 데이터를 하나의 프레임으로 복구
* - 이 프레임을 공유하던 모든 페이지에 대해 pml4 매핑 복원
* - 스왑 슬롯은 복구 이후 재사용 가능하도록 반환
*
* @param page : 복구 대상 페이지 (공유 프레임 기준)
* @param kva : 매핑할 커널 가상 주소 (unused, frame->kva 사용)
* @return true if success
*/
static bool anon_swap_in(struct page *page, void *kva)
{
struct anon_page *anon_page = &page->anon;
struct swap_slot *slot = anon_page->slot;
struct list *page_list = &slot->page_list;
int read = 0;
// (1) swap-out 당시 이 프레임을 공유하던 모든 페이지에 대해 복구 수행
while (!list_empty(page_list))
{
// (2) 해당 페이지를 꺼내 pml4에 다시 매핑
struct page *in_page = list_entry(list_pop_front(page_list), struct page, out_elem);
pml4_set_page(in_page->pml4, in_page->va, page->frame->kva, in_page->writable);
// (3) 디스크에서 실제 데이터 복구는 단 한 번만 수행 (첫 번째 페이지 기준)
if (read++ == 0)
{
for (int i = 0; i < SLOT_SIZE; i++)
{
// 디스크로부터 sector 단위로 읽어서 프레임에 로드
disk_read(swap_disk, slot->start_sector + i, in_page->va + DISK_SECTOR_SIZE * i);
// 해당 sector는 zero_set으로 덮어서 "지운다" (중복 쓰기 방지)
disk_write(swap_disk, slot->start_sector + i, zero_set + DISK_SECTOR_SIZE * i);
}
}
// (4) 복구된 페이지를 다시 frame과 연결
in_page->frame = page->frame;
page->frame->cnt_page += 1;
list_push_back(&page->frame->page_list, &in_page->out_elem);
}
// (5) 사용한 swap 슬롯을 다시 swap_slot_list에 반환 (재사용 가능)
list_push_back(&swap_slot_list, &slot->slot_elem);
return true;
}
vm.cvm_get_victim()한 줄 요약: 교체 대상(victim) 프레임을 Clock 알고리즘 기반으로 선택
주요 역할:
페이지 교체가 필요할 때 victim frame을 선정
accessed 비트를 활용하여 최근 사용 여부를 판단
핵심 포인트:
Clock 알고리즘: Second Chance 방식으로 accessed == true면 기회를 한 번 더 주고 비트를 초기화함
모든 프레임이 accessed == true인 경우를 대비해 fallback으로 맨 앞 프레임을 반환
/* 페이지 교체 알고리즘: victim frame 선택
* - Clock 방식 (accessed 비트 확인 및 unset 반복) */
static struct frame *vm_get_victim(void)
{
for (struct list_elem *e = list_begin(&frame_table); e != list_end(&frame_table); e = list_next(e))
{
struct frame *frame = list_entry(e, struct frame, frame_elem);
// (1) 최근 접근된 적 없는 프레임이면 바로 교체 대상
if (!pml4_is_accessed(frame->page->pml4, frame->page->va))
return frame;
// (2) 접근된 흔적이 있다면 accessed 비트 초기화 후 다음 기회 부여
pml4_set_accessed(frame->page->pml4, frame->page->va, 0);
}
// (3) 모든 프레임이 accessed == true였던 경우: 2바퀴 째에 교체될 예정
// → 현재는 임시로 맨 앞 프레임을 반환
return list_entry(list_front(&frame_table), struct frame, frame_elem);
}
vm_evict_frame()한 줄 요약: 교체 대상 프레임을 선정하고, swap-out을 수행하는 프레임 회수 함수
주요 역할:
vm_get_victim()을 통해 교체 대상(victim frame)을 선정
해당 프레임의 페이지를 스왑 디스크로 내보내는 swap_out() 호출
중요 포인트:
프레임을 회수하기 전에 반드시 swap-out이 먼저 수행되어야 함
victim의 frame->page는 이후 재사용될 수 있으므로 링크를 끊거나 대체 필요
/* 교체할 frame을 선택하고 swap-out까지 수행 (swap_out은 TODO) */
static struct frame *vm_evict_frame(void)
{
// (1) Clock 알고리즘으로 교체 대상 frame 선택
struct frame *victim = vm_get_victim();
// (2) 해당 프레임의 페이지를 swap 디스크로 내보냄
// swap_out(victim->page); ← 아직 구현되지 않았거나 별도 호출 위치일 수 있음
// 교체 대상 프레임은 이후 vm_get_frame()에서 재사용 예정
// (3) 선택된 victim 프레임 반환
return victim;
}
vm_get_frame() 내부: 메모리 부족 시 스왑 흐름한 줄 요약: 유저 페이지 할당 실패 시 → 희생 프레임을 교체 정책으로 선정 → 해당 페이지를 swap-out
트리거 조건: palloc_get_page(PAL_USER | PAL_ZERO)가 NULL을 반환하는 경우
if (upage == NULL)
{
// (1) 메모리 부족으로 프레임 할당 실패 → frame 구조체 반환 필요
free(frame);
// (2) 교체 정책으로 victim frame 선정
frame = vm_evict_frame(); // Clock 방식으로 희생 프레임 선택
swap_out(frame->page); // 해당 페이지는 디스크로 내보냄 (swap-out)
// (3) 프레임 테이블에서 제거 후 다시 삽입 (LRU 효과)
list_remove(&frame->frame_elem); // 기존 위치 제거
list_push_back(&frame_table, &frame->frame_elem); // 가장 뒤로 (최근 사용 프레임처럼)
// (4) 프레임이 새로운 페이지를 위해 재사용될 것이므로 링크 초기화
frame->page = NULL;
}
file.cfile_backed_initializer()한 줄 요약: 파일 기반 페이지(VM_FILE) 초기화 및 메타데이터 세팅
호출 위치: vm_alloc_page_with_initializer() → lazy loading 등록 시
bool file_backed_initializer(struct page *page, enum vm_type type, void *kva)
{
// (1) 해당 페이지의 핸들러를 file-backed 용으로 설정
page->operations = &file_ops;
// (2) aux를 통해 전달된 파일 로딩 정보 해석 (lazy_load()에서 세팅한 정보)
struct file_load *aux = page->uninit.aux;
// (3) 파일 페이지 메타데이터 설정
struct file_page *file_page = &page->file;
file_page->file = aux->file; // 매핑 대상 파일
file_page->ofs = aux->ofs; // 파일 내 오프셋
file_page->read_bytes = aux->read_bytes; // 실제 읽어올 바이트 수
file_page->zero_bytes = aux->zero_bytes; // 나머지는 0으로 패딩
file_page->file_length = aux->file_length; // mmap 전체 길이 (munmap 시 사용)
// (4) 현재 스레드의 pml4와 연결
page->pml4 = thread_current()->pml4;
// (5) 프레임 → 페이지 역참조 리스트에 이 페이지 등록 (mmap 시 여러 페이지가 하나의 frame을 공유 가능)
list_push_back(&page->frame->page_list, &page->out_elem);
return true;
}
file_backed_swap_out()한 줄 요약: 파일 기반 페이지를 디스크에 기록하고, 프레임의 모든 매핑을 해제
호출 위치: vm_evict_frame() 내부에서 swap_out() 호출 시
static bool file_backed_swap_out(struct page *page)
{
struct file_page *file_page = &page->file;
// (1) 해당 프레임을 공유 중인 페이지들을 저장할 리스트 생성
struct list *file_list = malloc(sizeof(struct list));
list_init(file_list);
file_page->file_list = file_list;
struct frame *frame = page->frame;
// (2) 파일 접근 동기화를 위해 락 획득
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
// (3) frame이 참조 중인 모든 페이지를 swap-out
while (!list_empty(&frame->page_list))
{
struct page *out_page = list_entry(list_pop_front(&frame->page_list), struct page, out_elem);
// (3-1) 해당 페이지가 dirty한 경우에만 파일에 write-back
if (pml4_is_dirty(out_page->pml4, out_page->va))
{
file_write_at(out_page->file.file,
out_page->frame->kva,
out_page->file.read_bytes,
out_page->file.ofs);
}
// (3-2) 복원을 위해 file_list에 보관 (swap-in 시 사용됨)
list_push_back(file_list, &out_page->out_elem);
// (3-3) 물리 주소와의 매핑 해제
pml4_clear_page(out_page->pml4, out_page->va);
}
// (4) 락 반환
if (!is_lock_held)
lock_release(&filesys_lock);
return true;
}
file_backed_swap_in()한 줄 요약: 파일에서 데이터를 읽어오고, 프레임을 공유하던 모든 페이지를 다시 매핑
호출 위치: vm_do_claim_page() → swap_in() → file_backed_swap_in()
static bool file_backed_swap_in(struct page *page, void *kva)
{
struct file_page *file_page = &page->file;
struct list *file_list = file_page->file_list; // swap-out 당시 저장된 페이지 목록
struct frame *frame = page->frame;
// (1) 파일로부터 데이터 복구 (file_read_at)
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
file_read_at(file_page->file,
frame->kva,
file_page->read_bytes,
file_page->ofs);
if (!is_lock_held)
lock_release(&filesys_lock);
// (2) 공유된 다른 모든 페이지에 대해 다시 프레임과 매핑 수행
while (!list_empty(file_list))
{
struct page *in_page = list_entry(list_pop_front(file_list), struct page, out_elem);
// frame과 다시 연결
list_push_back(&frame->page_list, &in_page->out_elem);
// MMU에 페이지 매핑 복원
pml4_set_page(in_page->pml4, in_page->va, frame->kva, in_page->writable);
}
// (3) file_list는 이제 필요 없으므로 해제
free(file_list);
return true;
}
lazy_load()한 줄 요약: 페이지 fault 발생 시, 파일에서 데이터를 읽고 남은 공간을 zero-fill하여 메모리에 적재
호출 위치: vm_do_claim_page() → uninit_initializer() → lazy_load()
static bool lazy_load(struct page *page, void *aux_)
{
// (1) 인자로 전달된 보조 정보(aux)를 파싱
struct file_load *aux = (struct file_load *)aux_;
struct file *file = aux->file;
off_t ofs = aux->ofs; // 파일의 읽기 시작 위치
uint32_t read_bytes = aux->read_bytes; // 실제로 읽어야 할 바이트 수
uint32_t zero_bytes = aux->zero_bytes; // 0으로 채워야 할 바이트 수
// (2) aux는 일회성이므로 메모리 해제
free(aux);
// (3) 파일 시스템 락 획득 (다중 스레드 환경 보호)
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
// (4) 읽기 위치를 오프셋으로 이동하고 read 수행
file_seek(file, ofs);
read_bytes = file_read(file, page->frame->kva, read_bytes);
// (5) 락 해제
if (!is_lock_held)
lock_release(&filesys_lock);
// (6) 읽은 이후 남은 바이트는 zero-fill
memset(page->frame->kva + read_bytes, 0, zero_bytes);
return true;
}
file_backed_destroy()static void file_backed_destroy(struct page *page)
{
struct file_page *file_page = &page->file;
// (1) 프레임 공유 카운트 감소
// - 하나의 frame을 여러 페이지가 공유할 수 있으므로
page->frame->cnt_page -= 1;
// (2) 파일 시스템 락 획득
// - 이미 락을 들고 있다면 중첩 방지
bool is_lock_held = lock_held_by_current_thread(&filesys_lock);
if (!is_lock_held)
lock_acquire(&filesys_lock);
// (3) 페이지가 dirty 상태면 → 파일에 write-back
if (pml4_is_dirty(thread_current()->pml4, page->va))
{
file_write_at(page->file.file,
page->frame->kva,
page->file.read_bytes,
page->file.ofs);
}
// (4) 페이지가 참조 중이던 파일을 닫아줌
// - 파일 포인터의 참조 카운트를 줄이는 역할
file_close(page->file.file);
// (5) 락 반환
if (!is_lock_held)
lock_release(&filesys_lock);
// (6) 프레임이 아직 다른 페이지와 공유 중이라면
// 현재 페이지만 MMU에서 언매핑 처리
if (page->frame->cnt_page > 0)
pml4_clear_page(thread_current()->pml4, page->va);
}
Swap 까지 구현을 하면 다음과 같은 테스트 결과가 나온다
pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
pass tests/userprog/rox-child
pass tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/vm/pt-grow-stack
pass tests/vm/pt-grow-bad
pass tests/vm/pt-big-stk-obj
pass tests/vm/pt-bad-addr
pass tests/vm/pt-bad-read
pass tests/vm/pt-write-code
pass tests/vm/pt-write-code2
pass tests/vm/pt-grow-stk-sc
pass tests/vm/page-linear
pass tests/vm/page-parallel
pass tests/vm/page-merge-seq
pass tests/vm/page-merge-par
pass tests/vm/page-merge-stk
pass tests/vm/page-merge-mm
pass tests/vm/page-shuffle
pass tests/vm/mmap-read
pass tests/vm/mmap-close
pass tests/vm/mmap-unmap
pass tests/vm/mmap-overlap
pass tests/vm/mmap-twice
pass tests/vm/mmap-write
pass tests/vm/mmap-ro
pass tests/vm/mmap-exit
pass tests/vm/mmap-shuffle
pass tests/vm/mmap-bad-fd
pass tests/vm/mmap-clean
pass tests/vm/mmap-inherit
pass tests/vm/mmap-misalign
pass tests/vm/mmap-null
pass tests/vm/mmap-over-code
pass tests/vm/mmap-over-data
pass tests/vm/mmap-over-stk
pass tests/vm/mmap-remove
pass tests/vm/mmap-zero
pass tests/vm/mmap-bad-fd2
pass tests/vm/mmap-bad-fd3
pass tests/vm/mmap-zero-len
pass tests/vm/mmap-off
pass tests/vm/mmap-bad-off
pass tests/vm/mmap-kernel
pass tests/vm/lazy-file
pass tests/vm/lazy-anon
pass tests/vm/swap-file
pass tests/vm/swap-anon
pass tests/vm/swap-iter
pass tests/vm/swap-fork
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
pass tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
pass tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
pass tests/filesys/base/syn-read
pass tests/filesys/base/syn-remove
pass tests/filesys/base/syn-write
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
FAIL tests/vm/cow/cow-simple
1 of 141 tests failed.
구현을 하며 정말 많은 구글링을 했던 것 같다. 정말 내가 다 구현한 것인지도 의문이 든다.
몇번 복기를 하며 코드를 다시 살펴봐야 할 것 같다