操作系统 MIT6.S081 Lab4 Trap

本文详细介绍了RISC-V架构下操作系统如何处理陷阱(包括系统调用、异常和中断),并展示了如何实现回溯功能来打印栈上的调用链。此外,还阐述了如何通过trapframe和自旋锁实现进程时间分片,详细描述了sys_sigalarm和sys_sigreturn系统调用的实现过程,以及它们在时间片到期时如何保存和恢复现场。最后,通过实验结果验证了backtrace和时间分片功能的正确性。

操作系统 MIT6.S081 Lab4 Trap

实验原理

Trap:系统调用、异常和中断会导致CPU停止当前工作,这三种情况统称为trap

Xv6 将所有的 Traps 都放在内核中处理:

  • 对于系统调用,则执行相应的系统调用
  • 对于中断,则调用相应的设备驱动程序
  • 对于异常,直接杀死抛出异常的进程

② RISC-V 的栈和寄存器

每一个 RISC-V CPU 都有一组寄存器,内核可以从中读取 Trap 的信息,也可以往寄存器中写值以告知 CPU 如何处理 Trap,比较重要的有:

  • stvec:处理 Trap 的 handler 入口
  • sepc:保存当前进程的 PC
  • scause:其中保存的数值反映了 Trap 的类型

Trampoline:当 trap 发生时,RISCV并不会切换页表,因此用户空间到stvec应当有着稳定的映射(即trampoline)所有用户的 trampoline 都映射到同一片区域,而且只能由内核访问。

Trapframe:保存了当前进程所使用的寄存器的所有值

RISC-V 的寄存器:

reg    | name  | saver  | description
-------+-------+--------+------------
x0     | zero  |        | hardwired zero
x1     | ra    | caller | return address
x2     | sp    | callee | stack pointer
x3     | gp    |        | global pointer
x4     | tp    |        | thread pointer
x5-7   | t0-2  | caller | temporary registers
x8     | s0/fp | callee | saved register / frame pointer
x9     | s1    | callee | saved register
x10-11 | a0-1  | caller | function arguments / return values
x12-17 | a2-7  | caller | function arguments
x18-27 | s2-11 | callee | saved registers
x28-31 | t3-6  | caller | temporary registers
pc     |       |        | program counter

RISC-V 的栈结构:

      +-> |       ...       |   |
      |   +-----------------+   |
      |   | return address  |   |
      |   |   previous fp ------+
      |   | saved registers |
      |   | local variables |
      |   |       ...       | <-+
      |   +-----------------+   |
      |   | return address  |   |
      +------ previous fp   |   |
          | saved registers |   |
          | local variables |   |
  $fp --> |       ...       |   |
          +-----------------+   |
          | return address  |   |
          |   previous fp ------+
          | saved registers |
  $sp --> | local variables |
          +-----------------+

Part 1 Backtrace

实验目的: 实现 backtrace 功能,用于打印出栈上调用链的所有返回地址

实验步骤:

① 已知当前函数的帧指针存在寄存器 s0 中,因此在 kernel/riscv.h 中添加获得该值的函数:

// kernel/riscv.h
static inline uint64
r_fp()
{
    uint64 x;
    asm volatile("mv %0, s0" : "=r" (x) );
    return x;
}

② 实现 backtrace() ,并在 defs.h 中声明:

// kernel/defs.h
// printf.c
void            backtrace(void);
// kernel/printf.c
void backtrace(void)
{
  printf("backtrace:\n");
  uint64 retAddr = r_fp();
  while (retAddr < PGROUNDUP(retAddr)) {
    printf("%p\n", *((uint64*)(retAddr - 8)));
    retAddr = *((uint64*)(retAddr - 16));
  }
}

使用 PGUPGROUND() 获得当前栈的顶部地址;由 RISC-V 的栈结构可知,返回地址存放在帧指针的 -8 偏移量处,保存的帧指针存放在帧指针的 -16 偏移量处

③ 在 sys_sleep() 中调用 backtrace() ,也可以在 panic() 中调用 backtrace()

// kernel/sysproc.c
uint64
sys_sleep(void)
{
  ...
  backtrace();
  return 0;
}
// kernel/printf.c
void
panic(char *s)
{
  ...
  backtrace();
  for(;;) ;
}

实验结果:

执行 bttest ,可以看到打印出了三个地址:

请添加图片描述

执行 addr2line -e kernel/kernel ,查找地址对应的代码:

请添加图片描述

查看源码,可以观察到调用链为:usertrap() →\to syscall() →\to sys_sleep()

Part 2 Alarm

实验目标: 实现进程时间分片的功能

实验步骤:

① 在 struct proc 中添加相关变量,并添加构造和析构动作,用于辅助 sigalarm()sigreturn() 系统调用实现:

// kernel/proc.h
struct proc {
  ...
  int interval;			// 从 sigalarm 开始,该进程能运行多少个 ticks
  void (*handler)();	// 指向处理函数的函数指针
  int handler_lock;		// 自旋锁,防止对处理程序的重入调用(有点像关中断的动作)
  int ticks;			// 从 sigalarm 开始,该进程移进运行了多少个 ticks
  struct trapframe *trapframe_copy;	// 保存现场的一份 copy
};
// kernel/proc.c
static struct proc*
allocproc(void)
{
  ...
  p->interval = 0;
  p->handler = 0;
  p->handler_lock = 0;
  p->ticks = 0;
  if ((p->trapframe_copy = (struct trapframe*)kalloc()) == 0) {
    freeproc(p);
    release(&p->lock);
  }
  return p;
}
static void
freeproc(struct proc *p)
{
  ...
  if (p->trapframe_copy)	// 释放现场的copy的空间
    kfree((void*)p->trapframe_copy);
}

② 每个 tick 都会产生一个中断,因此需要在处理这个中断时判断时间片是否到期,如果已经到了的话就要保存现场,并且调用处理函数(即把 PC 值改为 handler 的地址):

// kernel/trap.c
void
usertrap(void)
{
    ...
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    ++(p->ticks);
    if (p->interval != 0 && p->ticks == p->interval) {
      if (p->handler_lock == 0) {
        p->handler_lock = 1;
        copy_trapframe(p->trapframe, p->trapframe_copy);
        p->trapframe->epc = (uint64)p->handler;
      }
    }
    yield();
  }
}

其中 copy_trapframe() 是个辅助函数,只是简单拷贝所有寄存器的值,之后恢复寄存器的值时也会用到,因此抽象成了一个函数;也要在 defs.h 中声明以供外界调用:

// kernel/trap.c
void copy_trapframe(struct trapframe* from, struct trapframe* to) {
  to->kernel_satp = from->kernel_satp;
  to->kernel_sp = from->kernel_sp;
  to->kernel_trap = from->kernel_trap;
  to->epc = from->epc;
  to->kernel_hartid = from->kernel_hartid;
  to->ra = from->ra;
  to->sp = from->sp;
  to->gp = from->gp;
  to->tp = from->tp;
  to->t0 = from->t0;
  to->t1 = from->t1;
  to->t2 = from->t2;
  to->s0 = from->s0;
  to->s1 = from->s1;
  to->a0 = from->a0;
  to->a1 = from->a1;
  to->a2 = from->a2;
  to->a3 = from->a3;
  to->a4 = from->a4;
  to->a5 = from->a5;
  to->a6 = from->a6;
  to->a7 = from->a7;
  to->s2 = from->s2;
  to->s3 = from->s3;
  to->s4 = from->s4;
  to->s5 = from->s5;
  to->s6 = from->s6;
  to->s7 = from->s7;
  to->s8 = from->s8;
  to->s9 = from->s9;
  to->s10 = from->s10;
  to->s11 = from->s11;
  to->t3 = from->t3;
  to->t4 = from->t4;
  to->t5 = from->t5;
  to->t6 = from->t6;
}
// kernel/defs.h
// trap.c
void            copy_trapframe(struct trapframe*, struct trapframe*);

③ 为 sigalarm()sigreturn() 系统调用添加相关声明:

// user/user.h
int sigalarm(int, void(*)(void));
int sigreturn(void);
# user.usys.pl
entry("sigalarm");
entry("sigreturn");
// kernel/syscall.h
#define SYS_sigalarm 22
#define SYS_sigreturn 23
// kernel/syscall.c
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
static uint64 (*syscalls[])(void) = {
...
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
};

④ 实现 sigalarm()sigreturn() 系统调用

// sys_proc.c
uint64
sys_sigalarm(void) {
  int interval;
  uint64 handler;
  argint(0, &interval);
  argaddr(1, &handler);
  myproc()->interval = interval;
  myproc()->handler = (void(*)(void))handler;
  return 0;
}

uint64
sys_sigreturn(void) {
  copy_trapframe(myproc()->trapframe_copy, myproc()->trapframe);
  myproc()->ticks = 0;
  myproc()->handler_lock = 0;
  return myproc()->trapframe->a0;
}

sys_sigalarm() 从 a0 和 a1 获得用户调用系统调用时传入的 ticks 数和处理函数,并且用这些值更新 struct proc 里的成员;

sys_sigreturn() 需要恢复现场、清除刚刚设置的成员、解除对处理函数的锁;由于中断处理对用户来说应当是透明的,当 sys_sigreturn() 返回值时会更新 a0 寄存器的值,因此直接让其返回当前 a0 的值以保证不变。

⑤ 在 Makefile 中添加 alarmtest 的指令声明:

# Makefile
UPROGS=\
	...
	$U/_alarmtest\

实验结果: 通过了 alarmtestusertests -q 测试:

3_result_1
请添加图片描述请添加图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Air浩瀚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值