6.s081 中斷
中斷和裝置驅動
產生中斷的硬體
中斷是trap
的一種, 當裝置需要得到OS注意就會發出中斷並呼叫驅動的中斷處理器(devintr
函式).
中斷與其它型別的trap
不同點:
- asynchronous. 中斷處理器與當前執行的程序在CPU上沒有關聯.
- concurrency. CPU和生成中斷的裝置並行執行.
- program device. 產生中斷的裝置需要被程式設計(每個裝置都有一個程式設計手冊), 包含裝置有什麼樣的暫存器, 可以執行什麼操作等.
本節課主要關注外部裝置的中斷. 外設中斷來自主機板上的裝置. 例如UART(UART會連線到電腦和顯示器), 該裝置對映到實體地址0x10000000(UART0
所有裝置連線在處理器上, 處理器通過PLIC(platform level interrupt control)管理外設的中斷.
左上角有53個來自裝置的中斷, 這些中斷到達PLIC之後, PLIC路由這些中斷到右邊的CPU核. 如果所有核都在忙, 就保留中斷, 直到有CPU核忙完來處理中斷.
- PLIC通知有一個帶處理的中斷.
- 其中一個CPU核接收.
- CPu核處理完後通知PLIC.
- PLIC不再儲存中斷資訊.
裝置驅動
驅動: os中的程式碼, 管理一個特定的裝置(配置裝置硬體, 告訴裝置執行哪些操作, 處理中斷結果, 和等待裝置完成的程序互動). 驅動分成兩個部分: bottom/top.
- bottom: 在中斷時執行, 通常是interrupt handler. 當中斷傳送到CPU, CPU接收這個中斷, 會呼叫interrupt handler來處理中斷(不執行在程序的上下文中). 由於不執行在程序的context, 程序的page table不知道向哪個地址讀寫資料.
- top: 在程序的核心執行緒執行. 通過像
read/write
這種系統呼叫被呼叫. top會要求硬體開始操作, 然後等待操作結束. 當裝置操作結束後產生中斷, interrupt handler就會喚醒等待中的程序然後告訴裝置進行下一輪工作.
驅動中有一些buffer, top程式碼可以從buffer讀寫資料, interrupt handler也可以向buffer讀寫資料. 從而將裝置和CPU解耦開.
對裝置的程式設計是通過memory mapped I/O完成. 裝置的地址在實體地址的固定處, 從而os知道這些位置, 然後通過load/store
(控制暫存器)對這些地址程式設計.
UART是16550型號, 通過這個裝置來與鍵盤和console互動.
- 控制暫存器000(
RHR
和THR
), 如果寫他會將資料寫入暫存器中並傳輸到其他地方, 如果讀它就可以讀出儲存在暫存器中的內容, UART可以通過串列埠傳送資料bit, 線上路另一端會有另一個UART, 能將bit組合成一個byte. 每次暫存器中的字元被讀取, UART就會從buffer中刪除並且當buffer為空時, 清除LSR
中的ready位. 同理通過load
將資料寫入這個暫存器中, UART會通過串列埠線將這個byte送出, 送出後, UART會發送中斷給核心, 這時候才能再次寫入下一個資料. - 控制暫存器001(
IER
), 通過它來控制UART是否產生中斷. 一個暫存器中的每一bit都有不同作用.IER
中的bit0-bit3分別控制了不同的中斷. - 控制暫存器010(
ISR
), 包含幾個位來表明中斷狀態. 是否輸入的字元等待被程序讀取.
設定中斷
xv6啟動時, shell會輸出“$ “, 當我們在鍵盤上輸入ls, 最終可以看到“$ ls”. “$ ”是shell程式的輸出, “ls”是鍵盤輸入後顯示出來的.
- 對於“$ ”, 裝置將字元傳輸給UART的暫存器, UART在傳送後生成一箇中斷, 在傳送的另一端也有一個UART, 這個UART連線console, 會進一步將“$ ”顯示在console上.
- 對於“ls”, 鍵盤連線到UART輸入, 當按下一個鍵, UART會將按鍵字元通過序列口傳送給另一端的UART, 另一端的UART會將bit組合成一個byte, 再產生一箇中斷, 並告訴處理器有一個來自鍵盤的字元, 之後interrupt handler會處理這個字元.
中斷相關暫存器:
-
SIE(supervisor interrupt enable). E位專門針對外設的中斷, S位專門針對軟體中斷, T位專門針對定時器的中斷.
-
SSTATUS(supervisor status). 有一個bit來開啟或關閉中斷. 每個CPU核有獨立的SIE和SSTATUS.
-
SIP(supervisor interrupt pending). 可以檢視發生的中斷是什麼型別的.
-
SCAUSE.表明中斷的原因.
-
STVEC. 儲存trap時CPU執行使用者程式的pc, 為之後恢復程式執行做準備.
start
函式將中斷和異常設定到supervisor mode, 然後設定SIE來接收中斷(E, S和T), 最後初始化定時器.
main
函式呼叫一系列函式初始化.
先呼叫consoleinit
來初始化UART. consoleinit
呼叫uartinit
.
uartinit
配置了UART晶片. 關閉中斷, 設定字元長度為8bit重置FIFO, 最後再開啟中斷.
main
中還會呼叫plicinit
和plicinithart
, PLIC實體地址為0xC0000000. plicinit由0號CPU執行, 之後每個CPU核都需要呼叫plicinithart
來表明對哪些中斷感興趣.
這時配置好了外部裝置, 並有PLIC來傳遞中斷. 接下來需要設定CPU來接收中斷. scheduler
會排程程序, 在執行程序前會執行intr_on
來使CPU接收中斷(設定SSTATUS, 開啟中斷標誌位).
UART驅動的top部分
shell輸出“$ ”到console
. 在init.c
的main
函式, 是啟動後的第一個程序.
先通過mknod
建立了console裝置, 之後通過dup
建立stdout
和stderr
. 最終檔案描述符0, 1, 2都代表console. 然後執行sh
程式. 之後shell向檔案描述符列印“$ ”
由於裝置是由檔案來表示的, 所以shell並不知道2對應了什麼. shell輸出的每個字元都會觸發write
系統呼叫, wirte
系統呼叫會走到sysfile.c
中的sys_write
.
sys_write
呼叫filewrite
, filewrite
會先判斷檔案描述符的型別, 當為FD_DEVICE
時, 會為特定裝置執行write
函式, 當前裝置為console, 所以呼叫console.c
的consolewrite
函式.
consolewrite
通過either_copyin
將字元拷入, 在呼叫uartputc
函式.
uartputc
將字元寫入UART裝置, 所以consolewrite
可以看成是UART的top部分. UART內部有一個buffer, 大小為32個字元, 還有一個為consumer提供的讀指標和一個為producer提供的寫指標, buffer是環形的.
shell是producer, 所以呼叫uartputc函式. 函式先判定buffer是否滿, 滿了就sleep, 使cpu出讓. 否則就將字元寫入buffer, 更新指標, 再呼叫uartstart
.
uartstart
先檢查當前裝置是否空閒並且buffer非空, 如果是, 就從buffer中讀出資料寫入THR
傳送暫存器告訴裝置需要傳送位元組. 然後返回到shell(與trap一樣). 這樣UART可以同時將資料發出, 並中斷.
UART驅動的bottom部分
假設鍵盤生成了一箇中斷髮送給PLIC, PLIC路由給CPU, 那麼:
-
清除SIE的bit(阻止CPU核被其他中斷打擾), 處理完後在恢復SIE相應bit.
-
設定SEPC為當前pc.
-
儲存當前mode.
-
將mode設為supervisor mode.
-
將
pc
設為STVEC的
值(trap處理程式地址). 當中斷時程式執行在使用者空間是,STVEC
儲存的是uservec
函式地址, 當執行在核心空間時, 儲存著kernelvec
函式地址. -
uservec
會呼叫usertrap
函式.usertrap
呼叫devintr
. -
devintr
先通過SCAUSE
暫存器判定單曲是否是外設中斷. 如果是的話呼叫plic_claim
來獲取中斷. -
plic_claim
會找到CPU核來處理中斷並返回中斷號. -
然後
devintr
通過返回的中斷號來判斷是否是UART中斷, 如果是就呼叫uartintr
. -
uartintr
會從UART的接收暫存器讀取資料, 之後將獲取到的資料傳遞給consoleintr
函式. 當前為空. 所以執行uartstart
函式. -
uartstart
會將shell儲存在buffer的字元輸出(“$”). -
這樣top和bottom就解耦開了.
併發
中斷的併發包括:
- 裝置和CPU是並行的. 當UART向console傳送字元是, CPU會返回到shell, 但shell可能會執行系統呼叫, 向buffer中再寫入字元, 這叫做producer-consumer並行.
- 中斷會停止當前執行的程式. 使用者程式沒問題, 之後從中斷返回後會繼續執行停止的指令. 但核心也被中斷打斷, 對於一些程式碼, 如果不能在執行期間被中斷, 核心需要臨時關閉中斷來保證程式碼的原子性.
- 驅動的top和bottom是並行執行的. 可能會在某一時刻有兩個核同時對buffer進行讀寫操作.
中斷的演進
由於中斷相對於處理器來說很慢, 所以在產生中斷之前, 裝置上會執行大量的操作(非同步).
- 對於快裝置, 產生中斷的頻率非常高, 超過了CPU的處理能力. 可以使用polling, CPU一直讀取外設的控制暫存器來檢查是否有資料, CPU不停輪詢裝置直到裝置有了資料.
- 對於慢裝置, 這種方法浪費了CPU cycles, 因為在CPU輪詢裝置時, 沒有用CPU執行任何程式, 所以可以在裝置沒有資料時切換出來執行一些其他程式.