티스토리 뷰
지연 로딩은 메모리 로딩을 필요한 시점까지 미루는 설계 방식이다. 페이지 구조체는 할당되어 페이지에 해당하지만, 전용 물리 프레임은 없고 페이지의 실제 내용은 아직 로드되지 않는다. 내용은 실제로 필요한 시점에만 로드되며, 페이지 폴트에 의해 로드된다.
페이지 타입이 세 가지이므로 초기화 루틴은 각 페이지마다 다르다. 커널이 페이지 할당 요청을 받으면 vm_alloc_page_with_initializer를 호출한다. vm_alloc_page_with_initializer함수는 페이지 구조체를 할당하고 페이지 타입에 따라 적절한 초기화 함수를 설정하여 초기화한 후 supple_ment_page_table에 삽입한다. 프로세스는 실제 메모리에 로드가 되지 않은 가상주소로 접근을 한다. 이때 페이지에 내용이 없기 때문에 페이지 폴트가 발생한다. 페이지 폴트 절차에 따라 uninit_initializer가 호출되고 이전에 설정한 초기화 함수를 호출한다. 익명 페이지의 경우 anon_initializer가 호출되고, 파일 백업 페이지의 경우 file_backed_initializer가 초기화 함수로 실행이 된다.
페이지 생명주기
페이지는 page initialize -> ( lazy loding -> swap in -> swap out -> ...) -> destroy의 생명 주기를 갖는다.
실행 파일의 lazy loading
모든 바이너리 파일을 한번에 메모리에 로드하는 즉시 로딩(eager loading)은 오버헤드를 발생시킨다. 프로세스를 실행할때 필요한 메모리 부분만 주 메모리에 로드하는 지연 로딩 (lazy loading)은 이런 오버헤드를 줄일 수 있다.
모든 페이지는 처음에 VM_UNINIT페이지로 생성된다. 페이지 폴트 발생 시, page_fault 핸들러는 vm_try_handle_fault로 전달한다. vm_try_handle_fault는 먼저 유효한 페이지 폴트인지 확인한다. 핸들링할 수 있는 즉, 회복가능한 가짜 폴트이면 이를 회복한다.
가짜 폴트는 세가지 경우가 있다. 지연 로딩된 페이지, 스왑 아웃된 페이지, 쓰기 보호된 페이지(Copy-on-Write)가 있다.
우선 lazy loading page만 고려할거다. lazy loading page fault인 경우 커널은 vm_alloc_page_with_initializer에서 페이지에 설정한 초기화 함수를 호출하고 lazy_load_segment를 실행한다.
load_segment
static bool
load_segment (struct file *file, off_t ofs, uint8_t *upage,
uint32_t read_bytes, uint32_t zero_bytes, bool writable) {
ASSERT ((read_bytes + zero_bytes) % PGSIZE == 0);
ASSERT (pg_ofs (upage) == 0);
ASSERT (ofs % PGSIZE == 0);
upage = pg_round_down(upage);
while (read_bytes > 0 || zero_bytes > 0) {
/* Do calculate how to fill this page.
* We will read PAGE_READ_BYTES bytes from FILE
* and zero the final PAGE_ZERO_BYTES bytes. */
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
/* TODO: Set up aux to pass information to the lazy_load_segment. */
struct file_page *aux = malloc(sizeof(struct file_page));
if (aux == NULL)
return false;
aux->file = file;
aux->offset = ofs;
aux->read_bytes = page_read_bytes;
aux->zero_bytes = page_zero_bytes;
if (!vm_alloc_page_with_initializer (VM_ANON, upage, writable, lazy_load_segment, aux)) {
free(aux);
return false;
}
/* Advance. */
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
ofs += page_read_bytes;
upage += PGSIZE;
}
return true;
}
vm_alloc_page_with_initializer
uapge 주소를 PAGESIZE 정렬시켜 페이지를 할당해야한다. 그렇게하기위해 pg_round_down을 사용한다.
/* Create the pending page object with initializer. If you want to create a
* page, do not create it directly and make it through this function or
* `vm_alloc_page`. */
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;
/* Check wheter the upage is already occupied or not. */
if (spt_find_page(spt, upage) == NULL)
{
/* malloc으로 유저 영역에 페이지 하나 할당 */
/* malloc을 쓰는 이유?
palloc은 물리 프레임, 즉 vm_get_frame에서 페이지를 생성할 때 사용한다.
또한, 구현의 편의성을 위해 malloc을 사용한다. */
struct page *new_page = (struct page *)malloc(sizeof(struct page));
if (new_page == NULL)
goto err;
/* page round down */
upage = pg_round_down(upage);
/* type에 맞게 uninit page를 생성한다. */
switch (VM_TYPE(type))
{
case VM_ANON:
uninit_new(new_page, upage, init, type, aux, anon_initializer);
break;
case VM_FILE:
uninit_new(new_page, upage, init, type, aux, file_backed_initializer);
break;
default:
free(new_page);
goto err;
}
/* 쓰기 권한 업데이트 */
new_page->writable = writable;
/* TODO: Insert the page into the spt. */
if (!spt_insert_page(spt, new_page))
{
free(new_page);
goto err;
}
return true;
}
err:
return false;
}
vm_try_handle_fault
잘못된 영역에 접근하려고 하는 페이지가 아닌 유효한 페이지 폴트인지 확인한다.
다음과 같은 Bogus 폴트를 확인한다
- Lazy Loading된 페이지에 접근시도 -> 해당 페이지를 이제서야 로딩한다
- swap-out된 페이지에 접근 시도 -> 다시 swap-in을 한다.
- Copy on Write 정책에서 읽기 전용 페이지에 쓰려고 시도 -> 해당 페이지를 복사 후, 복사본에 쓰기 권한을 부여한다.
/* Return true on success */
bool vm_try_handle_fault(struct intr_frame *f UNUSED, void *addr UNUSED,
bool user UNUSED, bool write UNUSED, bool not_present UNUSED)
{
struct supplemental_page_table *spt UNUSED = &thread_current()->spt;
/* 1. 커널 주소에대한 접근인지 확인한다 */
if(is_kernel_vaddr(addr))
exit(-1);
/* spt에서 해당하는 page를 찾아온다. */
addr = pg_round_down(addr);
struct page *page = spt_find_page(spt, addr);
/* 해당 가상 주소에 대한 페이지가 메모리에 없는 상태*/
if (page == NULL)
exit(-1);
if(write && !page->writable)
exit(-1);
if (not_present)
{
/* 해당 가상 주소에 대한 페이지는 메모리에 존재하지만, r/o 페이지에 write 시도*/
return vm_do_claim_page(page);
}
/* 2. Bogus fault인지 확인한다. */
/* 2-1. 이미 초기화된 페이지 (즉 UNINIT이 아닌 페이지)에 PF 발생:
swap-out된 페이지에 대한 PF이다. */
if (page->operations->type != VM_UNINIT || page != NULL)
{
if (vm_do_claim_page(page))
return true;
}
return false;
}
✅ 왜 vm_claim_page를 사용하지 않고 vm_do_claim_page만 사용하는가?
우리는 page를 우선 가져와서 해당 페이지가 유효한지 검증하는 절차를 추가해야 한다. vm_claim_page는 page를 가져와서 바로 do_claim_page를 호출한다.
✅ 페이지 자체에 대한 초기화는 나중에 한다!
(page fault → swap_in → uninit_initialize 에서 실제 페이지 초기화를 한다.)
lazy_laod_segment
page 타입에 맞춰 initializer가 실행된 후 lazy_load_segment가 호출된다. aux에 미리 저장해 놓은 file_page에 대한 필드를 사용한다. 실제로 파일을 읽어 page에 매핑된 frame에 로드한다.
static bool
lazy_load_segment (struct page *page, void *aux) {
/* load_segment (from userprog.) 가져옴 */
/* aux에 저장해놓은 필드들을 이제 쓴다. */
struct file_page *fp = (struct file_page *)aux;
struct file *file = fp->file;
off_t ofs = fp->offset;
uint32_t read_bytes = fp->read_bytes;
uint32_t zero_bytes = fp->zero_bytes;
file_seek (file, ofs);
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
/* 실제로 파일을 읽어와서 페이지에 매핑된 물리 프레임에 로드한다. */
uint8_t *kva = page->frame->kva;
if (kva == NULL)
return false;
/* 읽기 실패 */
if (file_read (file, kva, page_read_bytes) != (int) page_read_bytes)
return false;
memset (kva + page_read_bytes, 0, page_zero_bytes);
/* 더이상 aux는 쓰이지 않는다. */
free(aux);
return true;
}
set_up_stack
stack_bottom에 해당하는 공간에 anon 타입 페이지를 만들어 준다. 먼저 uninit 페이지를 만들어주고 바로 vm_claim_page를 호출해 page와 frame을 매핑한다.
/* Create a PAGE of stack at the USER_STACK. Return true on success. */
static bool
setup_stack (struct intr_frame *if_) {
bool success = false;
void *stack_bottom = (void *) (((uint8_t *) USER_STACK) - PGSIZE);
/* stack_bottom에 해당하는 공간에 페이지를 할당한다.
일단은 uninit으로. */
/* round_down 안하는 이유?
stack_bottom에 해당하는 공간에 페이지를 할당해주어야 하므로
round_down하면 안된다. */
success = vm_alloc_page(VM_ANON, stack_bottom, true);
if (success) {
/* 바로 로드 요청을 함으로써 ANON 타입 page로 만들어준다. */
success = vm_claim_page(stack_bottom);
if (success)
if_->rsp = USER_STACK;
else {
struct page *page = spt_find_page(&thread_current()->spt, stack_bottom);
vm_dealloc_page(page);
}
}
return success;
}
참고
https://casys-kaist.github.io/pintos-kaist/project3/anon.html
https://fatalism-developer.tistory.com/
'운영체제' 카테고리의 다른 글
[PintOS] Project3-2 Frame Table and claim page (1) | 2024.06.09 |
---|---|
[PintOS] Project3-1 Supplement Page Table (0) | 2024.06.02 |
[PintOS] Project3-0 가상메모리는 DRAM의 추상화다 (0) | 2024.06.02 |
[PintOS] Project2-3 System calls: File System Call (1) | 2024.05.26 |
[PintOS] Project2-2 User Memory Access (0) | 2024.05.26 |