進程、線程、協程
進程和線程的關係:
協程與線程的微妙關係
並行和併發
同步和異步
我們編寫的代碼只是一個存儲在硬盤的靜態文件,通過編譯後就會生成二進制可執行文件,當我們運行這個可執行文件後,它會被裝載到內存中,接着 CPU 會執行程序中的每一條指令,那麼這個運行中的程序,就被稱爲「進程」。
現在我們考慮有一個會讀取硬盤文件數據的程序被執行了,那麼當運行到讀取文件的指令時,就會去從硬盤讀取數據,但是硬盤的讀寫速度是非常慢的,那麼在這個時候,如果 CPU 傻傻的等硬盤返回數據的話,那 CPU 的利用率是非常低的。
做個類比,你去煮開水時,你會傻傻的等水壺燒開嗎?很明顯,小孩也不會傻等。我們可以在水壺燒開之前去做其他事情。當水壺燒開了,我們自然就會聽到「嘀嘀嘀」的聲音,於是再把燒開的水倒入到水杯裏就好了。
所以,當進程要從硬盤讀取數據時,CPU 不需要阻塞等待數據的返回,而是去執行另外的進程。當硬盤數據返回時,CPU 會收到箇中斷,於是 CPU 再繼續運行這個進程。
這種多個程序、交替執行的思想,就有 CPU 管理多個進程的初步想法。
對於一個支持多進程的系統,CPU 會從一個進程快速切換至另一個進程,其間每個進程各運行幾十或幾百個毫秒。
雖然單核的 CPU 在某一個瞬間,只能運行一個進程。但在 1 秒鐘期間,它可能會運行多個進程,這樣就產生並行的錯覺,實際上這是併發。
到了晚飯時間,一對小情侶肚子都咕咕叫了,於是男生見機行事,就想給女生做晚飯,所以他就在網上找了辣子雞的菜譜,接着買了一些雞肉、辣椒、香料等材料,然後邊看邊學邊做這道菜。
突然,女生說她想喝可樂,那麼男生只好把做菜的事情暫停一下,並在手機菜譜標記做到哪一個步驟,把狀態信息記錄了下來。
然後男生聽從女生的指令,跑去下樓買了一瓶冰可樂後,又回到廚房繼續做菜。
這體現了,CPU 可以從一個進程(做菜)切換到另外一個進程(買可樂),在切換前必須要記錄當前進程中運行的狀態信息,以備下次切換回來的時候可以恢復執行。
所以,可以發現進程有着「運行 - 暫停 - 運行」的活動規律。
在上面,我們知道了進程有着「運行 - 暫停 - 運行」的活動規律。一般說來,一個進程並不是自始至終連續不停地運行的,它與併發執行中的其他進程的執行是相互制約的。
它有時處於運行狀態,有時又由於某種原因而暫停運行處於等待狀態,當使它暫停的原因消失後,它又進入準備運行狀態。
所以,在一個進程的活動期間至少具備三種基本狀態,即運行狀態、就緒狀態、阻塞狀態。
上圖中各個狀態的意義:
運行狀態(Runing):該時刻進程佔用 CPU;
就緒狀態(Ready):可運行,但因爲其他進程正在運行而暫停停止;
阻塞狀態(Blocked):該進程正在等待某一事件發生(如等待輸入/輸出操作的完成)而暫時停止運行,這時,即使給它CPU控制權,它也無法運行;
當然,進程另外兩個基本狀態:
創建狀態(new):進程正在被創建時的狀態;
結束狀態(Exit):進程正在從系統中消失時的狀態;
於是,一個完整的進程狀態的變遷如下圖:
再來詳細說明一下進程的狀態變遷:
NULL -> 創建狀態:一個新進程被創建時的第一個狀態;
創建狀態 -> 就緒狀態:當進程被創建完成並初始化後,一切就緒準備運行時,變爲就緒狀態,這個過程是很快的;
就緒態 -> 運行狀態:處於就緒狀態的進程被操作系統的進程調度器選中後,就分配給 CPU 正式運行該進程;
運行狀態 -> 結束狀態:當進程已經運行完成或出錯時,會被操作系統作結束狀態處理;
運行狀態 -> 就緒狀態:處於運行狀態的進程在運行過程中,由於分配給它的運行時間片用完,操作系統會把該進程變爲就緒態,接着從就緒態選中另外一個進程運行;
運行狀態 -> 阻塞狀態:當進程請求某個事件且必須等待時,例如請求 I/O 事件;
阻塞狀態 -> 就緒狀態:當進程要等待的事件完成時,它從阻塞狀態變到就緒狀態;
另外,還有一個狀態叫掛起狀態,它表示進程沒有佔有物理內存空間。這跟阻塞狀態是不一樣,阻塞狀態是等待某個事件的返回。
由於虛擬內存管理原因,進程的所使用的空間可能並沒有映射到物理內存,而是在硬盤上,這時進程就會出現掛起狀態,另外調用 sleep 也會被掛起。
掛起狀態可以分爲兩種:
阻塞掛起狀態:進程在外存(硬盤)並等待某個事件的出現;
就緒掛起狀態:進程在外存(硬盤),但只要進入內存,即刻立刻運行;
這兩種掛起狀態加上前面的五種狀態,就變成了七種狀態變遷(留給我的顏色不多了),見如下圖:
在操作系統中,是用進程控制塊(process control block,PCB)數據結構來描述進程的。
那 PCB 是什麼呢?打開知乎搜索你就會發現這個東西並不是那麼簡單。
PCB 是進程存在的唯一標識,這意味着一個進程的存在,必然會有一個 PCB,如果進程消失了,那麼 PCB 也會隨之消失。
PCB 具體包含什麼信息呢?
進程描述信息:
進程控制和管理信息:
資源分配清單:
CPU 相關信息:
可見,PCB 包含信息還是比較多的。
每個 PCB 是如何組織的呢?
通常是通過鏈表的方式進行組織,把具有相同狀態的進程鏈在一起,組成各種隊列。比如:
那麼,就緒隊列和阻塞隊列鏈表的組織形式如下圖:
除了鏈接的組織方式,還有索引方式,它的工作原理:將同一狀態的進程組織在一個索引表中,索引表項指向相應的 PCB,不同狀態對應不同的索引表。
一般會選擇鏈表,因爲可能面臨進程創建,銷燬等調度導致進程狀態發生變化,所以鏈表能夠更加靈活的插入和刪除。
01 創建進程
操作系統允許一個進程創建另一個進程,而且允許子進程繼承父進程所擁有的資源,當子進程被終止時,其在父進程處繼承的資源應當還給父進程。同時,終止父進程時同時也會終止其所有的子進程。
創建進程的過程如下:
爲新進程分配一個唯一的進程標識號,並申請一個空白的 PCB,PCB 是有限的,若申請失敗則創建失敗;
爲進程分配資源,此處如果資源不足,進程就會進入等待狀態,以等待資源;
初始化 PCB;
如果進程的調度隊列能夠接納新進程,那就將進程插入到就緒隊列,等待被調度運行;
02 終止進程
進程可以有 3 種終止方式:正常結束、異常結束以及外界干預(信號kill 掉)。
終止進程的過程如下:
03 阻塞進程
當進程需要等待某一事件完成時,它可以調用阻塞語句把自己阻塞等待。而一旦被阻塞等待,它只能由另一個進程喚醒。
阻塞進程的過程如下:
04 喚醒進程
進程由「運行」轉變爲「阻塞」狀態是由於進程必須等待某一事件的完成,所以處於阻塞狀態的進程是絕對不可能叫醒自己的。
如果某進程正在等待 I/O 事件,需由別的進程發消息給它,則只有當該進程所期待的事件出現時,才由發現者進程用喚醒語句叫醒它。
喚醒進程的過程如下:
進程的阻塞和喚醒是一對功能相反的語句,如果某個進程調用了阻塞語句,則必有一個與之對應的喚醒語句。
各個進程之間是共享 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 中,這使得這個進程可以繼續執行,如下圖所示:
大家需要注意,進程的上下文開銷是很關鍵的,我們希望它的開銷越小越好,這樣可以使得進程可以把更多時間花費在執行程序上,而不是耗費在上下文切換。
發生進程上下文切換有哪些場景?
在早期的操作系統中都是以進程作爲獨立運行的基本單位,直到後面,計算機科學家們又提出了更小的能獨立運行的基本單位,也就是線程。
我們舉個例子,假設你要編寫一個視頻播放器軟件,那麼該軟件功能的核心模塊有三個:
對於單進程的實現方式,我想大家都會是以下這個方式:
.
對於單進程的這種方式,存在以下問題:
那改進成多進程的方式:
對於多進程的這種方式,依然會存在問題:
那到底如何解決呢?需要有一種新的實體,滿足以下特性:
這個新的實體,就是線程( Thread ),線程之間可以併發運行且共享相同的地址空間。
線程是進程當中的一條執行流程。
同一個進程內多個線程之間可以共享代碼段、數據段、打開的文件等資源,但每個線程都有獨立一套的寄存器和棧,這樣可以確保線程的控制流是相對獨立的。
線程的優缺點
線程的優點:
線程的缺點:
線程與進程的比較如下:
對於,線程相比進程能減少開銷,體現在:
所以,線程比進程不管是時間效率,還是空間效率都要高。
在前面我們知道了,線程與進程最大的區別在於:線程是調度的基本單位,而進程則是資源擁有的基本單位。
所以,所謂操作系統的任務調度,實際上的調度對象是線程,而進程只是給線程提供了虛擬內存、全局變量等資源。
對於線程和進程,我們可以這麼理解:
另外,線程也有自己的私有數據,比如棧和寄存器等,這些在上下文切換時也是需要保存的。
線程上下文切換的是什麼?
這還得看線程是不是屬於同一個進程:
所以,線程的上下文切換相比進程,開銷要小很多。
進程都希望自己能夠佔用 CPU 進行工作,那麼這涉及到前面說過的進程上下文切換。
一旦操作系統把進程切換到運行狀態,也就意味着該進程佔用着 CPU 在執行,但是當操作系統把進程切換到其他狀態時,那就不能在 CPU 中執行了,於是操作系統會選擇下一個要運行的進程。
選擇一個進程運行這一功能是在操作系統中完成的,通常稱爲調度程序(scheduler)。
那到底什麼時候調度進程,或以什麼原則來調度進程呢?
在進程的生命週期中,當進程從一個運行狀態到另外一狀態變化的時候,其實會觸發一次調度。
比如,以下狀態的變化都會觸發操作系統的調度:
因爲,這些狀態變化的時候,操作系統需要考慮是否要讓新的進程給 CPU 運行,或者是否讓當前進程從 CPU 上退出來而換另一個進程運行。
另外,如果硬件時鐘提供某個頻率的週期性中斷,那麼可以根據如何處理時鐘中斷
把調度算法分爲兩類:
原則一:如果運行的程序,發生了 I/O 事件的請求,那 CPU 使用率必然會很低,因爲此時進程在阻塞等待硬盤的數據返回。這樣的過程,勢必會造成 CPU 突然的空閒。所以,爲了提高 CPU 利用率,在這種發送 I/O 事件致使 CPU 空閒的情況下,調度程序需要從就緒隊列中選擇一個進程來運行。
原則二:有的程序執行某個任務花費的時間會比較長,如果這個程序一直佔用着 CPU,會造成系統吞吐量(CPU 在單位時間內完成的進程數量)的降低。所以,要提高系統的吞吐率,調度程序要權衡長任務和短任務進程的運行完成數量。
原則三:從進程開始到結束的過程中,實際上是包含兩個時間,分別是進程運行時間和進程等待時間,這兩個時間總和就稱爲週轉時間。進程的週轉時間越小越好,如果進程的等待時間很長而運行時間很短,那週轉時間就很長,這不是我們所期望的,調度程序應該避免這種情況發生。
原則四:處於就緒隊列的進程,也不能等太久,當然希望這個等待的時間越短越好,這樣可以使得進程更快的在 CPU 中執行。所以,就緒隊列中進程的等待時間也是調度程序所需要考慮的原則。
原則五:對於鼠標、鍵盤這種交互式比較強的應用,我們當然希望它的響應時間越快越好,否則就會影響用戶體驗了。所以,對於交互式比較強的應用,響應時間也是調度程序需要考慮的原則。
針對上面的五種調度原則,總結成如下:
說白了,這麼多調度原則,目的就是要達到天下武功,唯快不破!。
不同的調度算法適用的場景也是不同的。
接下來,說說在單核 CPU 系統中常見的調度算法。
先來先服務調度算法
最簡單的一個調度算法,就是非搶佔式的先來先服務(First Come First Severd, FCFS)算法了
顧名思義,先來後到,每次從就緒隊列選擇最先進入隊列的進程,然後一直運行,直到進程退出或被阻塞,纔會繼續從隊列中選擇第一個進程接着運行。
這似乎很公平,但是當一個長作業先運行了,那麼後面的短作業等待的時間就會很長,不利於短作業。
FCFS 對長作業有利,適用於 CPU 繁忙型作業的系統,而不適用於 I/O 繁忙型作業的系統。
最短作業優先調度算法
最短作業優先(Shortest Job First, SJF)調度算法同樣也是顧名思義,它會優先選擇運行時間最短的進程來運行,這有助於提高系統的吞吐量。
這顯然對長作業不利,很容易造成一種極端現象。
比如,一個長作業在就緒隊列等待運行,而這個就緒隊列有非常多的短作業,那麼就會使得長作業不斷的往後推,週轉時間變長,致使長作業長期不會被運行。
高響應比優先調度算法
前面的「先來先服務調度算法」和「最短作業優先調度算法」都沒有很好的權衡短作業和長作業。
那麼,高響應比優先 (Highest Response Ratio Next, HRRN)調度算法主要是權衡了短作業和長作業。
每次進行進程調度時,先計算「響應比優先級」,然後把「響應比優先級」最高的進程投入運行,「響應比優先級」的計算公式:
從上面的公式,可以發現:
如果兩個進程的「等待時間」相同時,「要求的服務時間」越短,「響應比」就越高,這樣短作業的進程容易被選中運行;
如果兩個進程「要求的服務時間」相同時,「等待時間」越長,「響應比」就越高,這就兼顧到了長作業進程,因爲進程的響應比可以隨時間等待的增加而提高,當其等待時間足夠長時,其響應比便可以升到很高,從而獲得運行的機會;
時間片輪轉調度算法
最古老、最簡單、最公平且使用最廣的算法就是時間片輪轉(Round Robin, RR)調度算法。
每個進程被分配一個時間段,稱爲時間片(Quantum),即允許該進程在該時間段中運行。
另外,時間片的長度就是一個很關鍵的點:
最高優先級調度算法
前面的「時間片輪轉算法」做了個假設,即讓所有的進程同等重要,也不偏袒誰,大家的運行時間都一樣。
但是,對於多用戶計算機系統就有不同的看法了,它們希望調度是有優先級的,即希望調度程序能從就緒隊列中選擇最高優先級的進程進行運行,這稱爲最高優先級(Highest Priority First,HPF)調度算法。
進程的優先級可以分爲,靜態優先級或動態優先級:
該算法也有兩種處理優先級高的方法,非搶佔式和搶佔式:
但是依然有缺點,可能會導致低優先級的進程永遠不會運行。
多級反饋隊列調度算法
多級反饋隊列(Multilevel Feedback Queue)調度算法是「時間片輪轉算法」和「最高優先級算法」的綜合和發展。
顧名思義:
來看看,它是如何工作的:
可以發現,對於短作業可能可以在第一級隊列很快被處理完。對於長作業,如果在第一級隊列處理不完,可以移入下次隊列等待被執行,雖然等待的時間變長了,但是運行時間也會更長了,所以該算法很好的兼顧了長短作業,同時有較好的響應時間。
看的迷迷糊糊?那我拿去銀行辦業務的例子,把上面的調度算法串起來,你還不懂,你錘我!
辦理業務的客戶相當於進程,銀行窗口工作人員相當於 CPU。
現在,假設這個銀行只有一個窗口(單核 CPU ),那麼工作人員一次只能處理一個業務。
那麼最簡單的處理方式,就是先來的先處理,後面來的就乖乖排隊,這就是先來先服務(FCFS)調度算法。但是萬一先來的這位老哥是來貸款的,這一談就好幾個小時,一直佔用着窗口,這樣後面的人只能乾等,或許後面的人只是想簡單的取個錢,幾分鐘就能搞定,卻因爲前面老哥辦長業務而要等幾個小時,你說氣不氣人?
有客戶抱怨了,那我們就要改進,我們乾脆優先給那些幾分鐘就能搞定的人辦理業務,這就是短作業優先(SJF)調度算法。聽起來不錯,但是依然還是有個極端情況,萬一辦理短業務的人非常的多,這會導致長業務的人一直得不到服務,萬一這個長業務是個大客戶,那不就撿了芝麻丟了西瓜
那就公平起見,現在窗口工作人員規定,每個人我只處理 10 分鐘。如果 10 分鐘之內處理完,就馬上換下一個人。如果沒處理完,依然換下一個人,但是客戶自己得記住辦理到哪個步驟了。這個也就是時間片輪轉(RR)調度算法。但是如果時間片設置過短,那麼就會造成大量的上下文切換,增大了系統開銷。如果時間片過長,相當於退化成退化成 FCFS 算法了。
既然公平也可能存在問題,那銀行就對客戶分等級,分爲普通客戶、VIP 客戶、SVIP 客戶。只要高優先級的客戶一來,就第一時間處理這個客戶,這就是最高優先級(HPF)調度算法。但依然也會有極端的問題,萬一當天來的全是高級客戶,那普通客戶不是沒有被服務的機會,不把普通客戶當人是嗎?那我們把優先級改成動態的,如果客戶辦理業務時間增加,則降低其優先級,如果客戶等待時間增加,則升高其優先級。
那有沒有兼顧到公平和效率的方式呢?這裏介紹一種算法,考慮的還算充分的,多級反饋隊列(MFQ)調度算法,它是時間片輪轉算法和優先級算法的綜合和發展。它的工作方式:
可以發現,對於要辦理短業務的客戶來說,可以很快的輪到並解決。對於要辦理長業務的客戶,一下子解決不了,就可以放到下一個隊列,雖然等待的時間稍微變長了,但是輪到自己的辦理時間也變長了,也可以接受,不會造成極端的現象,可以說是綜合上面幾種算法的優點。
https://blog.csdn.net/FL63Zv9Zou86950w/article/details/107373399