1. 程式人生 > >程序間通訊的方式——訊號、管道、訊息佇列、共享記憶體

程序間通訊的方式——訊號、管道、訊息佇列、共享記憶體

多程序:

首先,先來講一下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)