主要分析 XV6 系统调用流程及整体框架,总体来说分为 2 个流程

  • 注册系统调用
  • 使用系统调用

    1. 注册系统调用

    read 系统调用为例,其注册的流程主要如下:

    1.1 增加调用声明

    声明接口,给用户进程使用
    1. int read(int, void*, int);
    增加系统调用号
    1. #define SYS_read 5

    1.2 在 user/usys.pl 增加 entry

    执行 make 指令编译 xv6 的时候会执行 usys.pl 脚本, usys.pl 文件内容大致如下: ```c print “# generated by usys.pl - do not edit\n”;

print “#include \”kernel/syscall.h\”\n”;

sub entry { my $name = shift; print “.global $name\n”; print “${name}:\n”; print “ li a7, SYS_${name}\n”; print “ ecall\n”; print “ ret\n”; }

entry(“fork”); entry(“exit”); entry(“wait”); entry(“pipe”); entry(“read”); entry(“write”); entry(“close”); entry(“kill”); entry(“exec”); entry(“open”); entry(“mknod”); entry(“unlink”); entry(“fstat”); entry(“link”); entry(“mkdir”); entry(“chdir”); entry(“dup”); entry(“getpid”); entry(“sbrk”); entry(“sleep”); entry(“uptime”);

  1. 这一步操作的目的主要是为了在编译 xv6 时,为 `read` 接口生成如下的汇编代码定义:

以 read 为例

read: li a7, SYS_read # 将 SYS_read 系统调用号放到 a7 寄存器 ecall # 执行 ecall 指令进入中断 ret

<a name="TefCb"></a>
### 1.3 注册系统调用表
内核中有一个 `syscalls` 数组,用于关联 **系统调用号** 与 **系统调用 **,因此需要在此增加 `sys_read` 调用。
```c
extern uint64 sys_read(void);

static uint64 (*syscalls[])(void) = {
// some code ...
[SYS_read]    sys_read,
};

1.4 实现系统调用

这一步骤就是进一步实现 sys_read 系统调用的逻辑,根据具体需求再具体实现。

可以看到,关于 read 有两个接口:

  1. int read(int, void*, int);
  2. uint64 sys_read(void);

这里为什么要拆分成 2 个接口呢? 个人观点,主要还是由于用户进程无法直接执行内核的接口(权限问题),需要通过中断触发进入内核态才能执行,因此需要封装一个 wrapper function,供用户进程使用,做一次中转。用户进程只需要知道 read 的函数签名,不关心其具体实现。 read 主要提供给用户进程使用,进而触发中断执行 sys_read 接口,sys_read 才是真正执行逻辑的地方。

2. 使用系统调用

read 为例,分析执行 read 接口之后的执行流程

2.1 执行用户态接口

根据 1.1 ,我们在 user.h 中增加了 read 接口,因此用户进程只需要 include 该头文件即可调用该接口。当用户进程调用 read 时,其本质是调用了 usys.pl 生成的汇编函数:

read:
li a7, SYS_read  # 将 SYS_read 系统调用号放到 a7 寄存器
ecall                         # 执行 ecall 指令进入中断
ret

2.2 中断进入内核态

在 2.1 的汇编函数中,其主要干了 2 件事:

  • 保存系统调用号到 a7 寄存器
  • 执行 ecall 指令

ecall 指令会触发中断,进入内核,内核对在用户态发生的中断的处理大致如下:

void usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();

  // save user program counter.
  p->trapframe->epc = r_sepc();

  if(r_scause() == 8){
    // system call

    if(p->killed)
      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 sstatus &c registers,
    // so don't enable until 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());
    p->killed = 1;
  }

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

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

可以看到,17 ~ 31 行专门处理,由于 ecall 指令触发的中断,其中断号为 8。主要执行了 syscall 函数

2.3 处理系统调用中断

处理系统调用中断的接口很简单,流程大致如下:

  1. 获取 a7 寄存器中的 系统调用号
  2. 根据调用号,在 syscalls 数组中找到对应的系统调用函数
  3. 执行系统调用函数,返回值保存在 a0 寄存器

    void
    syscall(void)
    {
    int num;
    struct proc *p = myproc();
    
    num = p->trapframe->a7;
    if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
     p->trapframe->a0 = syscalls[num]();
    } else {
     printf("%d %s: unknown sys call %d\n",
             p->pid, p->name, num);
     p->trapframe->a0 = -1;
    }
    }
    

    这里可能会有疑惑,系统调用的参数是如何传递的,这里可以简单了解下 risc-v 的 calling-convention,其参数传递与 x86 类似,存放在寄存器中,risc-v 的前 5 个参数会存放在 a0 ~ a5 寄存器中。

    3. 小结

    小结一下 xv6 系统调用的注册与使用流程,以 read 为例:

    生成

  • 增加 系统调用 声明,供用户进程使用,如 int read(int, void*, int);
  • 增加 系统调用号,如 #define SYS_read 5
  • usys.pl 增加 entry("read") ,生成 read 接口的汇编代码,从而用户进程在执行 read 后可以触发中断,进入内核态。
  • 定义真正执行逻辑的 系统调用 uint64 sys_read(void);,这里的格式都是 sys_xxx,返回值都为 uint64,且都无参数,这是因为参数都存放在寄存器中。
  • 注册系统调用,在 syscalls 数组增加 系统调用号 SYS_read系统调用 sys_read 的对应关系

    执行

  • 用户进程执行 read,保存系统调用号到 a7 寄存器,执行 ecall 指令进入中断,陷入内核态

  • 内核处理中断,根据中断号确定是执行系统调用触发的中断ecall 指令)
  • 根据 a7 寄存器的中断号,在 syscalls 数组中找到系统调用,并执行之