[Pintos] Project3 Memory Mapped Files 6/22~6/24

UBIN·2023년 6월 24일
1

3일이나 걸려 mmap, munmap 구현이 끝났다.

do_mmap

먼저 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_fault 처리

이러한 상태에서 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 함수이다.

do_mummap

이제 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으로 초기화 해준다.

미해결

  1. file을 굳이 malloc으로 공간을 할당받고 memcpy로 가져와야 할까
  2. file_segment, kpage, frame은 도대체 언제 해제 해줘야하는가
  3. file_reopen이 필요한 이유
  4. 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;
}
profile
ubin

0개의 댓글