進程和線程

 

1.概念和區別

概念

從本質上說,一個進程就是一個正在執行的程序,它是系統進行資源分配和調度的基本單元,是操作系統結構的基礎。每個進程都有自己的地址空間,包括可執行程序,程序的數據,棧,一組寄存器(程序計算器,棧指針以及其他運行程序需要的信息)

線程有時被稱爲輕量級進程,是程序執行的最小執行流,它是進程的一個實體,是系統獨立調度和分配的基本單位。

進程和線程的區別

 

  • 地址空間:同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間。

  • 資源擁有:同一進程內的線程共享本進程的資源如內存、I/O、CPU等。但是進程之間的資源是獨立的。進程切換時,消耗的資源大,效率低,所以涉及到頻繁的切換時,使用線程好於進程。同樣如果要求同時進行並且又要共享某些變量的併發操作,只能用線程不能用進程。

  • 執行過程:每個獨立的進程有一個程序運行的入口,順序執行序列和程序入口。但是線程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。

  • 線程是處理器調度的進本單位,但進程不是

  • 兩者都可併發執行。

2.進程和線程佔有的資源

​ 首先我們知道線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他線程共享進程所擁有的全部資源,不包括堆棧,如果要修改共享空間裏的資源,需要加鎖

​ 進程佔有堆棧。

3.進程

進程的創建和終止

​ 進程的創建主要有四個原因:系統的初始化,正在運行的進程執行了穿件進程的系統調用,用戶請求創建一個進程以及批處理作業的初始化。

常見的就是一個進程調用了fork()函數創建新的進程。

操作系統創建一個新的進程的過程

​ 1.爲新進程分配一個唯一的進程標識號,並申請一個空白的PCB(PCB是有限的)。若PCB申請失敗則創建失敗。

​ 2.爲進程分配資源,爲新進程的程序和數據,以及用戶棧分配必要的內存空間(在PCB中體現)。注意:如果資源不足(比如內存空間),並不是創建失敗,而是處於“等待狀態”,或則稱爲“阻塞狀態”,等待的是內存這個資源。

​ 3.初始化PCB,主要包括初始化標誌信息,初始化處理機狀態信息和初始化處理機控制信息,以及設置進程的優先級等。

​ 4.如果進程就緒隊列能夠接納新錦成,或將新進程插入到就緒隊列,等待被調度運行。

注:PCB是進程存在的唯一標識,它包含進程標識符(內部標識符:每個進程唯一的一個數字標識符,外部標識符:創建者提供),處理機狀態進程調度信息進程控制信息

進程終止的一些原因:工作完成正常退出,出錯退出,嚴重錯誤,被其他進程殺死。

進程的狀態和控制原語

進程有三種狀態:運行態,阻塞態,就緒態。這三種狀態的轉換是:

 

 

 

 

 

就緒:當一個進程獲得了除處理機以外的一切所需資源,一旦得到處理機即可運行,則稱此進程處於就緒狀態。

阻塞:也稱爲等待或睡眠狀態,一個進程正在等待某一事件發生(例如請求I/O而等待I/O完成等)而暫時停止運行,這時即使把處理機分配給進程也無法運行,故稱該進程處於阻塞狀態(是一種主動的行爲)。

運行:當一個進程在處理機上運行時,則稱該進程處於運行狀態。

注意不可能存在直接從阻塞態轉換到執行態。

除了這三個基本狀態還有一個掛起狀態,新建狀態,終止狀態

引起掛起狀態的原因:終端用戶的請求,父進程請求,負荷調節的需要,操作系統的需要。

用於控制進程的原語有:

  1. 創建原語(Create):創建一個就緒狀態的進程,使進程從創建狀態變遷爲就緒狀態。

  2. 阻塞原語(Block):使進程從執行狀態變遷爲阻塞狀態。

  3. 喚醒原語(Wakeup):使進程從阻塞狀態變遷爲就緒狀態。

  4. 掛起原語(Suspend):將指定的進程或處於阻塞的進程掛起

     

4.Java的Runnable狀態與操作系統中進程運行狀態的關係

RUNNABLE 狀態對應了傳統的 readyrunning 以及部分的 waiting狀態,也就是上面的三種狀態,但是操作體系中其實是有五種狀態的。

 

 

 

5.進程間通信

​ 每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核,在內核中開闢一塊緩衝區,進程A把數據從用戶空間拷貝到內核緩衝區,進程B再從內核緩衝區把數據讀走,內核提供這種機制稱爲進程間通信

管道

所謂管道,就是用於連接一個讀進程和一個寫進程以實現他們之間通信的一個共享文件,又名"pipe"文件。

管道是由pipe函數來創建

#include<unistd.h>
int pipe (int fd[2]);
//返回:成功返回0,出錯返回-1    
// fd參數返回兩個文件描述符,fd[0]指向管道的讀端,fd[1]指向管道的寫端。fd[1]的輸出是fd[0]的輸入。那麼此時這個管道對於用戶程序就是一個文件,可以通過read(fd [0]);或者write(fd [1])進行操作。pipe函數調用成功返回0,否則返回-1。

實現進程通信的方式

  • 父進程創建管道,得到兩個文件描述符指向管道的兩端

  • 父進程fork出子進程,子進程也有兩個文件描述符指向同一管道

  • 父進程關閉fd[0],子進程關閉fd[1],即父進程關閉管道讀端,子進程關閉管道寫端,(因爲管道只支持單向通信)。父進程可以往管道里寫,子進程可以往管道里讀,管道是用環形隊列實現的數據從寫端流入,從讀端流出,這樣就實現了進程間通信。

管道讀取數據的幾種情況(讀、寫具有互斥性):

  • 讀端不讀,寫端一直寫

  • 寫端不寫,但是讀端一直讀

  • 讀端一直讀,且fd[0]保持打開,而寫端寫了一部分數據不寫了,並且關閉fd[1]

  • 讀端讀了一部分數據,不讀了且關閉fd[0],寫端一直在寫且fd[1]還保持打開狀態。

讀/寫必須確定對方是否存在,只有確定對方存在時,才能進行通信。

對應的處理:

  • 如果一個管道的寫端一直在寫,而讀端的引⽤計數是否⼤於0決定管道是否會堵塞,引用計數大於0,只寫不讀再次調用write會導致管道堵塞;

  • 如果一個管道的讀端一直在讀,而寫端的引⽤計數是否⼤於0決定管道是否會堵塞,引用計數大於0,只讀不寫再次調用read會導致管道堵塞;

  • 而當他們的引用計數等於0時,只寫不讀會導致寫端的進程收到一個SIGPIPE信號,導致進程終止,只寫不讀會導致read返回0,就像讀到⽂件末尾⼀樣。

四個特殊情況:

  1. 如果所有指向管道寫端的文件描述符都關閉了,而仍然有進程從管道的讀端讀數據,那麼管道中剩餘的數據都被讀取後,再次read會返回0,就像讀到文件末尾一樣

  2. 如果有指向管道寫端的文件描述符沒關閉,而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那麼管道中剩餘的數據都被讀取後,再次read會阻塞,直到管道中有數據可讀了纔讀取數據並返回。

  3. 如果所有指向管道讀端的文件描述符都關閉了,這時有進程指向管道的寫端write,那麼該進程會收到信號SIGPIPE,通常會導致進程異常終止。

  4. 如果有指向管道讀端的文件描述符沒關閉,而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那麼在管道被寫滿時再write會阻塞,直到管道中有空位置了才寫入數據並返回。

管道的特點:

  • 管道只允許具有血緣關係的進程間通信,如父子進程間的通信。

  • 管道只允許單向通信。

  • 管道內部保證同步機制,從而保證訪問數據的一致性。

  • 面向字節流

  • 管道隨進程,進程在管道在,進程消失管道對應的端口也關閉,兩個進程都消失管道也消失。

管道通信的同步:指當寫(輸入)進程把一定的數量的數據寫入pipe,便去睡眠等待,直到讀(輸出)進程取走數據後,再把它喚醒。當讀進程讀一空pipe時,也應該睡眠等待,直到寫進程將數據寫入管道後,纔將之喚醒。

匿名管道

匿名管道主要用於本地父進程和子進程之間的通信,在父進程中的話,首先是要創建一個匿名管道,在創建匿名管道成功後,可以獲取到對這個匿名管道的讀寫句柄,然後父進程就可以向這個匿名管道中寫入數據和讀取數據了,但是如果要實現的是父子進程通信的話,那麼還必須在父進程中創建一個子進程,同時,這個子進程必須能夠繼承和使用父進程的一些公開的句柄,爲什麼呢?因爲在子進程中必須要使用父進程創建的匿名管道的讀寫句柄,通過這個匿名管道才能實現父子進程的通信,所以必須繼承父進程的公開句柄。同時在創建子進程的時候,必須將子進程的標準輸入句柄設置爲父進程中創建匿名管道時得到的讀管道句柄,將子進程的標準輸出句柄設置爲父進程中創建匿名管道時得到的寫管道句柄。然後在子進程就可以讀寫匿名管道了。

信號量

信號量本質上是一個計數器(不設置全局變量是因爲進程間是相互獨立的,而這不一定能看到,看到也不能保證++引用計數爲原子操作 ),用於多進程對共享數據的讀取,它和管道有所不同,他不以傳送數據爲主要目的,他主要用來保護共享資源(信號量也屬於臨界資源),使得資源在一個時刻只有一個進程獨享。

工作原理:

由於信號量只能進行兩種操作等待和發送信號,即P(sv)V(sv),他們的行爲是這樣的:

  • P(sv):如果sv的值大於零,就給它減1;如果它的值爲零,就掛起該進程的執行

  • V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1.

在信號量進行PV操作時都爲原子操作(單條指令的執行是不會被打斷的,因爲它需要保護臨界資源)

與信號量相關的函數:

// 創建信號量,返回:成功返回信號集ID,出錯返回-1
int semget(key_t key,int nsems,int flags)

// 刪除和初始化信號量
int semctl(int semid, int semnum, int cmd, ...);

// 改變信號量的值
int semop(int semid, struct sembuf *sops, size_t nops);

// 對應的參數的含義:https://blog.csdn.net/skyroben/article/details/72513985

消息隊列

消息隊列是消息的鏈接表,存放在內核中並由消息隊列標識符標識。用戶可以從消息隊列中讀取和添加消息,其中發送進程添加消息到隊列的末尾,接收進程在隊列的頭部接收消息,消息一旦被接收,就會從隊列中刪除

消息隊列常用的一些函數:

  • msgget 創建或者打開消息隊列

  • msgsnd 添加消息

  • msgrcv 讀取消息

  • msgctl 控制消息隊列

  • ftok 由於文件路徑工程ID生成的標準key

 

共享內存

共享內存就是允許兩個或多個進程共享一定的存儲區。就如同malloc() 函數向不同進程返回了指向同一個物理內存區域的指針。當一個進程改變了這塊地址中的內容的時候,其它進程都會察覺到這個更改。因爲數據不需要在客戶機和服務器端之間複製,數據直接寫到內存,不用若干次數據拷貝。

但是共享內存沒有任何的同步與互斥機制,所以要使用信號量來實現對共享內存的存取的同步

共享內存的涉及到的函數:

// 創建共享內存,成功返回共享內存的ID,出錯返回-1  
int shmget(key_t key, size_t size, int shmflg);
​
// 操作共享內存,成功返回0,出錯返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
​
 

共享內存優缺點:

  • 優點:我們可以看到使用共享內存進行進程間的通信真的是非常方便,而且函數的接口也簡單,數據的共享還使進程間的數據不用傳送,而是直接訪問內存,也加快了程序的效率。同時,它也不像匿名管道那樣要求通信的進程有一定的父子關係。

  • 缺點:共享內存沒有提供互斥同步的機制,這使得我們在使用共享內存進行進程間通信時,往往要藉助其他的手段比如信號量等來進行進程間的同步工作。

 

6.爲什要進程通信

進程是一個獨立的資源分配單元,不同進程(這裏所說的進程通常指的是用戶進程)之間的資源是獨立的,沒有關聯,不能在一個進程中直接訪問另一個進程的資源(例如打開的文件描述符)但是,進程不是孤立的,不同的進程需要進行信息的交互和狀態的傳遞等,因此需要進程間通信 。

進程間通信的目的:

  • 數據傳輸:一個進程需要將它的數據發送給另一個進程。

  • 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。

  • 資源共享:多個進程之間共享同樣的資源。爲了做到這一點,需要內核提供互斥和同步機制。

  • 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,並能夠及時知道它的狀態改變。

 

7.線程間通信

同步

指多個線程通過Synchronized關鍵字這種方式來實現線程間的通信。

比方說由於線程A和線程B持有同一個MyObject類的對象object,儘管這兩個線程需要調用不同的方法,但是它們是同步執行的,比如:線程B需要等待線程A執行完了methodA()方法之後,它才能執行methodB()方法。這樣,線程A和線程B就實現了通信。

這種方式,本質上就是“共享內存”式的通信。多個線程需要訪問同一個共享變量,誰拿到了鎖(獲得了訪問權限),誰就可以執行。

while輪詢的方式

參考:Java多線程通信方式

wait/notify機制

通過進程調用對應的函數,通知對應另外的線程從而實現線程的通信

比方說當條件未滿足時,線程A調用wait()放棄CPU,並進入阻塞狀態,當條件滿足時,線程B調用 notify()通知線程A,所謂通知線程A,就是喚醒線程A,並讓它進入可運行狀態。

 

8.進程同步

進程同步是一個操作系統級別的概念,有兩種形式的制約:間接性制約(同一個系統的進程需要共享着某種資源),直接性制約(源於進程間的合作)表示是在多道程序的環境下,存在着不同的制約關係,爲了協調這種互相制約的關係,實現資源共享和進程協作,從而避免進程之間的衝突,引入了進程同步。 比如說進程B需要從緩衝區讀取進程A產生的信息,當緩衝區爲空時,進程B因爲讀取不到信息而被阻塞。而當進程A產生信息放入緩衝區時,進程B纔會被喚醒。

臨界資源

在操作系統中,進程是佔有資源的最小單位,但對於某些資源來說,其在同一時間只能被一個進程所佔用。這些一次只能被一個進程所佔用的資源就是所謂的臨界資源。典型的臨界資源比如物理上的打印機

對於臨界資源的訪問,必須是互斥進行。也就是當臨界資源被佔用時,另一個申請臨界資源的進程會被阻塞,直到其所申請的臨界資源被釋放。而進程內訪問臨界資源的代碼被稱爲臨界區。

對於臨界區的訪問過程分爲四個部分:

  • 進入區:查看臨界區是否可訪問,如果可以訪問,則轉到步驟二,否則進程會被阻塞

  • 臨界區:在臨界區做操作

  • 退出區:清除臨界區被佔用的標誌

  • 剩餘區:進程與臨界區不相關部分的代碼

 

9.進程互斥

進程互斥是進程之間的間接制約關係。當一個進程進入臨界區使用臨界資源時,另一個進程必須等待。只有當使用臨界資源的進程退出臨界區後,這個進程纔會解除阻塞狀態。

比如進程 B 需要訪問打印機,但此時進程 A 佔有了打印機,進程 B 會被阻塞,直到進程 A 釋放了打印機資源,進程B 纔可以繼續執行。

實現臨界區互斥的基本方法

  • 通過硬件實現臨界區最簡單的辦法就是關 CPU 的中斷

  • 信號量實現:常見的 P,V 操作

 

10.進程同步與進程通信區別

進程同步:控制多個進程按一定的順序執行;

進程通信:進程間傳輸信息。

進程通信是一種手段,而進程同步是一種目的也可以說,爲了能夠達到進程同步的目的,需要讓進程進行通信,傳輸一些進程同步所需要的信息

 

11.如何正確的停止一個線程

結束一個線程有一個最基本的方法:Thread.stop() 方法,但是這個方法已經是被建議不要使用的方法(會立即釋放該線程所持有的所有的鎖導致數據得不到同步的處理,出現數據不一致的問題;即刻停止 run() 方法中剩餘的全部工作,包括在 catch 或 finally 語句中,並拋出 ThreadDeath 異常(通常情況下此異常不需要顯示的捕獲),因此可能會導致一些清理性的工作的得不到完成,如文件,數據庫等的關閉。)。

正確的是使用中斷,也就是使用 Thread.interrupt() 方法,嚴格的講,線程中斷不會使線程立即退出,而是給線程發送一個通知,告訴目標線程,有人需要你退出!至於目標線程接到通知後如何處理,則完全由目標線程自行決定。

所以置爲中斷狀態,還需要增加中斷處理邏輯程序,不然就沒有作用。需要使用 Thread.isInterrupted() 判斷線程是否被中斷,然後進入中斷處理邏輯代碼。

 

12.中斷和異常

所謂中斷是指CPU對系統發生的某個事件作出的一種反應:CPU暫停正在執行的程序,保留現場後自動地轉去執行相應的處理程序,處理完該事件後再返回斷點繼續執行被“打斷”的程序。

引起中斷的事件稱爲中斷源,中斷源向CPU提出進行處理的請求稱爲中斷請求。它是由CPU以外的事件引起的中斷,如I/O中斷、時鐘中斷、控制檯中斷等。

引入中斷的目的:實現併發活動,實現實時處理,故障自動處理

異常來自 CPU 的內部事件或程序執行中的事件引起的過程。如由於CPU本身故障、程序故障和請求系統服務的指令引起的中斷等。

13.進程隔離

它是爲保護操作系統中進程互不干擾而設計的一組不同硬件和軟件)的技術。這個技術是爲了避免進程 A 寫入進程B 的情況發生。 進程的隔離實現,使用了虛擬地址空間。進程 A 的虛擬地址和進程 B 的虛擬地址不同,這樣就防止進程 A 將數據信息寫入進程 B。

虛擬內存

虛擬內存:虛擬內存是一種邏輯上擴充物理內存的技術。基本思想是用軟、硬件技術把內存與外存這兩級存儲器當做一級存儲器來用。虛擬內存技術的實現利用了自動覆蓋和交換技術。簡單的說就是將硬盤的一部分作爲內存來使用

虛擬地址空間

通過虛擬內存的概念,操作系統爲每一個進程提供完全一致的內存視圖,這個內存視圖的地址空間,叫虛擬地址空間。CPU在尋址的時候,是按照虛擬地址來尋址,然後通過MMU(內存管理單元)將虛擬地址轉換爲物理地址。因爲只有程序的一部分加入到內存中,所以會出現所尋找的地址不在內存中的情況(CPU產生缺頁異常),如果在內存不足的情況下,就會通過頁面調度算法來將內存中的頁面置換出來,然後將在外存中的頁面加入到內存中,使程序繼續正常運行。

14.多線程、多進程的區別及適用場景

對比維度多進程多線程總結
數據共享、同步 數據共享複雜、需要用IPC;數據是分開的,同步簡單 因爲共享進程數據,數據共享簡單,但也是因爲這個原因導致同步複雜 各有優勢
內存、CPU 佔用內存多,切換複雜,CPU利用率低 佔用內存少,切換簡單,CPU利用率高 線程佔優
創建銷燬、切換 創建銷燬、切換複雜,速度慢 創建銷燬,切換簡單,速度很快 線程佔優
編程、調試 編程簡單,調試簡單 編程複雜,調試複雜 進程佔優
可靠性 進程間不會相互影響 一個線程掛掉將導致整個進程掛掉 進程佔優
分佈式 適用於多核、多機分佈式;如果一臺機器不夠,擴展到多態機器比較簡單 是英語多核分佈式 進程佔優

多線程相比於多進程佔用內存少、CPU利用率高,創建銷燬,切換都比較簡單,速度很快。多進程相比於多線程共享數據複雜,需要將進程間通信。但是同步簡單,多線程因爲數據共享簡單,導致同步複雜。多進程編程調試都比多線程簡單。進程之間互相不影響,一個線程掛掉將導致整個進程掛掉。多進程適合多核,多機分佈,多線程適合多核分佈。

舉個例子,谷歌瀏覽器是使用多進程來實現的,瀏覽器中你打開的每個頁面,都是一個進程。如果一個頁面崩潰了,不會影響其他頁面(進程相互獨立)。但是谷歌瀏覽器佔用內存相比於其他瀏覽器多,實際應用中,打開頁面太多,佔用內存較大。其他瀏覽器採用多線程來實現,每個頁面就是一個線程,所以一個頁面崩潰,會導致整個瀏覽器崩潰。

 

發佈了675 篇原創文章 · 獲贊 30 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章