Linux進程與線程的區別——不要太經典

Linux進程與線程的區別

進程與線程的區別,早已經成爲了經典問題。自線程概念誕生起,關於這個問題的討論就沒有中止過。不管是初級程序員,仍是資深專家,都應該考慮過這個問題,只是層次角度不一樣罷了。通常程序員而言,搞清楚兩者的概念,在工做實際中去運用成爲了焦點。而資深工程師則在考慮系統層面如何實現兩種技術及其各自的性能和實現代價。以致於到今天,Linux內核還在持續更新完善(關於進程和線程的實現模塊也是內核完善的任務之一)。linux

本文將以一個從事Linux平臺系統開發的程序員角度描述這個經典問題。本文素材所有來源於工做實踐經驗與知識規整,如有疏漏或不正之處,敬請讀者慷慨指出。程序員

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

而這一切,都是基於進程的。每個任務(進程)被建立時,系統會爲他分配存儲空間等必要資源,而後在內核管理區爲該進程建立管理節點,以便後來控制和調度該任務的執行。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地址:

http://my.oschina.net/cnyinlinux/blog/367910

對於線程而言,若要主動終止須要調用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()就能夠了。

線程池

線程池的思想與上述相似,只是它更爲輕量級,因此調度起來不用等待額外的資源。

要讓線程阻塞,用條件變量就是了,須要幹活的時候父線程改變條件,子線程就被激活。

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

線程幹完以後本身再改變條件,這樣父線程也就知道該收割成果了。

整個程序結束時,逐個改變條件並改變激活狀態讓子線程結束,最後逐個回收便可。

<<<本文完結>>>

相關文章
相關標籤/搜索