6.S081|Lab2-系统调用流程
本文最后更新于 142 天前,其中的信息可能已经有所发展或是发生改变。

系统调用过程当中有几个关键技术点:

  • 系统调用号:用户程序通过指定一个系统调用号来告诉操作系统应该执行哪个系统调用。xv6中使用a7寄存器来传递这个调用号。

  • 系统调用指令:指定系统调用号以后,用户程序执行系统调用指令,该指令会提升特权级只内核态,保存用户程序执行的上下文,然后跳到内核的处理系统调用的代码去执行。xv6这条指令是ecall,x86里是int80。

  • trapframe:trapframe是一小段内存,主要负责在内核态和用户态转换时保存一些执行现场。

  • 用户栈,内核栈:执行系统调用前,用户程序使用用户栈,跳转到系统调用后,内核代码使用独立的内核栈。

  • 传参:系统调用使用寄存器传参,如果寄存器不够用,那么可以让寄存器指向一块内存,在这块内存里在存放多个参数。

我们来看一下一个用户级程序调用系统调用的过程。就拿echo来举例。

//user/echo.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
  int i;

  for(i = 1; i < argc; i++){
    write(1, argv[i], strlen(argv[i]));
    if(i + 1 < argc){
      write(1, " ", 1);
    } else {
      write(1, "\n", 1);
    }
  }
  exit(0);
}

这里的writeexit都是系统调用。我们查看用户头文件,就可以找到它们的声明。那么这个声明又是去哪的呢?我们发现我们按下Command + 左键无法再跳转了。

//user/user.h
struct stat;
struct rtcdate;
struct sysinfo;

// system calls
int fork(void);
int exit(int) __attribute__((noreturn));
int wait(int*);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);

// ulib.c
int stat(const char*, struct stat*);
char* strcpy(char*, const char*);
void *memmove(void*, const void*, int);
char* strchr(const char*, char c);
int strcmp(const char*, const char*);
void fprintf(int, const char*, ...);
void printf(const char*, ...);
char* gets(char*, int max);
uint strlen(const char*);
void* memset(void*, int, uint);
void* malloc(uint);
void free(void*);
int atoi(const char*);
int memcmp(const void *, const void *, uint);
void *memcpy(void *, const void *, uint);

实际上这些都是前往跳板函数的入口,我们打开kernel/usys.pl就能发现,这里的代码定义着生成跳板函数汇编代码。函数声明在 user.h ,而编译器在编译时会链接到 usys.S 中的对应汇编实现。

//kernel/usys.pl
#!/usr/bin/perl -w

# Generate usys.S, the stubs for syscalls.

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");



生成的汇编代码则如下

//kernel/usys.S
# generated by usys.pl - do not edit
#include "kernel/syscall.h"
.global fork
fork:
 li a7, SYS_fork
 ecall
 ret
.global exit
exit:
 li a7, SYS_exit
 ecall
 ret
.global wait
wait:
 li a7, SYS_wait
 ecall
 ret
.global pipe
pipe:
 li a7, SYS_pipe
 ecall
 ret
.global read
read:
 li a7, SYS_read
 ecall
 ret
.global write
write:
 li a7, SYS_write
 ecall
 ret
.global close
close:
 li a7, SYS_close
 ecall
 ret
.global kill
kill:
 li a7, SYS_kill
 ecall
 ret
.global exec
exec:
 li a7, SYS_exec
 ecall
 ret
.global open
open:
 li a7, SYS_open
 ecall
 ret
.global mknod
mknod:
 li a7, SYS_mknod
 ecall
 ret
.global unlink
unlink:
 li a7, SYS_unlink
 ecall
 ret
.global fstat
fstat:
 li a7, SYS_fstat
 ecall
 ret
.global link
link:
 li a7, SYS_link
 ecall
 ret
.global mkdir
mkdir:
 li a7, SYS_mkdir
 ecall
 ret
.global chdir
chdir:
 li a7, SYS_chdir
 ecall
 ret
.global dup
dup:
 li a7, SYS_dup
 ecall
 ret
.global getpid
getpid:
 li a7, SYS_getpid
 ecall
 ret
.global sbrk
sbrk:
 li a7, SYS_sbrk
 ecall
 ret
.global sleep
sleep:
 li a7, SYS_sleep
 ecall
 ret
.global uptime
uptime:
 li a7, SYS_uptime
 ecall
 ret

在这里我们将需要的系统调用号保存在a7寄存器中,然后执行ecall系统调用。关于系统调用号,定义在kernel/syscall.h里面

// 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

接着,通过ecall进入到内核态之后,便开始根据系统调用号的不同采用不同的系统调用实现,然后将执行结果返回过用户程序,通过a0寄存器传递结果。

//kernel/syscall.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "syscall.h"
#include "defs.h"

// Fetch the uint64 at addr from the current process.
int
fetchaddr(uint64 addr, uint64 *ip)
{
  struct proc *p = myproc();
  if(addr >= p->sz || addr+sizeof(uint64) > p->sz)
    return -1;
  if(copyin(p->pagetable, (char *)ip, addr, sizeof(*ip)) != 0)
    return -1;
  return 0;
}

// Fetch the nul-terminated string at addr from the current process.
// Returns length of string, not including nul, or -1 for error.
int
fetchstr(uint64 addr, char *buf, int max)
{
  struct proc *p = myproc();
  int err = copyinstr(p->pagetable, buf, addr, max);
  if(err < 0)
    return err;
  return strlen(buf);
}

//用户程序传递入参是通过a0~a5寄存器
static uint64
argraw(int n)
{
  struct proc *p = myproc();
  switch (n) {
  case 0:
    return p->trapframe->a0;
  case 1:
    return p->trapframe->a1;
  case 2:
    return p->trapframe->a2;
  case 3:
    return p->trapframe->a3;
  case 4:
    return p->trapframe->a4;
  case 5:
    return p->trapframe->a5;
  }
  panic("argraw");
  return -1;
}

// Fetch the nth 32-bit system call argument.
int
argint(int n, int *ip)
{
  *ip = argraw(n);
  return 0;
}

// Retrieve an argument as a pointer.
// Doesn't check for legality, since
// copyin/copyout will do that.
int
argaddr(int n, uint64 *ip)
{
  *ip = argraw(n);
  return 0;
}

// Fetch the nth word-sized system call argument as a null-terminated string.
// Copies into buf, at most max.
// Returns string length if OK (including nul), -1 if error.
int
argstr(int n, char *buf, int max)
{
  uint64 addr;
  if(argaddr(n, &addr) < 0)
    return -1;
  return fetchstr(addr, buf, max);
}

extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);

static uint64 (*syscalls[])(void) = {
[SYS_fork]    sys_fork,
[SYS_exit]    sys_exit,
[SYS_wait]    sys_wait,
[SYS_pipe]    sys_pipe,
[SYS_read]    sys_read,
[SYS_kill]    sys_kill,
[SYS_exec]    sys_exec,
[SYS_fstat]   sys_fstat,
[SYS_chdir]   sys_chdir,
[SYS_dup]     sys_dup,
[SYS_getpid]  sys_getpid,
[SYS_sbrk]    sys_sbrk,
[SYS_sleep]   sys_sleep,
[SYS_uptime]  sys_uptime,
[SYS_open]    sys_open,
[SYS_write]   sys_write,
[SYS_mknod]   sys_mknod,
[SYS_unlink]  sys_unlink,
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
};

// kernel/syscall.c
const char *syscall_names[] = {
[SYS_fork]    "fork",
[SYS_exit]    "exit",
[SYS_wait]    "wait",
[SYS_pipe]    "pipe",
[SYS_read]    "read",
[SYS_kill]    "kill",
[SYS_exec]    "exec",
[SYS_fstat]   "fstat",
[SYS_chdir]   "chdir",
[SYS_dup]     "dup",
[SYS_getpid]  "getpid",
[SYS_sbrk]    "sbrk",
[SYS_sleep]   "sleep",
[SYS_uptime]  "uptime",
[SYS_open]    "open",
[SYS_write]   "write",
[SYS_mknod]   "mknod",
[SYS_unlink]  "unlink",
[SYS_link]    "link",
[SYS_mkdir]   "mkdir",
[SYS_close]   "close",
};

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

关于ecall的tips:

  1. 这个指令是我们的关键指令,也是进入内核的必要指令。在一般情况下,user目录下的代码都是运行在用户模式态下的,不具有访问硬件资源的权限。使用ecall指令后,会从用户态(User model)转到 监管模式(Supervisor model),此时便具有更高的权限。同时这条指令也与x86里面的 int 0x80 指令一样,会触发异常处理程序,从而陷入内核态。

  2. 注意,现在准备工作还没有做完。在使用ecall指令时,我们的意图就是进入内核态,但是,原先的pc指针怎么办,我们还要靠它回去呢,并且跳转到哪去呢?所以,执行这条命令后,先是提升权限,然后是把我们原来的next pc 存储到sepc(S 态异常寄存器 )里面,最后把pc设置为stvec[S态陷阱(Trap)向量(Vector)基地址寄存器) ].在stvec寄存器里面就是我们预先设置好的处理代码,也就是指向trampoline.S,接着通过trampoline.S里的函数,再跳到kernel/trap.c中的usertrap()函数,处理由用户态陷入内核时要干的事情,比如处理系统调用、设备中断和其他异常的入口函数。在这个函数中,系统会检测陷入的原因,并采取相应的操作,比如处理系统调用或者设备中断。

  3. 用户态和内核态的tempoline都处于一个虚拟地址上(也就是地址空间的最顶层),且映射到的物理地址也一样。如果不是的话,想想一下 ,在我们执行ecall指令后,内核页表切换后,不在一块内存岂不是会使程序崩溃掉。

Pasted image 20240927222049.png

trampoline:是一段汇编代码,有两段实现uservec和userret

  1. uservec对应ecall,主要任务是把寄存器状态存在trapframe中,然后跳转到内核syscall函数

  2. userret对应sret,主要任务是把寄存器状态从trapframe中恢复,然后跳转到之前用户程序的指令(应该值保存指令的下一条指令)并继续执行

trapframe:一小段内存,用于保存执行现场(寄存器状态),阅读下面的代码,则可以发现处理过程中将trapframe的地址存入a0寄存器,然后通过偏移量的方式进行存取。

//kernel/trampoline.S
	#
        # code to switch between user and kernel space.
        #
        # this code is mapped at the same virtual address
        # (TRAMPOLINE) in user and kernel space so that
        # it continues to work when it switches page tables.
	#
	# kernel.ld causes this to be aligned
        # to a page boundary.
        #
	.section trampsec
.globl trampoline
trampoline:
.align 4
.globl uservec
uservec:    
	#
        # trap.c sets stvec to point here, so
        # traps from user space start here,
        # in supervisor mode, but with a
        # user page table.
        #
        # sscratch points to where the process's p->trapframe is
        # mapped into user space, at TRAPFRAME.
        #
        
	# swap a0 and sscratch
        # so that a0 is TRAPFRAME
        csrrw a0, sscratch, a0

        # save the user registers in TRAPFRAME
        sd ra, 40(a0)
        sd sp, 48(a0)
        sd gp, 56(a0)
        sd tp, 64(a0)
        sd t0, 72(a0)
        sd t1, 80(a0)
        sd t2, 88(a0)
        sd s0, 96(a0)
        sd s1, 104(a0)
        sd a1, 120(a0)
        sd a2, 128(a0)
        sd a3, 136(a0)
        sd a4, 144(a0)
        sd a5, 152(a0)
        sd a6, 160(a0)
        sd a7, 168(a0)
        sd s2, 176(a0)
        sd s3, 184(a0)
        sd s4, 192(a0)
        sd s5, 200(a0)
        sd s6, 208(a0)
        sd s7, 216(a0)
        sd s8, 224(a0)
        sd s9, 232(a0)
        sd s10, 240(a0)
        sd s11, 248(a0)
        sd t3, 256(a0)
        sd t4, 264(a0)
        sd t5, 272(a0)
        sd t6, 280(a0)

	# save the user a0 in p->trapframe->a0
        csrr t0, sscratch
        sd t0, 112(a0)

        # restore kernel stack pointer from p->trapframe->kernel_sp
        ld sp, 8(a0)

        # make tp hold the current hartid, from p->trapframe->kernel_hartid
        ld tp, 32(a0)

        # load the address of usertrap(), p->trapframe->kernel_trap
        ld t0, 16(a0)

        # restore kernel page table from p->trapframe->kernel_satp
        ld t1, 0(a0)
        csrw satp, t1
        sfence.vma zero, zero

        # a0 is no longer valid, since the kernel page
        # table does not specially map p->tf.

        # jump to usertrap(), which does not return
        jr t0

.globl userret
userret:
        # userret(TRAPFRAME, pagetable)
        # switch from kernel to user.
        # usertrapret() calls here.
        # a0: TRAPFRAME, in user page table.
        # a1: user page table, for satp.

        # switch to the user page table.
        csrw satp, a1
        sfence.vma zero, zero

        # put the saved user a0 in sscratch, so we
        # can swap it with our a0 (TRAPFRAME) in the last step.
        ld t0, 112(a0)
        csrw sscratch, t0

        # restore all but a0 from TRAPFRAME
        ld ra, 40(a0)
        ld sp, 48(a0)
        ld gp, 56(a0)
        ld tp, 64(a0)
        ld t0, 72(a0)
        ld t1, 80(a0)
        ld t2, 88(a0)
        ld s0, 96(a0)
        ld s1, 104(a0)
        ld a1, 120(a0)
        ld a2, 128(a0)
        ld a3, 136(a0)
        ld a4, 144(a0)
        ld a5, 152(a0)
        ld a6, 160(a0)
        ld a7, 168(a0)
        ld s2, 176(a0)
        ld s3, 184(a0)
        ld s4, 192(a0)
        ld s5, 200(a0)
        ld s6, 208(a0)
        ld s7, 216(a0)
        ld s8, 224(a0)
        ld s9, 232(a0)
        ld s10, 240(a0)
        ld s11, 248(a0)
        ld t3, 256(a0)
        ld t4, 264(a0)
        ld t5, 272(a0)
        ld t6, 280(a0)

	# restore user a0, and save TRAPFRAME in sscratch
        csrrw a0, sscratch, a0
        
        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret

总结

  • 来自群友 Days

ecall会更改程序计数器,ecall之后,pc指到trampoline page,接着就执行uservec。

  • 来自Claude3.5:

从用户空间到内核空间的转换,然后再返回用户空间。

  1. 用户空间触发 ecall:

    • 用户程序执行 ecall 指令(通常是通过系统调用库函数)。

    • ecall 指令会触发一个同步异常。

  2. 硬件响应:

    • CPU 硬件自动执行以下操作:

      a. 将当前的程序计数器(PC)保存到 sepc 寄存器。

      b. 将当前的特权级别保存到 sstatus 寄存器的 SPP 字段。

      c. 将特权级别切换到 supervisor 模式。

      d. 将 stvec 寄存器中的地址加载到 PC,跳转到该地址。

  3. 进入 trampoline:

    • PC 跳转到 stvec 指向的地址,即 trampoline 页面中的 uservec 函数。

    • uservec 函数开始执行(在 trampoline.S 中)。

  4. uservec 函数:

    • 保存用户空间的寄存器到 trapframe。

    • 加载内核页表。

    • 设置内核栈。

    • 跳转到 usertrap 函数(在 trap.c 中)。

  5. usertrap 函数:

    • 进一步设置trap处理环境。

    • 判断trap的原因(在这个case中是系统调用)。

    • 调用 syscall 函数来处理系统调用(在 syscall.c 中)。

  6. syscall 函数:

    • 根据系统调用号调用相应的系统调用处理函数。

    • 执行请求的系统调用操作。

    • 将结果存储在 a0 寄存器中(这将成为用户空间看到的返回值)。

  7. 系统调用完成后:

    • 控制返回到 usertrap 函数。

    • usertrap 调用 usertrapret 函数来准备返回用户空间。

  8. usertrapret 函数:

    • 设置 stvec 为处理下一个trap的地址。

    • 设置 trapframe 中的一些字段。

    • 通过跳转到 userret(在trampoline中)来返回用户空间。

  9. userret 函数:

    • 恢复用户页表。

    • 从 trapframe 恢复用户寄存器。

    • 执行 sret 指令返回用户空间。

  10. 返回用户空间:

    • CPU 硬件从 sepc 恢复 PC。

    • 从 sstatus 恢复特权级别。

    • 用户程序继续执行,就好像从 ecall 指令的下一条指令开始。

附上一些参考资料

xv6内存布局 - Deniffer's Blog

xv6的系统调用过程_xv6自定义系统调用怎么实现-CSDN博客

MIT 6.S081 聊聊xv6中的trap - KatyuMarisa - 博客园 (cnblogs.com)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇