Lab4_traps:中断

发布于 2024-09-05  511 次阅读


00_前言

Lab4_trap主要是熟悉xv6系统调用的时候进出kernel的细节

01_backtrace(回溯)

1.实验要求

回溯(Backtrace)通常对于调试很有用:它是一个存放于栈上用于指示错误发生位置的函数调用列表。

kernel/printf.c\中实现名为backtrace()的函数。在sys_sleep中插入一个对此函数的调用,然后运行bttest,它将会调用sys_sleep。你的输出应该如下所示:

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

bttest退出qemu后。在你的终端:地址或许会稍有不同,但如果你运行addr2line -e kernel/kernel(或riscv64-unknown-elf-addr2line -e kernel/kernel),并将上面的地址剪切粘贴如下:

$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D

你应该看到类似下面的输出:

kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85

编译器向每一个栈帧中放置一个帧指针(frame pointer)保存调用者帧指针的地址。你的backtrace应当使用这些帧指针来遍历栈,并在每个栈帧中打印保存的返回地址。

2.具体实现

(1)声明 backtrace() 函数

kernel/defs.hprintf.c区域中添加 backtrace() 函数声明

void backtrace();

这个步骤的目的是为 backtrace 函数提供声明,以便在其他文件中调用。

(2)获取当前栈帧的位置(volatile关键字)

修改 kernel/riscv.h 文件,增加 r_fp() 函数

static inline uint64 r_fp() {
    uint64 x;
    asm volatile("mv %0, s0" : "=r" (x));
    return x;
}
  • static:将函数声明为静态函数,表示这个函数只在当前的编译单元(通常是 .c 文件)内可见,不能被其他文件所访问。这避免了命名冲突。
  • inline:这是一个内联函数的声明,提示编译器在调用该函数时,直接将函数的代码嵌入到调用处,而不是生成一个函数调用。这种优化可以减少函数调用的开销(尤其是在频繁调用时),提高性能。不过,编译器并不一定总是内联,最终决定权在编译器。
  • uint64 x:声明一个64位的无符号整数变量 x,它将用于存储当前的帧指针(Frame Pointer, FP)。在RISC-V中,s0寄存器通常用于存储当前栈帧的基地址,即帧指针。
  • asm volatile("mv %0, s0" : "=r" (x)) :汇编指令 mv 将 RISC-V 架构中 s0 寄存器的值(帧指针)传送到通用寄存器 %0,并将其结果存储在变量 x 中。asm 关键字用于插入汇编代码,而 volatile 告诉编译器不要优化该指令,确保每次都执行它。"=r" 是约束条件,表示使用通用寄存器存储值,而 x 则是存储目标。在这个上下文中,这段汇编代码用于获取当前帧指针的值并返回给调用者

这个函数使用内嵌汇编读取寄存器 s0,该寄存器通常作为栈帧指针(frame pointer)。这个函数将帮助 backtrace 获取当前栈帧的位置。

(3)实现 backtrace() 函数

kernel/printf.c 中实现 backtrace() 函数

void backtrace() {
    uint64* fp = (uint64*)r_fp();
    uint64 up = PGROUNDUP((uint64)fp);
    uint64* ra;
    printf("backtrace:\n");
    while ((uint64)fp != up) {
        fp = (uint64*)((uint64)fp - 16);
        ra = (uint64*)((uint64)fp + 8);
        printf("%p\n", *ra);
        fp = (uint64*)*fp;
    }
}
  • r_fp() 函数获取当前栈帧指针 fp
  • 通过遍历栈帧,逐步回溯并打印每个栈帧中的返回地址(ra)。
  • 输出函数调用的内存地址,以便调试时可以看到函数调用的层次关系。
(4)在 sys_sleep() 中调用 backtrace()

kernel/sysproc.c 中的 sys_sleep 函数中添加对 backtrace() 的调用:

backtrace();

这个调用将在 sys_sleep() 执行时触发,打印出当前的调用栈信息。

3.结果

(1)bttrace 测试输出
  • bttrace 命令运行后,输出了函数调用栈的内存地址(即栈回溯的返回地址),依次为:

    0x00000000008002d44
    0x00000000008002ba6
    0x00000000008002890
(2)地址转换为源码行号
  • 通过 addr2line 工具将这些地址映射到源代码行号。命令格式如下:

    addr2line -e kernel/kernel 0x地址
  • 转换后的结果展示了对应的文件和行号:

    • trap.c:76
    • syscall.c:140
    • sysproc.c:74

这说明 backtrace 成功打印出了从 sys_sleep() 开始的函数调用链,并且通过地址映射工具定位到了具体的源码文件及对应行号。

QQ_1725520395738

02_Alarm-test0():invoke handler(调用处理程序)

1.实验要求

这个实验的目的是在 xv6 操作系统中添加一个定时器报警功能,即在程序运行时,定期触发用户定义的处理函数。实验的核心是通过为 xv6 添加 sigalarm 系统调用,让操作系统能够在每经过一定数量的 CPU 时间片(ticks)后,自动调用一个用户定义的函数(handler),实现定时执行的功能。

在本实验中:

  • xv6 实现一个 sigalarm 系统调用,让用户可以指定一个处理函数(handler),比如 fn
  • 这个处理函数会在指定的时间片(ticks)之后被内核调用,也就是触发时调用处理程序(invoke handler)。
  • 在系统中,这种处理函数可以用来处理定时事件,类似于定时器到达时发出的警报。

2.具体实现

(1)添加 UPROGS字段
  • 在 Makefile 中,确保 alarmtest.c 文件被编译为用户程序。在 UPROGS 中添加 alarmtest,如图中所示:

    $U/_alarmtest\
(2) 添加系统调用条目
  • user/usys.pl 是生成用户系统调用文件 usys.S 的脚本。你需要在这个文件中添加 sigalarmsigreturn 的条目。

    entry("sigalarm");
    entry("sigreturn");
(3)声明用户系统调用接口
  • user/user.h 中声明系统调用 sigalarmsigreturn,方便用户程序调用:

    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
  • sigalarmsigreturn 系统调用是用户程序调用的系统调用接口

(4)分配用户系统调用编号
  • kernel/syscall.h 中为 sigalarmsigreturn 分配系统调用编号:

    #define SYS_sigalarm 22
    #define SYS_sigreturn 23
(5)添加内核系统调用实现
  • kernel/syscall.c 中添加 sys_sigalarmsys_sigreturn 系统调用的实现:

    extern uint64 sys_sigalarm(void);
    extern uint64 sys_sigreturn(void);
  • 确保系统调用表中添加了这两个函数:

    [SYS_sigalarm] = sys_sigalarm,
    [SYS_sigreturn] = sys_sigreturn,
  • 系统调用号和系统调用表是为了串联起来用户系统调用和内核系统调用

(6)添加用于存储定时器间隔处理函数指针的字段
  • kernel/proc.h 中的 struct proc 结构体中添加与 sigalarm 相关的字段,这些字段用于存储定时器间隔处理函数指针

    uint64 interval;           // 定时器间隔
    void (*handler)();   // 定时器触发的处理函数
    uint64 spend;                // 已经过了多少个ticks
    • uint64 interval:表示报警间隔时间,即经过多少个时钟周期(ticks)后,触发报警。这个值由 sigalarm 系统调用设置,决定每隔多长时间触发一次用户定义的处理函数。

    • void (*handler)():这是一个指向报警处理函数的函数指针。当定时器触发报警时,内核会调用这个处理函数。用户通过 sigalarm 系统调用传递处理函数的地址,内核则在时间到时执行该函数。

    • uint64 spend:用于跟踪自上次报警后经过的时间(以时钟周期为单位)。内核使用这个值来判断是否已经到达设定的报警间隔。如果经过的 spend 值达到了 interval,就会触发报警并执行处理函数。

(7)实现 sys_sigalarmsys_sigreturn系统调用
  • kernel/sysproc.c 中实现 sigalarm 系统调用,存储用户传递的定时器间隔和处理函数:

    uint64
    sys_sigalarm(void) {
      struct proc* myProc = myproc();
      int n;
      uint64 handler;
    
      if (argint(0, &n) < 0)
          return -1;
      myProc->interval = n;
    
      if (argaddr(1, &handler) < 0)
          return -1;
    
      myProc->handler = (void (*)())handler;
      return 0;
    }
    
    uint64
    sys_sigreturn(void) {
      return 0;
    }
  • sys_sigalarm 实现思路

    • sys_sigalarm 通过获取用户传递的时间间隔 n 和处理函数 handler,将它们分别存储在当前进程的 intervalhandler 字段中。这样,内核能够在定时器到期时调用用户定义的处理函数,实现定期报警功能。
  • sys_sigreturn 实现思路

    • sys_sigreturn 用于在信号处理函数执行结束后恢复正常程序执行。当前实现简单返回 0,标志处理完成。
(8)在 kernel/trap.c 中添加处理逻辑
  • usertrap() 中,当时钟设备中断(which_dev == 2)发生时,检查是否需要触发定时器:

    if (which_dev == 2) {
      p->spend = p->spend + 1;
      if (p->spend >= p->interval) {
          p->spend = 0;
          p->trapframe->epc = (uint64)p->handler;
      }
    }

QQ_1725525765934

3.结果

test0_result

03_Alarm-test1/test2(): resume interrupted code

1.实验要求

该实验的主要目的是确保在定时器中断处理程序(alarm handler)执行完毕后,用户程序可以从被中断的地方恢复执行,并且处理程序的执行不会影响用户程序的正常运行。

实验将通过多个测试(test0test1test2)验证用户程序是否能在处理完定时器信号后正常恢复,以及是否可以避免重复调用 handler

2.具体实现

(1)添加寄存器状态字段
  • 修改 kernel/proc.h

    uint64 interval;         // 定时器的时间间隔
    void (*handler)();       // 定时器处理函数的指针
    uint64 spend;            // 已经过的时间计数
    //本次步骤添加
    struct trapframe *trapframeSave;  // 保存被中断时的寄存器和状态
    int waitReturn;          // 是否正在等待定时器处理函数返回
  • trapframeSave 用来保存进程的寄存器状态,便于在定时器触发和处理完毕后恢复原有状态。

  • waitReturn 是一个标志位,用于防止在处理函数还未返回时,定时器重复调用 handler

(2)分配和释放寄存器状态

修改 kernel/proc.c:分配和释放 trapframeSave

  • allocproc() 中分配 trapframeSave

    if ((p->trapframeSave = (struct trapframe *)kalloc()) == 0) {
      release(&p->lock);
      return 0;
    }
  • freeproc() 中释放 trapframeSave

    if (p->trapframeSave)
      kfree((void*)p->trapframeSave);
    p->trapframeSave = 0;
(3)实现保存和恢复进程状态的函数
  • 修改:kernel/trap.c(trap:陷阱)

  • switchTrapFrame() 函数:保存和恢复进程状态的函数

    void switchTrapFrame(struct trapframe *trapframe, struct trapframe *trapframeSave) {
      trapframe->kernel_satp = trapframeSave->kernel_satp;
      trapframe->kernel_sp = trapframeSave->kernel_sp;
      trapframe->kernel_hartid = trapframeSave->kernel_hartid;
      trapframe->epc = trapframeSave->epc;
      trapframe->ra = trapframeSave->ra;
      trapframe->sp = trapframeSave->sp;
      trapframe->gp = trapframeSave->gp;
      trapframe->tp = trapframeSave->tp;
      trapframe->t0 = trapframeSave->t0;
      trapframe->t1 = trapframeSave->t1;
      trapframe->t2 = trapframeSave->t2;
      trapframe->s0 = trapframeSave->s0;
      trapframe->s1 = trapframeSave->s1;
      trapframe->a0 = trapframeSave->a0;
      trapframe->a1 = trapframeSave->a1;
      trapframe->a2 = trapframeSave->a2;
      trapframe->a3 = trapframeSave->a3;
      trapframe->a4 = trapframeSave->a4;
      trapframe->a5 = trapframeSave->a5;
      trapframe->a6 = trapframeSave->a6;
      trapframe->a7 = trapframeSave->a7;
      trapframe->s2 = trapframeSave->s2;
      trapframe->s3 = trapframeSave->s3;
      trapframe->s4 = trapframeSave->s4;
      trapframe->s5 = trapframeSave->s5;
      trapframe->s6 = trapframeSave->s6;
      trapframe->s7 = trapframeSave->s7;
      trapframe->s8 = trapframeSave->s8;
      trapframe->s9 = trapframeSave->s9;
      trapframe->s10 = trapframeSave->s10;
      trapframe->s11 = trapframeSave->s11;
      trapframe->t3 = trapframeSave->t3;
      trapframe->t4 = trapframeSave->t4;
      trapframe->t5 = trapframeSave->t5;
      trapframe->t6 = trapframeSave->t6;
    }
  • 修改usertrap()函数:

    从:

      if (which_dev == 2) {
          p->spend = p->spend + 1;
          if (p->spend >= p->interval) {
              p->spend = 0;
              p->trapframe->epc = (uint64)p->handler;
          }
      }

    变为

    if (which_dev == 2 && p->waitReturn == 0) {
      if (p->interval != 0) {
          p->spend = p->spend + 1;
          if (p->spend >= p->interval) {
              switchTrapFrame(p->trapframeSave, p->trapframe);
              p->spend = 0;
              p->trapframe->epc = (uint64)p->handler;
              p->waitReturn = 1;
          }
      }
    }
    

    主要变化是在调用hander前加了一个保存寄存器状态的函数switchTrapFrame

(4)声明保存和恢复进程状态的函数

修改:kernel/defs.h

  • struct trapframe:在 defs.h 中声明 struct trapframe,意味着该结构体在其他文件中可以被使用。

    struct trapframe;
  • 声明 switchTrapFrame 函数:该声明让 switchTrapFrame 函数可以在多个文件中被调用,允许其他文件使用该函数来在两个 trapframe 之间进行切换或复制。

    void switchTrapFrame(struct trapframe*, struct trapframe*);
(5)实现复原定时器状态的系统调用
  • 修改 kernel/sysproc.c:在原有的sigreturn 系统调用上添加了对寄存器状态复原的实现

    uint64 sys_sigreturn(void) {
      struct proc* myProc = myproc();
      switchTrapFrame(myProc->trapframe, myProc->trapframeSave);
      myProc->waitReturn = 0;
      return 0;
    }
    • sigreturn 是当用户程序的定时器处理函数执行完毕后,通过系统调用返回到原来的状态。

    • switchTrapFrame 在这里被用来从 trapframeSave 中恢复保存的寄存器状态,确保程序能够从被中断的地方继续执行。

    • waitReturn = 0:在处理完定时器后,允许定时器下一次触发,避免重复调用 handler

3.结果

test12_result


踏上取经路,比抵达灵山更重要