-
[PintOS] WIL (3) Lazy load segment / 지연 로딩 간단한 수준으로만SW 정글/운영체제 2024. 10. 14. 23:56
지연로딩은 왜 하는 걸까요
사용자가 필요할 때만 물리 메모리를 할당해서 사용하기 위함입니다.
지연 로딩을 구현하려면, 페이지 할당 과정에서 해당 페이지에 대응하는 (uninit 타입의) page 구조체만 생성하고,
물리 프레임은 할당하지 않습니다. 이렇게 생성된 페이지는 초기에 아무런 내용도 담고 있지 않습니다.
실제 내용을 담을 물리 메모리 프레임은 페이지 폴트가 발생했을 때에만 연결됩니다.
페이지 폴트는 시스템이 해당 페이지의 실제 데이터가 필요하다는 "시그널"을 받는 순간 발생합니다.
이는 사용자가 해당 메모리 주소에 접근했다는 것을 의미하며, 필요한 데이터를 메모리로 로드하도록 구현되어야 합니다.
페이지 폴트가 처리된 후에는, 페이지의 타입을 uninit에서 실제 데이터 타입(예: anon 또는 file_backed)으로 변경합니다.
즉, uninit 타입의 페이지를 가리키는 가상 주소(va)에 접근하면, 필연적으로 페이지 폴트가 발생하고 해당 페이지에 실제 물리 메모리 프레임이 연결되는 것입니다.
vm_alloc_page_with_initializer 함수에서 이러한 지연 로딩 로직을 다룹니다
vm.c
bool vm_alloc_page_with_initializer(enum vm_type type, void *upage, bool writable, vm_initializer *init, void *aux) { ASSERT(VM_TYPE(type) != VM_UNINIT) struct supplemental_page_table *spt = &thread_current()->spt; struct page* page = spt_find_page(spt, upage); if (page == NULL) { page = malloc(sizeof(struct page)); // 1) (uninit 타입의) page 구조체 만들기 if (page == NULL) goto err; bool (*initializer)(struct page *, enum vm_type, void *); switch (VM_TYPE(type)){ case VM_ANON: initializer = anon_initializer; // 5) 시그널에 따른 내용 로드 break; case VM_FILE: initializer = file_backed_initializer; // 5) 시그널에 따른 내용 로드 break; default: goto err; } uninit_new(page, upage, init, type, aux, initializer); // 4) uninit 타입 변경 page->is_writable = writable; return spt_insert_page(spt, page); // 6) 페이지 삽입 } err: return false; }
process.c
lazy_load_segment
static bool lazy_load_segment(struct page *page, void *aux) { /* TODO: 파일로부터 세그먼트를 로드하는 작업을 수행해야 합니다. */ /* TODO: 이 함수는 주소 VA에 대한 첫 페이지 폴트가 발생할 때 호출됩니다. */ /* TODO: 이 함수를 호출할 때 VA는 사용 가능한 상태입니다. */ struct load_info *info = (struct load_info *)aux; // 페이지를 할당하려고 시도하고, 실패하면 false를 반환하는 코드는 주석 처리되었습니다. // if(vm_claim_page(page->va)) // return false; uint8_t *kpage = page->frame->kva; // 할당된 커널 페이지가 없다면 false를 반환하는 코드는 주석 처리되었습니다. // if (kpage == NULL) // return false; // 파일 포인터를 오프셋 위치로 이동합니다. // printf("now ofs %d\n", info->ofs); file_seek(info->file, info->ofs); // 파일에서 페이지 읽기 바이트만큼 데이터를 읽어서 커널 페이지에 저장합니다. // 읽은 바이트 수가 요청한 바이트 수와 일치하지 않을 경우, 할당된 페이지를 해제하고 실패로 처리합니다. if (info->file != NULL && file_read(info->file, kpage, info->page_read_bytes) != (int)info->page_read_bytes){ palloc_free_page(kpage); // 페이지를 메모리 맵에서 제거하고 페이지 구조체를 해제하는 코드는 주석 처리되었습니다. // pml4_clear_page(&thread_current()->pml4, page->va); // free(page); return false; } // 페이지의 나머지 부분을 0으로 초기화합니다. memset(kpage + info->page_read_bytes, 0, info->page_zero_bytes); return true; }
1) (uninit 타입의)page 구조체 만들기
>> page = malloc(sizeof(struct page));
커널이 새로운 페이지 요청을 받으면 vm_alloc_page_with_initializer 함수가 호출되고
여기서 uninit 타입의 페이지 구조체를 생성합니다.
이 구조체에 페이지 유형에 맞는 적절한 초기화 함수들을 담아만 둔다.
이렇게 초기화되지 않은 상태의 페이지의 타입은 VM과 uninitialized의 약자를 합친 VM_UNINIT입니다.2) 물리 프레임 미할당: 실제 물리 메모리 프레임은 vm_alloc_page_with_initializer 함수 내에서 할당되지 않습니다. 이는 initializer 함수(여기서는 lazy_load_segment)가 설정되어 있으나, 이 함수 내에서 직접적으로 메모리를 할당하지 않고,
페이지 폴트 시 초기화가 처리됩니다.
3) page fault 시 내용 로드: initializer = lazy_load_segment;
이 부분에서 페이지 폴트가 발생했을 때 lazy_load_segment 함수를 통해 실제 페이지 내용을 로드하도록 설정합니다
4) uninit 타입 변경
@vm.c 의 vm_alloc_page_with_initializer 함수에서
>> uninit_new(page, upage, init, type, aux, initializer);
uninit 타입에서 다른 타입으로의 변경은 이 함수 호출로 초기화 설정을 마칩니다.
페이지 폴트 발생 시, 실제 타입으로의 전환(예: anon 또는 file_backed)이 이루어집니다
5) 시그널에 따른 내용 로드
initializer 설정은 페이지 폴트 시그널을 받고, 해당 시그널에 따라 지연 로딩이 발생하도록 합니다.
bool (*initializer)(struct page *, enum vm_type, void *);위의 라인에서 initializer라는 포인터 함수를 선언합니다. 이 함수는 struct page*, enum vm_type, void*를 매개변수로 받는 함수를 가리킬 수 있습니다.
switch (VM_TYPE(type)){case VM_ANON:initializer = anon_initializer;break;case VM_FILE:initializer = file_backed_initializer;break;default:goto err;}위의 switch 문에서는 페이지의 유형(type)에 따라 해당하는 초기화 함수를 initializer 포인터에 할당합니다.
VM_ANON 유형의 경우 anon_initializer 함수를, VM_FILE 유형의 경우 lazy_load_segment 함수를 할당합니다.
6) 페이지 삽입
>> return spt_insert_page(spt, page);
페이지 테이블에 새 페이지를 삽입하면, 이 페이지에 접근 시 페이지 폴트가 필연적으로 발생하여 실제 물리 메모리 프레임이 연결됩니다.
uninit_new()
void uninit_new (struct page *page, void *va, vm_initializer *init,enum vm_type type, void *aux,bool (*initializer)(struct page *, enum vm_type, void *kva));uninit_new 함수에서는 va라는 매개변수를 사용하여 언급하고 있는데 일반적으로 사용자 공간의 가상 주소를 의미할 수 있지만, 커널 주소 공간 내에서도 동작할 수 있는 기능을 제공합니다.
중요한 점은 uninit_new가 실제로 페이지를 가상 주소에 매핑하는 것이 아니라, 페이지를 초기화하고 나중에 사용될 수 있도록 준비하는 역할을 한다는 것입니다.
페이지의 실제 메모리 매핑은 initializer 함수에 의해 처리되며**, 이 함수는 커널 가상 주소(kva)를 사용하여 페이지를 물리 메모리와 연결합니다.
매개변수
더보기- struct page *page: 페이지 메타데이터를 관리하는 구조체.
- void *va: 페이지가 매핑될 가상 주소.
- vm_initializer *init: 페이지 초기화 함수.
- enum vm_type type: 페이지 유형.
- void *aux: 초기화 함수에 전달될 추가 데이터.
- bool (*initializer)(struct page *, enum vm_type, void *kva): 실제 페이지를 커널 가상 주소에 매핑하는 초기화 함수.
**
struct uninit_page { /* Initiate the contets of the page */ vm_initializer *init; enum vm_type type; void *aux; /* Initiate the struct page and maps the pa to the va */ bool (*page_initializer) (struct page *, enum vm_type, void *kva); };
전체 구조를 그려서 즉시로딩과 지연로딩 방식을 비교하면 다음과 같다.
1) 즉시로딩
2) 지연로딩
그림 설명⬇️ (같은 말 반복 주의)
더보기lazy_loading은 최상단의 세 단계까지는 즉시 로딩방식과 동일하나,
load_segment에서 파일을 읽어올 때 필요한 정보들을 aux구조체에 담아
vm_alloc_page()에 전달하면서 시작합니다.
여기서 uninit 타입의 페이지 구조체를 생성하는데요, 여기서 vm_alloc_page_with_initializer()를 더 들어가 보면
uninit_new()와 spt_insert_page()가 있습니다.
uninit_new 함수에서는 실제로 페이지를 가상 주소에 매핑하는 것이 아니라, 페이지를 초기화하고 나중에 사용될 수 있도록 준비하는 역할을 합니다. 페이지의 실제 메모리 매핑은 initializer 함수에 의해 처리되며,
이 함수는 커널 가상 주소(kva)를 사용하여 페이지를 물리 메모리와 연결합니다.
이후 initializer 설정은 페이지 폴트 시그널을 받고, 해당 시그널에 따라 지연 로딩이 발생하도록 합니다.
vm_try_handle_fault에서는 bogus를 판별하여 찐 page_fault인지 확인합니다.
spt = 보조테이블에서 페이지를 찾고, do_claim_page로 페이지를 할당합니다.
do_claim_page 함수를 한 번 더 타고 들어가,
vm_get_frame과 pml4_set_page()로 물리적 프레임을 할당받고 PML4에 페이지를 설정합니다.
이후!
디스크에 있는 페이지를 물리 메모리에 load하는 “swap_in”을 사용하게 되는데 현재 uninit_initialize와 동일하게 취급되고 있습니다.
최종적으로 lazy_load_segment() 함수가 호출되면서 끝납니다.
**
vm_get_frame() : 물리 메모리를 할당 받음
pml4_set_page() : pml4 테이블에 유저가상주소와 커널의 가상주소를 맵핑
👩🌾aux ?
이 aux 인자는 어떻게 함수가 실행되지 않은 상태에서도 필요한 인자를 전달할 수 있는지에서의 중요한 역할을 한다.
특히, load_segment() 함수에서는 필요한 파일 정보들을 aux 구조체에 담아 vm_alloc_page() 함수로 전달하고,
이후 lazy_load_segment()가 이 정보를 활용하여 파일을 실제 메모리로 로드한다.
aux의 라이프사이클 관리에 대한 이미지를 찾아보았다.
동적 할당된 aux의 메모리를 언제 해제할지 결정하는 것은 메모리 관리의 복잡성과 직결되는데,
만약 aux가 너무 일찍 해제되면, munmap()에서 필요한 정보를 찾을 수 없게 되어 메모리 매핑 해제 시 디스크 파일에 변경사항을 반영하지 못할 수도 있다.
따라서, 가장 안전한 방법이 Deep copy를 사용하는 것이다.
이 방법을 통해 malloc을 사용하여 원본과 동일한 값을 가진 새로운 객체를 생성함으로써 메모리 해제 타이밍으로 인한 문제를 방지할 수 있다.
반면에, Shallow copy는 값의 주소만을 복사하여 비동기 작업에서 잘못된 값 참조 문제가 발생할 수 있다고 한다.
reference
https://velog.io/@jisu1288/Pintos-week11.-Swap-InOut-Mmap
https://velog.io/@hyeonjin/lazy-loading
'SW 정글 > 운영체제' 카테고리의 다른 글
[Sorting] 56. Merge Intervals (0) 2024.10.24 [PintOS] WIL (4) 난 이해를 한 걸까..? (0) 2024.10.22 [PintOS] WIL (2) Argument_Passing / System_Call (수정전) (0) 2024.10.08 [PintOS] WIL (1) alarm-clock 위주 (추후 보완 예정) (2) 2024.10.01