MIT6S081 課程筆記
mit6s081 lecture notes
Created: 2023-06-05T20:26+08:00
Published: 2024-05-12T12:13+08:00
Categories: OperatingSystem
關於這門課程使用到的資料:
- schedule: https://pdos.csail.mit.edu/6.S081/2020/schedule.html
schedule 可以認為提供了資源(如 pdf、video)和給出了學習的順序 - 課程轉錄:https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/
- 課程影片:https://www.bilibili.com/video/BV19k4y1C7kA/
- 書籍翻譯,實驗解析:http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/summary.html
- Lecture 01
- Lecture 02 - 16
- Lecture 17 Virtual Memory for Applications
- Lecture 18 OS Organization
- Lecture 19 Virtual Machine
- 頁錶轉換
- 裝置
- Lecture 20 Kernels and High Level Languages
- Lecture 21 Networking
- Lecture 22 Meltdown
- Lecture 23 RCU
- 讀寫鎖存在效能問題
- RCU
Lecture 01
- 如何建立使用者軟體和硬體之間的聯絡?
- 什麼是 jump into the kernel?
- 你覺得作業系統課程哪裡有趣呢?給出自己的答案,可以參考教授說的。
- 介紹 open、fork 等系統呼叫,引入 fd、process 和 copy 等概念
- 告訴我們 shell 執行命令,是在 exec
比如電腦上有 VI 這個 text editor,CC 這個 compiler,SH 這個 shell
硬體就是 CPU、RAM、DISK 之類的,之間的聯絡是 Kernel Space,這裡面有 File System(FS),FS 作為橋樑,要對那些軟體(Process)提供一些介面:
- Memory Alloc:不同軟體的記憶體多少
- Access Control:能不能讀寫這個檔案
- ……
we are interested in how the applications interact with the kernel and how the interfaces look like
example:
fd = open("out", 1)
jump into the kernel and kernel achieve the args.
jump into the kernel 是針對函式說的,當函式 jump into the kernel 的時候,它就有了 privilege,可以 modify disk 之類的,而這是普通的 function call 無法做到的。
計算機還是神奇的,在硬體和軟體之間提供了物理橋樑,你看相同的硬體可以執行不同的作業系統,比如在主機板上裝 Win10 或者 Ubuntu
這種硬體和軟體建立聯絡的結果很吸引人。
我們需要自己設計 interface,在 simple 和 efficient 之間做取捨。
教授還展示了 open 等一系列 syscall 的樣例
- open:介紹了 fd,每一個程序都會維護自己的 fd table,一個 fd 就是一個程序開啟了一個檔案
- fork:建立一個新的程序
- exec:替換當前程式
fork 使用的例子比較特殊,會交替printf
,列印字元
當我們在 shell 中執行命令的時候,比如 $ echo abc
,把 echo 對應的二進位制碼讓處理器去執行,真的背後是 shell fork 了一個 process,在這個 child process 裡面執行了 echo
Lecture 02 - 16
都寫到 xv6 程式碼執行流 這篇文章裡面了。
Lecture 17 Virtual Memory for Applications
這一講介紹的是「應用程式使用虛擬地址」。
讓我印象最深的是,可以讓應用程式像 kernel 那樣處理異常和中斷,教授上課介紹了 signal 機制,
我想起對於一些應用程式,按下 Ctrl+C 是無法中止的,因為應用程式自定義了 SIGINT 的處理方式。
來自 GPT 的對話:
為什麼一些程式無法使用 ctrl + c 中止
有幾種可能的原因導致某些程式無法使用 Ctrl+C 中止:
- 程式沒有正確處理中斷訊號:當你按下 Ctrl+C 時,作業系統會傳送一箇中斷訊號(SIGINT)給正在執行的程式。如果程式沒有正確處理這個訊號,它可能會忽略中斷請求,導致無法中止。
- 程式正在執行一個耗時操作:如果程式正在執行一個耗時的操作,例如計算密集型任務或者長時間的迴圈,它可能無法立即響應中斷訊號。在這種情況下,你可能需要等待一段時間才能中止程式。
- 程式被設計為無法中止:有些程式可能被設計為無法透過中斷訊號中止,這可能是出於安全性或其他特定需求的考慮。這些程式可能會忽略中斷訊號或者採取其他措施來防止被中止。
- 作業系統或終端程式的問題:有時,作業系統或終端程式本身可能存在問題,導致 Ctrl+C 無法正常工作。這可能是由於軟體錯誤、配置問題或其他因素引起的。
如果你遇到無法使用 Ctrl+C 中止的程式,你可以嘗試以下方法:
- 等待一段時間,看看程式是否會在某個點響應中斷訊號。
- 嘗試使用其他中止程式的方法,例如傳送其他訊號(如 SIGTERM)或者使用作業系統提供的工作管理員來終止程式。
- 檢查程式的文件或官方資源,瞭解是否有特定的中止方法或選項。
- 如果問題持續存在,可能需要進一步調查作業系統、終端程式或程式本身是否存在問題,並嘗試修復或尋求其他解決方案。
應用程式對於 SIGINT 預設的處理方法是什麼
對於大多數應用程式,預設情況下,它們會對 SIGINT(中斷訊號)採取預設的處理方法。預設處理方法通常是終止程式的執行。
當應用程式接收到 SIGINT 訊號時,作業系統會傳送該訊號給應用程式。應用程式會捕獲該訊號並執行預設的處理方法,這通常是終止程式的執行。這意味著按下 Ctrl+C 或傳送 SIGINT 訊號將導致應用程式退出。
然而,需要注意的是,應用程式可以透過註冊訊號處理函式來自定義對 SIGINT 訊號的處理方式。在這種情況下,應用程式可以選擇忽略 SIGINT 訊號、執行特定的操作或採取其他自定義行為。這取決於應用程式的設計和需求。
總之,大多數應用程式預設情況下會終止執行,但具體的處理方式可能因應用程式的自定義設定而有所不同。
Lecture 18 OS Organization
微核心的想法,介紹了 IPC,最原始的 IPC 需要兩個通訊程序分別進出 kernel,將發生使用者空間到核心、核心到使用者空間的資料複製,而且切換到 kernel 需要 flush TLB,是一個開銷非常大的動作。
於是介紹了 IPC speedup 的方法,一種 Synchronous 的 send-recv 機制。
通訊的兩個程序中一者已經在核心中,等待另一者。比如程序 P2 呼叫 recv 要求程序 P1 send,但是 P1 的 send 還沒來,於是 P2 自己就在核心中躺著,等到 send 一發生,就立刻響應這個 send,
讓核心直接複製 P1 的使用者資料到 P2 的使用者空間中。
就不用程序切換和兩次複製的開銷。
為了驗證效能,做了個 L4/Linux,讓 Linux kernel 以一個 process 的形式啟動在 L4 中,其他 VI、SH 等程序也是 L4 的 程序,讓 kernel 和 process 做 IPC。
Mach 是微核心,像 MacOS 就借鑑了 Mach 部分的實現。
Lecture 19 Virtual Machine
方法一:純粹用軟體模擬虛擬機器的執行,會慢。
方法二:trap and emulate:虛擬作業系統執行在 user space,當虛擬作業系統執行特權指令時候 trap,交給 Virtual Machine Monitor 執行。
分為 Guest Space 和 Host Space,一種實現方式是讓 Virtual Machine Monitor 在 Supervisor Mode 執行,每一個 Guest Machine 的指令直接在硬體上執行。
但是 Guest Machine 的指令如果涉及到如 satp 這類暫存器的修改,比如切換頁表,那麼就會 trap,在 trap 中執行 Guest 的特權指令。VMM 記錄特權指令的涉及到的暫存器的值,這個叫做 Virtual State,為每一個 Guest 儲存一個 Virtual State。
Virtual State 中記錄 s 暫存器的值,Guest 在 kernel 還是 user mode,模擬的 hart 數量。
頁錶轉換
satp 的切換 VMM 不能直接在硬體上執行,因為這會把整個機子的記憶體暴露給 Guest。
GuestPageTable 儲存了 gva(Guest Virtual Address)到 gpa(Guest Physical Address)的對映,
VMM 維護一個 shadow pagetable,儲存 gpa 到 hpa(Host Physical Address)的對映,
真正使用的 satp 是這兩個頁表的組合,防止虛擬機器從分配的記憶體中逃脫。
裝置
三種方法:
- emulation:為了讓 Guest 操作裝置,VMM 模擬一個裝置,當發現 Guest 要訪問特定範圍記憶體時候,VMM 知道了,就模擬一個對應的裝置。
- virtual device: 類似 virtuio_disk.c 的方法,不是模擬真實的物理裝置,Guest 也知道自己在使用某種設計好的介面操作裝置
- pass through real device
方法三:硬體層面支援虛擬化,如 intel 的 VT-x 方案,每一個 core 新增一組暫存器 和 EPT,用於給 Guest 執行特權指令。
最後是 Dune,利用 VT-x 讓程序直接擁有自己獨立的頁表,從而不必再使用虛擬機器實現沙箱機制。
Lecture 20 Kernels and High Level Languages
用高階語言開發 Kernel 的得失。
高階語言有高階語言的好處:比如 GC、型別檢查、協程、自帶的資料結構(string、map)……
課程組用 Go 開發了一個 Kernel,起名叫做 Biscuit。
先在裸機上透過一些 shim code 對硬體做調整啟動 go runtime,然後用 go 語言寫 kernel。
結果:roughly similar
Lecture 21 Networking
Ethernet 同級別有 WiFi,Ethernet 是物理層的,比如電腦用網線連線起來組成區域網,這個區域網內的 packet 傳輸需要表示從哪一個網路卡傳送到哪一個網路卡,每個網路卡有 MAC 地址標識。
所以每個 Ethernet 的 packet 資料格式包括 Source, Destination, Type, Payload. Type 標識這個 Packet 交給哪一種上層協議處理,可以是 IP 或者 TCP。
Source, Destination, Type 就是 Ethernet Header。
NIC 是 Network Interface Card(網路介面卡)的縮寫,也被稱為網路介面卡、網路介面控制器或網路介面器。它是一種用於連線計算機與計算機網路之間的硬體裝置。
論文中,也就是傳統的處理 Packet 的方法:
現在我們有了一張網路卡,有了一個系統核心。當網路卡收到了一個 packet,它會生成一箇中斷。系統核心中處理中斷的程式會被觸發,並從網路卡中獲取 packet。因為我們不想現在就處理這個 packet,中斷處理程式通常會將 packet 掛在一個佇列中並返回,packet 稍後再由別的程式處理。所以中斷處理程式這裡只做了非常少的工作,也就是將 packet 從網路卡中讀出來,然後放置到佇列中。
之後,在一個獨立的執行緒中,會有一個叫做 IP processing thread 的程式。它會讀取記憶體中的 packet 佇列,並決定如何處理每一個 packet。其中一個可能是將 packet 向上傳遞給 UDP,再向上傳遞給 socket layer 的某個佇列中,最後等待某個應用程式來讀取。通常來說,這裡的向上傳遞實際上就是在同一個執行緒 context 下的函式呼叫。
通常來說網路卡會有傳送中斷程式,當網路卡傳送了一個 packet,並且準備好處理更多 packet 的時候,會觸發一箇中斷。所以網路卡的傳送中斷也很重要。
實驗中是用 DMA:
接下來我將討論一下 E1000 網路卡的結構,這是你們在實驗中要使用的網路卡。E1000 網路卡會監聽網線上的電訊號,但是當收到 packet 的時候,網路卡內部並沒有太多的快取,所以網路卡會直接將 packet 複製到主機的記憶體中,而記憶體中的 packet 會等待驅動來讀取自己。所以,網路卡需要事先知道它應該將 packet 複製到主機記憶體中的哪個位置。E1000 是這樣工作的,主機上的軟體會格式化好一個 DMA ring,ring 裡面儲存的是 packet 指標。所以,DMA ring 就是一個陣列,裡面的每一個元素都是指向 packet 的指標。
21.7 Ring Buffer | MIT6.S081
Lecture 22 Meltdown
介紹了 L1 L2 TLB 的細節:22.4 CPU caches | MIT6.S081
前置知識:
- 使用者態可能直接擁有核心態的頁表複製,這樣避免切換到核心態的時候 TLB flush。所以如果不做許可權檢查,使用者可以直接不切換到 kernel 直接讀到 kernel 的資料。
- speculative execution:CPU 預先執行指令,如 load 後面的指令不必等待 load 執行完成;if-else 分支內的指令不必等待 cond 計算完成
- retirement:預先執行的指令可能是無效的,要 retire 這些指令,比如分支預測錯誤,修改了 r1,要改回 r3 為原來的值
- rdtsc(read time stamp counter)指令得到多少個 CPU cycle,clflush 確保某個地址不在 cache 中
Meltdown 利用了預先執行指令卻不檢查指令的許可權的漏洞。
buf[8192]
// flush cache
clflush buf[0]
clflush buf[1]
<some expensive instruction like divide> // try to defer (1) retirement
r1 = <a kernel virutal addr>
r2 = *r1 // (1) will pagefault in retirement
r2 = r2 & 1 // get lowest bit
r2 *= 4096
r3 = buf[r2] // (2) before retirement try to load into cache
<handle page fault from "r2 = *r1">
// reload of flush+reload
a = rdtsc
r0 = buf[0]
b = rdtsc
r1 = buf[4096]
c = rdtsc
if b-a < c-b:
low bit was probably a 0
為了利用 CPU 預先執行的特點,把 (1)-(2) 之間的指令都執行了,就要推遲 r2=*r1
retirement 的時間。透過 expensive instruction 延遲 r2=*r1
的時間。
一旦 r3=buf[r2]
執行,哪怕被 retire,但是 buf 已經被 cache 了,所以最後判斷 r2 是 4096 還是 0.
note. 結合 clflush 指令和 rdtsc 指令可以判斷 cache 大小。
Lecture 23 RCU
RCU 的全稱是 Read-Copy-Update
讀寫鎖存在效能問題
r_lock(l):
while 1:
x = l->n
if x < 0:
continue
if CAS(&l->n, x, x+1):
return
所以 r_lock 中最關鍵的就是它對共享資料做了一次寫操作。所以我們期望找到一種方式能夠在讀資料的同時,又不需要寫資料,哪怕是寫鎖的計數器也不行。這樣讀資料實際上才是一個真正的只讀操作。
如果有 n 個執行緒在 n 個 CPU 上讀一個資料,也會因為要寫 l->n
導致 \(O(n^2)\) 獲取鎖的開銷:
- 第一次 n 個執行緒 CAS,但是 1 個成功
- 第二次 n-1 個執行緒 CAS,但是 1 個成功
- ……
RCU
以連結串列為例,解決方法是讀者完全不需要鎖,直接讀,但是寫者只是修改資料結構的 node,得到讀者讀完以後,再 commit writing,修改資料結構,free 掉。
規則:
- reader 讀資料不允許被 context switch,透過關閉中斷實現,這段程式碼是 reader 的 critical section
- writer commit 資料前,要求自己在所有 cpu 上都被 schedule 過,這樣 reader 如果在讀取修改的 node 時候,必須結束讀取。writer 透過 schedule 讓所有 reader 結束對修改的 node 持有,這樣就可以安全地釋放 E2.
23.6 RCU 用例程式碼 | MIT6.S081