정글/Pintos

[pintos] 3주차 - 유저풀, 커널풀 / pml4 / KVA, UVA, KERN_BASE

nkdev 2025. 6. 5. 11:38

VM 프로젝트 문서를 정리하다가 3일이 지났는데 아직 다 못 봤습니당....

내용이 굉장히 많으네요..

 

https://casys-kaist.github.io/pintos-kaist/project3/introduction.html

 

Introduction · GitBook

Locate the page that faulted in the supplemental page table. If the memory reference is valid, use the supplemental page table entry to locate the data that goes in the page, which might be in the file system, or in a swap slot, or it might simply be an al

casys-kaist.github.io

 

가상 주소 공간

x86-64 cpu는 연산을 위해 64비트를 쓸 수 있는데, 그 중 48비트만 사용해서 가상 메모리 주소를 표현한다.

(0 ~ 2^48-1 개만 사용해도 충분히 가상 주소 전체를 표현할 수 있기 때문이다.)

 

가상 주소의 48비트는 두 부분으로 나뉜다.

  • VPN (상위 36비트) : 어떤 페이지인지
  • VPO (하위 12비트) : 그 페이지 중 어느 곳에 있는 데이터인지

그리고 이 48비트의 가상 주소 하나는 물리 공간의 1byte짜리 데이터를 가리킬 수 있다!

 

왜 '가리킬 수 있다'라고 했냐면

가리키고있을 수도 있고 안 가리키고있을 수도 있기 때문이다.

 

이전 글

https://nkdev.tistory.com/181

 

[pintos] 3주차 - Virtual Memory Unallocated/Cached/Uncached 상태, Anonymous page, Lazy Loading

Unallocated page에 대한 고찰... 이틀째 ㅎ 나는 왜 vm 구현도 안 하고 이런거에만 집착하는 걸까 ㅠㅠ으아악...그래도 이 개념을 정리하고 넘어가고 싶어서 내가 이해한 만큼만 정리해본다. VM page의

nkdev.tistory.com

을 보면 알겠지만 가상 주소 하나가 물리 공간의 데이터를 아직 가리키고 있지 않을 수도 있다. 헤헤

PML4

64비트 시스템의 가상 주소는 0 ~ 2^48-1개 즉 512TB 만큼이나 있다... 가상 주소의 개수가 너어무 많으므로 페이지 테이블 개수도 늘어날 수밖에 없다. 그래서 페이지 테이블을 4단계로 나눠서 탐색해야 한다. 그 중 맨 위에 있는 것이 PML4이다.

 

PML4(page-map level 4) - PDP(page-directory pointer) - PD(page-directory) - PT(page-table)

페이지 테이블이 이렇게 네 단계로 구성되어 있고, MMU는 가상 주소를 가지고 이 페이지 테이블을 walking하면서 물리 주소를 알아낸다.

 

PML4 페이지 테이블 안에는 512개의 엔트리가 있고, 각 엔트리는 다음 단계인 PDP의 물리 주소를 담고 있다.

PML4[i] -> PDP의 물리 주소 -> ... 이런 식으로 인덱스를 가지고 페이지 테이블을 단계적으로 탐색하여 물리 주소를 알아낸다.

 

이동석 코치님께서 Pintos Project3을 구현하기 위해서 pml4의 내부 탐색 원리에 대해서는 깊게 이해할 필요는 없고, Pintos는 pml4 방식으로 가상 주소-물리 주소를 변환한다는 것까지만 알고 구현에 사용할 수 있는 것이 더 중요하다고 하셔서 더 깊게는 공부하지 않겠다.

(사실 지금 이것까지 하면 나 또 해뜨고 기숙사 가야됨(ㅠ_ㅠ))

 

Pintos에 어떻게 구현되어있는지 살펴보자!

threads/mmu.c에 pml4 페이지 테이블을 사용하는 함수가 300줄이나 구현되어있다. 그 중 2개만 보겠따 .

 

pml4_get_page()pml4를 통해 유저 가상 주소와 매핑된 물리 프레임을 찾는다.

pml4에 대응되는 물리 주소가 매핑되어 있으면 커널 가상 주소를 반환하고, 물리 주소가 매핑되어있지 않으면 null pointer을 반환한다.

/* 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); //pml4e_walk()는 인자로 받은 vaddr의 pte 주소를 반환함.

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

 

pml4_set_page()pml4에 upage, kpage 매핑 정보를 추가한다.

upage(유저 가상 페이지)kpage(커널 가상 페이지)에 대응되는 물리 프레임이 매핑된다.

upage는 아직 매핑되지 않은 상태이고, kpage는 palloc_get_page()로 유저 풀에 할당된 페이지이다.

rw라는 boolean값은 read/write인지 read-only인지 지정하는 플래그이다.

/* Adds a mapping in page map level 4 PML4 from user virtual page
 * UPAGE to the physical frame identified by kernel virtual address KPAGE.
 * UPAGE must not already be mapped. KPAGE should probably be a page obtained
 * from the user pool with palloc_get_page().
 * If WRITABLE is true, the new page is read/write;
 * otherwise it is read-only.
 * Returns true if successful, false if memory allocation
 * failed. */
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;
}

pml4_get_page(), pml4_set_page()는 둘 다 pml4e_walk()를 사용하여 물리 주소를 구한다.

pml4e_walk()는 va를 받아서 pml4를 탐색 후 va에 해당하는 pte 주소를 반환한다. 

 

User Pool, Kernel Pool

Pintos에서 물리 메모리는 두 개의 풀로 나뉜다.

  • 유저풀 : 유저 가상 페이지가 물리 메모리 프레임과 매핑될 때 사용되는 영역
  • 커널풀 : 커널 가상 페이지가 물리 메모리 프레임과 매핑될 때 사용되는 영역

유저 풀과 커널 풀을 따로 나눠놓은 이유는, 사용자 프로그램을 실행하는 것이 젤 중요한데 커널이 물리 메모리를 너무 많이 차지해버리면 안 되니까. 즉 유저 프로그램을 위한 메모리 공간충분히 보장하기 위함이다!

 

물리 메모리의 유저 풀에는 사용자 프로그램 관련 데이터가 적재되고, 커널 풀에는 커널 동작과 관련된 데이터가 적재된다.

 

그림으로 보면 다음과 같다. 앞으로 주구장창 보게 될 그림.. 이동석 코치님이 제공해주신 그림이다.

아직 모든 부분을 이해하지 못했지만 개념과 코드를 하나씩 파다 보면 언젠가 완전히 이해할 수 있지 않을까?

 

 

 

 

 

유저풀, 커널풀을 이해하기 위해 이 그림의 일부만 보자.

양쪽 공간은 주소가 1:1로 매핑된다 !!!!

내가 이해한 대로 다시 정리해봤다!

 

 

이렇게 물리 공간의 풀에 페이지를 할당하는 과정이

Pintos 코드에는 어떻게 구현되어있을까?

 

물리 공간에 페이지를 할당할 때 palloc_get_multiple()함수가 쓰인다. 인자로 유저풀, 커널풀 중 어디에 할당할 것인지에 대한 flag정보를 넘기면 flag값과 PAL_USER을 비트 연산하여 유저풀인지 커널 풀인지 구분한다.

void *
palloc_get_multiple (enum palloc_flags flags, size_t page_cnt) {
	struct pool *pool = flags & PAL_USER ? &user_pool : &kernel_pool;
    //flags값이 PAL_USER 비트를 모두 포함하면 user_pool, 아니면 kernel_pool
    ...
}

 

이 함수가 쓰이는 실행흐름 중 하나를 정리해보았다. pml4_get_page()로 물리 주소를 찾았는데 없을 때, pte를 물리 공간에 하나 추가하기 위해 페이지를 할당할 때 내부적으로 사용된다.

 

KVA, UVA, KERN_BASE

Pintos에서 프로세스 가상 주소는 KERN_BASE 기준으로 두 가지로 나뉜다.

/* Physical address of kernel base. */
#define LOADER_KERN_BASE 0x8004000000

include/threads/loader.h에 LOADER_KERN_BASE값이 매크로로 정의되어있다.

가상 주소값이 0X8004000000값보다 크면 커널 영역, 작으면 유저 영역이다.

 

커널 가상 영역은 모든 프로세스가 접근할 수 있는 전역적인 공간이다. 

유저 가상 영역은 각 프로세스만 독립적으로 접근할 수 있는 프라이빗한 공간이다.

 

(1) KERN_BASE 이상 - 커널 영역

커널 영역의 가장 중요한 특징은 커널 모드로 전환된 이후!! 접근 가능한 곳이라는 것이다.

그림에 나와있다시피 커널 영역에는 커널 코드와 커널 데이터가 있는데, 커널이 뭔가 작업을 해야할 때 얘네를 사용해야 하기 때문에 있을 것이고.. bootstrap은 운영체제가 처음 부팅될 때만 쓰이는 데이터인데 얘는 프로세스 실행하다가 갑자기 쓸 일이 있어서 커널 가상 메모리에 매핑되어있는 건 아닐 것이다. 그러면 왜 있으까...?

 

커널 영역의 가상 주소는 물리 메모리 주소와 1:1로 매핑되기 때문에 존재하는 것이다.

양쪽 공간은 주소가 1:1로 매핑된다 !!!!

 

그러니까  메모리랑 1:1로 주소가 매핑되어야 하는데 일단 메모리에 있으니까... 어차피 가상 주소는 가상환경이라서 할당해줘도 아무 비용이 드는 것도 아니니까. 메모리 레이아웃 일관성을 유지하기 위해 그냥 커널 가상 주소도 bootstrap을 가지고 있다 정도로 이해하면 되겠다!

 

근데 피티쌤한테 물어보니까 일부 시스템에서는 부팅 완료가 되면 bootstrap 코드가 사용하던 메모리 영역을 해제하고 다른 용도로 재사용하는 경우도 있다고 하는데, Pintos에서는 특별한 정리 작업 없이 그냥 커널 영역으로 포함된 채 남아있어서 이렇게 표현된 거라고 한다.

 

오호. bootstrap이 뭔지는 딱히 중요하지 않지만, 얘가 굳이 왜 커널 가상 메모리와 매핑되어 있는지 찾아보면서 '커널 가상 메모리와 물리 메모리의 1:1 매핑'이라는 개념을 이해할 수 있게 되었다. 

 

⭐️ 커널 영역의 가상 주소는 물리 메모리 영역과 1대 1로 매핑된다 ⭐️ 꼬옥 기억하기...

요 특징 덕분에 커널 가상 주소 <-> 물리 메모리 주소 변환을 하는 과정이 굉장히 쉽고 직관적이다.

 

include/threads/vaddr.h파일에 KERN_BASE를 사용한 매크로가 굉장히 많은데,

그 중 ptov(), vtop()가 둘을 서로 변환해주는 함수다. 어떻게 정의되어있냐몬,,

/* Returns kernel virtual address at which physical address PADDR
 *  is mapped. */
#define ptov(paddr) ((void *) (((uint64_t) paddr) + KERN_BASE))

/* Returns physical address at which kernel virtual address VADDR
 * is mapped. */
#define vtop(vaddr) \
({ \
	ASSERT(is_kernel_vaddr(vaddr)); \
	((uint64_t) (vaddr) - (uint64_t) KERN_BASE);\
})
  • ptov() : 물리 주소를 커널 가상 주소로 변환하는 매크로
  • vtop() : 커널 가상 주소를 물리 주소로 변환하는 매크로

이렇게...!

 

함수를 잘 뜯어보면 

  • 커널 가상 주소 = 물리 주소 + KERN_BASE
  • 물리 주소 = 커널 가상 주소 - KERN_BASE

요게 성립한다는 것을 알 수 있다.

.

.

 

그리고 커널 영역의 또 다른 특징으로 자주 언급되는 것! 내가 제일 이해하기 어려웠던 말.....

커널 영역은 모든 프로세스가 공유한다.

 

프로세스 가상 주소 공간의 KERN_BASE 위쪽 ‘커널 영역’을 얘기하는 거 맞다.

여기를 모든 프로세스가 접근할 수 있다고 한다.

 

엥 이상하다...

커널 영역은 프로세스 간에 공유된다고?

커널 영역의 User Pool에는 그 프로세스만의 독립적인 정보가 있을 텐데..

왜...그걸 공유해? 

 

VM 프로젝트를 하는 내내 주변에서 절~~~~대로 잊지 말라고 하는 개념이 하나 있다.

코치님도, 우리반 교수님들도, 핀토스를 공부하신 분들의 블로그에서도 매우 강조하는 개념..!

 

가상 주소 공간은 '가상'공간이다.
가상 공간에는 실제로 데이터가 저장되어있지 않다.

 

커널 영역은 가상 공간이다. 가상 공간을 쉽게 이해하기 위해서 마치 가상 공간에 값들이 저장된 것처럼 그림을 그릴 뿐이지, 사실은 주소만 존재한다. 가상으로 만들어낸 공간이기 때문에 실제로 데이터가 저장될 수 없다. 

 

따라서 여러 프로세스가 공유하는 것커널 영역의 '주소'이다. 모든 프로세스가 커널 영역의 어떤 주소든 접근할 수 있기 때문에 커널 영역이 여러 프로세스 간에 공유된다고 말하는 것이고, 어떤 주소든 접근은 할 수 있지만 그 주소를 다른 프로세스가 사용하고 있어서 접근 권한이 없거나 하는 문제가 있을 순 있다. 이건 cpu가 해당 가상 주소에 접근하려고 할 때 OS가 페이지 테이블(정확하게는 spt)을 참조해서 유효하지 않은 가상 주소라고 판단하여 segmentation fault를 발생시키거나 palloc으로 새로 할당하거나 disk 데이터를 메모리로 적재하는 등 적절한 조치가 일어날 것이다.

 

결론적으로, 커널 영역은(정확히 말하면 커널 영역의 가상 '주소'는) 모든 프로세스 간에 공유되고 있고, 실제로 데이터에 접근하려면 각 프로세스마다 갖고 있는 페이지 테이블을 참조해야하기 때문에 프로세스간 독립성이 보장되는 것이다.

 

이번에도 내가 이해한 대로 정리해봤다.

이틀 동안은 혼자 2층에 가서 하루종일 이 부분을 이해하려고 끙끙거렸던 것 같다. ㅋㅋ 코어타임이든 강의든 블로그든 다른 사람이 설명해주는 말만 듣고는 쉽게 와닿지 않았는데, 끝내 이해하고 팀원들과 함께 그 주제에 대해 토론할 수 있게 되어 정말 뿌듯하고 좋았다. (물론 내가 이해한 지식도 틀릴 수 있다.. 틀린 부분은 댓글로 날카롭게 지적 부탁드려요)

 

(2) KERN_BASE 이하 - 유저 영역

 

 

유저 영역은 유저 프로세스가 실행될 때 유저 모드에서 사용하는 공간이다.

커널의 처리가 필요하면 시스템 콜을 통해 커널 모드로 진입하고, 그 후에는 위에서 설명한 것과 같이 커널 가상 공간에서 작업이 이루어진다.

 

 

문서가 너무 길다... 일단 구현 요구사항 먼저 정리하러 가야겠다!