PintOS|가상 메모리 시스템 완전 정복: VA, PA, 페이지 폴트, Lazy Allocation

맹쥐·2025년 6월 3일

kaist - PintOS

목록 보기
7/10
post-thumbnail

PintOS에서 가상 메모리는 어떻게 작동될까?

PintOS가 부팅되면, 시스템은 mem_start부터 mem_end까지의 큰 메모리 공간을 확보한다.
오로지 PintOS만을 위한 공간이다.

이 메모리 공간은 크게 커널 영역과 유저 영역으로 나뉘며, 각 영역은 서로 다른 목적으로 사용된다.


그렇다면, 가상 메모리(Virtual Memory)는 왜 필요할까?

컴퓨터에서 우리는 많은 프로세스를 동시에 실행시킨다.
예를들어 유튜브, 크롬, 카카오톡, 게임, vscode 등등 ..

각 프로그램은 고유한 데이터를 가지고 있으며,
이 모든 정보는 디스크에 저장되어 있다.

( 이전에 공부했던 분들은 알 것이다. )
하지만 컴퓨터에서 프로그램이 실제로 실행되기 위해서는,
디스크에 저장된 데이터를 RAM(메인 메모리)에 불러와야 한다.

문제는 RAM의 크기가 제한되어 있다는 것.

엄청난 양의 데이터를 메모리인 RAM에 모두 올려놓아야한다.

결론적으로 여러 프로그램이 동시에 실행되기에 공간이 부족하다.

이러한 문제점을 해결하기 위해 도입한 개념이 바로
가상메모리이다.

  • 각 프로세스는 독립된 가상 주소 공간을 가지며,
  • 실제로는 운영체제가 그 주소들을 물리 메모리 주소로 매핑해준다.
  • 즉, 모든 프로세스가 똑같은 0x80400000을 쓰더라도,
    실제 물리 메모리에서는 서로 다른 위치를 가리키게 된다.

이해가 잘 안될 것이다.

이게 왜 효율적일까?

예를 들어, 카카오톡을 켰다고 해서
모든 채팅방, 사진, 설정 데이터를 한 번에 다 쓰는 건 아니다.

그래서 운영체제는 가상 메모리를 통해
"필요할 때만" 데이터를 디스크에서 RAM으로 불러온다.

가상메모리를 사용하기 전이다.

이렇게 가상의 메모리 공간을 던져주고,
프로세스는 그만큼의 공간을 모두 사용한다고 착각한다.

하지만 실제로는 그때그때 필요한 데이터만 디스크에서 메모리로 꺼내오는 똑똑한 방식이다.


우선, 각 프로세스마다 자신만의 가상 메모리(Virtual Memory)가 하나씩 주어진다.
이 가상 메모리의 크기를 예시로 0~99까지의 공간이라고 해보자.

재밌는 사실은, 모든 프로세스가 똑같이 0~99의 가상 주소를 사용한다는 것이다.
물론 실제로는 같은 물리 주소를 사용하지 않는다.
이처럼 프로세스 입장에서만 존재하는 주소를 우리는 가상 주소(VA: Virtual Address)라고 부른다.

반대로, 실제로 메모리(RAM)에서 접근되는 주소를 물리 주소(PA: Physical Address)라고 한다.
이 VA와 PA의 연결 관계를 저장해두는 것이 바로 페이지 테이블(Page Table)이다.

여기서 꼭 집고 넘어가야할 개념은,

CPU는 페이지 테이블을 통하여 VA에서 PA로 변환한다.

잊지말자.
CPU는 구조체 정보를 확인하지 않는다.
오로지 페이지 테이블만을 확인한다.

따라서 우리는 꼭 페이지 테이블에 이러한 정보를 등록해둬야한다.
(커널이 관리하는 부분과 cpu가 처리하는 부분을 구분하여 생각하자.)


🌸 흐름 따라가기

[1] 유저가 VA에 접근한다.
[2] CPU가 페이지 테이블을 통해 VA → PA를 찾는다.

  • 페이지 테이블에 VA → PA 매핑이 존재할 경우와 존재하지 않을 경우로 나뉜다.

경우 1 : PA가 존재한다.

두가지 상황이 있다.

1-1 잘 매핑이 되어있는 경우 - 성공!

성공했으니 잘 따라서 가져오면 된다.

1-2 두번째로, PA가 존재하지만 접근 불가능 - 실패!

  • 예: PTE_P == 0, 주소만 있고 현재는 메모리에 올라와 있지 않은 경우
  • 혹은 접근 권한이 없거나 잘못된 값일 수도 있다. (예: read-only인데 write 시도)

경우 2: PA가 없는 경우. - 매핑 없음

▶ 마찬가지로 페이지 폴트 발생

  • 이때 운영체제는 SPT(Supplemental Page Table)를 확인한다.
  • 해당 VA가 어떤 타입인지 확인하고, 각 타입에 맞는 조치를 취한다.

🌸 흐름 따라가기 - 코드 관점

  1. 유저가 어떤 VA에 접근

  2. CPU는 pml4를 통해 VA → PA 매핑을 확인

  3. 매핑이 없거나 권한이 맞지 않으면 페이지 폴트 발생

이때 page_fault() 함수가 호출됨


page_fault() 내부 흐름

static void
page_fault (struct intr_frame *f) {
	bool not_present;  
	/* True: 단순히 페이지가 아직 메모리에 없음 - 복구가능 (lazy allocation/ lazy loading/ swap-in), false: 권한 위반 - 복구 불가 */
	bool write;        
    /* True: write로 접근했을 때 , false: read로 접근했을 때. */
    /* writable과 다른개념임 !*/
	bool user;         
    /* True: access by user, false: access by kernel. */
	void *fault_addr;  
    /* Fault address. */

	fault_addr = (void *) rcr2(); /* 페이지 폴트가 발생한 가상주소 */

	/* Determine cause. */
	not_present = (f->error_code & PF_P) == 0;
	write = (f->error_code & PF_W) != 0;
	user = (f->error_code & PF_U) != 0;

#ifdef VM
	/* For project 3 and later. */
    /* ‼️ 여기로 ‼️ */
	if (vm_try_handle_fault (f, fault_addr, user, write, not_present))
		return;
#endif

	/* Count page faults. */
	page_fault_cnt++;
	sys_exit(-1);
	/* If the fault is true fault, show info and exit. */
	printf ("Page fault at %p: %s error %s page in %s context.\n",
			fault_addr,
			not_present ? "not present" : "rights violation",
			write ? "writing" : "reading",
			user ? "user" : "kernel");
	kill (f);
}
}

! 주석에 인자에 대한 설명을 살펴볼 것.

[참고]
PTE_P | Present (존재) 여부 | 이 페이지가 메모리에 실제 존재하는가?
PTE_W | Writable (쓰기 가능) | 이 페이지가 쓰기 가능한가?
PTE_U | User (유저 접근 가능) | 이 페이지가 유저모드에서 접근 가능한가?

여기서

  • f : 인터럽트 프레임
  • fault_addr : 페이지 폴트 난 주소
  • user : 유저로 접근했는지 (true/fasle)
  • write : 쓰기로 접근했는지 (true/false)
  • not_present : 페이지가 메모리에 존재하지 않을 때 (true/ false)

이 인자들을 vm_try_handle_fault()로 넘겨준다.


vm_try_handle_fault() 내부 흐름

vm_try_handle_fault() = 진짜 페이지 사용하기 전, 확인하는 함수 !

< skeleton code >

/* Return true on success */
// 성공 시 true를 반환합니다.
bool
vm_try_handle_fault (struct intr_frame *f UNUSED, void *addr UNUSED,
		bool user UNUSED, bool write UNUSED, bool not_present UNUSED) {
	struct supplemental_page_table *spt UNUSED = &thread_current ()->spt;
	struct page *page = NULL;
	/* TODO: Validate the fault */
	/* TODO: Your code goes here */
	// TODO: 페이지 폴트를 검증하고, 처리 코드를 작성하세요.
	return vm_do_claim_page (page);
}

아까 page_fault() 에서는 인자들을 넘겨 받기만 했다.
즉, vm_try_handle_fault()가 책임을 전가받은 셈이다.

⚡️ 따라서 이곳에서 페이지 폴트 종류를 검증할 필요가 있다.

쉽게 말하면 메모리를 할당할 필요조차 없는 애들을 걸러내야 한다.
크게 세가지 경우가 있다.

  1. 페이지가 존재하지 않는 경우
    -> 당연하다.
  2. write 로 접근했는데, 페이지가 writable하지 않은 경우
    -> 접근이 잘못 됐으므로, 바로 false를 반환한다.
  3. not_present = false
    -> 페이지는 있지만, 권한을 위반한 경우
bool
vm_try_handle_fault (struct intr_frame *f, void *addr,
                     bool user, bool write, bool not_present) {
    struct supplemental_page_table *spt = &thread_current ()->spt;

    struct page *page = spt_find_page(spt, addr);
    if (page == NULL) return false;

    // 접근이 불가능한 경우 → 쓰기 권한 등 체크
    if (write && !page->writable)
        return false;
        
    /* 권한 */
	if (!not_present){
		return false;
	}

    // 진짜로 메모리 할당 및 디스크에서 끌어오는 행위!
    return vm_do_claim_page (page);
}

이렇게 페이지를 요청할 필요가 없는 애들을 다 제쳐내고 나서야,
본격적으로 프레임을 요청한다 !


vm_do_claim_page(page) 흐름

자, 이제 VA에 해당하는 올바른 데이터를 가져올 차례이다.

  1. 새로운 물리 메모리 페이지를 할당하고 palloc_get_page
  2. 해당 물리 페이지를 해당 VA와 매핑하고 pml4_set_page -> CPU를 위해 ..
  3. 할당해준 공간을 디스크(파일) 또는 스왑 디스크 등에서 데이터를 로드해 채워 넣는다.

아무튼 swap_in을 한다는 말은?
뭔가 데이터를 가져와서 채워넣는다는 말이다.
( palloc_get_page 로 확보한 메모리 공간인 kva 에 !)


vm_do_claim_page()

static bool vm_do_claim_page(struct page *page) {
    struct frame *frame = vm_get_frame();  // 실제 물리 프레임 할당
    if (frame == NULL)
        return false;

    frame->page = page;
    page->frame = frame;
	bool writable = page -> writable;
    // pml4_set_page로 VA&PA 매핑 (CPU가 접근할 수 있게)
	if (pml4_get_page(thread_current()->pml4, page->va) == NULL){
    	bool check = pml4_set_page(thread_current()->pml4, page->va, frame->kva, writable);
   		if (!check){
        	return false;
		}
    }else {
		return false;
	}
   return swap_in(page, frame->kva);
}

swap_in() – 타입별 동작 처리

bool swap_in(struct page *page, void *kva) {
    return page->operations->swap_in(page, kva);  // 페이지 타입별 구현
}

page->operations는 다음과 같은 함수 테이블을 가지고 있다.

  • VM_ANON (vm_anon.c) 스왑 디스크에서 읽어온다.
  • VM_FILE (vm_file.c) 파일에서 해당 offset의 내용을 읽는다.
  • VM_UNINIT(vm_uninit.c) → 내부적으로 uninit.page_initializer() 호출한다.
static const struct page_operations uninit_ops = {
	.swap_in = uninit_initialize,
	.swap_out = NULL,
	.destroy = uninit_destroy,
	.type = VM_UNINIT,
};

VM_UNINIT의 Lazy Loading 초기화

처음에는 vm_alloc_page_with_initializer()가 호출되어 uninit_page가 만들어진다.

여기서 실제로는 데이터를 로딩하지 않는다.

나중에 vm_do_claim_page()swap_in()uninit.page_initializer()가 불린다.
타고 따라가보자.

static bool
uninit_initialize (struct page *page, void *kva) {
    struct uninit_page *uninit = &page->uninit;
    vm_initializer *init = uninit->init;
    void *aux = uninit->aux;

    // 실제 타입별로 초기화 함수 호출 (ex. lazy_load_segment 등)
    return uninit->page_initializer (page, uninit->type, kva) &&
           (init ? init (page, aux) : true);
}
결국 uninit → 실제 타입 (anon, file)로 전환되며 그에 맞는 동작을 함

전체 흐름 정리

  • 유저가 VA에 접근

  • CPU가 페이지 테이블 확인 → PTE_P == 0 이면 페이지 폴트

  • 커널이 vm_try_handle_fault() 호출

  • SPT에서 해당 VA에 해당하는 struct page 찾음

  • vm_do_claim_page() 호출:

  • 새 frame 할당

  • page-frame 연결

  • VA ↔ PA 매핑 등록 (pml4_set_page)

  • 타입별 swap_in() 호출하여 메모리 적재

profile
이유민

0개의 댓글