1. Uninitialized page

    프로세스가 vm에 페이지를 처음 할당 요청을 하면 vm은 우선 uninit 상태로 초기화 한다. 그리고 나중에 페이지에 있는 내용이 필요하여 페이지를 방문하면 아직 메모리에 올라가있지 않아 페이지 폴트가 발생한다. 이때 uninit 상태의 페이지는 anon 또는 file-backed 상태가 된다. 이 방식은 Lazy loading 방식이다. 기존에는 프로세스가 필요로 하는 페이지를 전부 메모리에 올렸다면 이제는 프로세스가 요청 할 때 그제서야 메모리에 올리는 방식이다. uninit 타입의 구조체 내에 페이지 폴트 시 새로 변경 될 타입을 저장하고 있다.

    /* vm.c */
    bool
    vm_alloc_page_with_initializer (enum vm_type type, void *upage, bool writable,
    		vm_initializer *init, void *aux) {
    	/* type은 추후 변경될 타입으로 uninit일 수 없다. */	
    	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. */
    
    		struct page* page = (struct page*)malloc(sizeof(struct page));
    
        typedef bool (*initializerFunc)(struct page *, enum vm_type, void *);
        initializerFunc initializer = NULL;
    		
    		/* type에 따른 초기화 함수를 uninit 안에 기록하기 위한 작업 */
        switch(VM_TYPE(type)) {
            case VM_ANON:
                initializer = anon_initializer;
                break;
            case VM_FILE:
                initializer = file_backed_initializer;
                break;
    		}
    		
    		/* uninit 안에 기록 저장 */
        uninit_new(page, upage, init, type, aux, initializer);
    
        /* 나머지 정보도 기록 */
        page->writable = writable;
    
    		/* 페이지를 spt에 추가 */
    		return spt_insert_page(spt, page);
    	}
    err:
    	return false;
    }
    

    vm_alloc_page_with_initializer으로 할당 된 페이지에서 페이지 폴트가 일어나면 저장되어 있던 함수들을 이용하여 anon 또는 file-backed로 초기화한다. 이 역할은 uninit_initialized 함수가 담당한다.

    static bool uninit_initialize (struct page *page, void *kva) {
    	struct uninit_page *uninit = &page->uninit; // 페이지 구조체 내 UNION 내 uninit struct.
     
    	/* Fetch first, page_initialize may overwrite the values */
    	vm_initializer *init = uninit->init;
    	void *aux = uninit->aux;
    
    	/* 해당 페이지의 타입에 맞도록 페이지를 초기화한다. */
    	return uninit->page_initializer (page, uninit->type, kva) &&
    		(init ? init (page, aux) : true);
    }
    

    vm_alloc_page_with_initializer는 Load_segment에서 호출되며 uninit_initialize는 swap_in에서 호출 된다.

  2. Anononymous Page

    anonymous 페이지는 페이지 내의 모든 데이터가 0인 페이지 이다. 이런 페이지는 주로 스택이나 힙에서 사용된다. 그 이유는 보안 때문이라고 한다. 스택이나 힙은 파일의 내용이 없이 바로 페이지를 할당 받는다. 이전에 파일내용이 쓰여져 있는 채로 페이지를 할당 한다면 이전 파일 내용을 다른 프로세스에서도 볼 가능성이 있다. 해커들이 이러한 점을 악용할 수 있다.

    프로세스가 uninit 페이지를 접근하면 anon_initializer가 호출된다.

    /* anon.c */
    
    bool
    anon_initializer (struct page *page, enum vm_type type, void *kva) {
    	
      /* 데이터를 0으로 모두 초기화 */
      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;
    }
    

    page fault handler : page_fault() → vm_try_handle_fault() → vm_do_claim_page() → swap_in → uninit_initialize() → 각 타입의 initializer()와 vm_init()

  3. Segment Loading

    기존 핀토스에서는 프로세스 실행에 필요한 파일을 메모리에 전부 올렸다. 하지만 Lazy loading 방식으로 변경되면서 페이지에서 필요한 파일에 대한 정보가 필요하다. 이를 위해 container 라는 구조체 안에 파일에 대한 정보를 저장하여 spt에 함께 저장한다. 이 역할을 하는 함수는 load_segment 함수이다.

    /* process.h */
    ...
    struct container{
        struct file *file;
        off_t offset;
        size_t page_read_bytes;
    };
    ...
    
    /* process.c */
    
    #else
    ...
    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;
    
    		/* container 생성 */
    		struct container *container = (struct container *)malloc(sizeof(struct container));
    		container->file = file;
    		container->page_read_bytes = page_read_bytes;
    		container->offset = ofs;
    		
    		/* container를 vm_alloc_page_with_initializer를 통해 spt에 삽입 */
    		if (!vm_alloc_page_with_initializer (VM_ANON, upage,
    					writable, lazy_load_segment, container))
    			return false;
    
    		/* Advance. */
    		read_bytes -= page_read_bytes;
    		zero_bytes -= page_zero_bytes;
    		upage += PGSIZE;
    		ofs += page_read_bytes;
    	}
    	return true;
    }
    ...
    

    uninit 페이지에 처음 접근하여 페이지 폴트가 발생하면 lazy_load_segment가 실행되어 비로소 물리 메모리에 파일 내용이 올라간다.

    /* process.c */
    
    #else
    ...
    static bool lazy_load_segment (struct page *page, void *aux) {
    	struct file *file = ((struct container*)aux)->file;
    	off_t offset = ((struct container*)aux)->offset;
    	size_t page_read_bytes = ((struct container*)aux)->page_read_bytes;
    	size_t page_zero_bytes = PGSIZE - page_read_bytes;
    	
    	/* 파일 시작점을 offset으로 재설정하여 offset 부터 읽어들인다. */
    	file_seek(file, offset);
    
    	/* 페이지에 매핑된 물리 메모리(frame, 커널 가상 주소)에 파일의 데이터를 읽어온다. */
    	/* 제대로 못 읽어오면 페이지를 FREE시키고 FALSE 리턴 */
    	if (file_read(file, page->frame->kva, page_read_bytes) != (int)page_read_bytes){
    		palloc_free_page(page->frame->kva);
    		return false;
    	}
    	/* 만약 1페이지 못 되게 받아왔다면 남는 데이터를 0으로 초기화한다. */
    	memset(page->frame->kva + page_read_bytes, 0, page_zero_bytes); 
    
    	return true;	
    }
    ...
    

    스택이나 힙은 파일이 필요하지 않으므로 lazy_load_segment가 아닌 setup_stack 함수를 호출한다. 스택 포인터와 스택 바텀을 저장하기 위해 구조체안에 변수를 선언해 둔다. 이를 위해 스레드 구조체 안에 미리 선언해 준다.

    /* thread.h */
    
    struct thread{
    ...
    #ifdef VM
        /* Table for whole virtual memory owned by thread. */
        struct supplemental_page_table spt;
        void *stack_bottom;
        void *rsp_stack;
    ...
    }
    

    setup_stack 에서 type에 VM_MARKER_0를 추가하여 이 페이지가 스택 내부에 있다는 것을 기록해 둔다.

    /* process.c */
    
    #else
    ...
    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 */
    	/* ANON 페이지로 만들 UNINIT 페이지를 stack_bottom에서 위로 PGSIZE만큼(1 PAGE) 만든다.
         이 때 TYPE에 VM_MARKER_0 flag를 추가함으로써 이 페이지가 STACK에 있다는 것을 표시한다. */
    	if (vm_alloc_page(VM_ANON | VM_MARKER_0, stack_bottom, 1)) {
    		success = vm_claim_page(stack_bottom);
    
    		if (success){
    			if_->rsp = USER_STACK;
    			thread_current()->stack_bottom = stack_bottom;
    		}
    	}
    	return success;
    }
    ...
    
  4. SPT 수정

    fork() 시스템 콜에서 자식은 부모의 가상 메모리를 복사한다. 그러면 우리는 부모의 spt를 복사하여 자식에게 주어야 한다. 이를 위한 supplemental_page_table_copy를 구현한다. 부모 페이지가 stack 영역에 있는 페이지인 경우에는 setup_stack 만 해주면 된다. 부모 페이지가 uninit인 경우에는 vm_alloc_page_with_initializer를 통해 uninit 페이지를 만들어 spt에 삽입한다. 나머지 경우에는 uninit 페이지를 만들어서 바로 물리 메모리와 매핑 한 후 초기화 시켜준다. 그 다음 uninit을 제외한 페이지들에 대해서 데이터를 복사한다.

    	/* vm.c */
    
    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->pages);
      while (hash_next (&i)) {	
          /* 복사할 내용들을 변수에 담는다. */
    			struct page *parent_page = hash_entry (hash_cur (&i), struct page, hash_elem); 
          enum vm_type type = page_get_type(parent_page);		
          void *upage = parent_page->va;				    	
          bool writable = parent_page->writable;				
          vm_initializer *init = parent_page->uninit.init; 
          void* aux = parent_page->uninit.aux;
    
    			// 부모 페이지가 STACK인 경우
          if (parent_page->uninit.type & VM_MARKER_0) { 
              setup_stack(&thread_current()->tf);
          }
    			// 부모 타입이 uninit인 경우
          else if(parent_page->operations->type == VM_UNINIT) { 
              if(!vm_alloc_page_with_initializer(type, upage, writable, init, aux))
    					// 자식 프로세스의 유저 메모리에 UNINIT 페이지를 하나 만들고 SPT 삽입.
                  return false;
          }
    			// STACK도 아니고 UNINIT도 아니면 vm_init 함수를 넣지 않은 상태에서 
          else {  
              if(!vm_alloc_page(type, upage, writable)) // uninit 페이지 만들고 SPT 삽입.
                  return false;
              if(!vm_claim_page(upage))  // 바로 물리 메모리와 매핑하고 Initialize한다.
                  return false;
          }
    
    			// UNIT이 아닌 모든 페이지(stack 포함)에 대응하는 물리 메모리 데이터를 부모로부터 memcpy
          if (parent_page->operations->type != VM_UNINIT) { 
              struct page* child_page = spt_find_page(dst, upage);
              memcpy(child_page->frame->kva, parent_page->frame->kva, PGSIZE);
          }
      }
      return true;
    }
    

    한 프로세스가 종료 되면 프로세스에 할당된 자원들을 할당 해제해야한다. 메모리의 경우 spt를 순회하며 메모리를 할당 해제 하며 더 이상 필요하지 않은 해시 테이블을 파괴해 준다. do_mumap은 뒤에서 따로 다룬다.

    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. */
    	struct hash_iterator i;
    
    	hash_first(&i, &spt->pages);
    	while (hash_next(&i))
    	{
    		struct page *page = hash_entry(hash_cur(&i), struct page, hash_elem);
    
    		if (page->operations->type == VM_FILE) {
    			do_munmap(page->va);
    		}
    	}
    	hash_destroy(&spt->pages, spt_destructor);
    }