一篇文章帶你「重新認識」線程上下文切換怎麼玩兒

調度

當一個計算機是多道程序設計系統時,會頻繁的有很多進程或者線程來同時競爭 CPU 時間片。當兩個或兩個以上的進程/線程處於就緒狀態時,就會發生這種情況。如果只有一個 CPU 可用,那麼必須選擇接下來哪個進程/線程可以運行。操作系統中有一個叫做 調度程序(scheduler) 的角色存在,它就是做這件事兒的,該程序使用的算法叫做 調度算法(scheduling algorithm)

儘管有一些不同,但許多適用於進程調度的處理方法同樣也適用於線程調度。當內核管理線程的時候,調度通常會以線程級別發生,很少或者根本不會考慮線程屬於哪個進程。下面我們會首先專注於進程和線程的調度問題,然後會明確的介紹線程調度以及它產生的問題。

調度介紹

讓我們回到早期以磁帶上的卡片作爲輸入的批處理系統的時代,那時候的調度算法非常簡單:依次運行磁帶上的每一個作業。對於多道程序設計系統,會複雜一些,因爲通常會有多個用戶在等待服務。一些大型機仍然將 批處理分時服務結合使用,需要調度程序決定下一個運行的是一個批處理作業還是終端上的用戶。由於在這些機器中 CPU 是稀缺資源,所以好的調度程序可以在提高性能和用戶的滿意度方面取得很大的成果。

進程行爲

幾乎所有的進程(磁盤或網絡)I/O 請求和計算都是交替運行的

如上圖所示,CPU 不停頓的運行一段時間,然後發出一個系統調用等待 I/O 讀寫文件。完成系統調用後,CPU 又開始計算,直到它需要讀更多的數據或者寫入更多的數據爲止。當一個進程等待外部設備完成工作而被阻塞時,纔是 I/O 活動。

上面 a 是 CPU 密集型進程;b 是 I/O 密集型進程進程,a 因爲在計算的時間上花費時間更長,因此稱爲計算密集型(compute-bound) 或者 CPU 密集型(CPU-bound),b 因爲I/O 發生頻率比較快因此稱爲 I/O 密集型(I/O-bound)。計算密集型進程有較長的 CPU 集中使用和較小頻度的 I/O 等待。I/O 密集型進程有較短的 CPU 使用時間和較頻繁的 I/O 等待。注意到上面兩種進程的區分關鍵在於 CPU 的時間佔用而不是 I/O 的時間佔用。I/O 密集型的原因是因爲它們沒有在 I/O 之間花費更多的計算、而不是 I/O 請求時間特別長。無論數據到達後需要花費多少時間,它們都需要花費相同的時間來發出讀取磁盤塊的硬件請求。

值得注意的是,隨着 CPU 的速度越來越快,更多的進程傾向於 I/O 密集型。這種情況出現的原因是 CPU 速度的提升要遠遠高於硬盤。這種情況導致的結果是,未來對 I/O 密集型進程的調度處理似乎更爲重要。這裏的基本思想是,如果需要運行 I/O 密集型進程,那麼就應該讓它儘快得到機會,以便發出磁盤請求並保持磁盤始終忙碌。

何時調度

第一個和調度有關的問題是何時進行調度決策。存在着需要調度處理的各種情形。首先,在創建一個新進程後,需要決定是運行父進程還是子進程。因爲二者的進程都處於就緒態下,這是正常的調度決策,可以任意選擇,也就是說,調度程序可以任意的選擇子進程或父進程開始運行。

第二,在進程退出時需要作出調度決定。因爲此進程不再運行(因爲它將不再存在),因此必須從就緒進程中選擇其他進程運行。如果沒有進程處於就緒態,系統提供的空閒進程通常會運行

什麼是空閒進程

空閒進程(system-supplied idle process) 是 Microsoft 公司 windows 操作系統帶有的系統進程,該進程是在各個處理器上運行的單個線程,它唯一的任務是在系統沒有處理其他線程時佔用處理器時間。System Idle Process 並不是一個真正的進程,它是核心虛擬出來的,多任務操作系統都存在。在沒有可用的進程時,系統處於空運行狀態,此時就是System Idle Process 在正在運行。你可以簡單的理解成,它代表的是 CPU 的空閒狀態,數值越大代表處理器越空閒,可以通過 Windows 任務管理器查看 Windows 中的 CPU 利用率

第三種情況是,當進程阻塞在 I/O 、信號量或其他原因時,必須選擇另外一個進程來運行。有時,阻塞的原因會成爲選擇進程運行的關鍵因素。例如,如果 A 是一個重要進程,並且它正在等待 B 退出關鍵區域,讓 B 退出關鍵區域從而使 A 得以運行。但是調度程序一般不會對這種情況進行考量。

第四點,當 I/O 中斷髮生時,可以做出調度決策。如果中斷來自 I/O 設備,而 I/O 設備已經完成了其工作,那麼那些等待 I/O 的進程現在可以繼續運行。由調度程序來決定是否準備運行新的進程還是重新運行已經中斷的進程。

如果硬件時鐘以 50 或 60 Hz 或其他頻率提供週期性中斷,可以在每個時鐘中斷或第 k 個時鐘中斷處做出調度決策。根據如何處理時鐘中斷可以把調度算法可以分爲兩類。非搶佔式(nonpreemptive) 調度算法挑選一個進程,讓該進程運行直到被阻塞(阻塞在 I/O 上或等待另一個進程),或者直到該進程自動釋放 CPU。即使該進程運行了若干個小時後,它也不會被強制掛起。這樣會在時鐘中斷髮生時不會進行調度。在處理完時鐘中斷後,如果沒有更高優先級的進程等待,則被中斷的進程會繼續執行。

另外一種情況是 搶佔式 調度算法,它會選擇一個進程,並使其在最大固定時間內運行。如果在時間間隔結束後仍在運行,這個進程會被掛起,調度程序會選擇其他進程來運行(前提是存在就緒進程)。進行搶佔式調度需要在時間間隔結束時發生時鐘中斷,以將 CPU 的控制權交還給調度程序。如果沒有可用的時鐘,那麼非搶佔式就是唯一的選擇。

調度算法的分類

毫無疑問,不同的環境下需要不同的調度算法。之所以出現這種情況,是因爲不同的應用程序和不同的操作系統有不同的目標。也就是說,在不同的系統中,調度程序的優化也是不同的。這裏有必要劃分出三種環境

  • 批處理(Batch)
  • 交互式(Interactive)
  • 實時(Real time)

批處理系統廣泛應用於商業領域,比如用來處理工資單、存貨清單、賬目收入、賬目支出、利息計算、索賠處理和其他週期性作業。在批處理系統中,一般會選擇使用非搶佔式算法或者週期性比較長的搶佔式算法。這種方法可以減少線程切換因此能夠提升性能。

在交互式用戶環境中,爲了避免一個進程霸佔 CPU 拒絕爲其他進程服務,所以需要搶佔式算法。即使沒有進程有意要一直運行下去,但是,由於某個進程出現錯誤也有可能無限期的排斥其他所有進程。爲了避免這種情況,搶佔式也是必須的。服務器也屬於此類別,因爲它們通常爲多個(遠程)用戶提供服務,而這些用戶都非常着急。計算機用戶總是很忙。

在實時系統中,搶佔有時是不需要的,因爲進程知道自己可能運行不了很長時間,通常很快的做完自己的工作並阻塞。實時系統與交互式系統的差別是,實時系統只運行那些用來推進現有應用的程序,而交互式系統是通用的,它可以運行任意的非協作甚至是有惡意的程序。

調度算法的目標

爲了設計調度算法,有必要考慮一下什麼是好的調度算法。有一些目標取決於環境(批處理、交互式或者實時)蛋大部分是適用於所有情況的,下面是一些需要考量的因素,我們會在下面一起討論。

所有系統

在所有的情況中,公平是很重要的。對一個進程給予相較於其他等價的進程更多的 CPU 時間片對其他進程來說是不公平的。當然,不同類型的進程可以採用不同的處理方式。

與公平有關的是系統的強制執行,什麼意思呢?如果某公司的薪資發放系統計劃在本月的15號,那麼碰上了疫情大家生活都很拮据,此時老闆說要在14號晚上發放薪資,那麼調度程序必須強制使進程執行 14 號晚上發放薪資的策略。

另一個共同的目標是保持系統的所有部分儘可能的忙碌。如果 CPU 和所有的 I/O 設備能夠一直運行,那麼相對於讓某些部件空轉而言,每秒鐘就可以完成更多的工作。例如,在批處理系統中,調度程序控制哪個作業調入內存運行。在內存中既有一些 CPU 密集型進程又有一些 I/O 密集型進程是一個比較好的想法,好於先調入和運行所有的 CPU 密集型作業,然後在它們完成之後再調入和運行所有 I/O 密集型作業的做法。使用後者這種方式會在 CPU 密集型進程啓動後,爭奪 CPU ,而磁盤卻在空轉,而當 I/O 密集型進程啓動後,它們又要爲磁盤而競爭,CPU 卻又在空轉。。。。。。顯然,通過結合 I/O 密集型和 CPU 密集型,能夠使整個系統運行更流暢,效率更高。

批處理系統

通常有三個指標來衡量系統工作狀態:吞吐量、週轉時間和 CPU 利用率吞吐量(throughout) 是系統每小時完成的作業數量。綜合考慮,每小時完成 50 個工作要比每小時完成 40 個工作好。週轉時間(Turnaround time) 是一種平均時間,它指的是從一個批處理提交開始直到作業完成時刻爲止平均時間。該數據度量了用戶要得到輸出所需的平均等待時間。週轉時間越小越好。

CPU 利用率(CPU utilization) 通常作爲批處理系統上的指標。即使如此, CPU 利用率也不是一個好的度量指標,真正有價值的衡量指標是系統每小時可以完成多少作業(吞吐量),以及完成作業需要多長時間(週轉時間)。把 CPU 利用率作爲度量指標,就像是引擎每小時轉動了多少次來比較汽車的性能一樣。而且知道 CPU 的利用率什麼時候接近 100% 要比什麼什麼時候要求得到更多的計算能力要有用。

交互式系統

對於交互式系統,則有不同的指標。最重要的是儘量減少響應時間。這個時間說的是從執行指令開始到得到結果的時間。再有後臺進程運行(例如,從網絡上讀取和保存 E-mail 文件)的個人計算機上,用戶請求啓動一個程序或打開一個文件應該優先於後臺的工作。能夠讓所有的交互式請求首先運行的就是一個好的服務。

一個相關的問題是 均衡性(proportionality),用戶對做一件事情需要多長時間總是有一種固定(不過通常不正確)的看法。當認爲一個請求很複雜需要較多時間時,用戶會認爲很正常並且可以接受,但是一個很簡單的程序卻花費了很長的運行時間,用戶就會很惱怒。可以拿彩印和複印來舉出一個簡單的例子,彩印可能需要1分鐘的時間,但是用戶覺得複雜並且願意等待一分鐘,相反,複印很簡單只需要 5 秒鐘,但是複印機花費 1 分鐘卻沒有完成複印操作,用戶就會很焦躁。

實時系統

實時系統則有着和交互式系統不同的考量因素,因此也就有不同的調度目標。實時系統的特點是必須滿足最後的截止時間。例如,如果計算機控制着以固定速率產生數據的設備,未能按時運行的話可能會導致數據丟失。因此,實時系統中最重要的需求是滿足所有(或大多數)時間期限。

在一些實事系統中,特別是涉及到多媒體的,可預測性很重要。偶爾不能滿足最後的截止時間不重要,但是如果音頻多媒體運行不穩定,聲音質量會持續惡化。視頻也會造成問題,但是耳朵要比眼睛敏感很多。爲了避免這些問題,進程調度必須能夠高度可預測的而且是有規律的。

批處理中的調度

現在讓我們把目光從一般性的調度轉換爲特定的調度算法。下面我們會探討在批處理中的調度。

先來先服務

很像是先到先得。。。可能最簡單的非搶佔式調度算法的設計就是 先來先服務(first-come,first-serverd)。使用此算法,將按照請求順序爲進程分配 CPU。最基本的,會有一個就緒進程的等待隊列。當第一個任務從外部進入系統時,將會立即啓動並允許運行任意長的時間。它不會因爲運行時間太長而中斷。當其他作業進入時,它們排到就緒隊列尾部。當正在運行的進程阻塞,處於等待隊列的第一個進程就開始運行。當一個阻塞的進程重新處於就緒態時,它會像一個新到達的任務,會排在隊列的末尾,即排在所有進程最後。

這個算法的強大之處在於易於理解和編程,在這個算法中,一個單鏈表記錄了所有就緒進程。要選取一個進程運行,只要從該隊列的頭部移走一個進程即可;要添加一個新的作業或者阻塞一個進程,只要把這個作業或進程附加在隊列的末尾即可。這是很簡單的一種實現。

不過,先來先服務也是有缺點的,那就是沒有優先級的關係,試想一下,如果有 100 個 I/O 進程正在排隊,第 101 個是一個 CPU 密集型進程,那豈不是需要等 100 個 I/O 進程運行完畢纔會等到一個 CPU 密集型進程運行,這在實際情況下根本不可能,所以需要優先級或者搶佔式進程的出現來優先選擇重要的進程運行。

最短作業優先

批處理中,第二種調度算法是 最短作業優先(Shortest Job First),我們假設運行時間已知。例如,一家保險公司,因爲每天要做類似的工作,所以人們可以相當精確地預測處理 1000 個索賠的一批作業需要多長時間。當輸入隊列中有若干個同等重要的作業被啓動時,調度程序應使用最短優先作業算法

如上圖 a 所示,這裏有 4 個作業 A、B、C、D ,運行時間分別爲 8、4、4、4 分鐘。若按圖中的次序運行,則 A 的週轉時間爲 8 分鐘,B 爲 12 分鐘,C 爲 16 分鐘,D 爲 20 分鐘,平均時間內爲 14 分鐘。

現在考慮使用最短作業優先算法運行 4 個作業,如上圖 b 所示,目前的週轉時間分別爲 4、8、12、20,平均爲 11 分鐘,可以證明最短作業優先是最優的。考慮有 4 個作業的情況,其運行時間分別爲 a、b、c、d。第一個作業在時間 a 結束,第二個在時間 a + b 結束,以此類推。平均週轉時間爲 (4a + 3b + 2c + d) / 4 。顯然 a 對平均值的影響最大,所以 a 應該是最短優先作業,其次是 b,然後是 c ,最後是 d 它就只能影響自己的週轉時間了。

需要注意的是,在所有的進程都可以運行的情況下,最短作業優先的算法纔是最優的。

最短剩餘時間優先

最短作業優先的搶佔式版本被稱作爲 最短剩餘時間優先(Shortest Remaining Time Next) 算法。使用這個算法,調度程序總是選擇剩餘運行時間最短的那個進程運行。當一個新作業到達時,其整個時間同當前進程的剩餘時間做比較。如果新的進程比當前運行進程需要更少的時間,當前進程就被掛起,而運行新的進程。這種方式能夠使短期作業獲得良好的服務。

交互式系統中的調度

交互式系統中在個人計算機、服務器和其他系統中都是很常用的,所以有必要來探討一下交互式調度

輪詢調度

一種最古老、最簡單、最公平並且最廣泛使用的算法就是 輪詢算法(round-robin)。每個進程都會被分配一個時間段,稱爲時間片(quantum),在這個時間片內允許進程運行。如果時間片結束時進程還在運行的話,則搶佔一個 CPU 並將其分配給另一個進程。如果進程在時間片結束前阻塞或結束,則 CPU 立即進行切換。輪詢算法比較容易實現。調度程序所做的就是維護一個可運行進程的列表,就像下圖中的 a,當一個進程用完時間片後就被移到隊列的末尾,就像下圖的 b。

時間片輪詢調度中唯一有意思的一點就是時間片的長度。從一個進程切換到另一個進程需要一定的時間進行管理處理,包括保存寄存器的值和內存映射、更新不同的表格和列表、清除和重新調入內存高速緩存等。這種切換稱作 進程間切換(process switch)上下文切換(context switch)。如果進程間的切換時間需要 1ms,其中包括內存映射、清除和重新調入高速緩存等,再假設時間片設爲 4 ms,那麼 CPU 在做完 4 ms 有用的工作之後,CPU 將花費 1 ms 來進行進程間的切換。因此,CPU 的時間片會浪費 20% 的時間在管理開銷上。耗費巨大。

爲了提高 CPU 的效率,我們把時間片設置爲 100 ms。現在時間的浪費只有 1%。但是考慮會發現下面的情況,如果在一個非常短的時間內到達 50 個請求,並且對 CPU 有不同的需求,此時會發生什麼?50 個進程都被放在可運行進程列表中。如果 CP畫U 是空閒的,第一個進程會立即開始執行,第二個直到 100 ms 以後纔會啓動,以此類推。不幸的是最後一個進程需要等待 5 秒才能獲得執行機會。大部分用戶都會覺得對於一個簡短的指令運行 5 秒中是很慢的。如果隊列末尾的某些請求只需要幾號秒鐘的運行時間的話,這種設計就非常糟糕了。

另外一個因素是如果時間片設置長度要大於 CPU 使用長度,那麼搶佔就不會經常發生。相反,在時間片用完之前,大多數進程都已經阻塞了,那麼就會引起進程間的切換。消除搶佔可提高性能,因爲進程切換僅在邏輯上必要時才發生,即流程阻塞且無法繼續時才發生。

結論可以表述如下:將上下文切換時間設置得太短會導致過多的進程切換並降低 CPU 效率,但設置時間太長會導致一個短請求很長時間得不到響應。最好的切換時間是在 20 - 50 毫秒之間設置。

優先級調度

輪詢調度假設了所有的進程是同等重要的。但事實情況可能不是這樣。例如,在一所大學中的等級制度,首先是院長,然後是教授、祕書、後勤人員,最後是學生。這種將外部情況考慮在內就實現了優先級調度(priority scheduling)

它的基本思想很明確,每個進程都被賦予一個優先級,優先級高的進程優先運行。

但是也不意味着高優先級的進程能夠永遠一直運行下去,調度程序會在每個時鐘中斷期間降低當前運行進程的優先級。如果此操作導致其優先級降低到下一個最高進程的優先級以下,則會發生進程切換。或者,可以爲每個進程分配允許運行的最大時間間隔。當時間間隔用完後,下一個高優先級的進程會得到運行的機會。

可以靜態或者動態的爲進程分配優先級。在一臺軍用計算機上,可以把將軍所啓動的進程設爲優先級 100,上校爲 90 ,少校爲 80,上尉爲 70,中尉爲 60,以此類推。UNIX 中有一條命令爲 nice ,它允許用戶爲了照顧他人而自願降低自己進程的優先級,但是一般沒人用。

優先級也可以由系統動態分配,用於實現某種目的。例如,有些進程爲 I/O 密集型,其多數時間用來等待 I/O 結束。當這樣的進程需要 CPU 時,應立即分配 CPU,用來啓動下一個 I/O 請求,這樣就可以在另一個進程進行計算的同時執行 I/O 操作。這類 I/O 密集型進程長時間的等待 CPU 只會造成它長時間佔用內存。使 I/O 密集型進程獲得較好的服務的一種簡單算法是,將其優先級設爲 1/f,f 爲該進程在上一時間片中所佔的部分。一個在 50 ms 的時間片中只使用 1 ms 的進程將獲得優先級 50 ,而在阻塞之前用掉 25 ms 的進程將具有優先級 2,而使用掉全部時間片的進程將得到優先級 1。

可以很方便的將一組進程按優先級分成若干類,並且在各個類之間採用優先級調度,而在各類進程的內部採用輪轉調度。下面展示了一個四個優先級類的系統

它的調度算法主要描述如下:上面存在優先級爲 4 類的可運行進程,首先會按照輪轉法爲每個進程運行一個時間片,此時不理會較低優先級的進程。若第 4 類進程爲空,則按照輪詢的方式運行第三類進程。若第 4 類和第 3 類進程都爲空,則按照輪轉法運行第 2 類進程。如果不對優先級進行調整,則低優先級的進程很容易產生飢餓現象。

多級隊列

最早使用優先級調度的系統是 CTSS(Compatible TimeSharing System)。CTSS 是一種兼容分時系統,它有一個問題就是進程切換太慢,其原因是 IBM 7094 內存只能放進一個進程。

IBM 是哥倫比亞大學計算機中心在 1964 - 1968 年的計算機

CTSS 在每次切換前都需要將當前進程換出到磁盤,並從磁盤上讀入一個新進程。CTSS 的設計者很快就認識到,爲 CPU 密集型進程設置較長的時間片比頻繁地分給他們很短的時間要更有效(減少交換次數)。另一方面,如前所述,長時間片的進程又會影響到響應時間,解決辦法是設置優先級類。屬於最高優先級的進程運行一個時間片,次高優先級進程運行 2 個時間片,再下面一級運行 4 個時間片,以此類推。當一個進程用完分配的時間片後,它被移到下一類。

最短進程優先

對於批處理系統而言,由於最短作業優先常常伴隨着最短響應時間,所以如果能夠把它用於交互式進程,那將是非常好的。在某種程度上,的確可以做到這一點。交互式進程通常遵循下列模式:等待命令、執行命令、等待命令、執行命令。。。如果我們把每個命令的執行都看作一個分離的作業,那麼我們可以通過首先運行最短的作業來使響應時間最短。這裏唯一的問題是如何從當前可運行進程中找出最短的那一個進程。

一種方式是根據進程過去的行爲進行推測,並執行估計運行時間最短的那一個。假設每個終端上每條命令的預估運行時間爲 T0,現在假設測量到其下一次運行時間爲 T1,可以用兩個值的加權來改進估計時間,即aT0+ (1- 1)T1。通過選擇 a 的值,可以決定是儘快忘掉老的運行時間,還是在一段長時間內始終記住它們。當 a = 1/2 時,可以得到下面這個序列

可以看到,在三輪過後,T0 在新的估計值中所佔比重下降至 1/8。

有時把這種通過當前測量值和先前估計值進行加權平均從而得到下一個估計值的技術稱作 老化(aging)。這種方法會使用很多預測值基於當前值的情況。

保證調度

一種完全不同的調度方法是對用戶做出明確的性能保證。一種實際而且容易實現的保證是:若用戶工作時有 n 個用戶登錄,則每個用戶將獲得 CPU 處理能力的 1/n。類似地,在一個有 n 個進程運行的單用戶系統中,若所有的進程都等價,則每個進程將獲得 1/n 的 CPU 時間。

彩票調度

對用戶進行承諾並在隨後兌現承諾是一件好事,不過很難實現。但是存在着一種簡單的方式,有一種既可以給出預測結果而又有一種比較簡單的實現方式的算法,就是 彩票調度(lottery scheduling)算法。

其基本思想是爲進程提供各種系統資源(例如 CPU 時間)的彩票。當做出一個調度決策的時候,就隨機抽出一張彩票,擁有彩票的進程將獲得該資源。在應用到 CPU 調度時,系統可以每秒持有 50 次抽獎,每個中獎者將獲得比如 20 毫秒的 CPU 時間作爲獎勵。

George Orwell 關於 所有的進程是平等的,但是某些進程能夠更平等一些。一些重要的進程可以給它們額外的彩票,以便增加他們贏得的機會。如果出售了 100 張彩票,而且有一個進程持有了它們中的 20 張,它就會有 20% 的機會去贏得彩票中獎。在長時間的運行中,它就會獲得 20% 的CPU。相反,對於優先級調度程序,很難說明擁有優先級 40 究竟是什麼意思,這裏的規則很清楚,擁有彩票 f 份額的進程大約得到系統資源的 f 份額。

如果希望進程之間協作的話可以交換它們之間的票據。例如,客戶端進程給服務器進程發送了一條消息後阻塞,客戶端進程可能會把自己所有的票據都交給服務器,來增加下一次服務器運行的機會。當服務完成後,它會把彩票還給客戶端讓其有機會再次運行。事實上,如果沒有客戶機,服務器也根本不需要彩票。

可以把彩票理解爲 buff,這個 buff 有 15% 的機率能讓你產生 速度之靴 的效果。

公平分享調度

到目前爲止,我們假設被調度的都是各個進程自身,而不用考慮該進程的擁有者是誰。結果是,如果用戶 1 啓動了 9 個進程,而用戶 2 啓動了一個進程,使用輪轉或相同優先級調度算法,那麼用戶 1 將得到 90 % 的 CPU 時間,而用戶 2 將之得到 10 % 的 CPU 時間。

爲了阻止這種情況的出現,一些系統在調度前會把進程的擁有者考慮在內。在這種模型下,每個用戶都會分配一些CPU 時間,而調度程序會選擇進程並強制執行。因此如果兩個用戶每個都會有 50% 的 CPU 時間片保證,那麼無論一個用戶有多少個進程,都將獲得相同的 CPU 份額。

實時系統中的調度

實時系統(real-time) 是一個時間扮演了重要作用的系統。典型的,一種或多種外部物理設備發給計算機一個服務請求,而計算機必須在一個確定的時間範圍內恰當的做出反應。例如,在 CD 播放器中的計算機會獲得從驅動器過來的位流,然後必須在非常短的時間內將位流轉換爲音樂播放出來。如果計算時間過長,那麼音樂就會聽起來有異常。再比如說醫院特別護理部門的病人監護裝置、飛機中的自動駕駛系統、列車中的煙霧警告裝置等,在這些例子中,正確但是卻緩慢的響應要比沒有響應甚至還糟糕。

實時系統可以分爲兩類,硬實時(hard real time)軟實時(soft real time) 系統,前者意味着必須要滿足絕對的截止時間;後者的含義是雖然不希望偶爾錯失截止時間,但是可以容忍。在這兩種情形中,實時都是通過把程序劃分爲一組進程而實現的,其中每個進程的行爲是可預測和提前可知的。這些進程一般壽命較短,並且極快的運行完成。在檢測到一個外部信號時,調度程序的任務就是按照滿足所有截止時間的要求調度進程。

實時系統中的事件可以按照響應方式進一步分類爲週期性(以規則的時間間隔發生)事件或 非週期性(發生時間不可預知)事件。一個系統可能要響應多個週期性事件流,根據每個事件處理所需的時間,可能甚至無法處理所有事件。例如,如果有 m 個週期事件,事件 i 以週期 Pi 發生,並需要 Ci 秒 CPU 時間處理一個事件,那麼可以處理負載的條件是

只有滿足這個條件的實時系統稱爲可調度的,這意味着它實際上能夠被實現。一個不滿足此檢驗標準的進程不能被調度,因爲這些進程共同需要的 CPU 時間總和大於 CPU 能提供的時間。

舉一個例子,考慮一個有三個週期性事件的軟實時系統,其週期分別是 100 ms、200 m 和 500 ms。如果這些事件分別需要 50 ms、30 ms 和 100 ms 的 CPU 時間,那麼該系統時可調度的,因爲 0.5 + 0.15 + 0.2 < 1。如果此時有第四個事件加入,其週期爲 1 秒,那麼此時這個事件如果不超過 150 ms,那麼仍然是可以調度的。忽略上下文切換的時間。

實時系統的調度算法可以是靜態的或動態的。前者在系統開始運行之前做出調度決策;後者在運行過程中進行調度決策。只有在可以提前掌握所完成的工作以及必須滿足的截止時間等信息時,靜態調度才能工作,而動態調度不需要這些限制。

調度策略和機制

到目前爲止,我們隱含的假設系統中所有進程屬於不同的分組用戶並且進程間存在相互競爭 CPU 的情況。通常情況下確實如此,但有時也會發生一個進程會有很多子進程並在其控制下運行的情況。例如,一個數據庫管理系統進程會有很多子進程。每一個子進程可能處理不同的請求,或者每個子進程實現不同的功能(如請求分析、磁盤訪問等)。主進程完全可能掌握哪一個子進程最重要(或最緊迫),而哪一個最不重要。但是,以上討論的調度算法中沒有一個算法從用戶進程接收有關的調度決策信息,這就導致了調度程序很少能夠做出最優的選擇。

解決問題的辦法是將 調度機制(scheduling mechanism)調度策略(scheduling policy) 分開,這是長期一貫的原則。這也就意味着調度算法在某種方式下被參數化了,但是參數可以被用戶進程填寫。讓我們首先考慮數據庫的例子。假設內核使用優先級調度算法,並提供了一條可供進程設置優先級的系統調用。這樣,儘管父進程本身並不參與調度,但它可以控制如何調度子進程的細節。調度機制位於內核,而調度策略由用戶進程決定,調度策略和機制分離是一種關鍵性思路。

線程調度

當若干進程都有多個線程時,就存在兩個層次的並行:進程和線程。在這樣的系統中調度處理有本質的差別,這取決於所支持的是用戶級線程還是內核級線程(或兩者都支持)。

首先考慮用戶級線程,由於內核並不知道有線程存在,所以內核還是和以前一樣地操作,選取一個進程,假設爲 A,並給予 A 以時間片控制。A 中的線程調度程序決定哪個線程運行。假設爲 A1。由於多道線程並不存在時鐘中斷,所以這個線程可以按其意願任意運行多長時間。如果該線程用完了進程的全部時間片,內核就會選擇另一個進程繼續運行。

在進程 A 終於又一次運行時,線程 A1 會接着運行。該線程會繼續耗費 A 進程的所有時間,直到它完成工作。不過,線程運行不會影響到其他進程。其他進程會得到調度程序所分配的合適份額,不會考慮進程 A 內部發生的事情。

現在考慮 A 線程每次 CPU 計算的工作比較少的情況,例如:在 50 ms 的時間片中有 5 ms 的計算工作。於是,每個線程運行一會兒,然後把 CPU 交回給線程調度程序。這樣在內核切換到進程 B 之前,就會有序列 A1,A2,A3,A1,A2,A3,A1,A2,A3,A1 。 如下所示

運行時系統使用的調度算法可以是上面介紹算法的任意一種。從實用方面考慮,輪轉調度和優先級調度更爲常用。唯一的侷限是,缺乏一個時鐘中斷運行過長的線程。但由於線程之間的合作關係,這通常也不是問題。

現在考慮使用內核線程的情況,內核選擇一個特定的線程運行。它不用考慮線程屬於哪個進程,不過如果有必要的話,也可以這麼做。對被選擇的線程賦予一個時間片,而且如果超過了時間片,就會強制掛起該線程。一個線程在 50 ms 的時間片內,5 ms 之後被阻塞,在 30 ms 的時間片中,線程的順序會是 A1,B1,A2,B2,A3,B3。如下圖所示

用戶級線程和內核級線程之間的主要差別在於性能。用戶級線程的切換需要少量的機器指令(想象一下Java程序的線程切換),而內核線程需要完整的上下文切換,修改內存映像,使高速緩存失效,這會導致了若干數量級的延遲。另一方面,在使用內核級線程時,一旦線程阻塞在 I/O 上就不需要在用戶級線程中那樣將整個進程掛起。

從進程 A 的一個線程切換到進程 B 的一個線程,其消耗要遠高於運行進程 A 的兩個線程(涉及修改內存映像,修改高速緩存),內核對這種切換的消耗是瞭解到,可以通過這些信息作出決定。

後記

提出一個《機械工業出版社》的翻譯勘誤,不知道能不能看到,先提了再說,很不嚴謹。

文章參考:

《現代操作系統》

《Modern Operating System》forth edition

https://www.encyclopedia.com/computing/news-wires-white-papers-and-books/interactive-systems

https://j00ru.vexillium.org/syscalls/nt/32/

https://www.bottomupcs.com/process_hierarchy.xhtml

https://en.wikipedia.org/wiki/Runtime_system

https://en.wikipedia.org/wiki/Execution_model

https://zhidao.baidu.com/question/113227654.html

https://baike.baidu.com/item/等待隊列/9223804?fr=aladdin

http://www.columbia.edu/cu/computinghistory/7094.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章