[Pint OS] 4-Level Paging이란? | mmu.c 분석

설현아·2025년 5월 16일

이 블로그에 4-Level Paging이 엄청 잘 정리되어있다!

서두에 말하자면, Pint OS에서 가상메모리를 어떤 방법으로 설계하였는지 이해하는 것은 매우 중요하다.
x86-64 linux 환경에서는 이 포스팅에서 다룰 4-Level Paging 기법을 사용한다.

코드를 보면 PTE에서 저장된 PA를 빈번하게 VA로 변경한다.
어렵게 어렵게 매핑한 PA인데 왜 직접 사용하지 않고 또 VA로 바꾸어 사용하는 걸까?
코드만 보면 이런 의문이 계속 든다.

하지만 가상메모리의 컨셉을 다시 떠올리자. 이에 대해 잘 이해해야 한다.

우리는 OS, 더 좁게 보면 kernel 코드를 구현하고 있다.
커널은 소프트웨어이기 때문에 가상 메모리만으로 CPU에게 일을 시킬 수 있다.
그리고 그 사이에서 MMU라는 하드웨어 장치가 물리메모리로 변환한다.
MMU는 커널이 만든 Page Table을 보고 변환한다.

따라서 우리는
1. MMU가 물리 메모리로 잘 변환할 수 있도록 Table을 잘 업데이트 해야 한다.
2. 커널 작업을 할 때는 VA로 CPU에게 일을 시켜야 한다.(page를 만든다거나, 수정한다거나, 등등의 작업이 있겠다.)

ptov, vtop의 헬퍼 함수가 잦게 쓰이니 미리 그 쓰임을 알고있는 게 좋다.

4-Level Paging

64 bit 아키텍처에서 사용하는 페이징 기법으로, x86-64 linux에서는 이 기법을 채택한다.
4-Level Paging은 이름에서 나타나듯, 네 개의 레벨을 가진 page table을 사용하며, 모든 structures는 4096B(4KB)의 사이즈와 512개의 entry로 이루어져있다.

[ 64bit 중, 48bit만 사용하면... ]

  • page size: 2^12(4096)
  • pointer size: 2^3(8)
  • Page Table size(PTE 수): 2^9(512)

따라서, 2^48만큼의 주소를 표현하기 위해서는 그만큼의 PTE가 있어야 한다. 그렇기에 PT 하나로는 안 된다.

  • 2^9 * 2^9 * 2^9 * 2^9 = 2^36
    총 4개의 Page Table과 페이지 오프셋 2^12을 곱해주면
  • 2^36 * 2^12 = 2^48

이렇게 2^48의 가상 주소를 가질 수 있게 된다.


https://wiki.osdev.org/Paging

위의 그림을 보자.
CR3 레지스터가 Page Directory의 시작 주소를 가리킨다.
Page Directory는 또다시 Page Tables의 각 시작 주소를 가리킨다.
Page Table Entry는 PA(Physical Addres)를 가진다.

이 흐름을 좀 더 자세히 알아보자.


32bit 아키텍처에서는 2^32 = 4GiB의 주소 공간을 제공하고
64bit 아키텍처에서는 2^48 = 256TiB의 주소 공간을 제공한다. (12bit는 남겨둔다.)

Paging is achieved through the use of the Memory Management Unit (MMU). On the x86, the MMU maps memory through a series of tables, two to be exact. They are the paging directory (PD), and the paging table (PT).

페이징은 MMU를 통해 진행된다. MMU는 메모리를 2개 종류의 table을 통해 매핑한다.
Paging directory(PD), Paging table(PT)이다.

이는 모두 512개의 8바이트 entry로 구성된다. 그렇게 각 테이블은 총 4KB의 크기를 가진다.

4단계의 페이지 테이블은 이름이 존재한다.
1. PML4 Page-map level 4
2. PDP Page-directory pointer
3. PD Page-directory
4. PT Page-Table

VA에서 PA로 변환하려면 우선, 위의 PML4/PDP/PD/PT/offset 등의 정보가 포함된 64bit의 Virtual Address를 파트별로 나누어 보아야 한다.


https://blog.xenoscr.net/2021/09/06/Exploring-Virtual-Memory-and-Page-Structures.html

48-63 : sign extend
39-47 : PML4 Page-Map Level 4 Offset
30-38 : PDP Page-Directory Pointer
21-29 : PDE Page-Directory Offset
12-20 : PTE Page-Table Offset
 0-11 : Physival Offset

MMU는 이 정보를 가지고 page directory → page table → physical page frame → page offset
의 순서로 변환한다.
(만약, 변환에 실패했다면 페이지 폴트를 일으킨다.)

각 단계의 구조는 아래와 같다!


PintOS 코드와 개념 연결

Pint OS 또한 X86-64 linux 환경으로 위와 같은 4-Level paging 기법을 사용하고있다.


이렇게 친절한 주석도 있다.

pte.h

Page table entry의 구조이다.
매크로로 구조의 각 요소를 정의한다.


P == PTE_P
R/W == PTE_W
U/S == PTE_U
A == PTE_A
D == PTE_D
이렇게 매핑되는 것이 보이는가?
위의 매크로는 PTE의 각 요소를 표현한다.
.
.
또한,
PML4 : PML4 Offset 시작주소를 반환한다.
PDPE : Page Directory Pointer의 시작 주소를 반환한다.
PDX : Page Directory Offset 시작주소를 반환한다.
PTX : Page Table Offset 시작주소를 반환한다.
PTE_ADDR : Physical Offset 시작주소를 반환한다.

mmu.h


하나씩 뜯어보자.
자세한 코드는 mmu.c에서 확인할 수 있다.

  1. pml4e_walk()
    최상위 PML4 테이블에서 시작해, 하위 레벨을 따라 내려가 PTE 주소를 반환하는 함수로, 가상 주소 va를 기준으로 아래 단계들을 거친다.

    1단계 : pml4e_walk()
    ① va에서 PML4 offset을 파싱한다.
    ② 해당 PML4 Table에서 offset으로 PDP Table 주소를 찾는다.

    2단계 : pdpe_walk()
    ① va에서 PDP offset을 파싱한다.
    ② 해당 PDP Table에서 offset으로 PD Table 주소를 찾는다.

    3단계 : pgdir_walk()
    ① va에서 PD offset을 파싱한다.
    ② 해당 PD Table에서 offset으로 Page Table 주소를 찾는다.
    ③ va에서 PT offset을 파싱한다.
    ④ 해당 Page Table에서 offset만큼 이동(8Bytes * offset)하여 PTE를 찾는다.

  1. pml4_create()
    새로운 pml4 table을 만든다. 이는 base_pml4라는 커널 영역에 이미 매핑되어있는 base 메모리를 복사하여, 자동으로 커널 영역에 매핑되도록 한다.(user address는 나중에 매핑)

    단, pml4 table이 만들어졌다고 해서, 그와 연결된 PDPT, PD, PT가 만들어지는 것은 아니다. 접근 혹은 매핑 설정 시점에서 필요에 따라 만들어진다.(on-demand)

    특정 가상주소 0x00400000를 매핑하려고 하면
    1. PML4 엔트리 확인 → 비어있으면 PDPT 새로 할당
    2. PDPT 엔트리 확인 → 비어있으면 PD 새로 할당
    3. PD 엔트리 확인 → 비어있으면 PT 새로 할당
    4. PT 엔트리 확인 → 거기다가 물리주소 매핑

    즉, 필요할 때 단계별로 할당하는 방식(lazy allocation)

  2. pml4_for_each()
    PML4 table의 모든 entries(0~511)를 순회한다. 인자로 받는 func를 수행하며, 함수에 따른 T/F를 반환한다.
    .
    이 함수는 내부에서 재귀적으로 하위 계층구조를 호출한다.
    pml4_for_each()pdp_for_each()pgdir_for_each()pt_for_each()
    전체 가상 주소 공간의 페이지 테이블을 다 순회하면서, 각 엔트리에 대해 특정 작업(func)을 수행하는 역할을 한다.

  3. pml4_destroy()
    PML4를 인자로 받고, 위와 같이 재귀적으로 하위 계층구조를 호출한다. 모든 PTE, PT, PDP, PDPE, PML4를 palloc_free_page(), 할당 해제한다.

  4. pml4_activate()
    CR3 레지스터는 현재 사용 중인 PML4 Table의 물리주소를 저장한다. CPU는 항상 이 CR3를 보고 페이지 테이블 탐색을 시작한다.
    .
    CR3 레지스터에 새로운 pml4 Table을 저장하게 한다.(context switching 발생)

  5. pml4_get_page()
    인자로 받은 user address(가상 주소)를 4단계의 테이블을 통해 PA로 변환하고, CPU가 읽을 수 있는 커널 가상 주소로 반환한다. ptov

  6. pml4_set_page()
    user address(가상 주소)가 물리 메모리의 페이지 프레임을 가리키게 한다.
    .
    user page를 인자로 받고 그 페이지가 속한 PTE를 알아낸 이후에 vtop로 얻은 물리 메모리 주소로 PTE 값을 설정한다.

  7. pml4_clear_page()
    PTE_P(Present bit)가 1이라면 이미 사용 중(매핑된) 페이지라는 것이다. 이 함수는 매핑을 해제해준다.

  8. pml4_is_dirty() / pml4_set_dirty()
    테이블이 수정되었는지 여부를 반환하고, 설정한다.

  9. pml4_is_accessed() / pml4_set_accessed()
    최근 접근된 적이 있는지 확인하rh, 설정한다.


커널이 Virtual Address로 물리 메모리에 접근하는 방법

글의 시작에서 간단히 언급했지만, 중요한 부분이기에 다시 짚어보겠다.

우선, 커널 또한 소프트웨어이다. 그래서 직접 메모리에 접근할 수 없다.
이는 가상 메모리의 컨셉을 이해해야만 하는데,

  • CPU가 직접 메모리에 접근한다면 유저 프로세스에서도 바로 메모리에 접근하여 수정할 수 있다. 이는 굉장히 위험하게 느껴진다!
  • 그리고 각 프로세스는 독립적인 메모리 영역을 가지고 있는데 CPU는 진짜 독립적인 메모리를 사용하고 있다고 생각해야 한다.
    .
    그래야 하나의 물리 메모리 주소는 여러 프로세스의 여러 개의 가상 메모리 주소로 매핑될 수 있다.(매핑된 가상 메모리는 커널 영역일 수도, 유저 영역일 수도 있다.)

따라서 CPU는 가상 주소로만 처리하고, 이 가상 주소를 물리 주소로 변환시키는 것은 MMU라는 하드웨어가 수행한다.
.
.

그래도 커널이 해야하는 일이 있다.
MMU가 잘 변환시킬 수 있도록 VA와 PA의 매핑 테이블(Page Table)을 관리하는 것이다.
사용자의 요청에 따라, 페이지를 생성/초기화하여 VA와 PA를 매핑하고 테이블을 업데이트한다.

그러면 과연 커널이 사용하는 메모리는 어디에 저장될까??


이 구조를 보면 쉽게 이해할 수 있다.
가상 메모리는 논리적으로 Kernel 영역과 User 영역을 나눈다.
즉, 커널이 사용하는 메모리 주소(가상 주소) 또한 가상 메모리 안에 있다는 것이다.

🙋🏻‍♀️ 그러면 user 프로그램이 kernel 영역을 침범하면 어떻게 해?
PTE에는 User mode인지 Kernel mode인지 구분하는 bit가 있다.PTE_U
이 bit를 확인하고 user mode의 프로그램은 절대 kernel 영역에 접근할 수 없게 한다.

정리하자면,
실제 물리 메모리는 유저/커널 상관 없이 사용할 수 있지만, 어떤 가상주소에 매핑할지는 페이지 테이블로 논리적으로 구분한다.

.
.

PintOS에서 'ptov' 가 중요한 이유

우리가 PintOS에서 작성하는 대부분의 코드는 OS 코드이다. (교육용 운영체제를 만드는 것이니까 당연하다.)

하지만 커널 코드라고 해서 물리 주소(Physical Address)를 직접 dereference(참조)하는 것은 불가능하다.
CPU는 메모리에 접근할 때 반드시 가상 주소(Virtual Address)를 사용하고, 이 가상 주소를 물리 주소로 변환하는 작업은 MMU가 맡는다.

문제는, 페이지 테이블(PTE)에는 물리 주소가 저장되어 있다는 점이다.
따라서 커널이 이 물리 주소를 참조해서 데이터를 읽거나 쓰기 위해서는, 해당 물리 주소에 대응하는 커널 가상 주소(Kernel Virtual Address)로 변환해야 한다.

바로 그 역할을 하는 것이 ptov() 매크로이다.
ptov()를 통해 얻은 가상 주소로 접근하면, MMU가 실제 물리 주소로 매핑해 주기 때문에 커널이 메모리 접근을 안전하게 수행할 수 있다.

profile
어서오세요! ☺️ 후회 없는 내일을 위해 오늘을 열심히 살아가는 개발자입니다.

0개의 댓글