每一個學習作業系統的人都不可避免地要接觸程式,執行緒(核心執行緒|使用者執行緒),協程,纖程等概念。針對這些概念有很多經典的問題,諸如程式和執行緒的區別等等。一開始我覺得辨析這些概念似乎有糾結"茴"字有幾種寫法之嫌,對理解作業系統無益。但當我想要寫一個自己的作業系統核心時,發現這些概念含混不清是我將真實作業系統中的技巧對映到自己程式碼的最大障礙。
1. 最經典:程式與執行緒的區別
簡單來說,程式是作業系統分配資源的最小單位,執行緒是作業系統排程的最小單位。
早期的作業系統並沒有執行緒,程式是作業系統執行程式的最小單位,但隨著多處理器的普及,程式這一抽象顯得很笨重[1]:
- 建立程式需要獨立的地址空間,載入資料和程式碼段,初始化堆疊。即使通過
fork()
建立程式也需要大量拷貝。 - 由於地址空間的隔離,不同程式之間通訊要麼以頁為單位維護公共對映,要麼採用訊號量等開銷較大的程式間通訊機制。
因此,當有一些輕量級的併發任務時,執行緒的概念呼之欲出:擁有獨立的執行流但共享地址空間。
2. 核心眼中的程式和執行緒
我們知道了執行緒是作業系統排程的最小單位,從狀態機的視角來看一個執行緒持有的狀態無非就是暫存器、棧、執行緒區域性變數。可是當作業系統在兩個程式之間切換時,不可避免地要涉及程式特有的狀態(比如通知父程式子程式的狀態改變等)。如果僅在切換執行緒時維護執行緒的上下文,似乎程式的抽象減弱了。
實際上,以Linux為例,它在建立執行緒時是通過clone()
這一系統呼叫。clone是用來建立程式的,不過在建立執行緒時會加上精細的控制選項使得新建立出來的程式和之前的程式擁有相同的pid,地址空間等等[1:1]。
可以看出,作業系統核心並不像上面所說的涇渭分明地區分程式與執行緒。更進一步地,在作業系統眼中它排程的是任務[2],任務有程式/執行緒的全部資訊,而核心並不知道一次排程是在同一程式的不同執行緒,還是涉及了不同程式切換。
3. 核心執行緒與使用者態執行緒
我們似乎從程式和核心兩個角度搞清楚了程式與執行緒的區別。但一個新概念的引入將會打亂上面的分析:核心執行緒和使用者執行緒。
這兩個執行緒最核心的區別就是排程器是核心還是在使用者態。
很多教科書上會提到,作業系統真正能排程的其實是核心執行緒,使用者執行緒只有通過核心執行緒才能訪問作業系統的資源。而核心執行緒與使用者執行緒的關聯又可分為多對一、一對一、多對多...[1:2]
在我們已經熟知的概念裡,我們通過pthreads
建立的執行緒直接被作業系統排程而且也可以主動陷入核心啊,那他們到底是使用者執行緒還是核心執行緒?
事實上,POSIX threads
只是一組公共介面,它建立的是使用者態執行緒,不過實現因作業系統而異。這組介面並沒有規定建立的執行緒採用哪種方式被排程,換句話說它也可以只在使用者態發生排程(介面並沒有規定)。只是大部分作業系統(Linux, BSD等)的實現都採用上圖中的一對一模型,讓我們產生了一種使用者態執行緒也直接受作業系統核心排程的錯覺。
以上圖的多對一模型為例,我們可以看看pthreads的另一種實現方式:
- 有一個專門的排程執行緒
master thread
,它與唯一的核心執行緒相連 - 其它使用者態執行緒的執行受master thread排程,當控制權交給master thread時,它可以通過執行緒訊號終止其它執行緒的執行完成排程。
即使我們知道了Linux採用一對一模型,但還存在一個問題:到底什麼是核心執行緒?它是區別於使用者執行緒額外的實體還是使用者執行緒陷入核心後就搖身一變成為了核心執行緒?
為了解釋這個概念,需要創造一些新名詞:
native threads
:在一對一模型中,使用者執行緒陷入核心後變成native threadskernel threads
:作業系統建立的用於執行額外工作的執行緒,比如回收資源等等
區分開這兩個名詞,這個問題的答案已經很清楚了。使用者態執行緒陷入核心後會發生棧切換等操作,實際上變成了核心中的執行緒,它會執行系統呼叫、被排程等等。但它和對應的使用者態執行緒不會同時存在。
作業系統核心有時還需要自己建立一些執行緒執行額外的工作,這些執行緒我稱之為kernel threads
,它們區別於作業系統上執行的使用者執行緒,是可以並行執行也是完全不同的實體。
從作業系統排程的角度來說,它們都是核心執行緒(任務
),但從功能上來說有必要區分開。
最後還有一點邊角問題,為什麼說作業系統只排程核心執行緒?
實際上即使是使用者執行緒,每次陷入核心後都會變成native threads,而作業系統排程的也都是陷入核心後的執行緒所以這麼說也不足為怪了。
4. 協程,纖程和其它...
在現在主流作業系統採用一對一模型的情況下,協程纖程其實更像是前面所謂的使用者態執行緒,它們的排程真正地發生在使用者態。
排程發生在使用者態,好處是更加輕量級,上下文較短。壞處自然是隻能主動地申請排程了。
在這裡不去展開這些名詞的概念了。程式進化到執行緒,執行緒進化到協程、纖程,本質上都是針對特定workload的一種刪減。