지금까지의 스택은 USER_STACK
에서 시작하는 단일 페이지였고, 프로그램의 실행은 이 크기로 제한되었다.
VM에서 동작하는 케이스가 아닌 경우, setup_stack은 최소 크기의 스택을 생성하는데 이는 0으로 초기화된 한 페이지를 user stack으로 할당해주는 방식이었다.
이번 파트에서는 필요할 때마다 1 page씩 늘어나는 스택을 만들어줄 것이다.
#ifndef VM
...
static bool
setup_stack(struct intr_frame *if_)
{
uint8_t *kpage;
bool success = false;
kpage = palloc_get_page(PAL_USER | PAL_ZERO);
if (kpage != NULL)
{
success = install_page(((uint8_t *)USER_STACK) - PGSIZE, kpage, true);
if (success)
if_->rsp = USER_STACK;
else
palloc_free_page(kpage);
}
return success;
}
왜 크기가 fix된 스택보다 growth하는 스택이 좋을까?
고정된 크기의 스택에 가상 메모리 페이지를 올릴 경우, 아직 물리 메모리와 매핑되지 않았지만 매핑된 메모리로 취급한다. 즉, 한꺼번에 물리 메모리에 파일 하나를 통째로 올리는 것이기 때문에 메모리 낭비일 수 있다.
Page_Fault가 발생했을 때, 이 Page_Fault가 stack_growth에 대한 Fault라면 스택을 키워준다.
즉, 스택이 가득찼을 때 수행하는 것인데, USER_STACK에는 함수가 호출되면 그 리턴값이 차곡차곡 쌓인다.
이는 rsp(스택 포인터)가 내려오면서 해당 스택 페이지에 내용을 작성하는데, 우리가 할당해준 영역 밑으로
rsp가 접근하게 된다면 Page Fault가 발생하게 되기 때문이다.
어디에서 Page Fault가 발생했는지 체크해야하는데, 이를 위해서는 User program의 스택 포인터(rsp)의 현재 주소값을 알아야 한다.
사용자 프로그램에서 system call 또는 Page fault가 일어났을 때에는, 해당 프로세스의 intr_frame
의 rsp 멤버값을 그대로 사용해도 된다.
왜냐하면 Page fault가 발생해서 모드 전환이 되었을 때, User program의 스택 포인터 값이 intr_frame->rsp에 저장이 되어 Page-Fault-Handler나 Syscall-handler에 인자로 전달되기 때문이다.
하지만, 커널 주소 공간에서 Page fault가 발생하는 경우는 상황이 다르다. 예를 들어 Context switching 중에 Page Fault가 발생할 수도 있기 때문이다.
하지만 프로세서는 유저에서 커널 모드로 전환되는 exception이 발생했을 때만 스택 포인터를 저장하므로, page_fault()
에서 전달된 struct intr_frame
의 rsp는, 사용자 스택 포인터가 아닌 정의되지 않은 값일 수 있다.
이런 이유로, 유저 모드에서 커널 모드로의 초기 전환에서, rsp를 thread 구조체에 저장하는 등의 다른 방법을 준비해야 한다.
이제 stack growth 기능을 구현해보자.
이를 구현하려면 먼저 vm_try_handle_fault
를 수정하여 stack growth에 대한 Page Fault인지 식별해야 한다.
이 함수는 Page fault exception을 처리하는 동안 page_fault()
에서 호출된다.
우선 Page fault가 발생한 주소의 페이지를 spt에서 찾는다.
page가 NULL인 경우, 스택이 가득 차서 할당이 불가능한 경우이다.
그렇다면 vm_try_handle_fault
의 인자로 들어온 user
인자를 통해 어디서 발생한 Page Fault인지 체크하고, USER면 인터럽트 프레임의 rsp를, kernel이면 thread의 rsp를 사용한다.
이제, Page Fault가 발생한 주소가 stack_growth에 해당하는지 조건을 체크한다.
깃북을 보면 핀토스뿐만 아니라 대부분 OS는 유저 프로세스 당 스택 크기에 상한을 두고 있다고 하는데,
UNIX는 유저에 맞게 limit이 달라지고, GNU/LINUX에서는 8MB로 제한을 두고 있으며
우리의 Pintos에서는 스택 사이즈가 반드시 1MB가 되어야 한다.
따라서 USER_STACK내에(1MB) 존재하는지 체크해주어야 하는 것이 첫번째이고,
USER_STACK>addr && addr >= USER_STACK - (1<<20)
스택 포인터가 stack 영역에 들어와있더라도 문제가 발생하는 케이스를 고려해주어야 하는데
이 문제는 깃북의 내용을 보자.
유저 프로그램은 이 프로그램이 스택 포인터 아래 영역의 스택에 write을 할 때 버그가 생긴다.
왜냐면 실제 OS는 스택에 데이터를 수정하는 시그널을 전달하기 위해 언제든지 프로세스에 인터럽트를 걸 수 있기 때문이다. 하지만, x86-64에서 PUSH 명령어는 스택 포인터 위치를 조정하기 전에 접근 권한을 체크하는데, 이는 스택 포인터 아래 8바이트 위치에서 page fault를 일으킨다.
addr>=rsp-8
해당 케이스까지 만족하면 stack growth에 대한 page_fault이므로 vm_stack_growth()
함수를 호출하여 실제 스택을 늘려준다.
/* 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;
struct page *page = NULL;
if (not_present)
{
page = spt_find_page(spt, addr);
if (page == NULL)
{
// 어디서 발생한 Page Fault니?
void* rsp = (void*)user ? f->rsp : thread_current()->rsp;
// USER_STACK 내에 존재하는지, rsp 아래에 8byte 위치에 존재하는지
if (USER_STACK>addr && addr >= USER_STACK - (1<<20) && addr>=rsp-8)
{
vm_stack_growth(pg_round_down(addr));
return true;
}
return false;
}
return vm_do_claim_page(page);
}
return false;
}
kernel에서 발생한 Page Fault이면 thread의 rsp를 사용해야 하므로
syscall_handler가 호출되는 시점의 thread의 rsp에 이를 호출한 user program의 rsp를 저장해준다.
이 정보는 인자로 들어온 intr_frame에 들어있다.
또한 이 정보를 저장할 수 있게 thread 구조체에 멤버를 추가해준다.
void syscall_handler(struct intr_frame *f UNUSED)
{
struct thread*t = thread_current();
t->rsp = f->rsp;
...
...
}
struct thread
{
...
uintptr_t rsp; // 추가
...
}
스택의 맨 밑(stack_bottom)보다 1 PAGE 아래에 페이지를 하나 만든다.
이 페이지의 타입은 ANON이어야 한다. 왜냐하면 스택은 anonymous page이기 때문이다.
static void
vm_stack_growth(void *addr UNUSED)
{
vm_alloc_page(VM_ANON | VM_MARKER_0, addr, true);
vm_claim_page(addr);
}