iT邦幫忙

1

[6.1810][code] xv6 的 traps (二)

  • 分享至 

  • xImage
  •  

系列文章 : [6.1810] 跟著 MIT 6.1810 學習基礎作業系統觀念

大綱

  • kernel/kernelvec.S/kernelvec
  • kernel/trap.c/kerneltrap
  • ch4.5 Traps from kernel space
  • kernel/trap.c/clockintr
  • kernel/trap.c/devintr

kernel/kernelvec.S/kernelvec

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/kernelvec.S#L12 }

       #
        # interrupts and exceptions while in supervisor
        # mode come here.
        #
        # the current stack is a kernel stack.
        # push registers, call kerneltrap().
        # when kerneltrap() returns, restore registers, return.
        #
kernelvec:

當我們會進來這裡,表示我們在 kernel ( S-mode ) 遇到了 interrupt 或是 exception 了!

因為原本就是在 S-mode,所以當前的 stack 已經是該 process 的 kernel stack。不同於 user trap 的處理,kernel trap 就不需要再進行 stack 的切換,也不需要進行 page table 的切換。

這邊要做的事情相對單純

  • 把發生 trap 時的狀態 ( e.g. registers ) 放進 stack
  • 呼叫 kerneltrap
  • 從 kerneltrap 返回後,把 stack 還原
  • 用 sret 返回中斷前的工作


        # make room to save registers.
        addi sp, sp, -256

這邊替 stack 空出 256 bytes 空位。



        # save caller-saved 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 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 t3, 216(sp)
        sd t4, 224(sp)
        sd t5, 232(sp)
        sd t6, 240(sp)

把 caller-save 的 registers 放進 stack 裡面。
我們不需要把 sp 給放進去,因為我們知道目前的 sp 值加上 256 就是原來的 sp ( stack pointer ) 值了。



        # call the C trap handler in trap.c
        call kerneltrap

呼叫 kernel/trap.c/kerneltrap
在 kerneltrap 裡面,這個 function 會負責儲存 callee-saved registers ( 在這裡,callee 就是 kerneltrap 這個 function。)

並在離開 kerneltrap 這個 function 的時候,它也該負責把 callee-saved registers 恢復回來。

因此,在 kernelvec 才沒有特別去儲存 callee-saved registers 的資訊。



        # restore registers.
        ld ra, 0(sp)
        # ld sp, 8(sp)
        ld gp, 16(sp)
        # not tp (contains hartid), in case we moved CPUs
        ld t0, 32(sp)
        ld t1, 40(sp)
        ld t2, 48(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 t3, 216(sp)
        ld t4, 224(sp)
        ld t5, 232(sp)
        ld t6, 240(sp)

執行完 kerneltrap function,開始準備回到 trap 發生前的執行流程。
這邊會把存在 stack 內的 registers 倒回 CPU 上。
要注意到

  • sp 不需要 load 進 CPU,因為等等就會 addi sp, sp, 256,把 sp 恢復原狀了。
  • tp 不需要 load 進 CPU
    • 在 kernel space 裡面,tp 本來就是指 hartid 的值了
    • 假如當我們在 kernelvec 後,這個 process 被換到其他 CPU 上執行,那 stack 上的 tp 值就是錯誤的,舊的值,不該被使用!


        addi sp, sp, 256

把 stack pointer 恢復原狀。



        # return to whatever we were doing in the kernel.
        sret

用 sret 跳回 trap 發生前的執行流程。



kernel/trap.c/kerneltrap

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trap.c#L136 }

// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
void 
kerneltrap()

會被 kernelvec 呼叫,開始處理 interrupt 以及 exception。



  int which_dev = 0;
  uint64 sepc = r_sepc();
  uint64 sstatus = r_sstatus();
  uint64 scause = r_scause();
  • which_dev : 這次發出 trap 的裝置類型
    • 0 : 不知道是什麼類型的中斷,可能是 exception
    • 1 : 已知的硬體設備 ( e.g. uart )
    • 2 : timer interrupt
  • sepc
    • 發生 trap 前的 pc
  • sstatus
    • sstatus.SPP : 發生 trap 前的特權階級
    • sstatus.SPIE : 發生 trap 是否有 enable interrupt
  • scause
    • 發生 exception or interrupt 的原因

這邊要先把 sepc, sstatus, 跟 scause 放進這個 process 的 kernel stack

sepc, 跟 sstatus 存起來,是因為可能會在 kernel space 做 context switch ( yield,放棄 CPU 的使用權,並讓其他 process 有被執行的機會 ),假如不存進 stack 裡面,則可能會用到其他 process 留下來的舊值。



  if((sstatus & SSTATUS_SPP) == 0)
    panic("kerneltrap: not from supervisor mode");

假如在 trap 前的特權階級是 user mode ( U-mode ),則不正常,發出 panic。
這裡可是為了 S-mode 所做的 kernelvec 呀!



  if(intr_get() != 0)
    panic("kerneltrap: interrupts enabled");

當 RISC-V 的硬體發生 trap 的時候,硬體會自動關閉中斷 ( 將 sstatus.SIE 清成 0 ),以便防止發生中斷後,又馬上被中斷 的情況。

這裡確認一下中斷是否被關閉了,假如沒關的話,那就發出 panic。



  if((which_dev = devintr()) == 0){
    // interrupt or trap from an unknown source
    printf("scause=0x%lx sepc=0x%lx stval=0x%lx\n", scause, r_sepc(), r_stval());
    panic("kerneltrap");
  }

假如 devintr 回傳 0 的話,表示出現了無法處理,無法被識別的中斷,於是發出 panic。



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

假如遇到的是 timer 中斷,則呼叫 yield 來讓當前的 process 放棄 CPU,讓其他 process 有機會起來執行。



  // 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);
}

準備回到 trap 發生前的狀態,把 stack 上的 sepcsstatus 存回 CPU。



ch4.5 Traps from kernel space

xv6-riscv 這個作業系統,在處理來自 U-mode 的 trap,以及來自 S-mode 的 trap 的時候,會是不同的方式。

當進入 kernel ( S-mode ) 的時候,stvec 會指向 kernelvec

因為會進入 kernelvec 的條件之一,就是在發生 trap 之前,就已經是在 S-mode 了。所以我們不需要像 user trap 一樣切換 page table ( 我們早就是用 kernel root page table 了! ),也不需要切換 stack pointer ( 我們已經正在使用 kernel stack pointer 了! )。

所以不像是 user trap 那樣,需要仰賴 TRAPFRAME 來儲存 trap 發生前的 CPU 狀態,在 kernelvec 的情況,我們只需要把發生 trap 前 CPU 狀態存放在 kernel stack 上就可以了。並且在 kernelvec 想返回到 trap 發生前的執行流程,也只需要從 stack 上把資訊塞回 CPU 也就可以了。

kernelvec 會把資訊放在當前 process 的 kernel stack 上。當發生 context switch,CPU 執行另外一個 process 的情況,還是會切換 stack pointer。而發生 trap 時的資訊,會安安穩穩地放在原本 process 的 kernel stack。

kernelvec 只會處理三種 trap 的其中一種 : 中斷 ( interrupt )。

  • syscall : 不會在 kernel ( S-mode ) 去使用 ecall。
  • exception : 在 kernel ( S-mode ) 出現 exception 的話,不像 user trap 可以把 user process 結束掉 ( kill ),只能把整個 kernel 凍結住 ( 發出 panic )。

假如 kerneltrap 的起因是 timer interrupt,且目前執行是在 process 的 kernel space ( 而不是 scheduler thread ),kerneltrap 會呼叫 yield 來放棄 CPU,讓其他 process 有機會執行。

在未來的某個時機點,某個 process 也會呼叫 yield,並讓原本這個 process 回來繼續執行。

當 kerneltap 該做的事情已經做完之後,它需要回到被 trap 的地方開始執行。
因為 yield 會修改到 sepc 以及 sstatus,所以在 yield 之前,我們需要把這兩個值存在當前 process 的 kernel stack 裡面。

當要從 trap 回到原本的執行流時,需要從該 process 的 kernel stack 將 sepc 以及 sstatus 載回到 CPU。

在 kernelvec 的尾聲,會把 caller-saved register 載回到 CPU,並呼叫 sret 回到 trap 發生前的狀態。



kernel/trap.c/clockintr

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trap.c#L164C1-L178C2 }

void
clockintr()

當遇到 timer interrupt,就進到這裡進行處理。



  if(cpuid() == 0){
    acquire(&tickslock);
    ticks++;
    wakeup(&ticks);
    release(&tickslock);
  }

這邊可以看到,只有 cpuid == 0 ( 也就是 hartid == 0 ) 的時候,才會進入這裡。

Q: 那這邊為什麼還需要用 acquirerelease 呢 ? 只有一個 CPU 可以進來這個部分。

每當 cpuid == 0 的 CPU 發生一次 timer interrupt,就會把 ticks++。
ticks 會讓 xv6-riscv 作業系統記錄自開機以來,發生了多少次的 timer interrupt ( 經歷了多少個 tick ),讓 xv6-riscv 可以判定流逝了多少的時間。

wakeup 會把所有等待時間流逝,正在沉睡著的 process 給喚醒。



  // ask for the next timer interrupt. this also clears
  // the interrupt request. 1000000 is about a tenth
  // of a second.
  w_stimecmp(r_time() + 1000000);
}

這一行是在設一個鬧鐘,表示 CPU 希望下一發 timer interrupt 是在 1000000 個 hardware cycles 之後發生。

要注意到

  • time 有可能會是多個 hart 共用的,雖然每個 hart 都有自己的 time CSR,但在硬體上可能會指向相同的來源。
  • stimecmp 是每一個 hart 自己有一份。


kernel/trap.c/devintr

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trap.c#L186 }

// 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()

識別一個外部中斷需要由哪一個 device driver 來處理。

  • 假如遇到 timer interrupt : return 2
  • 假如遇到 其他外部中斷 ( e.g. UART, virtio_disk ) : return 1
  • 假如無法識別,沒辦法處理, return 0


  if(scause == 0x8000000000000009L){
    // this is a supervisor external interrupt, via PLIC.
  • 當 Most Significant Bit (MSB) 為 1 的時候,表示發生中斷 ( interrupt )
  • 當 Most Significant Bit (MSB) 為 0 的時候,表示發生例外 ( exception )

因為這裡的 scause 的 MSB 為 1,且 exception code == 9,表示這是一個 Supervisor external interrupt

對 xv6-riscv 預設的 virtual platform 而前,這表示有訊號從 PLIC 傳過來了。
假如是其他平台,設計者可以依照自己的需求在 CPU 的第九個 interrupt 上接上自己想要街上的 device,只是在 xv6-riscv 所預想的平台上,這跟訊號會來自 PLIC。

所以接下來,需要 PLIC 的 driver 來進行處理了。



    // irq indicates which device interrupted.
    int irq = plic_claim();

以 xv6-riscv 所預設的平台 ( 此為 QEMU 的 virt machine ),PLIC 會接上 UART, virtio_disk

UART 或是 virtio_disk 想要通知 CPU,讓 CPU 發生 interrupt ( trap 的其中一種 ) 的時候,就會發訊號給 PLIC,然後 PLIC 再發訊號給 CPU,讓 CPU 發生第 9 的 supervisor external interrupt

  • CPU 會去問 PLIC 說,是哪個傢伙發中斷給你呀 ? ( CPU 發出 plic_claim )
  • PLIC 會回說,是 UART! 或是 virtio_disk

plic_claim 這個 function 的 return value 可以讓我們知道是 UART, virtio_disk,或是不知道是誰 ( unexpected interrupt ) 發出的中斷。



    if(irq == UART0_IRQ){
      uartintr();
    } else if(irq == VIRTIO0_IRQ){
      virtio_disk_intr();
    } else if(irq){
      printf("unexpected interrupt irq=%d\n", irq);
    }

在這裡會去透過 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;

假如 irq 指向能被識別出來的 device,就跟 PLIC 說,我們已經處理完這個中斷了 ( plic_complete ),並 return 1,表示這個中斷有被識別並處理。



  } else if(scause == 0x8000000000000005L){
    // timer interrupt.
    clockintr();
    return 2;

假如遇到 timer 中斷,就進入 clockintr 處理 timer 中斷,並回傳 2,告訴 caller 說,我們遇到了一個 timer 中斷。



  } else {
    return 0;
  }
}

假如中斷不知道是哪個 device 發出來的,就回傳 0,告知 caller 說,我們沒有去處理這個中斷。



Reference


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言