Adventure Time - Finn 3

새소식

운영체제

[Pintos-Kaist] Project3 - Anonymous Page

  • -

이전 포스트에서는 프로젝트 3의 Memory Management부분에 대해 구현을 했습니다. 마지막 부분에 해당하는 vm_alloc_page_with_initializer라는 함수를 시작으로 나머지 Anonymous Page를 구현 시작해 보도록 하겠습니다. 

 

 

✔️ Anonymous Page란?

 

  • Anonymous 페이지는 파일 기반 페이지와 달리 이름이 지정된 파일 소스가 없기 때문에 Anonymous page라고 부릅니다.
  • Anonymous 페이지는 스택 및 힙과 같은 실행 파일에 사용됩니다.
  • Anonymous 페이지는 프로그램이 실행되는 동안 필요한 메모리 공간을 동적으로 할당하여 프로그램의 요구에 맞게 메모리를 사용할 수 있습니다.
  • Anonymous 페이지는 가상 메모리 공간에서 실제 메모리로 데이터를 옮기거나 실제 메모리에서 가상 메모리로 데이터를 가져오는 데 사용됩니다.

 

 

구현

 

1️⃣ bool vm_alloc_page_with_initializer (enum vm_type type, void *va, bool writable, vm_initializer *init, void *aux)

 

  • 주어진 타입의 초기화되지 않은 페이지를 생성합니다.
  • uninit 페이지의 swap_in 핸들러는 유형에 따라 페이지를 자동으로 초기화하고, 주어진 AUX로 INIT를 호출합니다.
  • 페이지 구조가 완성되면 프로세스의 보조 페이지 테이블에 페이지를 삽입합니다.
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;

	// upage가 이미 사용 중인지 확인합니다.
	if (spt_find_page (spt, upage) == NULL) {
		
		// 새로운 페이지 구조체를 동적으로 할당합니다.
		struct page *page = malloc(sizeof(struct page));

		if (page == NULL)
			goto err;

		// 가상 메모리 타입에 따라 초기화자 함수 포인터를 설정합니다.
		int ty = VM_TYPE (type);
		bool (*initializer)(struct page *, enum vm_type, void *);
		
		switch(ty){
			case VM_ANON:
				initializer = anon_initializer;
				break;
			case VM_FILE:
				initializer = file_backed_initializer;
				break;
	}

		// uninit_new 함수를 호출하여 페이지를 초기화합니다.
        uninit_new(page, upage, init, type, aux, initializer);


		 // 페이지를 보조 페이지 테이블에 삽입합니다.
        if (!spt_insert_page(spt, page)) {
            free(page);
            goto err;
        }

        return true;
	}
err:
	return false;
}

 

 

 

 

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

 

  • 현재 코드는 파일에서 읽을 바이트 수와 메인 루프 내에서 0으로 채울 바이트 수를 계산합니다.
  • 그런 다음 vm_alloc_page_with_initializer를 호출하여 보류 중인 객체를 생성합니다.
  • vm_alloc_page_with_initializer에 제공할 보조 값을 보조 인수로 설정해야 합니다.
  • 바이너리 로딩에 필요한 정보를 포함하는 구조를 생성할 수 있습니다.
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) {
		size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;  
		size_t page_zero_bytes = PGSIZE - page_read_bytes;  

		struct file_loader *file_loader = malloc(sizeof(struct file_loader));
		file_loader->page_read_bytes = page_read_bytes;  // 페이지에 실제로 읽어들일(read_bytes) 바이트 수
		file_loader->page_zero_bytes = page_zero_bytes;  // 페이지에 0으로 초기화할 바이트 수
		file_loader->ofs = ofs;  // 파일 오프셋(ofs)
		file_loader->file = file;  // 파일 포인터(file)
		
		// 익명 페이지(VM_ANON)를 할당하기 위해 vm_alloc_page_with_initializer 함수 호출
		// 할당된 페이지에는 지연 로딩(lazy loading)을 위해 lazy_load_segment 함수를 호출하여 파일 로더(file_loader)를 전달함
		if (!vm_alloc_page_with_initializer (VM_ANON, upage,
					writable, lazy_load_segment, file_loader)){
			free(file_loader);  // 페이지 할당에 실패한 경우 할당된 자원을 해제
			return false;  // 세그먼트 로딩 실패를 나타내는 false 반환
		}

		read_bytes -= page_read_bytes;  // 읽어들인 바이트 수 갱신
		zero_bytes -= page_zero_bytes;  // 초기화할 바이트 수 갱신
		upage += PGSIZE;  // 페이지 주소(upage) 갱신
		ofs += page_read_bytes;  // 오프셋(ofs) 갱신
	}
	return true;  // 세그먼트 로딩 성공을 나타내는 true 반환
}

 

아래의 lazy_load_segment함수를 구현하면 알겠지만 load_segment를 하던 중에 vm_alloc_page_with_initializer함수를 실행하게 되면서 aux값을 뭘 넘겨줘야 할지에 대해 고민을 많이 했었는데, 결국 파일에 대한 내용을 보내줘야 하니 파일의 정보를 담을 구조체하나를 만들어서 구조체를 넘겨주는 식으로 구현을 했습니다.

 

이때 aux를 구조체로 넘겨주고나서 구현이 다 끝났는지 알고 테스트 체크를 할 때 ofs을 갱신 안 해준 상태로 테스트를 하니 테스트가 다 fail 뜨는 것을 확인할 수 있었습니다. 이 부분을 찾으려고 주소를 다 printf로 찍어보는 과정을 통해 겨우 찾아낼 수 있었습니다. 

 

이와 관련된 Trouble shooting은 주간 발표 때 정리해 둔 github discussion링크를 통해 확인할 수 있습니다.(링크는 포스트 맨 아래)

 

 

 

 

3️⃣ static bool lazy_load_segment (struct page *page, void *aux)

 

  • 이 함수는 실행 파일의 페이지에 대한 이니셜라이저이며 페이지 오류가 발생할 때 호출됩니다.
  • 페이지 구조체와 aux를(file_loader) 인자로 받습니다. 
  • 이 정보를 사용하여 세그먼트를 읽을 파일을 찾아서 결국 세그먼트를 메모리로 읽어야 합니다.
static bool
lazy_load_segment (struct page *page, void *aux) {

	struct file_loader *file_loader = (struct file_loader*)aux;  
	struct file *file = file_loader->file;  
	off_t ofs = file_loader->ofs; 
	uint8_t *upage = page->va;  
	uint32_t page_read_bytes = file_loader->page_read_bytes;  
	uint32_t page_zero_bytes = file_loader->page_zero_bytes; 
	

	file_seek(file, ofs);  // 파일을 오프셋(ofs) 위치로 이동
	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);  // 읽어들인 바이트 이후의 메모리를 0으로 초기화
	memcpy(page->va, page->frame->kva, PGSIZE);  // 페이지의 가상 주소에 로드된 데이터 복사

	return true;
}

 

 

 

4️⃣ static bool setup_stack(struct intr_frame *if_)

 

  •  로드 시점에 명령줄 인수를 사용하여 할당하고 초기화할 수 있으므로 결함이 발생할 때까지 기다릴 필요가 없습니다. 
  • 스택을 식별하는 방법을 제공해야 할 수도 있습니다.
  • vm_ 유형에 있는 보조 마커(예: VM_MARKER_0)를 사용하여 페이지를 표시할 수 있습니다.

 

static bool setup_stack(struct intr_frame *if_) {
    bool success = false;
    void *stack_bottom = (void *) (((uint8_t *) USER_STACK) - PGSIZE);

	// 페이지를 할당하여 스택의 최하단에 해당하는 메모리 영역을 확보
	// 페이지 타입은 익명 페이지(VM_ANON)와 표시 페이지(VM_MARKER_0)를 OR 연산하여 사용
	// 스택 페이지는 쓰기 가능(writable)한 속성을 갖도록 설정
	if (!vm_alloc_page(VM_ANON | VM_MARKER_0, stack_bottom, writable)) {
		return false; 
	}

	// 페이지를 소유하고 있는 스레드의 페이지 테이블(pml4)에 스택 페이지를 등록
	// 스택 페이지를 주소 공간에 실제로 차지한 것으로 표시
	if (!vm_claim_page(stack_bottom)) {
		vm_dealloc_page(stack_bottom); 
		return false;  
	}

	if_->rsp = USER_STACK;  // 스택 포인터(if_->rsp)를 USER_STACK으로 설정하여 스택 시작 지점을 가리킴
	success = true; 

    return success;
}

 

 

 

5️⃣ bool supplemental_page_table_copy (struct supplemental_page_table *dst, struct supplemental_page_table *src)

 

  • supplemental page table src에서 dst로 복사합니다.
  • 자식이 부모의 실행 컨텍스트를 상속해야 할 때 사용됩니다(예: 포크()). src의 보충 페이지 테이블에 있는 각 페이지를 반복하고 dst의 보충 페이지 테이블에 있는 항목의 정확한 복사본을 만듭니다.
  • uninit 페이지를 할당하고 즉시 클레임해야 합니다.

 

bool supplemental_page_table_copy(struct supplemental_page_table *dst, struct supplemental_page_table *src) {
    struct hash_iterator i;
    struct hash *src_hash = &src->pages;
    struct hash *dst_hash = &dst->pages;

    hash_first(&i, src_hash);
    while (hash_next(&i)) {
        struct page *src_page = hash_entry(hash_cur(&i), struct page, hash_elem);

        enum vm_type type = src_page->operations->type;
        if (type == VM_UNINIT) {  // 초기화되지 않은 페이지(VM_UNINIT)인 경우
            struct uninit_page *uninit_page = &src_page->uninit;
            struct file_loader* file_loader = (struct file_loader*)uninit_page->aux;

            // 새로운 파일 로더(new_file_loader)를 할당하고 기존의 파일 로더 정보를 복사
            struct file_loader* new_file_loader = malloc(sizeof(struct file_loader));
            memcpy(new_file_loader, uninit_page->aux, sizeof(struct file_loader));
            new_file_loader->file = file_duplicate(file_loader->file);  // 파일을 복제하여 새로운 파일 포인터를 생성

            // 초기화할 페이지에 신규 파일 로더를 이용하여 초기화할 페이지 할당
            vm_alloc_page_with_initializer(uninit_page->type, src_page->va, true, uninit_page->init, new_file_loader);
            vm_claim_page(src_page->va);  // 페이지를 소유하고 있는 스레드의 페이지 테이블(pml4)에 페이지 등록
        } else {  // 초기화된 페이지인 경우
            vm_alloc_page(src_page->operations->type, src_page->va, true);  // 페이지 할당
            vm_claim_page(src_page->va);  // 페이지를 소유하고 있는 스레드의 페이지 테이블(pml4)에 페이지 등록
            memcpy(src_page->va, src_page->frame->kva, PGSIZE);  // 페이지의 가상 주소에 초기화된 데이터 복사
        }
    }
    
    return true;
}

 

 

6️⃣ void supplemental_page_table_kill (struct supplemental_page_table *spt)

 

  • 보조 페이지 테이블이 보유하고 있던 모든 리소스를 해제합니다.
  • 이 함수는 프로세스가 종료될 때 호출됩니다.
  • 페이지 항목을 반복하고 테이블의 페이지에 대해 destroy(page)를 호출해야 합니다. 
  • 이 함수에서 실제 페이지 테이블(pml4)과 물리적 메모리(팔로잉된 메모리)는 걱정할 필요가 없습니다.
void supplemental_page_table_kill(struct supplemental_page_table *spt) {
    // 보조 페이지 테이블의 모든 엔트리를 제거하고, 관련 자원을 해제하는 함수입니다.

    // hash_action_destroy 콜백 함수를 사용하여 각 엔트리의 자원을 해제
    hash_clear(&spt->pages, hash_action_destroy);
}

 

void hash_action_destroy(struct hash_elem* hash_elem_, void *aux) {
    // 해시 테이블 엔트리에 대해 수행되는 콜백 함수입니다.
    // 엔트리에 대한 자원을 해제하고, 페이지를 제거합니다.

    struct page* page = hash_entry(hash_elem_, struct page, hash_elem);

    if (page != NULL) {
        destroy(page);  // 페이지에 할당된 자원을 해제하는 함수 호출
        free(page);  // 페이지 자체의 동적 메모리 해제
    }
}

 

 

위에서 보조 페이지 테이블을 제거하는 함수인 supplemental_page_table_kill을 구현할 때 hash_clear라는 함수를 사용해서 bucket에 있는 모든 엔트리를 제거합니다. hash_clear를 할 때 콜백 함수로 hash_action_destroy라는 함수를 만들어서 사용합니다.

hash_action_destroy는 요구사항에 맞게 destroy(page)를 해서 페이지에 할당된 자원을 해제하고 마지막으로 페이지의 동적 메모리 할당된 것을 해제하기 위해 free(page)를 사용합니다.

 

 

위의 함수들을 다 구현하면  아래 사진과 같이 프로젝트 2에서 구현한 대부분의 테스트를 통과한 것을 확인할 수 있습니다.

이번 Anonymous Page를 구현하면서 정리한 개념과 관련 트러블 슈팅에 대해서 적어놓은 github discussion 주소입니다.

관련 내용과 구현된 코드를 확인해볼 수 있습니다.

https://github.com/Blue-club/pintos2_Team3/discussions/8

 

WIL('23.06.12. ~ '23.06.19.) · Blue-club/pintos2_Team3 · Discussion #8

💡3조 WIL ('23.06.12. ~ '23.06.19.) 팀원 : 박희은, 박윤찬, 홍윤표 박희은 활동 1줄 요약 : 깃북, 구현시도 -> 이해도 부족 확인 -> OSTEP 읽기(13장 ~ 24장) 일정 06.12(월) ~ 06.13(화) : GITBOOK Introduction 및 관련

github.com

 

 

Test 중간 결과

 

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.