作業系統思考 第八章 多工

飛龍發表於2019-05-12

第八章 多工

作者:Allen B. Downey

原文:Chapter 8 Multitasking

譯者:飛龍

協議:CC BY-NC-SA 4.0

在當前的許多系統上,CPU包含多個核心,也就是說它可以同時執行多個程式。而且,每個核心都具有“多工”的能力,也就是說它可以從一個程式快速切換到另一個程式,創造出同時執行許多程式的幻象。

作業系統中,實現多工的這部分叫做“核心”。在堅果或者種子中,核心是最內層的部分,由外殼所包圍。在作業系統各種,核心是軟體的最底層,由一些其它層包圍,包括稱為“Shell”的介面。電腦科學家喜歡引喻。

究其本質,核心的工作就是處理中斷。“中斷”是一個事件,它會停止通常的指令週期,並且使執行流跳到稱為“中斷處理器”的特殊程式碼區域內。

當一個裝置向CPU傳送訊號時,會發生硬體中斷。例如,網路裝置可能在資料包到達時會產生中斷,或者磁碟驅動器會在資料傳送完成時產生中斷。多數系統也帶有以固定週期產生中斷的計時器。

軟體中斷由執行中的程式所產生。例如,如果一條指令由於某種原因沒有完成,可能就會觸發中斷,便於這種情況可被作業系統處理。一些浮點數的錯誤,例如除零錯誤,會由中斷處理。

當程式需要訪問硬體裝置時,會進行“系統呼叫”,它就像函式呼叫,除了並非跳到函式的起始位置,而是執行一條特殊的指令來觸發中斷,使執行流跳到核心中。核心讀取系統呼叫的引數,執行所請求的操作,之後使被中斷程式恢復執行。

8.1 硬體狀態

中斷的處理需要硬體和軟體的配合。當中斷髮生時,CPU上可能正在執行多條指令,暫存器中也儲存著資料。

通常硬體負責將CPU設為一致狀態。例如,每條指令應該執行完畢,或者完全沒有執行,不應該出現執行到一半的指令。而且,硬體也負責儲存程式計數器(PC),便於核心瞭解從哪裡恢復執行。

之後,中斷處理器通常負責儲存暫存器的上下文。為了完成工作,核心需要執行指令,這會修改資料暫存器和位暫存器。所以這個“硬體狀態”需要在修改之前儲存,並在被中斷的程式恢復執行後復原。

下面是這一系列事件的大致過程:

  1. 當中斷髮生時,硬體將程式計數器儲存到一個特殊的暫存器中,並且跳到合適的中斷處理器。

  2. 中斷處理器將程式計數器和位暫存器,以及任何打算使用的資料暫存器的內容儲存到記憶體中。

  3. 中斷處理器執行處理中斷所需的程式碼。

  4. 之後它復原所儲存暫存器的內容。最後,復原被中斷程式的程式計數器,這會跳回到被中斷的程式。

如果這一機制正常工作,被中斷程式通常沒有辦法知道這是一箇中斷,除非它檢測到了指令間的小變化。

8.2 上下文切換

中斷處理器非常快,因為它們不需要儲存整個硬體狀態。它們只需要儲存打算使用的暫存器。

但是當中斷髮生時,核心並不總會恢復被中斷的程式。它可以選擇切換到其它程式,這種機制叫做“上下文切換”。

通常,核心並不知道一個程式會用到哪個暫存器,所以需要全部儲存。而且,當它切換到新的程式時,它可能需要清除儲存在記憶體管理單元(MMU)中的資料。以及在上下文切換之後,它可能要花費一些時間,為新的程式將資料載入到快取中。 出於這些因素,上下文切換相對較慢,大約是幾千個週期或幾毫秒。

在多工的系統中,每個程式都允許執行一小段時間,叫做“時間片”或“quantum”。在上下文切換的過程中,核心會設定一些硬體計數器,它們會在時間片的末尾產生中斷。當中斷髮生時,核心可以切換到另一個程式,或者允許被中斷的程式繼續執行。作業系統中做決策的這一部分叫做“排程器”。

8.3 程式的生命週期

當程式被建立時,作業系統會為程式分配包含程式資訊的資料結構,稱為“程式控制塊”(PCB)。在其它方面,PCB跟蹤程式的狀態,這包括:

  • 執行(Running),如果程式正在執行於某個核心上。

  • 就緒(Ready),如果程式可以但沒有執行,通常由於就緒程式數量大於核心的數量。

  • 阻塞(Blocked),如果程式由於正在等待未來的事件,例如網路通訊或磁碟讀取,而不能執行。

  • 終止(Done):如果程式執行完畢,但是帶有沒有讀取的退出狀態資訊。

下面是一些可導致程式狀態轉換的事件:

  • 一個程式在執行中的程式執行類似於fork的系統呼叫時誕生。在系統呼叫的末尾,新的程式通常就緒。之後排程器可能恢復原有的程式(“父程式”),或者啟動新的程式(“子程式”)。

  • 當一個程式由排程器啟動或恢復時,它的狀態從就緒變為執行。

  • 當一個程式被中斷,並且排程器沒有選擇使它恢復,它的狀態從執行變成就緒。

  • 如果一個程式執行不能立即完成的系統呼叫,例如磁碟請求,它會變為阻塞,並且排程器會選擇另一個程式。

  • 當類似於磁碟請求的操作完成時,會產生中斷。中斷處理器弄清楚哪個程式正在等待請求,並將它的狀態從阻塞變為就緒。

  • 當一個程式呼叫exit時,中斷處理器在PCB中儲存退出程式碼,並將程式的狀態變為終止。

8.4 排程

就像我們在2.3節中看到的那樣,一臺計算機上可能執行著成百上千條程式,但是通常大多數程式都是阻塞的。大多數情況下,只有一小部分程式是就緒或者執行的。當中斷髮生時,排程器會決定那個程式應啟動或恢復。

在工作站或筆記本上,排程器的首要目標就是最小化響應時間,也就是說,計算機應該快速響應使用者的操作。響應時間在伺服器上也很重要,但是排程器同時也可能嘗試最大化吞吐量,它是單位時間內所完成的請求。

排程器通常不需要關於程式所做事情的大量資訊,所以它基於一些啟發來做決策:

  • 程式可能被不同的資源限制。執行大量計算的程式是計算密集的,也就是說它的執行時間取決於得到了多少CPU時間。從網路或磁碟讀取資料的程式是IO密集的,也就是說如果資料輸入和輸出更快的話,它就會更快,但是在更多CPU時間下它不會執行得更快。最後,與使用者互動的程式,在大多數時間裡可能都是阻塞的,用於等待使用者的動作。作業系統有時可以將程式基於它們過去的行為分類,並做出相應的排程。例如,當一個互動型程式不再被阻塞,應該馬上執行,因為使用者可能正在等待回應。另一方面,已經執行了很長時間的CPU密集的程式可能就不是時間敏感的。

  • 如果一個程式可能會執行較短的時間,之後發出了阻塞的請求,它可能應該立即執行,出於兩個原因:(1)如果請求需要一些時間來完成,我們應該儘快啟動它,(2)長時間執行的程式應該等待短時間的程式,而不是反過來。作為類比,假設你在做蘋果餡餅。麵包皮需要5分鐘來準備,但是之後需要半個小時的冷卻。而餡料需要20分鐘來準備。如果你首先準備麵包皮,你可以在其冷卻時準備餡料,並且可以在35分鐘之內做完。如果你先準備餡料,就會花費55分鐘。

大多數排程器使用一些基於優先順序的排程形式,其中每個程式都有可以調上或調下的優先順序。當排程器執行時,它會選擇最高優先順序的就緒程式。

下面是決定程式優先順序的一些因素:

  • 具有較高優先順序的程式通常執行較快。

  • 如果一個程式在時間片結束之前發出請求並被阻塞,就可能是IO密集型程式或互動型程式,優先順序應該升高。

  • 如果一個程式在整個時間片中都執行,就可能是長時間執行的計算密集型程式,優先順序應該降低。

  • 如果一個任務長時間被阻塞,之後變為就緒,它應該提升為最高優先順序,便於響應所等待的東西。

  • 如果程式A在等待程式B的過程中被阻塞,例如,如果它們由管道連線,程式B的優先順序應升高。

  • 系統呼叫nice允許程式降低(但不能升高)自己的優先順序,並允許程式設計師向排程器傳遞顯式的資訊。

對於執行普通工作負載的多數系統,排程演算法對效能並沒有顯著的影響。簡單的排程策略就足夠好了。

8.5 實時排程

但是,對於與真實世界互動的程式,排程非常重要。例如,從感測器和控制馬達讀取資料的程式,可能需要以最小的頻率完成重複的任務,並且以最大的響應時間對外界事件做出反應。這些需求通常表述為必須在“截止期限”之前完成的“任務”。

排程滿足截止期限的任務叫做“實時排程”。對於一些應用,類似於Linux的通用作業系統可以被修改來處理實時排程。這些修改可能包括:

  • 為控制任務的優先順序提供更豐富的API。

  • 修改排程器來確保最高優先順序的程式在固定時間內執行。

  • 重新組織中斷處理器來保證最大完成時間。

  • 修改鎖和其它同步機制(下一章會講到),允許高優先順序的任務預先佔用低優先順序的任務。

  • 選擇保證最大完成時間的動態記憶體分配實現。

對於更苛刻的應用,尤其是實時響應是生死攸關的領域,“實時作業系統”提供了專用能力,通常比通用作業系統擁有更簡單的設計。

相關文章