Vitrual Memory의 3번째 과제는 이전까지 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 */
/* TODO: Your code goes here */
/* spt_find_page로 spt에서 페이지 찾아옴 */
if(is_kernel_vaddr(addr))
return false;
/* ----------------------------------- project3-3_Stack Growth ----------------------------------- */
// 스택의 증가로 page fault를 해결할 수 있는지 확인
/* */
// if (f->rsp - 8 <= addr && addr <= USER_STACK && USER_STACK - 0x100000 <= addr) { // 0x100000 = 1MB(스택 사이즈 제한)
// 왜 -8? : so it may cause a page fault 8 bytes below the stack pointer..
if (USER_STACK - 0x100000 <= thread_current()->rsp_stack - 8 && thread_current()->rsp_stack - 8 <= addr && addr <= thread_current()->stack_bottom) { // 0x100000 = 1MB(스택 사이즈 제한)
// 스택 증가 함수 호출
// 주소를 현재 스택의 마지막 주소에서 새롭게 할당받을 크기인 PGSIZE로 넘겨줌
vm_stack_growth(thread_current()->stack_bottom - PGSIZE);
return true;
}
.
.
static void
vm_stack_growth (void *addr UNUSED) {
ASSERT(!is_kernel_vaddr(addr))
addr = pg_round_down(addr);
if (vm_alloc_page_with_initializer(VM_STACK, addr, true, NULL, NULL)) {
vm_claim_page(addr);
thread_current()->stack_bottom -= PGSIZE;
}
}
내가 생각했을때, 이번 과제에서 관건은 vm_try_handle_fault 에서 stack_growth를 호출해줘야 하는 fault인지를 판단하는데 있는 듯 하다.
이걸 어떻게 판별하냐고? 일단 이 그림을 보자
부족하지만 이게 무슨 그림이냐면... 대충 stack_growth가 호출되는 범위를 나타낸 그림이다.
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 */
// VM_MARKER_0 은 스택을 의미
if (vm_alloc_page_with_initializer(VM_STACK, stack_bottom, true, NULL, NULL))
{
// va에 페이지를 할당하고, 해당 페이지에 프레임 할당하고 mmu 설정
if (vm_claim_page(stack_bottom))
{
// rsp 설정
if_->rsp = USER_STACK;
/* 궁금: USER_STACK은 유저 스택의 시작점인데, 인터럽트 프레임의 rsp는 유저 스택의 끝을 가리켜야 하는거 아님??
그리고 지금 코드면, stack_bottom은 계속 밑으로 내려가는게 아니라, 그냥 유저 스택의 시작점에서 한단계 밑의 페이지자나..
ㄴ 이게 맞다. 왜? setup_stack은 맨 처음에 프로세스 만들때 load에서 딱 한번만 호출되기 때문에, 한 페이지 크기만큼만 밑으로 내려가는게 맞음
ㄴ 마찬가지로 rsp도 USER_STACK을 가리켜서 initial rsp를 USER_STACK과 동일하게 두는게 맞음
*/
/* ----------------------------------- project3-2_Stack Growth ----------------------------------- */
// 스택의 끝부분 저장
thread_current()->stack_bottom = stack_bottom;
/* ----------------------------------- project3-2_Stack Growth ----------------------------------- */
// success를 true로 값 변경
success = true;
}
}
return success;
}
그래서 이 범위가 맞는지 확인하고, 스택의 크기를 늘려주기만 하면 된다.
USER_STACK - 1MB <= rsp-8 <= addr <= stack_bottom
문제는 위의 if문에서 rsp를 활용하자고 하니, gitbook의 다음 문장이 마음에 걸린다
If you depend on page faults to detect invalid memory access, you will need to handle another case, where a page fault occurs in the kernel. Since the processor only saves the stack pointer when an exception causes a switch from user to kernel mode, reading rsp out of the struct intr_frame passed to page_fault() would yield an undefined value, not the user stack pointer.
이 문장을 나는 이렇게 해석했다.
"exception이 발생할시, 인터럽트 프레임의 rsp값이 유저 스택을 가리키다가, 커널 스택을 가리키게 바뀜"(틀린 해석이다, 밑에서 설명함)
그래서, exception을 통해 호출된 vmtry_handle_fault의 인자로 들어온 if 안의 rsp는 이미 커널 스택을 가리키고 있을 가능성이 있다. 이렇게 되면 stack_growth를 호출해줘야 하는지에 대한 올바른 범위 판단이 안된다.
그래서 쓰레드 구조체 안에 if_의 rsp가 유저 스택에서 커널 스택을 가리키게 변화하기 전에 미리 유저스택에서의 rsp값을 저장하는 rsp_stack을 만들어 줬다.
struct thread {
.
.
#ifdef VM
/* Table for whole virtual memory owned by thread. -> vm_entry set역할을 하는 supplemental page table */
struct supplemental_page_table spt;
void* stack_bottom;
void* rsp_stack; // exception(page fault), syscall 호출시 유저 -> 커널로 이관되는데, 이때 intr_frame의 rsp가 커널 스택을 가리키게 되니까,
// 유저 스택의 값을 저장하고 있기 위해서 해당 값을 둠
#endif
.
.
}
그리고 얘의 값을 if_의 rsp가 바뀌기 전인syscall_handler랑 exception.c에서 업데이트를 해주었다.
void syscall_handler (struct intr_frame *f UNUSED) {
thread_current()->rsp_stack = f->rsp;
.
.
}
static void
page_fault (struct intr_frame *f) {
.
.
#ifdef VM
/* For project 3 and later. */
// 유저이면, rsp값을 thread구조체의 rsp_stack에 저장해야함(인터럽트 프레임의 rsp값을 커널 스택을 가리키는 값으로 바뀔테니까)
if (user) {
thread_current()->rsp_stack = f->rsp;
}
if (vm_try_handle_fault (f, fault_addr, user, write, not_present)) {
return;
}
#endif
.
.
}
요러면 if_의 rsp값이 커널 스택을 가리키게 바뀌기 전에, 유저 스택의 값을 저장할 수 있겠지?? 라고 생각을 했는데, 뭔가 이상하다.
일단 정확히 어느 순간에 rsp가 유저스택->커널 스택 으로 넘어가는지 모르겠다.
실제로 해당 if문 안에 rsp_stack을 넣건, f->rsp를 넣건 통과하는 테스트의 개수도 똑같다. 뭔가 쓸데없는 일을 하고 있다는생각이 들었다.
if (f->rsp - 8 <= addr && addr <= USER_STACK && USER_STACK - 0x100000 <= addr)
ㄴ 이렇게 해도 아무 차이가 없다는 말...
그리고, 상식적으로 생각을 해보면, 이미 exception이 불리고 난 이후면 rsp가 커널 스택으로 넘어간 이후일텐데, exception 안에서 이미 값이 바뀐 이후에 rsp_stack의 값을 저장하는게 별 의미도 없는 듯하다.
그래서 조교님께 "어디서 rsp값이 유저스택 -> 커널 스택으로 바뀌나요??" 하고 다소 뻔뻔한 질문을 했다.
이에 대한 조교님의 답변은 다음과 같다
그러니까,
이고, 따라서 "intr_frame의 rsp값이 유저 스택을 가리키다가 커널 스택을 가리킨다" 는 나의 잘못된 뇌피셜이었다.
thread 구조체의 rsp_stack에 값을 백업하는 메커니즘과, 백업시기는 옳았지만 나의 이해가 잘못된 부분이 있었다.
우선, exception.c에서 rsp_stack에다가 값을 백업하는 부분의 경우, 이미 exception이 일어난 직후에 intr_frame의 rsp에 유저의 %rsp가 저장돼있기 때문에, 저 부분에서 값을 저장한다고 해서 추후에 intr_frame의 rsp 값이 커널 레벨로 바뀔것을 대비하는 효과를 가질 일은 없다.
static void
page_fault (struct intr_frame *f) {
.
.
#ifdef VM
/* For project 3 and later. */
// 유저이면, rsp값을 thread구조체의 rsp_stack에 저장해야함(인터럽트 프레임의 rsp값을 커널 스택을 가리키는 값으로 바뀔테니까)
// ㄴ 위는 잘못된 주석
if (user) {
thread_current()->rsp_stack = f->rsp;
}
읽어와서 저장하는 부분은 맞다만, 나의 이해가 잘못된 것이다.
또한, syscall.c의 syscall_handler()에서 rsp_stack에 intr_frame의 값을 백업하는 부분의 경우, syscall을 위해 커널 모드로 넘어갔다고 해서, intr_frame의 rsp가 백업되지는 않는다.
따라서, 해당부분에서 미리 유저레벨에 해당하는 rsp를 쓰레드 구조체의 rsp_stack에 저장해두어야 추후 sysall에서 exception을 처리하다가 fault가 났을 경우 올바른 유저 스택 값을 얻어올 수 있다.
만약 여기서 저장하지 않는다면, syscall 처리 중에 호출된page_fault에서 해당 부분
if (user) {
thread_current()->rsp_stack = f->rsp;
}
에서 if(user)의 조건문을 충족하지 못하므로, 유저레벨의 rsp값을 제대로 백업할 수 없으니, vm_try_handle_fault에서 rsp_stack을 가지고 올바른 stack_growth인지를 판별할 수 없게 된다.