1. 程式人生 > >Linux程序與執行緒的區別 詳細總結

Linux程序與執行緒的區別 詳細總結

首先,簡要了解一下程序和執行緒。對於作業系統而言,程序是核心之核心,整個現代作業系統的根本,就是以程序為單位在執行任務。系統的管理架構也是基於程序層面的。在按下電源鍵之後,計算機就開始了複雜的啟動過程,此處有一個經典問題:當按下電源鍵之後,計算機如何把自己由靜止啟動起來的?本文不討論系統啟動過程,請讀者自行科普。作業系統啟動的過程簡直可以描述為上帝創造萬物的過程,期初沒有世界,但是有上帝,是上帝創造了世界,之後創造了萬物,然後再創造了人,然後塑造了人的七情六慾,再然後人類社會開始遵循自然規律繁衍生息。。。作業系統啟動程序的階段就相當於上帝造人的階段。本文討論的全部內容都是“上帝造人”之後的事情。第一個被創造出來的程序是0號程序,這個程序在作業系統層面是不可見的,但它存在著。0號程序完成了作業系統的功能載入與初期設定,然後它創造了1號程序(init),這個1號程序就是作業系統的“耶穌”。1號程序是上帝派來管理整個作業系統的,所以在用pstree檢視程序樹可知,1號程序位於樹根。再之後,系統的很多管理程式都以程序身份被1號程序創造出來,還創造了與人類溝通的橋樑——shell。從那之後,人類可以跟作業系統進行交流,可以編寫程式,可以執行任務。。。

而這一切,都是基於程序的。每一個任務(程序)被建立時,系統會為他分配儲存空間等必要資源,然後在核心管理區為該程序建立管理節點,以便後來控制和排程該任務的執行。

程序真正進入執行階段,還需要獲得CPU的使用權,這一切都是作業系統掌管著,也就是所謂的排程,在各種條件滿足(資源與CPU使用權均獲得)的情況下,啟動程序的執行過程。

除CPU而外,一個很重要的資源就是儲存器了,系統會為每個程序分配獨有的儲存空間,當然包括它特別需要的別的資源,比如寫入時外部裝置是可使用狀態等等。有了上面的引入,我們可以對程序做一個簡要的總結:

程序,是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。它的執行需要系統分配資源建立實體之後,才能進行。

隨著技術發展,在執行一些細小任務時,本身無需分配單獨資源時(多個任務共享同一組資源即可,比如所有子程序共享父程序的資源),程序的實現機制依然會繁瑣的將資源分割,這樣造成浪費,而且還消耗時間。後來就有了專門的多工技術被創造出來——執行緒。

執行緒的特點就是在不需要獨立資源的情況下就可以執行。如此一來會極大節省資源開銷,以及處理時間。

1.好了,前面的一段文字是簡要引入兩個名詞,即程序和執行緒。本文討論目標是解釋清楚程序和執行緒的區別,關於二者的技術實現,請讀者查閱相關資料。

下面我們開始重點討論本文核心了。從下面幾個方面闡述程序和執行緒的區別。

1).二者的相同點

2).實現方式的差異

3).多工程式設計模式的區別

4).實體間(程序間,執行緒間,進執行緒間)通訊方式的不同

5).控制方式的異同

6).資源管理方式的異同

7).個體間輩分關係的迥異

8).程序池與執行緒池的技術實現差別

接下來我們就逐個進行解釋。

1).二者的相同點

無論是程序還是執行緒,對於程式設計師而言,都是用來實現多工併發的技術手段。二者都可以獨立排程,因此在多工環境下,功能上並無差異。並且二者都具有各自的實體,是系統獨立管理的物件個體。所以在系統層面,都可以通過技術手段實現二者的控制。而且二者所具有的狀態都非常相似。而且,在多工程式中,子程序(子執行緒)的排程一般與父程序(父執行緒)平等競爭。

其實在Linux核心2.4版以前,執行緒的實現和管理方式就是完全按照程序方式實現的。在2.6版核心以後才有了單獨的執行緒實現。

 

2).實現方式的差異

程序是資源分配的基本單位,執行緒是排程的基本單位

這句經典名言已流傳數十年,各種作業系統教材都可見此描述。確實如此,這就是二者的顯著區別。讀者請注意“基本”二字。相信有讀者看到前半句的時候就在心裡思考,“程序豈不是不能排程?”,非也!程序和執行緒都可以被排程,否則多程序程式該如何執行呢!

只是,執行緒是更小的可以排程的單位,也就是說,只要達到執行緒的水平就可以被排程了,程序自然可以被排程。它強調的是分配資源時的物件必須是程序,不會給一個執行緒單獨分配系統管理的資源。若要執行一個任務,想要獲得資源,最起碼得有程序,其他子任務可以以執行緒身份執行,資源共享就行了

    簡而言之,程序的個體間是完全獨立的,而執行緒間是彼此依存的。多程序環境中,任何一個程序的終止,不會影響到其他程序。而多執行緒環境中,父執行緒終止,全部子執行緒被迫終止(沒有了資源)。而任何一個子執行緒終止一般不會影響其他執行緒,除非子執行緒執行了exit()系統呼叫。任何一個子執行緒執行exit(),全部執行緒同時滅亡。

其實,也沒有人寫出只有執行緒而沒有程序的程式。多執行緒程式中至少有一個主執行緒,而這個主執行緒其實就是有main函式的程序。它是整個程式的程序,所有執行緒都是它的子執行緒。我們通常把具有多執行緒的主程序稱之為主執行緒。

從系統實現角度講,程序的實現是呼叫fork系統呼叫:

pid_t fork(void);

執行緒的實現是呼叫clone系統呼叫:

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...

/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */

);

其中,fork()是將父程序的全部資源複製給了子程序。而執行緒的clone只是複製了一小部分必要的資源。在呼叫clone時可以通過引數控制要複製的物件。可以說,fork實現的是clone的加強完整版。當然,後來作業系統還進一步優化fork實現——寫時複製技術。在子程序需要複製資源(比如子程序執行寫入動作更改父程序記憶體空間)時才複製,否則建立子程序時先不復制。

實際中,編寫多程序程式時採用fork建立子程序實體。而建立執行緒時並不採用clone系統呼叫,而是採用執行緒庫函式。常用執行緒庫有Linux-Native執行緒庫和POSIX執行緒庫。其中應用最為廣泛的是POSIX執行緒庫。因此讀者在多執行緒程式中看到的是pthread_create而非clone。

我們知道,庫是建立在作業系統層面上的功能集合,因而它的功能都是作業系統提供的。由此可知,執行緒庫的內部很可能實現了clone的呼叫。不管是程序還是執行緒的實體,都是作業系統上執行的實體。

    最後,我們說一下vfork() 。這也是一個系統呼叫,用來建立一個新的程序。它建立的程序並不複製父程序的資源空間,而是共享,也就說實際上vfork實現的是一個接近執行緒的實體,只是以程序方式來管理它。並且,vfork()的子程序與父程序的執行時間是確定的:子程序“結束”後父程序才執行。請讀者注意“結束”二字。並非子程序完成退出之意,而是子程序返回時。一般採用vfork()的子程序,都會緊接著執行execv啟動一個全新的程序,該程序的程序空間與父程序完全獨立不相干,所以不需要複製父程序資源空間。此時,execv返回時父程序就認為子程序“結束”了,自己開始執行。實際上子程序繼續在一個完全獨立的空間執行著。舉個例子,比如在一個聊天程式中,彈出了一個視訊播放器。你說視訊播放器要繼承你的聊天程式的程序空間的資源幹嘛?莫非視訊播放器想要窺探你的聊天隱私不成?懂了吧!

3).多工程式設計模式的區別

由於程序間是獨立的,所以在設計多程序程式時,需要做到資源獨立管理時就有了天然優勢,而執行緒就顯得麻煩多了。比如多工的TCP程式的服務端,父程序執行accept()一個客戶端連線請求之後會返回一個新建立的連線的描述符DES,此時如果fork()一個子程序,將DES帶入到子程序空間去處理該連線的請求,父程序繼續accept等待別的客戶端連線請求,這樣設計非常簡練,而且父程序可以用同一變數(val)儲存accept()的返回值,因為子程序會複製val到自己空間,父程序再覆蓋此前的值不影響子程序工作。但是如果換成多執行緒,父執行緒就不能複用一個變數val多次執行accept()了。因為子執行緒沒有複製val的儲存空間,而是使用父執行緒的,如果子執行緒在讀取val時父執行緒接受了另一個客戶端請求覆蓋了該值,則子執行緒無法繼續處理上一次的連線任務了。改進的辦法是子執行緒立馬複製val的值在自己的棧區,但父執行緒必須保證子執行緒複製動作完成之後再執行新的accept()。但這執行起來並不簡單,因為子執行緒與父執行緒的排程是獨立的,父執行緒無法知道子執行緒何時複製完畢。這又得發生執行緒間通訊,子執行緒複製完成後主動通知父執行緒。這樣一來父執行緒的處理動作必然不能連貫,比起多程序環境,父執行緒顯得效率有所下降。

PS:這裡引述一個知名的面試問題:多程序的TCP服務端,能否互換fork()與accept()的位置?請讀者自行思考。

關於資源不獨立,看似是個缺點,但在有的情況下就成了優點。多程序環境間完全獨立,要實現通訊的話就得采用程序間的通訊方式,它們通常都是耗時間的。而執行緒則不用任何手段資料就是共享的。當然多個子執行緒在同時執行寫入操作時需要實現互斥,否則資料就寫“髒”了。

4).實體間(程序間,執行緒間,進執行緒間)通訊方式的不同

程序間的通訊方式有這樣幾種

A.共享記憶體    B.訊息佇列    C.訊號量    D.有名管道    E.無名管道    F.訊號

G.檔案        H.socket

執行緒間的通訊方式上述程序間的方式都可沿用,且還有自己獨特的幾種:

A.互斥量      B.自旋鎖      C.條件變數  D.讀寫鎖      E.執行緒訊號

G.全域性變數

值得注意的是,執行緒間通訊用的訊號不能採用程序間的訊號,因為訊號是基於程序為單位的,而執行緒是共屬於同一程序空間的。故而要採用執行緒訊號。

綜上,程序間通訊手段有8種。執行緒間通訊手段有13種。

而且,程序間採用的通訊方式要麼需要切換核心上下文,要麼要與外設訪問(有名管道,檔案)。所以速度會比較慢。而執行緒採用自己特有的通訊方式的話,基本都在自己的程序空間內完成,不存在切換,所以通訊速度會較快。也就是說,程序間與執行緒間分別採用的通訊方式,除了種類的區別外,還有速度上的區別。

另外,程序與執行緒之間穿插通訊的方式,除訊號以外其他程序間通訊方式都可採用。     執行緒有核心態執行緒與使用者級執行緒,相關知識請參看我的另一篇博文《Linux執行緒的實質》。

5).控制方式的異同

程序與執行緒的身份標示ID管理方式不一樣,程序的ID為pid_t型別,實際為一個int型的變數(也就是說是有限的):

/usr/include/unistd.h:260:typedef __pid_t   pid_t;

/usr/include/bits/types.h:126:# define __STD_TYPE    typedef

/usr/include/bits/types.h:142:__STD_TYPE  __PID_T_TYPE   __pid_t;

/usr/include/bits/typesizes.h:53:#define __PID_T_TYPE   __S32_TYPE

/usr/include/bits/types.h:100:#define   __S32_TYPE      int

在全系統中,程序ID是唯一標識,對於程序的管理都是通過PID來實現的。每建立一個程序,核心去中就會建立一個結構體來儲存該程序的全部資訊:

注:下述程式碼來自 Linux核心3.18.1

include/linux/sched.h:1235:struct task_struct {

        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */

        void *stack;...

        pid_t pid;

        pid_t tgid;...

};

每一個儲存程序資訊的節點也都儲存著自己的PID。需要管理該程序時就通過這個ID來實現(比如傳送訊號)。當子程序結束要回收時(子程序呼叫exit()退出或程式碼執行完),需要通過wait()系統呼叫來進行,未回收的消亡程序會成為殭屍程序,其程序實體已經不復存在,但會虛佔PID資源,因此回收是有必要的。

執行緒的ID是一個long型變數:

/usr/include/bits/pthreadtypes.h:60:typedef unsigned long int pthread_t;

它的範圍大得多,管理方式也不一樣。執行緒ID一般在本程序空間內作用就可以了,當然系統在管理執行緒時也需要記錄其資訊。其方式是,在核心建立一個核心態執行緒與之對應,也就是說每一個使用者建立的執行緒都有一個核心態執行緒對應。但這種對應關係不是一對一,而是多對一的關係,也就是一個核心態執行緒可以對應著多個使用者級執行緒。還是請讀者參看《Linux執行緒的實質》普及相關概念。此處貼出blog地址:

對於執行緒而言,若要主動終止需要呼叫pthread_exit() ,主執行緒需要呼叫pthread_join()來回收(前提是該執行緒沒有被detached,相關概念請查閱執行緒的“分離屬性”)。像線傳送執行緒訊號也是通過執行緒ID實現的。

6).資源管理方式的異同

程序本身是資源分配的基本單位,因而它的資源都是獨立的,如果有多程序間的共享資源,就要用到程序間的通訊方式了,比如共享記憶體。共享資料就放在共享記憶體去,大家都可以訪問,為保證資料寫入的安全,加上訊號量一同使用。一般而言,共享記憶體都是和訊號量一起使用。訊息佇列則不同,由於訊息的收發是原子操作,因而自動實現了互斥,單獨使用就是安全的。

執行緒間要使用共享資源不需要用共享記憶體,直接使用全域性變數即可,或者malloc()動態申請記憶體。顯得方便直接。而且互斥使用的是同一程序空間內的互斥量,所以效率上也有優勢。

實際中,為了使程式內資源充分規整,也都採用共享記憶體來儲存核心資料。不管程序還是執行緒,都採用這種方式。原因之一就是,共享記憶體是脫離程序的資源,如果程序發生意外終止的話,共享記憶體可以獨立存在不會被回收(是否回收由使用者程式設計實現)。程序的空間在程序崩潰的那一刻也被系統回收了。雖然有coredump機制,但也只能是有限的彌補。共享記憶體在程序down之後還完整儲存,這樣可以拿來分析程式的故障原因。同時,執行的寶貴資料沒有丟失,程式重啟之後還能繼續處理之前未完成的任務,這也是採用共享記憶體的又一大好處。

總結之,程序間的通訊方式都是脫離於程序本身存在的,是全系統都可見的。這樣一來,程序的單點故障並不會損毀資料,當然這不一定全是優點。比如,程序崩潰前對訊號量加鎖,崩潰後重啟,然後再次進入執行狀態,此時直接進行加鎖,可能造成死鎖,程式再也無法繼續運轉。再比如,共享記憶體是全系統可見的,如果你的程序資源被他人誤讀誤寫,後果肯定也是你不想要的。所以,各有利弊,關鍵在於程式設計時如何考量,技術上如何規避。這說起來又是程式設計技巧和經驗的事情了。

7).個體間輩分關係的迥異

程序的備份關係森嚴,在父程序沒有結束前,所有的子程序都尊從父子關係,也就是說A建立了B,則A與B是父子關係,B又建立了C,則B與C也是父子關係,A與C構成爺孫關係,也就是說C是A的孫子程序。在系統上使用pstree命令列印程序樹,可以清晰看到備份關係。

多執行緒間的關係沒有那麼嚴格,不管是父執行緒還是子執行緒建立了新的執行緒,都是共享父執行緒的資源,所以,都可以說是父執行緒的子執行緒,也就是隻存在一個父執行緒,其餘執行緒都是父執行緒的子執行緒。

8).程序池與執行緒池的技術實現差別

我們都知道,程序和執行緒的建立時需要時間的,並且系統所能承受的程序和執行緒數也是有上限的,這樣一來,如果業務在執行中需要動態建立子程序或執行緒時,系統無法承受不能立即建立的話,必然影響業務。綜上,聰明的程式設計師發明了一種新方法——池。

在程式啟動時,就預先建立一些子程序或執行緒,這樣在需要用時直接使喚。這就是老人口中的“多生孩子多種樹”。程式才開始執行,沒有那麼多的服務請求,必然大量的程序或執行緒空閒,這時候一般讓他們“冬眠”,這樣不耗資源,要不然一大堆孩子的口食也是個負擔啊。對於程序和執行緒而言,方式是不一樣的。另外,當你有了任務,要分配給那些孩子的時候,手段也不一樣。下面就分別來解說。

程序池

首先建立了一批程序,就得管理,也就是你得分開儲存程序ID,可以用陣列,也可用連結串列。建議用陣列,這樣可以實現常數內找到某個執行緒,而且既然做了程序池,就預先估計好了生產多少程序合適,一般也不會再動態延展。就算要動態延展,也能預估範圍,提前做一個足夠大的陣列。不為別的,就是為了快速響應。本來程序池的目的也是為了效率。

接下來就要讓閒置程序冬眠了,可以讓他們pause()掛起,也可用訊號量掛起,還可以用IPC阻塞,方法很多,分析各自優缺點根據實際情況採用就是了。

然後是分配任務了,當你有任務的時候就要讓他幹活了。喚醒了程序,讓它從哪兒開始幹呢?肯定得用到程序間通訊了,比如訊號喚醒它,然後讓它在預先指定的地方去讀取任務,可以用函式指標來實現,要讓它幹什麼,就在約定的地方設定程式碼段指標。這也只是告訴了它怎麼幹,還沒說幹什麼(資料條件),再通過共享記憶體把要處理的資料設定好,這也子程序就知道怎麼做了。幹完之後再來一次程序間通訊然後自己繼續冬眠,父程序就知道孩子幹完了,收割成果。

最後結束時回收子程序,向各程序傳送訊號喚醒,改變啟用狀態讓其主動結束,然後逐個wait()就可以了。

執行緒池

執行緒池的思想與上述類似,只是它更為輕量級,所以排程起來不用等待額外的資源。

要讓執行緒阻塞,用條件變數就是了,需要幹活的時候父執行緒改變條件,子執行緒就被啟用。

執行緒間通訊方式就不用贅述了,不用繁瑣的通訊就能達成,比起程序間效率要高一些。

執行緒幹完之後自己再改變條件,這樣父執行緒也就知道該收割成果了。

整個程式結束時,逐個改變條件並改變啟用狀態讓子執行緒結束,最後逐個回收即可。

簡述小結:

根本區別:程序是作業系統資源分配的基本單位,而執行緒是任務排程和執行的基本單位

在開銷方面:每個程序都有獨立的程式碼和資料空間(程式上下文),程式之間的切換會有較大的開銷;執行緒可以看做輕量級的程序,同一類執行緒共享程式碼和資料空間,每個執行緒都有自己獨立的執行棧和程式計數器(PC),執行緒之間切換的開銷小。

所處環境:在作業系統中能同時執行多個程序(程式);而在同一個程序(程式)中有多個執行緒同時執行(通過CPU排程,在每個時間片中只有一個執行緒執行)

記憶體分配方面:系統在執行的時候會為每個程序分配不同的記憶體空間;而對執行緒而言,除了CPU外,系統不會為執行緒分配記憶體(執行緒所使用的資源來自其所屬程序的資源),執行緒組之間只能共享資源。

包含關係:沒有執行緒的程序可以看做是單執行緒的,如果一個程序內有多個執行緒,則執行過程不是一條線的,而是多條線(執行緒)共同完成的;執行緒是程序的一部分,所以執行緒也被稱為輕權程序或者輕量級程序。