Anonymous Page #
Anonymous page(익명 페이지)는 디스크 기반이 아닌 이미지의 일종이다.
- Anonymous mapping은 backing 파일이나 장치가 없다.
- (File-backed 페이지와는 다르게) 이름이 있는 파일 소스를 가지고 있지 않기 때문에 "익명"이라고 부른다.
- 실행 가능 파일에서 스택이나 힙 등에서 사용된다.
anon.h에 anonymous page를 나타내는 구조체가 선언되어 있고,vm.h에 선언된struct page에struct 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.c의page_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를 구현해야 한다.
- 일단 lazy-loaded page의 경우만 고려하자: lazy loading에 대한 페이지 폴트라면, 커널은 세그먼트를 지연 로딩하기 위해
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_initializer와aux를 가져오고, 함수 포인터로 대응하는 page_initializer를 호출한다.
anon.c에서 vm_anon_init과 anon_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_segment와 lazy_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_segment는load_segment에서vm_alloc_page_with_initializer의 4번째 인자로 제공된다.- 이 함수는 실행 파일의 페이지의 초기화 함수로, 페이지 폴트에서 실행된다.
- page 구조체와
aux를 인자로 받는다. aux는load_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.c의 supplemental_page_table_copy와 supplemental_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 |