3일이나 걸려 mmap, munmap 구현이 끝났다.
먼저 mmap을 살펴보자.
void *mmap (void *addr, size_t length, int writable, int fd, off_t offset) {
if (is_kernel_vaddr(addr) || addr == NULL)
return NULL;
if (is_kernel_vaddr(addr + length) || (addr + length) == NULL)
return NULL;
if (addr != pg_round_down(addr))
return NULL;
if (length == 0)
return NULL;
if (offset % PGSIZE)
return NULL;
struct file *file = process_get_file(fd);
if (file == NULL)
return NULL;
return do_mmap(addr, length, writable, file, offset);
}
예외처리를 잘 해주어야 mmap test case중 예외사항에 관한 test를 통과할 수 있다.
현재 프로세스에서 전달받은 fd를 가지고 file을 찾아 do_mmap에 넘겨준다.
do_mmap은 아래와 같다.
void *do_mmap (void *addr, size_t length, int writable, struct file *f, off_t offset) {
uint64_t va = addr;
struct file *file = file_reopen(f);
int pg_cnt = DIV_ROUND_UP(length, PGSIZE);
size_t file_len = file_length(file);
size_t read_bytes = file_len < length ? file_len : length;
while (true) {
pg_cnt--;
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
struct file_segment *file_segment = malloc(sizeof(struct file_segment));
// file_segment->file = malloc(sizeof(struct file));
// memcpy(file_segment->file, file, sizeof(struct file));
file_segment->file = file;
file_segment->page_read_bytes = page_read_bytes;
file_segment->page_zero_bytes = page_zero_bytes;
file_segment->ofs = offset;
if (!vm_alloc_page_with_initializer(VM_FILE, va, writable, lazy_load_segment, file_segment)) {
return NULL;
}
struct page *page = spt_find_page(&thread_current()->spt, va);
page->marker = VM_DUMMY;
if(pg_cnt == 0) {
page->marker = VM_FILE_END;
break;
}
read_bytes -= page_read_bytes;
offset += page_read_bytes;
va += PGSIZE;
}
return addr;
}
아직도 length가 정확하게 무엇을 의미하는지는 모르겠으나 실제 file의 길이보다 짧거나 길수도 있다.
최대한 기존의 lazy_load_segment를 변경하지 않기 위해서 고민을 많이 했다.
length가 8KB인 반면에 실제 file의 길이가 PGSIZE보다 작다면 위의 코드에 따르면 pg_cnt = 2로 2개의 page를 만들게 된다. 하지만 이러한 경우 2번째 page는 page_fault가 났을 때 0으로만 채워진다. 즉, 불필요한 page이다.
위와 같은 상황에서 lazy_load_segment의 변경없이 1개의 page만 만드는 방법이 도저히 생각이 나지 않아 일단은 입력받은 length에 따라 만들 page의 갯수를 정하기로 하였다.
file_reopen을 하지 않으면 test case가 통과 되지 않았다.
-> 정확히 모르겠음
기존의 process.c의 load_segment() 함수도 마찬가지인데 항상 file_segment->file도 malloc으로 공간을 할당받아 원래의 file을 memcpy하여 저장해주었는데 직접 file을 넣어줘도 되길래 주석처리를 하였다.
전체적인 do_mmap의 기능은 length를 통해 만들 page의 갯수를 정하고, file을 어디에서부터 얼마나 읽을지에 대한 정보를 file_segment에 담아 vm_alloc_page_with_initializer()를 호출한다. 마지막 page라면 page 구조체의 marker 변수에 VM_FILE_END를 담는다.
vm_alloc_page_with_initializer()를 호출하였을 때를 살펴보자.
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;
/* Project 3 */
/* 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. */
struct page *page = malloc(sizeof(struct page));//palloc_get_page(PAL_ASSERT);
void *new_initializer;
switch (VM_TYPE(type)) {
case VM_ANON:
new_initializer = anon_initializer;
break;
case VM_FILE:
new_initializer = file_backed_initializer;
break;
}
uninit_new(page, upage, init, type, aux, new_initializer);
page->writable = writable;
return spt_insert_page(spt, page);
}
err:
return false;
}
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,
}
};
}
type에 따라 initializer가 바뀐다. uninit_new가 호출이 되면 operations에 uninit_ops의 주소가 담기고, uninit_page의 멤버 변수가 초기화 된다.
init에는 lazy_load_segment(), aux에는 file_segment*, initializer에는 타입별 initializer가 담긴다.
처음에는 헷갈렸다. 분명 VM_ANON과 VM_FILE을 type으로 넘겼지만 operations에도 uninit의 것이 담기고, union에서도 uninit page를 선택한다. 근데 또 uninit_page인데 멤버 변수에는 type에 따라 적절한 정보가 담긴다.
이러한 상태에서 page를 참조하려 하면 page_fault가 발생한다. page를 참조하려 한다는 것은 page가 물리 메모리와 맵핑이 되어 있어야 한다는 것이다.
순서는 이렇다.
-> page_fault -> vm_try_handle_fault -> vm_do_claim_page -> vm_get_frame
-> pml4_set_page -> swap_in -> operations.swap_in -> uninit_initialize
-> initializer -> lazy_load_segment
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 = NULL;
if (addr == NULL)
return false;
else if (is_kernel_vaddr(addr))
return false;
else if (USER_STACK >= addr && addr >= USER_STACK - (1 << 20)) {
if (f->rsp - 8 != addr)
return false;
vm_stack_growth(addr);
return true;
}
else if (not_present) {
page = spt_find_page(&spt->spt_hash, addr);
if (page == NULL)
return false;
return vm_do_claim_page(page);
}
return false;
}
static bool vm_do_claim_page(struct page *page) {
struct frame *frame = vm_get_frame();
/* Set links */
frame->page = page;
page->frame = frame;
/* Project 3. */
/* TODO: Insert page table entry to map page's VA to frame's PA. */
uint64_t* pml4 = thread_current()->pml4;
// 가상 주소와 물리 주소를 맵핑한 정보를 페이지 테이블에 추가한다.
pml4_set_page(pml4, page->va, frame->kva, page->writable);
// #define swap_in(page, v) (page)->operations->swap_in ((page), v)
return swap_in(page, frame->kva);
}
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);
}
bool lazy_load_segment(struct page *page, void *aux) {
struct file_segment *file_segment = (struct file_segment *)aux;
struct file *file = file_segment->file;
size_t page_read_bytes = file_segment->page_read_bytes;
size_t page_zero_bytes = file_segment->page_zero_bytes;
off_t ofs = file_segment->ofs;
void *kpage = page->frame->kva;
if (kpage == NULL)
return false;
file_seek(file, ofs);
if (file_read(file, kpage, page_read_bytes) != (int)page_read_bytes) {
palloc_free_page(kpage);
return false;
}
memset((uint64_t)kpage + page_read_bytes, 0, page_zero_bytes);
return true;
}
page를 참조하려는데 page_fault가 났다면 해당 page를 찾아서 물리 메모리에 올려주면 된다.
물리 메모리에 올려주는 작업이 바로 vm_get_frame과 pml4_set_page이다.
vm_get_frame에서는 malloc으로 size만큼 kernel pool에서 메모리를 할당받아온다. 이어서 palloc_get_page로 실질적인 물리 페이지를 user pool에서 할당 받는다.
pml4_set_page에서는 스택 영역의 upage와 커널 영역 user pool의 kpage를 맵핑을 해준다. 해당 함수를 정상적으로 마친다면 어떤 va가 들어왔을 때 va가 포함되는 upage를 찾고 이 upage를 가지고 table을 walk하며 연결되어 있는 kpage를 찾을 수 있다. kpage를 찾으면 va의 하위 12비트(오프셋)을 더하여 실제 읽으려 했던 위치를 찾아간다.
vm_try_handle_fault에서는 사용자 영역에 대해서만 처리를 해주어야 한다.
그러므로 전달받은 주소가 커널 영역이라면 return false로 예외처리를 해주어야한다. 그렇지 않으면 물리메모리에 정보를 변경하거나 입력하려 할 때 무한 page_fault에 갇혀버린다.
swap_in이 호출되고 uninit_initialize를 거치면 type에 따라 initializer가 호출된다.
initializer가 호출되면 union의 uninit_page에서 type_page로 덮어씌운다.
operations도 자기 type걸로 바뀐다.
-> union 공용체의 특성인듯한데 uninit_page에서 type_page로 덮는 순간 uninit_page에 담았었던 정보들은 변질될 가능성이 매우크다.
init 즉, lazy_load_segment 함수를 전달 받았다면 호출하고, 전달 받지 않았다면 true를 반환한다.
lazy_load_segment 함수는 전달 받은 aux 즉, file_segment에 따라 file을 읽어 물리 메모리에 적재한다. 읽으려는 file의 크기가 PGSIZE보다 작다면 남은 공간은 0으로 채운다.
-> Project 2까지는 page를 할당하는 순간 바로 물리 메모리에 올라갔지만, Project 3부터는 처음에는 page를 할당만 하고, 이후에 page_fault가 났을 때 물리 메모리에 올린다. 이 역할을 하는 함수가 lazy_load_segment 함수이다.
이제 mummap을 살펴보자.
void munmap (void *addr) {
do_munmap(addr);
}
void do_munmap (void *addr) {
while (true) {
struct page *page = spt_find_page(&thread_current()->spt, addr);
if (page->marker & VM_FILE_END) {
destroy(page);
break;
}
else {
destroy(page);
}
addr += PGSIZE;
}
}
별거 없다. 들어온 주소에 해당하는 page를 찾고 destroy만 호출해주면 된다.
여기서 들어오는 addr은 do_mmap에서 반환한 주소로 같은 file에 대해 맵핑한 page들 중 가장 처음 page의 주소가 들어온다.
종료조건으로는 do_mmap에서 마지막 page의 marker에 넣어줬던 VM_FILE_END를 이용하였다.
destroy를 호출하게 되면 현재 page의 type에 따른 destroy 함수가 실행된다.
static void uninit_destroy (struct page *page) {
struct uninit_page *uninit = &page->uninit;
struct file_segment *file_segment = (struct file_segment *)uninit->aux;
hash_delete(&thread_current()->spt.spt_hash, &page->h_elem);
pml4_clear_page(thread_current()->pml4, page->va);
}
static void file_backed_destroy (struct page *page) {
struct file_page *file_page = &page->file;
if (pml4_is_dirty(thread_current()->pml4, page->va)) {
file_write_at(file_page->file, page->frame->kva, file_page->page_read_bytes, file_page->ofs);
pml4_set_dirty(thread_current()->pml4, page->va, 0);
}
hash_delete(&thread_current()->spt.spt_hash, &page->h_elem);
pml4_clear_page(thread_current()->pml4, page->va);
}
static void anon_destroy (struct page *page) {
struct anon_page *anon_page = &page->anon;
}
아마 page를 만들고 참조하지 않아서 page_fault가 한번도 나지 않은 상태에서 destroy를 호출하게 되면 initializer가 호출된 적이 없어서 operations가 uninit의 것이라 uninit_destroy가 호출될 것이다.
page_fault가 나서 initializer가 호출된 적이 있다면 type에 file_backed_destroy 또는 anon_destroy가 호출될 것이다.
이 함수에서는 page talbe의 정보를 지운다. 즉, 물리메모리와의 맵핑을 끊고 현재 프로세스의 spt에서 해당 page를 제거 해준다.
만약 VM_FILE 이었다면 물리메모리의 정보가 변경된 적이 있으면 file에 덮어 씌우고 dirty를 다시 0으로 초기화 해준다.
- file을 굳이 malloc으로 공간을 할당받고 memcpy로 가져와야 할까
- file_segment, kpage, frame은 도대체 언제 해제 해줘야하는가
- file_reopen이 필요한 이유
- supplemental_page_table_copy에서 VM_FILE의 경우 file_segment를 만들어서 넘겨야 안터지는 이유
bool supplemental_page_table_copy (struct supplemental_page_table *dst, struct supplemental_page_table *src) {
struct hash_iterator i;
hash_first (&i, &src->spt_hash);
while (hash_next (&i)) {
struct page *parent_page = hash_entry(hash_cur(&i), struct page, h_elem);
enum vm_type type = parent_page->operations->type;
switch (VM_TYPE(type)) {
case VM_UNINIT: {
if(!vm_alloc_page_with_initializer(VM_ANON, parent_page->va, parent_page->writable, parent_page->uninit.init, parent_page->uninit.aux)) {
return false;
}
break;
}
case VM_ANON: {
if (!vm_alloc_page(type, parent_page->va, parent_page->writable)) {
return false;
}
if (!vm_claim_page(parent_page->va)) {
return false;
}
struct page *child_page = spt_find_page(dst, parent_page->va);
memcpy (child_page->frame->kva, parent_page->frame->kva, PGSIZE);
break;
}
case VM_FILE: {
struct file_segment *file_segment = malloc(sizeof(struct file_segment));
file_segment->file = parent_page->file.file;
file_segment->page_read_bytes = parent_page->file.page_read_bytes;
file_segment->page_zero_bytes = parent_page->file.page_zero_bytes;
file_segment->ofs = parent_page->file.page_zero_bytes;
if (!vm_alloc_page_with_initializer(type, parent_page->va, parent_page->writable, NULL, file_segment)) {
return false;
}
if (!vm_claim_page(parent_page->va)) {
return false;
}
struct page *child_page = spt_find_page(dst, parent_page->va);
memcpy (child_page->frame->kva, parent_page->frame->kva, PGSIZE);
break;
}
}
}
return true;
}