6 minute read

pintos 3주차 배운점 정리

가상메모리

가상메모리는 운영체제의 핵심 디자인인 protection, isolation, sharing space를 위해 필수적으로 사용되는 추상화이다. 모든 유저 프로세스, 심지어 커널코드 자체도 가상메모리 환경의 가상주소 상에서 동작함.

이에 대한 실제 물리주소로의 번역은 MMU(memory manage unit)라는 하드웨어가 지원해준다. 단 mmu가 이를 위해 ‘페이지 테이블’을 walking 하는데, 이 페이지테이블을 구성하는 것은 여전히 소프트웨어, 즉 커널의 몫이다.

커널은 자신의 주소공간에 프로세스별 페이지 테이블을 구성해놓고, 각 프로세스가 실행 될 때마다 CR3레지스터에 해당 페이지 테이블 주소를 적어줌으로써, mmu가 그 페이지 테이블을 walking 할 수 있도록 해준다.

walking 한다는 의미는 페이지 테이블의 엔트리들을 타고 들어가서, 최하단 페이지 테이블의 page table entry 를 찾아 물리 주소를 알아낸다는 것이다.

스크린샷 2023-10-17 04.43.25

주의해야 할 점은, 페이지테이블의 엔트리에는 ‘물리주소’가 적혀야 한다는 점이다. 하드웨어인 MMU는 가상주소라는 개념을 이해하지 못한다. 단지 그 PTE에 적힌 어떤 값(물리주소)를 참고해서 자신의 임무를 수행할 뿐이다.

64비트 컴퓨터의 등장으로 페이지 테이블 개수가 증가할 수 밖에 없었다. 이중 48비트만 사용한다고 해도 페이지 테이블의 4단계 구성을 지녀야 한다. 4단계의 페이지 테이블 구성을 통해 가능한 PTE의 개수가 2^9 * 2^9 * 2^9 * 2^9 = 2^36개가 되고 페이지 오프셋 갯수 2^12를 곱해주면, 2^48..무려 281테라가 된다. 그래서 4단계의 페이지 테이블 구성을 통해 64비트 컴퓨터의 가상 주소가 동작한다. 각 단계별로 페이지 테이블 이름이 존재한다.

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

스크린샷 2023-10-17 04.43.25

이렇게 페이지 테이블이 구성 되어있을때, MMU는 어떻게 이 페이지 테이블을 walking 하면서 주소를 번역하는 것일까.가상 주소 ‘값’에 해답이 있다. 가상 주소는 위에서 언급한 단계별 페이지 테이블의 인덱스 정보를 가진 어떤 값이다. 따라서 MMU는 어떤 가상 주소를 가지고, 그 안에 포함된 인덱스 정보를 활용하여 walking 한 후, 해당하는 PTE를 찾을 수 있다. 자세한 예시는 pintos 구현을 살펴보면서 정리해보려고 한다.

LOADER_KERN_BASE

pintos에서 가상 주소 공간은 2가지로 나뉜다. 커널 가상 주소(kernel virtual address)와 유저 가상주소(user virtual address)

그 둘 사이의 구분은 LOADER_KERN_BASE 라는 매크로를 통해 정의하는데 그 값은 0x8004000000 이다. 즉 커널 가상 주소는 LOADER_KERN_BASE 값보다 같거나 큰 주소를 의미하게 된다.

앞서 말했듯이 가상주소는 단계별 페이지 테이블의 인덱스 정보를 담고 있다. LOADER_KERN_BASE 또한 마찬가지이다.

스크린샷 2023-10-17 04.43.25

그림에서 보이듯 0x8004000000 라는 가상주소는 pml4의 offset이( 테이블에서의 인덱스가) 1로 설정되어있는 주소이다. 또한 PD의 offset이 32로 설정되어있는 주소이다. 그래서 커널 가상 주소를 이용한다는 말은 곧 MMU가 위의 offset에 해당하는 walking 을 하면서 PTE를 찾게 된다는 말과 같게 된다.

그렇다면 LOADER_KERN_BASE = 0x8004000000 보다 작은 주소에 해당하는 유저 가상 주소의 인덱스 정보는 어떨까.

스크린샷 2023-10-17 04.43.25pml4의 offset은 여전히 1이지만, PD의 오프셋이 31로 감소한것을 알 수 있다. 물론 주소가 더 감소한다면 PD offset, pt offset 둘다 감소하게 될것이다. 결론은 LOADER_KERN_BASE 라는 기준점에 따라서 페이지 테이블을 walking하는 경로또한 구분될 수 있다는 것이다.

pml4

이렇게 자세히 알아본 이유는 각 프로세스가 고유로 가지는pml4의 생성 과정을 파악하기 위해서였다. 결론부터 말하면 커널은 base_pml4 라는 기본 바탕이 되는 pml4를 부팅시에 만들어 전역변수로 등록해두었다가, 각 프로세스가 자신의 pml4를 생성하고자 할때, 빈 페이지에 base_pml4의 값을 복사해준 후 그 페이지를 프로세스의 고유 pml4로 등록해준다.

하지만 커널 가상 주소에 대한 페이지 테이블 정보는 모든 프로세스가 같다. 커널은 프로세스와 다르게 자신의 개별적인 pml4를 가질 필요가 없다. 단지 각 프로세스의 pml4에서 LOADER_KERN_BASE 이상인 주소에 해당하는 PTE가 커널 가상 주소에 맞게 작성이 되어있으면 된다.

커널이 base_pml4를 만드는 과정

스크린샷 2023-10-17 04.43.25

palloc_get_page()와 PAL_ZERO를 통해 0으로 초기화된 빈 페이지를 할당받고 그것을 base_pml4로 삼아서 페이지 테이블 작성을 시작한다. 반복문을 통해 페이지 테이블을 쭉 작성하게 된다. LOADER_KERN_BASE~ LOADER_KERN_BASE + mem_end 범위 내의 커널 가상 주소에 대한 entry 가 base_pml4에 적히게 된다. pml4_walk() 함수는 va를 인자로 받아 방금전까지 설명했던 walking을 진행한다. pml4 offset:1 PD offset:32 에 해당하는 엔트리 ‘경로’를 시작으로, 증가하는 주소에 따라 offset이 증가하면서 walking을 하게 될것이다.

이렇게 만들어진 base_pml4는 process 별 pml4를 생성하는 함수 pml4_create()에서 활용된다.

pml4_create()는 load()과정에서 또는 do_fork()루틴에서 호출된다.

호출된 이후 pml4의 시작주소는 그 프로세스의 PCB인 스레드 구조체의 멤버 pml4에 등록한다. 이 pml4는 pml4_create이 반환 된 이후 남은 과정을 통해, 유저가 사용할 부분에 해당하는 엔트리 정보들로 채워질 것이다.

그리고 프로세스 별로 pml4를 생성했으니, 특정 프로세스가 실행 될 떄는 그 프로세스의 pml4를 CR3레지스터에 load 해야 할것이다. 이것을 위한 함수가 pml4_activate()이다.

특정 프로세스가 실행되는 경우가 하나 더있다. 스케줄링에 의해 실행 프로세스가 바뀔때이다. 그래서 schedule() 함수를 살펴보면 process_activate(next) 호출을 통해 다음으로 실행될 프로세스 next가 가진 Pml4를 activate 해준다.

Updated:

Leave a comment