iT邦幫忙

2022 iThome 鐵人賽

DAY 15
0

Interrupt

中斷 (Interrupt) 可以用來改變 CPU 執行程式的流程,當 CPU 在執行一個程式時,可以通過 Interrupt 讓 CPU 跑去執行其他的程式,而在完成其他程式的任務時,就會 return 到 CPU 先前執行的程式。

硬體和軟體都可以產生出 Interrupt。

  • 硬體產生的 Interrupt 稱為 signal
  • 軟體產生的 Interrupt 稱為 trap

在 RISC-V 中,exception 跟 Interrupt 都算是一種 trap。(不同於有些地方將 trap 以及 Interrupt 作為 Exception 的子集合)

Trap

Trap 由 user space 中的 Process 通過 Exception 所觸發,為一種同步中斷 (synchronous interrupt),trap 發生時會從 user mode 進入到 supervisor mode (進入到 kernel) 中,而在 kernel 做完一些 System call 等等操作之後再回到 user mode。

同步中斷 (synchronous interrupt)
這裡的同步中斷指的是如果一個 process 發生了 Exception,導致 trap,該 process 會先被暫停,而當 System call 完成之後,接著才會回到 process 讓他繼續執行,這稱為同步中斷。

  • 例外情況 (Exception) :
    • 程式執行了 System call (ecall 指令會導致 Exception)。
    • 程式發生了 page fault,或是除以 0 的錯誤,program Error(非法指令 Illegal Instruction 使用, Alignment Error)
  • 中斷情況 (Interrupt) :
    • 某一個 I/O 裝置觸發了中斷 (Interrupt),Interrupt 會發生在 user mode 或是 supervisor mode 底下,當 Interrupt 發生時,我們需要對 Interrupt 進行處理,而處理 Interrupt 的程式稱為 Handler,Handler 會在 supervisor mode 底下執行。

Trap 涉及了許多細節,而這些細節對於作業系統的隔離性以及性能有十分重要的引響,許多應用程式,會因為頻繁的 System call,或是 page fault 會不斷的觸發 Trap (Trap 在 xv6 中可以解釋成處理 Interrupt 的流程,概念上類似於作業系統概論中的 ISR (Interrupt Service Routine))。

在最一開始,講到 printf() 的例子時,有提及 printf() 會使用到 write 的 System call,而過程中便會有 Traps 的產生。讓我們從 user mode 切換到 supervisor mode 中。

下面這張圖很好的表示 trap 的概念

source

Trap 相關的 CSRs

以下列出與 Trap 相關的 CSRs

  • stvec: 當 Trap 發生時,RISC-V 會跳進 stvec 存放的記憶體地址去處理 Traps。kernel 會將 Traps handler 的記憶體地址寫入到 stvec 中。
  • sepc: 當 Trap 發生時,RISC-V 會將 program counter 存放於此 (因為 program counter 的值會被 stvec 覆蓋,使其跳到 Traps handler 的記憶體地址)。sret 指令 (從 Trap 回傳) 會將 sepc 的值寫入到 program counter,回到 Trap 發生時的 program counter。kernel 可以通過寫入 sepc 來控制回到的地方 (在 xv6 啟動中可以看到類似的手法)。
  • scause: 使用數字來描述發生 Trap 的原因。
  • ssctatch: 保存其他暫存器的值。
  • sstatus: 在 xv6 的啟動與架構中看到了 sstatus 暫存器的結構,SIE 域控制是否啟用中斷 (Interrupt)。如果 kernel 將 SIE 設置為 0,則 I/O 設備產生的中斷將被閒置 (pending) 直到 kernel 設置 SIE 啟用中斷。SPP 域表示 Trap 是發生在 user mode 還是 supervisor mode 中。並控制 sret 要回到哪一個特權模式。

stvec (trap vector)

stvec 為 CSR,包含 Handler 的記憶體地址,在 xv6 中有兩種 handler code 處理 interrupt,一種為 kernelvec (Handler supervisor mode 底下的 trap),另外一種為 uservec (Handler user mode底下的 trap)。

sstatus


上圖為 sstatus 的結構,裡面有幾個欄位控制 Interrupt。

  • SIE: SIE (Interrupts Enabled) 控制是否允許 Interrupt 發生,0 表示禁用 Interrupt,1 表示啟用 Interrupt。
  • SPIE: 當 Interrupt 發生時,我們需要儲存 SIE 的 bit,而這個 bit 會儲存到 SPIE (previous Interrupt Enabled) 中。
  • SPP: SPP (privilege/previous level) 記住我們是在什麼樣的特權模式底下發生了 trap,0 表示 user mode,1 表示 supervisor mode。

Trap 需要完成的操作

在 Trap 發生的最一開始,CPU 處於的特權模式為 user mode,而為了執行在 kernel 中的程式碼,我們需要切換到 supervisor mode 中,在 Trap 的過程中我們需要切換模式。

  1. Trap 讓我們從 user mode 進入到 supervisor mode 中,過程中會涉及許多暫存器狀態的改變,在最一開始涉及特權狀態的 bit 為 user mode,我們會需要在 trap 的時候對特權狀態的 bit 進行一些更動,而當我們要回復到 user mode 時候,我們會需要恢復 process 的狀態,而在 RISC-V 架構中 (RV64),process 可以使用的總共有 32 個暫存器,因此為了恢復狀態,我們需要在 trap 發生之前保存這 32 個暫存器。
  2. 我們需要儲存 Program counter 的值,讓我們能夠在 user mode 中斷的位置之後繼續執行 user mode 底下之後的程式碼。
  3. 為了使用一些高權限指令,我們需要由 user mode 進入到 supervisor mode 中。
  4. satp 指向的 page table 為 user page table,user page table 無法映射到 kernel,因此在執行 kernel 中的程式碼之前,需要讓 satp 指向到 kernel page table。
  5. 由於我們需要呼叫在 kernel 中的函式,因此 stack pointer 需要指向到 kernel 中某一個記憶體地址。
  6. 在完成以上的操作之後,我們需要跳入kernel中的程式碼。

由於安全性考量,trap 的操作不能依賴任何來自 user mode 的資料,避免安全性被破壞,因此上述的 32 個來自 user mode 的暫存器在 supervisor mode 中只是保存這一些資料,並不會去存取他們。

當我們處於 supervisor mode 中,我們會獲得更多的權限,包含以下

  1. 我們可以讀寫 CSR,包含讀寫 satp ,決定指向的記憶體分頁或是禁用記憶體分頁的功能,而在 user mode 我們並不能完成這一些操作。
  2. 可以對 PTE 中的 flag 進行更動,像是將 PTE_U 設置成 1,表示可以在 user mode 底下使用這一個虛擬記憶體到物理實體記憶體的轉換。

在 supervisor mode 底下,我們仍然需要通過 page table 的方式去存取記憶體,也就是我們無法直接去存取實體物理記憶體。supervisor mode 還是受到目前 page table 的虛擬記憶體地址空間限制。

Trap 流程

發生在 supervisor mode 的 Trap

  • 硬體層面
    1. program counter 的值存入 sepc
    2. stvec 的值寫入到 program counter
    3. 將發生 trap 的原因以數字寫入到 scause,下圖為 scause 的結構

      如果異常 (exception) 是由 Interrupt 引起的,則 Interrupt 域設置為 1,而後面的 WLRL 域會描述 Interrupt 發生的原因,如下表所示
    4. 寫入一些資訊到 stval 用來幫助處理 trap,stval 內存放中斷處理所需要的一些訊息,例如 page fault,instruction fetch 等等等,紀錄發生目標的記憶體地址或是指令。
    5. 將 trap 發生之前的特權模式寫入到 sstatusSPP 域,如果是 user mode,則用 0 表示,如果是 supervisor mode,則用 1 表示。
    6. sstatusSIE 域寫入到 SIE 域中。
    7. sstatusSIE 域設置為 0 ,表示禁用中斷。
    8. 將特權模式切換到 supervisor mode。

在以上動作完成之後,便會開始執行 trap handler。trap handler 結束後,會使用 RISC-V 中的 ret 指令回到 trap 發生的地方以及特權模式,以 sret 來說,會進行以下動作

  1. sstatusSPIE 域寫到 sstatusSIE 域。
  2. 回到 sstatusSPP 域中表示的特權模式,也就是進入 trap 之前的特權模式。
  3. 將 program counter 的值設置成 sepc 暫存器中的值,也就是回復 program counter 的值。

整個System call的動作我們可以先看作是以下操作

  • 使用System call write()
  • write()使用 ecall 指令切換到 supervisor mode 中。
  • 執行 trap handler,第一個函式為 trampoline.s 中的 uservec。
  • uservec 呼叫 trap.c 中的 usertrap()
  • usertrap()呼叫 syscall()syscall() 通過傳入的 System call 的編號,找到 sys_write()
  • sys_write() 結束後回到 syscall()
  • syscall() 需要完成從 supervisor mode 回復到發生 trap 時的特權模式,因此 syscall() 會通過位於 trap.c 中的 usertrapret() 完成這一件事情。
  • usertrapret() 為 C 語言中的函式,只能處理部分回復發生 trap 的特權模式的部份工作,剩下的部分 usertrapret() 會呼叫 trampoline.s 中的 userret() 完成剩下的工作
  • userret() 會使用一些指令回到 trap 發生時的模式,在本例為 user mode。
    我們可以通過 System call,write() 來了解 trap 的處理流程。
    以 Shell 的角度來看,write() 就和 printf() 一樣是一個 C 語言的函式呼叫,但實際上 write() 通過執行 ecall 指令來執行 System call。ecall 會切換到 supervisor mode中,在這個過程中,kernel 執行的第一條指令為一個使用組合語言寫的函式 uservec。也就是 trap handler 開始執行的地方。以下為 getcmd(),其中包含了 write() 的呼叫。
int
getcmd(char *buf, int nbuf)
{
  write(2, "$ ", 2);
  memset(buf, 0, nbuf);
  gets(buf, nbuf);
  if(buf[0] == 0) // EOF
    return -1;
  return 0;
}

在user mode底下的Shell呼叫write()的時候,會呼叫一個關連到Shell的函式庫,這個函式庫為user/usys.pl

#!/usr/bin/perl -w

# Generate usys.S, the stubs for syscalls.

print "# generated by usys.pl - do not edit\n";

print "#include \"kernel/syscall.h\"\n";

sub entry {
    my $name = shift;
    print ".global $name\n";
    print "${name}:\n";
    print " li a7, SYS_${name}\n";
    print " ecall\n";
    print " ret\n";
}
	
entry("fork");
entry("exit");
entry("wait");
entry("pipe");
entry("read");
entry("write");
entry("close");
entry("kill");
entry("exec");
entry("open");
entry("mknod");
entry("unlink");
entry("fstat");
entry("link");
entry("mkdir");
entry("chdir");
entry("dup");
entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");

可以看到li a7, SYS_${name},這裡的 name 為 write,因此這邊會將 SYS_wirte 載入到 a7 暫存器中,而SYS_write為巨集,表示 16。這裡為告知 kernel 要呼叫第16個 System call,也就是 SYS_write,接著可以看到下一行執行了 ecall。

從 ecall 開始會進入到 kernel space 中,也就是進入到 kernel 中,在 kernel 的工作完成後會回到 user space 中,執行 ecall 後面的 ret,最後回到 Shell。

before ecall (environment call)

當我們在 supervisor mode 底下執行 ecall,會觸發一個 ecall-from-s-mode-exception,接著會進入 machine mode 的中斷處理。而當我們在 user mode 底下執行 ecall,會觸發 ecall-from-u-mode-exception,進入到 supervisor mode 的中斷處理,通常這個情況會發生在 System call,也就是以下說明的情況 :

通過 gdb 來了解 ecall 的行為,我們知道 write() 會使用 ecall,而在編譯執行 xv6 時,會產生出 .asm 檔來幫助我們除錯,因此,我們可以通過檢視 sh.asm 來得知在 write()中 ecall 的記憶體地址,在得知 ecall 的記憶體地址後,我們就能夠使用 gdb 在這個位置下斷點並且追蹤 ecall 的行為

我們可以看到 write 的記憶體地址位於 0x00000dea,而 ecall 位於 0x00000dec,因此我們可以在這個位置下一個斷點

接著開始執行 xv6,xv6 會在 ecall 之前停下

我們在 getcmd() 中執行了 write(2, "$ ", 2);,我們可以把在 user mode 底下 32 個暫存器印出,得知目前暫存器的狀況

ra             0xe84	0xe84
sp             0x3e90	0x3e90
gp             0x505050505050505	0x505050505050505
tp             0x505050505050505	0x505050505050505
t0             0x505050505050505	361700864190383365
t1             0x505050505050505	361700864190383365
t2             0x505050505050505	361700864190383365
fp             0x3eb0	0x3eb0
s1             0x12e9	4841
a0             0x2	2
a1             0x3e9f	16031
a2             0x2	2
a3             0x505050505050505	361700864190383365
a4             0x505050505050505	361700864190383365
a5             0x24	36
a6             0x505050505050505	361700864190383365
a7             0x10	16
s2             0x24	36
s3             0x0	0
s4             0x25	37
s5             0x2	2
s6             0x3f50	16208
s7             0x1430	5168
s8             0x64	100
s9             0x6c	108
s10            0x78	120
s11            0x70	112
t3             0x505050505050505	361700864190383365
t4             0x505050505050505	361700864190383365
t5             0x505050505050505	361700864190383365
t6             0x505050505050505	361700864190383365
pc             0xdec	0xdec

首先由 pc (Program counter) 可以得知我們下一行要執行的指令的記憶體地址為 0xdec,也就是 ecall 所在的記憶體位置。可以發現目前記憶體地址較小,而我們可以回憶前幾天看到的圖

可以發現到記憶體較小的區域為user space的區域,而到了後面進入到kernel space的時候,可以發現到記憶體地址會相應的增長。

a1, a2 暫存器存放我們傳入的參數,a1 裡面是記憶體地址,指向我們要寫入的字串,我們可以反參考 a1 中的記憶體地址得知我們要 write() 的內容。

而 a7 存放的是 System call 的代號,這邊存放的便是 Sys_write 的代號,16。(決定 a7 存放的是 System call 的代號是 user/usys.pl),a0 存放 System call 的回傳值。

我們也可以試著查看 satp 的內容 (這裡查看 satp 不是在 xv6 中察看,因為我們目前是處於 user mode,我們是在 QEMU 中察看,xv6 是跑在 QEMU 上的)。這裡印出 satp 印出的是 page table 所在的物理記憶體地址。這是 process 的 page table。


我們可以通過在 QEMU 上的 console 印出 page table 中每一個entry。

(qemu) info mem
vaddr            paddr            size             attr
---------------- ---------------- ---------------- -------
0000000000000000 0000000087f61000 0000000000001000 rwxu-a-
0000000000001000 0000000087f5e000 0000000000001000 rwxu-a-
0000000000002000 0000000087f5d000 0000000000001000 rwx----
0000000000003000 0000000087f5c000 0000000000001000 rwxu-ad
0000003fffffe000 0000000087f70000 0000000000001000 rw---ad
0000003ffffff000 0000000080007000 0000000000001000 r-x--a-

我們可以看到虛擬記憶體 0x2000 這個 page,是一張無效的 page,因為他的 U 域並沒有設置,我們可以推測這是 guardpage,防止 Shell 使用過多 stack page,而在這個 page table 中,我們可以發現最後兩條 entry 的虛擬記憶體地址十分的大,我們回憶先前看到的 process virtual address space

可以推測第一張 page 和第二張 page 分別為 trampoline 和 trapframe,而由權限等級可以判斷我們目前無法存取該區域 (U 域沒有被設置,只有被設置的情況下才能夠在 user mode 底下存取),後面我們進入到 supervisor mode 後我們便可以對這兩張 page 進行存取了。

這裡我們可以看到權限的地方可以看到 a,d,a 表示是否被使用過 (access),d 表示 PTE 是否被寫入過 (Dirty),這是我們在 Qemu 中看見的,但在先前我們知道在 xv6 中並沒有相關的實作與使用。

可以看到在 page table 中並沒有任何到 kernel page 的映射。

execute ecall

我們執行 ecall 指令後,我們就進入到了 supervisor mode 中,也就是我們現在位於 kernel space 中,從上面的推論中,我們可由記憶體地址判斷目前我們所在的空間,可以通過執行完 ecall 後印出 program counter 的值,知道下一個指令所在的記憶體地址,從而知道我們目前所處的 space。

關於 gdb trace ecall :
ecall 為 RISC-V 架構下的 CPU 指令,因此我們無法通過 gdb 直接使用 step 追蹤進入並查看 ecall 具體的內容,在 ecall 中會設置 stvec 暫存器的內容,而 ecall 會跳轉到 stvec 暫存器的內容,在這裡 stvec 暫存器的內容為 0x3ffffff000,也就是我們看到的,是 ecall 執行完成之後,已經完成跳轉的結果。

而我們將program counter印出來,可以發現到我們在 0x3ffffff000 的記憶體地址,而根據我們印出來的 page table 可以知道我們現在位於 trampoline 中 (也就是整個 page table 中最上面得 page),而目前的模式為 supervisor mode。而以下是trap機制中最一開始要執行的指令,也就是 trap handler 最一開始的指令,為trampoline.S中的 uservec。

uservec:    
	#
        # trap.c sets stvec to point here, so
        # traps from user space start here,
        # in supervisor mode, but with a
        # user page table.
        #

        # save user a0 in sscratch so
        # a0 can be used to get at TRAPFRAME.
        csrw sscratch, a0

        # each process has a separate p->trapframe memory area,
        # but it's mapped to the same virtual address
        # (TRAPFRAME) in every process.
        li a0, TRAPFRAME
        
        # save the user registers in TRAPFRAME
        sd ra, 40(a0)
        sd sp, 48(a0)
        sd gp, 56(a0)
...

以上這一些指令都是在 supervisor mode 底下執行,且目前我們正處於 trampoline,也就是目前 trampoline 包含 kernel 的 trap 程式碼,而目前暫存器還是 user mode 的內容,我們接下來需要將這一些暫存器的內容儲存到某一個地方,以便之後結束 trap 之後,要回到 user mode 的時候可以成功回復狀態。

在 ecall 中並不會切換記憶體分頁,因此我們需要 trap 的處理程式碼儲存在 user page table 中,而 trap 會在 user page table 中某一個地方執行,也就是 trampoline 這一個 page 中。ecall 會跳轉到的記憶體位置是存放在stvec暫存器中,stvec從名子可以判斷出是一個在 supervisor mode 底下才能夠使用的暫存器。而 kernel 會設置好stvec的內容,因此我們在執行 ecall 之後,會跳轉到 trampoline 這個 page 中。

ecall 做了什麼?

到這裡,我們可以知道我們通過了 ecall,進入到 trampoline 中,並且從 user mode 切換到 supervisor mode 了,而整理一下,ecall 執行了以下三件事情

  1. 從 user mode 切換到 supervisor mode (從進入到trampoline可以看出)
  2. 把進入 trap 之前的 program counter 的值儲存到 sepc 暫存器中,以上面的例子我們可以知道 sepc 暫存器的內容為 0xdec (通過gdb印出暫存器內容得知)
  3. ecall 會跳轉到 stvec 暫存器所儲存的記憶體地址。(在執行完ecall後的program counter為stvec儲存的記憶體地址)

而以上為 ecall 完成的工作,下面還有很多事情需要完成 (像是儲存 32 個位於 user mode 底下的暫存器內容),而完成這一些事情依靠的就是在 System call 的動作中提及的其他需要依靠 usertrap()trap.c 完成等等的操作。

我們可以發現到 ecall 實際上完成了很少的工作,原因是因為 RISC-V 想要讓程式設計者有更多的靈活性去設計軟體。前面說到 ecall 不會切換 page table,而如果我們讓 ecall 去切換 page table,對於一些沒有必要這麼做的 System call,我們很難對效能進行一些優化 ( 切換 page table 需要一定的效能開銷)。

reference

SiFive FU540-C000 Manual v1p0
xv6-riscv
Operating System Concepts, 9/e
RISC-V xv6 Book


上一篇
Day-13 Exception vs Interrupt, Trap overview Driver
下一篇
Day-15 xv6 Trap (user mode): Trace Trap
系列文
與作業系統的第一類接觸 : 探索 xv631
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言