본문 바로가기
정글/pintos

[Project3] Virtual Memory(3) - Anonymous Page

by 위대한초밥V 2023. 6. 17.

이번 프로젝트에서는 anonymous page라고 하는 non-disk based image를 구현할 것이다.

  • non-disk based image란 디스크(하드 디스크, 다른 저장 매체)가 아닌 다른 형태의 저장 매체나 메모리에 저장되는 이미지를 말한다.
  • 예를 들어 메모리(RAM)나 캐시와 같은 임시 저장 공간이나 네트워크를 통해 전송되어 메모리에 로드된 이미지를 말하는데, 이 방식은 일시적이거나 동적인 이미지 저장에 유용하고 디스크 기반 이미지 저장 방식에 비해 속도와 접근성이 좋다.

Anonymous mapping은 백업 파일, 장치(메모리 매핑이 지원되는 물리적인 장치나 파일)가 없다. Anonymous라는 뜻은 파일 백업 페이지와 다르게 이름이 지정된 backing 파일이나 장치가 없다는 것을 뜻한다.

이러한 이유로 Anonymous page는 스택, 힙과 같은 실행 파일에 사용된다. (참고로, 스택과 힙은 각각 프로그램 실행 중에 함수 호출과 로컬 변수 할당 해제를 관리하는 데 사용하는 메모리 영역과 동적으로 할당되는 메모리 영역을 말한다. 즉 프로세스가 실행되는 동안 필요한 메모리를 동적으로 할당하고 해제한다.)

 

관련된 구조체는 [더보기]를 확인해라!!

더보기

struct anon_page(include/vm/anon.h)

현재는 비어있지만, 구현할 때 필요한 정보나 익명 페이지 상태를 저장하기 위한 멤버를 추가해도 된다.

#ifndef VM_ANON_H
#define VM_ANON_H
#include "vm/vm.h"
struct page;
enum vm_type;

// TODO: 채워야하는 것!!!!👩‍💻
struct anon_page {
};

void vm_anon_init (void);
bool anon_initializer (struct page *page, enum vm_type type, void *kva);

#endif

 

 

struct page(include/vm/vm.h)

page가 가지고 있는 일반적인 정보들이 남겨있다.

또한! struct anon_page는 page 구조체 일부로 포함되어 있다.

메모리 관리 시스템은 페이지 단위로 메모리를 관리하는데, 페이지 구조체는 각 페이지에 대한 정보를 저장하고 관리하는 데 사용되며, 익명 페이지의 경우 struct anon_page 를 이용해 익명 페이지의 특성과 동작을 정의하고 유지할 수 있는 것이다.

struct page {
	const struct page_operations *operations;
	void *va;              /* Address in terms of user space */
	struct frame *frame;   /* Back reference for frame */

	/* Your implementation */

	/* Per-type data are binded into the union.
	 * Each function automatically detects the current union */
	union {
		struct uninit_page uninit;
		struct anon_page anon;     // ✅: page 구조체의 일부로 포함되어있는 anon_page
		struct file_page file;
#ifdef EFILESYS
		struct page_cache page_cache;
#endif
	};
};

 


 

Page Initialization with Lazy Loading

레이지 로딩은 메모리가 필요한 시점까지 메모리 로딩을 지연하는 설계이다.

즉 페이지가 할당되어 해당 페이지에 해당하는 페이지 구조는 있지만 전용 physical frame이 없고 페이지의 실제 콘텐츠가 아직 로드되지 않은 상태이다. 


그럼 콘텐츠는 언제 로드될까?🧐

콘텐츠는 페이지 내용이 실제 필요할 때 로드된다. 그 시점은 "page fault"가 발생할 때이다. page fault는 프로세스가 특정 가상 주소에 접근하려고 할 때 해당 페이지가 물리 메모리에 없는 상태를 말한다. 


운영체제는 1) 이를 감지하고, 2) 필요한 페이지의 내용을 로드하여 물리 메모리에 할당하고, 3) 페이지 테이블 엔트리를 업데이트하여 가상 주소와 물리 주소 맵핑을 수행한다. 그 결과 4) 필요한 페이지 내용이 실제 메모리에 로드된다. 이렇게 필요한 시점에만 로드되어, 불필요한 메모리 사용을 줄여 성능을 향상할 수 있다.


Lazy Loading 방식으로 Page 초기화되는 과정은 페이지 타입에 따라 달라진다.(uninit_page, anon_page, file_page)

 

페이지 초기화 흐름을 다음과 같다.

  • 커널이 새 페이지 요청을 받으면 vm_alloc_page_with_initializer(vm/vm.c)가 호출된다.
  • Initializer는 페이지 구조를 할당하고 페이지 유형에 따라 적절한 이니셜라이저를 설정하여 새 페이지를 초기화하고 사용자 프로그램에 제어를 반환한다.
  • 사용자 프로그램이 실행되는 동안, 특정 시점에 page fault가 발생한다. 그 이유는 프로그램이 지니고 있다고 생각되는 페이지에 계속 접근하려고 하지만 해당 페이지에는 콘텐츠가 아직 없기 때문에 발생하는 문제이다.
  • 오류 처리 과정 중에 uninit_initialize(vm/uninit.c)를 호출하여 앞에서 설정한 initializer를 호출한다. initializer는 익명 페이지라면 anon_initializer 가 호출되고, file 지원 페이지라면 file_backed_initializer 된다.

하나의 페이지는 initialize → page_fault → lazy-load → swap-in → swap-out → destroy 라는 라이프 사이클을 갖는다. 라이프 사이클의 각 전환마다 필요한 절차는 page type(또는 VM_TYPE)에 따라 다르다.

 

이번 프로젝트는 각각의 페이지 유형에 대한 전환 프로세스를 구현한다.

 

 


Lazy Loading for Executable

Lazy 로딩은 프로세스를 실행할 때, 당장 필요한 메모리 부분만 주 메모리에 로드한다. 이 점은 바이너리 이미지를 한 번에 메모리에 로드하는 eager 로딩에 비해 오버헤드를 줄일 수 있다.

Lazy Loading은 include/vm/vm.h 파일의 enum 타입의 vm_type 변수에서 VM_UNINIT 페이지 타입을 이용한다.

 

include/vm/vm.h

enum vm_type {
	/* page not initialized */
	VM_UNINIT = 0, // 👈 Lazy Loading 구현에 사용되는 VM_UNINIT 페이지 타입
	/* page not related to the file, aka anonymous page */
	VM_ANON = 1,
	/* page that realated to the file */
	VM_FILE = 2,
	/* page that hold the page cache, for project 4 */
	VM_PAGE_CACHE = 3,
	...
};

모든 페이지는 처음에 VM_UNINIT 페이지 타입으로 생성된다.

 

uninit_page를 초기화할 때는 struct uninit_page를 사용한다.

 

include/vm/uninit.h

struct uninit_page {
	/* Initiate the contets of the page */
	vm_initializer *init;
	enum vm_type type;
	void *aux;
	/* Initiate the struct page and maps the pa to the va */
	bool (*page_initializer) (struct page *, enum vm_type, void *kva);
};

초기화되지 않은 페이지를 생성(uninit_new), 초기화(uninit_initialize), 삭제(uninit_destroy)하는 함수는 vm/uninit.c에서 확인할 수 있고, 일부 함수는 구현이 필요하다.

 

page_fault에서 page fault 핸들러(userprog/exception.c) 제어권을 vm_try_handle_fault(vm/vm.c)로 전달하고, 먼저 유효한 페이지 오류인지 확인한다.

vm/vm.c

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 */

	return vm_do_claim_page (page);
}

유효한 오류란 유효하지 않는 페이지에 접근하는 오류를 말한다. 만약 가짜 오류라면, 일부 컨텐츠만 로드하고 사용자 프로그램에 제어권을 반환한다.

  • 가짜 오류(bogus fault)는 실제로는 오류가 아니지만, 시스템이나 프로그램에서 오류로 잘못 감지한 상황을 말한다.

bogus fault가 발생하는 원인은 상황에 따라 다르다.

대표적으로 lazy-loaded, swaped-out page, write-protected page가 있는데 현재는 lazy-loaded 페이지만 고려하면 된다. 만약 lazy loading 때문에 page fault가 발생한다면, 커널은 이전에 vm_alloc_page_with_initializer에서 설정한 initializer 중 하나를 호출해서 세그먼트를 지연 로드한다.

 

userprog/process.c에서 lazy_load_segment를 구현하여 이 기능을 개발한다.

 

 

vm_alloc_page_with_initializer()를 구현한다.

 

이 기능은 인자로 전달받은 vm_type에 따라 initializer를 가져와서 uninit_new 를 호출한다.

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

주어진 타입으로 uninitialized page를 생성한다.
uninit page의 swap_in 핸들러는 자동으로 type에 따라 페이지를 초기화하고 AUX 인자로 INIT콜을 호출한다.
일단 page 구조체로 page 객체를 프로젝트의 SPT에 추가한다. vm.h에 있는 VM_TYPE 매크로 함수를 이용하면 편리하게 할 수 있다.

page fault handler는 호출을 따라가다가 swap_in을 호출할 때 uninit_initialize에 도착한다.

uninit_initialize는 이미 구현되어있지만 설계에 따라 수정될 수 있다.

 

static bool uninit_initialize (struct page page, void kva); 

첫번째 오류 발생 시, 페이지를 초기화한다. 템플릿 코드는 vm_initializer와 aux를 가져와 함수 포인터로 해당하는 page_initializer로 호출한다. 디자인에 따라 설계가 수정될 수 있다.

필요하다면 vm/anon.c 의  vm_anon_init 과 anon_initializer 를 수정해도 된다.

 

void vm_anon_init (void); 

익명 페이지 subsystem을 초기화한다. anonymous page와 관련된 것을 셋팅할 수 있다.

 

bool anon_initializer (struct page *page, enum vm_type type, void *kva);

page->operations 작업에서 익명 페이지에 대한 핸들러를 설정한다. anon_page 는 빈 구조제이므로, 필드를 추가해야 한다. 이 함수는 anonymous page를 초기화하는데 사용된다.(VM_ANON)

 

load_segment와 lazy_load_segment를 구현해라!

모든 페이지들은 커널이 page fault를 발생시킬 때, lazy_loading으로 구현한다.

 

프로그램 로더의 중심이 되는 userprog/process.c의 load_segment에 있는 반복문을 수정해야 한다.

 

이 반복문은 vm_alloc_page_with_initializer를 호출하여 보류 중인 page 객체를 생성한다. 즉, page fault가 발생하면 파일에서 세그먼트가 실제로 로드되는 시점이다. (gitbook에서는 peding object라는 표현을 자주 사용하는데, 아직 메모리에 로드되지 않은 페이지이기 때문이다.)

 

static bool load_segment (struct file *file, off_t ofs, uint8_t *upage, uint32_t read_bytes, uint32_t zero_bytes, bool writable); 

현재 코드는 파일에서 읽을 byte 수와 반복문 내에서 0으로 채울 바이트 수를 계산한다. 그리고 나서 대기중인 객체를 생성하기 위해 vm_alloc_page_with_initializer 를 호출한다. vm_alloc_page_with_initializer 에서 사용할 aux 값을 설정해야 한다. binary loading에 필요한 정보들이 포함된 구조체를 생성할 수도 있다.

 

static bool lazy_load_segment (struct page *page, void *aux); 

load_segment 함수에서 vm_alloc_page_with_initializer 함수의 4번째 인수로 lazy_load_segment가 사용되는 것을 확인할 수 있다. 이 함수는 실행 파일 페이지에 대한 initializer이며 page fault가 발생할 때 호출된다. aux 는 load_segment에서 설정한 정보로, 이 정보를 사용하여 세그먼트를 읽을 파일을 찾고 세그먼트를 메모리로 읽는다.

 

새로운 memory management system에 stack을 할당하기 위해 userprog/process.c에 있는 setup_stack을 수정해야 한다.

 

첫번째 스택 페이지는 레이지 로딩으로 할당하지 않고, 즉시 할당되고 초기화된다. 따라서 page fault가 발생할 때까지 기다리지 않는다.

스택을 사용할 때는 스택을 식별하는 방법이 필요하다. 스택은 프로그램 실행 중에 동적으로 변하기 때문에, 스택 영역을 식별하여 다른 메모리 영역과 구별해야 한다. 이때, vm/vm.h의 vm_type (ex. VM_MARKER_0 ) 을 사용해 페이지를 표시할 수 있다.

 

마지막으로, vm_try_handle_fault 함수를 수정하여 spt_find_page로 supplemental page table을 참조하여 오류 주소에 해당하는 page 구조체를 해결하도록 한다.

 

요구 사항을 모두 구현하면, fork를 제외한 프로젝트2의 모든 테스트가 통과되어야 한다.

 


 

Supplemental Page Table - Revisit

복사와 정리 기능을 구현하기 위해 Supplemental Page Table 인터페이스를 다시 확인해보자.

이 작업은 프로세스를 생성하거나(정확히는 자식 프로세스 생성) 기존 프로세스를 삭제할 때, 필요하다. 이때 이전에 구현한 함수 중 일부를 다시 수정할 수도 있다.

 

vm/vm.c의 supplemental_page_table_copy 와 supplemental_page_table_kill 을 구현한다.

bool supplemental_page_table_copy (struct supplemental_page_table *dst, struct supplemental_page_table *src); 

supplemental_page_table을 src에서 dst로 복사한다. 이 작업은 자식이 부모의 실행 컨텍스트를 상속할 때 발생한다.(ex. fork())
이 과정은 src의 supplemental_page_table을 반복하면서, dst의 supplemental_page_table에 정확하게 복사하는 작업이다. 복사할 때, 복사된 페이지 테이블에 초기화되지 않은 페이지를 할당하고, 즉시 해당 페이지를 현재 프로세스가 소유하도록 한다.

 

void supplemental_page_table_kill (struct supplemental_page_table *spt); 

supplemental page table이 사용하고 있는 리소스를 해제한다. 이 함수는 프로세스가 종료될 때(userprog/process.c의 process_exit() ) 호출된다. 페이지 항목을 반목문으로 돌면서, 테이블의 각 페이지에 대해 destroy(page) 를 호출한다.
이 함수에서는 실제 페이지 테이블(pml4)과 물리 메모리(palloc-ed memory)를 고려하지 않아도 된다. caller가 supplemental page table이 정리된 후에 정리하기 때문이다.

 


 

Page Cleanup

vm/uninit.c의 uninit_destroy 함수와 vm/anon.c 의 anon_destroy 함수를 구현한다.

 

이 부분은 초기화되지 않은 페이지의 destroy 명령을 수행하기 위한 핸들러이다. 아직 초기화되지 않은 페이지가 다른 페이지 객체로 변환되더라도 프로세스가 종료될 때, 여전히 초기화되지 않은 페이지가 남아있을 수 있다.

static void uninit_destroy (struct page *page); 

페이지 구조체가 사용중인 리소스를 해제한다. 페이지의 VM 유형을 확인하고, 유형에 맞게 처리한다.

현재는 anonymous page만 처리하지만, 추후 file-backed page도 처리한다.

 

static void anon_destroy (struct page *page); 

anonymous page가 가지고 있는 리소스를 해제한다. anon_destroy 함수 안에서 page 구조체를 해제하는 것이 아니라, 함수 밖에서 함수 호출자가 페이지 구조체 해제를 수행한다.

이 작업까지 모두 완료되었다면 project2의 모든 테스트가 패스되어야 한다.

 

 

반응형