xv6 6.S081 Lab6:Syscall

syscall代码在这里。不行了,今天已经快飙到极限了,写了5篇博客了……药不能停,再来一篇!

写在前面

syscall即system call,系统调用。完成本次实验可以让我们了解诸如:fork()write()exec()之类的函数究竟是如何调用的——这些函数在user文件夹中真的找不到原型……

和往常一样,我的博客OS实验xv6 6.S081 开坑中给出了一些有用的参考资料供大家参考。

实验介绍

MIT官方指导书献上。
本次实验内容主要有三个:

  • 回答给出的关于RISC-V汇编的几个问题;
  • 实现Uthread: switching between threads,即线程上下文的转换;
  • 实现Syscall:Alarm,即熟悉系统调用方式;

开始!

回答问题

在这里插入图片描述
阅读user/call.c与其汇编代码call.asm,回答下述问题:
1. 哪个寄存器用于函数传参?对于本例,那个寄存器将13传给了printf
2. f函数和g函数分别在哪里被调用了?
3. printf函数在哪个位置?
4. 执行完3a: 00000097 auipc ra,0x0后,ra中保存了什么?

通过阅读源码,我们可以给出

  1. register a2

    24:	4635                	li	a2,13
    
  2. f: line 26 ; g: line 26

    printf("%d %d\n", f(8)+1, 13);
    24:	4635                	li	a2,13
    26:	45b1                	li	a1,12
    

    可以看到,f(8)+1是一个inline function,于是在26: 45b1 li a1,12直接调用了f与g,将值算了出来(为12);

  3. 5c8

     34:	598080e7          	jalr	1432(ra) # 5c8 <printf>
    
  4. 3a

    3a:	00000097          	auipc	ra,0x0
    

    将当前pc的值加上0x0送入ra,故此时ra为3a。

Uthread: switching between threads

在这里插入图片描述
实验的任务是修改uthread.c的代码,使之能够支持进程的切换。我们首先应该完成thread_create来正确地创建线程;其次,应该在uthread_Switch.S中实现进程的切换(主要是涉及的操作是保存哪些寄存器,修改哪些寄存器)

乍一看,这个实验需要写汇编,好困难,事实上我们可以参考进程上下文切换的方式来完成线程上下文的切换。在proc.h中,对进程上下文的定义如下:

struct context {
  uint64 ra;
  uint64 sp;

  // callee-saved
  uint64 s0;
  uint64 s1;
  uint64 s2;
  uint64 s3;
  uint64 s4;
  uint64 s5;
  uint64 s6;
  uint64 s7;
  uint64 s8;
  uint64 s9;
  uint64 s10;
  uint64 s11;
};

其切换上下文的汇编代码如下:

# Context switch
#
#   void swtch(struct context *old, struct context *new);
# 
# Save current registers in old. Load from new.	


.globl swtch
swtch:
        sd ra, 0(a0)
        sd sp, 8(a0)
        sd s0, 16(a0)
        sd s1, 24(a0)
        sd s2, 32(a0)
        sd s3, 40(a0)
        sd s4, 48(a0)
        sd s5, 56(a0)
        sd s6, 64(a0)
        sd s7, 72(a0)
        sd s8, 80(a0)
        sd s9, 88(a0)
        sd s10, 96(a0)
        sd s11, 104(a0)

        ld ra, 0(a1)
        ld sp, 8(a1)
        ld s0, 16(a1)
        ld s1, 24(a1)
        ld s2, 32(a1)
        ld s3, 40(a1)
        ld s4, 48(a1)
        ld s5, 56(a1)
        ld s6, 64(a1)
        ld s7, 72(a1)
        ld s8, 80(a1)
        ld s9, 88(a1)
        ld s10, 96(a1)
        ld s11, 104(a1)
        
        ret

于是我们如法炮制,首先修改uthread.c中的thread定义如下:

struct thread {
  /** My Implementation  */
  uint64     ra;
  uint64     sp;
  // callee registers
  /** thread_switch needs to save/restore only the callee-save registers.   */
  uint64     s0;
  uint64     s1;
  uint64     s2;
  uint64     s3;
  uint64     s4;
  uint64     s5;
  uint64     s6;
  uint64     s7;
  uint64     s8;
  uint64     s9;
  uint64     s10;
  uint64     s11; 

  char     stack[STACK_SIZE]; /* the thread's stack */
  int        state;             /* FREE, RUNNING, RUNNABLE */
};

然后模仿进程上下文的切换,在uthread_Switch.S中补充线程上下文的切换:

	.text

/* Switch from current_thread to next thread_thread, and make
 * next_thread the current_thread.  Use t0 as a temporary register,
 * which should be caller saved. */

	.globl thread_switch
thread_switch:
	/* YOUR CODE HERE */
	sd ra, 0(a0)
	sd sp, 8(a0)
	sd s0, 16(a0)
	sd s1, 24(a0)
	sd s2, 32(a0)
	sd s3, 40(a0)
	sd s4, 48(a0)
	sd s5, 56(a0)
	sd s6, 64(a0)
	sd s7, 72(a0)
	sd s8, 80(a0)
	sd s9, 88(a0)
	sd s10, 96(a0)
	sd s11, 104(a0)

	ld ra, 0(a1)
	ld sp, 8(a1)
	ld s0, 16(a1)
	ld s1, 24(a1)
	ld s2, 32(a1)
	ld s3, 40(a1)
	ld s4, 48(a1)
	ld s5, 56(a1)
	ld s6, 64(a1)
	ld s7, 72(a1)
	ld s8, 80(a1)
	ld s9, 88(a1)
	ld s10, 96(a1)
	ld s11, 104(a1)
	ret    /* return to ra */

接下来,修改thread_create,使之能够记录线程的返回地址ra与栈地址sp。其中,返回地址意味着当切换线程时,线程应该返回到什么哪个地址。这里应该是传入的函数的入口func。代码如下:

void 
thread_create(void (*func)())
{
  struct thread *t;

  for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
    if (t->state == FREE) break;
  }
  t->state = RUNNABLE;
  // YOUR CODE HERE
  t->ra = (uint64)func;
  /** 栈是倒着生长 —— addi sp, sp, -STACK_SIZE  */
  t->sp = (uint64)(t->stack + STACK_SIZE);
}

最后,在thread_schedule中添加thread_switch调用:

void 
thread_schedule(void)
{
  struct thread *t, *next_thread;

  /* Find another runnable thread. */
  next_thread = 0;
  t = current_thread + 1;
  for(int i = 0; i < MAX_THREAD; i++){
    if(t >= all_thread + MAX_THREAD)
      t = all_thread;
    if(t->state == RUNNABLE) {
      next_thread = t;
      break;
    }
    t = t + 1;
  }

  if (next_thread == 0) {
    printf("thread_schedule: no runnable threads\n");
    exit(-1);
  }

  if (current_thread != next_thread) {         /* switch threads?  */
    next_thread->state = RUNNING;
    t = current_thread;
    current_thread = next_thread;
    /* YOUR CODE HERE
     * Invoke thread_switch to switch from t to next_thread:
     * thread_switch(??, ??);
     */
    /** 
     * My Implementatioon
     */
    thread_switch((uint64)t, (uint64)next_thread);
  } else
    next_thread = 0;
}

至此,我们便完成了Uthread。

Alarm

在这里插入图片描述
在Alarm中,我们要实现sigalarm(n, fn)int sigreturn(void)的系统调用。

参考指导书的步骤:
首先,我们在user/user.h声明Alarm系统调用接口:

/** AlarmTest  */
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

然后,参考user/usys.plkernel/syscall.hkernel/syscall.c的写法,添加sigalarmsigreturn的系统调用:
user/usys.pl

# AlarmTest
entry("sigalarm");
entry("sigreturn");

kernel/syscall.h

/** AlarmTest  */
#define SYS_sigalarm 26
#define SYS_sigreturn 27 

kernel/syscall.c

/** Alarm */
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(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,
[SYS_ntas]    sys_ntas,
[SYS_crash]   sys_crash,
/** Alarm */
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn
};

然后,我们可以在 kernel/sysfile.c中实现sigalarmsigreturn的系统调用:

/** AlarmTest  */
uint64 
sys_sigalarm(void){
  return 0;
}

uint64 
sys_sigreturn(void){
  return 0;
}

下面,我们将sys_sigalarm补充完整。首先要明确sigalarm(int ticks, void (*handler)())函数的作用:每ticks秒,执行一次handler函数。现在,我们遇到了第一个问题,如何传参?参考kernel/sysfile.c中其他系统调用函数的实现,我们可以很容易知道依靠argintargaddr等函数即可实现参数传递。接下来,根据指导书给出的几个Hints:

  • You’ll need to keep track of how many ticks have passed since the last call (or are left until the next call) to a process’s alarm handler; you’ll need a new field in struct proc for this too. You can initialize proc fields in allocproc() in proc.c.

  • Every tick, the hardware clock forces an interrupt, which is handled in usertrap(); you should add some code here.

  • You only want to manipulate a process’s alarm ticks if there’s a timer interrupt; you want something like
    if(which_dev == 2) …

  • Only invoke the alarm function if the process has a timer outstanding. Note that the address of the user’s alarm function might be 0 (e.g., in alarmtest.asm, periodic is at address 0).

我们可以很快完成sys_sigalarm的实现。
首先在在proc.h中对proc添加如下定义:

  /** AlarmTest  */
  int ticks;
  void (*handler)();
  /** 已经过了多久tick  */
  int tickpassed;

接着,在proc.c中的allocproc初始化这几个字段:

  /** AlarmTest  */
  p->ticks = -1;
  p->tickpassed = 0;
  p->handler = 0;

然后再trap.c中的时钟中断里修改进程proc的时钟状态,当流逝的时间等于预设的时间时,我们便调用handler函数,需要注意的是,由于中断发生在内核态,因此我们不能直接用p->handler()调用handler函数,因为用户态与内核态的函数地址不同,这里只能通过p->tf->epc = (uint64)p->handler将下一条指令的地址修改为handler地址才能正常运行。

if(which_dev == 2){
    // printf("got tick intr\n");
    // printf("num:%d\n",p->tf->a7);
    p->tickpassed++;
    if(p->ticks != 0){
      if(p->tickpassed == p->ticks){
        //printf("hh\n");
        //printf("handler: %p\n",p->handler);
        /** 
         * 
         * 存疑:为什么不能直接p->handler(),
         * 
         * 
         * 解决:usertrapret执行后执行userret,
         * userret使用sret,所以可以直接这样写;
         * 在内核态下,用户态页表被更换为内核态页表;
         * */
        p->tf->epc = (uint64)p->handler;
      }
    }
    yield();
  }

接下来,开始正式实现sys_sigalarm。很简单,利用argintargaddr获得参数后更新进程字段即可。

/** AlarmTest  */
uint64 
sys_sigalarm(void){
  struct proc *p = myproc();
  uint64 handler;
  int ticks;

  if(argint(0, &ticks) < 0)
    return -1;
  
  if(argaddr(1, &handler) < 0)
    return -1;
 
  p->ticks = ticks;
  p->handler = (void *)handler;
  return 0;
}

完善Alarm

在这一阶段,我们主要需要实现sigreturn。根据实验指导书的Hints,我们需要在执行handler之前将用户进程的地址保存起来,然后在调用sigreturn后恢复到调用handler之前的状态。为了实现这一点,指导书给出了一个非常重要的Hint:

Your solution will require you to save and restore registers—what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).

Hint中讲到,要保存许多寄存器。于是我的想法是干脆直接将整个trapframe保存起来,那里面含有足够多的寄存器。于是,向proc添加了新的字段,该字段用于保存trapframe:

  struct trapframe savedtf;

然后在trap.c中完成trapframe的保存:

  if(which_dev == 2){
    // printf("got tick intr\n");
    // printf("num:%d\n",p->tf->a7);
    p->tickpassed++;
    if(p->ticks != 0){
      if(p->tickpassed == p->ticks){
        //printf("hh\n");
        //printf("handler: %p\n",p->handler);
        /** 
         * 
         * 存疑:为什么不能直接p->handler(),
         * 
         * 
         * 解决:usertrapret执行后执行userret,
         * userret使用sret,所以可以直接这样写;
         * 在内核态下,用户态页表被更换为内核态页表;
         * */
        memmove(&p->savedtf, p->tf, sizeof(struct trapframe));
        p->tf->epc = (uint64)p->handler;
      }
    }
    yield();
  }

最后,填充sigreturn,使之能够将保存的trapframe重新恢复回来,代码如下:

uint64 
sys_sigreturn(void){
  struct proc *p = myproc();
  p->tickpassed = 0;
  memmove(p->tf, &p->savedtf, sizeof(struct trapframe));
  // printf("call return;\n");
  return 0;
}

至此,我们完成了整个syscall,测试结果如下,起飞!🛫
在这里插入图片描述

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
syscall6是golang runtime/internal/syscall包中的一个函数。它是用来调用系统调用的一个通用函数。 golang是一种编程语言,通常用来开发高性能的服务器端应用程序。在开发这样的应用程序时,有时需要直接与操作系统进行交互,这时就会用到系统调用。 系统调用是操作系统提供给应用程序的一组接口,可以让应用程序向操作系统发出请求,以执行一些底层的操作,如文件输入输出、进程管理等。不同的操作系统对应的系统调用可能有所不同。 而在golang中,为了方便开发者直接调用系统调用,runtime/internal/syscall包提供了一组函数,其中就包括syscall6。syscall6函数是一个通用函数,用来调用各种系统调用。 具体来说,syscall6函数接受六个参数:系统调用号、参数1、参数2、参数3、参数4和参数5。它会根据不同的系统调用号和参数,将请求传递给操作系统,并返回系统调用的结果。 使用syscall6函数可以让我们在golang中直接使用系统调用,而不需要依赖外部库或者传统的C语言接口。这样可以简化代码,提高效率,并且可以充分利用golang的高并发和轻量级线程的特性。 总的来说,syscall6函数是golang中用来调用系统调用的一个通用函数,它可以方便地与操作系统进行交互,执行一些底层的操作。通过使用syscall6函数,我们可以更好地利用golang的优势,并且简化代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值