[PintOS] Project 3 : VM - Swap

CorinBeom·2025년 6월 9일

PintOS

목록 보기
16/19
post-thumbnail

Swap이란 무엇이고 왜 필요한가?

운영체제는 제한된 물리 메모리로 많은 프로세스를 동시에 실행해야 한다.
이때 메모리가 부족하면, 사용하지 않는 페이지를 디스크에 임시로 저장하고 필요할 때 다시 불러오는 방식이 사용되며,
이를 Swap이라 부른다.

PintOS의 가상 메모리 시스템에서도 Swap 기능은 중요한 구성 요소이다.
이 글에서는 PintOS에서 Swap 기능을 직접 구현한 과정을 설계 → 구현 흐름 → 주요 함수 설명 순서로 정리해보겠다.

Swap을 어떻게 구현할 것인가?

어떤 자료구조를 쓸 것인가?

  • swap_disk: 디스크 핸들러

  • swap_slot_list: 사용 가능한 swap 슬롯들 (프레임 하나에 해당하는 디스크 섹터 묶음)

  • swap_slot.page_list: 하나의 프레임을 공유하는 페이지들

어떤 방식으로?

vm_get_frame()에서 프레임이 부족하면 vm_evict_frame() → swap_out() 호출

스왑 영역이 꽉 차면 어떻게 해야 할까? (일단 PintOS 과제에선 무제한으로 가정해도 무방)

  • 동기화: swap_lock을 사용한 동시 접근 제어

anon.c

📌 vm_anon_init()

  • swap_disk를 초기화하고, swap_slot_list를 구성
  • 디스크 전체를 SLOT_SIZE 단위로 나누어 스왑 슬롯으로 예약
void 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에 사용
}

📌 anon_swap_out()

  • victim frame의 모든 페이지를 스왑 슬롯에 기록
  • 각 페이지의 매핑 해제 및 swap_slot.page_list에 등록
/* 스왑 디스크에서 데이터를 읽어와 메모리로 복구 (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;
}

📌 anon_swap_in()

  • 스왑 슬롯에서 디스크 내용을 읽어 복원
  • 해당 슬롯에 포함됐던 모든 페이지의 매핑 복구
/* 스왑 디스크에서 데이터를 읽어와 메모리로 복구 (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.c

📌 vm_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.c

📌 file_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()

  • 한 줄 요약: file-backed 페이지 제거 시, dirty 상태면 write-back 하고, 파일 닫고, 매핑 제거
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.

구현을 하며 정말 많은 구글링을 했던 것 같다. 정말 내가 다 구현한 것인지도 의문이 든다.
몇번 복기를 하며 코드를 다시 살펴봐야 할 것 같다

profile
Before Sunrise

0개의 댓글