樸素的UNIX之-進程/線程模型

UNIX的傳統傾向於將一個任務交給一個進程全權受理。但是一個任務內部也不只僅是一個運行緒。比方一個公司的所有成員。你們都在作同一件事,每個人卻僅僅負責一部分。粒度減少以後,所有的事情便可以同一時候進行,不管怎樣,你們還都共享着所有的資源。

所以就出現了線程。shell

線程事實上就是共享資源的不一樣的運行緒。多線程

線程的語義和樸素的UNIX進程是不一樣的。
less

0.原始進程模型-著名的fork調用

樸素的UNIX進程依託於著名的fork調用。就是這個fork調用讓UNIX進程和Windows進程大相徑庭。也正是因爲這個fork調用,使兩者沒有兼容的餘地。這個fork調用的根源有久遠的歷史。早在UNIX以前的大型操做系統中,它就存在了。UNIX剛出現的1969年,事實上並未引入fork調用。當時之有兩個固定的進程鏈接兩個終端。當fork調用引入後,進程的數量便高速添加了,注意,此時暫且尚未exec調用!
       在理解fork背後的哲學以前,先看一下什麼是fork。fork就是叉子,由同一個叉子柄逐漸分叉,變成一把叉子,也類似那種道生一,一輩子二,二生三。三生萬物。咱們看到。有了fork。理論上可以生成無數的進程。它們都可以向上回溯到一樣的根!爲什麼UNIX會採用這個模型?咱們首先要理解,在尚未「可運行文件」概念的時候。進程意味着什麼。
       試想程序最初是怎麼錄入到計算機的。

今天它們理所固然地存在於磁盤上,做爲「可運行文件」已經深刻人心。但是在1950-1960年代初,程序都是現場錄入的。經過原始的紙帶或者攜帶很是重的磁帶,文件系統尚未概念。整個紙帶。磁帶上的內容就是計算機要運行的程序,運行完了,想運行還有一個程序,就要換介質...人們寫一個程序固然是爲了作一件不止作一次的事,所以假設可以有多個「進程」同一時候運行紙帶/磁帶上的程序,系統的吞吐率將大大提升,注意,多個進程運行的是同一個程序!ide

這是最樸素的分時系統進程模型。ui

fork在伯克利分時系統應運而生!操作系統

fork提供了複製當前運行流的手段。fork出來的所有子進程可以方便地運行一樣的代碼。
       這個著名的fork調用深深影響了人們怎樣解釋分時系統!命令行

天然而然在1970年代初引入了樸素的UNIX,說fork調用著名,就是因爲它尾隨UNIX(以及類UNIX。比方Linux)至今。直接影響了UNIX的進程模型。現在總結UNIX爲什麼採用fork調用來生成進程。咱們知道從0到1很是難,從1到2相對easy。也比較難,從2到3...就很是easy了。這就是道生一,...三生萬物。1969年的UNIX中已經有了兩個進程。使用fork可以超級簡單地實現二生三,三生萬物。因而,或許是一種巧合,早先的伯克利分時系統的fork正好就在那裏,便被托馬斯引入了UNIX。
       我想說一下爲什麼是三生萬物而不是二生萬物。道生一這個是最難的,咱們都知道。線程

0和1是兩個極其特殊的數字,0更加特殊。設計

2也比較特殊。但是3就很是通常了,爲什麼2特殊呢?我不想用博弈理論來描寫敘述。僅僅是舉一個樣例。2我的在一塊兒,聞到一股屁味,每個人都確定能百分百肯定是誰放的,假設是我,那我確定知道,假設我沒有放,那確定是對方。固然兩人一塊兒放的概率也是有的。指針

但是3我的在一塊兒的時候,除了真正放屁的那我的以外的2我的根本沒法推斷這個屁到底是誰放的。這就是3和0,1。2的本質差異。因此三生萬物。


1.UNIX進程模型

在UNIX伊始,進程的概念和其史前前輩是一致的,那個時候文件系統至關不成熟。程序猿關注的是運行好不easy寫好的任務而不是編寫任務自己(首先是沒有那麼大的需求,其次是信息存儲是一個問題,沒有互聯網。可以對照一下現在的AppStore...)。fork調用便直接將UNIX的進程組織成了tree,因而:
1.0號swap/sched進程和1號init進程便有了特殊地位。
2.造成了誰fork誰wait並回收的模型,在tree組織中這個很是重要,便於資源回收;
3.假設父進程先退出,將所有子進程過繼給init,這致使init必須存在且不容退出。總之。不論什麼進程不能脫離整個進程tree。

總之,樸素的UNIX進程就是處在一棵樹的某個節點的可運行對象。

注意。它是可運行對象。
       UNIX進程模型就是在上述基本原則上構建的。除此以外,在外圍,UNIX延續了歇菜的Multics項目的shell思想,爲每個終端開放了一個shell。shell是UNIX系統的第二個重要特徵(假設先不說文件抽象的話!)。它需要fork出來的進程exec出一個新的不一樣的運行流。

從以上fork/exec的歷史上看。它們從一開始就是分離的,這就構建了完整的UNIX進程模型:fork+exec
       咱們看一下UNIX的進程模型可以構建哪些東西。早期的UNIX將進程進行了組織。夥同終端的概念。UNIX給出了進程組,會話的概念。


       進程組是相關聯的一組進程的集合,比方管道符鏈接的各個命令。不少其它的是它們之間的關聯由用戶來解釋。會話則是進程組的集合。會話的意義在於用戶可以方便地讓多個進程組以某種形式共享終端訪問權。因爲坐在一個終端前的是一我的,他每次運行一個操做,這個操做做用給誰就是一個問題。

他可以建立一個會話。該會話內建立多個進程組,他以本身的方式讓不一樣的進程組輪流成爲前臺進程組從而操做它。

會話和進程組的概念可以理解成由操做員控制的分時系統,僅僅是調度者再也不是操做系統,而成了終端前的操做員。和每個CPU同一時候僅僅能有一個進程運行類似,每個終端會話同一時候僅僅能有一個前臺進程組。
       咱們可以看到。UNIX進程模型構建的進程組織天然而然造成了一個分級的分時調度層次。最底層是進程,由操做系統內核調度。而後是進程組,協做完畢一個任務,組織多個進程,由建立所屬會話的操做員調度。在這個分級的層次底層,所有的進程組織成一棵tree。這就是完整的UNIX進程模型構建的圖景。

之因此可以構建如此漂亮的圖景,fork+exec是基本原則。fork和exec之間。給了進程不少其它的控制本身的空間。怎樣控制本身屬於哪個組或者會話,由進程本身決定而不是調用者決定,相反的樣例請看一下Win32 API的CreateProcess。

現在麻煩來了,線程出現了,該怎麼辦?假設你想知道Linux是怎麼創造歷史的,請直接跳到最後。
       我之因此沒有說起不論什麼UNIX版本號對上述構建的實現,是因爲思想遠比實現更重要,實現反而會拖累你構建新的模型。本文的最後。我會說明Linux是怎樣調和不一樣的進程模型之間的語義的,同一時候印證了UNIX進程模型的先進性。

2.提供資源環境的進程模型

Windows NT儘管在很是多方面都借鑑了UNIX的思想。但是在進程模型上卻採用了一種大相徑庭的思路。Windows NT出生的1990年代。應用已經開始遍地開花。文件系統也已經很是成熟,可運行文件的概念延續自MS-DOS時代(事實上UNIXv6版本號就有可運行文件的概念,在UNIX引入exec調用以後。可運行文件僅僅是進程的後備資源。僅此而已)。人們可以基於Win32 API開發大量不一樣的程序,而後讓它們分別運行,假設你想讓一個程序運行屢次,多點擊它幾回即是了。
       在這樣的時代,正如本文最初所說的。運行的粒度細化到了一個程序的內部。

一個應用程序要完畢一項任務,需要作不一樣的幾件事,可能需要同一時候進行這幾件事,類似數學中的統籌方法。進程。在WinNT中也可以等同於從可運行文件裏抽取出來的命名資源集合,已經再也不適合做爲可運行的對象,真正可運行的對象成了線程。

此時的進程僅僅是提供了一個資源環境,線程使用這些可以共享的資源共同完畢詳細的事情。

這樣的提供資源環境的進程模型我稱爲資源模型。
       在本小節。我儘管以WinNT做爲樣例來描寫敘述第二種進程模型。僅僅是因爲它做爲這樣的模型的表明比較純粹。實際上,很是多的UNIX版本號也在努力融合fork模型和資源模型這二者。企圖既能繼承UNIX的語義,又能實現多線程調度。



3.兩種模型的調和

首先,fork模型和資源模型的衝突是明顯的,典型體現於下面兩個方面:
1.信號問題:究竟哪一個線程運行信號處理;
2.fork語義:假設已經運行了一個線程,在當中運行了fork,怎樣來解釋fork的是哪一個運行流。

當中第一個問題比較好解決。規定假設不是線程自身引起的異常致使的信號,就由隨意線程來處理,反之由引起異常的線程來處理。第二個問題比較棘手,棘手之處在於某個UNIX是怎麼實現進程模型的。
       在進程結構體或者u區中維護一個鏈表。保存線程控制塊指針!Oh,NO。這是怎麼回事啊。UNIX怎麼會忘了可運行的對象是進程啊。如此一來,進程豈不成了線程的容器?直接倒向了資源模型,然而本身確實是純正的UNIX!設計LWP是一個好方案嗎?多是,但是它引入很是多的高層抽象,顯得複雜了,假設幾年後再引入一個新的什麼什麼程呢?總之。不論什麼改動樸素UNIX進程模型的方法都不是好方法。那麼用戶庫級別的線程呢?這不屬於內核的範疇,但表現了內核的無能爲力。
       拋開實現,回到思想。

咱們再來看看進程,進程組。會話之間的關係。最主要的可運行對象是進程,上面的進程組。會話都是以某種組織形式對進程集合的封裝。每個集合都有一系列的資源可供這個集合中的進程共享。比方會話的環境變量。進程組的命令行變量等,線程是什麼呢,線程不就是一組運行流的集合共享內存地址空間嗎?明確了些什麼嗎?假設不明確,咱們可以把UNIX進程模型圖景中的進程改爲調度實體,僅僅需要在這個圖景的基礎上往下走一層,線程天然而然就被支持了:
線程,線程集合。進程組,會話...
換成調度實體的說法,就是:
調度實體,調度實體組,進程組。會話...
就像進程組裏面可以僅僅有一個進程,組ID等於進程ID同樣,進程裏面也可以僅僅有一個線程,線程ID就是進程ID。一切都統一到這個UNIX進程模型的圖景中了,假設一個線程集合僅僅有一個線程。那麼咱們就稱其爲進程,假設擁有不止一個線程,咱們就稱這個集合爲進程,而集合的元素爲線程。事實上,此時此刻,怎麼稱呼已經無所謂了。
       現在還缺什麼?缺的是怎樣實現線程集合共享內存地址空間。傳統的UNIX fork模型無疑沒法作到這一點,因爲它沒有不論什麼參數用來指示實現這樣的行爲。

因而需要略微改動一下fork語義。引入一個clone調用,含實用戶可以控制的參數:

int clone(int (*fn)(void *), void *child_stack,
          int flags, void *arg, ...
          /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

用戶不但可以控制用戶棧的位置,還可以有諸多的flags可供選擇。假設要共享調用者的內存,CLONE_VM這個標誌無疑是需要的。固然想clone線程不只僅需要這一個標誌。這裏就不細說了。詳細可以參考NPTL最新規範。

4.Linux的對UNIX進程模型的實現

Linux實現的線程支持很是帥,它差點兒沒有觸動不論什麼已經有的task_struct結構體。也沒有改變不論什麼既有的fork語義。它僅僅是引入了一個PID類型,叫作TGID,即進程組ID。Linux中的可運行對象就是task_struct,而且僅僅有task_struct。每個task_struct擁有不止一個ID,按照這些ID的不一樣的解釋方式即不一樣的類型。將task_struct定位到一個進程或者是一個進程的某個線程。ID類型例如如下所看到的:
enum pid_type
{
    PIDTYPE_PID,   
    PIDTYPE_TGID, 
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX
};

當中:
PIDTYPE_PID:調度實體ID。

假設該task_struct是一個進程的線程,那麼它就是線程ID,假設該進程僅僅有惟一的線程。那麼它同一時候也是進程ID。
PIDTYPE_TGID,:線程集合ID。假設該task_struct所屬的進程擁有多個線程。它就是進程ID,假設僅僅有一個線程,它等同於PIDTYPE_PID。
PIDTYPE_PGID:進程組ID。不解釋;
PIDTYPE_SID:會話ID。

不解釋。
依據上述解釋。不管一個進程擁有一個線程仍是擁有多個線程。其進程ID即PID均等於PIDTYPE_TGID標識的ID。

而PIDTYPE_PID標識的ID則依據詳細狀況給予不一樣的解釋。詳細實施例如如下:
1.每個task_struct均有一個本PID命名空間內惟一的ID標識符,初始化時將其同一時候賦給進程ID和線程ID。
2.假設該task_struct是一個進程的第一個線程,即由標準的fork調用建立,那麼保持1的初始化數值不變。
3.假設該task_struct不是一個進程的第一個線程。即由帶有CLONE_VM等的clone調用建立,那麼將當前調用者的PIDTYPE_TGID標識的ID覆蓋新task_struct的PIDTYPE_TGID標識的ID。
4.關於進程組ID以及會話ID的設置。有專門的setpgid, setpgrp,setsid等系統調用來完畢,實現很是類似上述進程和線程。
5.每個task_struct中有4個pid結構體。將這些pid結構體而不是task_struct自己用鏈表鏈接起來,指示誰是進程。誰是哪一個進程的線程,誰是哪一個進程組當頭的組成員...

總之。在Linux中。不管是線程,仍是進程,都是使用task_struct這個結構體。由其PID type的值的鏈接方式指示怎樣構建UNIX進程模型的圖景,這真的是太帥了。我的以爲仍是用一張圖表示鏈接方式比較直觀,文字表達在這方面弱爆了:




假設理解了上面的圖。就會明確Linux在實現UNIX進程模型方面作的是多麼帥。如此精簡的一個模型和Linux如此精簡的實現正好搭配,不知爲什麼被傳統的UNIX引到了那麼複雜的方向...Linux的實現明顯洞察到了UNIX進程模型的層次化結構,即進程,進程組。會話這三個層次,假設再往下延伸一個層次,將task_struct向下移動到最底層。就基本繪製出了上面的圖景。

5.一段富有詩意的話

丹尼斯.裏奇在回想UNIX的發展史時,在最後說了一段話。這段話簡直出自詩人之口,此詩意僅僅有真情實感真性情方可抒發,可見丹尼斯.裏奇對UNIX的感情是多麼特殊:
One of the comforting things about old memories is their tendencyto take on a rosy glow.The programming environment provided by the early versions of Unix seems,when described here, to be extremely harsh and primitive.I am sure that if forced back to the PDP-7 I would find it intolerably limiting andlacking in conveniences.Nevertheless, it did not seem so at the time;the memory fixes on what was good and what lasted, and on the joy of helpingto create the improvements that made life better.In ten years, I hope we can look back with the same mixed impressionof progress combined with continuity.
相關文章
相關標籤/搜索