您好,登錄后才能下訂單哦!
這篇文章主要介紹“Linux進程與線程的區別”,在日常操作中,相信很多人在Linux進程與線程的區別問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Linux進程與線程的區別”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
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地址:
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()就可以了。
線程池
線程池的思想與上述類似,只是它更為輕量級,所以調度起來不用等待額外的資源。
要讓線程阻塞,用條件變量就是了,需要干活的時候父線程改變條件,子線程就被激活。
線程間通信方式就不用贅述了,不用繁瑣的通信就能達成,比起進程間效率要高一些。
線程干完之后自己再改變條件,這樣父線程也就知道該收割成果了。
整個程序結束時,逐個改變條件并改變激活狀態讓子線程結束,最后逐個回收即可。
到此,關于“Linux進程與線程的區別”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。