pintOS › pintOS Lazy Loading 정리(Week11_day12)
오늘은 이어서 lazy loading을 정리한다.
구현
setup_stack
stack의 페이지를 생성하는 함수로, 스택은 아래로 성장하기 때문에, 스택의 시작점에서, 페이지만큼 내려서 페이지를 생성한다.
이 때는 프로세스가 실행 될 때 command line arguments를 스택에 저장하기 위해 바로 접근하기 때문에 별도로 lazy loading하지 않고 바로 claim 한다.
스택에 할당 받는 페이지는 VM_MARKER_0을 이용해서 스택페이지임을 표시하면, 나중에 이 페이지가 스택의 페이지 임을 구별할 수 있다.
이제 rsp 값을 USER_STACK으로 바꿔주면, 이 위치부터 스택에 인자를 push 하게 된다.
여기서 쓰레드 구조체를 살펴보면, stack_bottom을 관리하고 있음을 알 수 있는데, 이는 현재 스택의 가장 아래 값을 저장하는 멤버로, 확장시 값이 감소할 수는 있지만, 스택의 사용에 따라 줄어들지는 않는다(추후 현재 스택 포인터 값을 저장할 멤버를 따로 추가 할 것이다).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static bool
setup_stack(struct intr_frame *if_)
{
bool success = false;
void *stack_bottom = (void *)(((uint8_t *)USER_STACK) - PGSIZE);
/* TODO: Map the stack on stack_bottom and claim the page immediately.
* TODO: If success, set the rsp accordingly.
* TODO: You should mark the page is stack. */
/* TODO: Your code goes here */
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;
}
vm_try_handle_fault
이제 spt_find_page를 통해 faulted address에 해당하는 페이지 구조체를 해결할 수 있도록 handle 함수를 만들어 준다.
할당된 물리 프레임이 없어서 발생하는 fault 일 경우에는 not_present 값으로 true를 전달 받고, spt에서 해당 주소에 해당 페이지를 확인해서 존재 할 시 물리 프레임 할당 을 요청하는 do_claim_page를 호출한다.
물리프레임과 페이지가 존재한지만, write protected 라 발생한 fault일 경우 cow를 위해 hanlder로 넘겨준다.
페이지가 존재하지 않는, stack_growth가 필요한 경우는 1MB이내에서 확장을 시켜준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 = spt_find_page(&thread_current()->spt, addr);
if (addr == NULL || is_kernel_vaddr(addr))
return false;
if (!not_present && write)
return vm_handle_wp(page);
if (!page)
{
void *stack_pointer = user ? f->rsp : thread_current()->stack_pointer;
if (stack_pointer - 8 <= addr && addr >= USER_STACK - MAX_STACK_POINT && addr <= USER_STACK)
{
vm_stack_growth(thread_current()->stack_bottom - PGSIZE);
return true;
}
return false;
}
return vm_do_claim_page(page);
}
supplemental_page_table_copy
fork 를 통해 자식 프로세스를 생성할 때 부모 프로세스의 spt를 복사하기위해 사용되는 함수로, cow 구현을 념두에 두고 구현해보겠다.
파일의 타입은 크게 세가지로 나뉜다.
UNINIT
아직 초기화 되지 않은 페이지를 복사할 때는 추후에 필요할 때 접근하여 lazy loading을 할 수 있도록 필요한 초기화 함수와 정보를 설정하는 과정이다.
이 때 aux 즉 파일의 정보를 담고있는 구조체를 복사하여 사용하고 있는데 이는 자식이 부모보다 사용하여 초기화를 진행할 경우 aux구조체를 참조하고 free하여 버려 부모가 쓰레기 값에 접근하는 경우가 생길 수 있기 때문에 새로 복사하여 전달하는 것이다.
또한 파일 핸들러를 file_reopen 하여 전달하고 있는데 이는 동일한 파일을 참조하되 독립된 파일 핸들을 사용하게 하기 위함이다.
그렇지 않으면 파일 오프셋을 서로 덮어쓰거나, race condition이 발생할 수 있기 때문에 이를 방지하기 위함이다.VM_FILE
해당 타입의 메모리 페이지는 원본 파일로 부터 데이터를 읽어 주로 읽기전용으로 사용한다.
예를 들어 동일 프로그램을 실행할 때 부모와 자식 프로세스가 코드섹션(실행파일의 읽기 전용 부분)을 공유할 수 있으며, 이때 물리 프레임을 공유해도 문제가 발생하지 않는다.
이런 상황에서 물리 메모리를 두 개 별도로 사용하는 것은 비효율 적이며 부모와 자식은 동일 프레임을 공유하게 된다.VM_ANON
여기를 가장 주의 해야 하는데, anonymous page의 경우 보통 힙, 스택 등의 동적으로 할당 된 영역으로 프로그램 실행 중 수정될 수 있기 때문에 동일 물리 메모리를 공유할 수 없다(자식이 바꿨는데 부모것도 바뀔 수 있음).
그렇기 때문에 vm_copy_claim_page 라는 함수를 새로 만들어서 부모와 독립적인 프레임 구조체를 생성하여 파일의 정보를 넣어주고, 부모와 같은 kva를 설정해주고 일단 같이 사용하며, accessible이라는 멤버를 페이지 구조체에 추가하여 부모의 writable 값을 저장해 놓고, 자식의 spt에는 해당 페이지를 write protected로 만들어준다. 추후 자식이 쓰기 작업을 수행하려하면 page fault가 발생할 것이고 별도의 handler를 통해서 새로운 물리 프레임을 복제하며 accesible 값을 참조하여 writable 값을 설정해 주게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
bool supplemental_page_table_copy(struct supplemental_page_table *dst UNUSED, struct supplemental_page_table *src UNUSED)
{
struct hash_iterator iter;
struct page *dst_page;
struct aux *aux;
hash_first(&iter, &src->hash_table);
while (hash_next(&iter))
{
struct page *src_page = hash_entry(hash_cur(&iter), struct page, hash_elem);
enum vm_type type = src_page->operations->type;
void *upage = src_page->va;
bool writable = src_page->writable;
struct uninit_page *uninit = &src_page->uninit;
switch (type)
{
case VM_UNINIT: // src 타입이 initialize 되지 않았을 경우
aux = uninit->aux;
struct lazy_load_arg *lazy_load_arg = malloc(sizeof(struct lazy_load_arg));
if (lazy_load_arg == NULL)
{
// malloc fail - kernel pool all used
}
memcpy(lazy_load_arg, (struct lazy_load_arg *)aux, sizeof(struct lazy_load_arg));
lazy_load_arg->file = file_reopen(((struct lazy_load_arg *)aux)->file); // get new struct file (calloc)
vm_alloc_page_with_initializer(uninit->type, src_page->va, src_page->writable, uninit->init, lazy_load_arg);
break;
case VM_FILE: // src 타입이 FILE인 경우
if (!vm_alloc_page_with_initializer(type, upage, writable, NULL, &src_page->file))
goto err;
dst_page = spt_find_page(dst, upage); // 대응하는 물리 메모리 데이터 복제
if (!file_backed_initializer(dst_page, type, NULL))
goto err;
dst_page->frame = src_page->frame;
if (!pml4_set_page(thread_current()->pml4, dst_page->va, src_page->frame->kva, src_page->writable))
goto err;
break;
case VM_ANON: // src 타입이 anon인 경우
if (!vm_alloc_page(type, upage, writable)) // UNINIT 페이지 생성 및 초기화
goto err;
if (!vm_copy_claim_page(dst, upage, src_page->frame->kva, writable)) // 물리 메모리와 매핑하고 initialize
goto err;
break;
default:
goto err;
}
}
return true;
err:
return false;
}
supplemental_page_table_kill
프로세스가 종료될 때, 생성될 때 process_cleanup에서 호출되는 함수로, 페이지 항목들을 순회하면서 테이블 내의 페이지의 타입에 맞는 destroy 함수를 호출한다.
hash_clear가 아닌 hash_destroy를 사용하면, hash가 사용하던 hash->bucket 도 반환되기 때문에 생성 시 process_cleanup이 실행 될 때는 table을 남겨두고 hash_clear가 호출 될 수 있도록 한다.
1
2
3
4
5
6
void supplemental_page_table_kill(struct supplemental_page_table *spt UNUSED)
{
/* TODO: Destroy all the supplemental_page_table hold by thread and
* TODO: writeback all the modified contents to the storage. */
hash_clear(&spt->hash_table, hash_destructor);
}
1
2
3
4
5
void hash_destructor(struct hash_elem *e, void *aux) {
const struct page *p = hash_entry(e, struct page, hash_elem);
destroy(p);
free(p);
}