xv6源码解读2:系统调用
⌨️

xv6源码解读2:系统调用

Last edited time
2023-12-28 / 13:27 PM
Created time
2023-12-28 / 13:20 PM
Tags
Operating Systems

第2篇:系统调用

在操作系统中,系统调用是从应用程序角度理解操作系统的关键。借助系统调用,我们可以从应用程序进入操作系统,而这个过程其实也很简单,就是把系统调用的参数放到寄存器中,然后程序把控制权完全交给操作系统,操作系统可以改变程序状态甚至终止程序。
我们来看看xv6中的系统调用。
查看kernel/syscall.h,我们可以看到xv6定义了21个系统调用,以及他们所对应的编号。
// System call numbers
#define SYS_fork    1
#define SYS_exit    2
#define SYS_wait    3
#define SYS_pipe    4
#define SYS_read    5
#define SYS_kill    6
#define SYS_exec    7
#define SYS_fstat   8
#define SYS_chdir   9
#define SYS_dup    10
#define SYS_getpid 11
#define SYS_sbrk   12
#define SYS_sleep  13
#define SYS_uptime 14
#define SYS_open   15
#define SYS_write  16
#define SYS_mknod  17
#define SYS_unlink 18
#define SYS_link   19
#define SYS_mkdir  20
#define SYS_close  21
接下来来看看xv6执行系统调用的过程。
在xv6启动之时,在main函数初始化几个设备和子系统后,便通过调用userinit函数来创建第一个进程,第一个进程执行一个用RISC-V程序集写的小型程序(在user/initcode.S)中,它通过调用exec系统调用重新进入内核。
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall
可以看到,li指令将参数SYS_exec(在kernel/syscall.h查看可知该值为7)存到寄存器a7中,然后使用ecall指令将控制权转移到操作系统内核中的一个特定地址,以执行特权级操作。通过这一系列操作之后,函数syscall(在kernel/syscall.c中)从陷阱帧(trapframe)中保存的a7中检索系统调用号(p->trapframe->a7),并用它索引到syscalls中。
void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  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;
  }
}
接下来就可以通过函数指针数组*syscalls[]kernel/syscall.c中)定位到相应的函数,在内核中执行该系统调用的功能,返回值将记录在p->trapframe->a0中,可以使得用户空间的系统调用返回这个值。之所以是该寄存器,是因为RISC-V上的C调用约定将返回值放在a0中。
那么接下来的一个问题是:ecall指令执行的过程中到底发生了什么,可以进入操作系统内核态。
在这个过程中,内核中执行的第一个指令是一个由汇编语言写的函数,叫做uservecuservec充斥着大量的汇编代码不便于阅读,在这里我们不需要过度关心uservec的细节。我们只需要知道之后在uservec函数中,代码执行跳转到了由C语言实现的函数usertrap。跳转到了C语言的函数,我们便能更好的理解了。在usertrap函数中,我们执行了函数syscall
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(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();
这里节选了一部分usertrap函数,可以看到,在执行函数syscall之前,还做了一些必要的操作,包括保证此时的系统调用是从用户态进入的,还有做一些中断相关的操作。
在函数syscall执行相应函数返回之后,同样的,我们需要返回用户空间中。而之前的ecall指令中断了用户空间代码的执行,恢复到用户空间,需要做一系列的事情,而usertrap函数的最后会执行usertrapret函数,该函数会完成这部分工作。当然,有一些工作只能由汇编代码完成,所以usertrapret函数最后会跳转到汇编代码执行userret函数。
我们可以用一幅图来概括这个过程:
notion image
职是之故,我们可以看到在用户空间看来一条简单的ecall指令,其中包含了大量的软硬件操作,对于用户空间来说,这些事情都是透明的,只需要对着系统调用发出请求,就可以得到自己想要的结果。一些更详细的关于trap的操作,可以阅读xv6 book的第四章进行了解。
有了系统调用作为应用程序和操作系统之间的媒介,我们也可以通过系统调用来研究操作系统,可以通过追踪应用程序的系统调用来观察程序的执行,这便是strace工具的强大之处,所以在xv6的lab中也设计了一个实现trace的lab,这也是在提示我们这一点的重要性。
你觉得怎么样?
YYDS
比心
加油
菜狗
views

Loading Comments...