System call 구현하기

xv6: a simple, Unix-like teaching operating system의 Ch 4.는 Trap과 System call에 대해서 다룬다. 이를 읽고 정리한 문서는 Ch4. Traps and system calls에 있다.

PA2 Specification

  1. kbdints() 구현하기
  • device가 boot한 시점부터 들어온 모든 keyboard interrupts의 개수를 세서 리턴하는 system call kbdints()를 구현해야 한다.
  • System call number는 22로 주어진다.
  1. time() 구현하기
  • mtime 레지스터를 읽어서 리턴하는 system call time()을 구현해야 한다.
  • mtime 레지스터를 읽어오는 rdtime instruction은 machine mode에서만 실행 가능하다.
  • System call number는 23으로 주어진다.

1. kbdints() 구현하기

kbdints()를 구현하기 위해서는 먼저 system call number를 추가해주어야 했다. 이는 주어진 스켈레톤 코드에서 이미 설정되어 있었다.

1
2
3
4
5
6
7
8
// kernel/syscall.h

...
#define SYS_close  21
#ifdef SNU
#define SYS_kbdints 22
#define SYS_time    23
#endif

따라서 kernel/syscall.c 에서 함수를 extern으로 선언해주고, syscall[]()에 추가해준다. 이렇게 해주면 ecall이 발생되었을 때 커널에서 system call임을 인지하고, syscall()에서는 a7 레지스터에 받은 system call number를 읽어 내가 구현한 custom system call을 실행해 주게 된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// kernel/syscall.c

static uint64 (*syscalls[])(void) = {
    [SYS_fork]    sys_fork,
    ...
    [SYS_close]   sys_close,
    #ifdef SNU
    [SYS_kbdints] sys_kbdints,
    [SYS_time]    sys_time,
    #endif
};

console.c에서는 keyboard interrupt가 날 때 마다 올려줄 변수 kbdints을 선언해주고, 인터럽트가 발생하면 실행되는 함수 consoleintr()의 마지막에 ++kbdints를 해주어 키보드 인터럽트의 횟수가 잘 저장될 수 있도록 해준다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// kernel/console.c

uint64 kbdints = 0;

...

void
consoleintr(int c)
{
    acquire(&cons.lock);
    ...
    ++kbdints;
    release(&cons.lock);
}

system call의 실체는 sysproc.c에 구현하였다.

1
2
3
4
5
6
// kernel/sysproc.c

int sys_kbdints(void)
{
  return kbdints;
}

이렇게 단순히 저장된 키보드 인터럽트 횟수를 리턴해주게 되면 원하는 system call kbdints()를 구현할 수 있다.

2. time() 구현하기

kbdints()는 비교적 단순하게 구현할 수 있었지만, time()의 경우에는 삽질을 많이 했다. 우선 xv6 운영체제는 3개의 privilege mode를 가지고 있는데, user mode, supervisor mode, machine mode가 이 3가지이다. 사용자는 user space에서 프로그램을 실행하고, 커널의 대부분은 supervisor mode에서 실행된다. 더 높은 privilege mode인 machine mode는 부팅 시에 운영체제를 세팅하는 코드만 조금 실행한다. xv6는 기본적으로 모든 trap을 machine mode에서 처리하는데, xv6는 부팅 시에 start() 함수에서 mideleg, medeleg 레지스터를 모두 0xffff로 수정하여 모든 interrupt와 exception을 supervisor mode로 delegate한다.

2.1. machine mode에서 코드 실행하기

우리는 time() 시스템콜 내에서 mtime 레지스터를 읽어야 하므로 machine mode에서 trap을 처리할 수 있도록 해주어야 한다. medelegmideleg에서 적절한 비트를 0으로 내려서 특정한 trap은 machine mode에서 처리할 수 있도록 해주어야 한다. mcause가 리턴하는 값에 해당하는 인덱스를 수정하면 되는데, 각 비트가 뜻하는 것은 다음에서 확인할 수 있다.

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
10Reserved
11Supervisor software interrupt
12Reserved
13Machine software interrupt (mcause only)
14Reserved
15Supervisor timer interrupt
16Reserved
17Machine timer interrupt (mcause only)
18Reserved
19Supervisor external interrupt
110Reserved
111Machine external interrupt (mcause only)
1>= 12Reserved

Interrupt가 0이면 medeleg에서, 1이면 mideleg에서 수정해주면 된다. 나는 time() System call 안에서 rdtime을 하고싶은 것이므로, environment call (ecall) from supervisor인 9번째 비트를 0으로 바꿔주어야 했다. 따라서 medeleg 레지스터의 9번째 비트를 0으로 내려주기 위해, 0xffff로 설정하던 medeleg를 0xfdff로 바꿔주었다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// kernel/start.c

void
start()
{
  ...
  // delegate interrupts and exceptions to supervisor mode.
  w_medeleg(0xfdff);
  w_mideleg(0xffff);
  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
  ...
}

내가 구현하고 있는 sys_time()에서 ecall instruction을 실행해서 machine mode로 privilege level을 올리기 위해서 위와 같이 비트를 내려주었다. 비트가 0xffff로 설정되어 있을 때 ecall을 실행하면, supervisor mode에서 ecall을 처리하므로 stvec가 가리키고 있는 kernelvec이 실행된다. 하지만 비트를 내려서 machine mode에서 처리하도록 하면 mtvec가 가리키고 있는 timervec이 실행된다. 이로서 machine mode로 privilege level을 올리는 것 까지는 성공하였다.

2.2. mtime 레지스터 읽어오기

내가 구현한 sys_time()은 다음과 같다.

1
2
3
4
5
6
7
8
9
// kernel/sysproc.c

uint64 sys_time(void)
{
  uint64 time;
  asm volatile("ecall");
  asm volatile("mv %0, a0" : "=r"(time) : : "memory");
  return time;
}

ecall instruction을 실행하면 timervec을 실행하게 된다. 그러나 timervec은 timer interrupt를 처리하기 위해 있는 코드이므로, timervec의 코드를 수정해주어야 했다. 따라서 timervec에서는 mcause를 읽어 ecall from s-mode인 경우에는 다른 함수를 실행할 수 있도록 해주고자 했다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// kernel/kernelvec.S

.globl timervec
.align 4
timervec:
    ...

    csrr t0, mcause
    li t1, 9
    beq t0, t1, machinevec

    ... # timervec code

machinevec:
    csrr t0, mepc
    addi t0, t0, 4
    csrw mepc, t0

    rdtime a0

    mret

따라서 임시 레지스터인 t0 레지스터에 mcause 레지스터를 읽어오고, mcause 가 9이면 내가 만든 함수인 machinevec으로 뛰도록 했다. machinevec에서는 a0레지스터에 mtime을 읽고, mepc에 4를 더해 ecall 다음 instruction으로 리턴할 수 있도록 했다. 이후 위에서 볼 수 있듯 sys_time() 함수에서 a0 레지스터를 읽어 리턴한다.

RISC-V 구조에서 사용하는 Register는 다음 그림에 있다.

RISC-V Registers
RISC-V Registers

컴퓨터구조 수업을 듣지 않고 듣는 수업이라 RISC-V 어셈블리를 처음 다뤄보는 것이어서 조금 헤맸지만, 구현하고 나니 생각보다 쉬운 과제였다. 이 과제에서도 이만큼 헤맸는데 다음 과제는 각오하고 풀어야 할 것 같다.