進程間常見通信方式

多進程:

首先,先來講一下fork之後,發生了什麼事情。

由fork創建的新進程被稱爲子進程(child process)。該函數被調用一次,但返回兩次。兩次返回的區別是子進程的返回值是0,而父進程的返回值則是新進程(子進程)的進程 id。將子進程id返回給父進程的理由是:因爲一個進程的子進程可以多於一個,沒有一個函數使一個進程可以獲得其所有子進程的進程id。對子進程來說,之所以fork返回0給它,是因爲它隨時可以調用getpid()來獲取自己的pid;也可以調用getppid()來獲取父進程的id。(進程id 0總是由交換進程使用,所以一個子進程的進程id不可能爲0 )。

fork之後,操作系統會複製一個與父進程完全相同的子進程,雖說是父子關係,但是在操作系統看來,他們更像兄弟關係,這2個進程共享代碼空間,但是數據空間是互相獨立的,子進程數據空間中的內容是父進程的完整拷貝,指令指針也完全相同,子進程擁有父進程當前運行到的位置兩進程的程序計數器pc值相同,也就是說,子進程是從fork返回處開始執行的),但有一點不同,如果fork成功,子進程中fork的返回值是0,父進程中fork的返回值是子進程的進程號,如果fork不成功,父進程會返回錯誤。
可以這樣想象,2個進程一直同時運行,而且步調一致,在fork之後,他們分別作不同的工作,也就是分岔了。這也是fork爲什麼叫fork的原因

至於那一個最先運行,可能與操作系統(調度算法)有關,而且這個問題在實際應用中並不重要,如果需要父子進程協同,可以通過原語的辦法解決。


 

常見的通信方式:

1. 管道pipe:管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程間使用。進程的親緣關係通常是指父子進程關係。
2. 命名管道FIFO:有名管道也是半雙工的通信方式,但是它允許無親緣關係進程間的通信。
4. 消息隊列MessageQueue:消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
5. 共享存儲SharedMemory:共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號兩,配合使用,來實現進程間的同步和通信。
6. 信號量Semaphore:信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。
7. 套接字Socket:套解口也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同及其間的進程通信。
8. 信號 ( sinal ) : 信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。

 

信號:

信號是Linux系統中用於進程之間通信或操作的一種機制,信號可以在任何時候發送給某一進程,而無須知道該進程的狀態。如果該進程並未處於執行狀態,則該信號就由內核保存起來,知道該進程恢復執行並傳遞給他爲止。如果一個信號被進程設置爲阻塞,則該信號的傳遞被延遲,直到其阻塞被取消時才被傳遞給進程。

 

Linux提供了幾十種信號,分別代表着不同的意義。信號之間依靠他們的值來區分,但是通常在程序中使用信號的名字來表示一個信號。在Linux系統中,這些信號和以他們的名稱命名的常量被定義在/usr/includebitssignum.h文件中。通常程序中直接包含<signal.h>就好。

 

信號是在軟件層次上對中斷機制的一種模擬,是一種異步通信方式,信號可以在用戶空間進程和內核之間直接交互。內核也可以利用信號來通知用戶空間的進程來通知用戶空間發生了哪些系統事件。信號事件有兩個來源:

1)硬件來源,例如按下了cltr+C,通常產生中斷信號sigint

2)軟件來源,例如使用系統調用或者命令發出信號。最常用的發送信號的系統函數是kill,raise,setitimer,sigation,sigqueue函數。軟件來源還包括一些非法運算等操作。

 

一旦有信號產生,用戶進程對信號產生的相應有三種方式:

1)執行默認操作,linux對每種信號都規定了默認操作。

2)捕捉信號,定義信號處理函數,當信號發生時,執行相應的處理函數。

3)忽略信號,當不希望接收到的信號對進程的執行產生影響,而讓進程繼續執行時,可以忽略該信號,即不對信號進程作任何處理。

  有兩個信號是應用進程無法捕捉和忽略的,即SIGKILL和SEGSTOP,這是爲了使系統管理員能在任何時候中斷或結束某一特定的進程。

上圖表示了Linux中常見的命令

1、信號發送:

信號發送的關鍵使得系統知道向哪個進程發送信號以及發送什麼信號。下面是信號操作中常用的函數:

例子:創建子進程,爲了使子進程不在父進程發出信號前結束,子進程中使用raise函數發送sigstop信號,使自己暫停;父進程使用信號操作的kill函數,向子進程發送sigkill信號,子進程收到此信號,結束子進程。

2、信號處理

當某個信號被髮送到一個正在運行的進程時,該進程即對次特定的信號註冊相應的信號處理函數,以完成所需處理。設置信號處理方式的是signal函數,在程序正常結束前,在應用signal函數恢復系統對信號的

默認處理方式。

3.信號阻塞

有時候既不希望進程在接收到信號時立刻中斷進程的執行,也不希望此信號完全被忽略掉,而是希望延遲一段時間再去調用信號處理函數,這個時候就需要信號阻塞來完成。

 

例子:主程序阻塞了cltr+c的sigint信號。用sigpromask將sigint假如阻塞信號集合。

 

管道:

管道允許在進程之間按先進先出的方式傳送數據,是進程間通信的一種常見方式。

管道是Linux 支持的最初Unix IPC形式之一,具有以下特點:

1) 管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道

2) 匿名管道只能用於父子進程或者兄弟進程之間(具有親緣關係的進程);

3) 單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,並且只存在與內存中。

 

管道分爲pipe(無名管道)和fifo(命名管道)兩種,除了建立、打開、刪除的方式不同外,這兩種管道幾乎是一樣的。他們都是通過內核緩衝區實現數據傳輸。

  • pipe用於相關進程之間的通信,例如父進程和子進程,它通過pipe()系統調用來創建並打開,當最後一個使用它的進程關閉對他的引用時,pipe將自動撤銷。
  • FIFO即命名管道,在磁盤上有對應的節點,但沒有數據塊——換言之,只是擁有一個名字和相應的訪問權限,通過mknode()系統調用或者mkfifo()函數來建立的。一旦建立,任何進程都可以通過文件名將其打開和進行讀寫,而不侷限於父子進程,當然前提是進程對FIFO有適當的訪問權。當不再被進程使用時,FIFO在內存中釋放,但磁盤節點仍然存在。

管道的實質是一個內核緩衝區,進程以先進先出的方式從緩衝區存取數據:管道一端的進程順序地將進程數據寫入緩衝區,另一端的進程則順序地讀取數據,該緩衝區可以看做一個循環隊列,讀和寫的位置都是自動增加的,一個數據只能被讀一次,讀出以後再緩衝區都不復存在了。當緩衝區讀空或者寫滿時,有一定的規則控制相應的讀進程或寫進程是否進入等待隊列,當空的緩衝區有新數據寫入或慢的緩衝區有數據讀出時,就喚醒等待隊列中的進程繼續讀寫。

無名管道:

pipe的例子:父進程創建管道,並在管道中寫入數據,而子進程從管道讀出數據

命名管道:

和無名管道的主要區別在於,命名管道有一個名字,命名管道的名字對應於一個磁盤索引節點,有了這個文件名,任何進程有相應的權限都可以對它進行訪問。

而無名管道卻不同,進程只能訪問自己或祖先創建的管道,而不能訪任意訪問已經存在的管道——因爲沒有名字。

 

Linux中通過系統調用mknod()或makefifo()來創建一個命名管道。最簡單的方式是通過直接使用shell

mkfifo myfifo

 

 等價於

mknod myfifo p

 

以上命令在當前目錄下創建了一個名爲myfifo的命名管道。用ls -p命令查看文件的類型時,可以看到命名管道對應的文件名後有一條豎線”|”,表示該文件不是普通文件而是命名管道。

使用open()函數通過文件名可以打開已經創建的命名管道,而無名管道不能由open來打開。當一個命名管道不再被任何進程打開時,它沒有消失,還可以再次被打開,就像打開一個磁盤文件一樣。

可以用刪除普通文件的方法將其刪除,實際刪除的事磁盤上對應的節點信息。

例子:用命名管道實現聊天程序,一個張三端,一個李四端。兩個程序都建立兩個命名管道,fifo1,fifo2,張三寫fifo1,李四讀fifo1;李四寫fifo2,張三讀fifo2。

用select把,管道描述符和stdin假如集合,用select進行阻塞,如果有i/o的時候喚醒進程。(粉紅色部分爲select部分,黃色部分爲命名管道部分)

 

 

在linux系統中,除了用pipe系統調用建立管道外,還可以使用C函數庫中管道函數popen函數來建立管道,使用pclose關閉管道。

例子:設計一個程序用popen創建管道,實現 ls -l |grep main.c的功能

分析:先用popen函數創建一個讀管道,調用fread函數將ls -l的結果存入buf變量,用printf函數輸出內容,用pclose關閉讀管道;

接着用popen函數創建一個寫管道,調用fprintf函數將buf的內容寫入管道,運行grep命令。

popen的函數原型:

FILE* popen(const char* command,const char* type);

 

參數說明:command是子進程要執行的命令,type表示管道的類型,r表示讀管道,w代表寫管道。如果成功返回管道文件的指針,否則返回NULL。

使用popen函數讀寫管道,實際上也是調用pipe函數調用建立一個管道,再調用fork函數建立子進程,接着會建立一個shell 環境,並在這個shell環境中執行參數所指定的進程。

消息隊列:

消息隊列,就是一個消息的鏈表,是一系列保存在內核中消息的列表。用戶進程可以向消息隊列添加消息,也可以向消息隊列讀取消息。

消息隊列與管道通信相比,其優勢是對每個消息指定特定的消息類型,接收的時候不需要按照隊列次序,而是可以根據自定義條件接收特定類型的消息。

可以把消息看做一個記錄,具有特定的格式以及特定的優先級。對消息隊列有寫權限的進程可以向消息隊列中按照一定的規則添加新消息,對消息隊列有讀權限的進程可以從消息隊列中讀取消息。

消息隊列的常用函數如下表:

進程間通過消息隊列通信,主要是:創建或打開消息隊列,添加消息,讀取消息和控制消息隊列。

例子:用函數msget創建消息隊列,調用msgsnd函數,把輸入的字符串添加到消息隊列中,然後調用msgrcv函數,讀取消息隊列中的消息並打印輸出,最後再調用msgctl函數,刪除系統內核中的消息隊列。(黃色部分是消息隊列相關的關鍵代碼,粉色部分是讀取stdin的關鍵代碼)

共享內存:

共享內存允許兩個或多個進程共享一個給定的存儲區,這一段存儲區可以被兩個或兩個以上的進程映射至自身的地址空間中,一個進程寫入共享內存的信息,可以被其他使用這個共享內存的進程,通過一個簡單的內存讀取錯做讀出,從而實現了進程間的通信。

 

採用共享內存進行通信的一個主要好處是效率高,因爲進程可以直接讀寫內存,而不需要任何數據的拷貝,對於像管道和消息隊裏等通信方式,則需要再內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次:一次從輸入文件到共享內存區,另一次從共享內存到輸出文件。

一般而言,進程之間在共享內存時,並不總是讀寫少量數據後就解除映射,有新的通信時在重新建立共享內存區域;而是保持共享區域,直到通信完畢爲止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在解除映射時才寫回文件,因此,採用共享內存的通信方式效率非常高。

共享內存有兩種實現方式:1、內存映射 2、共享內存機制

1、內存映射

內存映射 memory map機制使進程之間通過映射同一個普通文件實現共享內存,通過mmap()系統調用實現。普通文件被映射到進程地址空間後,進程可以

像訪問普通內存一樣對文件進行訪問,不必再調用read/write等文件操作函數。

例子:創建子進程,父子進程通過匿名映射實現共享內存。

分析:主程序中先調用mmap映射內存,然後再調用fork函數創建進程。那麼在調用fork函數之後,子進程繼承父進程匿名映射後的地址空間,同樣也繼承mmap函數的返回地址,這樣,父子進程就可以通過映射區域進行通信了。

2、UNIX System V共享內存機制

IPC的共享內存指的是把所有的共享數據放在共享內存區域(IPC shared memory region),任何想要訪問該數據的進程都必須在本進程的地址空間新增一塊內存區域,用來映射存放共享數據的物理內存頁面。

和前面的mmap系統調用通過映射一個普通文件實現共享內存不同,UNIX system V共享內存是通過映射特殊文件系統shm中的文件實現進程間的共享內存通信。

例子:設計兩個程序,通過unix system v共享內存機制,一個程序寫入共享區域,另一個程序讀取共享區域。

分析:一個程序調用fotk函數產生標準的key,接着調用shmget函數,獲取共享內存區域的id,調用shmat函數,映射內存,循環計算年齡,另一個程序讀取共享內存。

(fotk函數在消息隊列部分已經用過了,

根據pathname指定的文件(或目錄)名稱,以及proj參數指定的數字,ftok函數爲IPC對象生成一個唯一性的鍵值。)

key_t ftok(char* pathname,char proj)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章