xv6内核源码解析trap.c

基于实验4,我们先熟悉一下kernel/trap.c中的部分代码

可能需要使用到的文件:

  • /kernel
    • trap.c (important)
    • plic.c
    • memlayout.h
    • kernel.asm (这个需要编译之后才有,可能会用到,这个整个kernel编出来的汇编)
    • kalloc.c
    • vm.c
    • syscall.h
    • kernelvec.S
    • riscv.h (important)
    • trampoline.S (important)

这是地址:lab_analysis/lab4/4.1 · Prim./mit6.S086 - 码云 - 开源中国 (gitee.com)

为了方便我把整个实验代码都搬过去了,在windows下来看会方便一点,主要是招函数比较方便。要是觉得麻烦直接看博客也行,不过我觉得对着来看效果会好一点

ok 我们直接开始

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "defs.h"

// 一个自旋锁
struct spinlock tickslock;
// 时钟滴答
uint ticks;

// 这些其实都可以理解为一个函数指针,在 trampoline.S中
// trampoline 
// uservec      内部调用userret
// userret      返回用户态的最后一步
extern char trampoline[], uservec[], userret[];

// in kernelvec.S, calls kerneltrap().
// 先将当前cpu中进程的上下文进行保存
// 调用kerneltrap
// 加载内核的上下文
void kernelvec();

// 处理硬件中断
extern int devintr();

void
trapinit(void)
{
  initlock(&tickslock, "time");
}

// set up to take exceptions and traps while in the kernel.
void
trapinithart(void)
{
  // stvec寄存器:内核会将中断处理程序的位置写在这里
  // 然后RISC-V硬件就会跳到相应指令的地址执行对应的处理程序。
  // 这里将kernelvec函数的位置写到stvec中,进行上下文切换
  w_stvec((uint64)kernelvec);
}

//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
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.
  // 将中断处理程序加载stvec中,以便后续RISC-V硬件处理
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  // spec()寄存器:当一个陷入出现时,RISC-V会将程序计算器保存在这里
  // 因为pc等一下将会被stvec改写。sret指令(从陷入中返回)会将sepc中的值
  // 复制到pc中
  // 内核能够将向spec中写入数据以便控制sret跳转的位置

  // save user program counter.
  // 这个trapframe page和trampoline page是所有进程共享的,映射在进程地址空间的高地址
  p->trapframe->epc = r_sepc();
  
  // 开始判断中断的类型
  if(r_scause() == 8){
    // system call

    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    // ecall是用户态陷入内核态需要执行的指令,
    // 也就是说ecall是一个介于用户态和内核态之间的指令,所以
    // 当进行上下文切换的时候,ecall是前一个上下文最后执行的指令
    // 也是后一个上下文开始执行的第一条指令
    // 注意:这里的上下文件仅仅限于用户态陷入内核态,进程调度的我们另做讨论
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    // 打开硬件中断
    intr_on();
    //  // enable device interrupts
    //  static inline void
    //   intr_on()
    //  {
    //    w_sstatus(r_sstatus() | SSTATUS_SIE);
    //  }

    // 执行对应的系统调用
    syscall();
  } else if((which_dev = devintr()) != 0){
    // 硬件中断,这里应该是需要我们来补充
    // ok
  } else {
    // 当不是硬件中断也不是系统调用的时候,只能是异常了
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  // 时间中断是由一个时钟硬件产生的,通电会发出固定频率的信号,
  // 我们称之为时钟滴答 tick
  // 但是时钟中断又不同于硬件中断,这个我们后期会讲到
  if(which_dev == 2)
    yield();  // 所以这个yield()我们也先跳过 ^_^

  // 执行结束在返回用户态之前,需要恢复之前用户进程的上下文
  // 这个往下面看
  usertrapret();
}

//
// return to user space
//
void
usertrapret(void)
{
  struct proc *p = myproc();

  // we're about to switch the destination of traps from
  // kerneltrap() to usertrap(), so turn off interrupts until
  // we're back in user space, where usertrap() is correct.
  // 注释的意思:我们将要切换到陷入的终点,
  // 从kerneltrap到usertrap,所以需要关闭中断直到我们真正返回用户空间
  
  // 这里怎么理解呢?
  // 我是这么理解的,如果在进行切换的过程中(将进程的寄存器状态从
  // tramframe page 中恢复到cpu寄存器内的这个过程),发生了中断
  // 这时候,当前cpu一部分是属于内核的,一部分是属于用户态进程的,
  // 这个我记得小书上又讲,大家可以去翻一下(好吧,书上貌似也没讲)

  // 与上面的intr_on()相反这里是禁止中断
  intr_off();

  // send syscalls, interrupts, and exceptions to trampoline.S
  // 设置中断向量表的操作:
        // (uservec - trampoline) 是指uservec相对trampoline的偏移
        // 因为程序的指令都是存放在指令区,与程序运行时的数据是分隔开来的
        // 而TRAMPOLINE是一个进程中trampoline的起始地址(虚拟/物理,因为是直接映射的)
        // 所以 (TRAMPOLINE +(uservec - trampoline)) 计算得出来的是
        // uservec 的虚拟地址,也就是保存在共享trampoline page 中的地址
  w_stvec(TRAMPOLINE + (uservec - trampoline));
        // 内核页表设置trampoline page
        //  vm.c/kvminit()
        //  kvmmap(TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
        // 用户页表设置trampoline page 和 tramframe page
        //  proc.c/proc_pagetable(struct proc *p)
        //  if(mappages(pagetable, TRAMPOLINE, PGSIZE,
        //            (uint64)trampoline, PTE_R | PTE_X) < 0){
        //  if(mappages(pagetable, TRAPFRAME, PGSIZE,
        //            (uint64)(p->trapframe), PTE_R | PTE_W) < 0){

  // set up trapframe values that uservec will need when
  // the process next re-enters the kernel.
  // 为下一次陷入做准备,小书上有写
  // stap寄存器的作用?
  // stap寄存器主要是给MMU使用,stap寄存器保存了页表的基地址,
  // MMU通过stap可以找到第一级页表,进而找到物理地址。
  // MMU memory manage util 内存管理单元
  p->trapframe->kernel_satp = r_satp();         // kernel page table
  p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
  // 指向最开始的trap handler
  p->trapframe->kernel_trap = (uint64)usertrap;
  // cpu的序号
  p->trapframe->kernel_hartid = r_tp();         // hartid for cpuid()

  // set up the registers that trampoline.S's sret will use
  // to get to user space.
  
  // set S Previous Privilege mode to User.
  // 恢复陷入前的cpu模式并打开中断
  unsigned long x = r_sstatus();
  x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
  x |= SSTATUS_SPIE; // enable interrupts in user mode
  w_sstatus(x);

  // set S Exception Program Counter to the saved user pc.
  w_sepc(p->trapframe->epc);

  // tell trampoline.S the user page table to switch to.
  // 暂时还看不懂这个位运算的意思,不过
  // 大概意思就是算出了这个页表的物理地址,从下面就能够看出来
  uint64 satp = MAKE_SATP(p->pagetable);
  // #define SATP_SV39 (8L << 60)
  // #define MAKE_SATP(pagetable) (SATP_SV39 | (((uint64)pagetable) >> 12))


  // jump to trampoline.S at the top of memory, which 
  // switches to the user page table, restores user registers,
  // and switches to user mode with sret.
  // 这个和上面的uservec的原理一样
  // 不过上面的指令是store,这个函数的指令是load,将原先的
  // 上下文加载会cpu中,跟陷入的是差不多的,很好理解
  uint64 fn = TRAMPOLINE + (userret - trampoline);
  ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}

// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
void 
kerneltrap()
{
  // 先将寄存器的状态保存以下,因为可能会发生时钟中断
  // 时钟中断好像是要将它抽象为软件中断才能进行禁止
  // 这个我们后面的实验再说
  int which_dev = 0;
  uint64 sepc = r_sepc();
  uint64 sstatus = r_sstatus();
  uint64 scause = r_scause();
  
  // 前置判断,特权级检查和中断开关检查
  if((sstatus & SSTATUS_SPP) == 0)
    panic("kerneltrap: not from supervisor mode");
  if(intr_get() != 0)
    panic("kerneltrap: interrupts enabled");

  if((which_dev = devintr()) == 0){
    // 异常
    printf("scause %p\n", scause);
    printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
    panic("kerneltrap");
  }

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
    yield();

  // the yield() may have caused some traps to occur,
  // so restore trap registers for use by kernelvec.S's sepc instruction.
  w_sepc(sepc);
  w_sstatus(sstatus);
}

/**
 * 
 * 现在我们不能看出,从用户态中断和从内核态中断的差别
 * 简直就是天差地别,
 * 因为从用户态陷入,需要进行上下文切换,这个动作
 * 而陷入本就是在内核中处理,而且系统调用又不会从
 * 内核中引发,所以相对而言会简单许多
 * 
*/

// 时钟中断的东西,现在简单看看就好,
// 循序渐进
void
clockintr()
{
  acquire(&tickslock);
  ticks++;
  wakeup(&ticks);
  release(&tickslock);
}

// 最后一个硬件中断,搞完就下班,真顶不住了

// check if it's an external interrupt or software interrupt,
// and handle it.
// returns 2 if timer interrupt,
// 1 if other device,
// 0 if not recognized.
int
devintr()
{
  // 简单不解释
  uint64 scause = r_scause();

  if((scause & 0x8000000000000000L) &&
     (scause & 0xff) == 9){
    // this is a supervisor external interrupt, via PLIC.

    // irq indicates which device interrupted.
    // plic -> platform level interrupt controller
    // 平台级中断控制器
    // 从中断控制器中获知当前产生中断的设备是什么
    // 需要调用什么样的中断处理程序
    int irq = plic_claim();   // plic.c
    // memlayout.h
    // #define PLIC_SCLAIM(hart) (PLIC + 0x201004 + (hart)*0x2000)
    // plic.c
    // ask the PLIC what interrupt we should serve.
    //  int
    //  plic_claim(void)
    //  {
    //    int hart = cpuid();
    //    int irq = *(uint32*)PLIC_SCLAIM(hart);
    //    return irq;
    //  }

    // 根据不同的中断调用不同的中断处理程序
    if(irq == UART0_IRQ){ 
      uartintr();   // uart 通用非同步收发传输器
    } else if(irq == VIRTIO0_IRQ){
      virtio_disk_intr(); // 这个应该是磁盘设备
    } else if(irq){
      printf("unexpected interrupt irq=%d\n", irq); // 未能识别,没有注册的设备
    }

    // the PLIC allows each device to raise at most one
    // interrupt at a time; tell the PLIC the device is
    // now allowed to interrupt again.
    if(irq)
      plic_complete(irq);

    return 1;
  } else if(scause == 0x8000000000000001L){
    // 软件中断,有关时钟中断我们之后还会再讲到
    // software interrupt from a machine-mode timer interrupt,
    // forwarded by timervec in kernelvec.S.

    if(cpuid() == 0){
      clockintr();
    }
    
    // acknowledge the software interrupt by clearing
    // the SSIP bit in sip.
    w_sip(r_sip() & ~2);

    return 2;
  } else {
    return 0;
  }
}


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
"Runtime error" 是指在程序运行过程中发生了错误,而 "Signal 6: aborted" 和 "IOT trap" 是指操作系统向程序发送了信号,示意程序异常终止。 当程序出现 "Runtime error",往往有以下几个可能的原因: 1. 内存错误:程序访问了未被分配的内存、越界访问了数组或指针等情况。这可能会导致操作系统向程序发送 "Signal 6: aborted" 信号,强制终止程序运行,以避免内存破坏和数据损坏。 2. 数据错误:程序在运行过程中,数据输入或输出出现了异常,例如输入了无效的数据或输出数据超出了预期范围。这种情况下,操作系统可能向程序发送 "IOT trap" 信号,通知程序数据错误并终止其运行。 3. 代码逻辑错误:程序的逻辑错误导致运行时错误,例如除以零、死循环等。这种错误可能会导致程序崩溃或出现未定义的行为。 对于解决这类问题,我们可以采取以下几个步骤: 1. 检查程序代码,特别是与内存操作(如指针、数组等)相关的部分,确保没有越界访问或未初始化的变量。 2. 检查程序的输入和输出数据,确保它们符合预期的范围和类型,避免出现异常情况。 3. 使用调试器工具,对程序进行逐步运行并观察每一步的结果,以确定错误发生的具体位置。 4. 阅读相关的系统日志和错误信息,了解操作系统发送信号的原因。处理信号可能需要特定的操作和设置。 最后,需要注意的是,解决 "Runtime error" 的问题可能需要一定的编程经验和调试能力,如果无法解决,可以考虑寻求更高水平的技术支持。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值