[SW사관학교 정글] Week10. PintOS Virtual Memory

Youngeui Hong·2023년 10월 16일
0

SW사관학교 정글 7기

목록 보기
11/16
post-custom-banner

💚 Introduction

지난 주에는 User Program을 구현하면서 여러 개의 프로세스가 하나의 컴퓨터에서 동작할 수 있도록 했다. 그러면 하나의 물리 메모리를 여러 프로세스가 동시에 바라보는 상황이 되는데, 어떻게 하면 물리 메모리를 효과적으로 공유할 수 있을까?

운영체제는 각 프로세스마다 전용 주소 공간을 제공하기 위해 주소공간을 가상화하는데, 이것이 바로 가상 메모리이다.

💾 PintOS의 메모리 구조

PintOS의 메모리는 대략 위의 그림과 같은 구조로 이루어진다.

그림을 봤을 때 대략 이해했다고 생각했는데, 막상 코드를 짜다보니 Virtual Memory와 Physical Memory의 관계, 그리고 Kernel Virtual Memory 영역과 User Virtual Memory 영역의 구분 등에 있어서 헷갈리는 부분들이 있었다.

그래서 각각의 메모리 영역이 어떻게 초기화되고 로드되는지 살펴보았다.

1) Physical Memory

physical memory의 경우 palloc_init() 함수에서 초기화하고 있었다.

resolve_area_info() 함수는 물리 메모리에 대한 정보를 담고 있는 E820 entry를 읽어서 메모리 영역의 범위를 설정한다.

여기에서 base_mem은 loader나 kernel code를 포함해서 컴퓨터 운영에 필수적인 기본 정보가 담기는 영역이고, ext_mem은 palloc 등을 사용해서 커널이 자유롭게 메모리를 할당해서 사용할 수 있는 영역이다.

핀토스 테스트 코드를 실행해보면 물리 메모리의 각 영역에 얼마만큼의 용량이 할당되어 있는지 확인할 수 있다.

2) kernel Virtual Memory

📌 KERN_BASE

kernel virtual memory와 관련해서 궁금했던 점은 어떻게 KERN_BASE를 기준으로 user 메모리 영역과 kernel 메모리 영역이 구분될 수 있는지였다.

이와 관련된 내용은 kernel.lds.S에서 확인할 수 있었다. 이 파일은 커널의 컴파일과 로드에 사용되는 linker script 파일이다.

이 코드의 상단을 보면 아래와 같이 kernel base의 가상 주소를 지정해주는 부분이 있다. LOADER_KERN_BASE + LOADER_PHYS_BASE만큼 떨어진 부분을 kernel 영역의 시작점으로 삼고 있기 때문에, 0 ~ KERN_BASE 영역은 user 메모리 영역이 되고, KERN_BASE ~ 나머지 공간은 kernel 메모리 영역이 되는 것이다.

  /* Specifies the virtual address for the kernel base. */
	. = LOADER_KERN_BASE + LOADER_PHYS_BASE;

📌 세그먼트별 메모리 위치

그리고 추가로 궁금했던 내용은 user virtual memory에서 text, data, bss 세그먼트의 메모리 위치가 어떻게 정해지는지였는데, 이 내용도 kernel.lds.S에서 확인할 수 있었다.

text의 세그먼트의 시작 위치는 LOADER_PHYS_BASE로 지정되어 있었고, data와 bss 세그먼트의 시작 위치는 별도로 지정되어 있지 않아서 앞 세그먼트가 종료되는 지점 다음에 위치하게 되어 있었다.

따라서 위 그림과 같이 text, data, bss 세그먼트가 나란히 위치할 수 있는 것이었다.

  /* Kernel starts with code, followed by read-only data and writable data. */
	.text : AT(LOADER_PHYS_BASE) {
		*(.entry)
		*(.text .text.* .stub .gnu.linkonce.t.*)
	} = 0x90

📌 User Pool과 Kernel Pool

PintOS는 전체 메모리 영역을 절반으로 나누어서 kernel pool과 user pool에 할당한다. 관련 코드는 palloc_init()populate_pools()에서 확인할 수 있다.

이처럼 kernel pool 영역과 user pool 영역으로 나누어서 관리하는 이유는 커널이 작업하는 데에 필요한 최소한의 메모리를 보장 받기 위해서이다.

populate_pools()의 코드를 살펴보니 몇 가지 헷갈리는 점이 생겨서 조교님께 질문을 드렸는데, 자세히 알려주셔서 많이 배울 수 있었다.

👀 질문

🤓 답변

3) User Virtual Memory

User Virtual Memory의 text, data, bss 영역에 실제 데이터를 로드하는 것은 load_segment 함수에서 이루어지고, stack 영역을 셋팅하는 것은 setup_stack 함수에서 이루어진다.

load_segment 함수와 setup_stack을 구현하는 것은 이번 과제 중 하나였으므로 다음 절에서 좀 더 자세히 살펴보자.

📄 Page와 Frame

PintOS에서는 실제 physical memory를 구성하는 단위를 Frame이라 하고, virtual memory를 구성하는 단위를 Page라고 한다.

가상 메모리를 잘 관리하기 위해서는 이들 간의 관계를 잘 추적하는 것이 필요하다.

PintOS에서는 pml4가 물리 메모리와 가상 메모리 간의 매핑을 담당한다.

🔍 PML4

PML4 (Page-Map Level 4)은 가상 메모리 주소와 물리 메모리 주소 간의 매핑 정보를 저장하는 방식 중 하나로, x86 아키텍처의 메모리 관리 시스템에서 사용된다.

PML4는 페이지 테이블 계층 구조의 최상위 레벨을 나타내며, 페이지 테이블 계층 구조는 다음과 같이 구성된다.

각각의 상위 계층 테이블은 다른 페이지 테이블 계층을 가리키는 엔트리들을 포함한다.

PintOS에서는 pml4_create 함수를 호출하여 PML4 테이블을 위한 메모리 공간을 할당 받는다.

uint64_t *
pml4_create (void) {
	uint64_t *pml4 = palloc_get_page (0);
	if (pml4)
		memcpy (pml4, base_pml4, PGSIZE);
	return pml4;
}

그리고 context switching에 사용되는 process_activate 함수에서는 pml4_activate 함수를 호출하는데, pml4_activate 함수는 CR3 레지스터에 PML4 테이블의 시작 주소를 로드한다.
(CR3 (Control Register 3) 레지스터는 x86 아키텍처에서 페이지 테이블 주소를 저장하는 데 사용되는 레지스터이다.)

즉, 현재 프로세스에서 사용되는 pml4 테이블을 cr3 레지스터에 로드함으로써, 해당 프로세스의 가상 주소 공간 정보를 가져오게 되는 것이다.

/* Loads page directory PD into the CPU's page directory base
 * register. */
void
pml4_activate (uint64_t *pml4) {
	lcr3 (vtop (pml4 ? pml4 : base_pml4));
}

그리고 가상 메모리 주소에 해당되는 물리 메모리 주소를 저장하려고 할 때는 pml4_set_page 함수를 호출한다.

이 함수의 내용을 보면 먼저 pml4e_walk 함수를 사용해서 PML4 테이블에서 upage, 즉 user virtual address에 해당하는 페이지 테이블 엔트리를 찾아온다.

그리고 vtop 매크로를 사용해서 kernel virtual address를 physical address로 변환하여 그 값을 페이지 엔트리의 값으로 저장해준다.

bool
pml4_set_page (uint64_t *pml4, void *upage, void *kpage, bool rw) {
	ASSERT (pg_ofs (upage) == 0);
	ASSERT (pg_ofs (kpage) == 0);
	ASSERT (is_user_vaddr (upage));
	ASSERT (pml4 != base_pml4);

	uint64_t *pte = pml4e_walk (pml4, (uint64_t) upage, 1);

	if (pte)
		*pte = vtop (kpage) | PTE_P | (rw ? PTE_W : 0) | PTE_U;
	return pte != NULL;
}

그리고 user virtual address에 맵핑된 물리 메모리의 kernel virtual address를 가지고 올 때에는 마찬가지로 pml4e_walk 함수를 사용해서 upage에 해당되는 페이지 엔트리를 찾아온 다음, 이 페이지 테이블 엔트리에 저장된 물리 주소를 ptov 매크로를 사용해서 kernel virtual address로 변환하여 리턴한다.

/* Looks up the physical address that corresponds to user virtual
 * address UADDR in pml4.  Returns the kernel virtual address
 * corresponding to that physical address, or a null pointer if
 * UADDR is unmapped. */
void *
pml4_get_page (uint64_t *pml4, const void *uaddr) {
	ASSERT (is_user_vaddr (uaddr));

	uint64_t *pte = pml4e_walk (pml4, (uint64_t) uaddr, 0);

	if (pte && (*pte & PTE_P))
		return ptov (PTE_ADDR (*pte)) + pg_ofs (uaddr);
	return NULL;
}
post-custom-banner

0개의 댓글