[pintos] project 3 - VM

bang·2023년 5월 16일
0

어느덧 핀토스도 프로젝트 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이 실질적으로 이루어지는 부분을 알아보겠다.

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의 initaux를 뽑아 마지막에 실행시켜주는 부분을 확인할 수 있는데 여기서 우리가 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이 어떻게 이루어졌는지 알아봤다.

0개의 댓글