MIT6S081課程筆記

ticlab發表於2024-05-12

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 中止:

  1. 程式沒有正確處理中斷訊號:當你按下 Ctrl+C 時,作業系統會傳送一箇中斷訊號(SIGINT)給正在執行的程式。如果程式沒有正確處理這個訊號,它可能會忽略中斷請求,導致無法中止。
  2. 程式正在執行一個耗時操作:如果程式正在執行一個耗時的操作,例如計算密集型任務或者長時間的迴圈,它可能無法立即響應中斷訊號。在這種情況下,你可能需要等待一段時間才能中止程式。
  3. 程式被設計為無法中止:有些程式可能被設計為無法透過中斷訊號中止,這可能是出於安全性或其他特定需求的考慮。這些程式可能會忽略中斷訊號或者採取其他措施來防止被中止。
  4. 作業系統或終端程式的問題:有時,作業系統或終端程式本身可能存在問題,導致 Ctrl+C 無法正常工作。這可能是由於軟體錯誤、配置問題或其他因素引起的。

如果你遇到無法使用 Ctrl+C 中止的程式,你可以嘗試以下方法:

  1. 等待一段時間,看看程式是否會在某個點響應中斷訊號。
  2. 嘗試使用其他中止程式的方法,例如傳送其他訊號(如 SIGTERM)或者使用作業系統提供的工作管理員來終止程式。
  3. 檢查程式的文件或官方資源,瞭解是否有特定的中止方法或選項。
  4. 如果問題持續存在,可能需要進一步調查作業系統、終端程式或程式本身是否存在問題,並嘗試修復或尋求其他解決方案。

應用程式對於 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 是這兩個頁表的組合,防止虛擬機器從分配的記憶體中逃脫。

裝置

三種方法:

  1. emulation:為了讓 Guest 操作裝置,VMM 模擬一個裝置,當發現 Guest 要訪問特定範圍記憶體時候,VMM 知道了,就模擬一個對應的裝置。
  2. virtual device: 類似 virtuio_disk.c 的方法,不是模擬真實的物理裝置,Guest 也知道自己在使用某種設計好的介面操作裝置
  3. 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 的時候,會觸發一箇中斷。所以網路卡的傳送中斷也很重要。

network-intr

實驗中是用 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

前置知識:

  1. 使用者態可能直接擁有核心態的頁表複製,這樣避免切換到核心態的時候 TLB flush。所以如果不做許可權檢查,使用者可以直接不切換到 kernel 直接讀到 kernel 的資料。
  2. speculative execution:CPU 預先執行指令,如 load 後面的指令不必等待 load 執行完成;if-else 分支內的指令不必等待 cond 計算完成
  3. retirement:預先執行的指令可能是無效的,要 retire 這些指令,比如分支預測錯誤,修改了 r1,要改回 r3 為原來的值
  4. 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)\) 獲取鎖的開銷:

  1. 第一次 n 個執行緒 CAS,但是 1 個成功
  2. 第二次 n-1 個執行緒 CAS,但是 1 個成功
  3. ……

RCU

以連結串列為例,解決方法是讀者完全不需要鎖,直接讀,但是寫者只是修改資料結構的 node,得到讀者讀完以後,再 commit writing,修改資料結構,free 掉。

規則:

  1. reader 讀資料不允許被 context switch,透過關閉中斷實現,這段程式碼是 reader 的 critical section
  2. writer commit 資料前,要求自己在所有 cpu 上都被 schedule 過,這樣 reader 如果在讀取修改的 node 時候,必須結束讀取。writer 透過 schedule 讓所有 reader 結束對修改的 node 持有,這樣就可以安全地釋放 E2.

23.6 RCU 用例程式碼 | MIT6.S081

相關文章