Project_3 : Virtual Memory - Anonymous Page(2)
맨 처음 페이지를 만들면 해당 페이지는 uninit 상태의 페이지이다.
다시 말해, 페이지의 타입 지정이 anon 혹은 file-backed일 수 있지만 아직은 해당 타입으로 초기화되지 않은 상태이다.
추후에 페이지 폴트가 발생했을 때, 타입에 맞는 초기화 함수를 호출함으로써 페이지의 상태가 변한다.
vm_alloc_page_with_initializer( ) 함수는 uninit 상태의 페이지를 만들어 spt에 해당 페이지를 삽입하는 함수이다.
해당 함수는 커널이 새로운 페이지 요청을 수신할 때 호출된다. 우선, malloc( ) 함수를 이용하여 페이지를 할당받는다.
그리고 입력 받은 type 인자에 맞추어 initializer를 초기화 한다.
이후, uninit_new( ) 함수를 호출하여 최초 인자로 받은 정보들을 새로운 page 구조체의 멤버들로 채워준다.
해당 과정을 통해 새로운 uninit 페이지가 만들어지며, 이를 spt에 삽입한다.
/* vm/vm.c */
bool vm_alloc_page_with_initializer(enum vm_type type, void *upage, bool writable, vm_initializer *init, void *aux){
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. */
/*--------------- PROJECT3: Virtual Memory ----------------*/
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;
}
uninit_new(page, upage, init, type, aux, initializer);
page->writable = writable;
/*---------------------------------------------------------*/
/* TODO: Insert the page into the spt. */
/*--------------- PROJECT3: Virtual Memory ----------------*/
spt_insert_page(spt, page);
/*---------------------------------------------------------*/
}
err:
return false;
}
앞서 언급하였듯 모든 페이지들은 만들어질 때 uninit 페이지로 만들어진다. 그리고 해당 페이지에 처음 접근 시 페이지 폴트가 발생한다.
페이지 폴트 핸들러는 호출 체인을 따르는데, swap_in을 호출할 때 아래에서 알 수 있듯이 uninit_initialize( ) 함수에 도달한다.
/* vm/uninit.c */
static const struct page_operations uninit_ops = {
.swap_in = uninit_initialize,
.swap_out = NULL,
.destroy = uninit_destroy,
.type = VM_UNINIT,
};
uninit_initialize( ) 함수에서는 uninit 페이지를 초기화하며, 각 타입에 맞게 page_initializer가 설정된다.
/* vm/uninit.c */
/* Initalize the page on first fault */
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);
}
이제 본격적으로 Anonymous Page를 구현하기 위해 anon_page 타입과 관련한 함수들을 수정해주어야 한다.
우선, 비어있는 anon_page 구조체에 아래와 같이 swap_index 변수를 추가해주자.
/* include/vm/anon.h */
struct anon_page {
/*--------------- PROJECT3: Virtual Memory ----------------*/
int swap_index;
/*---------------------------------------------------------*/
};
그리고 uninit 페이지에 접근해 페이지 폴트가 발생했을 때, anon_type을 초기화하는 anon_initializer( ) 함수를 수정하도록 하자.
여기서 중요한 점은 anon_type의 경우 물리 메모리에서 받아와 초기화 할 때 모든 데이터를 Zeroing 해주어야 한다는 것이다.
/* vm/anon.c */
bool anon_initializer (struct page *page, enum vm_type type, void *kva) {
/*--------------- PROJECT3: Virtual Memory ----------------*/
struct uninit_page *uninit = &page->uninit;
memset(uninit, 0, sizeof(struct uninit_page));
/* Set up the handler */
page->operations = &anon_ops;
struct anon_page *anon_page = &page->anon;
anon_page->swap_index = -1;
return true;
/*---------------------------------------------------------*/
}
현재까지 Pintos는 디스크에서 실행시킬 실행 파일 전체를 물리 메모리에 올려놓고, 페이지와 물리 메모리 프레임을 매핑하는 방식이었다.
이제는 spt에 프로세스의 각 페이지와 관련된 정보를 저장해 놓고, 프로세스가 가상 주소에 접근했을 때 매핑이 되어 있지 않으면,
그 때 spt에서 해당 메모리와 관련한 정보를 가져와 물리 프레임에 매핑하는 방식으로 변환하고자 한다.
이를 Lazy Loading이라고 하며, 관련하여 여러 가지 구조체와 함수들을 수정해야 한다.
우선, Lazy Loading을 위한 정보 구조체이다. 이 안에 해당 페이지에 대응되는 파일 정보들이 들어가 있다.
이 구조체를 통해서 페이지 폴트가 발생한 후 디스크에서 파일을 불러올 때 필요한 파일 정보를 알 수 있다.
/* include/userprog/process.h */
/*--------------- PROJECT3: Virtual Memory ----------------*/
struct container {
struct file *file;
off_t offset; // 읽어야 할 파일 오프셋
size_t page_read_bytes; // 가상 페이지에 쓰여져 있는 데이터 크기
};
/*---------------------------------------------------------*/
uninit 페이지에 파일 정보를 담기 위해서 load_segment( ) 함수를 수정하자.
페이지 폴트 핸들러가 페이지 타입 별 initializer를 호출할 때, lazy_load_segment( ) 함수도 호출하여,
페이지 안의 정보들을 바탕으로 디스크에서 파일을 읽어오면서 물리 메모리에 Lazy Loading이 가능하도록 만들어야 한다.
우선, 위에서 구현한 container 구조체에 file에 있는 정보들 중 페이지 폴트 핸들러가 참고할 정보들을 넣는다.
이후, vm_alloc_page_with_initializer( ) 함수 호출 시 aux 인자로 넣어준다.
그리고 vm_alloc_page_with_initializer( ) 함수의 인자로 lazy_load_segment( ) 함수를 넣는다.
/* userprog/process.c */
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);
/* upage 주소부터 1페이지 단위씩 UNINIT 페이지를 만들어 프로세스의 spt에 넣는다(vm_alloc_page_with_initializer).
이 때 각 페이지의 타입에 맞게 initializer도 맞춰준다. */
while (read_bytes > 0 || zero_bytes > 0) {
/* 1 Page보다 같거나 작은 메모리를 한 단위로 해서 읽어 온다.
페이지보다 작은 메모리를 읽어올때 (페이지 - 메모리) 공간을 0으로 만들 것이다. */
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
/* 새 UNINIT 페이지를 만들어서 현재 프로세스의 spt에 넣는다.
페이지에 해당하는 파일의 정보들을 container 구조체에 담아서 AUX로 넘겨준다.
타입에 맞게 initializer를 설정해준다. */
struct container *container = (struct container *)malloc(sizeof(struct container));
container->file = file;
container->page_read_bytes = page_read_bytes;
container->offset = ofs;
if (!vm_alloc_page_with_initializer (VM_ANON, upage,
writable, lazy_load_segment, container))
return false;
// page fault가 호출되면 페이지가 타입별로 초기화되고 lazy_load_segment()가 실행된다.
/* Advance. */
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
upage += PGSIZE;
ofs += page_read_bytes;
}
return true;
}
uninit_initialize( ) 함수가 호출되어 페이지 타입에 맞는 페이지 초기화를 해줄 때, lazy_load_segment( ) 함수가 실행된다.
이때 가상 페이지와 관련된 파일의 offset부터 page_read_bytes까지의 데이터를 읽어와 페이지와 매핑된 물리 메모리에 작성한다.
즉, 페이지에 있는 파일 정보대로 물리 메모리에 load 한다.
/* userprog/process.c */
static bool lazy_load_segment (struct page *page, void *aux) {
/* TODO: Load the segment from the file */
/* TODO: This called when the first page fault occurs on address VA. */
/* TODO: VA is available when calling this function. */
/*--------------- PROJECT3: Virtual Memory ----------------*/
struct file *file = ((struct container *)aux)->file;
off_t offsetof = ((struct container *)aux)->offset;
size_t page_read_bytes = ((struct container *)aux)->page_read_bytes;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
file_seek(file, offsetof);
if (file_read(file, page->frame->kva, page_read_bytes) != (int)page_read_bytes) {
palloc_free_page(page->frame->kva);
return false;
}
memset(page->frame->kva + page_read_bytes, 0, page_zero_bytes);
return true;
/*---------------------------------------------------------*/
}
스택은 디스크에서 파일을 읽어올 필요가 없다. 따라서 lazy_load_segment( ) 함수를 호출하지 않아야 한다.
아래와 같이 setup_stack( ) 함수를 수정한다.
/* userprog/process.c */
static bool setup_stack (struct intr_frame *if_) {
bool success = false;
void *stack_bottom = (void *) (((uint8_t *) USER_STACK) - PGSIZE);
/* TODO: Map the stack on stack_bottom and claim the page immediately.
* TODO: If success, set the rsp accordingly.
* TODO: You should mark the page is stack. */
/* TODO: Your code goes here */
/*--------------- PROJECT3: Virtual Memory ----------------*/
if (vm_alloc_page(VM_ANON | VM_MARKER_0, stack_bottom, 1)) { // type, upage, writable
success = vm_claim_page(stack_bottom);
if (success) {
if_->rsp = USER_STACK;
thread_current()->stack_bottom = stack_bottom;
}
/*---------------------------------------------------------*/
return success;
}
thread 구조체에 스택을 위한 변수를 추가해준다.
struct thread {
// 중략
#ifdef VM
/* Table for whole virtual memory owned by thread. */
struct supplemental_page_table spt;
/*--------------- PROJECT3: Virtual Memory ----------------*/
void *stack_bottom;
void* rsp_stack;
/*---------------------------------------------------------*/
// 중략
}
Lazy Loading의 경우, 가상 주소가 페이지 테이블(pml4)에 존재하지 않기 때문에 시스템 콜 호출 시 오류가 발생할 수 있다.
특히, 가상 주소를 확인하는 check_address( ) 함수의 경우가 문제가 된다.
따라서 입력 받은 가상 주소를 spt에서 찾을 수 있도록 아래와 같이 함수를 수정해주어야 한다.
/* userporg/syscall.c */
/*--------------- PROJECT3: Virtual Memory ----------------*/
struct page * check_address(void *addr) {
if (is_kernel_vaddr(addr)) {
exit(-1);
}
return spt_find_page(&thread_current()->spt, addr);
}
/*---------------------------------------------------------*/
일부 시스템 콜의 경우 파일 이름의 주소를 받는 대신 버퍼의 주소를 받는다.
따라서 해당 주소가 포함된 페이지가 spt에 없거나 쓰기가 허용되지 않은 페이지의 경우 프로세스를 종료하도록 새로운 함수를 추가한다.
/* userporg/syscall.c */
/*--------------- PROJECT3: Virtual Memory ----------------*/
void check_valid_buffer(void* buffer, unsigned size, void* rsp, bool to_write) {
for (int i = 0; i < size; i++) {
struct page* page = check_address(buffer + i); // 인자로 받은 buffer부터 buffer + size까지의 크기가 한 페이지의 크기를 넘을수도 있음
if(page == NULL)
exit(-1);
if(to_write == true && page->writable == false)
exit(-1);
}
}
/*---------------------------------------------------------*/