PintOS의 가상 메모리 시스템에서 중요한 기능 중 하나는 Stack Growth, 즉 스택 확장입니다.
이 글에서는 페이지 폴트가 발생했을 때, 해당 주소가 스택 확장 대상이면 어떻게 메모리를 할당하는지,
그리고 그 과정에서 호출되는 함수들의 역할을 Control Flow와 함께 상세히 정리해 보겠습니다.
Stack Growth란?
프로그램 실행 중 지역 변수 선언, 함수 호출 등으로 스택 영역이 확장될 수 있습니다.
PintOS에서는 스택이 커질 때마다 새로 페이지를 할당해줘야 하며,
이를 페이지 폴트(Page Fault)를 이용해 지연 할당 방식(lazy allocation)으로 처리합니다.
전체 흐름(Control Flow)
[유저 프로그램에서 스택 영역에 접근 (ex: PUSH)]
↓
[page_fault() 호출]
↓
[vm_try_handle_fault() → SPT에 addr가 없음]
↓
[스택 확장 조건 검사: rsp - STACK_GROW_LIMIT 이하인지?]
↓
[조건 만족 → vm_stack_growth(addr) 호출]
↓
[vm_alloc_page(VM_ANON, addr, true)]
↓
[SPT에 anon page 등록 (초기화 함수: anon_initializer)]
↓
[vm_claim_page(addr)]
↓
[물리 프레임 할당 및 유저 주소 공간에 매핑]
↓
[프로세스 정상 실행 재개]
1. vm_try_handle_fault() – 페이지 폴트를 처리하고, 필요한 경우 스택 확장 수행하기
/* Return true on success */
/* bogus 폴트인지? 스택확장 폴트인지?
* SPT 뒤져서 존재하면 bogus 폴트!!
* addr이 유저 스택 시작 주소 + 1MB를 넘지 않으면 스택확장 폴트
* 찐폴트면 false 리턴
* 아니면 vm_do_claim_page 호출
* 스택확장 폴트에서 valid를 확인하려면 유저 스택 시작 주소 + 1MB를 넘는지 확인
* addr = thread 내의 user_rsp
* addr은 user_rsp보다 크면 안됨
* stack_growth 호출해야함 */
bool vm_try_handle_fault(struct intr_frame *f UNUSED, void *addr UNUSED,
bool user UNUSED, bool write UNUSED, bool not_present UNUSED)
{
// 현재 쓰레드의 SPT를 가져옴
struct supplemental_page_table *spt UNUSED = &thread_current()->spt;
// Fault가 발생한 주소를 페이지 단위로 정렬(페이지 시작 주소로 내림)
addr = pg_round_down(addr);
// 유저 스택의 rsp 가져오기
uintptr_t rsp = thread_current()->user_rsp;
// spt page 찾기
struct page *page = spt_find_page(spt, addr);
// 스택 확장 판단 조건
if (page == NULL // 1. SPT에 존재 X
&& (uintptr_t)addr >= rsp - STACK_GROW_RANGE // 2. 접근 주소가 현재 유저 스택보다 아래
&& addr < USER_STACK // 3. 접근 주소가 유저 영역(USER_STACK) 안쪽이고
&& addr >= USER_STACK - (1 << 20)) // 4. 접근 주소가 USER_STACK 기준으로 1MB 이상 확장되지 않은 경우
{
vm_stack_growth(addr);
return true;
}
// 찐폴트 → 페이지가 없고 스택 확장 조건도 안됨
if (page == NULL)
return false;
// 쓰기 접근인데 페이지가 쓰기 불가능한 경우 → 보호 위반
if (write == true && !page->writable)
return false;
// Project 3 : VM extra COW
if (write == true && page->writable && page->frame != NULL)
return vm_handle_wp(page);
// swap_in 함수 존재 여부 확인
ASSERT(page->operations != NULL && page->operations->swap_in != NULL);
// 실제 물리 메모리에 페이지를 할당하고 매핑
return vm_do_claim_page(page);
}
1.1. vm_try_handle_fault() 핵심 요약
- 페이지 폴트 발생 시 호출되어, 접근 주소가 SPT에 등록되어 있는지 확인합니다.
- 등록되어 있지 않다면, 해당 주소가 스택 확장 대상인지 검사합니다.
- 조건을 만족하면 vm_stack_growth()를 호출합니다.
1.2. vm_try_handle_fault()를 이렇게 구현한 이유
- 정상적인 접근인지 확인하는 단계입니다.
- 예외적 스택 접근 외에는 허용하지 않기 위해, 조건을 세심하게 구성합니다.
- Copy-on-Write 기능이 있는 경우, 여기서 쓰기 권한에 따라 vm_handle_wp()도 호출됩니다.
2. vm_stack_growth() – 유저 스택 확장을 위한 anonymous 페이지 생성 및 등록하기
/* Growing the stack. */
static void
vm_stack_growth(void *addr UNUSED)
{
/* 스택 최하단에 익명 페이지를 추가하여 사용
* addr은 PGSIZE로 내림(정렬)하여 사용 */
vm_alloc_page(VM_ANON, addr, true); // 스택 최하단에 익명 페이지 추가
/* 실제 물리 메모리 프레임을 할당하고, 페이지 활성화(claim)함
이 작업이 있어야 페이지 폴트 해결 및 해당 주소 접근 가능
*/
vm_claim_page(addr);
}
2.1. vm_stack_growth() 핵심 요약
- 스택 확장 조건을 만족한 주소에 대해 새로운 anonymous 페이지를 할당합니다.
- vm_alloc_page()를 통해 SPT에 struct page 객체를 등록하고,
- 이후 vm_claim_page()로 물리 메모리까지 실제 할당합니다.
2.2. vm_stack_growth()를 이렇게 구현한 이유
- 유저 스택의 지연 확장 처리: 실제 접근이 있을 때만 메모리를 할당해 효율적 메모리 사용이 가능합니다.
- 직접 claim까지 연결해 줘야 프로세스가 정상적으로 실행을 재개할 수 있기 때문에, vm_claim_page()까지 호출합니다.