어느덧 핀토스도 프로젝트 3에 접어들었다. 이번 프로젝트 3는 virtual memory를 구현하는 것이다.
virtual memory에는 많은 개념이 있지만 이번 정리에서는 vm에서 lazy_loading이 어떻게 일어나나 흐름에 대해서 정리해볼려고 한다.
처음 프로세스가 생성될 때 initd 함수에서 supplemental pae table을 초기화한다.
//process.c
static void initd(void *f_name)
{
#ifdef VM
supplemental_page_table_init(&thread_current()->spt);
#endif
process_init();
if (process_exec(f_name) < 0)
PANIC("Fail to launch initd\n");
NOT_REACHED();
}
그런 다음 process_exec로 넘어간다.
int process_exec(void *f_name)
{
char *file_name = f_name; // 매개변수 void* 로 넘겨받은 f_name을 char* 로 변환 처리
bool success;
...
/* And then load the binary */
success = load(file_name, &_if);
/* If load failed, quit. */
if (!success)
{
palloc_free_page(file_name);
return -1;
}
...
do_iret(&_if);
NOT_REACHED();
}
여기서 중점적으로 볼 부분은 9번째 줄의 함수 load이다. 기존의 핀토스에서는 load가 실행되면 파일의 정보를 바로 물리메모리에 적재했다. 하지만 이번 핀토스 프로젝트 3부터는 페이지만 할당하고 page fault가 발생했을 때 메모리를 적재한다.
load 함수를 보면 load_segment함수를 통해 세그먼트를 load하는데 이번부터는 vm에서 사용하는 다른 load_segment를 사용한다.
static bool load_segment(struct file *file, off_t ofs, uint8_t *upage,
uint32_t read_bytes, uint32_t zero_bytes, bool writable)
{
ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
ASSERT(pg_ofs(upage) == 0);
ASSERT(ofs % PGSIZE == 0);
while (read_bytes > 0 || zero_bytes > 0)
{
/* Do calculate how to fill this page.
* We will read PAGE_READ_BYTES bytes from FILE
* and zero the final PAGE_ZERO_BYTES bytes. */
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
/* TODO: Set up aux to pass information to the lazy_load_segment. */
struct binary_aux *aux = (struct binary_aux *)malloc(sizeof(struct binary_aux));
aux->file = file;
aux->ofs = ofs;
aux->read_bytes = page_read_bytes;
aux->zero_bytes = page_zero_bytes;
if (!vm_alloc_page_with_initializer(VM_ANON, upage,
writable, lazy_load_segment, aux))
{
free(aux);
return false;
}
// spt_insert_page(&thread_current()->spt, upage);
/* Advance. */
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
ofs += page_zero_bytes;
upage += PGSIZE;
}
return true;
}
위 load_segment함수는 aux에 파일의 정보들을 담고 vm_alloc_page_with_initializer를 통해 lazy_load_segment함수와 타입, 페이지, 읽기/쓰기 여부 등 담아서 호출한다.
bool vm_alloc_page_with_initializer(enum vm_type type, void *upage, bool writable,
vm_initializer *init, void *aux)
{
/*1. 초기화되지 않은 주어진 type의 페이지 생성
2. 위 페이지의 swap_in 핸들러는 자동적으로 페이지 타입에 맞게 페이지를 초기화
3. AUX를 인자로 삼는 INIT함수 호출
4. 페이지 구조체를 가지게 되면 프로세스의 보조 테이블에 그 페이지를 삽입
tip) VM_TYPE 매크로를 사용할 것*/
ASSERT(VM_TYPE(type) != VM_UNINIT)
struct supplemental_page_table *spt = &thread_current()->spt;
/* Check wheter the upage is already occupied or not. */
if (spt_find_page(spt, upage) == NULL)
{
/* TODO: Create the page, fetch the initialier according to the VM type,
* TODO: and then create "uninit" page struct by calling uninit_new. You
* TODO: should modify the field after calling the uninit_new. */
/* TODO: Insert the page into the spt. */
struct page *page = (struct page *)malloc(sizeof(struct page));
typedef bool (*initializerFunc)(struct page *, enum vm_type, void *);
initializerFunc initializer = NULL;
switch (VM_TYPE(type))
{
case VM_ANON:
initializer = anon_initializer;
break;
case VM_FILE:
initializer = file_backed_initializer;
break;
default:
break;
}
uninit_new(page, upage, init, type, aux, initializer);
page->rw = writable;
return spt_insert_page(spt, page);
}
return false;
}
vm_alloc_page_with_initializer가 실행되면 존재하는 페이지인지 검사하고 존재하지 않는 페이지이면 malloc을 통해 페이지를 하나 할당해준다.
switch-case문을 통해 type에 따라 다른 initializer를 설정해주고 uninit_new함수를 호출한다.
uninit_new 함수가 함수의 인자로 넘겨준 매개변수들을 통해 page를 초기화한다.
void
uninit_new (struct page *page, void *va, vm_initializer *init,
enum vm_type type, void *aux,
bool (*initializer)(struct page *, enum vm_type, void *)) {
ASSERT (page != NULL);
*page = (struct page) {
.operations = &uninit_ops,
.va = va,
.frame = NULL, /* no frame for now */
.uninit = (struct uninit_page) {
.init = init,
.type = type,
.aux = aux,
.page_initializer = initializer,
}
};
}
여기까지가 대기하고 있는 page를 만드는 과정이다. 내가 깨달은 바는 처음에 대기하는 것과 page fault가 나면 lazy_loading하는게 연속적으로 이루어진다고 생각했는데 이 둘을 분리해서 생각해야 이해가 잘되는 거 같다.
지금 부터는 page fault가 발생하면 lazy_loading이 실질적으로 이루어지는 부분을 알아보겠다.
처음 page fault가 발생하면 vm_try_handle_fault가 실행돼 진짜 page fault가 발생한건지 bogus page fault가 발생했는지 확인한다. bogus page fault라면 어떤 종류인지 파악하고 vm_do_claim_page를 호출한다.
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;
...
return vm_do_claim_page(page);
}
이제 vm_do_claim_page에서 실질적으로 물리 프레임을 할당해주고 pml4에 등록하게 된다.
static bool
vm_do_claim_page(struct page *page)
{
struct frame *frame = vm_get_frame();
/* Set links */
frame->page = page;
page->frame = frame;
if (pml4_set_page(&thread_current()->pml4, page->va, frame->kva, page->rw))
return swap_in(page, frame->kva);
return false;
}
pml4 등록에 성공하게 되면 swap_in을 실행하게 된다. 이 swap_in 함수는 함수형 포인터로
각각 페이지에 따라 등록되어 있는 함수가 달라지게 된다. 현재의 경우 위에서 uninit_new 함수를 실행할 때 page->operations에 uninit_ops를 넣어줘서 uninit_initialize 함수가 실행되게 된다.
static const struct page_operations uninit_ops = {
.swap_in = uninit_initialize,
.swap_out = NULL,
.destroy = uninit_destroy,
.type = VM_UNINIT,
};
uninit_initialize 함수를 살펴보면
static bool
uninit_initialize (struct page *page, void *kva) {
struct uninit_page *uninit = &page->uninit;
/* Fetch first, page_initialize may overwrite the values */
vm_initializer *init = uninit->init;
void *aux = uninit->aux;
/* TODO: You may need to fix this function. */
return uninit->page_initializer (page, uninit->type, kva) &&
(init ? init (page, aux) : true);
}
page의 init과 aux를 뽑아 마지막에 실행시켜주는 부분을 확인할 수 있는데 여기서 우리가 uninit_new에서 설정해준 lazy_load_segment가 파일의 정보가 들어있는 aux로 실행돼 실질적으로 메모리에 적재되게 된다.
static bool lazy_load_segment(struct page *page, void *aux)
{
struct binary_aux *binary_aux = aux;
uint8_t kpage = page->frame->kva;
struct file *file = binary_aux->file;
uint32_t page_read_bytes = binary_aux->read_bytes;
uint32_t page_zero_bytes = binary_aux->zero_bytes;
if (file_read_at(file, kpage, page_read_bytes, binary_aux->ofs) != (int)page_read_bytes)
{
palloc_free_page(kpage);
free(aux);
return false;
}
memset(kpage + page_read_bytes, 0, page_zero_bytes);
}
지금까지 lazy_loading이 어떻게 이루어졌는지 알아봤다.