您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關(guān)UNIX中的進程及線程模型是怎樣的,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。
UNIX的傳統(tǒng)傾向于將一個任務(wù)交給一個進程全權(quán)受理,但是一個任務(wù)內(nèi)部也不僅僅是一個執(zhí)行緒,比如一個公司的所有成員,大家都在做同一件事,每個人卻只
負責一部分,粒度減小之后,所有的事情便可以同時進行,不管怎樣,大家還都共享著所有的資源。因此就出現(xiàn)了線程。線程其實就是共享資源的不同的執(zhí)行緒。線程的語義和樸素的UNIX進程是不同的。
樸素的UNIX進程依托于著名的fork調(diào)用,
就是這個fork調(diào)用讓UNIX進程和Windows進程截然不同,也正是因為這個fork調(diào)用,使二者沒有兼容的余地。這個fork調(diào)用的根源有久遠的
歷史。早在UNIX之前的大型操作系統(tǒng)中,它就存在了,UNIX剛出現(xiàn)的1969年,其實并未引入fork調(diào)用,當時之有兩個固定的進程連接兩個終端。當
fork調(diào)用引入后,進程的數(shù)量便快速增加了,注意,此時暫且還沒有exec調(diào)用!
在理解fork背后的哲學(xué)之前,先看一下什么是fork。fork就是叉子,由同一個叉子柄逐漸分叉,變成一把叉子,也類似那種道生一,一生二,二生三,
三生萬物。我們看到,有了fork,理論上可以生成無數(shù)的進程,它們都可以向上回溯到相同的根!為何UNIX會采用這個模型?我們首先要理解,在還沒有
“可執(zhí)行文件”概念的時候,進程意味著什么。
試想程序最初是怎么錄入到計算機的。今天它們理所當然地存在于磁盤上,作為“可執(zhí)行文件”已經(jīng)深入人心,可是在1950-1960年代初,程序都是現(xiàn)場錄
入的,通過原始的紙帶或者攜帶很重的磁帶,文件系統(tǒng)還沒有概念,整個紙帶,磁帶上的內(nèi)容就是計算機要執(zhí)行的程序,執(zhí)行完了,想執(zhí)行另一個程序,就要換介
質(zhì)...人們寫一個程序當然是為了做一件不止做一次的事,因此如果可以有多個“進程”同時執(zhí)行紙帶/磁帶上的程序,系統(tǒng)的吞吐率將大大提高,注意,多個進
程執(zhí)行的是同一個程序!這是最樸素的分時系統(tǒng)進程模型。fork在伯克利分時系統(tǒng)應(yīng)運而生!fork提供了復(fù)制當前執(zhí)行流的手段,fork出來的所有子進
程可以方便地執(zhí)行相同的代碼。
這個著名的fork調(diào)用深深影響了人們?nèi)绾谓忉尫謺r系統(tǒng)!自然而然在1970年代初引入了樸素的UNIX,說fork調(diào)用著名,就是因為它跟隨
UNIX(以及類UNIX,比如Linux)至今,直接影響了UNIX的進程模型?,F(xiàn)在總結(jié)UNIX為何采用fork調(diào)用來生成進程。我們知道從0到1很
難,從1到2相對容易,也比較難,從2到3...就很簡單了。這就是道生一,...三生萬物!1969年的UNIX中已經(jīng)有了兩個進程,使用fork可以
超級簡單地實現(xiàn)二生三,三生萬物,于是,也許是一種巧合,早先的伯克利分時系統(tǒng)的fork正好就在那里,便被托馬斯引入了UNIX。
我想說一下為何是三生萬物而不是二生萬物。道生一這個是最難的,我們都知道。0和1是兩個極其特殊的數(shù)字,0更加特殊。2也比較特殊,但是3就很一般了,
為何2特殊呢?我不想用博弈理論來描述,只是舉一個例子,2個人在一起,聞到一股屁味,每個人都肯定能百分百確定是誰放的,如果是我,那我肯定知道,如果
我沒有放,那肯定是對方,當然兩人一起放的幾率也是有的。但是3個人在一起的時候,除了真正放屁的那個人之外的2個人根本無法判斷這個屁到底是誰放的。這
就是3和0,1,2的本質(zhì)區(qū)別。所以三生萬物。
在UNIX伊始,進程的概念和其史前前輩是一致的,那個時
候文件系統(tǒng)相當不成熟,程序員關(guān)注的是執(zhí)行好不容易寫好的任務(wù)而不是編寫任務(wù)本身(首先是沒有那么大的需求,其次是信息存儲是一個問題,沒有互聯(lián)網(wǎng),可以
對比一下如今的AppStore...)。fork調(diào)用便直接將UNIX的進程組織成了tree,于是:
1.0號swap/sched進程和1號init進程便有了特殊地位;
2.形成了誰fork誰wait并回收的模型,在tree組織中這個很重要,便于資源回收;
3.如果父進程先退出,將所有子進程過繼給init,這導(dǎo)致init必須存在且不容退出,總之,任何進程不能脫離整個進程tree。
總之,樸素的UNIX進程就是處在一棵樹的某個節(jié)點的可執(zhí)行對象。注意,它是可執(zhí)行對象。
UNIX進程模型就是在上述基本原則上構(gòu)建的,除此之外,在外圍,UNIX延續(xù)了歇菜的Multics項目的shell思想,為每一個終端開放了一個
shell。shell是UNIX系統(tǒng)的第二個重要特征(如果先不說文件抽象的話!),它需要fork出來的進程exec出一個新的不同的執(zhí)行流。從以上
fork/exec的歷史上看,它們從一開始就是分離的,這就構(gòu)建了完整的UNIX進程模型:fork+exec。
我們看一下UNIX的進程模型可以構(gòu)建哪些東西。早期的UNIX將進程進行了組織,伙同終端的概念,UNIX給出了進程組,會話的概念。
進程組是相關(guān)聯(lián)的一組進程的集合,比如管道符連接的各個命令。更多的是它們之間的關(guān)聯(lián)由用戶來解釋。會話則是進程組的集合,會話的意義在于用戶可以方便地
讓多個進程組以某種形式共享終端訪問權(quán)。因為坐在一個終端前的是一個人,他每次執(zhí)行一個操作,這個操作作用給誰就是一個問題。他可以創(chuàng)建一個會話,該會話
內(nèi)創(chuàng)建多個進程組,他以自己的方式讓不同的進程組輪流成為前臺進程組從而操作它。會話和進程組的概念可以理解成由操作員控制的分時系統(tǒng),只是調(diào)度者不再是
操作系統(tǒng),而成了終端前的操作員。和每個CPU同時只能有一個進程運行類似,每一個終端會話同時只能有一個前臺進程組。
我們可以看到,UNIX進程模型構(gòu)建的進程組織自然而然形成了一個分級的分時調(diào)度層次,最底層是進程,由操作系統(tǒng)內(nèi)核調(diào)度,然后是進程組,協(xié)作完成一個任
務(wù),組織多個進程,由創(chuàng)建所屬會話的操作員調(diào)度。在這個分級的層次底層,所有的進程組織成一棵tree。這就是完整的UNIX進程模型構(gòu)建的圖景。之所以
可以構(gòu)建如此美麗的圖景,fork+exec是基本原則,fork和exec之間,給了進程更多的控制自己的空間,如何控制自己屬于哪一個組或者會話,由
進程自己決定而不是調(diào)用者決定,相反的例子請看一下Win32
API的CreateProcess。現(xiàn)在麻煩來了,線程出現(xiàn)了,該怎么辦?如果你想知道Linux是怎么創(chuàng)造歷史的,請直接跳到最后。
我之所以沒有提及任何UNIX版本對上述構(gòu)建的實現(xiàn),是因為思想遠比實現(xiàn)更重要,實現(xiàn)反而會拖累你構(gòu)建新的模型。本文的最后,我會說明Linux是如何調(diào)和不同的進程模型之間的語義的,同時印證了UNIX進程模型的先進性。
Windows
NT雖然在很多方面都借鑒了UNIX的思想,但是在進程模型上卻采用了一種截然不同的思路。Windows
NT出生的1990年代,應(yīng)用已經(jīng)開始遍地開花,文件系統(tǒng)也已經(jīng)非常成熟,可執(zhí)行文件的概念延續(xù)自MS-DOS時代(其實UNIXv6版本就有可執(zhí)行文件
的概念,在UNIX引入exec調(diào)用之后,可執(zhí)行文件僅僅是進程的后備資源,僅此而已),人們可以基于Win32
API開發(fā)大量不同的程序,然后讓它們分別運行,如果你想讓一個程序執(zhí)行多次,多點擊它幾次便是了。
在這樣的時代,正如本文最初所說的,執(zhí)行的粒度細化到了一個程序的內(nèi)部。一個應(yīng)用程序要完成一項任務(wù),需要做不同的幾件事,可能需要同時進行這幾件事,類
似數(shù)學(xué)中的統(tǒng)籌方法。進程,在WinNT中也可以等同于從可執(zhí)行文件中抽取出來的命名資源集合,已經(jīng)不再適合作為可執(zhí)行的對象,真正可執(zhí)行的對象成了線
程。此時的進程只是提供了一個資源環(huán)境,線程使用這些可以共享的資源共同完成具體的事情。這種提供資源環(huán)境的進程模型我稱為資源模型。
在本小節(jié),我雖然以WinNT作為例子來描述另外一種進程模型,只是因為它作為這種模型的代表比較純粹。實際上,很多的UNIX版本也在努力融合fork模型和資源模型這兩者,企圖既能繼承UNIX的語義,又能實現(xiàn)多線程調(diào)度。
首先,fork模型和資源模型的沖突是明顯的,典型體現(xiàn)于以下兩個方面:
1.信號問題:到底哪個線程執(zhí)行信號處理;
2.fork語義:假設(shè)已經(jīng)運行了一個線程,在其中執(zhí)行了fork,如何來解釋fork的是哪個執(zhí)行流;
其中第一個問題比較好解決,規(guī)定如果不是線程自身引發(fā)的異常導(dǎo)致的信號,就由任意線程來處理,反之由引發(fā)異常的線程來處理。第二個問題比較棘手,棘手之處在于某個UNIX是怎么實現(xiàn)進程模型的。
在進程結(jié)構(gòu)體或者u區(qū)中維護一個鏈表,保存線程控制塊指針!Oh,NO!這是怎么回事??!UNIX怎么會忘了可執(zhí)行的對象是進程??!如此一來,進程豈不成
了線程的容器?直接倒向了資源模型,然而自己確實是純正的UNIX!設(shè)計LWP是一個好方案嗎?可能是,但是它引入很多的高層抽象,顯得復(fù)雜了,如果幾年
后再引入一個新的什么什么程呢?總之,任何修改樸素UNIX進程模型的方法都不是好方法。那么用戶庫級別的線程呢?這不屬于內(nèi)核的范疇,但表現(xiàn)了內(nèi)核的無
能為力。
拋開實現(xiàn),回到思想。我們再來看看進程,進程組,會話之間的關(guān)系,最基本的可執(zhí)行對象是進程,上面的進程組,會話都是以某種組織形式對進程集合的封裝,每
個集合都有一系列的資源可供這個集合中的進程共享。比如會話的環(huán)境變量,進程組的命令行變量等,線程是什么呢,線程不就是一組執(zhí)行流的集合共享內(nèi)存地址空
間嗎?明白了些什么嗎?如果不明白,我們可以把UNIX進程模型圖景中的進程改成調(diào)度實體,只需要在這個圖景的基礎(chǔ)上往下走一層,線程自然而然就被支持
了:
線程,線程集合,進程組,會話...
換成調(diào)度實體的說法,就是:
調(diào)度實體,調(diào)度實體組,進程組,會話...
就
像進程組里面可以只有一個進程,組ID等于進程ID一樣,進程里面也可以只有一個線程,線程ID就是進程ID。一切都統(tǒng)一到這個UNIX進程模型的圖景中
了,如果一個線程集合只有一個線程,那么我們就稱其為進程,如果擁有不止一個線程,我們就稱這個集合為進程,而集合的元素為線程。其實,此時此刻,怎么稱
呼已經(jīng)無所謂了。
現(xiàn)在還缺什么?缺的是如何實現(xiàn)線程集合共享內(nèi)存地址空間。傳統(tǒng)的UNIX fork模型無疑無法做到這一點,因為它沒有任何參數(shù)用來指示實現(xiàn)這種行為。于是需要稍微修改一下fork語義,引入一個clone調(diào)用,含有用戶可以控制的參數(shù):
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
用戶不但可以控制用戶棧的位置,還可以有諸多的flags可供選擇,如果要共享調(diào)用者的內(nèi)存,CLONE_VM這個標志無疑是需要的,當然想clone線程不僅僅需要這一個標志,這里就不細說了,具體可以參考NPTL最新規(guī)范。
Linux
實現(xiàn)的線程支持非常帥,它幾乎沒有觸動任何已經(jīng)有的task_struct結(jié)構(gòu)體,也沒有改變?nèi)魏渭扔械膄ork語義。它只是引入了一個PID類型,叫做
TGID,即進程組ID。Linux中的可執(zhí)行對象就是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:調(diào)度實體ID。如果該task_struct是一個進程的線程,那么它就是線程ID,如果該進程只有唯一的線程,那么它同時也是進程ID;
PIDTYPE_TGID,:線程集合ID。如果該task_struct所屬的進程擁有多個線程,它就是進程ID,如果只有一個線程,它等同于PIDTYPE_PID;
PIDTYPE_PGID:進程組ID。不解釋;
PIDTYPE_SID:會話ID。不解釋。
根據(jù)上述解釋,不管一個進程擁有一個線程還是擁有多個線程,其進程ID即PID均等于PIDTYPE_TGID標識的ID。而PIDTYPE_PID標識的ID則根據(jù)具體情況給予不同的解釋。具體實施如下:
1.每一個task_struct均有一個本PID命名空間內(nèi)唯一的ID標識符,初始化時將其同時賦給進程ID和線程ID;
2.如果該task_struct是一個進程的第一個線程,即由標準的fork調(diào)用創(chuàng)建,那么保持1的初始化數(shù)值不變;
3.如果該task_struct不是一個進程的第一個線程,即由帶有CLONE_VM等的clone調(diào)用創(chuàng)建,那么將當前調(diào)用者的PIDTYPE_TGID標識的ID覆蓋新task_struct的PIDTYPE_TGID標識的ID;
4.關(guān)于進程組ID以及會話ID的設(shè)置,有專門的setpgid, setpgrp,setsid等系統(tǒng)調(diào)用來完成,實現(xiàn)很類似上述進程和線程;
5.每個task_struct中有4個pid結(jié)構(gòu)體,將這些pid結(jié)構(gòu)體而不是task_struct本身用鏈表連接起來,指示誰是進程,誰是哪個進程的線程,誰是哪個進程組當頭的組成員...
總之,在Linux中,不管是線程,還是進程,都是使用task_struct這個結(jié)構(gòu)體,由其PID type的值的連接方式指示如何構(gòu)建UNIX進程模型的圖景,這真的是太帥了。個人認為還是用一張圖表示連接方式比較直觀,文字表達在這方面弱爆了:
如
果理解了上面的圖,就會明白Linux在實現(xiàn)UNIX進程模型方面做的是多么帥。如此精簡的一個模型和Linux如此精簡的實現(xiàn)正好搭配,不知為何被傳統(tǒng)
的UNIX引到了那么復(fù)雜的方向...Linux的實現(xiàn)明顯洞察到了UNIX進程模型的層次化結(jié)構(gòu),即進程,進程組,會話這三個層次,如果再往下延伸一個
層次,將task_struct向下移動到最底層,就基本繪制出了上面的圖景。
關(guān)于UNIX中的進程及線程模型是怎樣的就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。