lazy-loading?
- eager loading : 프로그램 실행 시 필요한 모든 파일 내용을 미리 메모리에 로드 -> 메모리 낭비 발생 가능
- lazy loading : 파일의 내용을 실제로 필요할 때(페이지 폴트가 발생할 때)만 메모리에 로드
3가지 타입의 페이지
uninit_page
(초기화되지 않은 페이지)anon_page
: 파일과 매핑되지 않은 페이지, 커널로부터 할당된 메모리 페이지file_page
: 파일과 매핑된 페이지페이지를 하나 생성하고, vm_type에 맞게 anonymous or file-backed type에 맞는 초기화 함수를 가져온다.
그리고 uninit
타입의 페이지로 초기화하는데 인자로 타입에 맞는 initializer도 같이 전달해준다.
이후 uninit 타입으로 초기화된 페이지를 spt에 추가한다.
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);
//페이지 타입이 UNINIT인지 확인 -> VM_UNINIT이 아닐 때
struct supplemental_page_table *spt = &thread_current ()->spt;
/* Check wheter the upage is already occupied or not. */
/* spt 테이블에서 upage(va)와 매핑되는 페이지가 없다면 -> 새로운 페이지를 생성 */
if (spt_find_page (spt, upage) == NULL) {
/* TODO: Create the page, fetch the initialier according to the VM type,
* TODO: and then create "uninit" page struct by calling uninit_new. You
* TODO: should modify the field after calling the uninit_new. */
/*페이지 타입에 따라서 적절한 페이지 초기화 함수를 가져와서 새로운 struct page를 생성해야 한다.*/
struct page *page = (struct page *)malloc(sizeof(struct page));
bool (*page_initializer) (struct page *, enum vm_type, void *);
switch (VM_TYPE(type)){
case VM_ANON:
page_initializer = anon_initializer;
break;
case VM_FILE:
page_initializer = file_backed_initializer;
break;
default:
free(page);
break;
}
uninit_new(page, upage, init, type, aux, page_initializer);
page->writable = writable; // 초기화 이후 별도로 writable 속성 설정
/* TODO: Insert the page into the spt. */
/* spt에 새로 생성한 page를 넣는다. */
// return spt_insert_page(spt, page);
if (!spt_insert_page(spt, page)){
return false;
}
return true;
}
err:
return false;
}
uninit_new
: 매개변수로 받은 page 구조체를 uninit type으로 만들어서 초기화해준다.
va, 초기화함수, type, aux를 설정하여 페이지를 준비해둔다.
이렇게 준비된 uninit 페이지가 나중에 page fault가 발생할 때 해당 초기화 함수에 의해 실제로 초기화되게 된다.
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 *)) {
ASSERT (page != NULL);
*page = (struct page) {
.operations = &uninit_ops,
.va = va,
.frame = NULL, /* no frame for now */
.uninit = (struct uninit_page) {
.init = init,
.type = type,
.aux = aux,
.page_initializer = initializer,
}
};
}
process_exec ()
=> load ()
=> load_segment ()
파일로부터 데이터를 읽어와서 메모리에 로드하는 함수로 프로세스가 실행될 때 현재 쓰레드로 로드하는 load() 함수에서 호출된다.
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); //읽을 바이트와 0으로 채울 바이트의 합이 페이지 크기의 배수인지 확인
ASSERT(pg_ofs(upage) == 0); //upage가 페이자 정렬되어 있는지 확인
ASSERT(ofs % PGSIZE == 0); //파일 오프셋이 페이지 크기의 배수인지 확인
//read_bytes, zero_bytes가 모두 0이 될 때까지 loop 실행
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;
/*lazy_load_segment에 정보를 전달하기 위한 aux 설정*/
struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)malloc(sizeof(struct lazy_load_arg));
lazy_load_arg->file = file;
lazy_load_arg->read_bytes = page_read_bytes; //읽어야 하는 바이트 수
lazy_load_arg->zero_bytes = page_zero_bytes; //read_bytes만큼 읽고 나서 0으로 채워야 하는 바이트 수
lazy_load_arg->ofs = ofs; //offset - 페이지에서 읽기 시작할 위치
/*vm_alloc_page_with_initializer
- true : 페이지 할당, 초기화 성공
- false => 페이지 할당, 초기화 실패
aux 인자에 lazy_load_arg를 전달*/
if (!vm_alloc_page_with_initializer(VM_ANON, upage,
writable, lazy_load_segment, lazy_load_arg))
return false;
/* Advance. */
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
upage += PGSIZE; //다음 페이지의 시작 주소로 업데이트
ofs += page_read_bytes; //다음 읽기 위치 update
}
return true;
}
lazy_load_arg
: lazy_load_segment
함수에 필요한 정보를 담는 구조체 - file, offset, read_bytes, zero_bytes를 포함한다.
uninit 상태의 page가 생성됨
-> page fault가 처음 발생하면lazy_load_segment
가 실행되고 이 때lazy_load_arg
구조체가 인자로 전달된다.
bool
lazy_load_segment(struct page *page, void *aux)
{
/* TODO: Load the segment from the file. 파일로부터 세그먼트를 메모리에 로드해야 함*/
/* TODO: This called when the first page fault occurs on address VA.
특정 주소 VA 접근할 때 청므으로 페이지 폴트가 발생되면 실행된다.*/
/* TODO: VA is available when calling this function.
함수가 호출될 떄 가상 주소 VA가 사용 가능하다. - VA는 이미 정의되어 있음. 함수 내에서 사용 가능하다.*/
struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)aux;
struct file *file = lazy_load_arg->file;
off_t offset = lazy_load_arg->ofs;
size_t page_read_bytes = lazy_load_arg->read_bytes;
size_t page_zero_bytes = lazy_load_arg->zero_bytes;
//size_t page_zero_bytes = PGSIZE - page_read_bytes;
//파일의 오프셋 설정 - 파일의 현재 위치 변경
file_seek(file, offset);
//파일에서 데이터를 읽어 page frame에 로드
if (file_read(file, page->frame->kva, page_read_bytes) != (int)page_read_bytes) {
//읽기에 실패한 경우 할당된 페이지 해제
palloc_free_page(page->frame->kva);
return false;
}
//페이지의 kva 주소에서 read_bytes만큼 떨어진 위치부터 나머지 바이트(zero_bytes)만큼을 0으로 채운다.
memset(page->frame->kva + page_read_bytes, 0, page_zero_bytes);
return true;
}
특정 주소에 대한 첫 번째 페이지 폴트가 발생할 때 파일에서 세그먼트를 메모리에 로드하는 함수
page fault가 처음 발생했을 때 호출되어
-> 파일의 내용을 페이지로 로딩한다.
page
: 페이지 폴트가 발생한 페이지aux
: lazy_load_arg
구조체 - 파일 로딩에 필요한 정보들이 담겨 있음파일의 현재 위치를 오프셋으로 설정하고 파일에서 데이터를 읽어서 페이지 프레임에 로드한다.
페이지의 kva 주소에서 read_bytes만큼 떨어진 위치부터 남은 zero_bytes를 0으로 채운다.
vm_do_claim_page
에서 페이지 폴트가 발생했을 때 물리 페이지 프레임을 할당하고, 해당 프레임의 kva와 페이지의 va 를 매핑해준다.
그러니 lazy_load_segment
에서는 file_read
를 사용하여 파일 내용을 로딩해주는 작업만 수행해주면 된다.
file_seek
: 열린 파일의 위치를 이동하는 함수.
: 파일의 현재 위치 변경 -> 파일 내에서 읽기 또는 쓰기를 시작할 위치 설정
file_read
: 파일에서 데이터를 읽어서 지정된 메모리 위치, 버퍼에 복사하는 역할
파일에서 데이터를 읽어서 지정된 메모리 위치, 버퍼에 복사하는 역할
⇒ 파일에서 page_read_bytes만큼의 데이터를 읽어와서 페이지 프레임의 커널 가상 주소 → kva (그냥 물리 주소라고 생각하자)에 저장.
첫 번째 스택은 lazy하게 할당하지 않는다.
→ 첫 번째 스택 페이지는 프로그램 시작 시점에서 바로 접근이 필요함
→ 첫 번째 스택 페이지를 lazy하게 할당하면 프로그램이 시작될 때마다 스택 접근시 page fault
가 발생할 수 있다. ⇒ 프로그램의 초기화 속도 저하 + 불필요한 페이지 폴트 유발
static bool
setup_stack(struct intr_frame *if_)
{
bool success = false;
/* stack 아래로 성장 -> USER_STACK(스택의 시작점)에서 PGSIZE만큼 내린 지점에서 페이지를 생성함 */
void *stack_bottom = (void *)(((uint8_t *)USER_STACK) - PGSIZE);
/*
첫 번째 스택 페이지는 lazy loading할 필요가 없음
=> page fault가 발생할 때까지 기다릴 필요가 없이 바로 물리 프레임 할당
- stack_bottom에 스택을 매핑하고 해당 페이지를 즉시 할당
- 성공했다면 rsp(스택 포인터)를 적절한 위치로 설정
- 해당 페이지가 stack임을 표시하라 - VM_MARKER_0
*/
/*
vm_alloc_page(type, upage, writable)함수는 vm_alloc_page_with_initializer에서
1, 2, 3번째 인자만 받는 함수 4, 5번째 함수는 NULL로 받는다.
*/
if (vm_alloc_page(VM_ANON | VM_MARKER_0, stack_bottom, 1)) {
success= vm_claim_page(stack_bottom); //물리 페이지 프레임 할당
if (success) {
if_->rsp = USER_STACK;
thread_current()->stack_bottom = stack_bottom;
}
}
return success;
}
스택은 아래로 성장하기 때문에 USER_STACK - PGSIZE인 stack_bottom에서 첫 페이지를 생성한다.
첫 스택 페이지는 command line arguments를 스택에 추가하기 때문에 lazy-load가 필요 없다.
vm_alloc_page
를 활용해서 페이지를 할당받고, vm_claim_page
를 통해 할당된 페이지에 물리 프레임 매핑이 성공하면 인터럽트 프레임의 rsp(현재 스택의 최상위 주소)
를 USER_STACK
으로 설정해준다.
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;
struct page *page = NULL;
/* TODO: Validate the fault. page fault 주소에 대한 유효성 검증*/
static void *STACK_MINIMUM_ADDR = USER_STACK - (1<<20); /* 스택이 확장될 수 있는 최하단 경계 주소
1 << 20: 2의 20승 => 1MB (0x100000)
스택의 최소 주소 - USER_STACK - (1MB) => 사용자 스택의 최상위 주소 - (1MB) */
//page fault가 나는 주소 addr == NULL인 경우
if (addr == NULL)
return false;
/*접근한 가상 주소 va가 커널 주소인 경우 - return false
사용자 프로그램이 커널 주소 공간에 접근하는 것을 막음
사용자 요청에 의한 fault 또한 처리 불가 - return false */
if (is_kernel_vaddr(addr))
return false;
//not_present true => page fault가 발생한 경우
//접근한 페이지의 물리적 페이지가 존재하지 않는 경우
if (not_present){
...
page = spt_find_page(spt, addr); //spt에서 addr와 일치하는 page가 있는지 찾기
//page를 찾지 못하는 경우
if (page == NULL){
return false;
}
//쓰기 요청인데 페이지가 쓰기 불가능한 경우
if (write && !page->writable) {
return false;
}
//page를 claim하지 못한 경우 - addr와 kva 물리 메모리 프레임을 매핑하지 못한 경우
if (!vm_do_claim_page (page)) {
return false;
}
//모든 조건을 만족하면 return true
return true;
}
return false;
}
1.
not_present
가 `true => 접근한 메모리 주소에 물리 페이지 프레임이 존재하지 않을 때
: spt에서 addr와 일치하는 페이지가 있는지 찾고 있다면vm_do_claim_page(page)
를 통해 addr와 kva 물리 프레임을 매핑한다.
2. 읽기 전용 페이지에 쓰기 요청을 했을 때 ( write가 true인데, page는 writable하지 않은 경우)
모든 테스트 케이스가 exit(-1)이 되는 상황이었다.
알고보니 page_fault
함수에 문제가 있었다.
static void
page_fault(struct intr_frame *f)
{
bool not_present; /* True: not-present page, false: writing r/o page. */
bool write; /* True: access was write, false: access was read. */
bool user; /* True: access by user, false: access by kernel. */
void *fault_addr; /* Fault address. */
/* Obtain faulting address, the virtual address that was
accessed to cause the fault. It may point to code or to
data. It is not necessarily the address of the instruction
that caused the fault (that's f->rip). */
fault_addr = (void *)rcr2();
/* Turn interrupts back on (they were only off so that we could
be assured of reading CR2 before it changed). */
intr_enable();
/* Determine cause. */
not_present = (f->error_code & PF_P) == 0;
write = (f->error_code & PF_W) != 0;
user = (f->error_code & PF_U) != 0;
/* NOTE: [2.4] 페이지 폴트 발생 시 exit(-1) 호출 */
// exit(-1);
#ifdef VM
/* For project 3 and later. */
if (vm_try_handle_fault(f, fault_addr, user, write, not_present))
return;
#endif
/* Count page faults. */
page_fault_cnt++;
exit(-1);
/* If the fault is true fault, show info and exit. */
printf("Page fault at %p: %s error %s page in %s context.\n",
fault_addr,
not_present ? "not present" : "rights violation",
write ? "writing" : "reading",
user ? "user" : "kernel");
kill(f);
}
→ vm_try_handle_fault 호출 전에 무조건 exit(-1)이 호출되어서 프로그램이 그냥 종료된다.
→ vm_try_handle_fault 함수는 절대 호출되지 않으며 VM의 페이지 폴트 처리 기능이 아예 처리가 되지 않는다.
#ifdef VM
if (vm_try_handle_fault(f, fault_addr, user, write, not_present))
return;
#endif
exit(-1);
vm_try_handle_fault
가 호출되어서 페이지 폴트를 성공적으로 처리하면 exit(-1)이 호출되지 않고 함수가 정상적으로 종료됨
but page fault를 성공적으로 처리하지 못할 경우 exit(-1)이 호출되어서 프로그램이 종료됨 → 페이지 폴트 처리 기능이 정상적으로 작동하게 된다.
주의하자!