---
# System prepended metadata

title: 1.作業系統筆記  虛擬內存   trap(stack and syscall)

---

## 1. 虛擬地址

每一個process有自己的pagetable，來完成虛擬地址到物理地址的映射

**虛擬地址 映射至物理地址關係**
cpu在使用虛擬地址到物理地址時，內部會有satp的寄存器，每個process儲存的地址不一樣，以實現進程間的隔離，satp會儲存第一層page directory的地址，如圖中所示。

同時以下步驟是依靠MMU(Memory Management Unit)電路來實現，不需要依靠軟件。

![image](https://hackmd.io/_uploads/Sko_4vN30.png)
1. 虛擬地址實際上儲存的是三個偏移量，而不是真的有2^27次方的虛擬記憶體空間，來達到節省物理內存的目的，因為沒用到就不需要創建在往下層級的page table。
2. 三級列表都是實際存在的物理地址，透過satp和第一個偏移量，來尋找下一級table的物理地址，在使用第二層偏移量，以此類推。
3. 而要將虛擬地址映射到物理地址，需要查找三次物理地址，太過耗費時間，有叫做TLB(Translation Lookaside Buffer)的存在，作用類似cache，但原理不太一樣，cache是直接儲存地址對應資料，TLB是儲存虛擬地址對應的物理地址，可以跳過三級查找的時間。
4. 因此TLB在切換進程時，需要清空，避免翻譯錯誤。
5. user mode 跟 kernel 的虛擬地址應設規則是不同的。
6. 第一個pagetable directory 並沒有被全部使用，只使用38bit，所以trampline的偏移量只到２５６。
7. guard page 被映射在每個process的stack下面，包含kernel 和 user。

**xv6 內核虛擬記憶體配置**
物理地址記憶體的配置由工程師決定，如果記憶體位置大於0x86.....則向DRAM，如果小於，則走向對應IO的寄存器，這部分需查詢對應開發版的DATASHEET。
作業系統會讀取對應地址從0X100讀取BOOT ROM資料來起動bootloader，bootloader會初始化硬件，並創建c語言的STACK，並跳轉到dram的 0X8000..來啟動作業系統，最後將其映射為對應虛擬地址。
![image](https://hackmd.io/_uploads/rk0s5vVnC.png)
由映射規則可以發現，在phystop上的資料會被映射到kernel data和kernel text 同一塊物理地址，而在phystop以下的虛擬地址，經過三級映射後，會跟物理地址一樣。
kernel 會用 free memory 來儲存user process的 pagetable

**user mode虛擬記憶體配置(每個process)**
user mode 記憶體配置 跟 kernel mode 配置差不多，但是內容不一樣，有heap和stack這些常見的配置。

![image](https://hackmd.io/_uploads/S1AlhKBhC.png)


**vmprint實驗(這個比較簡單，沒看解答)**  


這是user mode 的虛擬內存實驗，因為是ｐ－＞ｐｉｄ＝＝１，會分配test，data，stack等等資料。

在exec.c加這行指令
![image](https://hackmd.io/_uploads/SkmscjrnA.png)
代表創建第一個進程會使用vmprint()

根據提示在kernel/vm.c 創建 vmprint()函數 函數參數原本沒有int，但我為了遞歸起來比較漂亮加的，基本上是參考freewalk函數寫出來的。

freewalk()
![image](https://hackmd.io/_uploads/HJqYiiBh0.png)
freewalk根據觀察是walk函數的相反，可將虛擬記憶體的物理記憶體解放

**從這裡的程式可以發現，前兩級pagetable PTE_R PTE_W PTE_X是至0的。**

vmprint()
![image](https://hackmd.io/_uploads/H10Usorh0.png)
參數int 是為了更好的遞歸，pagetable_t是指向512組pte_t的指標，可以從riscv.h發現
![image](https://hackmd.io/_uploads/HJDX3iBnC.png)

for迴圈作用是找到存在的pte，
![image](https://hackmd.io/_uploads/B1ZSpjBnC.png)
根據宏定義PTE2PA則是單純的對指標進行位元操作。
![image](https://hackmd.io/_uploads/B1-IasShA.png)
![image](https://hackmd.io/_uploads/S1Mg6oB20.png)

這裡的if語句，則是查看是否為前兩級pte，是的話則進行遞歸，不是的話則終止。
![image](https://hackmd.io/_uploads/Hyl6TsrhA.png)


後面實驗有空再補筆記
## 2.trap
有三種情況會導致發生trap，CPU會執行特殊代碼，第一種是user mode使用ecall，會跳轉到kernel mode處理對應呼叫，第二種是非法情形，除0或是使用不存在的虛擬地址，第三種是中斷，系統中斷去處理硬件的讀寫請求。

trap處理完成後會希望代碼能繼續執行，像中斷一樣越快越好，trap 的處理會交給內核，才能操控硬件。

**risc-v處理trap 的寄存器**

**stvec** :kernel 處理trap的address會放在這裡，進trap時，PC寄存器會跳轉到stvec。

**sepc**  :用來記錄原先PC的address，因為stvec會寫入到pc寄存器，trap結束後，會使用sret寄存器讓sepc寫回pc寄存器。

**scause**:用來描述trap發生原因的寄存器。

**sscratch**: 內核放置在這裡的值，trap發生後會記住其中一個寄存器的值，讓uservec能操控其中一個寄存器。

**sstatus**:  SIE bit 決定是否處理中斷，置0時內核不會處理中斷，直到變為1，SPP bit 則是儲存trap發生時是在user 還是 kernel，決定返回 user mode 還是 kernel mode，幫助sret寄存器判斷返回的位置。


**note: trap的發生除了保存PC寄存器，其他都是由軟件完成，以提供軟件的靈活性。**

在user mode 發生trap時，會使用**uservec來執行指令(stvec寄存器**)，會幫助切換satp到kernel的pagetable。

在kernel 和 user 的pagetable 中，**TRAMPOLINE都會指向同一塊物理地址**，用來執行uservec，讓trap的情況下，user跳轉到kernel能繼續執行uservec。

uservec不會改變發生當下所有寄存器的狀態，會用**sscratch**去紀錄a0寄存器，**讓uservec能操控a0寄存器**。


之後uservec 會保存用戶寄存器。在進入用戶空間之前，內核已經設置sscratch 指向每個進程的trapframe，trapframe有空間來保存所有的用戶寄存器（參見 kernel/proc.h:44）。
![image](https://hackmd.io/_uploads/HJpa9yRn0.png)
因為satp 還在user page table ，uservec 要在用user mode中映射trapframe。所以在創建每個process時，Xv6 每個process的trapframe分配一頁，並映射到user mode虛擬地址 TRAPFRAME，(通常在TRAMPOLINE下)。p->trapframe 也指向trapframe，但指向的是物理地址，以便內核通過內核頁面表使用它。

trapframe包含當前進程的內核指針、當前 CPU 的 hartid（硬件線程 ID）、usertrap 的地址以及內核頁表的地址。uservec 會檢索這些值，將 satp 切換到內核頁表，然後調用 usertrap(程式)。

usertrap 的任務是確定陷阱的原因、處理它，並返回（在 kernel/trap.c:37）。如上所述，首先更改 stvec，以便在內核中發生trap時由 kernelvec處理。會保存 sepc（保存的用戶程序計數器），因為sepc可能被覆蓋。如果trap是系統調用，則由 syscall 處理，syscall將保存的用戶程序計數器（PC）加上四，因為在 RISC-V 架構下，syscall發生時，程序計數器指向是 ecall 指令；如果是中斷，則由 devintr 處理；否則是異常，內核將終止故障進程，在結束時，usertrap 檢查進程是否被終止或是應該讓出 CPU（trap是計時器中斷）。
返回用戶空間的第一步是調用 usertrapret，此函數設置 RISC-V寄存器，以為以後user mode的trap做好準備。將 stvec指向uservec，準備uservec依賴的trapframe，並將 sepc 設置為先前保存的user PC。最後，usertrapret 在映射到user和kernel page的 trampoline上調用 userret；因為 userret的匯編代碼將切換page table，切換之後可以繼續運行。

usertrapret 調用 userret，將進程的用戶頁表指針傳遞給 a0，並將 TRAPFRAME 傳遞給 a1（kernel/trampoline.S:88）。userret 將 satp 切換到進程的用戶頁表。

userret 將trapframe中保存的用戶 a0 複製到 sscratch，為稍後與 TRAPFRAME 進行交換做準備。此時，userret 只能使用寄存器和trapframe的內容。接下來，userret 從trapframe恢復保存的用戶寄存器，最後一次交換 a0 和 sscratch 以恢復用戶的 a0 並保存 TRAPFRAME 以供下次trap使用，並返回用戶空間。



**ecall 執行流程** 
ecall時將參數放置在寄存器 a0 和 a1 中，並將系統調用號放置在 a7 中，這些會存在trapframe中。
之後 uservec->usertrap->syscall
syscall從trapframe中 a7 檢索系統調用號，用來找是哪一個syscalls，調用系統調用實現函數。

當系統調用返回時，syscall 將其返回值記錄在 p->trapframe->a0 中。這會導致原始的用戶空間調用返回該值，因為 RISC-V 上的 C 調用約定將返回值放在a0 中。系統調用通常返回負數以表示錯誤，返回零或正數以表示成功。如果系統調用號無效，syscall 會打印錯誤並返回 1。

**利用 page fault的trap**
fork創毽子進程和父進程時會共享物理內存，但設置為只讀，當出現修改只讀的物理內存時，會出現錯誤進trap，trap會複製錯誤內容到新的可讀寫的內存給子進程和父進程，當trap結束時不會報錯，而會將資訊寫入新複製的內存。

另外還有延遲分配，並非所有虛擬內存都會映射到物理內存，當使用到invalid的虛擬內存時，會進錯誤trap，在分配對應內存，這個技術就是 lazy allocation。





不同架構使用的gdb工具版本不一樣

stack 地址從高往低，所以每創建一個function，sp指針會做減法
![image](https://hackmd.io/_uploads/HJBSMna2C.png)



PC寄存器會保存當前是user mode 還是 kernel mode。


user mode 通常是較低的地址  kernel mode 較高 

uservec 使用彙編撰寫，可以避免編譯器編譯程式使用到額外的寄存器，導致恢復到user mode時出現錯誤。

usermode 的 pagetabel 中的 trapframe 或 trapline pte_u 的U沒有被置U，代表只能由kernel mode 訪問。

每個process 都有 自己的  stack 和  kernel stack。


**ecall()就做三件事  1.切換到superior mode  2.儲存當前pc，也就是ecall的程式位置到sepc  3.將pc寄存器替換成stvec寄存器(trampline page 的起始位置)**



**實驗 backtrace**
透過r_fp()讀取s0寄存器，為當前正在執行的kernel的堆棧地址，使用PGROUNDUP(cur_fp)會讀取頁表的頂端，risc-v的stack通常用一個頁表儲存，所以頁表的頂端也能理解成stack頂端，(fp-0x08)會是上一個函數的return address，(fp-0x10)這裡是16進制，代表-16，會是上一個函數fp的地址，
![image](https://hackmd.io/_uploads/SkqqyV1pA.png)
這裡使用(pte_t*)代表轉成指標類型，在用*使用指標，才能得到對應記憶體的數據，pte_t改成uint64也可以，最後迴圈整個程式，直到到stack top為止。
![image](https://hackmd.io/_uploads/Bk1DR7y6R.png)



**alarm實驗**

**test0:**
先將sysalarm和sysreturn函數加入syscall。
sysalarm有兩個輸入，分別是幾個cpu_tick要使用function，和要使用的function。
![image](https://hackmd.io/_uploads/r1vsyIl6R.png)
![image](https://hackmd.io/_uploads/rkQnJUgaC.png)
在proc 結構體中添加這三樣:
![image](https://hackmd.io/_uploads/rk-my8eTR.png)
其中input_count是決定幾個cpu_tick後要使用function，function則是periodic()，tickcount則是用來記錄距離上一次使用alarm過了幾個tick。

接著是設計sigalarm:
myproc()是讀取當前進程，argint 和 argaddr  則是將usermode傳入的參數，從a0,a1寄存器中讀取出來，tick_count則為0。
![image](https://hackmd.io/_uploads/SkBHWIxp0.png)
最後則是修改usertrap，這裡的which_dev==2，代表每過一個tick，系統會自己觸發的硬件中斷，所以alarm實際上是通過syscall修改這裡的值，當input_count被賦值時，通過設置p->trapframe->epc，代表中斷返回user mode時，會執行alarm給的function，這裡剛好是0x0000，因為是user mode，也是這個process的一開始，剛好是periodic，syscall則會直接返回periodic的地址。

![image](https://hackmd.io/_uploads/BkjhmUxaR.png)
所以最後結果等for迴圈造成的...跑完後才出現periodic，因為是tick造成的硬件中斷觸發alarm，而不是syscall觸發的。
![image](https://hackmd.io/_uploads/ryImHUea0.png)
![image](https://hackmd.io/_uploads/BJlhE8gaR.png)

test 01 and 02
則是在創建一個syscall，當呼叫alarm函數時，可以用sigreturn回到一開始呼叫sigalarm的位置，不然呼叫完periodic，原先的trapframe就被修改了，還會一直觸發alarm，不能達成計算CPU周期的功能。



修改pro.h，增加兩個東西，使用一個為inalam的類似鎖的東西，另一個是alarmframe，用來儲存觸發alarm之前的trapframe，讓sigreturn可以正確使用。
![image](https://hackmd.io/_uploads/BkxXPPxpA.png)

觸發alarm之後，函數有一個syscall，就是sigreturn
![image](https://hackmd.io/_uploads/S11wDDxT0.png)
所以修改usertrap和sigreturn即可，首先是usertrap，inalarm為0時觸發alarm函數，代表第一次觸發，或是sigreturn成功之後，才能正確計時，並儲存觸發前的trapframe，才進行修改，紅色部分則是需要用指標，代表整個結構體複製，沒用的，只是把alarmframe和trapframe複製到同一塊記憶體，沒起到保存的作用。
![image](https://hackmd.io/_uploads/HJN9IveT0.png)
最後是sigreturn:
syscall後把鎖解開，代表繼續計時，不會計算到periodic的時間，並將trapframe還原，同樣需要用引用，這裡我猜是內核trapframe必須固定是那個位置，不能是別塊記憶體，最後將計數器歸零。
![image](https://hackmd.io/_uploads/HyvVOwe60.png)

這張實驗滿有意思的，學到中斷和syscall的結合使用，也學到stack。















