1. 程式人生 > >Linux程序間通訊與生產者消費者問題

Linux程序間通訊與生產者消費者問題

生產者消費者問題(英語:Producer-consumerproblem),也稱有限緩衝問題(英語:Bounded-bufferproblem),是一個多執行緒同步問題的經典案例。該問題描述了兩個共享固定大小緩衝區的執行緒——即所謂的“生產者”和“消費者”——在實際執行時會發生的問題。生產者的主要作用是生成一定量的資料放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些資料。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入資料,消費者也不會在緩衝區中空時消耗資料。

要解決該問題,就必須讓生產者在緩衝區滿時休眠(要麼乾脆就放棄資料),等到下次消費者消耗緩衝區中的資料的時候,生產者才能被喚醒,開始往緩衝區新增資料。同樣,也可以讓消費者在緩衝區空時進入休眠,等到生產者往緩衝區新增資料之後,再喚醒消費者。

本文簡要介紹了Linux下的通訊方式,包括管道(有名管道),訊號,訊息佇列,訊號燈與共享記憶體。其中訊息佇列,訊號燈與共享記憶體是基於system V系統,程式碼可以在這裡下載。

Linux程序間通訊

linux下的程序通訊手段基本上是從Unix平臺上的程序通訊手段繼承而來的。而對Unix發展做出重大貢獻的兩大主力AT&T的貝爾實驗室及BSD(加州大學伯克利分校的伯克利軟體釋出中心)在程序間通訊方面的側重點有所不同。前者對Unix早期的程序間通訊手段進行了系統的改進和擴充,形成了“system V IPC”,通訊程序侷限在單個計算機內;後者則跳過了該限制,形成了基於套介面(socket)的程序間通訊機制。Linux則把兩者繼承了下來,如圖示:


其中,最初Unix IPC包括:管道、FIFO、訊號;System V IPC包括:System V訊息佇列、System V訊號燈、System V共享記憶體區;Posix IPC包括: Posix訊息佇列、Posix訊號燈、Posix共享記憶體區。有兩點需要簡單說明一下:1)由於Unix版本的多樣性,電子電氣工程協會(IEEE)開發了一個獨立的Unix標準,這個新的ANSI Unix標準被稱為計算機環境的可移植性作業系統介面(PSOIX)。現有大部分Unix和流行版本都是遵循POSIX標準的,而Linux從一開始就遵循POSIX標準;2)BSD並不是沒有涉足單機內的程序間通訊(socket本身就可以用於單機內的程序間通訊)。事實上,很多Unix版本的單機IPC留有BSD的痕跡,如4.4BSD支援的匿名記憶體對映、4.3+BSD對可靠訊號語義的實現等等。

圖一給出了linux 所支援的各種IPC手段,在本文接下來的討論中,為了避免概念上的混淆,在儘可能少提及Unix的各個版本的情況下,所有問題的討論最終都會歸結到Linux環境下的程序間通訊上來。並且,對於Linux所支援通訊手段的不同實現版本(如對於共享記憶體來說,有Posix共享記憶體區以及System V共享記憶體區兩個實現版本),將主要介紹Posix API。

linux下程序間通訊的幾種主要手段簡介:

  1.     管道(Pipe)及有名管道(named pipe):管道可用於具有親緣關係程序間的通訊,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係程序間的通訊;
  2.     訊號(Signal):訊號是比較複雜的通訊方式,用於通知接受程序有某種事件發生,除了用於程序間通訊外,程序還可以傳送訊號給程序本身;linux除了支援Unix早期訊號語義函式sigal外,還支援語義符合Posix.1標準的訊號函式sigaction(實際上,該函式是基於BSD的,BSD為了實現可靠訊號機制,又能夠統一對外介面,用sigaction函式重新實現了signal函式);
  3.     報文(Message)佇列(訊息佇列):訊息佇列是訊息的連結表,包括Posix訊息佇列system V訊息佇列。有足夠許可權的程序可以向佇列中新增訊息,被賦予讀許可權的程序則可以讀走佇列中的訊息。訊息佇列克服了訊號承載資訊量少,管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
  4.     共享記憶體:使得多個程序可以訪問同一塊記憶體空間,是最快的可用IPC形式。是針對其他通訊機制執行效率較低而設計的。往往與其它通訊機制,如訊號量結合使用,來達到程序間的同步及互斥。
  5.     訊號量(semaphore):主要作為程序間以及同一程序不同執行緒之間的同步手段。
  6.     套介面(Socket):更為一般的程序間通訊機制,可用於不同機器之間的程序間通訊。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支援套接字。

基於管道

管道是Linux支援的最初UnixIPC形式之一,具有以下特點:
  •     管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道;
  •     只能用於父子程序或者兄弟程序之間(具有親緣關係的程序);
  •     單獨構成一種獨立的檔案系統:管道對於管道兩端的程序而言,就是一個檔案,但它不是普通的檔案,它不屬於某種檔案系統,而是自立門戶,單獨構成一種檔案系統,並且只存在與記憶體中。
  •     資料的讀出和寫入:一個程序向管道中寫的內容被管道另一端的程序讀出。寫入的內容每次都新增在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出資料。
#include <unistd.h>
int pipe(int fd[2])

該函式建立的管道的兩端處於一個程序中間,在實際應用中沒有太大意義,因此,一個程序在由pipe()建立管道後,一般再fork一個子程序,然後通過管道實現父子程序間的通訊(因此也不難推出,只要兩個程序中存在親緣關係,這裡的親緣關係指的是具有共同的祖先,都可以採用管道方式來進行通訊)。

管道兩端可分別用描述字fd[0]以及fd[1]來描述,需要注意的是,管道的兩端是固定了任務的。即一端只能用於讀,由描述字fd[0]表示,稱其為管道讀端;另一端則只能用於寫,由描述字fd[1]來表示,稱其為管道寫端。如果試圖從管道寫端讀取資料,或者向管道讀端寫入資料都將導致錯誤發生。一般檔案的I/O函式都可以用於管道,如close、read、write等等。

從管道中讀取資料:

    如果管道的寫端不存在,則認為已經讀到了資料的末尾,讀函式返回的讀出位元組數為0;

    當管道的寫端存在時,如果請求的位元組數目大於PIPE_BUF,則返回管道中現有的資料位元組數,如果請求的位元組數目不大於PIPE_BUF,則返回管道中現有資料位元組數(此時,管道中資料量小於請求的資料量);或者返回請求的位元組數(此時,管道中資料量不小於請求的資料量)。注:(PIPE_BUF在include/linux/limits.h中定義,不同的核心版本可能會有所不同。Posix.1要求PIPE_BUF至少為512位元組,redhat 7.2中為4096)。

向管道中寫入資料:

    向管道中寫入資料時,linux將不保證寫入的原子性,管道緩衝區一有空閒區域,寫程序就會試圖向管道寫入資料。如果讀程序不讀走管道緩衝區中的資料,那麼寫操作將一直阻塞。
    注:只有在管道的讀端存在時,向管道中寫入資料才有意義。否則,向管道中寫入資料的程序將收到核心傳來的SIFPIPE訊號,應用程式可以處理該訊號,也可以忽略(預設動作則是應用程式終止)。

基於命名管道

管道應用的一個重大限制是它沒有名字,因此,只能用於具有親緣關係的程序間通訊,在有名管道(named pipe或FIFO)提出後,該限制得到了克服。FIFO不同於管道之處在於它提供一個路徑名與之關聯,以FIFO的檔案形式存在於檔案系統中。這樣,即使與FIFO的建立程序不存在親緣關係的程序,只要可以訪問該路徑,就能夠彼此通過FIFO相互通訊(能夠訪問該路徑的程序以及FIFO的建立程序之間),因此,通過FIFO不相關的程序也能交換資料。值得注意的是,FIFO嚴格遵循先進先出(first in first out),對管道及FIFO的讀總是從開始處返回資料,對它們的寫則把資料新增到末尾。它們不支援諸如lseek()等檔案定位操作。

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char * pathname, mode_t mode)
該函式的第一個引數是一個普通的路徑名,也就是建立後FIFO的名字。第二個引數與開啟普通檔案的open()函式中的mode 引數相同。 如果mkfifo的第一個引數是一個已經存在的路徑名時,會返回EEXIST錯誤,所以一般典型的呼叫程式碼首先會檢查是否返回該錯誤,如果確實返回該錯誤,那麼只要呼叫開啟FIFO的函式就可以了。一般檔案的I/O函式都可以用於FIFO,如close、read、write等等。

有名管道比管道多了一個開啟操作:open。
FIFO的開啟規則:
如果當前開啟操作是為讀而開啟FIFO時,若已經有相應程序為寫而開啟該FIFO,則當前開啟操作將成功返回;否則,可能阻塞直到有相應程序為寫而開啟該FIFO(當前開啟操作設定了阻塞標誌);或者,成功返回(當前開啟操作沒有設定阻塞標誌)。
如果當前開啟操作是為寫而開啟FIFO時,如果已經有相應程序為讀而開啟該FIFO,則當前開啟操作將成功返回;否則,可能阻塞直到有相應程序為讀而開啟該FIFO(當前開啟操作設定了阻塞標誌);或者,返回ENXIO錯誤(當前開啟操作沒有設定阻塞標誌)。

從FIFO中讀取資料:
約定:如果一個程序為了從FIFO中讀取資料而阻塞開啟FIFO,那麼稱該程序內的讀操作為設定了阻塞標誌的讀操作。

  • 如果有程序寫開啟FIFO,且當前FIFO內沒有資料,則對於設定了阻塞標誌的讀操作來說,將一直阻塞。對於沒有設定阻塞標誌讀操作來說則返回-1,當前errno值為EAGAIN,提醒以後再試。
  • 對於設定了阻塞標誌的讀操作說,造成阻塞的原因有兩種:當前FIFO內有資料,但有其它程序在讀這些資料;另外就是FIFO內沒有資料。解阻塞的原因則是FIFO中有新的資料寫入,不論信寫入資料量的大小,也不論讀操作請求多少資料量。
  • 讀開啟的阻塞標誌只對本程序第一個讀操作施加作用,如果本程序內有多個讀操作序列,則在第一個讀操作被喚醒並完成讀操作後,其它將要執行的讀操作將不再阻塞,即使在執行讀操作時,FIFO中沒有資料也一樣(此時,讀操作返回0)。
  • 如果沒有程序寫開啟FIFO,則設定了阻塞標誌的讀操作會阻塞。

注:如果FIFO中有資料,則設定了阻塞標誌的讀操作不會因為FIFO中的位元組數小於請求讀的位元組數而阻塞,此時,讀操作會返回FIFO中現有的資料量。

向FIFO中寫入資料:
約定:如果一個程序為了向FIFO中寫入資料而阻塞開啟FIFO,那麼稱該程序內的寫操作為設定了阻塞標誌的寫操作。
對於設定了阻塞標誌的寫操作:

  • 當要寫入的資料量不大於PIPE_BUF時,linux將保證寫入的原子性。如果此時管道空閒緩衝區不足以容納要寫入的位元組數,則進入睡眠,直到當緩衝區中能夠容納要寫入的位元組數時,才開始進行一次性寫操作。
  • 當要寫入的資料量大於PIPE_BUF時,linux將不再保證寫入的原子性。FIFO緩衝區一有空閒區域,寫程序就會試圖向管道寫入資料,寫操作在寫完所有請求寫的資料後返回
對於沒有設定阻塞標誌的寫操作:
  • 當要寫入的資料量大於PIPE_BUF時,linux將不再保證寫入的原子性。在寫滿所有FIFO空閒緩衝區後,寫操作返回。
  • 當要寫入的資料量不大於PIPE_BUF時,linux將保證寫入的原子性。如果當前FIFO空閒緩衝區能夠容納請求寫入的位元組數,寫完後成功返回;如果當前FIFO空閒緩衝區不能夠容納請求寫入的位元組數,則返回EAGAIN錯誤,提醒以後再寫。

基於訊號

訊號本質:
訊號是在軟體層次上對中斷機制的一種模擬,在原理上,一個程序收到一個訊號與處理器收到一箇中斷請求可以說是一樣的。訊號是非同步的,一個程序不必通過任何操作來等待訊號的到達,事實上,程序也不知道訊號到底什麼時候到達。
訊號是程序間通訊機制中唯一的非同步通訊機制,可以看作是非同步通知,通知接收訊號的程序有哪些事情發生了。訊號機制經過POSIX實時擴充套件後,功能更加強大,除了基本通知功能外,還可以傳遞附加資訊。
訊號來源:
訊號事件的發生有兩個來源:硬體來源(比如我們按下了鍵盤或者其它硬體故障);軟體來源,最常用傳送訊號的系統函式是kill, raise, alarm和setitimer以及sigqueue函式,軟體來源還包括一些非法運算等操作。

可以從兩個不同的分類角度對訊號進行分類:(1)可靠性方面:可靠訊號與不可靠訊號;(2)與時間的關係上:實時訊號與非實時訊號。

程序可以通過三種方式來響應一個訊號:(1)忽略訊號,即對訊號不做任何處理,其中,有兩個訊號不能忽略:SIGKILL及SIGSTOP;(2)捕捉訊號。定義訊號處理函式,當訊號發生時,執行相應的處理函式;(3)執行預設操作,Linux對每種訊號都規定了預設操作,程序對實時訊號的預設反應是程序終止。

傳送訊號的主要函式有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

linux主要有兩個函式實現訊號的安裝:signal()、sigaction()。其中signal()在可靠訊號系統呼叫的基礎上實現, 是庫函式。它只有兩個引數,不支援訊號傳遞資訊,主要是用於前32種非實時訊號的安裝;而sigaction()是較新的函式(由兩個系統呼叫實現:sys_signal以及sys_rt_sigaction),有三個引數,支援訊號傳遞資訊,主要用來與 sigqueue() 系統呼叫配合使用,當然,sigaction()同樣支援非實時訊號的安裝。sigaction()優於signal()主要體現在支援訊號帶有引數。

訊息佇列

訊息佇列(也叫做報文佇列)能夠克服早期unix通訊機制的一些缺點。作為早期unix通訊機制之一的訊號能夠傳送的資訊量有限,後來雖然POSIX 1003.1b在訊號的實時性方面作了拓廣,使得訊號在傳遞資訊量方面有了相當程度的改進,但是訊號這種通訊方式更像"即時"的通訊方式,它要求接受訊號的程序在某個時間範圍內對訊號做出反應,因此該訊號最多在接受訊號程序的生命週期內才有意義,訊號所傳遞的資訊是接近於隨程序持續的概念(process-persistent);管道及有名管道則是典型的隨程序持續IPC,並且,只能傳送無格式的位元組流無疑會給應用程式開發帶來不便,另外,它的緩衝區大小也受到限制。
訊息佇列就是一個訊息的連結串列。可以把訊息看作一個記錄,具有特定的格式以及特定的優先順序。對訊息佇列有寫許可權的程序可以向其中按照一定的規則新增新訊息;對訊息佇列有讀許可權的程序則可以從訊息佇列中讀走訊息。訊息佇列是隨核心持續的。
目前主要有兩種型別的訊息佇列:POSIX訊息佇列以及系統V訊息佇列,系統V訊息佇列目前被大量使用。考慮到程式的可移植性,新開發的應用程式應儘量使用POSIX訊息佇列。

訊息佇列基本概念

  1. 系統V訊息佇列是隨核心持續的,只有在核心重起或者顯示刪除一個訊息佇列時,該訊息佇列才會真正被刪除。因此係統中記錄訊息佇列的資料結構(struct ipc_ids msg_ids)位於核心中,系統中的所有訊息佇列都可以在結構msg_ids中找到訪問入口。
  2. 訊息佇列就是一個訊息的連結串列。每個訊息佇列都有一個佇列頭,用結構struct msg_queue來描述。佇列頭中包含了該訊息佇列的大量資訊,包括訊息佇列鍵值、使用者ID、組ID、訊息佇列中訊息數目等等,甚至記錄了最近對訊息佇列讀寫程序的ID。讀者可以訪問這些資訊,也可以設定其中的某些資訊。

下圖說明了核心與訊息佇列是怎樣建立起聯絡的:
其中:struct ipc_ids msg_ids是核心中記錄訊息佇列的全域性資料結構;struct msg_queue是每個訊息佇列的佇列頭。

從上圖可以看出,全域性資料結構 struct ipc_ids msg_ids 可以訪問到每個訊息佇列頭的第一個成員:struct kern_ipc_perm;而每個struct kern_ipc_perm能夠與具體的訊息佇列對應起來是因為在該結構中,有一個key_t型別成員key,而key則唯一確定一個訊息佇列。

操作訊息佇列

對訊息佇列的操作無非有下面三種類型:
1、 開啟或建立訊息佇列
訊息佇列的核心持續性要求每個訊息佇列都在系統範圍內對應唯一的鍵值,所以,要獲得一個訊息佇列的描述字,只需提供該訊息佇列的鍵值即可;
注:訊息佇列描述字是由在系統範圍內唯一的鍵值生成的,而鍵值可以看作對應系統內的一條路經。
2、 讀寫操作
訊息讀寫操作非常簡單,對開發人員來說,每個訊息都類似如下的資料結構:

struct msgbuf{
long mtype;
char mtext[1];
};

mtype成員代表訊息型別,從訊息佇列中讀取訊息的一個重要依據就是訊息的型別;mtext是訊息內容,當然長度不一定為1。因此,對於傳送訊息來說,首先預置一個msgbuf緩衝區並寫入訊息型別和內容,呼叫相應的傳送函式即可;對讀取訊息來說,首先分配這樣一個msgbuf緩衝區,然後把訊息讀入該緩衝區即可。
3、 獲得或設定訊息佇列屬性:
訊息佇列的資訊基本上都儲存在訊息佇列頭中,因此,可以分配一個類似於訊息佇列頭的結構(struct msqid_ds),來返回訊息佇列的屬性;同樣可以設定該資料結構。

系統V訊息佇列API

1)int msgget(key_t key, int msgflg)
引數key是一個鍵值,由ftok獲得;msgflg引數是一些標誌位。該呼叫返回與健值key相對應的訊息佇列描述字。
在以下兩種情況下,該呼叫將建立一個新的訊息佇列:

  • 如果沒有訊息佇列與健值key相對應,並且msgflg中包含了IPC_CREAT標誌位;
  • key引數為IPC_PRIVATE;

引數msgflg可以為以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或結果。
呼叫返回:成功返回訊息佇列描述字,否則返回-1。
注:引數key設定成常數IPC_PRIVATE並不意味著其他程序不能訪問該訊息佇列,只意味著即將建立新的訊息佇列。


2)int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg);
該系統呼叫從msgid代表的訊息佇列中讀取一個訊息,並把訊息儲存在msgp指向的msgbuf結構中。
msqid為訊息佇列描述字;訊息返回後儲存在msgp指向的地址,msgsz指定msgbuf的mtext成員的長度(即訊息內容的長度),msgtyp為請求讀取的訊息型別;讀訊息標誌msgflg可以為以下幾個常值的或:

  • IPC_NOWAIT 如果沒有滿足條件的訊息,呼叫立即返回,此時,errno=ENOMSG
  • IPC_EXCEPT 與msgtyp>0配合使用,返回佇列中第一個型別不為msgtyp的訊息
  • IPC_NOERROR 如果佇列中滿足條件的訊息內容大於所請求的msgsz位元組,則把該訊息截斷,截斷部分將丟失。

msgrcv手冊中詳細給出了訊息型別取不同值時(>0; <0; =0),呼叫將返回訊息佇列中的哪個訊息。
msgrcv()解除阻塞的條件有三個:

  • 訊息佇列中有了滿足條件的訊息;
  • msqid代表的訊息佇列被刪除;
  • 呼叫msgrcv()的程序被訊號中斷;

呼叫返回:成功返回讀出訊息的實際位元組數,否則返回-1。


3)int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);
向msgid代表的訊息佇列傳送一個訊息,即將傳送的訊息儲存在msgp指向的msgbuf結構中,訊息的大小由msgze指定。
對傳送訊息來說,有意義的msgflg標誌為IPC_NOWAIT,指明在訊息佇列沒有足夠空間容納要傳送的訊息時,msgsnd是否等待。造成msgsnd()等待的條件有兩種:
當前訊息的大小與當前訊息佇列中的位元組數之和超過了訊息佇列的總容量;
當前訊息佇列的訊息數(單位"個")不小於訊息佇列的總容量(單位"位元組數"),此時,雖然訊息佇列中的訊息數目很多,但基本上都只有一個位元組。

msgsnd()解除阻塞的條件有三個:

  • 不滿足上述兩個條件,即訊息佇列中有容納該訊息的空間;
  • msqid代表的訊息佇列被刪除;
  • 呼叫msgsnd()的程序被訊號中斷;

呼叫返回:成功返回0,否則返回-1。


4)int msgctl(int msqid, int cmd, struct msqid_ds *buf);
該系統呼叫對由msqid標識的訊息佇列執行cmd操作,共有三種cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。

  • IPC_STAT:該命令用來獲取訊息佇列資訊,返回的資訊存貯在buf指向的msqid結構中;
  • IPC_SET:該命令用來設定訊息佇列的屬性,要設定的屬性儲存在buf指向的msqid結構中;可設定屬性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes,同時,也影響msg_ctime成員。
  • IPC_RMID:刪除msqid標識的訊息佇列;

呼叫返回:成功返回0,否則返回-1。

附:

IPC隨程序持續、隨核心持續以及隨檔案系統持續的定義:

  • 隨程序持續:IPC一直存在到開啟IPC物件的最後一個程序關閉該物件為止。如管道和有名管道;
  • 隨核心持續:IPC一直持續到核心重新自舉或者顯示刪除該物件為止。如訊息佇列、訊號燈以及共享記憶體等;
  • 隨檔案系統持續:IPC一直持續到顯示刪除該物件為止。

訊號燈

訊號燈與其他程序間通訊方式不大相同,它主要提供對程序間共享資源訪問控制機制。相當於記憶體中的標誌,程序可以根據它判定是否能夠訪問某些共享資源,同時,程序也可以修改該標誌。除了用於訪問控制外,還可用於程序同步。訊號燈有以下兩種型別:

  • 二值訊號燈:最簡單的訊號燈形式,訊號燈的值只能取0或1,類似於互斥鎖。二值訊號燈能夠實現互斥鎖的功能,但兩者的關注內容不同。訊號燈強調共享資源,只要共享資源可用,其他程序同樣可以修改訊號燈的值;互斥鎖更強調程序,佔用資源的程序使用完資源後,必須由程序本身來解鎖。
  • 計算訊號燈:訊號燈的值可以取任意非負值(當然受核心本身的約束)。

系統V訊號燈是隨核心持續的,只有在核心重起或者顯示刪除一個訊號燈集時,該訊號燈集才會真正被刪除。因此係統中記錄訊號燈的資料結構(struct ipc_ids sem_ids)位於核心中,系統中的所有訊號燈都可以在結構sem_ids中找到訪問入口。
2、下圖說明了核心與訊號燈是怎樣建立起聯絡的:
其中:struct ipc_ids sem_ids是核心中記錄訊號燈的全域性資料結構;描述一個具體的訊號燈及其相關資訊。

其中,struct sem結構如下:

struct sem{
int semval;        // current value
int sempid        // pid of last operation
}

從圖中可以看出,全域性資料結構struct ipc_ids sem_ids可以訪問到struct kern_ipc_perm的第一個成員:struct kern_ipc_perm;而每個struct kern_ipc_perm能夠與具體的訊號燈對應起來是因為在該結構中,有一個key_t型別成員key,而key則唯一確定一個訊號燈集;同時,結構struct kern_ipc_perm的最後一個成員sem_nsems確定了該訊號燈在訊號燈集中的順序,這樣核心就能夠記錄每個訊號燈的資訊了。

操作訊號燈

對訊息佇列的操作無非有下面三種類型:
1、 開啟或建立訊號燈
與訊息佇列的建立及開啟基本相同,不再詳述。
2、  訊號燈值操作
linux可以增加或減小訊號燈的值,相應於對共享資源的釋放和佔有。具體參見後面的semop系統呼叫。
3、  獲得或設定訊號燈屬性:
系統中的每一個訊號燈集都對應一個struct sem_array結構,該結構記錄了訊號燈集的各種資訊,存在於系統空間。為了設定、獲得該訊號燈集的各種資訊及屬性,在使用者空間有一個重要的聯合結構與之對應,即union semun。

系統V訊號燈API

系統V訊息佇列API只有三個,使用時需要包括幾個標頭檔案:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

1、int semget(key_t key, int nsems, int semflg)
引數key是一個鍵值,由ftok獲得,唯一標識一個訊號燈集,用法與msgget()中的key相同;引數nsems指定開啟或者新建立的訊號燈集中將包含訊號燈的數目;semflg引數是一些標誌位。引數key和semflg的取值,以及何時開啟已有訊號燈集或者建立一個新的訊號燈集與msgget()中的對應部分相同,不再祥述。
該呼叫返回與健值key相對應的訊號燈集描述字。
呼叫返回:成功返回訊號燈集描述字,否則返回-1。
注:如果key所代表的訊號燈已經存在,且semget指定了IPC_CREAT|IPC_EXCL標誌,那麼即使引數nsems與原來訊號燈的數目不等,返回的也是EEXIST錯誤;如果semget只指定了IPC_CREAT標誌,那麼引數nsems必須與原來的值一致,在後面程式例項中還要進一步說明。

2、int semop(int semid, struct sembuf *sops, unsigned nsops);
semid是訊號燈集ID,sops指向陣列的每一個sembuf結構都刻畫一個在特定訊號燈上的操作。nsops為sops指向陣列的大小。
sembuf結構如下:

struct sembuf {
    unsigned short      sem_num;        /* semaphore index in array */
    short            sem_op;        /* semaphore operation */
    short            sem_flg;        /* operation flags */
};
sem_num對應訊號集中的訊號燈,0對應第一個訊號燈。sem_flg可取IPC_NOWAIT以及SEM_UNDO兩個標誌。如果設定了SEM_UNDO標誌,那麼在程序結束時,相應的操作將被取消,這是比較重要的一個標誌位。如果設定了該標誌位,那麼在程序沒有釋放共享資源就退出時,核心將代為釋放。如果為一個訊號燈設定了該標誌,核心都要分配一個sem_undo結構來記錄它,為的是確保以後資源能夠安全釋放。事實上,如果程序退出了,那麼它所佔用就釋放了,但訊號燈值卻沒有改變,此時,訊號燈值反映的已經不是資源佔有的實際情況,在這種情況下,問題的解決就靠核心來完成。這有點像殭屍程序,程序雖然退出了,資源也都釋放了,但核心程序表中仍然有它的記錄,此時就需要父程序呼叫waitpid來解決問題了。
sem_op的值大於0,等於0以及小於0確定了對sem_num指定的訊號燈進行的三種操作。具體請參考linux相應手冊頁。
這裡需要強調的是semop同時操作多個訊號燈,在實際應用中,對應多種資源的申請或釋放。semop保證操作的原子性,這一點尤為重要。尤其對於多種資源的申請來說,要麼一次性獲得所有資源,要麼放棄申請,要麼在不佔有任何資源情況下繼續等待,這樣,一方面避免了資源的浪費;另一方面,避免了程序之間由於申請共享資源造成死鎖。
也許從實際含義上更好理解這些操作:訊號燈的當前值記錄相應資源目前可用數目;sem_op>0對應相應程序要釋放sem_op數目的共享資源;sem_op=0可以用於對共享資源是否已用完的測試;sem_op<0相當於程序要申請-sem_op個共享資源。再聯想操作的原子性,更不難理解該系統呼叫何時正常返回,何時睡眠等待。
呼叫返回:成功返回0,否則返回-1。
3、 int semctl(int semid,int semnum,int cmd,union semun arg)
該系統呼叫實現對訊號燈的各種控制操作,引數semid指定訊號燈集,引數cmd指定具體的操作型別;引數semnum指定對哪個訊號燈操作,只對幾個特殊的cmd操作有意義;arg用於設定或返回訊號燈資訊。

共享記憶體

主要有兩種方式:mmap系統呼叫和系統V共享記憶體。系統呼叫mmap()通過對映一個普通檔案實現共享記憶體。系統V則是通過對映特殊檔案系統shm中的檔案實現程序間的共享記憶體通訊。也就是說,每個共享記憶體區域對應特殊檔案系統shm中的一個檔案。

系統V共享記憶體原理

程序間需要共享的資料被放在一個叫做IPC共享記憶體區域的地方,所有需要訪問該共享區域的程序都要把該共享區域對映到本程序的地址空間中去。系統V共享記憶體通過shmget獲得或建立一個IPC共享記憶體區域,並返回相應的識別符號。核心在保證shmget獲得或建立一個共享記憶體區,初始化該共享記憶體區相應的shmid_kernel結構注同時,還將在特殊檔案系統shm中,建立並開啟一個同名檔案,並在記憶體中建立起該檔案的相應dentry及inode結構,新開啟的檔案不屬於任何一個程序(任何程序都可以訪問該共享記憶體區)。所有這一切都是系統呼叫shmget完成的。

對於系統V共享記憶體,主要有以下幾個API:shmget()、shmat()、shmdt()及shmctl()。

#include <sys/ipc.h>
#include <sys/shm.h>

shmget()用來獲得共享記憶體區域的ID,如果不存在指定的共享區域就建立相應的區域。shmat()把共享記憶體區域對映到呼叫程序的地址空間中去,這樣,程序就可以方便地對共享區域進行訪問操作。shmdt()呼叫用來解除程序對共享記憶體區域的對映。shmctl實現對共享記憶體區域的控制操作。
注:shmget的內部實現包含了許多重要的系統V共享記憶體機制;shmat在把共享記憶體區域對映到程序空間時,並不真正改變程序的頁表。當程序第一次訪問記憶體對映區域訪問時,會因為沒有物理頁表的分配而導致一個缺頁異常,然後核心再根據相應的儲存管理機制為共享記憶體對映區域分配相應的頁表。

共享記憶體允許兩個或多個程序共享一給定的儲存區,因為資料不需要來回複製,所以是最快的一種程序間通訊機制。共享記憶體可以通過mmap()對映普通檔案(特殊情況下還可以採用匿名對映)機制實現,也可以通過系統V共享記憶體機制實現。應用介面和原理很簡單,內部機制複雜。為了實現更安全通訊,往往還與訊號燈等同步機制共同使用。

參考資料:

相關推薦

Linux程序通訊生產者消費者問題

生產者消費者問題(英語:Producer-consumerproblem),也稱有限緩衝問題(英語:Bounded-bufferproblem),是一個多執行緒同步問題的經典案例。該問題描述了兩個共享固定大小緩衝區的執行緒——即所謂的“生產者”和“消費者”——在實際執行時會

Linux Pipe (程序通訊生產者消費者

PIPE是Linux下可以用來實現程序間通訊的一種手段,當我們呼叫pipe系統呼叫時,將會產生一對檔案描述符,fd[0]可以用來讀,fd[1]用來寫,fd[1]寫的資料將會在fd[0]中讀到,我們稱之為管道。程序之間可以依賴管道的方式實現程序間通訊,pipe是半

訊號量共享記憶體實現程序通訊生產者消費者問題為例)

(一)訊號量訊號量是IPC的一種,可以看做是一個計數器,計數值為可用的共享資源的數量,訊號量可用於多程序的同步,為多個程序提供對共享資源的訪問。linux下的訊號量的介面函式如下:/*(1)獲取訊號量*/int semget(key_t key, int semnum, in

Linux FIFO (程序通訊生產者消費者

上一篇中我們寫到了PIPE無名管道,的確是一種很方便的通訊機制,但是其有一個缺點就是,PIPE是依賴於檔案描述符的,並不在檔案系統中維護,如果兩個通訊程序之間沒有共同的祖先,他們就無法拿到相同的檔案表項,所以沒有共同祖先的兩個程序是不能通過PIPE直接通訊的。為

Linux程序通訊--共享記憶體訊號量

1. 建立共享記憶體,shmget() shmget(建立或開啟共享記憶體) 表頭檔案#include <sys/ipc.h>#include <sys/shm.h> 函式定義  int shmget(key_t key, size_t size, int shmflg); 函式說明k

Linux程序通訊(IPC)方式總結

程序間通訊概述 程序通訊的目的 資料傳輸  一個程序需要將它的資料傳送給另一個程序,傳送的資料量在一個位元組到幾M位元組之間 共享資料  多個程序想要操作共享資料,一個程序對共享資料 通知事件 一個程序需要向另一個或一組程序傳送訊息,通知它(它們)

c/c++ linux 程序通訊系列7,使用pthread mutex

linux 程序間通訊系列7,使用pthread mutex #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/shm.h> #include <pthr

c/c++ linux 程序通訊系列4,使用共享記憶體

linux 程序間通訊系列4,使用共享記憶體 1,建立共享記憶體,用到的函式shmget, shmat, shmdt 函式名 功能描述 shmget 建立共享記憶體,返回pic key

c/c++ linux 程序通訊系列3,使用socketpair,pipe

linux 程序間通訊系列3,使用socketpair,pipe 1,使用socketpair,實現程序間通訊,是雙向的。 2,使用pipe,實現程序間通訊 使用pipe關鍵點:fd[0]只能用於接收,fd[1]只能用於傳送,是單向的。 3,使用pipe,用標準輸入往裡寫。 疑問:在

linux 程序通訊之FIFO

1.概述 FIFO與管道幾乎類似,所以FIFO也是一個位元組流,從FIFO讀取的順序也是與被寫入FIFO的順序一致,容量是也有限的,也是可以確保寫入不超過PIPE_BUF位元組的操作是原子的,FIFO的本質也是一個管道,但傳遞方向是可以雙向的,它們兩者之間的最大差別在於FIFO在檔案系統中擁有一個名稱,並且

c/c++ linux 程序通訊系列2,使用UNIX_SOCKET

linux 程序間通訊系列2,使用UNIX_SOCKET 1,使用stream,實現程序間通訊 2,使用DGRAM,實現程序間通訊 關鍵點:使用一個臨時的檔案,進行資訊的互傳。 s_un.sun_family = AF_UNIX; strcpy(s_un.sun_path, "

c/c++ linux 程序通訊系列1,使用signal,kill

linux 程序間通訊系列1,使用signal,kill 訊號基本概念:  軟中斷訊號(signal,又簡稱為訊號)用來通知程序發生了非同步事件。程序之間可以互相通過系統呼叫kill傳送軟中斷訊號。核心也可以因為內部事件而給程序傳送訊號,通知程序發生了某個事件。注意,訊號只是用來通知某程序發生了什

Linux程序通訊IPC的幾種方式簡介

Linux程序通訊的源頭       linux下的程序通訊手段基本上是從Unix平臺上的程序通訊手段繼承而來的。而對Unix發展做出重大貢獻的兩大主力AT&T(原為American Telephone & Tele

Linux程序通訊總結

  目錄 訊號 管道 命名管道 System V IPC 組成 識別符號 ftok函式 結構定義 特點 訊息佇列 訊號量 共享記憶體 套接字 Linux下的程序間通訊(Interprocess Communication,

linux 程序通訊方式

1 無名管道通訊 無名管道( pipe ):管道是一種半雙工的通訊方式,資料只能單向流動,而且只能在具有親緣關係的程序間使用。程序的親緣關係通常是指父子程序關係。 2 高階管道通訊 高階管道(popen):將另一個程式當做一個新的程序在當前程式程序中啟動,則它算是當前程式的子程序

Linux 程序通訊的一種實現方式

程序間通訊的方式一般有三種:管道(普通管道、流管道和命名管道)、系統IPC(訊息佇列、訊號和共享儲存)、套接字(Socket) 本部落格以之前所做的智慧車專案為例,簡單介紹下共享儲存的一種實現過程。簡單說來,程序1將資料寫入到一個公共檔案input.txt中,程序2對其進行

Linux 程序通訊(四)訊號量

1 訊號量概述 訊號量和其他IPC不同,並沒有在程序之間傳送資料,訊號量用於多程序在存取共享資源時的同步控制就像交通路口的紅路燈一樣,當訊號量大於0,表示綠燈允許通過,當訊號量等於0,表示紅燈,必須停下來等待綠燈才能通過。 程序間的互斥關係與同步關係存在的根源在於臨界資

Linux 程序通訊(五)IPC的特性

1.識別符號和鍵 每個核心中的IPC結構(訊息佇列、訊號量或共享儲存段)都用一個非負整數的識別符號 (identifier)加以引用。 例如,要向一個訊息佇列傳送訊息或者從一個訊息佇列取訊息,只需要知道其佇列識別符號。 當一個IPC結構被建立,然後又被刪除時,與這種結構

Linux 程序通訊(六)共享記憶體

可以說, 共享記憶體是一種最為高效的程序間通訊方式, 因為程序可以直接讀寫記憶體, 不需要任何資料的複製。 為了在多個程序間交換資訊, 核心專門留出了一塊記憶體區, 這段記憶體區可以由需要訪問的程序將其對映到自己的私有地址空間。 因此, 程序就可以直接讀寫這一記憶體區而不需要

linux程序通訊(IPC)小結

linux IPC型別 1、匿名管道 2、命名管道 3、訊號 4、訊息佇列 5、共享記憶體 6、訊號量 7、Socket 1、匿名管道 過程: 1、管道實質是一個核心緩衝區,先進先出(佇列)讀取緩衝區記憶體資料 2、一個數據只能讀一次,讀完後在緩衝區就不存在