您现在的位置是:首页 >学无止境 >6.S081——陷阱部分(内核陷阱)——xv6源码完全解析系列(6)网站首页学无止境

6.S081——陷阱部分(内核陷阱)——xv6源码完全解析系列(6)

Zheyuan Zou 2024-09-13 12:01:06
简介6.S081——陷阱部分(内核陷阱)——xv6源码完全解析系列(6)

0.briefly speaking

点此跳转到上一篇博客

在上一篇博客中,我们通过系统调用这个重要的机制了解了Xv6操作系统中用户态陷阱的处理全流程。这篇博客则准备研究一下内核陷阱的处理流程,在研究内核陷阱流程中一个麻烦的家伙是定时器中断,首先它是一种由CLINT转发而来的本地中断,定时器中断往往会导致CPU的调度,进而将陷阱的处理流程变得错综复杂,这篇博客并不打算深入研究调度过程,这部分内容我们放在后面阅读对应源码时仔细研究。

上次我们在阅读从用户态陷阱的流程时,是以系统调用作为研究对象的,从内核态陷阱是没有系统调用这种情况的,同时因为内核代码的相对可靠性,内核出现严重错误时一般会使用panic停止内核的工作,所以从内核陷阱在Xv6中几乎就是中断(定时器中断、外部中断等)的代名词这部分逻辑将会在kerneltrap函数中看到

本篇博客将要阅读的代码:

1.kernel/trap.c
2.kernel/kernelvec.s

1.从内核空间陷阱

上一篇博客我们研究了系统调用,当时我们在阅读到usertrap函数时(kernel/trap.c:37),里面有一段这样的代码,它将stvec寄存器的值设置为kernelvec

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.
  // 译:将中断和异常的处理发送到kerneltrap中去
  // 因为我们现在处于内核态
  w_stvec((uint64)kernelvec);
  // 此后的所有代码略

这段代码将内核态陷阱处理程序kernelvec的地址放置到寄存器stvec中,这样在内核发生陷阱时PC就会从kernelvec中开始执行。和用户态进入陷阱不同,因为此时已经进入了内核态,所以有很多准备工作已经完成了,比如内核页表和内核堆栈指针等等(我们在uservec中使用汇编代码已经完成了这些寄存器的初始化)。我们下面来看看内核陷阱的具体流程:

2.kernelvec

kernelvec函数定义在kernel/kernelvec.c文件中,代码如下:

#
# interrupts and exceptions while in supervisor
# mode come here.
# push all registers, call kerneltrap(), restore, return.
# 译:在S-Mode下的中断和异常会到达这里处理
# 将所有寄存器入栈,调用kerneltrap处理,回复寄存器并返回
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
        // make room to save registers.
        // 译:为存放寄存器腾出空间
        // 堆栈都是向低地址生长的,所以将sp指针向下调整256个字节
        // 足以装下32个8字节(64)长的寄存器
        // sp指针是在trampoline.S中提前设置的,它指向内核栈的地址顶端
        addi sp, sp, -256

        // save the registers.
        // 将当前寄存器的值全部放入内核栈中保存
        sd ra, 0(sp)
        sd sp, 8(sp)
        sd gp, 16(sp)
        sd tp, 24(sp)
        sd t0, 32(sp)
        sd t1, 40(sp)
        sd t2, 48(sp)
        sd s0, 56(sp)
        sd s1, 64(sp)
        sd a0, 72(sp)
        sd a1, 80(sp)
        sd a2, 88(sp)
        sd a3, 96(sp)
        sd a4, 104(sp)
        sd a5, 112(sp)
        sd a6, 120(sp)
        sd a7, 128(sp)
        sd s2, 136(sp)
        sd s3, 144(sp)
        sd s4, 152(sp)
        sd s5, 160(sp)
        sd s6, 168(sp)
        sd s7, 176(sp)
        sd s8, 184(sp)
        sd s9, 192(sp)
        sd s10, 200(sp)
        sd s11, 208(sp)
        sd t3, 216(sp)
        sd t4, 224(sp)
        sd t5, 232(sp)
        sd t6, 240(sp)

		// call the C trap handler in trap.c
		// 译:调用trap.c中C语言编写的陷阱处理程序
        call kerneltrap

        // restore registers.
        // 恢复寄存器内容
        // 注意tp(thread pointer)的值不再恢复
        // 因为可能发生了定时器中断而导致执行当前线程的CPU发生了变化
        // 因此hartid就保留当前值即可
        ld ra, 0(sp)
        ld sp, 8(sp)
        ld gp, 16(sp)
        // not this, in case we moved CPUs: ld tp, 24(sp)
        ld t0, 32(sp)
        ld t1, 40(sp)
        ld t2, 48(sp)
        ld s0, 56(sp)
        ld s1, 64(sp)
        ld a0, 72(sp)
        ld a1, 80(sp)
        ld a2, 88(sp)
        ld a3, 96(sp)
        ld a4, 104(sp)
        ld a5, 112(sp)
        ld a6, 120(sp)
        ld a7, 128(sp)
        ld s2, 136(sp)
        ld s3, 144(sp)
        ld s4, 152(sp)
        ld s5, 160(sp)
        ld s6, 168(sp)
        ld s7, 176(sp)
        ld s8, 184(sp)
        ld s9, 192(sp)
        ld s10, 200(sp)
        ld s11, 208(sp)
        ld t3, 216(sp)
        ld t4, 224(sp)
        ld t5, 232(sp)
        ld t6, 240(sp)
		
		// 恢复栈顶指针
        addi sp, sp, 256

        // 回到内核代码中当时被中断的地方
        sret

可以看到其实kernelvec的实现逻辑相对于之前uservec来说非常简单,这是因为kernelvec相对于uservec不用处理包含切换地址空间、设置内核栈、交换sscratch寄存器等逻辑的问题,而是只需要保存寄存器,调用kerneltrap,回复寄存器并返回即可

3.kerneltrap函数

之前我们在研究系统调用的时候,usertrap函数负责在内核态中鉴别陷阱的原因并调用不同的处理函数来处理之。其实kerneltrap函数也是一样的,因为在内核态没有系统调用这一说,这里导致陷阱的原因只剩下中断和程序错误而引起的异常

所以在下面的代码中我们将会看到kerneltrap首先尝试调用devintr来尝试处理中断,如果devintr并不能识别陷阱的来源,那么说明这不是一个中断导致的陷阱,而是因为程序异常而导致的,那么就会触发一个panic并中止内核的运行。

// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
// 译:从内核代码的中断和异常会经由kernelvec到达这里
// 无论当前的内核栈是什么
void 
kerneltrap()
{
  // 保存当前CPU的一些重要的寄存器
  // 因为有可能当前处理的是一个时钟中断,进而会导致CPU的调度
  // 再次返回到此进程时,sepc,sstatus和scause寄存器可能已经面目全非了
  // 所以必须保留下来以备将来恢复
  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");
  
  // 尝试使用devintr去响应中断(包括时钟中断和外部中断)
  // devintr也相当于一个中转站,它会通过检查scause寄存器中的值
  // 确定中断的类型并加以分门别类的处理
  // 返回值表明了中断的类型,0表示没能识别中断来源,那其实也就意味着这是个异常
  // 打印出必要的debug信息后陷入panic即可
  if((which_dev = devintr()) == 0){
    printf("scause %p
", scause);
    printf("sepc=%p stval=%p
", r_sepc(), r_stval());
    panic("kerneltrap");
  }

  // give up the CPU if this is a timer interrupt.
  // 如果是一个时间中断,那么就会产生CPU的调度,当前进程放弃CPU
  // yield函数的细节在此不再展开
  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.
  // 译:yield调度CPU到另外一个进程时,可能在那个新的进程中会有陷阱发生
  // 所以为了kernelvec.S中的sret(这里注释有误?)指令所用,恢复陷阱寄存器
  w_sepc(sepc);
  w_sstatus(sstatus);
}

kerneltrap函数其实和usertrap函数很类似,只不过因为在内核态不会有系统调用异常,所以kerneltrap只需要处理外部中断和时钟中断即可,kerneltrap调用了devintr来处理中断,并在中断类型是时钟中断时使用yield()让出CPU

在kerneltrap函数完成之后,就会返回到上述的kernelvec函数中恢复寄存器并返回到内核被中断的地方继续执行下去,这就是内核陷阱的完整执行流程,不得不说它远远比用户态陷阱要简单的多。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。