본문 바로가기
개발자 도전기/[OS] pintOS

pintOS | Project 3 | Anonymouse Page

by 답수 2021. 10. 19.
728x90
SMALL

 

 

pintOS | Project 3 | Memory Management

 

 

Anonymous page 파트에서는 anonymous page라고 불리는 비디스크 기반(non-disk based) 이미지를 구현할 것이다. 라고 깃북에 적혀 있다.

 

anonymous 매핑은 백업 파일이나 장치가 없다. file-backed page와는 달리 명명된 파일 소스가 없기 때문에 anonymous이다. 즉 anonymous page는 스택, 힙 같은 실행 파일에서 사용된다.

 

anonymous page 구조체는 include/vm/anon.h 에 있다.

struct anon_page {
};

 

응. 역시 있네. 껍데기만 ㅋ 일단 깃북 먼저 더 읽어보고 뭘 채워야 하는지 봐야겠다.

 

전 포스팅에서 페이지 유형 3가지를 말했었다. uninit_page, anon_page, file_page. 각 페이지마다 초기화 루틴이 다르다. 

 

먼저 vm_alloc_page_with_initalizer() 함수는 커널이 새로운 페이지를 요청할 때 호출된다. initializer는 페이지 구조체를 할당하고 페이지 타입에 따라 initializer를 설정하여 새 페이지를 초기화하고, 컨트롤을 유저 프로그램으로 다시 반환한다. 유저 프로그램이 실행될 때, 페이지에 아직 어떤 내용도 있지 않기 때문에 page fault가 발생한다.

 

page fault가 나는 동안, uninit_initialize()가 호출되고 전에 설정했던 initializer가 호출된다. 이 때 anonyomous page면 anon_initializer()가 호출되고, file_backed page면 file_backed_initializer()가 호출된다.

 

페이지는 하나의 life cycle을 가지게 된다; page faule -> lazy load -> swap in -> swap out -> ... -> destroy. 그리고 각 수명주기 변환에 따라 요청되는 함수들은 페이지 타입에 따라 다르다.

 

그래서! 각각의 페이지 타입에 따라 초기화하는 것들도 구현을 하라고 하신다!!!! 후우..!!

 

lazy loading으로 프로세스가 실행될 때는 즉시 사용할 메모리만 메인 메모리에 로드된다. 이렇게 하면 프로젝트 2까지 했던 모든 이진 이미지(binary image)를 한 번에 메모리로 불러오는 것보다 오버헤드를 줄일 수 있다고 한다. 

 

 

근데 아까부터 계속 '이미지'라는 단어가 등장한다. 한 때 짝사랑 하던 여자애 이름도 이미지인데 ㅠ 예전에 OS 강의를 들을 때 memory image에 대해 배웠던 기억이 있다.

 

 

이 강의에 의하면, memory image는 프로세스가 swap-out될 때 프로그램이 가지고 있는 정보를 swap device에 임시 저장하기 위한 용도라고 한다. 스택오버플로우에서 한 귀인께서는 이렇게 정의해주심

A memory image is simply a copy of the process's virtual memory, saved in a file. It's used when debugging the program, as you can examine the values of the program's variables and determine which functions were being called at the time of the failure.

 

ㅇㅋ. 요약하자면 스왑할 때, 프로세스가 가진 정보를 저장하는 복사된 파일이고, 디스크에 저장되었다가 필요할 때 다시 메인 메모리에 올라온다, 정도로 이해하면 되려나?

 

다음으로 넘어가서, lazy loading을 하기 위해서는 VM_UNINIT 페이지 타입을 알아야 한다고 한다. 모든 페이지들은 vm_init에서 생성된다. 초기화되지 않은 페이지에 대한 페이지 구조체는 vm/uninit.h에 있는 uninit_page 구조체에서 제공한다고 한다. vm_init()은 다음과 같이 구현한다.

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);
    start = list_begin(&frame_table);
}

 

페이지를 생성, 초기화, 초기화되지 않은 페이지 제거를 위한 함수는 vm/uninit.c.에 있다고 한다. 이것도 나중에 구현해야 한다고 하네 할 게 왜이리 많아 -.-

 

page fault 상황에서 page fault 핸들러(page_fault in exception.c)는 vm_try_handle_fault로 제어권을 전달하며, vm_fault가 유효한(valid) 페이지 폴트인지를 먼저 확인한다.

여기서 valid라는 것은 유효하지 않게 접근하는 fault를 말한다.

bogus fault는 페이지에 일부 내용을 로드하고 유저 프로그램으로 컨트롤을 반환한다.

 

bogus fault가 뭔데... 라고 생각이 들 때 바로 밑에 설명을 해준다. bogus fault의 세 가지 경우라고 한다: lazy-loaded, swaped-out page, write-protexted page

 

여튼, 지금은 우선 lazy-loaded page를 다룬다고 한다. 만약 lazy loading에서 page fault가 있다면 커널은 세그먼트를 lazy-load하기 위해 vm_alloc_page_with_initializer()에서 세팅했던 이니셜라이저 중 하나를 호출한다. 즉,  vm_alloc_page_with_initializer() 를 구현해야 한다는 것!!  vm_type에 따라 적절한 이니셜라이저를 가져와서 uninit_new를 호출하도록 구현하라고 한다.

/* 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. */
		
		// project3 | uninit_new
		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 member 초기화
        page->writable = writable;

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

 

다음은 uninit_initialize() 함수를 구현한다. 첫 번째 fault에서 페이지를 초기화한다. 템플릿 코드는 vm_initializer와 aux를 먼저 가져오고 함수의 포인터를 통해 해당 page_initializer를 호출한다.

/* 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);
}

 

 

다음은 세그먼트를 불러오는 방법에 대해서 수정을 할 것이다. 

project 2까지의 pintos

 

project 3 구현 후 pintos

 

우리가 기본적으로 알고 있는 OS의 메모리 구조는 disk에 있는 파일의 위치를 알 수 있는 포인터와 파일 사이즈, 오프셋과 페이지 테이블의 번호를 통해 페이지 테이블을 참조하고, 테이블 내에 있는 페이지 프레임의 번호로 메인 메모리에 접근한다.

 

하지만 프로젝트2 까지 스레드와 시스템 콜을 구현하면서 프로세스가 실행될 때 segment를 실제 메모리에 직접 로드하는 방식이었다. 그래서 프로젝트2 까지 page fault는 커널이나 유저 프로그램에서 나타나는 버그였는데, 이제는 spt에 필요한 정보들만 넣어서 page fault가 발생했을 때(즉 페이지가 요청되었을 때) 메모리에 로드하는 방식(lazy load)으로 바꿀 것이다.

 

흠.. 그럼 로드하는 부분에서 수정이 필요하겠다. process.c 에서 load_segment() 함수에 lazy_load_segment() 함수를 불러와서 원하는 file을 kpage에 로드할 수 있도록 해보자.

 

* 참고로, 현제 process.c 에는 load_segment() 함수가 두 개가 있다. #ifndef 매크로 안에 있는 함수는 project 2를 실행시키기 위한 파일로, 앞서 얘기했던 것처럼 세그멘트들을 메모리에 직접 로드하는 방식이기 때문에 우리는 매크로 함수 밖에 있는 함수를 수정해야 한다.

// ../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. */
	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;
}


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(container로 대체) to pass information to the lazy_load_segment. */
		// struct container *container;
		// container = palloc_get_page(PAL_ZERO | PAL_USER);
		struct container *container = (struct container *)malloc(sizeof(struct container));
		container->file = file;
		container->page_read_bytes = page_read_bytes;
		container->offset = ofs;
		// container->writable = writable;

		if (!vm_alloc_page_with_initializer(VM_ANON, upage, writable, lazy_load_segment, container)) {
			return false;
		}

		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		upage += PGSIZE;
		ofs += page_read_bytes;
	}
	return true;
}
}

 

새 메모리 관리 시스템에 스택 할당을 맞추도록 하기 위해서 이 부분도 수정이 필요

/* Create a PAGE of stack at the USER_STACK. Return true on success. */
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 */
	// stack 영역인 page MARK
	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;
}

 

stack_bottom은 thread.h에서 VM 부분에 추가해준다.

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

 

 

마지막으로 spt_find_page을 통해 fault된 주소에 해당하는 페이지 구조체를 해결하기 위해 vm_try_handle_fault() 함수를 수정하라고 한다.

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 */
	if (is_kernel_vaddr(addr)) {
        return false;
	}

    void *rsp_stack = is_kernel_vaddr(f->rsp) ? thread_current()->rsp_stack : f->rsp;
    if (not_present){
        if (!vm_claim_page(addr)) {
            if (rsp_stack - 8 <= addr && USER_STACK - 0x100000 <= addr && addr <= USER_STACK) {
                vm_stack_growth(thread_current()->stack_bottom - PGSIZE);
                return true;
            }
            return false;
        }
        else
            return true;
    }
    return false;
}

여기에서 void *rsp_stack 부분은 다음 포스팅에서 더 자세하게 다뤄보자!

 

 

그리고 thread구조체 VM 부분에 srp_stack 변수도 추가해준다.

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

 

여기까지 구현하면 vm 테스트케이스 중 fork를 제외한 프로젝트2 부분 테스트들이 패스될 것이라고 하는데 안됨;;

 

동기들한테 물어보니 그 전에 system call을 구현할 때 만들었던 check_address() 함수를 수정해야 한다고 한다.

 

기존 check_address() 함수는 rsp에 대한 유저 메모리 영역을 체크하는 것이었고, 지금은 page를 사용하여 유효성 검사를 하도록 코드를 수정해야 한다. addr이 page에 존재하면 page를 반환하도록 코드를 작성해야 한다.

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

 

다음으로는 check_valid_buffer() 함수를 구현해야 한다. read() 시스템 콜의 경우 버퍼의 주소가 유효한 가상 주소인지 아닌지 검사를 하고, 실제로 프로젝트 2를 할 때 코드로 구현하기도 했었다. 이 부분을 page에 맞게 수정해야 한다. 또한 write()도 마찬가지로 버퍼에 내용을 쓸 수 있는지 없는지 검사해야 하기 때문에 역시 수정해준다.

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

 

시스템 콜 핸들러 swtich문에서 read와 write에 check_valid_buffer() 함수를 적용해준다.

...
	case SYS_READ:
		check_valid_buffer(f->R.rsi, f->R.rdx, f->rsp, 1);
		f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
		break;
	case SYS_WRITE:
		check_valid_buffer(f->R.rsi, f->R.rdx, f->rsp, 0);
		f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
		break;
 ...

 

사진으로 보면 조금 더 쉽게 이해할 수 있을 것 같다.

 

 

자 테스트 돌려보즈아

 

 

 

어허이.... 프로젝트1 부분들은 다 통과됐고, 프로젝트 2의 fork() 이전까지 부분에서 꽤 많이 통과는 됐다만, 

create-null
create-bad-ptr
open-null
open-bad-ptr

이 부분들은 fail.... 마음대로 다 잘 되는 것들이 읎다 ㅜㅜ 어딜 빼먹었는지 강의 자료를 보면서 더 꼼꼼하게 찾아보자.

..

음. 강의안 순서가 깃북이랑 좀 다르다. ㅋ. 일단 깃북 순서대로 먼저 구현해보고 있자. 그럼 어딜 더 수정하거나 구현해야 하는지 알게 될 것이고, pass도 더 늘겠지.. 킵고잉!

 

 

 

728x90
LIST

댓글