들어가며


이 글은 xv6-Trap 글에서 다룬 내용을 기반으로 새로운 system call을 만들어보는 과정을 담은 글입니다.

이번 글에서 만들어볼 system call은 총 두 가지입니다. 하나는 sys_kbdints()라는 keyboard interrupt의 개수를 반환해주는 함수이고, 하나는 sys_time()이라는 mtime register의 값을 반환해주는 함수입니다. sys_kbdints에 비해 sys_time이 특이한 점은 mtime 레지스터가 m-mode에서만 읽을 수 있는 레지스터라는 점입니다. 그래서 sys_time()을 구현하기 위해서는 추가적인 작업을 해주어야합니다. 이 점에 집중하여 글을 보시면 좋을 것 같습니다.


System call 구현 해보기


1. sys_kbdints 구현

xv6-Trap을 읽으셨다면 system call이 어떻게 구현되는지는 아실 것입니다.

잠시 usertrap의 함수를 다시보겠습니다. usertrap은 6번째 줄을 통해서 system call인지 device interrupt인지 파악합니다. 그리고 system call이라고 파악되면 31번째 줄에서 syscall()을 통해서 그 system call을 실행하죠.

void
usertrap(void)
{
  ...
  
  if(r_scause() == 8){
    // system call

    if(killed(p))
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sepc, scause, and sstatus,
    // so enable only now that we're done with those registers.
    intr_on();
    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    setkilled(p);
  }

  ...

  usertrapret();
}

지금 하고자하는 것은 sys_kbdints라는 새로운 함수를 추가하고자 하는 것이니 syscall()에서 제 새 함수를 부를 수 있도록 조작해야겠습니다. 그러므로 syscalls 함수 포인터 배열에 새로운 함수를 추가해주고, sys_kbdints를 정의해줍니다.

이제 사용자는 sys_kbdints를 부를 수 있게되었습니다.

uint64
sys_kbdints(void)
{
  return uartintr_cnt;
}

static uint64 (*syscalls[])(void) = {
...
[SYS_unlink]  sys_unlink,
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
[SYS_kbdints] sys_kbdints,
[SYS_time]    sys_time,
};

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7; // system call 종류
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    // Use num to lookup the system call function for num, call it,
    // and store its return value in p->trapframe->a0
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

한편 sys_kbdints의 역할은 키보드 인터럽트의 개수를 리턴해주는 것이었죠. 그렇다면 이걸 어떻게 세야할까요? 아까 usertrap()에서 if(r_scause() == 8)를 통해서 system call이냐 device interrupt냐를 따졌죠. 키보드 인터럽트는 device interrupt니까 else if((which_dev = devintr()) != 0) 부분에서 처리될 것입니다.

devintr()는 device interrupt를 처리하는 함수로 그 안에서 uartintr()라는 함수를 통해 키보드 interrupt를 처리합니다.

그리고 uartintr() 안에서는 consoleintr()라는 함수를 통해 keyboard interrupt를 처리하게 됩니다. 그러므로 consoleintr()함수 안에 uartintr_cnt++을 심어놓으면 키보드 인터럽트의 수를 그대로 셀 수 있겠죠.

void
consoleintr(int c)
{
  acquire(&cons.lock);

  uartintr_cnt++;
  switch(c){
  case C('P'):  // Print process list.
    procdump();
    break;
  case C('U'):  // Kill line.
    while(cons.e != cons.w &&
          cons.buf[(cons.e-1) % INPUT_BUF_SIZE] != '\n'){
      cons.e--;
      consputc(BACKSPACE);
    }
    break;
  case C('H'): // Backspace
  case '\x7f': // Delete key
    if(cons.e != cons.w){
      cons.e--;
      consputc(BACKSPACE);
    }
    break;
  default:
    if(c != 0 && cons.e-cons.r < INPUT_BUF_SIZE){
      c = (c == '\r') ? '\n' : c;
      // echo back to the user.
      consputc(c);

      // store for consumption by consoleread().
      cons.buf[cons.e++ % INPUT_BUF_SIZE] = c;

      if(c == '\n' || c == C('D') || cons.e-cons.r == INPUT_BUF_SIZE){
        // wake up consoleread() if a whole line (or end-of-file)
        // has arrived.
        cons.w = cons.e;
        wakeup(&cons.r);
      }
    }
    break;
  }
}

이렇게 전역 변수인 uartintr_cnt를 증가시키고, system call이 불렸을 때 그 값을 리턴해줌으로써 sys_kbdints라는 키보드 인터럽트의 개수를 리턴해주는 system call을 작성할 수 있었습니다.


2. sys_time 구현

sys_kbdints를 구현하는 것은 꽤 단순했습니다. system call이 일어나는 과정을 파악하고, 키보드 인터럽트를 처리하는 과정만 이해하면 됐으니까요. 그러면 sys_time은 어떨까요?

sys_time은 mtime 레지스터의 값을 읽어서 반환해주는 함수입니다. mtime 시스템의 절대적인 실행 시간을 측정해주는 레지스터이며 m-mode에서만 읽을 수 있습니다.

그러면 이 mtime을 읽기 위해서는 m-mode로 진입해야하는데 Trap이 발생한다하더라도 s-mode로 진입하지 m-mode로 진입하지는 않습니다.

그러면 생각해볼 수 있는 방법은 두 가지였습니다. u-mode에서 다이렉트로 m-mode로 갈 수 있는 방법이 있는지 보는 것과, s-mode로 진입한 후 m-mode로 진입할 수 있는 방법이 있는지 파악해보는 것입니다.

그런데 살펴보니 u-mode에서 다이렉트로 m-mode로 가는 것은 불가능했습니다. 그래서 s-mode에서 m-mode로 진입해야겠다고 생각을 하게되었습니다.

그리고 그 방법은 u-mode에서 s-mode로 진입할 때와 같았습니다. system call을 부를 대 u-mode에서 ecall을 통해 exception을 발생시켜 s-mode로 진입하여 커널에서 system call을 처리했습니다.

그러면 s-mode에서도 ecall을 부르면 m-mode로 가서 exception을 처리해줄 것 같지만 보통은 똑같은 s-mode에서 kernelvec이라는 함수를 통해서 처리하게 됩니다.

하지만 medeleg(Machine Trap Delegation Registers)라는 레지스터의 값을 바꿔주면 s-mode에서 특정 exception이 발생했을 때 m-mode trap handler를 실행하게 할 수 있습니다. 이 내용은 ‘The RISC-V Instruction Set Manual Volume II: Privileged Architecture Document Version 20211203’의 3.1.8파트에서 확인할 수 있습니다.

그런데 이 레지스터의 값을 수정하려보니 m-mode에서만 수정할 수 있습니다. 어떻게 해야하나 생각하다보니 컴퓨터가 시작할 때는 m-mode였습니다.

그러므로 컴퓨터가 시작될 때 실행되는 코드인 kernel/start.c에서 medeleg의 값을 수정하면 됩니다. 아래와 같이 말입니다.

void
start()
{
  ...

  // delegate all interrupts and exceptions to supervisor mode. except interrupt 9
  w_medeleg(0xfdff);
  w_mideleg(0xffff);
  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

  ...

  // switch to supervisor mode and jump to main().
  asm volatile("mret");
}

이렇게 medeleg의 값을 0xfdff로 바꿔놓으면 Environment call(ecall) from S-mode인 9번째 Exception에 대해서만 m-mode로 처리하고 나머지는 s-mode에서 처리하게 됩니다.

InterruptException codeDescription
00Instruction address misaligned
01Instruction access fault
02Illegal instruction
03Breakpoint
04Load address misaligned
05Load access fault
06Store/AMO address misaligned
07Store/AMO access fault
08Environment call from U-mode
09Environment call from S-mode
010Reserved
011Environment call from M-mode (mcause only)
012Instruction page fault
013Load page fault
014Reserved
015Store/AMO page fault
0>= 16Reserved

이렇게 설정을 하고나면 s-mode에서 ecall을 불러서 excpetion을 발생시키면 m-mode의 trap handler인 mtvec으로 가게 됩니다. 그러므로 아래와 같이 sys_time()에서 ecall을 부르면 mtvec으로 뛰게 됩니다.

uint64
sys_time(void)
{
  asm volatile("ecall");

//   unsigned int t0_value;
//   asm volatile("mv %0, t0" : "=r"(t0_value) : : "memory");
//   return t0_value;
}

그런데 mtvec 레지스터는 컴퓨터가 시작할 때의 설정으로 인해 timervec이라는 것을 가리키고 있고, 이는 바꿔서는 안됩니다. timer interrupt라는 것을 처리해야하기때문이죠.

그렇다면 timervec의 내용을 조금 수정하는 것으로 목표를 달성해야겠습니다.

이를 위해 기존의 timervec 위에 세줄을 추가합니다. mcause에 trap의 원인이 담겨있을 것이니 그 값이 9(Environment call from S-mode)인지 파악해서 맞다면 call이라는 함수로 뛰고, 그렇지 않다면 기존의 timervec을 실행하게 하는 것입니다.

call은 rdtime이라는 instruction을 통해 mtime의 값을 t0에 담아두고 mret instruction을 통해 기존 코드의 다음 줄로 돌아오게 됩니다.

.globl timervec
.align 4
timervec:               
		# 추가 ####################
        csrr t0, mcause
        li t1, 9
        beq t0, t1, call
		#########################
        
        csrrw a0, mscratch, a0
        sd a1, 0(a0)
        sd a2, 8(a0)
        sd a3, 16(a0)

		...

        mret

.globl call
.align 4
call:
        csrr t0, mepc
        addi t0, t0, 4
        csrw mepc, t0
        rdtime t0
        mret

그러면 sys_time()에서 t0의 값을 읽어옴으로써 mtime 레지스터의 값을 확보할 수 있는 것입니다.

uint64
sys_time(void)
{
  asm volatile("ecall");

  unsigned int t0_value;
  asm volatile("mv %0, t0" : "=r"(t0_value) : : "memory");
  return t0_value;
}

마무리


이렇게 sys_kbdints()와 sys_time()을 구현하는 과정을 알아보았습니다. system call이 구현되는 과정을 바탕으로 새로운 system call을 추가해보았고 mode간의 전환이 이뤄지는 과정을 이해하여 m-mode에서만 가능한 일을 처리하는 시스템 콜을 구현해보았습니다. 운영체제가 어떻게 동작하는지 항상 추상적으로 이해했었는데 이번 기회를 통해 Trap, 특히 system call에 대해 자세히 분석해볼 수 있어서 좋았고 도움이 되길 바라겠습니다.