카이스트 정글 - 프로젝트/Pintos

Project_3 : Virtual Memory - Anonymous Page(2)

bewisesh91 2022. 1. 22. 00:41
728x90

맨 처음 페이지를 만들면 해당 페이지는 uninit 상태의 페이지이다.

다시 말해, 페이지의 타입 지정이 anon 혹은 file-backed일 수 있지만 아직은 해당 타입으로 초기화되지 않은 상태이다.

추후에 페이지 폴트가 발생했을 때, 타입에 맞는 초기화 함수를 호출함으로써 페이지의 상태가 변한다.

 

vm_alloc_page_with_initializer( ) 함수는 uninit 상태의 페이지를 만들어 spt에 해당 페이지를 삽입하는 함수이다.

해당 함수는 커널이 새로운 페이지 요청을 수신할 때 호출된다. 우선, malloc( ) 함수를 이용하여 페이지를 할당받는다.

그리고 입력 받은 type 인자에 맞추어 initializer를 초기화 한다.

이후, uninit_new( ) 함수를 호출하여 최초 인자로 받은 정보들을 새로운 page 구조체의 멤버들로 채워준다.

해당 과정을 통해 새로운 uninit 페이지가 만들어지며, 이를 spt에 삽입한다.

/* vm/vm.c */

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. */
		/*--------------- PROJECT3: Virtual Memory ----------------*/
                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;
                }

                uninit_new(page, upage, init, type, aux, initializer);

                page->writable = writable;
		/*---------------------------------------------------------*/

		/* TODO: Insert the page into the spt. */
		/*--------------- PROJECT3: Virtual Memory ----------------*/
		spt_insert_page(spt, page);
		/*---------------------------------------------------------*/
	}
err:
	return false;
}

앞서 언급하였듯 모든 페이지들은 만들어질 때 uninit 페이지로 만들어진다. 그리고 해당 페이지에 처음 접근 시 페이지 폴트가 발생한다. 

페이지 폴트 핸들러는 호출 체인을 따르는데, swap_in을 호출할 때 아래에서 알 수 있듯이 uninit_initialize( ) 함수에 도달한다.

/* vm/uninit.c */

static const struct page_operations uninit_ops = {
	.swap_in = uninit_initialize,
	.swap_out = NULL,
	.destroy = uninit_destroy,
	.type = VM_UNINIT,
};

uninit_initialize( ) 함수에서는 uninit 페이지를 초기화하며, 각 타입에 맞게 page_initializer가 설정된다.

/* vm/uninit.c */

/* Initalize the page on first fault */
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);
}

이제 본격적으로 Anonymous Page를 구현하기 위해 anon_page 타입과 관련한 함수들을 수정해주어야 한다.

우선, 비어있는 anon_page 구조체에 아래와 같이 swap_index 변수를 추가해주자.

/* include/vm/anon.h */

struct anon_page {
    /*--------------- PROJECT3: Virtual Memory ----------------*/
    int swap_index;
    /*---------------------------------------------------------*/
};

그리고 uninit 페이지에 접근해 페이지 폴트가 발생했을 때, anon_type을 초기화하는 anon_initializer( ) 함수를 수정하도록 하자.

여기서 중요한 점은 anon_type의 경우 물리 메모리에서 받아와 초기화 할 때 모든 데이터를 Zeroing 해주어야 한다는 것이다.

/* vm/anon.c */

bool anon_initializer (struct page *page, enum vm_type type, void *kva) {
	/*--------------- PROJECT3: Virtual Memory ----------------*/
	struct uninit_page *uninit = &page->uninit;
	memset(uninit, 0, sizeof(struct uninit_page));
    
	/* Set up the handler */
	page->operations = &anon_ops;

	struct anon_page *anon_page = &page->anon;
	anon_page->swap_index = -1;
	
	return true;
	/*---------------------------------------------------------*/
}

 

현재까지 Pintos는 디스크에서 실행시킬 실행 파일 전체를 물리 메모리에 올려놓고, 페이지와 물리 메모리 프레임을 매핑하는 방식이었다. 

이제는 spt에 프로세스의 각 페이지와 관련된 정보를 저장해 놓고, 프로세스가 가상 주소에 접근했을 때 매핑이 되어 있지 않으면,

그 때 spt에서 해당 메모리와 관련한 정보를 가져와 물리 프레임에 매핑하는 방식으로 변환하고자 한다. 

이를 Lazy Loading이라고 하며, 관련하여 여러 가지 구조체와 함수들을 수정해야 한다.

 

우선, Lazy Loading을 위한 정보 구조체이다. 이 안에 해당 페이지에 대응되는 파일 정보들이 들어가 있다.

이 구조체를 통해서 페이지 폴트가 발생한 후 디스크에서 파일을 불러올 때 필요한 파일 정보를 알 수 있다. 

/* include/userprog/process.h */

/*--------------- PROJECT3: Virtual Memory ----------------*/
struct container {
    struct file *file;
    off_t offset;               // 읽어야 할 파일 오프셋
    size_t page_read_bytes;     // 가상 페이지에 쓰여져 있는 데이터 크기
};
/*---------------------------------------------------------*/

uninit 페이지에 파일 정보를 담기 위해서 load_segment( ) 함수를 수정하자.

페이지 폴트 핸들러가 페이지 타입 별 initializer를 호출할 때, lazy_load_segment( ) 함수도 호출하여, 

페이지 안의 정보들을 바탕으로 디스크에서 파일을 읽어오면서 물리 메모리에 Lazy Loading이 가능하도록 만들어야 한다.

우선, 위에서 구현한 container 구조체에 file에 있는 정보들 중 페이지 폴트 핸들러가 참고할 정보들을 넣는다.

이후, vm_alloc_page_with_initializer( ) 함수 호출 시 aux 인자로 넣어준다. 

그리고 vm_alloc_page_with_initializer( ) 함수의 인자로 lazy_load_segment( ) 함수를 넣는다.

/* userprog/process.c */

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

	/* upage 주소부터 1페이지 단위씩 UNINIT 페이지를 만들어 프로세스의 spt에 넣는다(vm_alloc_page_with_initializer).
	   이 때 각 페이지의 타입에 맞게 initializer도 맞춰준다. */
	while (read_bytes > 0 || zero_bytes > 0) {
		/* 1 Page보다 같거나 작은 메모리를 한 단위로 해서 읽어 온다.
		   페이지보다 작은 메모리를 읽어올때 (페이지 - 메모리) 공간을 0으로 만들 것이다. */
		size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
		size_t page_zero_bytes = PGSIZE - page_read_bytes;

		/* 새 UNINIT 페이지를 만들어서 현재 프로세스의 spt에 넣는다. 
		   페이지에 해당하는 파일의 정보들을 container 구조체에 담아서 AUX로 넘겨준다.
		   타입에 맞게 initializer를 설정해준다. */
		struct container *container = (struct container *)malloc(sizeof(struct container));
		container->file = file;
		container->page_read_bytes = page_read_bytes;
		container->offset = ofs;

		if (!vm_alloc_page_with_initializer (VM_ANON, upage, 
				writable, lazy_load_segment, container))
			return false;
		// page fault가 호출되면 페이지가 타입별로 초기화되고 lazy_load_segment()가 실행된다. 

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

uninit_initialize( ) 함수가 호출되어 페이지 타입에 맞는 페이지 초기화를 해줄 때, lazy_load_segment( ) 함수가 실행된다.

이때 가상 페이지와 관련된 파일의 offset부터 page_read_bytes까지의 데이터를 읽어와 페이지와 매핑된 물리 메모리에 작성한다.

즉, 페이지에 있는 파일 정보대로 물리 메모리에 load 한다.

/* userprog/process.c */

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. */
	/*--------------- PROJECT3: Virtual Memory ----------------*/
	struct file *file = ((struct container *)aux)->file;
	off_t offsetof = ((struct container *)aux)->offset;
	size_t page_read_bytes = ((struct container *)aux)->page_read_bytes;
    	size_t page_zero_bytes = PGSIZE - page_read_bytes;

	file_seek(file, offsetof);

        if (file_read(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;
	/*---------------------------------------------------------*/
}

 

스택은 디스크에서 파일을 읽어올 필요가 없다. 따라서 lazy_load_segment( ) 함수를 호출하지 않아야 한다.

아래와 같이 setup_stack( ) 함수를 수정한다.

/* userprog/process.c */

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 */
	/*--------------- PROJECT3: Virtual Memory ----------------*/
	if (vm_alloc_page(VM_ANON | VM_MARKER_0, stack_bottom, 1)) {    // type, upage, writable
		success = vm_claim_page(stack_bottom);
		
		if (success) {
        		if_->rsp = USER_STACK;
                	thread_current()->stack_bottom = stack_bottom;
       	 	}
	/*---------------------------------------------------------*/	

	return success;
}

thread 구조체에 스택을 위한 변수를 추가해준다.

struct thread {
    	// 중략
    
#ifdef VM
    /* Table for whole virtual memory owned by thread. */
    struct supplemental_page_table spt;
    /*--------------- PROJECT3: Virtual Memory ----------------*/
    void *stack_bottom;
    void* rsp_stack;
    /*---------------------------------------------------------*/


	// 중략
}

 

Lazy Loading의 경우, 가상 주소가 페이지 테이블(pml4)에 존재하지 않기 때문에 시스템 콜 호출 시 오류가 발생할 수 있다.

특히, 가상 주소를 확인하는 check_address( ) 함수의 경우가 문제가 된다.

따라서 입력 받은 가상 주소를 spt에서 찾을 수 있도록 아래와 같이 함수를 수정해주어야 한다.

/* userporg/syscall.c */

/*--------------- PROJECT3: Virtual Memory ----------------*/
struct page * check_address(void *addr) {
    if (is_kernel_vaddr(addr)) {
        exit(-1);
    }
    return spt_find_page(&thread_current()->spt, addr);
}
/*---------------------------------------------------------*/

일부 시스템 콜의 경우 파일 이름의 주소를 받는 대신 버퍼의 주소를 받는다.

따라서 해당 주소가 포함된 페이지가 spt에 없거나 쓰기가 허용되지 않은 페이지의 경우 프로세스를 종료하도록 새로운 함수를 추가한다.

/* userporg/syscall.c */

/*--------------- PROJECT3: Virtual Memory ----------------*/
void check_valid_buffer(void* buffer, unsigned size, void* rsp, bool to_write) {
    for (int i = 0; i < size; i++) {
        struct page* page = check_address(buffer + i);    // 인자로 받은 buffer부터 buffer + size까지의 크기가 한 페이지의 크기를 넘을수도 있음
        if(page == NULL)
            exit(-1);
        if(to_write == true && page->writable == false)
            exit(-1);
    }
}
/*---------------------------------------------------------*/