
PintOS가 부팅되면, 시스템은 mem_start부터 mem_end까지의 큰 메모리 공간을 확보한다.
오로지 PintOS만을 위한 공간이다.
이 메모리 공간은 크게 커널 영역과 유저 영역으로 나뉘며, 각 영역은 서로 다른 목적으로 사용된다.
그렇다면, 가상 메모리(Virtual Memory)는 왜 필요할까?
컴퓨터에서 우리는 많은 프로세스를 동시에 실행시킨다.
예를들어 유튜브, 크롬, 카카오톡, 게임, vscode 등등 ..
각 프로그램은 고유한 데이터를 가지고 있으며,
이 모든 정보는 디스크에 저장되어 있다.
( 이전에 공부했던 분들은 알 것이다. )
하지만 컴퓨터에서 프로그램이 실제로 실행되기 위해서는,
디스크에 저장된 데이터를 RAM(메인 메모리)에 불러와야 한다.
엄청난 양의 데이터를 메모리인 RAM에 모두 올려놓아야한다.
결론적으로 여러 프로그램이 동시에 실행되기에 공간이 부족하다.
이러한 문제점을 해결하기 위해 도입한 개념이 바로
가상메모리이다.
이해가 잘 안될 것이다.
이게 왜 효율적일까?
예를 들어, 카카오톡을 켰다고 해서
모든 채팅방, 사진, 설정 데이터를 한 번에 다 쓰는 건 아니다.
그래서 운영체제는 가상 메모리를 통해
"필요할 때만" 데이터를 디스크에서 RAM으로 불러온다.

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

이렇게 가상의 메모리 공간을 던져주고,
프로세스는 그만큼의 공간을 모두 사용한다고 착각한다.
하지만 실제로는 그때그때 필요한 데이터만 디스크에서 메모리로 꺼내오는 똑똑한 방식이다.
우선, 각 프로세스마다 자신만의 가상 메모리(Virtual Memory)가 하나씩 주어진다.
이 가상 메모리의 크기를 예시로 0~99까지의 공간이라고 해보자.
재밌는 사실은, 모든 프로세스가 똑같이 0~99의 가상 주소를 사용한다는 것이다.
물론 실제로는 같은 물리 주소를 사용하지 않는다.
이처럼 프로세스 입장에서만 존재하는 주소를 우리는 가상 주소(VA: Virtual Address)라고 부른다.
반대로, 실제로 메모리(RAM)에서 접근되는 주소를 물리 주소(PA: Physical Address)라고 한다.
이 VA와 PA의 연결 관계를 저장해두는 것이 바로 페이지 테이블(Page Table)이다.
여기서 꼭 집고 넘어가야할 개념은,
잊지말자.
CPU는 구조체 정보를 확인하지 않는다.
오로지 페이지 테이블만을 확인한다.
따라서 우리는 꼭 페이지 테이블에 이러한 정보를 등록해둬야한다.
(커널이 관리하는 부분과 cpu가 처리하는 부분을 구분하여 생각하자.)
[1] 유저가 VA에 접근한다.
[2] CPU가 페이지 테이블을 통해 VA → PA를 찾는다.
두가지 상황이 있다.
성공했으니 잘 따라서 가져오면 된다.
PTE_P == 0, 주소만 있고 현재는 메모리에 올라와 있지 않은 경우▶ 마찬가지로 페이지 폴트 발생
유저가 어떤 VA에 접근
CPU는 pml4를 통해 VA → PA 매핑을 확인
매핑이 없거나 권한이 맞지 않으면 페이지 폴트 발생
이때
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 (유저 접근 가능) | 이 페이지가 유저모드에서 접근 가능한가?
여기서
이 인자들을 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()가 책임을 전가받은 셈이다.
⚡️ 따라서 이곳에서 페이지 폴트 종류를 검증할 필요가 있다.
쉽게 말하면 메모리를 할당할 필요조차 없는 애들을 걸러내야 한다.
크게 세가지 경우가 있다.
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에 해당하는 올바른 데이터를 가져올 차례이다.
palloc_get_pagepml4_set_page -> CPU를 위해 ..아무튼 swap_in을 한다는 말은?
뭔가 데이터를 가져와서 채워넣는다는 말이다.
( palloc_get_page 로 확보한 메모리 공간인 kva 에 !)
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);
}
bool swap_in(struct page *page, void *kva) {
return page->operations->swap_in(page, kva); // 페이지 타입별 구현
}
page->operations는 다음과 같은 함수 테이블을 가지고 있다.
static const struct page_operations uninit_ops = { .swap_in = uninit_initialize, .swap_out = NULL, .destroy = uninit_destroy, .type = VM_UNINIT, };
처음에는 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() 호출하여 메모리 적재