Krafton Jungle/5. PintOS

[PintOS 4주차] Day 3

munsik22 2025. 5. 31. 11:40

Anonymous Page #

Anonymous page(익명 페이지)는 디스크 기반이 아닌 이미지의 일종이다.

  • Anonymous mapping은 backing 파일이나 장치가 없다.
  • (File-backed 페이지와는 다르게) 이름이 있는 파일 소스를 가지고 있지 않기 때문에 "익명"이라고 부른다.
  • 실행 가능 파일에서 스택이나 힙 등에서 사용된다.
  • anon.h에 anonymous page를 나타내는 구조체가 선언되어 있고, vm.h에 선언된 struct pagestruct anon_page anon 필드가 포함되어 있다.
더보기

Page Initialization with Lazy Loading

  • Lazy loading(지연 로딩)은 필요하게 되는 시점까지 메모리의 로딩을 지연시키는 방법이다.
    • 페이지가 할당되었다는 것은 페이지 구조체가 그 페이지에 대응되는 것을 의미한다.
    • 하지만 아직 페이지에 연결된 물리 프레임은 없고, 페이지의 실제 내용은 아직 적재되지 않았다.
    • 페이지 내용은 페이지 폴트에 의해 신호를 받아, 그것이 진짜로 필요할 때에만 적재된다.
  • 3가지 페이지 타입이 존재하고, 각 페이지마다 초기화 방법이 다르다. high-level 관점에서의 초기화 과정은 다음과 같다.
    • 커널이 새로운 페이지 요청을 받으면 vm_alloc_page_with_initializer가 실행된다: Initializer는 페이지 구조체를 할당하고 페이지 타입에 따라 적절한 initializer를 설정함으로써 새로운 페이지를 초기화하고, 유저 프로그램에 제어를 돌려준다.
    • 이 시점에서 유저 프로그램이 실행되면, 프로그램이 페이지를 소유하고 있다고 생각하지만 아직 내용이 없는 페이지에 접근하려고 시도 중이기 때문에 페이지 폴트가 발생한다.
    • 폴트 처리 과정에서 uninit_initialize가 실행되고 초기에 설정했던 initializer를 호출한다: Anonymous page의 경우 anon_initializer가 호출되고, file-backed page의 경우 file_backed_initializer가 호출된다.
  • 페이지는 초기화 → [페이지 폴트 → lazy-load → swap-in → swap-out → …] → 파괴라는 생명 주기를 가질 수 있다.
    • 생명 주기의 각 전환에 있어, 페이지 타입(또는 VM_TYPE)에 따라 요구되는 절차가 다르다. 
    • 각 페이지 타입에 따른 전환 과정들을 구현해야 한다.

Lazy Loading for Executable

  • Lazy loading에서, 프로세스가 실행을 시작할 때, 바로 필요한 메모리 부분만 메인 메모리에 적재된다.
    • 모든 이진 이미지를 메모리에 한번에 적재하는 eager loading보다 오버헤드를 줄일 수 있다.
  • Lazy loading을 지원하기 위해 vm.h에서 VM_UNINIT이라는 페이지 타입이 도입되었다.
    • 모든 페이지는 처음에 VM_UNINIT 페이지로 생성된다.
    • uninit.h에서 초기화되지 않은 페이지를 위한 페이지 구조체인 struct uninit_page가 제공된다.
    • 초기화되지 않은 페이지를 생성/초기화/파괴하는 함수는 uninit.c에 있다(나중에 구현해야 함).
  • 페이지 폴트에서, 페이지 폴트 핸들러(exception.cpage_fault)는 유효한 PF인지 먼저 확인하는 vm_try_handle_fault(vm.c)에 제어권을 넘긴다.
    • valid는 무효에 접근하는 폴트를 의미한다.
    • bogus(허위) 폴트인 경우, 페이지에 내용을 적재해서 유저 프로그램에 제어권을 리턴한다.
  • Bogus 폴트에는 3가지 종류가 있다: lazy-loaded, swaped-out page, write-protected page
    • 일단 lazy-loaded page의 경우만 고려하자: lazy loading에 대한 페이지 폴트라면, 커널은 세그먼트를 지연 로딩하기 위해 vm_alloc_page_with_initializer에서 설정했던 initializers 중 하나를 호출한다.
    • process.c에서 lazy_load_segment를 구현해야 한다.

vm_alloc_page_with_initializer()를 구현해야 한다: 전달받은 vm_type에 따라 적절한 초기화 함수를 가져오고 uninit_new를 호출해야 한다.

bool vm_alloc_page_with_initializer (enum vm_type type, void *va,
        bool writable, vm_initializer *init, void *aux);
  • 주어진 type에 따라 초기화되지 않은 페이지를 만든다.
    • uninit 페이지의 swap_in 핸들러는 type에 따라 자동으로 페이지를 초기화하고, 주어진 aux를 가지고 init을 호출한다.
    • 일단 페이지 구조체를 가지게 되면, 프로세스의 SPT에 페이지를 삽입한다.
    • vm.h에서 정의된 VM_TYPE 매크로를 사용하면 편할 수 있다.

페이지 폴트 핸들러는 연쇄적인 호출을 따르고, swap_in을 호출하면 마침내 uninit_initialize에 도달한다. 이미 uninit_initialize가 구현되어 있지만, 설계에 따라 수정해야 할 수도 있다.

static bool uninit_initialize (struct page *page, void *kva);
  • 첫 번째 폴트에서 페이지를 초기화한다: 우선 vm_initializeraux를 가져오고, 함수 포인터로 대응하는 page_initializer를 호출한다.

anon.c에서 vm_anon_initanon_initializer도 필요에 따라 수정해야 할 수도 있다.

void vm_anon_init (void);
  • anonymous page subsystem을 초기화한다: Anonymous page와 관련된 어떤 것이나 설정할 수 있다.
bool anon_initializer (struct page *page,enum vm_type type, void *kva);
  • 우선 page->operations에서 anonymous page를 위한 핸들러를 설정한다.
    • 지금은 빈 구조체인 anon_page에 정보를을 업데이트해야 할 수도 있다.
    • 이 함수는 anonymous pages(VM_ANON)를 위한 초기화 함수로 사용된다.

process.c에서 load_segmentlazy_load_segment를 구현해야 한다: 실행 파일을 위한 세그먼트 로딩을 구현해야 한다. 페이지들은 지연 로딩되어야 한다. 즉, 커널이 페이지 폴트를 가로챌 때만 적재되어야 한다. load_segment의 루프인 프로그램 로더의 코어를 수정해야 할 것이다. 루프를 돌 때마다 보류 중인(uninit) 페이지 객체를 생성하기 위해 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);
  • 파일에서 읽어들일 바이트 수와 main 루프에서 0으로 채울 바이트 수를 계산한다.
    • 이후 보류 중인 객체를 만들기 위해  vm_alloc_page_with_initializer를 호출한다.
    • vm_alloc_page_with_initializer에 제공할 보조 값으로 aux 인자를 설정한다.
    • 바이너리 로딩에 필요한 정보들을 포함하는 구조체를 만들 수도 있다.
static bool lazy_load_segment (struct page *page, void *aux);
  • lazy_load_segmentload_segment에서 vm_alloc_page_with_initializer의 4번째 인자로 제공된다.
    • 이 함수는 실행 파일의 페이지의 초기화 함수로, 페이지 폴트에서 실행된다.
    • page 구조체와 aux를 인자로 받는다.
    • auxload_segment에 설정할 정보로, 이를 통해 세그먼트를 읽을 파일을 찾고 메모리에 세그먼트를 읽어들인다.

새로운 메모리 관리 시스템에 스택 할당을 맞추기 위해, process.c에서 setup_stack을 수정해야 한다: 첫번째 스택 페이지는 지연 할당될 필요는 없다. 폴트되기를 기다릴 필요 없이 적재를 할 때 커맨드 라인의 인자들로 할당하고 초기화할 수 있다. 스택을 식별하기 위한 방법을 제공해야 할 수도 있다. 페이지에 표시하기 위해 vm.h의 vm_type에 보조 마커들(VM_MARKER_0 등)을 사용할 수 있다.

최종적으로, spt_find_page를 거쳐 SPT를 참고해서 폴트된 주소에 대응하는 페이지 구조체를 해결하기 위한 vm_try_handle_fault 함수를 수정해야 한다.

모든 필수 사항들을 구현한 뒤에 fork를 제외한 프로젝트2의 모든 테스트들을 pass해야 한다.


Supplemental Page Table - Revisit

오퍼레이션의 복사와 정리를 지원하기 위해 SPT 인터페이스로 돌아가자. 동작들은 프로세스를 생성(정확히는 자식 프로세스를 만들 때)하거나 없앨 때 필요하다. 이 시점에서 다시 SPT로 돌아온 이유는 위에서 구현했던 초기화 함수 중 일부를 사용할 수도 있기 때문이다.

vm.csupplemental_page_table_copysupplemental_page_table_kill를 구현해야 한다.

bool supplemental_page_table_copy (struct supplemental_page_table *dst,
    struct supplemental_page_table *src);
  • src에서 dst로 SPT를 복제한다.
    • 자식 프로세스가 부모로부터 실행 컨텍스트를 상속받아야 할 때(fork() 등) 사용된다.
    • src의 SPT의 각 페이지에 대해 반복하고, dst의 SPT에 정확한 엔트리의 복사본을 만든다.
    • 초기화되지 않은(uninit) 페이지를 할당하고 즉시 요청(claim)해야 한다.
void supplemental_page_table_kill (struct supplemental_page_table *spt);
  • SPT가 가지고 있던 모든 리소스들을 free시킨다.
    • 프로세스가 종료될 때(process_exit()) 호출된다.
    • 페이지 엔트리들에 대해 반복하고, 테이지들의 페이지들에 대해 destroy(page)를 호출해야 한다.
    • 이 함수에서 실제 페이지 테이블(pml4)와 물리 메모리(palloc-ed memory)에 대해 걱정할 필요는 없다: SPT가 정리되고 난 뒤에 호출자가 그것들을 정리한다.

Page Cleanup

uninit.c에서 uninit_destroy를, anon.c에서 anon_destroy를 구현해야 한다: 초기화되지 않은 페이지의 destroy 동작에 대한 핸들러다. 초기화되지 않은 페이지들이 다른 페이지 객체로 변환되더라도, 프로세스가 종료될 때 uninit 페이지가 아직 있을 수 있다.

static void uninit_destroy (struct page *page);
  • page 구조체가 보유한 리소스를 free시킨다.
    • 페이지의 vm_type을 확인하고 알맞게 다뤄야 한다.
    • 현재로서는 anonymous pages만 다룰 수 있다. file-backed pages를 정리할 때 다시 수정해야 한다.
static void anon_destroy (struct page *page);
  • Anonymous page가 보유한 리소스를 free시킨다.
    • 명시적으로 page 구조체를 free할 필요는 없다: 호출자가 해야 한다.
    • 이제 프로젝트2의 모든 테스트들이 pass되어야 한다.

메모리 구조 그리기

 

[sw정글 11주차] 세상에서 가장 자세한 pintos 메모리구조 (그림으로 이해하는 OS 메모리 구조)

🤒소잃고 외양간 고치기

joong-sunny.github.io

더보기
  • 0xffffffff…ffff (최상단, 256TB)
    • 최상위 가상 주소 영역
    • 일반 사용자 프로그램이 접근할 수 없는 영역
  • user pool (0x80053e0000)
    • 유저 프로세스가 사용하는 물리 프레임을 할당받는 풀
    • 즉, 사용자용 프레임이 이곳에서 나오며, Lazy Allocation 시에도 여기에서 할당
  • kernel pool (0x8004a21000)
    • 커널이 사용하는 물리 프레임을 위한 풀
    • palloc_get_page(PAL_KERNEL) 같은 호출에서 사용됨
  • kva space (크기: 20MB)
    • 가상 주소를 통해 물리 주소(RAM)를 직접 참조할 수 있는 커널 영역
    • 페이지 테이블을 거치지 않고 커널이 빠르게 접근할 수 있음
  • KERN_BASE (0x8004000000)
    • 커널의 가상 메모리 시작 주소
    • 이 아래 주소들은 커널 코드 및 자료 구조 등이 위치
  • USER_STACK (0x47480000)
    • 사용자 스택(stack)이 위치하는 곳
    • 스택은 위에서 아래 방향(⬇) 으로 성장
  • va space (크기: 1GB)
    • 사용자 프로그램의 전체 가상 메모리 공간 (코드 + 데이터 + + 스택)
    • 여러 사용자 프로세스가 존재하더라도 각각 독립적인 1GB 공간을 가짐
    • MMU와 페이지 테이블 덕분에 격리 가능
  • stack, heap, bss, data, code
    • stack: 함수 호출 시 지역 변수 저장 (↓ 방향 성장)
    • heap: 동적 메모리 (malloc) 등 (↑ 방향 성장) 핀토스에서 user heap은 구현되지 않음
    • bss: 초기화되지 않은 전역 변수
    • data: 초기화된 전역 변수
    • code: 실행 코드 영역 (.text)
    • ELF 실행 파일 포맷을 따름

🤝 코어타임

  • 프레임 → user pool vs 테이블 → kernel pool
    • Frame table(kernel pool)에는 Frame(user pool)로의 포인터가 엔트리로 존재함
  • uninit page
    • page는 있지만 frame은 없음
    • user stack에 저장되었다가, 페이지 폴트 발생하면 anon이나 file로 바뀌어서 .bss/.data나 .code로 이동함
    • user 가상 메모리 및 PT 존재, present bit = 0

'Krafton Jungle > 5. PintOS' 카테고리의 다른 글

[PintOS 4주차] Day 6  (0) 2025.06.03
[PintOS 4주차] Day 4-5  (0) 2025.06.02
[PintOS 4주차] Day 2  (4) 2025.05.30
[PintOS 4주차] Day 0-1  (0) 2025.05.28
[PintOS 3주차] Day 5-6  (0) 2025.05.26