말감로그

크래프톤 정글 WEEK10 DAY 82 - PintOS Project3 - Anonymous Page 본문

Krafton jungle

크래프톤 정글 WEEK10 DAY 82 - PintOS Project3 - Anonymous Page

habbn 2024. 3. 30. 00:14
728x90

 

load_segment 흐름

load_segment (메모리만 로드) - > setupstack( 페이지 요구하고 매핑) -> page_fault (커널 제어권 획득) -> vm_try_handler (페이지 물리 프레임 매핑 ) -> lazy load segmet ( 프레임에 세그먼트 할당 )

 

🤔궁금했던 점

왜 load_segment 부분에서 페이지를 load할 때  uninit 페이지 생성하지 않고 anonymous 페이지로 생성하는가?

 

load_segment를 보면, vm_alloc_page_with_initializer를 호출할 때, 타입 인자를 VM_ANON으로 넘겨서, 모든 uninit 페이지가 page fault 이후 Anonymous로 생성되게 만들어놓은 것을 볼 수 있다.

 

왜 filed_backed가 아니라 Anonymous 페이지로 할당받도록 한 것일까라는 의문이 들었다.

 

첫 번째 가설은 페이지 폴트가 일어나면 그때 커널이 페이지 폴트 핸들러에 의해 물리 프레임 하나를 0으로 초기화를 시켜둔다. 그래서 lazy_loading이 나면 그때 필요한 데이터가 실제로 사용될 때 메모리에 로드되니까 anonymous 페이지로 초기화된 페이지를 만들어둔 다음 메모리에 로드시킬 수 있도록 하는 것이다.  

 

두 번째 가설(검색)은 우선 load_segment는 ELF 형식으로 지정된 응용프로그램을 로드하는데 사용되는데, 현재 로드하는 섹션이 .text인지, .bss인지, .data인지 알기 위해서는 추가적인 작업이 필요하다. 그래서 운영체제의 입장에서, 현재 로드하고 있는 섹션이 무엇인지를 정확히 구분하는 이점이 없다. 그래서 굳이 각 섹션의 목적에 따라 페이지의 종류를 다르게 할당 받을 필요가 없다

 

file_backed 페이지는, 해당 영역에 데이터를 쓰면 파일에 자동으로 반영되는 페이지다. 응용 프로그램으로부터 .data 같은 영역을 불러와서, 실행 중에 해당 영역의 값이 바뀐다고 하더라도, 실제 파일을 바꾸지는 않을 것이므로, 아직까지는 file_backed를 사용하지 않는다.

 

swap in/out 시에도 실행 중 변경된 값을 그대로 복원하기를 원하기에, swap disk를 사용하는 anonymous 페이지를 사용한다.

 

 

💥Trouble Shooting

1. load_segment()  aux 설정

 

load_segment() 함수에서 lazy_load_segment() 함수로 전달할 데이터를(aux) 설정하라고 한다.

그래서 lazy_load_segment() 함수로 전달할 인자들을 aux에 담아 전달해야하는데, aux를 이중 포인터로 선언해서 전달하고 접근하려고 했다.

void **aux = (file, &page_read_bytes, &page_zero_bytes, &ofs);

 

lazy_load_segment() 함수에서는 해당 구조체로 변환해서 인덱스로 접근하였다.

void **aux_ = (void **)aux;
struct file *file = ((struct file **)aux_)[0];
size_t *page_read_bytes = ((size_t *)aux_)[1];
size_t *page_zero_bytes = ((size_t *)aux_)[2];
off_t *ofs = ((off_t *)aux_)[3];

 

그러나 이렇게 한 결과 파일도 안 찾아지고 파일도 읽어지지 않았다. aux에 값들이 제대로 전달이 안되어서 그런 것 같다는 생각이 들었다. 

 

그래서 loading을 위해 필요한 정보를 포함하는 구조체를 만들어서 vm_alloc_page_with_initializer 함수의 마지막 인자 aux로 해당 구조체를 전달해주었다.

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);

	// printf("load_segment \n");

	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. */
		/* 이 페이지를 채우는 방법을 계산하세요.
		   FILE에서 PAGE_READ_BYTES 바이트를 읽고
		   최종 PAGE_ZERO_BYTES 바이트를 0으로 합니다. */
		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. */
		/* lazy_load_segment에 정보를 전달하도록 aux를 설정합니다.*/

		struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)malloc(sizeof(struct lazy_load_arg));
		lazy_load_arg->file = file;
		lazy_load_arg->ofs = ofs;
		lazy_load_arg->read_bytes = page_read_bytes;
		lazy_load_arg->zero_bytes = page_zero_bytes;

		if (!vm_alloc_page_with_initializer (VM_ANON, upage,
					writable, lazy_load_segment, lazy_load_arg))
			return false;

		/* Advance. */
		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		upage += PGSIZE;
		ofs += page_read_bytes;
	}
	return true;
}

 

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. */
	/* 1. 파일에서 세그먼트를 로드합니다.
	   2. 주소 VA에서 첫 번째 페이지 오류가 발생하면 호출됩니다. 
	   3. 이 함수를 호출할 때 VA를 사용할 수 있다.*/

	struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)aux;
	file_seek(lazy_load_arg->file, lazy_load_arg->ofs);

	if(file_read(lazy_load_arg->file, page->frame->kva, lazy_load_arg->read_bytes)!= (int)(lazy_load_arg->read_bytes)){
			palloc_free_page(page->frame->kva);
			return false;
	}

	memset(page->frame->kva + lazy_load_arg->read_bytes , 0 , lazy_load_arg->zero_bytes);

	return true;
}

 

 

2. Page fault at 0: not present error reading page in kernel context.

 

커널이 가상 주소 0에 액세스하여 페이지 폴트가 발생했다는 page fault가 발생하였다.

-> 주로 커널 코드가 유효하지 않은 주소를 참조하려고 시도할 때 발생한다.

 

이를 해결하기 위해서는 vm_try_handle_fault 함수에서 주어진 주소가 커널 영역에 속한다면, 이는 사용자 영역이 아닌 커널 영역에 접근하려는 시도이므로 처리를 중단하고 false를 반환해야한다.

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;
	/* TODO: Validate the fault */
	/* TODO: Your code goes here */
	
    	//커널 영역이면 false
	if(addr == NULL || (is_kernel_vaddr(addr))){
		return false;
	}

	if(not_present){
		page = spt_find_page(spt,addr);
		if(page == NULL){
			return false;
		}
			
		return vm_do_claim_page(page);
	}

	return false;
}

 

 

3. Page fault at 0x400c7f: not present error reading page in user context.

 

위의 문제를 해결해줬더니 뜬 에러다..

fail 에러 문구를 보면 저 prinft( Page fault at 0x400c7f: not present error reading page in user context. )  문이 세상 밖으로 나오면 안된다는 것인데 나왔다는 것은 기존의 exit(-1) 부분의 위치가 밑에 있다는 것이므로 위치만 옮겨주면 된다.

static void
page_fault (struct intr_frame *f) {
	bool not_present;  /* True: not-present page, false: writing r/o page. */
	bool write;        /* True: access was write, false: access was read. */
	bool user;         /* True: access by user, false: access by kernel. */
	void *fault_addr;  /* Fault address. */

	/* Obtain faulting address, the virtual address that was
	   accessed to cause the fault.  It may point to code or to
	   data.  It is not necessarily the address of the instruction
	   that caused the fault (that's f->rip). */

	fault_addr = (void *) rcr2();

	/* Turn interrupts back on (they were only off so that we could
	   be assured of reading CR2 before it changed). */
	intr_enable ();


	/* Determine cause. */
	not_present = (f->error_code & PF_P) == 0;
	write = (f->error_code & PF_W) != 0;
	user = (f->error_code & PF_U) != 0;
    	//기존 exit 위치
   	//exit(-1);

#ifdef VM
	/* For project 3 and later. */
	if (vm_try_handle_fault (f, fault_addr, user, write, not_present))
		return;
#endif
	// 페이지 폴트시 -1 종료 처리
	exit(-1);

	/* Count page faults. */
	page_fault_cnt++;

	/* If the fault is true fault, show info and exit. */
	printf ("Page fault at %p: %s error %s page in %s context.\n",
			fault_addr,
			not_present ? "not present" : "rights violation",
			write ? "writing" : "reading",
			user ? "user" : "kernel");
	kill (f);
}

 

 

이제 fork() test case를 성공시키기 위해서 supplemental_page_table_copy() 함수를 완성시켜야 한다.

로직 접근은 했는데 어떻게 해야할지 잘 모르겠어서 블로그 보면서 참고하였다.

src 부모 프로세스의 있는 정보들을 dst에 복사하는 함수다.

bool
supplemental_page_table_copy (struct supplemental_page_table *dst UNUSED,
		struct supplemental_page_table *src UNUSED) {
	
	struct hash_iterator i;
	hash_first(&i, &src->hash_table);

	while(hash_next(&i)){
		struct page *src_page = hash_entry(hash_cur(&i), struct page , hash_elem);
		enum vm_type vm_type = src_page->operations->type;
		void *va = src_page->va;
		bool writable = src_page->writable;

		if(vm_type == VM_UNINIT){
			//uninit page 생성 & 초기화
			vm_alloc_page_with_initializer(VM_ANON, va, writable, src_page->uninit.init, src_page->uninit.aux);
			continue;
		}

		if(!vm_alloc_page(vm_type, va, writable)){	//uninit page 생성 & 초기화
			//init이랑 aux는 Lazy Loading에 필요함
			//지금 만드는 페이지는 기다리지 않고 바로 내용을 넣어줄 것이므로 필요 없음
			return false;
		}

		if(!vm_claim_page(va)){
			return false;
		}

		struct page *dst_page = spt_find_page(dst, va);
		//부모 페이지의 물리 메모리 정보를 자식에게도 복사
		memcpy(dst_page->frame->kva, src_page->frame->kva , PGSIZE);
	}
	return true;
}

 

 

4. hash_destroy() - > run timeout!

 

hash_destroy() 함수를 사용하면 run timeout이 나는 이상한 에러를 발견했다.

처음엔 코드의 문제인가 싶어 하나하나 다 봤는데 hash_destroy() 함수로 인해서였다.

 

왜?

hash_destroy 함수를 사용하면 hash가 사용하던 메모리(hash->bucket) 자체도 반환된다.

process가 실행될 때 hash table을 생성한 이후에 process_cleanup()이 호출되는데, 이때는 hash table은 남겨두고 안의 요소들만 제거되어야 한다.

hash table까지 지워버리면 만들자마자 지워버리는게 되므로 process가 실행될 때 빈 hash table이 있어야 해서 hash table은 남겨두고 안의 요소들만 지워야 한다

 

그래서 hash_destroy() 대신 hash_clear() 함수를 사용하였다. 

void
supplemental_page_table_kill (struct supplemental_page_table *spt UNUSED) {
	/* TODO: Destroy all the supplemental_page_table hold by thread and
	 * TODO: writeback all the modified contents to the storage. */
	/* 스레드에 의해 보유된 모든 보조 페이지 테이블을 파괴하고
	 * 변경된 모든 내용을 저장소에 기록하세요. */

	hash_clear(&spt->hash_table, page_destroy);
}

 

 

5. fork() 와 exec() - read, close 에러

 

 read 와 close 에서 테스트가 fail이 떠서 이것은 시스템 콜의 문제이다 싶어 뜯어보다. check_address의 문제라는 걸 알게 되었다.

 

project 2의 시스템 콜을 구현하면서 유효성 검사를 해주었는데 그 함수 부분에서 pml4에 해당 주소의 페이지의 유효성 검사를 해주는 예외처리가 있었다. 우리는 지금 lazy_loading을 구현을 하는 중이다보니 lazy_loading은 spt에는 해당 주소의 페이지가 있으나 아직 물리 메모리와 매핑이 되어 있지 않은 상태이므로 pml4에는 없는 상태가 된다.

그러다 보니 저 예외처리에서 걸려서 exit(-1)의 출력을 하는 것이었다.

 

그래서 저 부분을 주석처리해주고, spt_find_page() 함수를 사용하여 spt에 있는지 없는지를 확인하였다.

void check_address(uintptr_t addr) {
	if (addr == NULL) {
		exit(-1);
	}
	if (!is_user_vaddr(addr)) {
		exit(-1);
	}
	
	// if (pml4_get_page(thread_current()->pml4, (void *)addr) == NULL) {
	// 	exit(-1);
	// }

	if(spt_find_page(&thread_current()->spt, addr) == NULL){
		exit(-1);
	}

	if (KERN_BASE < addr || addr < 0) {
		exit(-1);
	}

	if (KERN_BASE < addr + 8 || addr + 8 < 0) {
		exit(-1);
	}
}

 

 

현재까지 테스트 케이스는 38 fail이당

728x90