PintOS/Project 3 : VIRTUAL MEMORY

[PintOS] Project 3 : VM - Swap In/out(anon)

넌뭐가그렇게중요해 2025. 6. 11. 20:30

이번 글에서는 PintOS Project 3의 Swap in/out 파트 중,
anon과 관련된 구현 내용을 정리합니다.

KAIST 공식 문서의  중 아직 남은 함수들인 다음 네 가지를 중점적으로 다룹니다:

  • vm_anon_init(void)
  • anon_initializer(struct page *page, enum vm_type type, void *kva)
  • anon_swap_out(struct page *page)
  • anon_swap_in(struct page *page, void *kva)

Anonymous Page란?

Anonymous Page는 특정한 파일에 연결되지 않은 페이지입니다. 예를 들어, 프로그램 실행 중 malloc이나 brk를 통해 동적으로 할당되는 메모리 영역은 Anonymous Page에 해당합니다. 이 페이지들은 파일 백업이 없기 때문에, 물리 메모리에서 제거될 경우 이를 보존할 다른 저장소가 필요합니다.

이때 사용하는 것이 Swap Disk입니다. 익명 페이지는 메모리 부족 등의 이유로 스왑 아웃될 수 있으며, 이후 다시 필요할 때 스왑 안됩니다. 이때 실제 데이터를 임시로 저장하고 불러오는 장치가 바로 swap disk입니다.


Swap In / Swap Out이란?

  • Swap Out: 익명 페이지의 데이터가 현재 물리 메모리에 있지만, 메모리 공간이 부족한 경우 해당 페이지를 swap disk로 옮깁니다. 이 과정을 swap out이라고 합니다.
  • Swap In: 스왑 아웃된 페이지가 다시 필요해질 경우, swap disk로부터 해당 데이터를 읽어와 메모리에 복원하는 과정입니다.

Anonymous Page Swap In/Out - Control Flow (전체 흐름도)

[페이지가 메모리 부족 등으로 evict 대상이 됨]
            │
            ▼
      swap_out(page)
            │
            ▼
 [page->operations->swap_out(page) 호출]
            │
            ▼
     anon_swap_out(struct page *page)
            │
            ├─ 빈 swap 슬롯 찾기 (bitmap 등)
            ├─ page->frame->kva 에서 8개 sector 단위로 disk_write()
            ├─ page->anon.swap_slot = 할당된 slot index
            └─ page->frame 해제 (pml4_clear 등 포함)

  (결과: 페이지가 스왑 디스크에 저장되고 메모리에서 제거됨)
────────────────────────────────────────────────────────────
[해당 페이지에 접근 발생 → page fault 발생]
            │
            ▼
   handle_page_fault() → vm_try_handle_fault()
            │
            ▼
    spt_find_page(spt, addr) → page 구조체 반환
            │
            ▼
    page->operations->swap_in(page, kva)
            │
            ▼
     anon_swap_in(struct page *page, void *kva)
            │
            ├─ page->anon.swap_slot 위치에서 8개 sector disk_read()
            ├─ 데이터를 kva로 복사
            ├─ page->anon.swap_slot = -1 (또는 INVALID)
            └─ swap bitmap에서 해당 slot free 처리

  (결과: 페이지가 다시 메모리에 로드되어 정상 실행)

1. vm_anon_init() - Anonymous Page Swapping 서브시스템 초기화 함수

// .../vm/anon.c
/* Initialize the data for anonymous pages */
void vm_anon_init(void)
{
	/*	스왑 영역으로 사용할 디스크를 가져옵니다.
		디스크 번호(1, 1)은 PintOS에서 일반적으로 스왑 디스크로 설정된 위치입니다.
	*/
	swap_disk = disk_get(1, 1);

	// 예외 처리 : swap_disk가 없을 경우 커널 패닉 발생
	if (swap_disk == NULL)
	{
		PANIC("CAN'T FIND SWAP DISK!");
	}

	/** TODO: bitmap 자료구조로 스왑 테이블 만들기
		스왑 테이블은 각 스왑 슬록(swap slot)의 사용 여부를 추적하기 위한 비트맵
		- 스왑 슬롯 : 메모리의 한 페이지를 디스크에 저장할 수 있는 최소 단위(1 page = PGSIZE)
		- 각 스왑 슬롯은 여러 개의 디스크 섹터(sector)로 구성

		따라서 스왑 슬롯의 개수 = 전체 디스크 섹터 수 / 한 페이지를 구성하는 섹터 수
		- 디스크 전체 섹터 수 : disk_size(swap_disk)
		- 한 페이지당 섹터 수 : PGSIZE / DISK_SECTOR_SIZE

		이 값을 기반으로 비트맵을 생성
		- bitmap의 각 비트는 하나의 스왑 슬롯을 의미하며, 0이면 비어있고 1이면 사용중
	 */
	swap_table = bitmap_create(disk_size(swap_disk) / (PGSIZE / DISK_SECTOR_SIZE));
}

1.1. vm_anon_init() 핵심 요약

  • 스왑 디스크(swap_disk)는 disk_get(1,1)을 통해 할당되며, 이는 일반적으로 PintOS에서 스왑 용도로 사용되는 디스크입니다.
  • 스왑 테이블(swap_table)은 비트맵(bitmap)으로 구성되며, 각 비트는 하나의 스왑 슬롯 사용 여부를 나타냅니다.
  • 하나의 스왑 슬롯은 한 페이지(PGSIZE)를 저장할 수 있으며, 이는 여러 디스크 섹터(DISK_SECTOR_SIZE)로 이루어져 있으므로 전체 슬롯 수 계산 시 나눗셈이 필요합니다.

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

  • swap_disk를 설정하지 않으면 스왑 기능이 아예 작동하지 않기 때문에 커널이 부팅될 때 반드시 초기화해야 합니다.
  • 비트맵을 사용하는 이유는 간단하고 빠르게 사용 가능한 스왑 슬롯을 추적할 수 있기 때문입니다.
    예를 들어, bitmap_scan_and_flip()을 사용하여 빠르게 빈 슬롯을 찾고 할당할 수 있습니다.
  • 스왑 슬롯을 정해진 크기(PGSIZE)로 고정하여 관리하는 방식은 구현이 단순하며, 페이지 단위로 스왑 인/아웃할 때 일관성을 제공합니다.

2. anon_initializer() - Anonymous Page용 초기화 함수

//.../vm/anon.c
/* Initialize the file mapping */
bool anon_initializer(struct page *page, enum vm_type type, void *kva)
{
	// 예외 처리 → 들어온 page가 null일 경우
	if (page == NULL)
	{
		return false;
	}

	/*	이 페이지는 anonymous 페이지이므로, anon_ops로 설정
		anon_ops는 스왑 인/아웃 등을 포함한 함수 포인터 구조체
	*/
	page->operations = &anon_ops;

	/* uninit을 anon으로 변환 */
	struct anon_page *anon_page = &page->anon; // page->anon은 포인터가 아니라 구조체 자체여서 항상 유효한 주소를 반환함

	/* swap index 초기화 
		-1은 아직 스왑 아웃된 적이 없다는 것을 의미
	*/
	anon_page->swap_idx = -1;

	/*	페이지의 물리 주소(kva)가 유효하며,
		해당 페이지의 frame이 1개 이하로만 참조되고 있을 경우
		새로 할당된 물리 페이지라고 간주하고 초기화함
	*/
	if (kva != NULL && page->frame->ref_cnt <= 1)
	{
		// 물리 페이지 전체를 0으로 초기화(보안 및 예측 가능한 동작 보장)
		memset(kva, 0, PGSIZE);
	}

	return true;
}

2.1. anon_initializer() 핵심 요약

  • 이 함수는 uninit 상태의 페이지가 Anonymous 타입으로 전환될 때 호출됩니다.
  • page->operations를 anon_ops로 설정하여 해당 페이지의 스왑 인/아웃 함수들을 지정합니다.
  • 내부 구조체인 anon_page를 초기화하며, 특히 swap_idx를 -1로 설정해 아직 스왑 된 적 없음을 표시합니다.
  • 추가적으로, 물리 주소(kva)가 유효하고 프레임 참조 카운트가 1 이하일 경우, 해당 물리 페이지를 memset으로 0으로 초기화합니다.

2.2. anon_initializer()를 이렇게 구현한 이유

  • 함수 포인터(operations) 설정
    익명 페이지는 파일과 연결된 페이지(file-backed)와는 다르게, 디스크 백업이 없는 순수 메모리 페이지입니다. 따라서 해당 페이지에 스왑 인/아웃 기능을 연결하기 위해 anon_ops를 지정해 줍니다.
  • swap_idx를 -1로 초기화하는 이유
    해당 페이지가 아직 디스크에 스왑 아웃된 적이 없음을 명시적으로 표시함으로써, 이후 스왑 인/아웃 시 혼동을 방지할 수 있습니다.
  • 물리 페이지 초기화의 이유
    새로 할당된 물리 페이지는 과거의 데이터가 남아 있을 수 있으므로, 이를 0으로 초기화하여 보안을 강화하고 예측 가능한 동작을 보장합니다.
    단, 공유된 프레임이거나 재사용되는 경우는 제외해야 하므로 ref_cnt <= 1 조건을 둡니다

3. anon_swap_in() – Anonymous Page를 디스크에서 메모리로 복원하는 함수

//.../vm/anon.c
/* 스왑 디스크에서 내용을 읽어와 페이지를 스왑인합니다. */
static bool
anon_swap_in(struct page *page, void *kva)
{
	struct anon_page *anon_page = &page->anon;
	int swap_idx = anon_page->swap_idx;

	// 예외 처리 → swap_idx가 -1이면 페이지가 스왑아웃된 적이 없거나 이미 복구되었으므로 스왑 인 생략
	if (swap_idx < 0)
	{
		return false;
	}
	// disk_read에서 사용할 버퍼
	// void *buffer[PGSIZE];
	/** TODO: 페이지 스왑 인
	 * disk_read를 데이터를 읽고 kva에 데이터 복사
	 * swap_idx를 -1로 바꿔주어야 함
	 * 프레임 테이블에 해당 프레임 넣어주기
	 * 프레임하고 페이지 매핑해주기
	 */

	// 한 섹터는 512바이트이고, 한 페이지는 4KB(4096바이트)이므로
	// 총 8개의 섹터를 순차적으로 읽어야 전체 페이지 데이터를 복원할 수 있음
	// swap_idx는 스왑 테이블 상의 페이지 단위 인덱스를 의미하며,
	// 실제 섹터 번호는 swap_idx * 8부터 시작함
	for (int i = 0; i < 8; i++)
	{
		disk_read(swap_disk,					 // 스왑 디스크에서 데이터를 읽어옴
				  (swap_idx * 8) + i,			 // 8개의 연속된 섹터에 페이지가 저장되어 있으므로, i를 더해가며 읽음
				  kva + (DISK_SECTOR_SIZE * i)); // 읽어온 데이터를 커널 가상 주소 kva에 512B 단위로 복사
	}

	// 스왑 테이블에서 해당 스왑 슬롯을 비어있다고 표시 (해당 슬롯 재사용 가능하도록)
	bitmap_reset(swap_table, swap_idx);

	// 페이지가 더 이상 스왑 영역에 존재하지 않음을 나타내기 위해 swap_idx를 -1로 초기화
	anon_page->swap_idx = -1;

	return true;
}

3.1. anon_swap_in() 핵심 요약

anon_swap_in() 함수는 Anonymous Page가 디스크에 스왑 된 이후, 다시 메모리로 불러올 때 사용하는 함수입니다.
swap_idx를 이용해 디스크에서 데이터를 읽어와 kva 주소로 복사하며, 스왑 슬롯의 사용 상태를 해제하고 페이지의 swap_idx를 초기화합니다.

3.2. anon_swap_in()을 이렇게 구현한 이유

  • 스왑 디스크에서 페이지 단위로 읽기
    PintOS에서는 한 페이지가 디스크 상의 **8개의 섹터(=512B × 8 = 4096B)**에 저장됩니다.
    따라서 disk_read()를 8번 호출하여 스왑 된 데이터를 전체 읽어와야 합니다.
  • swap_idx를 사용한 정확한 위치 계산
    swap_idx는 스왑 슬롯 단위 인덱스이며, 실제 섹터 번호는 swap_idx * 8 + i로 계산됩니다.
    이를 통해 정확히 해당 페이지의 저장 위치를 추적하고 복구할 수 있습니다.
  • 비트맵 해제 및 swap_idx 초기화
    해당 스왑 슬롯이 다시 사용될 수 있도록 bitmap_reset()을 호출해 비트를 0으로 바꾸고,
    swap_idx를 -1로 설정하여 "스왑 아웃 상태가 아님"을 표시합니다.

4. anon_swap_out() – Anonymous Page를 디스크로 스왑 아웃하는 함수

//.../vm/anon.c
/* 페이지의 내용을 스왑 디스크에 기록하여 스왑아웃합니다. */
static bool
anon_swap_out(struct page *page)
{
	// 예외 처리: page가 NULL이면 실패
	if (page == NULL)
	{
		return false;
	}

	struct anon_page *anon_page = &page->anon;

	// 비어있는 스왑 슬롯을 비트맵에서 검색하고 할당 (false인 비트를 true로 뒤집음)
	size_t swap_idx = bitmap_scan_and_flip(swap_table, 0, 1, false);

	// 스왑 슬롯 할당 실패 시 (비트맵 에러)
	if (swap_idx == BITMAP_ERROR)
	{
		ASSERT(bitmap_test(swap_table, swap_idx) == false); // 디버깅용 확인
		return false;
	}

	// 페이지 전체(4KB)를 스왑 디스크에 저장 (512B * 8 = 4096B)
	for (int i = 0; i < 8; i++)
	{
		disk_write(
			swap_disk,							  // 스왑 디스크에
			(swap_idx * 8) + i,					  // 8개의 섹터에 순차 저장
			page->frame->kva + (DISK_SECTOR_SIZE * i) // 현재 페이지의 실제 데이터
		);
	}

	// 페이지와 프레임 간의 연결을 끊어 더 이상 메모리에 존재하지 않도록 함
	page->frame->page = NULL;
	page->frame = NULL;

	// anon_page에 swap_idx 저장 → 나중에 다시 swap_in할 때 사용
	anon_page->swap_idx = swap_idx;

	return true;
}

4.1. anon_swap_out() 핵심 요약

anon_swap_out()은 Anonymous Page를 메모리에서 제거하고, 해당 내용을 디스크의 스왑 영역으로 내보내는 함수입니다.
스왑 디스크에 저장된 페이지는 추후 필요시 anon_swap_in()을 통해 다시 메모리로 불러올 수 있습니다.

4.2. anon_swap_out()를 이렇게 구현한 이유 

  • 페이지 내용을 디스크에 기록하기 위한 스왑 아웃
    Anonymous Page를 메모리에서 디스크로 내보내기 위해, 스왑 디스크의 빈 공간을 찾아 disk_write()를 통해 데이터를 저장합니다. 이 과정을 통해 메모리를 효율적으로 관리하고, 이후 필요시 다시 불러올 수 있게 됩니다.
  • 비어 있는 스왑 슬롯 검색: bitmap_scan_and_flip()
    • 왜?: 스왑 디스크는 페이지 단위로 관리되며, 각 슬롯의 사용 여부는 bitmap을 통해 추적합니다.
    • bitmap_scan_and_flip(swap_table, 0, 1, false)는 **false(=비어있는 슬롯)**를 찾아 true로 설정합니다.
    • 이렇게 하면 중복 없는 슬롯 할당이 가능하고, 이후 복구 시 정확한 위치 추적이 용이합니다.
  • 예외 처리: BITMAP_ERROR 검사
    • bitmap_scan_and_flip()의 반환값이 BITMAP_ERROR라면 스왑 공간이 부족한 것이므로, 즉시 false를 반환해 에러를 처리합니다.
    • ASSERT(bitmap_test(...))는 디버깅 도중 논리 오류 방지를 위해 작성한 방어 코드입니다.
  • disk_write() 반복 호출: 페이지 전체 저장
    • 한 페이지는 4KB(=4096B), 디스크의 한 섹터는 512B이므로, 총 8개의 연속된 섹터에 나눠 저장해야 합니다.
    • 따라서 for (int i = 0; i < 8; i++) 반복문을 통해, disk_write()를 8번 호출하여 512B씩 데이터를 저장합니다.
    • 저장 위치는 swap_idx * 8 + i로 계산하여, 해당 슬롯의 정확한 물리 주소를 추적합니다.