크래프톤 정글

Pintos Project3 - Page Fault Handler (Page Fault 전반적인 과정 이해하기)

Jerry_K 2024. 12. 2. 10:19

🖥️ Pintos Page Fault Handler 

이 글에서는 Pintos에서의 인터럽트 초기화, 핸들러 등록, 스텁 함수 정의,

그리고 페이지 폴트 처리 과정을 하나씩 살펴본다.

Page Fault가 발생되는 과정들을 Pintos 코드로 세부적으로 파고 들고 이해 해본다.


✨ Interrupt system 초기화 

int main(void) {
    . . .
    /* 인터럽트 핸들러를 초기화합니다. */
    intr_init();
    . . .
    exception_init();
    . . .
    
}
  • intr_init() 함수 실행 

 

/* Initializes the interrupt system. */
void intr_init(void) {
    int i;

    /* Initialize interrupt controller. */
    pic_init();

    /* Initialize IDT. */
    for (i = 0; i < INTR_CNT; i++) {
        make_intr_gate(&idt[i], intr_stubs[i], 0);
        intr_names[i] = "unknown";
    }

#ifdef USERPROG
    /* Load TSS. */
    ltr(SEL_TSS);
#endif

    /* Load IDT register. */
    lidt(&idt_desc);

	. . .
    
  }
  • make_intr_gate( ) 함수로 IDT 초기화 
    • IDT (Interrupt Descriptor Table)
    • 각 인터럽트 벡터에 대응하는 핸들러 함수 (Stub 함수) 주소 설정
    • 각 IDT 엔트리에 intr_stubs에서 생성된 스텁 함수의 주소를 등록
    • 시스템이 기본적으로 작동 할 수 있도록 기본 설정
      • IDT 엔트리 구체적 설정은 register_handler() 에서 
  • lidt(&idt_desc) 함수로 IDT 레지스터에 등록
    • lidt (Load Interrupt Descriptor Table)

 


✨ Interrupt Handler 등록

int main(void) {
    . . .
    /* 인터럽트 핸들러를 초기화합니다. */
    intr_init();
    . . .
    exception_init();
    . . .
    
}
  • exception_init() 함수 실행

 

void exception_init(void) {
    . . .
    
    /* Most exceptions can be handled with interrupts turned on.
       We need to disable interrupts for page faults because the
       fault address is stored in CR2 and needs to be preserved. */
       
    intr_register_int(14, 0, INTR_OFF, page_fault, "#PF Page-Fault Exception");
}
  • intr_register_int(14, 0, INTR_OFF, page_fault, "#PF Page-Fault Exception") 함수 실행
    • 오류 번호 14
    • page_fault( ) 함수 매개변수로 전달 

 

void intr_register_int(uint8_t vec_no, int dpl, enum intr_level level, intr_handler_func *handler, const char *name) {
    ASSERT(vec_no < 0x20 || vec_no > 0x2f);
    register_handler(vec_no, dpl, level, handler, name);
}

 

  • 0x20 ≤ vec_no ≤ 0x2F는 하드웨어 인터럽트 범위
  • 아래 범위에 해당하는 vec_no 같은 경우 ASSERT 발생
  • register_handler ( ) 함수 실핼
    • page_fault( ) 함수 매개변수로 전달 

 

 

static void register_handler(uint8_t vec_no, int dpl, enum intr_level level, intr_handler_func *handler, const char *name) {
    ASSERT(intr_handlers[vec_no] == NULL);
    if (level == INTR_ON) {
        make_trap_gate(&idt[vec_no], intr_stubs[vec_no], dpl);
    } else {
        make_intr_gate(&idt[vec_no], intr_stubs[vec_no], dpl);
    }
    intr_handlers[vec_no] = handler;
    intr_names[vec_no] = name;
}

 

  • register_hadler로 특정 인터럽트 구체적으로 설정
  • 런타임에 동적으로 호출 
    • intr_init() 함수는 정적으로 IDT 초기화
  •  새로운 장치나 기능 활성화될 때, 해당 장치나 기능에 맞는 핸들러 설정 가능 
  • make_trap_gate 같은 경우 syscall 처리하는 엔트리에 사용
    • 주로 디버그 목적의 소프트웨어 예외 처리
  • make_intr_gate 같은 경우 하드웨어 인터럽트 (timer,keyboard) 처리
  • 각 인터럽트 벡터에 대응하는 핸들러 함수 (Stub 함수) 주소 설정

자, 이제 exception_init()와 intr_init()를 완료하면 IDT(Interrupt Descriptor Table)의 설정이 완료되고

페이지 폴트(Page fault)를 처리할 준비가 된 상태이다 !! 

이번에 Page Fault에서 핵심인 STUB 함수에 대해서 알아보자.

 

✨ STUB 함수 정의 

#define STUB(NUMBER, TYPE)                      \
.section .text;                                  \
.globl intr##NUMBER##_stub;                     \
.func intr##NUMBER##_stub;			\
intr##NUMBER##_stub:                            \
	TYPE;                                   \
	push $0x##NUMBER;                       /* 인터럽트 번호 푸시 */ \
	jmp intr_entry;                         /* 공통 핸들러로 점프 */ \
.endfunc; \
.section .data; \
.quad intr##NUMBER##_stub;

STUB(14, REAL)  /* 예: 페이지 폴트 (#PF) 처리용 스텁 */
  • intr-stubs.S(어셈블리 코드 파일)내부에서 작성 
  • STUB 함수는 C 핸들러와 CPU 상태 연결을 위한 함수 (중간 다리)
  • 해당 파일에서 STUB 함수 정의 

 

STUB(00, zero) STUB(01, zero) STUB(02, zero) STUB(03, zero)
STUB(04, zero) STUB(05, zero) STUB(06, zero) STUB(07, zero)
STUB(08, REAL) STUB(09, zero) STUB(0a, REAL) STUB(0b, REAL)
...
STUB(fe, zero) STUB(ff, zero)
  • 모든 인터럽트 벡터(0부터 255번까지)**에 대해 스텁 함수를 정의
    • zero: 0 오류 코드를 스택에 푸시
    • REAL: CPU가 푸시한 오류 코드를 사용.

 

(참고) 인터럽트 벡터의 범위

  • 0 ~ 31번 (0x00 ~ 0x1F): CPU 예약 예외(Exception).
    • 페이지 폴트(#PF), 디버그 예외, 분기 오류 등.
  • 32 ~ 47번 (0x20 ~ 0x2F): 하드웨어 인터럽트(IRQ).
    • 타이머, 키보드, 디스크 컨트롤러 등.
  • 48번 이상 (0x30 ~ 0xFF): 소프트웨어 인터럽트.
    • 시스템 호출, 사용자 정의 이벤트 등.

 

이렇게 하면 Stub 함수가 만들어진다. 

아까 위의 과정을 통해서 IDT에 해당 Stub 함수들의 주소가 매핑된다.

 

 

그럼 이제 본격적으로 Page Fault의 모든 과정들을 살펴보자 !

 

✨ Page Fault 과정

  1. mmu가 페이지 테이블에서 VA -> PA를 매핑 찾지 못함
  2. CPU는 페이지 폴트 예외 발생 시키고, OS의 페이지 폴트 핸들러 호출
  3. OS는 fault 가 발생한 VA 확인 
  4. VA가 적절한 주소인지 확인 (적절한 주소 아니면 Segmentation Fault)
  5. VA가 속한 페이지의 디스크 오프셋 계산
  6. 필요한 데이터를 디스크에서 읽어 물리 메모리로 로드
  7. 페이지 테이블을 업데이트하여 새로 매핑

 

" CPU는 페이지 폴트 예외 발생 시키고, OS의 페이지 폴트 핸들러 호출 "  

이 과정에 대해 구체적으로 살펴 볼 것이다. 

 

1. 잘못된 메모리에 접근 시 CPU는 IDT 14번 엔트리를 참조

  • CPU는 IDT에서 벡터 번호 14에 해당하는 엔트리를 참조

 

2. 해당 STUB(14, REAL) 함수 주소로 JUMP 

.section .text
.globl intr14_stub
.func intr14_stub
intr14_stub:
    /* REAL 타입: CPU가 이미 오류 코드를 스택에 푸시함 */
    push $0x14             /* 벡터 번호를 스택에 푸시 */
    jmp intr_entry         /* 공통 핸들러로 점프 */
.endfunc

.section .data
.quad intr14_stub          /* intr14_stub의 주소를 데이터 섹션에 저장 */
  • 특정 인터럽트 번호(NUMBER)와 타입(TYPE)에 따라 스텁 함수와 관련 데이터 섹션을 정의
  • jmp intr_entry로 공통 핸들러 intr_entry로 점프 
  • .quad intr14_stub (스텁 함수 주소를 intr_stubs 배열에 추가)

 

3.  공용 핸들러 intr_entry로 JUMP

.section .text
.func intr_entry
intr_entry:
	/* Save caller's registers. */
	subq $16,%rsp
	movw %ds,8(%rsp)
	
    . . .
    
	call intr_handler
	movq 0(%rsp), %r15
	movq 8(%rsp), %r14
    
	. . .
    
	movw (%rsp), %es
	addq $32, %rsp
	iretq
.endfunc
  • 레지스터 및 CPU 상태 저장
  • intr_handler 호출 준비
  • iretq는 인터럽트 반환 명령어로 인터럽트 발생 이전의 코드로 복귀

 

4. 레지스터 상태 저장 및 intr_handler 호출

/* Interrupt handlers. */
void intr_handler(struct intr_frame *frame) {
    bool external;
    intr_handler_func *handler;

    . . .
    
    /* Invoke the interrupt's handler. */
    handler = intr_handlers[frame->vec_no];
    if (handler != NULL)
        handler(frame);
    else if (frame->vec_no == 0x27 || frame->vec_no == 0x2f) {
        /* 핸들러는 없지만 이 인터럽트는 하드웨어 결함이나 하드웨어 경쟁 조건으로 인해
           허위로 트리거될 수 있습니다. 무시하세요. */
    } else {
        /* 핸들러도 없고 가짜도 아닙니다. 예상치 못한 호출 인터럽트 핸들러. */
        intr_dump_frame(frame);
        PANIC("Unexpected interrupt");
    }

    . . .
}
  • C로 작성된 intr_hanlder 호출
  • intr_frame에서 예외 번호 vec_no 확인 
  • intr_handler[14] 호출

 

5. Page Fault 실행 준비

handler = intr_handlers[frame->vec_no];
if (handler != NULL)
    handler(frame);
  • 이 부분에서 Page Fault 함수 실행
    • 매개 변수로 frame 전달 

 

6. 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. */

    
    fault_addr = (void *)rcr2();
    not_present = (f->error_code & PF_P) == 0;
    write = (f->error_code & PF_W) != 0;
    user = (f->error_code & PF_U) != 0;

    if (vm_try_handle_fault(f, fault_addr, user, write, not_present))
        return;

    /* Count page faults. */
    page_fault_cnt++;

    exit(-1);
    
    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 함수 실행

 

7. vm_try_handle_fault 함수 실행

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){
			return false;
		}
		return vm_do_claim_page (page);
	}
	return false ;	
}
  • spt_find_page / vm_do_claim_page (정의 한 함수)를 바탕으로 메모리 load 준비 

위의 포스팅을 쭉 정리하자면, 먼저 Page Fault가 발생하려면 IDT가 준비 되어있어야 한다.

IDT에는 STUB 함수 주소가 매핑되어있고, STUB 함수는 인터럽트로부터 C Handler를 연결시켜준다.

그렇게 Page Fault 함수가 실행되는 것이다.