Notice
Recent Posts
Recent Comments
«   2025/05   »
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
Tags
more
Archives
Today
Total
관리 메뉴

infra-whale 님의 블로그

PINTOS : 인터럽트 본격 탐구 본문

카테고리 없음

PINTOS : 인터럽트 본격 탐구

infra-whale 2024. 10. 22. 00:21

VM 테스트 케이스를 다 통과할 무렵, 우리 팀은 하나의 의문에 봉착했다.

 

2주동안 우리를 그토록 괴롭혔던 page fault.

그건 대체 어디에서 온 것일까?

 

솔직히 모른다고 해서 구현에 지장이 가는 것은 아니었다. 페이지 구조체는 만들었으나, 프레임이 할당되어 있지 않은 경우  page fault 가 알아서 잘 실행되어서 vm_try_handle_fault를 실행시킬 것이다. 그러면 우린 이 안에 필요로 하는 로직을 써놓기만 하면 된다. 실제로 몰라도 모든 테스트는 다 통과 했으니까 말이다.

4주간 고생한 결과물. All 141 tests passed !!!

 

하지만 프로젝트가 끝나는 마지막 날까지, page fault 더 나아가 interrupt가 어디서 오는 건지 모르고 떠나는 건 좀 아니란 생각이 들었고, 마지막 날 우린 여기서 삽질을 해보기로 하였다.

page fault 발생 시점

그럼 핀토스에서 page fault는 언제 발생하는걸까? 이를 알아보기 위해 우리가 구현한 코드를 살짝 보자. 우선 project 2에서 이미 구현되어 있던 코드다. 즉시 로딩으로 구현되어있다.

 

static bool
load_segment(struct file *file, off_t ofs, uint8_t *upage,
			 uint32_t read_bytes, uint32_t zero_bytes, bool writable)
{
	ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
	ASSERT(pg_ofs(upage) == 0);
	ASSERT(ofs % PGSIZE == 0);

	file_seek(file, ofs);
	while (read_bytes > 0 || zero_bytes > 0)
	{
		size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
		size_t page_zero_bytes = PGSIZE - page_read_bytes;

		uint8_t *kpage = palloc_get_page(PAL_USER); // 로드 시점부터 프레임을 할당받는다.
		if (kpage == NULL)
			return false;

		if (file_read(file, kpage, page_read_bytes) != (int)page_read_bytes)
		{
			palloc_free_page(kpage);
			return false;
		}
		memset(kpage + page_read_bytes, 0, page_zero_bytes);

		// 로드 시점부터 페이지 테이블에 매핑된다.
		if (!install_page(upage, kpage, writable))
		{
			palloc_free_page(kpage);
			return false;
		}

		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		upage += PGSIZE;
	}
	return true;
}

 

로딩 시점부터 프레임을 할당받고, pml4에 매핑도 되어서 page fault를 걱정할 일이 없었다. 하지만 지연 로딩을 구현하면서부터 조금 골치아파졌다.

static bool
load_segment(struct file *file, off_t ofs, uint8_t *upage,
			 uint32_t read_bytes, uint32_t zero_bytes, bool writable)
{
	ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
	ASSERT(pg_ofs(upage) == 0);
	ASSERT(ofs % PGSIZE == 0);

	file_seek (file, ofs);
	while (read_bytes > 0 || zero_bytes > 0)
	{
		size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
		size_t page_zero_bytes = PGSIZE - page_read_bytes;

		struct load_info *aux = (struct load_info  *)malloc(sizeof(struct load_info));
		if(aux == NULL)	
			return false;
		memset(aux, 0, sizeof(struct load_info));
		aux->file = file;
		aux->page_read_bytes = page_read_bytes;
		aux->page_zero_bytes = page_zero_bytes;
		aux->writable = writable;
		aux->ofs = ofs;

		if (!vm_alloc_page_with_initializer(VM_ANON, upage, writable, lazy_load_segment, aux))
			return false;

		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		upage += PGSIZE;
		ofs += page_read_bytes;
	}
	return true;
}

 

프로젝트 3에서의 구현이다. 프레임 할당도, pml4 매핑도 여기에선 수행하지 않는다. 만약 해당 페이지에 접근하게 되면 페이지 테이블은 페이지의 가상 주소를 통해 물리 주소를 반환받으려 할 것이고, 매핑이 되어있지 않으니 아무것도 반환할 수 없을 것이다.

이 때, 인터럽트가, 더 정확히 말하면 page fault가 발생하게 된다. 해당 과정을 더 자세히 알아보자.

page fault(interrupt)의 시작

page fault의 감지는 cpu에 의해 일어나게 된다. 위와 같은 상황이 발생하게 된되면 cpu는 해당 interrupt의 vec_no가 14번임을 이미 알고 있다. 그래서 intr-stubs.S에 있는 어셈블리어 코드가 실행되는 것을 시작으로 interrupt가 실행된다.

우리의 가장 큰 의문 중 하나는, cpu가 하드웨어 레벨에서 이를 어떻게 감지하냐는 것이었다. 열심히 뒤져보았으나 찾지 못했고 결국 아래와 같이 결론이 났다.

아쉽게도 코드 레벨에서는 보지 못하는듯 하다. 바로 intr-stubs.S로 넘어가자.

cpu는 해당 파일 안에, 각 vec_no에 해당하는 0부터 255의 STUB를 가지고 있다. page fault가 발생할 경우 16진법 14에 해당하는 0e가 실행될 것이다.

 

여기까지만 보면 잘 모르겠다. 그럼 STUB을 까보자.

/* This implements steps 1 and 2, described above, in the common
   case where we just push a 0 error code. */
#define zero pushq $0;

/* This implements steps 1 and 2, described above, in the case
   where the CPU already pushed an error code. */
#define REAL

.section .data
.globl intr_stubs
intr_stubs:

/* Emits a stub for interrupt vector NUMBER.
   TYPE is `zero', for the case where we push a 0 error code,
   or `REAL', if the CPU pushes an error code for us. */
#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은 각 인터럽트에 대응하는 간단한 핸들러 역할을 하는 코드로 생각하면 된다. 실제 핸들러로 이동하기 전, 기본적인 처리를 담당할 것이다.

STUB(NUMBER, TYPE)은 2가지 인자를 받는다.

 

먼저 두 번째 인자는 zero 혹은 REAL이다.

zero의 경우 에러코드가 없어 0을 수동으로 push 하는 경우를 말하는 것이고, REAL은 CPU가 이미 에러코드를 스택에 push 하여 할 것이 없는 경우를 말하는 것이다.

 

첫 번째 인자는 해당 인터럽트의 vector number가 온다. 해당 숫자를 받아서 STUB 매크로의 각 요소가 정의되고, 이를 호출하는 것으로 인터럽트가 실행된다.

예를 들어 page fault에 해당하는 14번이라면 위 코드는 아래와 같이 바뀔 것이다.

.section .text // 텍스트 영역에 코드를 저장한다.
.globl intr14_stub // 이 함수를 전역으로 선언한다.
.func intr14_stub
intr14_stub:
	REAL
	push $0x14      // 14번 벡터 번호를 push 한다.
	jmp intr_entry  // intr_entry로 점프한다.
.endfunc

.section .data // 데이터 영역에
.quad intr14_stub  // 이 스텁의 주소를 기록한다.

 

여기서 push란, 스택에 데이터를 저장함을 의미한다. 그 후, intr_entry로 점프한다 했으니 이 부분을 다시 보자.

.section .text
.func intr_entry
intr_entry:
	/* Save caller's registers. */
    
    // 현재 레지스터 상태를 스택에 저장한다.
    // 인터럽트 처리가 끝난 후 현재 상태로 돌아올 수 있게 만든다.
	subq $16,%rsp // 스택 포인터를 조정하여 16 바이트의 공간을 만든다.
	movw %ds,8(%rsp) // ds 세그먼트 레지스터를 스택에 저장한다.
	movw %es,0(%rsp) // es 세그먼트 레지스터를 스택에 저장한다.
    
    // 레지스터 값들을 모두 스택에 저장한다.
	subq $120,%rsp // 스택 포인터를 조정하여 120 바이트의 공간을 만든다.
	movq %rax,112(%rsp)
	movq %rbx,104(%rsp)
	movq %rcx,96(%rsp)
	movq %rdx,88(%rsp)
	movq %rbp,80(%rsp)
	movq %rdi,72(%rsp)
	movq %rsi,64(%rsp)
	movq %r8,56(%rsp)
	movq %r9,48(%rsp)
	movq %r10,40(%rsp)
	movq %r11,32(%rsp)
	movq %r12,24(%rsp)
	movq %r13,16(%rsp)
	movq %r14,8(%rsp)
	movq %r15,0(%rsp)
    
    // CPU 문자열을 처리할 때 앞쪽으로 처리하도록 한다.
	cld			/* String instructions go upward. */
    
    // 세그먼트 레지스터를 설정한다.
    
    //SEL_KDSEG 값을 rax 레지스터에 저장한다.
    // SEL_KDSEG : 커널 데이터 세그먼트를 나타내는 상수 값.
	movq $SEL_KDSEG, %rax
    // ax 값을 ds에 저장한다. CPU가 데이터를 읽거나 쓸때 이 세그먼트를 사용한다.
	movw %ax, %ds
	movw %ax, %es
	movw %ax, %ss
	movw %ax, %fs
	movw %ax, %gs
    // 스택 포인터 rsp 값을 rdi 레지스터에 복사한다.
	movq %rsp,%rdi
    
	call intr_handler // 인터럽트 핸들러를 호출한다.
    
    // 스택에서 레지스터 값을 복원한다.
	movq 0(%rsp), %r15
	movq 8(%rsp), %r14
	movq 16(%rsp), %r13
	movq 24(%rsp), %r12
	movq 32(%rsp), %r11
	movq 40(%rsp), %r10
	movq 48(%rsp), %r9
	movq 56(%rsp), %r8
	movq 64(%rsp), %rsi
	movq 72(%rsp), %rdi
	movq 80(%rsp), %rbp
	movq 88(%rsp), %rdx
	movq 96(%rsp), %rcx
	movq 104(%rsp), %rbx
	movq 112(%rsp), %rax
    
    // CPU 상태를 복원 한다.
	addq $120, %rsp
	movw 8(%rsp), %ds
	movw (%rsp), %es
	addq $32, %rsp
    
    // 인터럽트를 종료한다.
	iretq
.endfunc

 

요약하면 다음과 같다.

  1. 현재 레지스터 상태를 스택에 저장한다.
  2. 레지스터 값들을 스택에 모두 저장한다.
  3. 세그먼트 레지스터를 설정한다.
  4. 스택 포인터를 rdi 레지스터에 저장한다.
  5. 인터럽트 핸들러를 호출한다.
  6. 스택에서 레지스터 값들을 복원하고, 현재 레지스터 상태를 복원한다.
  7. 인터럽트를 종료한다.

중요한건 5번이다. 여기서 인터럽트 핸들러가 호출이 된다. 그럼 핀토스에서 어떤식으로 구현이 되어 있을까?

 

intr_handler

interrupt.c 파일에 들어가보면, 해당 함수를 확인할 수 있다.

 
/* Interrupt handlers. */

/* Handler for all interrupts, faults, and exceptions.  This
   function is called by the assembly language interrupt stubs in
   intr-stubs.S.  FRAME describes the interrupt and the
   interrupted thread's registers. */
void
intr_handler (struct intr_frame *frame) {
    bool external;
    intr_handler_func *handler;

    /* External interrupts are special.
       We only handle one at a time (so interrupts must be off)
       and they need to be acknowledged on the PIC (see below).
       An external interrupt handler cannot sleep. */
    external = frame->vec_no >= 0x20 && frame->vec_no < 0x30;
    if (external) {
        ASSERT (intr_get_level () == INTR_OFF);
        ASSERT (!intr_context ());

        in_external_intr = true;
        yield_on_return = false;
    }

    /* 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) {
        /* There is no handler, but this interrupt can trigger
           spuriously due to a hardware fault or hardware race
           condition.  Ignore it. */
    } else {
        /* No handler and not spurious.  Invoke the unexpected
           interrupt handler. */
        intr_dump_frame (frame);
        PANIC ("Unexpected interrupt");
    }

    /* Complete the processing of an external interrupt. */
    if (external) {
        ASSERT (intr_get_level () == INTR_OFF);
        ASSERT (intr_context ());

        in_external_intr = false;
        pic_end_of_interrupt (frame->vec_no);

        if (yield_on_return)
            thread_yield ();
    }
}
 

긴 코드 중에서 우리가 주목할 부분은 

	/* Invoke the interrupt's handler. */
	handler = intr_handlers[frame->vec_no];
	if (handler != NULL)
		handler (frame);

이 부분이다. handler는 함수 포인터고, page fault가 일어난다면, intr_handlers[] 배열의 14번째 요소를 받을 것이고 이를 호출하여 page fault가 일어날 것이다. 

 

이 intr_handlers[] 가 등록되는 과정을 보려면, exception.c와 interrupt.c를 같이 볼 필요가 있다.

먼저 exception.c 부터 시작한다.

 
void
exception_init (void) {
    /* These exceptions can be raised explicitly by a user program,
       e.g. via the INT, INT3, INTO, and BOUND instructions.  Thus,
       we set DPL==3, meaning that user programs are allowed to
       invoke them via these instructions. */
    intr_register_int (3, 3, INTR_ON, kill, "#BP Breakpoint Exception");
    intr_register_int (4, 3, INTR_ON, kill, "#OF Overflow Exception");
    intr_register_int (5, 3, INTR_ON, kill,
            "#BR BOUND Range Exceeded Exception");

    /* These exceptions have DPL==0, preventing user processes from
       invoking them via the INT instruction.  They can still be
       caused indirectly, e.g. #DE can be caused by dividing by
       0.  */
    intr_register_int (0, 0, INTR_ON, kill, "#DE Divide Error");
    intr_register_int (1, 0, INTR_ON, kill, "#DB Debug Exception");
    intr_register_int (6, 0, INTR_ON, kill, "#UD Invalid Opcode Exception");
    intr_register_int (7, 0, INTR_ON, kill,
            "#NM Device Not Available Exception");
    intr_register_int (11, 0, INTR_ON, kill, "#NP Segment Not Present");
    intr_register_int (12, 0, INTR_ON, kill, "#SS Stack Fault Exception");
    intr_register_int (13, 0, INTR_ON, kill, "#GP General Protection Exception");
    intr_register_int (16, 0, INTR_ON, kill, "#MF x87 FPU Floating-Point Error");
    intr_register_int (19, 0, INTR_ON, kill,
            "#XF SIMD Floating-Point Exception");

    /* 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를 호출하는 것을 볼 수 있다. 우선 맨 아래에 있는 page_fault 관련 코드를 기억해두자.

 

그 다음 부터는 모두 interrupt.c에서 확인하면 된다.

 
/* Registers internal interrupt VEC_NO to invoke HANDLER, which
   is named NAME for debugging purposes.  The interrupt handler
   will be invoked with interrupt status LEVEL.

   The handler will have descriptor privilege level DPL, meaning
   that it can be invoked intentionally when the processor is in
   the DPL or lower-numbered ring.  In practice, DPL==3 allows
   user mode to invoke the interrupts and DPL==0 prevents such
   invocation.  Faults and exceptions that occur in user mode
   still cause interrupts with DPL==0 to be invoked.  See
   [IA32-v3a] sections 4.5 "Privilege Levels" and 4.8.1.1
   "Accessing Nonconforming Code Segments" for further
   discussion. */
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);
}
 

 

internal inturrupt를 등록하는 함수다. page fault의 경우 vec_no 14로 등록되었고, dpl 0이므로 커널 모드에서만 일어나는게 가능할 것이다. 그리고 INTR_OFF이므로 실행되는 동안 다른 interrupt는 비활성화 될 것이다.

 

해당 함수가 호출하는 register_handler()도 살펴보자.

 
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;
}
 

드디어 찾는 부분이 나왔다.

intr_handlers[vec_no] = handler;

 

이 부분에서 해당 배열 요소에 hanler 함수 포인터에 저장된 함수가 들어간다. 앞서,

intr_register_int (14, 0, INTR_OFF, page_fault, "#PF Page-Fault Exception");

이 부분에서 실행되어야 할 함수로 page_fault를 넣어줬었다. 즉, intr_handler에서 intr_handlers[14] 가 호출되면 exception.c의 page_fault가 호출된다.

page_fault

이제 우리가 만나고 싶었던 page_fault를 볼 수 있다!!!

 
/* Page fault handler.  This is a skeleton that must be filled in
   to implement virtual memory.  Some solutions to project 2 may
   also require modifying this code.

   At entry, the address that faulted is in CR2 (Control Register
   2) and information about the fault, formatted as described in
   the PF_* macros in exception.h, is in F's error_code member.  The
   example code here shows how to parse that information.  You
   can find more information about both of these in the
   description of "Interrupt 14--Page Fault Exception (#PF)" in
   [IA32-v3a] section 5.15 "Exception and Interrupt Reference". */
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. */

    /* Obtain faulting address, the virtual address that was
       accessed to cause the fault.  It may point to code or to
       data.  It is not necessarily the address of the instruction
       that caused the fault (that's f->rip). */

    fault_addr = (void *) rcr2();

    /* Turn interrupts back on (they were only off so that we could
       be assured of reading CR2 before it changed). */
    intr_enable ();


    /* Determine cause. */
    not_present = (f->error_code & PF_P) == 0;
    write = (f->error_code & PF_W) != 0;
    user = (f->error_code & PF_U) != 0;

#ifdef VM
    /* For project 3 and later. */
    if (vm_try_handle_fault (f, fault_addr, user, write, not_present))
        return;
#endif

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

    /* If the fault is true fault, show info and exit. */
    exit(-1);
}
 

 

이후로는 우리가 구현한 vm.c 의 vm_try_handle_fault ()를 타고, 지연 로딩이 실행될 것이다.

 

다른 인터럽트들

우리가 주로 만났던 인터럽트는 project 1의 timer interrupt와 project 3의 page fault 였지만, 실제로는 더 많긴 하다. 우선 핀토스에 등록된 internal 인터럽트만 하더라도 다음과 같다.

 

void
intr_init (void) {
 
    ....

    /* Initialize intr_names. */
    intr_names[0] = "#DE Divide Error";
    intr_names[1] = "#DB Debug Exception";
    intr_names[2] = "NMI Interrupt";
    intr_names[3] = "#BP Breakpoint Exception";
    intr_names[4] = "#OF Overflow Exception";
    intr_names[5] = "#BR BOUND Range Exceeded Exception";
    intr_names[6] = "#UD Invalid Opcode Exception";
    intr_names[7] = "#NM Device Not Available Exception";
    intr_names[8] = "#DF Double Fault Exception";
    intr_names[9] = "Coprocessor Segment Overrun";
    intr_names[10] = "#TS Invalid TSS Exception";
    intr_names[11] = "#NP Segment Not Present";
    intr_names[12] = "#SS Stack Fault Exception";
    intr_names[13] = "#GP General Protection Exception";
    intr_names[14] = "#PF Page-Fault Exception";
    intr_names[16] = "#MF x87 FPU Floating-Point Error";
    intr_names[17] = "#AC Alignment Check Exception";
    intr_names[18] = "#MC Machine-Check Exception";
    intr_names[19] = "#XF SIMD Floating-Point Exception";
 
    /*  #DE Divide Error: 0으로 나누기를 시도할 때 발생합니다.
        #DB Debug Exception: 디버거가 설정한 중단점에 도달했을 때 발생합니다.
        NMI Interrupt: Non-Maskable Interrupt로, 일반적인 인터럽트를 무시할 수 없는 중요한 신호입니다.
        #BP Breakpoint Exception: 코드 실행 중 디버깅 중단점에 도달했을 때 발생합니다.
        #OF Overflow Exception: 연산 결과가 표현할 수 있는 범위를 초과했을 때 발생합니다.
        #BR BOUND Range Exceeded Exception: BOUND 명령이 설정된 범위를 초과할 때 발생합니다.
        #UD Invalid Opcode Exception: CPU가 알 수 없는 명령어를 만났을 때 발생합니다.
        #NM Device Not Available Exception: 사용할 수 없는 장치에 접근하려 할 때 발생합니다.
        #DF Double Fault Exception: 중첩된 예외가 발생했을 때 발생합니다.
        #TS Invalid TSS Exception: Task State Segment가 유효하지 않을 때 발생합니다.
        #NP Segment Not Present: 존재하지 않는 세그먼트에 접근할 때 발생합니다.
        #SS Stack Fault Exception: 스택이 부족할 때 발생합니다.
        #GP General Protection Exception: 일반적인 보호 위반이 발생했을 때 발생합니다.
        #PF Page-Fault Exception: 메모리에 접근하려 할 때 해당 페이지가 메모리에 없을 경우 발생합니다.
        #MF x87 FPU Floating-Point Error: 부동 소수점 연산에서 오류가 발생했을 때 발생합니다.
        #AC Alignment Check Exception: 데이터 정렬 문제가 발생했을 때 발생합니다.
        #MC Machine-Check Exception: 하드웨어 오류가 발생했을 때 발생합니다.
        #XF SIMD Floating-Point Exception: SIMD 부동 소수점 연산에서 오류가 발생했을 때 발생합니다.*/
}

intr_register_int  를 통하여 여러 인터럽트가 등록되는 과정도 이미 위에서 봤었다. 단지 대부분 handler를 등록해 놓지 않고 kill로 프로세스를 즉시 끝낼 뿐이다.

 

아무쪼록, 우리를 무기력하게도 하고 졸리게도 하고 미치게도 하였단 PINTOS는 이렇게 마무리되었다.

프로젝트 4를 넘기긴 했지만 시간이 허락한다면 이후에 그 부분까지도 도전해보고 싶다.