정글사관학교 개발일지/운영체제-PintOS

PintOS Project 3 - Virtual Memory (2) Anonymous Page (정글사관학교 79일차 TIL)

Woonys 2022. 1. 20. 01:06
반응형

바로 2번째 과제 달린다.

 

이번에는 Anonymous Page를 구현하는 것이 과제 목표. Gitbook 정리는 아래 노션 링크를 통해 참고하도록 하자.

 

 

Part 2: Anonymous Page

Anonymous: 어떤 파일과도 연결되지 않은 페이지를 뜻함. 주로 stack or heap에 사용된다.

woony.notion.site

0. Anonymous page 정리

전에 권영진 교수님 강의 말미에서 잠깐 나왔던 개념인데, 드디어 다루게 되다니 감개무량하다. 페이지에는 크게 두 가지 종류가 있다.

 

1. Anonymous page: 어떤 파일과도 연결되지 않은 페이지

2. File-backed page: 파일과 매핑된 페이지(즉, 페이지 안에 파일이 들어있는)

 

어차피 File-backed page는 나중에 다룰 것이니, 여기서는 anonymous page에 대해서만 설명해보자. anon 페이지는 커널로부터 프로세스에게 할당된 일반적인 메모리 페이지를 뜻한다. 즉, heap을 거치지 않고 할당받은 메모리 공간이다. 사실상 이 힙 역시도 anonymous page에 속한다고 보면 된다. 힙은 일종의 거대한 anonymous page의 집합으로 취급하면 될듯. heap 뿐만 아니라 stack을 사용할 때 역시 anonymous page를 할당받아서 쓴다.

 

따라서 heap을 거치지 않고 할당받은 메모리 공간이라는 말보다, 엄밀히 말하면 힙도 하나의 anon page 집합이고 stack도 하나의 anon page 집합이며 이외에 anon page를 할당받는다고 하면 당연히 힙을 거치지 않으니(일종의 힙과 병렬 개념) 힙을 거치지 않는 게 맞긴 하다. 아래 이미지 같은 개념이라고 보면 될듯. 우리가 흔히 쓰는 malloc() 역시 anonymous page ->heap ->malloc 식으로 anon page의 일부를 잘라 할당받는 것을 뜻한다.

이 anonymous page는 파일에 기반하고 있지 않은 페이지이기에 0으로 초기화되어 있다.

 

프로세스가 mmap()으로 커널에게 익명 페이지를 할당 요청하면 커널은 프로세스에게 가상 메모리 주소 공간을 부여한다. 이 부여된 가상 메모리 공간은 아직 실제 물리 메모리 페이지로는 할당되지 않은 공간이다. 실제 loading은 사용자 프로세스에서 해당 메모리에 접근을 시도할 때가 되어서야 이뤄지는데, 이를 lazy loading이라고 한다. 이 부분은 Gitbook 정리에 올라와있다. 조금만 더 정리하면

 

virtual address에서 페이지를 할당했다고 하자. 그러면 가상 주소 파트는 위와 같이 KERN_BASE 아래 가상 메모리에서는 할당이 되어있는 것처럼 인식하지만 실제 물리 메모리에는 텅텅 비어있는 상황이다. (위에는 마치 초록색 페이지를 위한 빈 frame이 존재하는 것처럼 보이나 이마저도 아니며 그냥 물리 frame과는 현재 아무런 연관이 없는 상황) 그런데 유저 프로세스가 돌다가 해당 메모리에 들어있는 정보를 요청하려고 하면 어떻게 될까? 이 프로세스는 해당 메모리에 당연히 정보가 있다고 생각하고 해당 위치에 찾아가 똑똑, 문을 두드릴 것이다. 하지만 안을 까보면? 아무 것도 없다는 것을 알고 그제서야 page fault를 띄운다. 그러면 이제 빈 frame을 찾아 연결해주는 방식.

 

다시 돌아와서, 프로세스가 mmap으로 커널에 anonymous 페이지 할당을 요청하면 커널은 프로세스에게 가상 메모리 주소 공간을 부여한다. 부여된 가상 메모리 페이지는 물리 프레임과는 연결되지 않았으며 시스템 콜과 같이 해당 페이지에 접근 요청시 그제서야 실제 프레임과 매핑된다. 예시로 read/write 시스템 콜을 생각해보자. 해당 페이지에 메모리 read/write 요청이 들어온다고 하면

 

1) read: 프로세스가 해당 메모리 공간에 읽기 작업을 시작하면 커널은 zero로 초기화된 메모리 페이지를 제공한다.

2) write: 프로세스가 해당 메모리 공간에 쓰기 작업을 시작하면 커널은 실제 물리 프레임을 할당하고 write된 데이터를 보관한다.

 

이 익명 페이지는 private or shared 방식으로 할당받을 수 있다. 여기서 private/shared의 의미는 "서로 다른 프로세스 간에 페이지를 공유할 수 있는지"의 여부를 의미한다. 우리가 위에서 언급한 각 프로세스별로 할당받는 힙과 스택은 private 방식으로 할당된 anonymous page이며, shared 방식은 프로세스 간 통신을 위해 사용되는 anonymous page이다.

 

anonymous page에 대해 개념을 간단히 정리했으니 이제 과제 구현으로 가보자.

 

1. 과제 구현

현재 anonymous page를 나타내는 구조체 anon_page는 include/vm/anon.h에 있으며 멤버가 텅텅 비어있는 빈 구조체이다. 여기에 작업을 해야하는 것이 이번 과제.

@/include/vm/anon.h

struct anon_page {
    struct page anon_p;
};

 

Step 1: Page Initialization with Lazy Loading

 

먼저 우리는 페이지 초기화와 함께 lazy loading 방식을 구현할 것이다. 페이지 초기화 흐름은 크게 세 가지로 볼 수 있는데

 

1. 커널이 새 페이지 request를 받으면 vm_alloc_page_with_initializer가 호출된다. 이 initializer는 인자로 받은 해당 페이지 타입에 맞게 새 페이지를 초기화한 뒤 다시 유저 프로그램으로 제어권을 넘긴다.

 

2. 유저 프로그램이 실행되는 시점에서 page fault가 발생한다. 유저 프로그램 입장에서는 해당 페이지에 정보가 있다고 믿고 page에 접근을 시도할 테지만 이 페이지는 어떤 프레임과도 연결되지 않았기 때문.

 

3. fault를 다루는 동안, uninit_initialize가 발동하고 우리가 1에서 세팅해 둔 initializer를 호출한다. initializer는 각 페이지 유형에 맞게 anonymous page에는 anon_initializer를, file-backed page에는 file_backed_initializer를 호출할 것.

 

가장 처음으로 구현하라고 지시하는 함수는 vm_alloc_page_with_initializer()이다. 그전에, lazy loading을 하기 위해서는 각 페이지별로 타입을 확인해줘야 한다고 했다. 이는 처음에 부팅이 시작될 때 호출되는 함수 vm_init()에 있는데, vm_init()을 수정할 필요가 있다.

 

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

 

처음에 가상 메모리를 초기화할 때, 프레임 테이블을 여기서 함께 초기화해줘야 한다.

 

바로 이어서 vm_alloc_page_with_initializer()를 수정해보자. 이 함수는 load_segment()에서 호출된다. 앞에서 커널이 새 페이지 request를 받았을 때 vm_alloc_page_with_initializer()가 호출된다고 하지 않았나. 예시를 보자.

 

핀토스를 실행하면 부팅이 완료되고(이 시점에서 vm 초기화 완료) main 프로그램이 실행될 것이다. 이때, 예전 글에서 정리했던 것처럼 쭉 진행될텐데, 예전 글 8번 - process_exec()에서 load()가 실행된다. 저 글에서는 생략해뒀지만 이 load()함수가 실제로 메모리에 적재해야 한다는 요청과도 같다. 따라서 load() -> load_segment()로 넘어간다. load_segment()는 세그먼트에 파일을 load하는 함수다. project 2에서는 page를 직접 할당해서 그 페이지에다 넣어주지만(ifndef로 감싸져있음) 이때는 가상 메모리가 완전히 구현되지 않은 상황이라 그렇고, project 3에서 쓰는 load_segment()가 따로 있다. 이 함수에서는 vm_alloc_page_initializer()가 호출된다. 이때가 커널이 새 페이지 request를 받은 상황.

 

bool vm_alloc_page_with_initializer (enum vm_type type, void *va,
        bool writable, vm_initializer *init, void *aux);

 

 

vm_alloc_page_with_initializer() 구현

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

 

맨 처음에 페이지를 생성하면 이 페이지는 초기화되지 않은(UNINIT) 상태다. 이후에 해당 요청이 ANON인지 FILE-BACKED인지에 따라 해당 페이지가 그에 맞게 초기화될 것이다. 코드로 가보자.

 

spt_find_page()에서 해당 페이지가 없어야 초기화를 해주니까 if(spt_find_page())로 NULL이 뜨면 malloc으로 페이지 크기만큼 할당받아온다. 이때, 이 페이지는 아직 UNINIT 상태.

 

이어서 initializer를 선언한다. 다음으로, VM_type에 맞게 switch문을 돌려 vm_type에 맞게 선언한 initializer를 anon/file로 바꿔준다. 끝나고 나면 uninit_new() 내 인자를 통해 정보를 새로 만들 페이지에 넣어준다. 마지막으로 해당 프로세스의 SPT에 페이지를 넣어준다.

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. */
		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_new()를 호출하는 상황을 보면, 위의 init에 해당하는 인자로 lazy_load_segment()가 들어간다. 나중에 page fault가 발생하고 만약 이 페이지가 들어있는 segment가 물리 프레임에 load되지 않은 상황이라면 해당 함수가 실행될 때 segment도 메모리에 lazy loading된다.

 

uninit_initialize() 함수 구현

프로세스가 처음 만들어진(UNINIT)페이지에 처음으로 접근할 때 page fault가 발생한다. 그러면 page fault handler는 해당 페이지를 디스크에서 프레임으로 swap-in하는데, UNINIT type일 때의 swap_in 함수가 바로 이 함수이다. 즉, UNINIT 페이지 멤버를 초기화해줌으로써 페이지 타입을 인자로 주어진 타입(ANON, FILE, PAGE_CACHE)로 변환시켜준다. 여기서 만약 segment도 load되지 않은 상태라면 lazy load segment도 진행한다.

 

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

	/* TODO: You may need to fix this function. */

	/*
	해당 페이지 타입에 맞도록 페이지를 초기화한다.
	만약 해당 페이지의 segment가 load되지 않은 상태면 lazy load해준다.
	init이 lazy_load_segmet일때에 해당.
	*/
	return uninit->page_initializer (page, uninit->type, kva) &&
		(init ? init (page, aux) : true);
}

 

위 함수가 호출되는 과정을 살펴보자. 먼저 page fault handler부터 발동할 것이다.

 

page fault handler: page_fault() -> vm_try_handle_fault()-> vm_do_claim_page() -> swap_in() -> uninit_initialize()-> r각 타입에 맞는 initializer()와 vm_init() 호출

프로세스가 페이지에 접근했는데 uninit page면 page fault handler가 호출된다. vm_do_claim_page()에서 해당 페이지와 물리 프레임을 연결시켜주고, uninit_initialize()에서 인자로 받은 타입에 맞게 페이지를 초기화해준다.

 

UNINIT 페이지의 swap_in operation은 uninit_initialize()이다.

 


 

Step 2: Loading Segment 관련 함수 구현

 

 

다음은 세그먼트를 loading하는 것과 관련한 함수를 구현한다.

 

아래 그림을 보면 초기화 구현 전후로 pintos의 구현 형태를 알 수 있다.

 

 

 

프로젝트 2까지는 세그먼트를 물리 프레임에 직접 load하는 방식이었다. 보면 맨 처음에 물리 프레임부터 할당을 받고 파일을 해당 프레임에 load한 다음, 페이지 테이블에서 가상 주소와 물리 주소를 맵핑하는 방식이다. 그래서 프로젝트 2까지 page fault는 커널이나 유저 프로그램에서 나타나는 버그였다.

 

이제는 spt에 필요한 정보들을 넣어서 page fault가 발생했을 때(페이지가 요청됐을 때)가 되어서야 메모리에 load하는 lazy load 방식으로 변경하려 한다.

 

process.c에 가보면 lazy_load_segment() 함수를 찾을 수 있다. load_segment()를 호출하면, vm_alloc_page_with_initializer()를 호출하는 과정에서 lazy_load_segment()를 호출한 뒤 반환값을 vm_alloc_page_with_initializer()의 인자로 넣는다.

 

 

 

(내일 계속...)

반응형