譯:4. 中斷、韌體與IO
原文地址
:
Interrupts, Firmware and I/O
譯者注:
已經徵得原作者同意,翻譯文件和把翻譯半成品放在知乎上,詳見issue。
對於文章中不理解、難翻譯的部分,我會諮詢作者本人,主要透過issue交流,儘量確保翻譯最基本的準確性。大家如果對翻譯有異議的、對內容有疑問的都可以提出來,自己在github上提issue或者我幫你提。
考慮到第一章中英文混合的方法看起來有點雜亂,所以目前採用的翻譯方式是把中文翻譯獨立出來,放在
xxxx_cn。md
,方便對照。
正文
本書關於計算機科學主題的通識內容已經接近尾聲了。
本部分要將所有的內容串起來,並研究整個計算機作為一個系統是如何處理I/O和併發的。
簡要概述
讓我們來看看從網絡卡中讀取資料的步驟:
免責宣告
此處為簡化內容,實際情況比這要複雜得多,但是我們只挑選我們最感興趣的部分,跳過一些其他內容。
1. 程式碼部分
透過向作業系統發出一個
syscall
,我們申請到一個socket。根據作業系統的不同,我們要麼拿到一個
檔案描述符
(Linux/MacOS)或者
socket
結構體(Windows)。
下一步,告知作業系統,我希望在該socket有任何讀事件發生時,作業系統能通知我。
2. 向作業系統註冊事件
處理方式有3種:
A。 我們告訴作業系統我們對
Read
事件感興趣,但我們希望透過將執行緒的控制權“交給”作業系統來等待該事件的發生。然後,作業系統透過儲存暫存器狀態來掛起我們的執行緒,並切換到其他的執行緒。
從我們的視角(本程式的角度)來看,這個操作會阻塞住我們的執行緒,直到有資料可讀。
B。 我們告訴作業系統我們對
Read
事件感興趣,但是我們只想拿到一個任務的控制代碼,透過
輪詢(poll)
來檢測資料是否已準備完畢。
作業系統不會將我們的執行緒掛起,所以這種方式不會阻塞我們的程式
C。 我們告訴作業系統我們可能對很多事件都感興趣,但是我只想訂閱一個事件佇列。當我們
輪詢
這個佇列時,它會保持阻塞,直到有一個或多個事件發生。
這種方式在我們等待事件發生時,會阻塞執行緒。
我的下一本書將是關於方案C的,因為這是一個非常有趣的處理I/O事件的模型,這對於理解為什麼Rusts的併發抽象模型會以那樣的方式建模是非常重要的。我們在這裡就不詳細介紹了。
3. 網絡卡
在這部分,我們會跳過一些對於理解無關緊要的步驟。
網絡卡上有一個正執行著專用韌體的微控制器。我們可以想象,這個微控制器在執行一個忙迴圈,不斷地輪詢,檢測是否有資料輸入。
網絡卡內部實際處理的方式很可能與上述內容有所差異。最重要的一點就是網絡卡上執行一塊簡單但專用的CPU,會不斷地檢測是否有事件到來。
網絡卡一旦感知到資料傳入,就會發出硬體中斷(Hardware Interrupt)。
4. 硬體中斷
這是一個相當簡化的解釋。如果你對具體實現感興趣,我推薦你去看看Robert Mustacchi‘s這篇給力的文章Turtles on the wire: understanding how the OS uses the modern NIC。
現代CPU有一系列的
中斷請求線Interrupt Request Lines
(外圍裝置透過向IRQ產生訊號來獲得CPU的注意英文解釋)用於處理外圍裝置發出的事件。一個CPU的IRQ數量是固定的。
硬中斷是一種隨時可以發出的電訊號。CPU會立即透過儲存暫存器狀態來
打斷
當前正常的工作流,轉而處理中斷,查詢對應的中斷處理程式。中斷處理函式定義在中斷描述符表(Interrupt Descriptor Table)中。
5. 中斷處理程式
作業系統在中斷描述符表(IDT,Interrupt Descriptor Table) 記錄了可能會發生的不同中斷所對應中斷處理程式。其中的每一項都指向了特定中斷對應的特定處理程式。網絡卡的中斷處理程式通常是由其驅動註冊和執行的。
IDT並不像圖中那樣儲存在CPU上。它位於主存中一個固定且已知的位置。CPU只在其中一個暫存器中持有指向表的指標。
6. 寫入資料
取決於CPU和網絡卡韌體,這一步可能會有很大的不同。如果網絡卡和CPU支援直接支援訪問(Direct Memory Access) (應該是所有現代作業系統的標配),網絡卡會直接將資料寫入到作業系統在記憶體中已經建立好的一系列緩衝區中。
在這樣的系統中,當有資料已經被寫入記憶體時,網絡卡的
韌體
可能才會發出一個
中斷
訊號。
DMA
的寫入效率非常高,因為資料已經在記憶體中準備好後作業系統才會收到通知。而在相對較老的系統中,CPU處理網絡卡發來的資料是需要一定的資源開銷的。
支援DMA的系統需要新增DMAC(Direct Memory Access Controller)以控制記憶體的訪問。DMAC並不是CPU的一部分,這點和上圖有出入。我們已經講得夠深了,而且這點也不是很重要,所以我們繼續下一部分。
7. 驅動部分
通常來說,
驅動
負責處理作業系統和網絡卡之間的通訊任務。
在某一時刻,緩衝區被填充完畢,網絡卡就會發出一個
中斷
訊號。接著,CPU會跳轉到該中斷對應的處理程式。這個特定型別的中斷所對應的處理程式由驅動程式所註冊的,所以實際上是驅動程式處理事件,再轉而去通知核心:資料已經準備完畢,等待讀取。
8. 讀取資料
(此時資料已經準備完畢)取決於我們選擇選項A、B還是C,作業系統會:
喚醒(掛起的)執行緒
在下次
輪詢(poll)
時,返回
Ready
。
喚醒執行緒,返回一個
Read
事件,也就是之前我們在socket控制代碼上註冊監聽的
Read
事件。
中斷
正如我上面提到的,有兩種型別的中斷:
硬中斷
軟中斷
二者在本質上有很大的區別。
硬中斷
CPU本身有兩條中斷請求線:非遮蔽中斷和可遮蔽中斷線。當這兩條線上收到中斷請求的電訊號而引起的中斷,則產生了硬中斷。硬體線路直接通知CPU。
軟中斷
這類中斷是透過軟體而非硬體層面觸發的。和硬體中斷一樣,CPU會跳轉到中斷描述符表,然後執行特定中斷所對應的中斷處理程式。
韌體
大多數人不會關注韌體。然而,韌體其實是我們所生活的世界不可或缺的一部分。韌體執行在各式各樣的硬體上,有著各種稀奇古怪的工作方式,組成了我們每天接觸的計算機。
每次一提到韌體,我的腦海裡總會浮現出星球大戰裡的場景:他們和各種奇怪、模糊的生物一同走進酒吧。我認為這就是韌體的世界,而很少有人知道它們是做什麼的或者在一個特定的系統上是如何工作的。
現如今,韌體需要配合微控制器或者類似的設施以才能正常工作。即使是CPU,也需要韌體的支援才能運作。這就意味著,一個硬體系統所承載的小型“CPU”比我們實際程式設計所面對的CPU的核心數量要多得多。
為什麼這很重要?你還記得併發就是追求效率的,對吧?既然我們的系統上有這麼多的CPU為我們幹活,那麼我們在編碼時需要注意的一點就是不要重複那些CPU已經幫我們處理好的工作了。
如果網絡卡的韌體能夠持續不斷地檢測是否有新的資料到達,那麼我們完全沒有必要再讓CPU浪費時間去執行同樣的任務了。比較好的方式是每隔一段時間檢測一次,更好的方式是在資料到達時通知CPU。