進程和線程基礎知識全家桶,30 張圖一套帶走


前言

先來看看一則小故事算法

咱們寫好的一行行代碼,爲了讓其工做起來,咱們還得把它送進城(進程)裏,那既然進了城裏,那確定不能胡做非爲了。緩存

城裏人有城裏人的規矩,城中有個專門管轄大家的城管(操做系統),人家讓你休息就休息,讓你工做就工做,畢竟攤位(CPU)就一個,每一個人都要佔這個攤位來工做,城裏要工做的人多着去了。微信

因此城管爲了公平起見,它使用一種策略(調度)方式,給每一個人一個固定的工做時間(時間片),時間到了就會通知你去休息而換另一我的上場工做。數據結構

另外,在休息時候你也不能偷懶,要記住工做到哪了,否則下次到你工做了,你忘記工做到哪了,那還怎麼繼續?多線程

有的人,可能還進入了縣城(線程)工做,這裏相對輕鬆一些,在休息的時候,要記住的東西相對較少,並且還能共享城裏的資源。併發

「哎喲,難道本文內容是進程和線程?」函數

能夠,聰明的你猜出來了,也不枉費我瞎編亂造的故事了。工具

進程和線程對於寫代碼的咱們,真的每天見、日日見了,但見的多不表明你就熟悉它們,好比簡單問你一句,你知道它們的工做原理和區別嗎?spa

不知道不要緊,今天就要跟你們討論操做系統的進程和線程操作系統

提綱

正文

進程

咱們編寫的代碼只是一個存儲在硬盤的靜態文件,經過編譯後就會生成二進制可執行文件,當咱們運行這個可執行文件後,它會被裝載到內存中,接着 CPU 會執行程序中的每一條指令,那麼這個運行中的程序,就被稱爲「進程」

如今咱們考慮有一個會讀取硬盤文件數據的程序被執行了,那麼當運行到讀取文件的指令時,就會去從硬盤讀取數據,可是硬盤的讀寫速度是很是慢的,那麼在這個時候,若是 CPU 傻傻的等硬盤返回數據的話,那 CPU 的利用率是很是低的。

作個類比,你去煮開水時,你會傻傻的等水壺燒開嗎?很明顯,小孩也不會傻等。咱們能夠在水壺燒開以前去作其餘事情。當水壺燒開了,咱們天然就會聽到「嘀嘀嘀」的聲音,因而再把燒開的水倒入到水杯裏就行了。

因此,當進程要從硬盤讀取數據時,CPU 不須要阻塞等待數據的返回,而是去執行另外的進程。當硬盤數據返回時,CPU 會收到個中斷,因而 CPU 再繼續運行這個進程。

進程 1 與進程 2 切換

這種多個程序、交替執行的思想,就有 CPU 管理多個進程的初步想法。

對於一個支持多進程的系統,CPU 會從一個進程快速切換至另外一個進程,其間每一個進程各運行幾十或幾百個毫秒。

雖然單核的 CPU 在某一個瞬間,只能運行一個進程。但在 1 秒鐘期間,它可能會運行多個進程,這樣就產生並行的錯覺,實際上這是併發

併發和並行有什麼區別?

一圖勝千言。

併發與並行

進程與程序的關係的類比

到了晚飯時間,一對小情侶肚子都咕咕叫了,因而男生見機行事,就想給女生作晚飯,因此他就在網上找了辣子雞的菜譜,接着買了一些雞肉、辣椒、香料等材料,而後邊看邊學邊作這道菜。

忽然,女生說她想喝可樂,那麼男生只好把作菜的事情暫停一下,並在手機菜譜標記作到哪個步驟,把狀態信息記錄了下來。

而後男生遵從女生的指令,跑去下樓買了一瓶冰可樂後,又回到廚房繼續作菜。

這體現了,CPU 能夠從一個進程(作菜)切換到另一個進程(買可樂),在切換前必需要記錄當前進程中運行的狀態信息,以備下次切換回來的時候能夠恢復執行。

因此,能夠發現進程有着「運行 - 暫停 - 運行」的活動規律。

進程的狀態

在上面,咱們知道了進程有着「運行 - 暫停 - 運行」的活動規律。通常說來,一個進程並非自始至終連續不停地運行的,它與併發執行中的其餘進程的執行是相互制約的。

它有時處於運行狀態,有時又因爲某種緣由而暫停運行處於等待狀態,當使它暫停的緣由消失後,它又進入準備運行狀態。

因此,在一個進程的活動期間至少具有三種基本狀態,即運行狀態、就緒狀態、阻塞狀態。

進程的三種基本狀態

上圖中各個狀態的意義:

  • 運行狀態(Runing):該時刻進程佔用 CPU;

  • 就緒狀態(Ready):可運行,但由於其餘進程正在運行而暫停中止;

  • 阻塞狀態(Blocked):該進程正在等待某一事件發生(如等待輸入/輸出操做的完成)而暫時中止運行,這時,即便給它CPU控制權,它也沒法運行;

固然,進程另外兩個基本狀態:

  • 建立狀態(new):進程正在被建立時的狀態;

  • 結束狀態(Exit):進程正在從系統中消失時的狀態;

因而,一個完整的進程狀態的變遷以下圖:

進程五種狀態的變遷

再來詳細說明一下進程的狀態變遷:

  • NULL -> 建立狀態:一個新進程被建立時的第一個狀態;

  • 建立狀態 -> 就緒狀態:當進程被建立完成並初始化後,一切就緒準備運行時,變爲就緒狀態,這個過程是很快的;

  • 就緒態 -> 運行狀態:處於就緒狀態的進程被操做系統的進程調度器選中後,就分配給 CPU 正式運行該進程;

  • 運行狀態 -> 結束狀態:當進程已經運行完成或出錯時,會被操做系統做結束狀態處理;

  • 運行狀態 -> 就緒狀態:處於運行狀態的進程在運行過程當中,因爲分配給它的運行時間片用完,操做系統會把該進程變爲就緒態,接着從就緒態選中另一個進程運行;

  • 運行狀態 -> 阻塞狀態:當進程請求某個事件且必須等待時,例如請求 I/O 事件;

  • 阻塞狀態 -> 就緒狀態:當進程要等待的事件完成時,它從阻塞狀態變到就緒狀態;

另外,還有一個狀態叫掛起狀態,它表示進程沒有佔有內存空間。這跟阻塞狀態是不同,阻塞狀態是等待某個事件的返回。

因爲虛擬內存管理緣由,進程的所使用的空間可能並無映射到物理內存,而是在硬盤上,這時進程就會出現掛起狀態。

虛擬內存管理-換入換出

掛起狀態能夠分爲兩種:

  • 阻塞掛起狀態:進程在外存(硬盤)並等待某個事件的出現;

  • 就緒掛起狀態:進程在外存(硬盤),但只要進入內存,即刻馬上運行;

這兩種掛起狀態加上前面的五種狀態,就變成了七種狀態變遷(留給個人顏色很少了),見以下圖:

七種狀態變遷

進程的控制結構

在操做系統中,是用進程控制塊process control block,PCB)數據結構來描述進程的。

那 PCB 是什麼呢?打開知乎搜索你就會發現這個東西並非那麼簡單。

知乎搜 PCB 的提示

打住打住,咱們是個正經的人,怎麼會去看那些問題呢?是吧,回來回來。

PCB 是進程存在的惟一標識,這意味着一個進程的存在,必然會有一個 PCB,若是進程消失了,那麼 PCB 也會隨之消失。

PCB 具體包含什麼信息呢?

進程描述信息:

  • 進程標識符:標識各個進程,每一個進程都有一個而且惟一的標識符;

  • 用戶標識符:進程歸屬的用戶,用戶標識符主要爲共享和保護服務;

進程控制和管理信息:

  • 進程當前狀態,如 new、ready、running、waiting 或 blocked 等;

  • 進程優先級:進程搶佔 CPU 時的優先級;

資源分配清單:

  • 有關內存地址空間或虛擬地址空間的信息,所打開文件的列表和所使用的 I/O 設備信息。

CPU 相關信息:

  • CPU 中各個寄存器的值,當進程被切換時,CPU 的狀態信息都會被保存在相應的 PCB 中,以便進程從新執行時,能從斷點處繼續執行。

可見,PCB 包含信息仍是比較多的。

每一個 PCB 是如何組織的呢?

一般是經過鏈表的方式進行組織,把具備相同狀態的進程鏈在一塊兒,組成各類隊列。好比:

  • 將全部處於就緒狀態的進程鏈在一塊兒,稱爲就緒隊列

  • 把全部因等待某事件而處於等待狀態的進程鏈在一塊兒就組成各類阻塞隊列

  • 另外,對於運行隊列在單核 CPU 系統中則只有一個運行指針了,由於單核 CPU 在某個時間,只能運行一個程序。

那麼,就緒隊列和阻塞隊列鏈表的組織形式以下圖:

就緒隊列和阻塞隊列

除了連接的組織方式,還有索引方式,它的工做原理:將同一狀態的進程組織在一個索引表中,索引表項指向相應的 PCB,不一樣狀態對應不一樣的索引表。

通常會選擇鏈表,由於可能面臨進程建立,銷燬等調度致使進程狀態發生變化,因此鏈表可以更加靈活的插入和刪除。

進程的控制

咱們熟知了進程的狀態變遷和進程的數據結構 PCB 後,再來看看進程的建立、終止、阻塞、喚醒的過程,這些過程也就是進程的控制。

01 建立進程

操做系統容許一個進程建立另外一個進程,並且容許子進程繼承父進程所擁有的資源,當子進程被終止時,其在父進程處繼承的資源應當還給父進程。同時,終止父進程時同時也會終止其全部的子進程。

建立進程的過程以下:

  • 爲新進程分配一個惟一的進程標識號,並申請一個空白的 PCB,PCB 是有限的,若申請失敗則建立失敗;

  • 爲進程分配資源,此處若是資源不足,進程就會進入等待狀態,以等待資源;

  • 初始化 PCB;

  • 若是進程的調度隊列可以接納新進程,那就將進程插入到就緒隊列,等待被調度運行;

02 終止進程

進程能夠有 3 種終止方式:正常結束、異常結束以及外界干預(信號 kill 掉)。

終止進程的過程以下:

  • 查找須要終止的進程的 PCB;

  • 若是處於執行狀態,則當即終止該進程的執行,而後將 CPU 資源分配給其餘進程;

  • 若是其還有子進程,則應將其全部子進程終止;

  • 將該進程所擁有的所有資源都歸還給父進程或操做系統;

  • 將其從 PCB 所在隊列中刪除;

03 阻塞進程

當進程須要等待某一事件完成時,它能夠調用阻塞語句把本身阻塞等待。而一旦被阻塞等待,它只能由另外一個進程喚醒。

阻塞進程的過程以下:

  • 找到將要被阻塞進程標識號對應的 PCB;

  • 若是該進程爲運行狀態,則保護其現場,將其狀態轉爲阻塞狀態,中止運行;

  • 將該 PCB 插入的阻塞隊列中去;

04 喚醒進程

進程由「運行」轉變爲「阻塞」狀態是因爲進程必須等待某一事件的完成,因此處於阻塞狀態的進程是絕對不可能叫醒本身的。

若是某進程正在等待 I/O 事件,需由別的進程發消息給它,則只有當該進程所期待的事件出現時,才由發現者進程用喚醒語句叫醒它。

喚醒進程的過程以下:

  • 在該事件的阻塞隊列中找到相應進程的 PCB;

  • 將其從阻塞隊列中移出,並置其狀態爲就緒狀態;

  • 把該 PCB 插入到就緒隊列中,等待調度程序調度;

進程的阻塞和喚醒是一對功能相反的語句,若是某個進程調用了阻塞語句,則必有一個與之對應的喚醒語句。

進程的上下文切換

各個進程之間是共享 CPU 資源的,在不一樣的時候進程之間須要切換,讓不一樣的進程能夠在 CPU 執行,那麼這個一個進程切換到另外一個進程運行,稱爲進程的上下文切換

在詳細說進程上下文切換前,咱們先來看看 CPU 上下文切換

大多數操做系統都是多任務,一般支持大於 CPU 數量的任務同時運行。實際上,這些任務並非同時運行的,只是由於系統在很短的時間內,讓各個任務分別在 CPU 運行,因而就形成同時運行的錯誤。

任務是交給 CPU 運行的,那麼在每一個任務運行前,CPU 須要知道任務從哪裏加載,又從哪裏開始運行。

因此,操做系統須要事先幫 CPU 設置好 CPU 寄存器和程序計數器

CPU 寄存器是 CPU 內部一個容量小,可是速度極快的內存(緩存)。我舉個例子,寄存器像是你的口袋,內存像你的書包,硬盤則是你家裏的櫃子,若是你的東西存放到口袋,那確定是比你從書包或家裏櫃子取出來要快的多。

再來,程序計數器則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。

因此說,CPU 寄存器和程序計數是 CPU 在運行任何任務前,所必須依賴的環境,這些環境就叫作 CPU 上下文

既然知道了什麼是 CPU 上下文,那理解 CPU 上下文切換就不難了。

CPU 上下文切換就是先把前一個任務的 CPU 上下文(CPU 寄存器和程序計數器)保存起來,而後加載新任務的上下文到這些寄存器和程序計數器,最後再跳轉到程序計數器所指的新位置,運行新任務。

系統內核會存儲保持下來的上下文信息,當此任務再次被分配給 CPU 運行時,CPU 會從新加載這些上下文,這樣就能保證任務原來的狀態不受影響,讓任務看起來仍是連續運行。

上面說到所謂的「任務」,主要包含進程、線程和中斷。因此,能夠根據任務的不一樣,把 CPU 上下文切換分紅:進程上下文切換、線程上下文切換和中斷上下文切換

進程的上下文切換究竟是切換什麼呢?

進程是由內核管理和調度的,因此進程的切換隻能發生在內核態。

因此,進程的上下文切換不只包含了虛擬內存、棧、全局變量等用戶空間的資源,還包括了內核堆棧、寄存器等內核空間的資源。

一般,會把交換的信息保存在進程的 PCB,當要運行另一個進程的時候,咱們須要從這個進程的 PCB 取出上下文,而後恢復到 CPU 中,這使得這個進程能夠繼續執行,以下圖所示:

進程上下文切換

你們須要注意,進程的上下文開銷是很關鍵的,咱們但願它的開銷越小越好,這樣可使得進程能夠把更多時間花費在執行程序上,而不是耗費在上下文切換。

發生進程上下文切換有哪些場景?

  • 爲了保證全部進程能夠獲得公平調度,CPU 時間被劃分爲一段段的時間片,這些時間片再被輪流分配給各個進程。這樣,當某個進程的時間片耗盡了,就會被系統掛起,切換到其它正在等待 CPU 的進程運行;

  • 進程在系統資源不足(好比內存不足)時,要等到資源知足後才能夠運行,這個時候進程也會被掛起,並由系統調度其餘進程運行;

  • 當進程經過睡眠函數 sleep 這樣的方法將本身主動掛起時,天然也會從新調度;

  • 當有優先級更高的進程運行時,爲了保證高優先級進程的運行,當前進程會被掛起,由高優先級進程來運行;

  • 發生硬件中斷時,CPU 上的進程會被中斷掛起,轉而執行內核中的中斷服務程序;

以上,就是發生進程上下文切換的常見場景了。


線程

在早期的操做系統中都是以進程做爲獨立運行的基本單位,直到後面,計算機科學家們又提出了更小的能獨立運行的基本單位,也就是線程。

爲何使用線程?

咱們舉個例子,假設你要編寫一個視頻播放器軟件,那麼該軟件功能的核心模塊有三個:

  • 從視頻文件當中讀取數據;

  • 對讀取的數據進行解壓縮;

  • 把解壓縮後的視頻數據播放出來;

對於單進程的實現方式,我想你們都會是如下這個方式:

單進程實現方式

對於單進程的這種方式,存在如下問題:

  • 播放出來的畫面和聲音會不連貫,由於當 CPU 能力不夠強的時候,Read  的時候可能進程就等在這了,這樣就會致使等半天才進行數據解壓和播放;

  • 各個函數之間不是併發執行,影響資源的使用效率;

那改進成多進程的方式:

多進程實現方式

對於多進程的這種方式,依然會存在問題:

  • 進程之間如何通訊,共享數據?

  • 維護進程的系統開銷較大,如建立進程時,分配資源、創建 PCB;終止進程時,回收資源、撤銷 PCB;進程切換時,保存當前進程的狀態信息;

那到底如何解決呢?須要有一種新的實體,知足如下特性:

  • 實體之間能夠併發運行;

  • 實體之間共享相同的地址空間;

這個新的實體,就是線程( Thread ),線程之間能夠併發運行且共享相同的地址空間。

什麼是線程?

線程是進程當中的一條執行流程。

同一個進程內多個線程之間能夠共享代碼段、數據段、打開的文件等資源,但每一個線程都有獨立一套的寄存器和棧,這樣能夠確保線程的控制流是相對獨立的。

多線程

線程的優缺點?

線程的優勢:

  • 一個進程中能夠同時存在多個線程;

  • 各個線程之間能夠併發執行;

  • 各個線程之間能夠共享地址空間和文件等資源;

線程的缺點:

  • 當進程中的一個線程奔潰時,會致使其所屬進程的全部線程奔潰。

舉個例子,對於遊戲的用戶設計,則不該該使用多線程的方式,不然一個用戶掛了,會影響其餘同個進程的線程。

線程與進程的比較

線程與進程的比較以下:

  • 進程是資源(包括內存、打開的文件等)分配的單位,線程是 CPU 調度的單位;

  • 進程擁有一個完整的資源平臺,而線程只獨享必不可少的資源,如寄存器和棧;

  • 線程一樣具備就緒、阻塞、執行三種基本狀態,一樣具備狀態之間的轉換關係;

  • 線程能減小併發執行的時間和空間開銷;

對於,線程相比進程能減小開銷,體如今:

  • 線程的建立時間比進程快,由於進程在建立的過程當中,還須要資源管理信息,好比內存管理信息、文件管理信息,而線程在建立的過程當中,不會涉及這些資源管理信息,而是共享它們;

  • 線程的終止時間比進程快,由於線程釋放的資源相比進程少不少;

  • 同一個進程內的線程切換比進程切換快,由於線程具備相同的地址空間(虛擬內存共享),這意味着同一個進程的線程都具備同一個頁表,那麼在切換的時候不須要切換頁表。而對於進程之間的切換,切換的時候要把頁表給切換掉,而頁表的切換過程開銷是比較大的;

  • 因爲同一進程的各線程間共享內存和文件資源,那麼在線程之間數據傳遞的時候,就不須要通過內核了,這就使得線程之間的數據交互效率更高了;

因此,線程比進程無論是時間效率,仍是空間效率都要高。

線程的上下文切換

在前面咱們知道了,線程與進程最大的區別在於:線程是調度的基本單位,而進程則是資源擁有的基本單位

因此,所謂操做系統的任務調度,實際上的調度對象是線程,而進程只是給線程提供了虛擬內存、全局變量等資源。

對於線程和進程,咱們能夠這麼理解:

  • 當進程只有一個線程時,能夠認爲進程就等於線程;

  • 當進程擁有多個線程時,這些線程會共享相同的虛擬內存和全局變量等資源,這些資源在上下文切換時是不須要修改的;

另外,線程也有本身的私有數據,好比棧和寄存器等,這些在上下文切換時也是須要保存的。

線程上下文切換的是什麼?

這還得看線程是否是屬於同一個進程:

  • 當兩個線程不是屬於同一個進程,則切換的過程就跟進程上下文切換同樣;

  • 當兩個線程是屬於同一個進程,由於虛擬內存是共享的,因此在切換時,虛擬內存這些資源就保持不動,只須要切換線程的私有數據、寄存器等不共享的數據

因此,線程的上下文切換相比進程,開銷要小不少。

線程的實現

主要有三種線程的實現方式:

  • 用戶線程(User Thread:在用戶空間實現的線程,不是由內核管理的線程,是由用戶態的線程庫來完成線程的管理;

  • 內核線程(Kernel Thread:在內核中實現的線程,是由內核管理的線程;

  • 輕量級進程(LightWeight Process:在內核中來支持用戶線程;

那麼,這還須要考慮一個問題,用戶線程和內核線程的對應關係。

首先,第一種關係是多對一的關係,也就是多個用戶線程對應同一個內核線程:

多對一

第二種是一對一的關係,也就是一個用戶線程對應一個內核線程:

一對一

第三種是多對多的關係,也就是多個用戶線程對應到多個內核線程:

多對多

用戶線程如何理解?存在什麼優點和缺陷?

用戶線程是基於用戶態的線程管理庫來實現的,那麼線程控制塊(Thread Control Block, TCB 也是在庫裏面來實現的,對於操做系統而言是看不到這個 TCB 的,它只能看到整個進程的 PCB。

因此,用戶線程的整個線程管理和調度,操做系統是不直接參與的,而是由用戶級線程庫函數來完成線程的管理,包括線程的建立、終止、同步和調度等。

用戶級線程的模型,也就相似前面提到的多對一的關係,即多個用戶線程對應同一個內核線程,以下圖所示:

用戶級線程模型

用戶線程的優勢

  • 每一個進程都須要有它私有的線程控制塊(TCB)列表,用來跟蹤記錄它各個線程狀態信息(PC、棧指針、寄存器),TCB 由用戶級線程庫函數來維護,可用於不支持線程技術的操做系統;

  • 用戶線程的切換也是由線程庫函數來完成的,無需用戶態與內核態的切換,因此速度特別快;

用戶線程的缺點

  • 因爲操做系統不參與線程的調度,若是一個線程發起了系統調用而阻塞,那進程所包含的用戶線程都不能執行了。

  • 當一個線程開始運行後,除非它主動地交出 CPU 的使用權,不然它所在的進程當中的其餘線程沒法運行,由於用戶態的線程無法打斷當前運行中的線程,它沒有這個特權,只有操做系統纔有,可是用戶線程不是由操做系統管理的。

  • 因爲時間片分配給進程,故與其餘進程比,在多線程執行時,每一個線程獲得的時間片較少,執行會比較慢;

以上,就是用戶線程的優缺點了。

那內核線程如何理解?存在什麼優點和缺陷?

內核線程是由操做系統管理的,線程對應的 TCB 天然是放在操做系統裏的,這樣線程的建立、終止和管理都是由操做系統負責。

內核線程的模型,也就相似前面提到的一對一的關係,即一個用戶線程對應一個內核線程,以下圖所示:

內核線程模型

內核線程的優勢

  • 在一個進程當中,若是某個內核線程發起系統調用而被阻塞,並不會影響其餘內核線程的運行;

  • 分配給線程,多線程的進程得到更多的 CPU 運行時間;

內核線程的缺點

  • 在支持內核線程的操做系統中,由內核來維護進程和線程的上下問信息,如 PCB 和 TCB;

  • 線程的建立、終止和切換都是經過系統調用的方式來進行,所以對於系統來講,系統開銷比較大;

以上,就是內核線的優缺點了。

最後的輕量級進程如何理解?

輕量級進程(Light-weight process,LWP)是內核支持的用戶線程,一個進程可有一個或多個 LWP,每一個 LWP 是跟內核線程一對一映射的,也就是 LWP 都是由一個內核線程支持。

另外,LWP 只能由內核管理並像普通進程同樣被調度,Linux 內核是支持 LWP 的典型例子。

在大多數系統中,LWP與普通進程的區別也在於它只有一個最小的執行上下文和調度程序所需的統計信息。通常來講,一個進程表明程序的一個實例,而 LWP 表明程序的執行線程,由於一個執行線程不像進程那樣須要那麼多狀態信息,因此 LWP 也不帶有這樣的信息。

在 LWP 之上也是可使用用戶線程的,那麼 LWP 與用戶線程的對應關係就有三種:

  • 1 : 1,即一個 LWP 對應 一個用戶線程;

  • N : 1,即一個 LWP 對應多個用戶線程;

  • N : N,即多個 LMP 對應多個用戶線程;

接下來針對上面這三種對應關係說明它們優缺點。先下圖的 LWP 模型:

LWP 模型

1 : 1 模式

一個線程對應到一個 LWP 再對應到一個內核線程,如上圖的進程 4,屬於此模型。

  • 優勢:實現並行,當一個 LWP 阻塞,不會影響其餘 LWP;

  • 缺點:每個用戶線程,就產生一個內核線程,建立線程的開銷較大。

N : 1 模式

多個用戶線程對應一個 LWP 再對應一個內核線程,如上圖的進程 2,線程管理是在用戶空間完成的,此模式中用戶的線程對操做系統不可見。

  • 優勢:用戶線程要開幾個都沒問題,且上下文切換髮生用戶空間,切換的效率較高;

  • 缺點:一個用戶線程若是阻塞了,則整個進程都將會阻塞,另外在多核 CPU  中,是沒辦法充分利用 CPU 的。

M : N 模式

根據前面的兩個模型混搭一塊兒,就造成 M:N 模型,該模型提供了兩級控制,首先多個用戶線程對應到多個 LWP,LWP 再一一對應到內核線程,如上圖的進程 3。

  • 優勢:綜合了前兩種優勢,大部分的線程上下文發生在用戶空間,且多個線程又能夠充分利用多核 CPU 的資源。

組合模式

如上圖的進程 5,此進程結合 1:1 模型和 M:N 模型。開發人員能夠針對不一樣的應用特色調節內核線程的數目來達到物理並行性和邏輯並行性的最佳方案。


調度

進程都但願本身可以佔用 CPU 進行工做,那麼這涉及到前面說過的進程上下文切換。

一旦操做系統把進程切換到運行狀態,也就意味着該進程佔用着 CPU 在執行,可是當操做系統把進程切換到其餘狀態時,那就不能在 CPU 中執行了,因而操做系統會選擇下一個要運行的進程。

選擇一個進程運行這一功能是在操做系統中完成的,一般稱爲調度程序scheduler)。

那到底何時調度進程,或以什麼原則來調度進程呢?

調度時機

在進程的生命週期中,當進程從一個運行狀態到另一狀態變化的時候,其實會觸發一次調度。

好比,如下狀態的變化都會觸發操做系統的調度:

  • 從就緒態 -> 運行態:當進程被建立時,會進入到就緒隊列,操做系統會從就緒隊列選擇一個進程運行;

  • 從運行態 -> 阻塞態:當進程發生 I/O 事件而阻塞時,操做系統必須另一個進程運行;

  • 從運行態 -> 結束態:當進程退出結束後,操做系統得從就緒隊列選擇另一個進程運行;

由於,這些狀態變化的時候,操做系統須要考慮是否要讓新的進程給 CPU 運行,或者是否讓當前進程從 CPU 上退出來而換另外一個進程運行。

另外,若是硬件時鐘提供某個頻率的週期性中斷,那麼能夠根據如何處理時鐘中斷
,把調度算法分爲兩類:

  • 非搶佔式調度算法挑選一個進程,而後讓該進程運行直到被阻塞,或者直到該進程退出,纔會調用另一個進程,也就是說不會理時鐘中斷這個事情。

  • 搶佔式調度算法挑選一個進程,而後讓該進程只運行某段時間,若是在該時段結束時,該進程仍然在運行時,則會把它掛起,接着調度程序從就緒隊列挑選另一個進程。這種搶佔式調度處理,須要在時間間隔的末端發生時鐘中斷,以便把 CPU 控制返回給調度程序進行調度,也就是常說的時間片機制

調度原則

原則一:若是運行的程序,發生了 I/O 事件的請求,那 CPU 使用率必然會很低,由於此時進程在阻塞等待硬盤的數據返回。這樣的過程,勢必會形成 CPU 忽然的空閒。因此,爲了提升 CPU 利用率,在這種發送 I/O 事件導致 CPU 空閒的狀況下,調度程序須要從就緒隊列中選擇一個進程來運行。

原則二:有的程序執行某個任務花費的時間會比較長,若是這個程序一直佔用着 CPU,會形成系統吞吐量(CPU 在單位時間內完成的進程數量)的下降。因此,要提升系統的吞吐率,調度程序要權衡長任務和短任務進程的運行完成數量。

原則三:從進程開始到結束的過程當中,其實是包含兩個時間,分別是進程運行時間和進程等待時間,這兩個時間總和就稱爲週轉時間。進程的週轉時間越小越好,若是進程的等待時間很長而運行時間很短,那週轉時間就很長,這不是咱們所指望的,調度程序應該避免這種狀況發生。

原則四:處於就緒隊列的進程,也不能等過久,固然但願這個等待的時間越短越好,這樣可使得進程更快的在 CPU 中執行。因此,就緒隊列中進程的等待時間也是調度程序所須要考慮的原則。

原則五:對於鼠標、鍵盤這種交互式比較強的應用,咱們固然但願它的響應時間越快越好,不然就會影響用戶體驗了。因此,對於交互式比較強的應用,響應時間也是調度程序須要考慮的原則。

五種調度原則

針對上面的五種調度原則,總結成以下:

  • CPU 利用率:調度程序應確保 CPU 是始終匆忙的狀態,這可提升 CPU 的利用率;

  • 系統吞吐量:吞吐量表示的是單位時間內 CPU 完成進程的數量,長做業的進程會佔用較長的 CPU 資源,所以會下降吞吐量,相反,短做業的進程會提高系統吞吐量;

  • 週轉時間:週轉時間是進程運行和阻塞時間總和,一個進程的週轉時間越小越好;

  • 等待時間:這個等待時間不是阻塞狀態的時間,而是進程處於就緒隊列的時間,等待的時間越長,用戶越不滿意;

  • 響應時間:用戶提交請求到系統第一次產生響應所花費的時間,在交互式系統中,響應時間是衡量調度算法好壞的主要標準。

說白了,這麼多調度原則,目的就是要使得進程要「快」。

調度算法

不一樣的調度算法適用的場景也是不一樣的。

接下來,說說在單核 CPU 系統中常見的調度算法。

01 先來先服務調度算法

最簡單的一個調度算法,就是非搶佔式的先來先服務(First Come First Severd, FCFS)算法了。

FCFS 調度算法

顧名思義,先來後到,每次從就緒隊列選擇最早進入隊列的進程,而後一直運行,直到進程退出或被阻塞,纔會繼續從隊列中選擇第一個進程接着運行。

這彷佛很公平,可是當一個長做業先運行了,那麼後面的短做業等待的時間就會很長,不利於短做業。

FCFS 對長做業有利,適用於 CPU 繁忙型做業的系統,而不適用於 I/O 繁忙型做業的系統。

02 最短做業優先調度算法

最短做業優先(Shortest Job First, SJF)調度算法一樣也是顧名思義,它會優先選擇運行時間最短的進程來運行,這有助於提升系統的吞吐量。

SJF 調度算法

這顯然對長做業不利,很容易形成一種極端現象。

好比,一個長做業在就緒隊列等待運行,而這個就緒隊列有很是多的短做業,那麼就會使得長做業不斷的日後推,週轉時間變長,導致長做業長期不會被運行。

03 高響應比優先調度算法

前面的「先來先服務調度算法」和「最短做業優先調度算法」都沒有很好的權衡短做業和長做業。

那麼,高響應比優先 (Highest Response Ratio Next, HRRN)調度算法主要是權衡了短做業和長做業。

每次進行進程調度時,先計算「響應比優先級」,而後把「響應比優先級」最高的進程投入運行,「響應比優先級」的計算公式:

從上面的公式,能夠發現:

  • 若是兩個進程的「等待時間」相同時,「要求的服務時間」越短,「響應比」就越高,這樣短做業的進程容易被選中運行;

  • 若是兩個進程「要求的服務時間」相同時,「等待時間」越長,「響應比」就越高,這就兼顧到了長做業進程,由於進程的響應比能夠隨時間等待的增長而提升,當其等待時間足夠長時,其響應比即可以升到很高,從而得到運行的機會;

04 時間片輪轉調度算法

最古老、最簡單、最公平且使用最廣的算法就是時間片輪轉(Round Robin, RR)調度算法

RR 調度算法

每一個進程被分配一個時間段,稱爲時間片(Quantum),即容許該進程在該時間段中運行。

  • 若是時間片用完,進程還在運行,那麼將會把此進程從 CPU 釋放出來,並把 CPU 分配另一個進程;

  • 若是該進程在時間片結束前阻塞或結束,則 CPU 當即進行切換;

另外,時間片的長度就是一個很關鍵的點:

  • 若是時間片設得過短會致使過多的進程上下文切換,下降了 CPU 效率;

  • 若是設得太長又可能引發對短做業進程的響應時間變長。將

一般時間片設爲 20ms~50ms 一般是一個比較合理的折中值。

05 最高優先級調度算法

前面的「時間片輪轉算法」作了個假設,即讓全部的進程同等重要,也不偏袒誰,你們的運行時間都同樣。

可是,對於多用戶計算機系統就有不一樣的見解了,它們但願調度是有優先級的,即但願調度程序能從就緒隊列中選擇最高優先級的進程進行運行,這稱爲最高優先級(Highest Priority First,HPF)調度算法

進程的優先級能夠分爲,靜態優先級或動態優先級:

  • 靜態優先級:建立進程時候,就已經肯定了優先級了,而後整個運行時間優先級都不會變化;

  • 動態優先級:根據進程的動態變化調整優先級,好比若是進程運行時間增長,則下降其優先級,若是進程等待時間(就緒隊列的等待時間)增長,則升高其優先級,也就是隨着時間的推移增長等待進程的優先級

該算法也有兩種處理優先級高的方法,非搶佔式和搶佔式:

  • 非搶佔式:當就緒隊列中出現優先級高的進程,運行完當前進程,再選擇優先級高的進程。

  • 搶佔式:當就緒隊列中出現優先級高的進程,當前進程掛起,調度優先級高的進程運行。

可是依然有缺點,可能會致使低優先級的進程永遠不會運行。

06 多級反饋隊列調度算法

多級反饋隊列(Multilevel Feedback Queue)調度算法是「時間片輪轉算法」和「最高優先級算法」的綜合和發展。

顧名思義:

  • 「多級」表示有多個隊列,每一個隊列優先級從高到低,同時優先級越高時間片越短。

  • 「反饋」表示若是有新的進程加入優先級高的隊列時,馬上中止當前正在運行的進程,轉而去運行優先級高的隊列;

多級反饋隊列

來看看,它是如何工做的:

  • 設置了多個隊列,賦予每一個隊列不一樣的優先級,每一個隊列優先級從高到低,同時優先級越高時間片越短

  • 新的進程會被放入到第一級隊列的末尾,按先來先服務的原則排隊等待被調度,若是在第一級隊列規定的時間片沒運行完成,則將其轉入到第二級隊列的末尾,以此類推,直至完成;

  • 當較高優先級的隊列爲空,才調度較低優先級的隊列中的進程運行。若是進程運行時,有新進程進入較高優先級的隊列,則中止當前運行的進程並將其移入到原隊列末尾,接着讓較高優先級的進程運行;

能夠發現,對於短做業可能能夠在第一級隊列很快被處理完。對於長做業,若是在第一級隊列處理不完,能夠移入下次隊列等待被執行,雖然等待的時間變長了,可是運行時間也會更長了,因此該算法很好的兼顧了長短做業,同時有較好的響應時間。

看的迷迷糊糊?那我拿去銀行辦業務的例子,把上面的調度算法串起來,你還不懂,你錘我!

辦理業務的客戶至關於進程,銀行窗口工做人員至關於 CPU。

如今,假設這個銀行只有一個窗口(單核 CPU ),那麼工做人員一次只能處理一個業務。

銀行辦業務

那麼最簡單的處理方式,就是先來的先處理,後面來的就乖乖排隊,這就是先來先服務(FCFS)調度算法。可是萬一先來的這位老哥是來貸款的,這一談就好幾個小時,一直佔用着窗口,這樣後面的人只能乾等,或許後面的人只是想簡單的取個錢,幾分鐘就能搞定,卻由於前面老哥辦長業務而要等幾個小時,你說氣不氣人?

先來先服務

有客戶抱怨了,那咱們就要改進,咱們乾脆優先給那些幾分鐘就能搞定的人辦理業務,這就是短做業優先(SJF)調度算法。聽起來不錯,可是依然仍是有個極端狀況,萬一辦理短業務的人很是的多,這會致使長業務的人一直得不到服務,萬一這個長業務是個大客戶,那不就撿了芝麻丟了西瓜

最短做業優先

那就公平起見,如今窗口工做人員規定,每一個人我只處理 10 分鐘。若是 10 分鐘以內處理完,就立刻換下一我的。若是沒處理完,依然換下一我的,可是客戶本身得記住辦理到哪一個步驟了。這個也就是時間片輪轉(RR)調度算法。可是若是時間片設置太短,那麼就會形成大量的上下文切換,增大了系統開銷。若是時間片過長,至關於退化成退化成 FCFS 算法了。

時間片輪轉

既然公平也可能存在問題,那銀行就對客戶分等級,分爲普通客戶、VIP 客戶、SVIP 客戶。只要高優先級的客戶一來,就第一時間處理這個客戶,這就是最高優先級(HPF)調度算法。但依然也會有極端的問題,萬一當天來的全是高級客戶,那普通客戶不是沒有被服務的機會,不把普通客戶當人是嗎?那咱們把優先級改爲動態的,若是客戶辦理業務時間增長,則下降其優先級,若是客戶等待時間增長,則升高其優先級。

最高優先級(靜態)

那有沒有兼顧到公平和效率的方式呢?這裏介紹一種算法,考慮的還算充分的,多級反饋隊列(MFQ)調度算法,它是時間片輪轉算法和優先級算法的綜合和發展。它的工做方式:

多級反饋隊列
  • 銀行設置了多個排隊(就緒)隊列,每一個隊列都有不一樣的優先級,各個隊列優先級從高到低,同時每一個隊列執行時間片的長度也不一樣,優先級越高的時間片越短

  • 新客戶(進程)來了,先進入第一級隊列的末尾,按先來先服務原則排隊等待被叫號(運行)。若是時間片用完客戶的業務還沒辦理完成,則讓客戶進入到下一級隊列的末尾,以此類推,直至客戶業務辦理完成。

  • 當第一級隊列沒人排隊時,就會叫號二級隊列的客戶。若是客戶辦理業務過程當中,有新的客戶加入到較高優先級的隊列,那麼此時辦理中的客戶須要中止辦理,回到原隊列的末尾等待再次叫號,由於要把窗口讓給剛進入較高優先級隊列的客戶。

能夠發現,對於要辦理短業務的客戶來講,能夠很快的輪到並解決。對於要辦理長業務的客戶,一會兒解決不了,就能夠放到下一個隊列,雖然等待的時間稍微變長了,可是輪到本身的辦理時間也變長了,也能夠接受,不會形成極端的現象,能夠說是綜合上面幾種算法的優勢。


好文推薦

真棒!20 張圖揭開內存管理的迷霧,瞬間豁然開朗


嘮叨嘮叨

其實,關於進程和線程的部分,小林周末就已經寫好了。

可是,寫到調度算法的時候,我就懵逼了,在想用什麼方式能更通俗易懂的表達這些晦澀難懂的算法,這一小結花了我很是多時間。唉,菜就是菜,小林我也不找藉口了。。。

另外,最近小林創了技術交流羣,裏面的人說話又好聽,各個都是人才,有興趣的讀者,能夠掃一掃小林的私人微信二維碼,備註「加羣」便可。

掃一掃,進羣

若是你們在閱讀過程當中,發現了不理解或有錯誤的地方,歡迎跟在底部留言,大家的每一條留言,小林都會回覆。

小林是專爲你們圖解的工具人,Goodbye,咱們下次見!

本文分享自微信公衆號 - 小林coding(CodingLin)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索