PintOS 3주차 Anonymous Page에 대해서 정리해봄

Designated Hitter Jack·2023년 10월 17일
0

SW 사관학교 정글

목록 보기
28/44


ㄴㄱㄴㄱㄴㄱ?ㅇㅍㅇㅇㄷ

Anonymous Page

Anonymous Page는 무엇인가?

anonymous page는 file-backed page와 달리 contents를 가져올 file이나 device가 없는 page를 말한다. 이들은 프로세스 자체에서 런타임 때 만들어지고 사용된다. stack 과 heap 영역의 메모리들이 여기에 해당된다. 내용을 가져올 file이 없기 때문에 0으로 초기화 되어있다.

lazy-loading이란?

lazy-loading 은 메모리 로딩을 필요한 시점까지 미루는 디자인 패턴이다. 이를 구현하기 위해선 페이지를 할당할 때 해당 페이지에 대응하는 struct page까지만 만든 후 물리 공간에 연결하는 frame은 할당하지 않고, 당연히 실제 내용도 아직 load하지 않는다.

실제로 내용을 load하는 시점은 page fault가 발생하는 시점이다. Project2까지의 시점에서는 page fault == 고쳐야 할 error가 발생했음 이었지만 여기서부터는 lazy-loading을 실행해야 하는 신호로도 해석할 수 있는 것이다.

이런 디자인 패턴을 가짐으로써 얻는 장점은 프로세스가 필요한 페이지를 필요한 시점에 로드하므로 불필요한 메모리 사용을 줄일 수 있다는 것이다.

vm_init()

@vm/vm.c
/* Initializes the virtual memory subsystem by invoking each subsystem's
 * intialize codes. */
void
vm_init (void) {
	vm_anon_init ();
	vm_file_init ();
#ifdef EFILESYS  /* For project 4 */
	pagecache_init ();
#endif
	register_inspect_intr ();
	/* DO NOT MODIFY UPPER LINES. */
	/* TODO: Your code goes here. */
	list_init(&frame_table);
}

list_init() 함수로 frame_table을 init 해준다.

vm_alloc_page_with_initializer()

인자로 주어진 type에 맞춘 uninitialized page를 하나 만든다.
나중에 이 페이지에 대한 page fault가 뜨면 이 페이지는 주어진 vm_type(anonymous/file-backed)에 맞게 초기화될 것이다. 그 전까지는 UNINIT 타입으로 존재한다.

/* Create the pending page object with initializer. If you want to create a
 * page, do not create it directly and make it through this function or
 * `vm_alloc_page`. */
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. */
		
		//1. 빈 페이지를 생성
		struct page *page = (struct page *)malloc(sizeof(struct page));
		//2. type에 따라 초기화 함수를 가져온다.
		bool (*page_initializer)(struct page*, enum vm_type, void*);
		
		switch (VM_TYPE(type)) {
			case VM_ANON:
				page_initializer = anon_initializer;
				break;

			case VM_FILE:
				page_initializer = file_backed_initializer;
				break; 
			default:
				free(page);
				break;
		}

		//3. uninit_new를 통해 page를 초기화 해준다.
		uninit_new (page, upage, init, type, aux, page_initializer);

		page -> writable = writable; 

		//spt에 page를 넣어준다.
		if (!spt_insert_page(spt, page)) {
			return false;
		}

		return true;
	}

err:
	return false;
}

이 함수에서는 우선 빈 struct page를 할당하고, 타입에 따라 어떤 initializer 함수를 적용할지를 결정한다.
그 후 uninit_new 함수를 통해 page를 uninit 타입으로 초기화한다.
이후 spt에 page를 넣어주면 된다.

unitit_new()

@vm/uninit.c
* DO NOT MODIFY this function */
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, //operations -> swap_in : uninit_initialize
		.va = va,
		.frame = NULL, /* no frame for now */
		.uninit = (struct uninit_page) {
			.init = init, //load_segment 함수에서 vm_alloc_page_with_initializer 함수를 호출할 때 init 인자로 lazy load segment를 전달함
			.type = type,
			.aux = aux,
			.page_initializer = initializer, //anon_initializer | file_backed_initializer
		}
	};
}

uninit_new 함수에서는 매개변수로 받은 page 구조체를 uninit type으로 만든다. 수정하면 안 되는 함수여서 주석으로 어떤 일을 하는지 써놓기만 했다.
다음은 uninit_new()함수에서 load_segment() 함수와 lazy_load_segment() 함수를 불러오므로 이 둘을 수정해야 한다. project 2에서 다루었던 함수가 아니라 process.c의 아래에 있는 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);

    off_t read_start = ofs;

    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;
        //이 페이지에서 read_byte만큼 읽고 난 후 0으로 채워야 하는 byte 수
        size_t page_zero_bytes = PGSIZE - page_read_bytes;
        /* TODO: Set up aux to pass information to the lazy_load_segment. */

        struct lazy_load_aux *aux = (struct lazy_load_aux *)malloc(sizeof(struct lazy_load_aux));

        aux -> file = file;
        aux -> ofs = read_start;
        aux -> read_bytes = page_read_bytes;
        aux -> zero_bytes = page_zero_bytes;
        aux -> writable = writable;

        //vm_alloc_page_with_initializer의 4번째 인자가 load할 때 이용할 함수, 5번쨰 인자가 그 때 필요한 인자이다.
        if (!vm_alloc_page_with_initializer(VM_ANON, upage, writable, lazy_load_segment, aux))
            return false;

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

이 함수는 프로세스가 실행될 때 실행 파일을 현재 스레드로 로드하는 함수 load()에서 호출된다.
파일의 내용을 upage에 load하는 함수이다. 파일의 내용을 load하기 위해서 upage를 할당할 page가 필요한데, 이를 위에서 작성한 vm_alloc_page_with_initializer()를 호출해서 페이지를 생성한다.
기존의 project2 load_segment() 함수를 참고하여 작성하면 된다.
lazy loading을 사용해야 하므로 파일 내용을 바로 로드하지 않고 나중에 실제로 load할 때 사용할 함수와 인자들을 넣어줄 구조체가 필요한데 그게 aux이다.

lazy_load_segment()

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. */
    ASSERT(page -> frame -> kva != NULL);

    struct lazy_load_aux *aux_ = (struct lazy_load_aux *)aux;

    file_seek(aux_ -> file, aux_ -> ofs);

    
        /* 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 = aux_->read_bytes;
        size_t page_zero_bytes = aux_->zero_bytes;

        /* Get a page of memory. */
        if (page == NULL) {
            return false;
        }

        /* Load this page. */
        if (file_read(aux_-> 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;
}

앞에서 넘겨준 page와 aux를 받아서 실제로 물리 메모리 프레임에 내용을 로딩하는 함수이다.
이 함수 역시 기존의 load_segment()함수가 실제로 페이지를 load하는 내용을 참고하여 작성하였다.

setup_stack()

/* Create a PAGE of stack at the USER_STACK. Return true on success. */
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 */

    if (vm_alloc_page(VM_ANON|VM_MARKER_0, stack_bottom, 1)) {

        if(vm_claim_page(stack_bottom)){
            if_->rsp = USER_STACK;
            success = true;
        }
    }

    return success;
}

이 함수는 한개의 페이지 크기 만큼을 USER_STACK 주소에서 내려서 새로운 stack bottom 으로 설정해주는 함수이다.
vm_alloc_page()를 호출하여 바로 하나의 uninit 페이지를 만들고, 이후 해당 페이지에 곧바로 물리 프레임을 할당하여 anon type으로 설정한다.
해당 함수 호출이 성공했다면 if -> rsp를 USER_STACK으로 변경한다.

여기까지 완성했더니 fork를 제외한 project 2의 테스트들이 거의 통과했다.

supplemental_page_table_copy()

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 -> spt_hash);
   	while (hash_next (&i)) {
		struct page *src_page = hash_entry(hash_cur(&i), struct page, hash_elem); 
   		enum vm_type type = src_page -> operations -> type;
		void *upage = src_page -> va;
		bool writable = src_page -> writable;
		// type == uninit 이라면 복사하는 페이지도 uninit
		if (type == VM_UNINIT) {
			vm_initializer *init = src_page -> uninit.init;
			void *aux = src_page -> uninit.aux;
			vm_alloc_page_with_initializer (VM_ANON, upage, writable, init, aux);
			continue;
		}
		//uninit이 아니라면
		if (!vm_alloc_page(type, upage, writable)) {
			// init이랑 aux는 Lazy Loading에 필요함
            // 지금 만드는 페이지는 기다리지 않고 바로 내용을 넣어줄 것이므로 필요 없음
			return false;
		}
		//vm_claim_page로 요청한 후 매핑 + 페이지 타입에 맞게 초기화
		if (!vm_claim_page(upage)) {
			return false;
		}

		struct page *dst_page = spt_find_page(dst, upage);
		memcpy(dst_page->frame->kva, src_page->frame->kva, PGSIZE);
		
  	}
	return true;
}

이 함수는 부모 프로세스가 가지고 있는 spt정보를 자식 프로세스에게 복사하는 역할을 수행한다.
hash.c 파일에서 이 때 사용할 수 있는 hash_iteration에 관련된 함수들을 사용하면 만들 수 있었다. iteration을 통해 hash 의 정보를 저장했다면 자식이 가질 새로운 페이지를 생성해야 하는데 부모 페이지가 uninit인 경우와 그렇지 않은 경우를 나누어서 생성해야 한다.
부모 페이지가 uninit인 경우에는 vm_alloc_page_with_initializer()함수를 통해 자식 프로세스의 새로운 페이지를 만들어야 하는데 여기서 간단한 문제를 가지고 좀 오래 막혔었다.
부모가 uninit이니까 자식 페이지의 타입도 uninit이어야 한다고 멋대로 생각해서 vm_alloc_page_with_initializer()의 첫 번째 인수를 VM_UNINIT으로 준 것이다.
그러나 vm_alloc_page_with_initializer()함수의 윗 부분에서 ASSERT로 인자가 UNINIT이 아닌지 검사를 하고 맞다면 커널 패닉을 일으키기 때문에 VM_ANON으로 인수를 주었다.
만약 부모가 uninit이 아닌 경우는 vm_claim_page로 페이지를 바로 생성한 뒤 해당 페이지에 맞는 initializer를 호출해 페이지 타입을 변경시킨 후 바로 매핑해준다.

supplemental_page_table_kill()

void hash_page_destroy (struct hash_elem *e, void *aux) {
	struct page *page = hash_entry(e, struct page, hash_elem);
	destroy(page);
	free(page);
}

/* Free the resource hold by the supplemental page table */
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 -> spt_hash, hash_page_destroy);
}

이 함수는 존재하는 spt를 모두 free하여 할당 해제하는 함수이다. 해시를 할당 해제하는 함수는 hash.c 에 여러 종류가 있는데, destroy 나 free를 언제 어떻게 하는지를 잘 보고 이를 중복해서 적용하지 않도록 해야 제대로 작동한다.

profile
Fear always springs from ignorance.

0개의 댓글