1. 程式人生 > 其它 >【windows 作業系統】程序間通訊(IPC)簡述|無名管道和命名管道 訊息佇列、訊號量、共享儲存、Socket、Streams等

【windows 作業系統】程序間通訊(IPC)簡述|無名管道和命名管道 訊息佇列、訊號量、共享儲存、Socket、Streams等

一、程序間通訊簡述


每個程序各自有不同的使用者地址空間,任何一個程序的全域性變數在另一個程序中都看不到,所以程序之間要交換資料必須通過核心,在核心中開闢一塊緩衝區,程序1把資料從使用者空間拷到核心緩衝區,程序2再從核心緩衝區把資料讀走,核心提供的這種機制稱為程序間通訊(IPC,InterProcess Communication),即指在不同程序之間傳播或交換資訊。

IPC的方式通常有管道(包括無名管道和命名管道)、訊息佇列、訊號量、共享儲存、Socket、Streams等。其中 Socket和Streams支援不同主機上的兩個程序IPC。IPC資源必須刪除,否則不會⾃動清除,除⾮重啟,所以system V IPC資源的⽣命週期隨核心而共生。


程序間通訊模型

程序間通訊的本質:讓兩個不同的程序看到同一份資源(該資源通常由作業系統直接或間接提供)

程序間通訊目的:
資料傳輸:一個程序需要將它的資料傳送給另一個程序
資源共享:多個程序之間共享有同樣的資源
通知事件:一個程序需要向另一個或一組程序傳送訊息,通知它(它們)發生了某種事件(如:程序終止時要通知父程序)
程序控制:有些程序希望完全控制另一個程序的執行(如:Debug程序),此時控制程序希望能夠攔截另一個程序的所有陷入和異常,並能夠及時知道它的狀態改變。

IPC的分類:

管道
匿名管道
命名管道

System V IPC
System V 訊息佇列
System V 共享儲存
System V 訊號量

POSIX IPC
訊息佇列
共享記憶體
訊號量
互斥量
條件變數
讀寫鎖

其它概念引入
臨界資源:多道程式系統中存在許多程序,它們共享各種資源,然而有很多資源一次只能供一個程序使用。一次僅允許一個程序使用的資源稱為臨界資源。許多物理裝置都屬於臨界資源,如輸入機、印表機、磁帶機等。
臨界區:每個程序中訪問臨界資源的那段程式碼稱為臨界區。
互斥:任何一個時刻,只允許有一個程序進入臨界資源進行資源訪問,在其資源訪問期間其他程序不得訪問。
同步:在保證安全的前提條件下,程序按照特定的順序訪問臨界資源。
原子性:指一個操作是不可中斷的,要麼執行成功要麼執行失敗,不會有第三態。

二、程序間通訊的7種方式

第一類:傳統的Unix通訊機制

1. 管道/匿名管道(pipe)

管道,一般指匿名管道,是 UNIX 系統中 IPC最古老的形式。
通常把從一個程序連結到另一個程序的一個數據流成為一個“管道”。管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道。
只能用於父子程序或者兄弟程序之間(具有親緣關係的程序)。
單獨構成一種獨立的檔案系統:管道對於管道兩端的程序而言,就是一個檔案,但它不是普通的檔案,它不屬於某種檔案系統,而是自立門戶,單獨構成一種檔案系統,並且只存在與記憶體中。
資料的讀出和寫入:一個程序向管道中寫的內容被管道另一端的程序讀出。寫入的內容每次都新增在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出資料。


程序間管道通訊模型

管道的實質:
管道的實質是一個核心緩衝區,程序以先進先出的方式從緩衝區存取資料,管道一端的程序順序的將資料寫入緩衝區,另一端的程序則順序的讀出資料。
該緩衝區可以看做是一個迴圈佇列,讀和寫的位置都是自動增長的,不能隨意改變,一個數據只能被讀一次,讀出來以後在緩衝區就不復存在了。
當緩衝區讀空或者寫滿時,有一定的規則控制相應的讀程序或者寫程序進入等待佇列,當空的緩衝區有新資料寫入或者滿的緩衝區有資料讀出來時,就喚醒等待佇列中的程序繼續讀寫。

管道的侷限:
管道的主要侷限性正體現在它的特點上:
只支援單向資料流;
只能用於具有親緣關係的程序之間;
沒有名字;
管道的緩衝區是有限的(管道制存在於記憶體中,在管道建立時,為緩衝區分配一個頁面大小);
管道所傳送的是無格式位元組流,這就要求管道的讀出方和寫入方必須事先約定好資料的格式,比如多少位元組算作一個訊息(或命令、或記錄)等等;

特點:
管道是一種特殊的檔案,對於它的讀寫可以使用普通的read、write 等函式。但是它不同於普通檔案,它不屬於其他任何檔案系統,且只存在於記憶體中。
管道是半雙工的,只允許單向通訊(即資料只能向一個方向上流動),具有固定的讀端和寫端;需要雙方通訊時,需要建立起兩個管道
面向位元組流
一般,核心會對管道操作進行同步與互斥(即管道自帶互斥和同步機制)
程序退出,管道釋放,所以管道的生命週期隨通訊雙方的程序

匿名管道
只能用於具有親緣關係的程序之間的通訊,常用於父子程序或者兄弟、爺孫程序之間通訊。
子程序繼承父程序的檔案描述符、資料、程式碼等。
子程序不可直接繼承父程序的時間片,由父程序分配(時間片即CPU分配給各個程式的時間,每個執行緒被分配一個時間段,稱作它的時間片,即該程序允許執行的時間,使各個程式從表面上看是同時進行的。)

原型
#include <unistd.h>
int pipe(int fd[2]);

功能:建立一個匿名管道
引數:fd—>檔案描述符,其中fd[0]表示讀端,fd[1]表示寫端
返回值:若成功返回0,失敗返回錯誤程式碼

建立一個管道時,會建立兩個檔案描述符,讀端和寫段,如下圖:



用fork共享管道原理




若要資料流從父程序流向子程序,則關閉父程序的讀端(fd[0])與子程序的寫端(fd[1]);反之,則可以使資料流從子程序流向父程序。

#include<stdio.h>
#include<unistd.h>

int main(){
int fd[2]; // 兩個檔案描述符
pid_t pid;
char buf[20];

if(pipe(fd) < 0) // 建立管道
printf("Create Pipe Error!\n");

if((pid = fork()) < 0) // 建立子程序
printf("Fork Error!\n");
else if(pid > 0){ // 父程序
close(fd[0]); // 關閉讀端
write(fd[1], "hello world\n", 12);
}else{
close(fd[1]); // 關閉寫端
read(fd[0], buf, 20);
printf("%s", buf);
}

return 0;
}


2. 命名管道(FIFO)

匿名管道,由於沒有名字,只能用於親緣關係的程序間通訊。為了克服這個缺點,提出了有名管道(FIFO)。

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

匿名管道和有名管道總結:
(1)管道是特殊型別的檔案,在滿足先入先出的原則條件下可以進行讀寫,但不能進行定位讀寫。
(2)匿名管道是單向的,只能在有親緣關係的程序間通訊;有名管道以磁碟檔案的方式存在,可以實現本機任意兩個程序通訊。
(3)無名管道阻塞問題:無名管道無需顯示開啟,建立時直接返回檔案描述符,在讀寫時需要確定對方的存在,否則將退出。如果當前程序向無名管道的一端寫資料,必須確定另一端有某一程序。如果寫入無名管道的資料超過其最大值,寫操作將阻塞,如果管道中沒有資料,讀操作將阻塞,如果管道發現另一端斷開,將自動退出。
(4)有名管道阻塞問題:有名管道在開啟時需要確實對方的存在,否則將阻塞。即以讀方式開啟某管道,在此之前必須一個程序以寫方式開啟管道,否則阻塞。此外,可以以讀寫(O_RDWR)模式開啟有名管道,即當前程序讀,當前程序寫,不會阻塞。


命名管道可以從命令列上建立,使用下面的命令:
$ mkfifo filename

命名管道也可以從程式裡建立,相關函式為:
#include <sys/stat.h>
int mkfifo ( const char *filename, mode_t mode );

返回值:成功返回0,出錯返回-1

建立命名管道:
int main ( int argc , char *argv[]){
mkfifo ( "p2" , 0644 );
return 0 ;
}

匿名管道與命名管道的區別
匿名管道由 pipe 函式建立並開啟
命名管道由 mkfifo 函式建立,開啟用 open
FIFO(命名管道)與 pipe(匿名管道)之間唯一的區別在於它們建立與開啟的方式不同,一旦這些工作完成之後,就會有相同的語義。

例子:讀取檔案,寫入命名管道
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m)
do{
perror(m);
exit(EXIT_FAILURE);
} while(0)

int main(int argc, char *argv[]){
mkfifo("tp", 0644);
int infd;
infd = open("abc", O_RDONLY);
if (infd == -1) ERR_EXIT("open");
int outfd;
outfd = open("tp", O_WRONLY);
if (outfd == -1) ERR_EXIT("open");
char buf[1024];
int n;
while ((n=read(infd, buf, 1024))>0){
write(outfd, buf, n);
}
close(infd);
close(outfd);
return 0;
}

延伸閱讀:該部落格有匿名管道和有名管道的C語言實踐

3. 訊號(Signal)

訊號是Linux系統中用於程序間互相通訊或者操作的一種機制,訊號可以在任何時候發給某一程序,而無需知道該程序的狀態。
如果該程序當前並未處於執行狀態,則該訊號就有核心儲存起來,知道該程序回覆執行並傳遞給它為止。
如果一個訊號被程序設定為阻塞,則該訊號的傳遞被延遲,直到其阻塞被取消是才被傳遞給程序。

Linux系統中常用訊號:
(1)SIGHUP:使用者從終端登出,所有已啟動程序都將收到該程序。系統預設狀態下對該訊號的處理是終止程序。
(2)SIGINT:程式終止訊號。程式執行過程中,按Ctrl+C鍵將產生該訊號。
(3)SIGQUIT:程式退出訊號。程式執行過程中,按Ctrl+\\鍵將產生該訊號。
(4)SIGBUS和SIGSEGV:程序訪問非法地址。
(5)SIGFPE:運算中出現致命錯誤,如除零操作、資料溢位等。
(6)SIGKILL:使用者終止程序執行訊號。shell下執行kill -9傳送該訊號。
(7)SIGTERM:結束程序訊號。shell下執行kill 程序pid傳送該訊號。
(8)SIGALRM:定時器訊號。
(9)SIGCLD:子程序退出訊號。如果其父程序沒有忽略該訊號也沒有處理該訊號,則子程序退出後將形成殭屍程序。

訊號來源
訊號是軟體層次上對中斷機制的一種模擬,是一種非同步通訊方式,,訊號可以在使用者空間程序和核心之間直接互動,核心可以利用訊號來通知使用者空間的程序發生了哪些系統事件,訊號事件主要有兩個來源:
硬體來源:使用者按鍵輸入Ctrl+C退出、硬體異常如無效的儲存訪問等。
軟體終止:終止程序訊號、其他程序呼叫kill函式、軟體異常產生訊號。

訊號生命週期和處理流程
(1)訊號被某個程序產生,並設定此訊號傳遞的物件(一般為對應程序的pid),然後傳遞給作業系統;
(2)作業系統根據接收程序的設定(是否阻塞)而選擇性的傳送給接收者,如果接收者阻塞該訊號(且該訊號是可以阻塞的),作業系統將暫時保留該訊號,而不傳遞,直到該程序解除了對此訊號的阻塞(如果對應程序已經退出,則丟棄此訊號),如果對應程序沒有阻塞,作業系統將傳遞此訊號。
(3)目的程序接收到此訊號後,將根據當前程序對此訊號設定的預處理方式,暫時終止當前程式碼的執行,保護上下文(主要包括臨時暫存器資料,當前程式位置以及當前CPU的狀態)、轉而執行中斷服務程式,執行完成後在回覆到中斷的位置。當然,對於搶佔式核心,在中斷返回時還將引發新的排程。


訊號的生命週期

4. 訊息(Message)佇列

訊息佇列是存放在核心中的訊息連結串列,每個訊息佇列由訊息佇列識別符號表示。

與管道(無名管道:只存在於記憶體中的檔案;命名管道:存在於實際的磁碟介質或者檔案系統)不同的是訊息佇列存放在核心中,只有在核心重啟(即,作業系統重啟)或者顯示地刪除一個訊息佇列時,該訊息佇列才會被真正的刪除。

另外與管道不同的是,訊息佇列在某個程序往一個佇列寫入訊息之前,並不需要另外某個程序在該佇列上等待訊息的到達。

訊息佇列提供了⼀個從⼀個程序向另外⼀個程序傳送⼀塊資料的⽅法;每個資料塊都被認為是有⼀個型別,接收者程序接收的資料塊可以有不同的型別值;訊息佇列也有管道⼀樣的不⾜,就是每個訊息的最⼤⻓度是有上限的(MSGMAX),每個訊息佇列的總的位元組數是有上限的(MSGMNB),系統上訊息佇列的總數也有⼀個上限(MSGMNI)。


延伸閱讀:訊息佇列C語言的實踐

訊息佇列特點總結:
(1)訊息佇列是訊息的連結串列,具有特定的格式,存放在記憶體中並由訊息佇列識別符號標識.
(2)訊息佇列允許一個或多個程序向它寫入與讀取訊息.
(3)管道和訊息佇列的通訊資料都是先進先出的原則。
(4)訊息佇列可以實現訊息的隨機查詢,訊息不一定要以先進先出的次序讀取,也可以按訊息的型別讀取.比FIFO更有優勢。
(5)訊息佇列克服了訊號承載資訊量少,管道只能承載無格式字 節流以及緩衝區大小受限等缺。
(6)目前主要有兩種型別的訊息佇列:POSIX訊息佇列以及System V訊息佇列,系統V訊息佇列目前被大量使用。系統V訊息佇列是隨核心持續的,只有在核心重起或者人工刪除時,該訊息佇列才會被刪除。

訊息佇列函式

msgget

原型
int msgget(key_t key, int msgflg);

功能:⽤來建立和訪問⼀個訊息佇列
引數:
key—> 某個訊息佇列的名字
msgflg—>由九個許可權標誌構成,它們的⽤法和建立⽂件時使⽤的mode模式標誌是⼀樣的
返回值:成功返回⼀個⾮負整數,即該訊息佇列的標識碼;失敗返回-1

msgctl

原型
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

功能 :訊息佇列的控制函式
引數 :
msqid—>由msgget函式返回的訊息佇列標識碼
cmd—>是將要採取的動作,(有三個可取值)
返回值 :成功返回0,失敗返回-1


5. 共享記憶體(share memory)

使得多個程序可以可以直接讀寫同一塊記憶體空間,是最快的可用IPC形式,是針對其他通訊機制執行效率較低而設計的。

為了在多個程序間交換資訊,核心專門留出了一塊記憶體區,可以由需要訪問的程序將其對映到自己的私有地址空間。程序就可以直接讀寫這一塊記憶體而不需要進行資料的拷貝,從而大大提高效率。
由於多個程序共享一段記憶體,因此需要依靠某種同步機制(如訊號量)來達到程序間的同步及互斥。

特點
共享記憶體是最快的一種 IPC,因為程序是直接對記憶體進行存取(⼀旦這樣的記憶體對映到共享它的程序的地址空間,這些程序間資料傳遞不再涉及到核心,換句話說是程序不再通過執⾏進⼊核心的系統調⽤來傳遞彼此的資料)。
因為多個程序可以同時操作,所以需要進行同步。
訊號量+共享記憶體通常結合在一起使用,訊號量用來同步對共享記憶體的訪問。

共享記憶體示意圖:




延伸閱讀:Linux支援的主要三種共享記憶體方式:mmap()系統呼叫、Posix共享記憶體,以及System V共享記憶體實踐


共享記憶體原理圖

共享記憶體函式

shmget

原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

功能:⽤來建立共享記憶體
引數 :
key—> 這個共享記憶體段名字
size—>共享記憶體⼤⼩
shmflg—>由九個許可權標誌構成,它們的⽤法和建立⽂件時使⽤的mode模式標誌是⼀樣的
(內部有 IPC_CREAT 和 IPC_EXCL :當兩個引數共同使用時,返回正常,則會建立新的共享記憶體;返回失敗,則已存在共享記憶體)
返回值:成功返回⼀個⾮負整數,即該共享記憶體段的標識碼;失敗返回-1

shmat(掛接、對映)

原型
void *shmat(int shmid, const void *shmaddr, int shmflg);

功能:將共享記憶體段連線到程序地址空間
引數:
shmid—> 共享記憶體標識
shmaddr—>指定連線的地址
shmflg—>它的兩個可能取值是SHM_RND和SHM_RDONLY
返回值(對映的地址):成功返回⼀個指標,指向共享記憶體第⼀個節;失敗返回-1

注意:
shmaddr為NULL,核⼼⾃動選擇⼀個地址
shmaddr不為NULL且shmflg⽆SHM_RND標記,則以shmaddr為連線地址。
shmaddr不為NULL且shmflg設定了SHM_RND標記,則連線的地址會⾃動向下調整為SHMLBA的整數倍。公式:s
hmaddr - (shmaddr % SHMLBA)
shmflg(許可權)=SHM_RDONLY,表⽰連線操作⽤來只讀共享記憶體

shmdt(取消關聯)

原型
int shmdt(const void *shmaddr);

功能:將共享記憶體段與當前程序脫離
引數:
shmaddr—>由shmat所返回的指標
返回值:成功返回0;失敗返回-1
注意:將共享記憶體段與當前程序脫離不等於刪除共享記憶體段

shmctl

原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能:⽤於控制共享記憶體
引數:
shmid—>由shmget返回的共享記憶體標識碼
cmd—>將要採取的動作(有三個可取值)
buf—>指向⼀個儲存著共享記憶體的模式狀態和訪問許可權的資料結構
返回值:成功返回0;失敗返回-1


6. 訊號量(semaphore)

訊號量主要用於同步和互斥,其本質上是一個具有原子特性的計數器,這一計數器用來描述臨界資源當中資源的數目。訊號量是一個計數器,用於多程序對共享資料的訪問,訊號量的意圖在於程序間同步。為了獲得共享資源,程序需要執行下列操作:
(1)建立一個訊號量:這要求呼叫者指定初始值,對於二值訊號量來說,它通常是1,也可是0。
(2)等待一個訊號量:該操作會測試這個訊號量的值,如果小於0,就阻塞。也稱為P操作。
(3)掛出一個訊號量:該操作將訊號量的值加1,也稱為V操作。

訊號量結構體虛擬碼 :
struct semaphore{
int value;
pointer_PCB queue;
}

為了正確地實現訊號量,訊號量值的測試及減1操作應當是原子操作。為此,訊號量通常是在核心中實現的。Linux環境中,有三種類型:Posix(可移植性作業系統介面)有名訊號量(使用Posix IPC名字標識)、Posix基於記憶體的訊號量(存放在共享記憶體區中)、System V訊號量(在核心中維護)。這三種訊號量都可用於程序間或執行緒間的同步。


兩個程序使用一個二值訊號量


兩個程序所以用一個Posix有名二值訊號量


一個程序兩個執行緒共享基於記憶體的訊號量


訊號量與普通整型變數的區別:
(1)訊號量是非負整型變數,除了初始化之外,它只能通過兩個標準原子操作:wait(semap) , signal(semap) ; 來進行訪問;
(2)操作也被成為PV原語(P來源於荷蘭語proberen"測試",V來源於荷蘭語verhogen"增加",P表示通過的意思,V表示釋放的意思),而普通整型變數則可以在任何語句塊中被訪問;

訊號量與互斥量之間的區別:
(1)互斥量用於執行緒的互斥,訊號量用於執行緒的同步。這是互斥量和訊號量的根本區別,也就是互斥和同步之間的區別。
互斥:是指某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。
同步:是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。

在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源

(2)互斥量值只能為0/1,訊號量值可以為非負整數。
也就是說,一個互斥量只能用於一個資源的互斥訪問,它不能實現多個資源的多執行緒互斥問題。訊號量可以實現多個同類資源的多執行緒互斥和同步。當訊號量為單值訊號量是,也可以完成一個資源的互斥訪問。

(3)互斥量的加鎖和解鎖必須由同一執行緒分別對應使用,訊號量可以由一個執行緒釋放,另一個執行緒得到。


程序互斥
由於各程序要求共享資源,⽽且有些資源需要互斥使⽤,因此各程序間競爭使⽤這些資源,程序的這種關係為程序的互斥
系統中某些資源⼀次只允許⼀個程序使⽤,稱這樣的資源為臨界資源或互斥資源
在程序中涉及到互斥資源的程式段叫臨界區

特性:IPC資源必須刪除,否則不會自動清除,除非重啟,所以 System V IPC 資源的生命週期隨核心。

訊號量函式

semget

int semget(key_t key, int nsems, int semflg);

功能:⽤來建立和訪問⼀個訊號量集
引數 :
key—>訊號集的名字
nsems—>訊號集中訊號量的個數
semflg—> 由九個許可權標誌構成,它們的⽤法和建立⽂件時使⽤的mode模式標誌是⼀樣的
返回值:成功返回⼀個⾮負整數,即該訊號集的標識碼;失敗返回-1

shmctl

原型
int semctl(int semid, int semnum, int cmd, ...);

功能:⽤於控制訊號量集
引數:
semid—>由semget返回的訊號集標識碼
semnum—>訊號集中訊號量的序號
cmd—>將要採取的動作(有三個可取值)
最後⼀個引數根據命令不同⽽不同
返回值:成功返回0;失敗返回-1

semop

原型
int semop(int semid, struct sembuf *sops, unsigned nsops);

功能:⽤來建立和訪問⼀個訊號量集
引數:
semid—>是該訊號量的標識碼,也就是semget函式的返回值
sops—>是個指向⼀個結構數值的指標
nsops—>訊號量的個數
返回值:成功返回0;失敗返回-1


7. 套接字(socket)

套接字是一種通訊機制,憑藉這種機制,客戶/伺服器(即要進行通訊的程序)系統的開發工作既可以在本地單機上進行,也可以跨網路進行。也就是說它可以讓不在同一臺計算機但通過網路連線計算機上的程序進行通訊。


Socket是應用層和傳輸層之間的橋樑

套接字是支援TCP/IP的網路通訊的基本操作單元,可以看做是不同主機之間的程序進行雙向通訊的端點,簡單的說就是通訊的兩方的一種約定,用套接字中的相關函式來完成通訊過程。

套接字特性

套接字的特性由3個屬性確定,它們分別是:域、埠號、協議型別。
(1)套接字的域
它指定套接字通訊中使用的網路介質,最常見的套接字域有兩種:
一是AF_INET,它指的是Internet網路。當客戶使用套接字進行跨網路的連線時,它就需要用到伺服器計算機的IP地址和埠來指定一臺聯網機器上的某個特定服務,所以在使用socket作為通訊的終點,伺服器應用程式必須在開始通訊之前繫結一個埠,伺服器在指定的埠等待客戶的連線。另一個域AF_UNIX,表示UNIX檔案系統,它就是檔案輸入/輸出,而它的地址就是檔名。

(2)套接字的埠號
每一個基於TCP/IP網路通訊的程式(程序)都被賦予了唯一的埠和埠號,埠是一個資訊緩衝區,用於保留Socket中的輸入/輸出資訊,埠號是一個16位無符號整數,範圍是0-65535,以區別主機上的每一個程式(埠號就像房屋中的房間號),低於256的埠號保留給標準應用程式,比如pop3的埠號就是110,每一個套接字都組合進了IP地址、埠,這樣形成的整體就可以區別每一個套接字。

(3)套接字協議型別
因特網提供三種通訊機制:
一是流套接字,流套接字在域中通過TCP/IP連線實現,同時也是AF_UNIX中常用的套接字型別。流套接字提供的是一個有序、可靠、雙向位元組流的連線,因此傳送的資料可以確保不會丟失、重複或亂序到達,而且它還有一定的出錯後重新發送的機制。

二個是資料報套接字,它不需要建立連線和維持一個連線,它們在域中通常是通過UDP/IP協議實現的。它對可以傳送的資料的長度有限制,資料報作為一個單獨的網路訊息被傳輸,它可能會丟失、複製或錯亂到達,UDP不是一個可靠的協議,但是它的速度比較高,因為它並一需要總是要建立和維持一個連線。

三是原始套接字,原始套接字允許對較低層次的協議直接訪問,比如IP、 ICMP協議,它常用於檢驗新的協議實現,或者訪問現有服務中配置的新裝置,因為RAW SOCKET可以自如地控制Windows下的多種協議,能夠對網路底層的傳輸機制進行控制,所以可以應用原始套接字來操縱網路層和傳輸層應用。比如,我們可以通過RAW SOCKET來接收發向本機的ICMP、IGMP協議包,或者接收TCP/IP棧不能夠處理的IP包,也可以用來發送一些自定包頭或自定協議的IP包。網路監聽技術很大程度上依賴於SOCKET_RAW。

原始套接字與標準套接字的區別在於:
原始套接字可以讀寫核心沒有處理的IP資料包,而流套接字只能讀取TCP協議的資料,資料報套接字只能讀取UDP協議的資料。因此,如果要訪問其他協議傳送資料必須使用原始套接字。

套接字通訊的建立


Socket通訊基本流程

伺服器端
(1)首先伺服器應用程式用系統呼叫socket來建立一個套接字,它是系統分配給該伺服器程序的類似檔案描述符的資源,它不能與其他的程序共享。
(2)然後,伺服器程序會給套接字起個名字,我們使用系統呼叫bind來給套接字命名。然後伺服器程序就開始等待客戶連線到這個套接字。
(3)接下來,系統呼叫listen來建立一個佇列並將其用於存放來自客戶的進入連線。
(4)最後,伺服器通過系統呼叫accept來接受客戶的連線。它會建立一個與原有的命名套接不同的新套接字,這個套接字只用於與這個特定客戶端進行通訊,而命名套接字(即原先的套接字)則被保留下來繼續處理來自其他客戶的連線(建立客戶端和服務端的用於通訊的流,進行通訊)。

客戶端
(1)客戶應用程式首先呼叫socket來建立一個未命名的套接字,然後將伺服器的命名套接字作為一個地址來呼叫connect與伺服器建立連線。
(2)一旦連線建立,我們就可以像使用底層的檔案描述符那樣用套接字來實現雙向資料的通訊(通過流進行資料傳輸)。


套接字(socket)也是一種程序間通訊機制,與其他通訊機制不同的是,它可用於不同機器間的程序通訊。通訊操作過程函式如下:

(1).命名socket
SOCK_STREAM 式本地套接字的通訊雙方均需要具有本地地址,其中伺服器端的本地地址需要明確指定,指定方法是使用 struct sockaddr_un 型別的變數。

(2).繫結
SOCK_STREAM 式本地套接字的通訊雙方均需要具有本地地址,其中伺服器端的本地地址需要明確指定,指定方法是使用 struct sockaddr_un 型別的變數,將相應欄位賦值,再將其繫結在建立的伺服器套接字上,繫結要使用 bind 系統呼叫,其原形如下:
int bind(int socket, const struct sockaddr *address, size_t address_len);

其中 socket表示伺服器端的套接字描述符,address 表示需要繫結的本地地址,是一個 struct sockaddr_un 型別的變數,address_len 表示該本地地址的位元組長度。

(3).監聽
伺服器端套接字建立完畢並賦予本地地址值(名稱,本例中為Server Socket)後,需要進行監聽,等待客戶端連線並處理請求,監聽使用 listen 系統呼叫,接受客戶端連線使用accept系統呼叫,它們的原形如下:
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr *address, size_t *address_len);

其中 socket 表示伺服器端的套接字描述符;backlog 表示排隊連線佇列的長度(若有多個客戶端同時連線,則需要進行排隊);address 表示當前連線客戶端的本地地址,該引數為輸出引數,是客戶端傳遞過來的關於自身的資訊;address_len 表示當前連線客戶端本地地址的位元組長度,這個引數既是輸入引數,又是輸出引數。

(4).連線伺服器

客戶端套接字建立完畢並賦予本地地址值後,需要連線到伺服器端進行通訊,讓伺服器端為其提供處理服務。對於SOCK_STREAM型別的流式套接字,需要客戶端與伺服器之間進行連線方可使用。連線要使用 connect 系統呼叫,其原形為:
int connect(int socket, const struct sockaddr *address, size_t address_len);

其中socket為客戶端的套接字描述符,address表示當前客戶端的本地地址,是一個 struct sockaddr_un 型別的變數,address_len 表示本地地址的位元組長度。實現連線的程式碼如下:
connect(client_sockfd, (struct sockaddr*)&client_address, sizeof(client_address));

(5).相互發送接收資料
無論客戶端還是伺服器,都要和對方進行資料上的互動,這種互動也正是我們程序通訊的主題。一個程序扮演客戶端的角色,另外一個程序扮演伺服器的角色,兩個程序之間相互發送接收資料,這就是基於本地套接字的程序通訊。傳送和接收資料要使用 write 和 read 系統呼叫,它們的原形為:
int read(int socket, char *buffer, size_t len);
int write(int socket, char *buffer, size_t len);

其中 socket 為套接字描述符;len 為需要傳送或需要接收的資料長度;
對於 read 系統呼叫,buffer 是用來存放接收資料的緩衝區,即接收來的資料存入其中,是一個輸出引數;
對於 write 系統呼叫,buffer 用來存放需要傳送出去的資料,即 buffer 內的資料被髮送出去,是一個輸入引數;返回值為已經發送或接收的資料長度。

(6).斷開連線
互動完成後,需要將連線斷開以節省資源,使用close系統呼叫,其原形為:
int close(int socket);


三、以Linux中的C語言程式設計為例

-------------------------------
管道

管道,通常指無名管道,是 UNIX 系統IPC最古老的形式。

1、特點
它是半雙工的(即資料只能在一個方向上流動),具有固定的讀端和寫端。
它只能用於具有親緣關係的程序之間的通訊(也是父子程序或者兄弟程序之間)。它可以看成是一種特殊的檔案,對於它的讀寫也可以使用普通的read、write等函式。但是它不是普通的檔案,並不屬於其他任何檔案系統,並且只存在於記憶體中。

2、原型
#include <unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失敗返回-1

當一個管道建立時,它會建立兩個檔案描述符:fd[0]為讀而開啟,fd[1]為寫而開啟。如下圖:


要關閉管道只需將這兩個檔案描述符關閉即可。

3、例子
單個程序中的管道幾乎沒有任何用處。所以,通常呼叫 pipe 的程序接著呼叫 fork,這樣就建立了父程序與子程序之間的 IPC 通道。如下圖所示:



若要資料流從父程序流向子程序,則關閉父程序的讀端(fd[0])與子程序的寫端(fd[1]);反之,則可以使資料流從子程序流向父程序。

#include<stdio.h>
#include<unistd.h>

int main(){
int fd[2]; // 兩個檔案描述符
pid_t pid;
char buff[20];

if (pipe(fd) < 0) // 建立管道
printf("Create Pipe Error!\n");

if ((pid = fork()) < 0) { // 建立子程序
printf("Fork Error!\n");
} else if(pid > 0) { // 父程序
close(fd[0]); // 關閉讀端
write(fd[1], "hello world\n", 12);
} else {
close(fd[1]); // 關閉寫端
read(fd[0], buff, 20);
printf("%s", buff);
}

return 0;
}

-------------------------------
FIFO

FIFO,也稱為命名管道,它是一種檔案型別。

1、特點
FIFO可以在無關的程序之間交換資料,與無名管道不同。
FIFO有路徑名與之相關聯,它以一種特殊裝置檔案形式存在於檔案系統中。

2、原型
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 返回值:成功返回0,出錯返回-1

其中的 mode 引數與open函式中的 mode 相同。一旦建立了一個 FIFO,就可以用一般的檔案I/O函式操作它。

當 open 一個FIFO時,是否設定非阻塞標誌(O_NONBLOCK)的區別:
若沒有指定O_NONBLOCK(預設),只讀 open 要阻塞到某個其他程序為寫而開啟此 FIFO。類似的,只寫 open 要阻塞到某個其他程序為讀而開啟它。
若指定了O_NONBLOCK,則只讀 open 立即返回。而只寫 open 將出錯返回 -1。如果沒有程序已經為讀而開啟該 FIFO,其errno置ENXIO。


3、例子
FIFO的通訊方式類似於在程序中使用檔案來傳輸資料,只不過FIFO型別檔案同時具有管道的特性。在資料讀出時,FIFO管道中同時清除資料,並且“先進先出”。下面的例子演示了使用 FIFO 進行 IPC 的過程:
write_fifo.c

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<time.h>

int main(){
int fd;
int n, i;
char buf[1024];
time_t tp;

printf("I am %d process.\n", getpid()); // 說明程序ID

if((fd = open("fifo1", O_WRONLY)) < 0) { // 以寫開啟一個FIFO
perror("Open FIFO Failed");
exit(1);
}

for(i=0; i<10; ++i){
time(&tp); // 取系統當前時間
n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));
printf("Send message: %s", buf); // 列印
if(write(fd, buf, n+1) < 0) { // 寫入到FIFO中
perror("Write FIFO Failed");
close(fd);
exit(1);
}
sleep(1);
}

close(fd); // 關閉FIFO檔案
return 0;
}


read_fifo.c

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>

int main(){
int fd;
int len;
char buf[1024];

if (mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) { // 建立FIFO管道
perror("Create FIFO Failed");
}

if ((fd = open("fifo1", O_RDONLY)) < 0) { // 以讀開啟FIFO
perror("Open FIFO Failed");
exit(1);
}

while((len = read(fd, buf, 1024)) > 0) { // 讀取FIFO管道
printf("Read message: %s", buf);
}

close(fd);
return 0;
}


在兩個終端裡用 gcc 分別編譯執行上面兩個檔案,可以看到輸出結果如下:
$ ./write_fifo
I am 5954 process.
Send message: Process 5954's time is Mon Apr 20 12:37:28 2015
Send message: Process 5954's time is Mon Apr 20 12:37:29 2015
Send message: Process 5954's time is Mon Apr 20 12:37:30 2015
Send message: Process 5954's time is Mon Apr 20 12:37:31 2015
Send message: Process 5954's time is Mon Apr 20 12:37:32 2015
Send message: Process 5954's time is Mon Apr 20 12:37:33 2015
Send message: Process 5954's time is Mon Apr 20 12:37:34 2015
Send message: Process 5954's time is Mon Apr 20 12:37:35 2015
Send message: Process 5954's time is Mon Apr 20 12:37:36 2015
Send message: Process 5954's time is Mon Apr 20 12:37:37 2015


上述例子可以擴充套件成 客戶程序—伺服器程序 通訊的例項,write_fifo的作用類似於客戶端,可以開啟多個客戶端向一個伺服器傳送請求資訊,read_fifo類似於伺服器,它適時監控著FIFO的讀端,當有資料時,讀出並進行處理,但是有一個關鍵的問題是,每一個客戶端必須預先知道伺服器提供的FIFO介面,下圖顯示了這種安排:



-------------------------------
訊息佇列

訊息佇列,是訊息的連結表,存放在核心中。一個訊息佇列由一個識別符號(即佇列ID)來標識。

1、特點
訊息佇列是面向記錄的,其中的訊息具有特定的格式以及特定的優先順序。
訊息佇列獨立於傳送與接收程序。程序終止時,訊息佇列及其內容並不會被刪除。
訊息佇列可以實現訊息的隨機查詢,訊息不一定要以先進先出的次序讀取,也可以按訊息的型別讀取。

2、原型
#include <sys/msg.h>
// 建立或開啟訊息佇列:成功返回佇列ID,失敗返回-1
int msgget(key_t key, int flag);
// 新增訊息:成功返回0,失敗返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 讀取訊息:成功返回訊息資料的長度,失敗返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制訊息佇列:成功返回0,失敗返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

在以下兩種情況下,msgget將建立一個新的訊息佇列:
如果沒有與鍵值key相對應的訊息佇列,並且flag中包含了IPC_CREAT標誌位。
key引數為IPC_PRIVATE。

函式msgrcv在讀取訊息佇列時,type引數有下面幾種情況:
type == 0,返回佇列中的第一個訊息;
type > 0,返回佇列中訊息型別為 type 的第一個訊息;
type < 0,返回佇列中訊息型別值小於或等於 type 絕對值的訊息,如果有多個,則取型別值最小的訊息。

可以看出,type值非 0 時用於以非先進先出次序讀訊息。也可以把 type 看做優先順序的權值。(其他的引數解釋,請自行Google之)

3、例子
下面寫了一個簡單的使用訊息佇列進行IPC的例子,服務端程式一直在等待特定型別的訊息,當收到該型別的訊息以後,傳送另一種特定型別的訊息作為反饋,客戶端讀取該反饋並打印出來。

msg_server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>

// 用於建立一個唯一的key
#define MSG_FILE "/etc/passwd"

// 訊息結構
struct msg_form {
long mtype;
char mtext[256];
};

int main(){
int msqid;
key_t key;
struct msg_form msg;

// 獲取key值
if((key = ftok(MSG_FILE,'z')) < 0){
perror("ftok error");
exit(1);
}

// 列印key值
printf("Message Queue - Server key is: %d.\n", key);

// 建立訊息佇列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1){
perror("msgget error");
exit(1);
}

// 列印訊息佇列ID及程序ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());

// 迴圈讀取訊息
for(;;) {
msgrcv(msqid, &msg, 256, 888, 0);// 返回型別為888的第一個訊息
printf("Server: receive msg.mtext is: %s.\n", msg.mtext);
printf("Server: receive msg.mtype is: %d.\n", msg.mtype);

msg.mtype = 999; // 客戶端接收的訊息型別
sprintf(msg.mtext, "hello, I'm server %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
}
return 0;
}


msg_client.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>

// 用於建立一個唯一的key
#define MSG_FILE "/etc/passwd"

// 訊息結構
struct msg_form {
long mtype;
char mtext[256];
};

int main(){
int msqid;
key_t key;
struct msg_form msg;

// 獲取key值
if ((key = ftok(MSG_FILE, 'z')) < 0) {
perror("ftok error");
exit(1);
}

// 列印key值
printf("Message Queue - Client key is: %d.\n", key);

// 開啟訊息佇列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1) {
perror("msgget error");
exit(1);
}

// 列印訊息佇列ID及程序ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());

// 新增訊息,型別為888
msg.mtype = 888;
sprintf(msg.mtext, "hello, I'm client %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);

// 讀取型別為777的訊息
msgrcv(msqid, &msg, 256, 999, 0);
printf("Client: receive msg.mtext is: %s.\n", msg.mtext);
printf("Client: receive msg.mtype is: %d.\n", msg.mtype);
return 0;
}

-------------------------------
訊號量

訊號量(semaphore)與已經介紹過的 IPC 結構不同,它是一個計數器。訊號量用於實現程序間的互斥與同步,而不是用於儲存程序間通訊資料。

1、特點
訊號量用於程序間同步,若要在程序間傳遞資料需要結合共享記憶體。
訊號量基於作業系統的 PV操作,程式對訊號量的操作都是原子操作。
每次對訊號量的 PV 操作不僅限於對訊號量值加 1 或減 1,而且可以加減任意正整數。
支援訊號量組。

2、原型
最簡單的訊號量是隻能取 0 和 1 的變數,這也是訊號量最常見的一種形式,叫做二值訊號量(Binary Semaphore)。而可以取多個正整數的訊號量被稱為通用訊號量。Linux 下的訊號量函式都是在通用的訊號量陣列上進行操作,而不是在一個單一的二值訊號量上進行操作。

#include <sys/sem.h>
// 建立或獲取一個訊號量組:若成功返回訊號量集ID,失敗返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 對訊號量組進行操作,改變訊號量的值:成功返回0,失敗返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制訊號量的相關資訊
int semctl(int semid, int sem_num, int cmd, ...);

當semget建立新的訊號量集合時,必須指定集合中訊號量的個數(即num_sems),通常為1; 如果是引用一個現有的集合,則將num_sems指定為 0 。

在semop函式中,sembuf結構的定義如下:
struct sembuf {
short sem_num; // 訊號量組中對應的序號,0~sem_nums-1
short sem_op; // 訊號量值在一次操作中的改變數
short sem_flg; // IPC_NOWAIT, SEM_UNDO
}

其中 sem_op 是一次操作中的訊號量的改變數:
若sem_op > 0,表示程序釋放相應的資源數,將 sem_op的值加到訊號量的值上。如果有程序正在休眠等待此訊號量,則喚醒它們。
若sem_op < 0,請求 sem_op的絕對值的資源。
如果相應的資源數可以滿足請求,則將該訊號量的值減去sem_op的絕對值,函式成功返回。
當相應的資源數不能滿足請求時,這個操作與sem_flg有關。
sem_flg指定IPC_NOWAIT,則semop函數出錯返回EAGAIN.
sem_flg沒有指定IPC_NOWAIT,則將該訊號量的semncnt值加1,然後程序掛起直到下述情況發生:
當相應的資源數可以滿足請求,此訊號量的semncnt值減1,該訊號量的值減去sem_op的絕對值。成功返回;
此訊號量被刪除,函式smeop出錯返回EIDRM;
程序捕捉到訊號,並從訊號處理函式返回,此情況下將此訊號量的semncnt值減1,函式semop出錯返回EINTR。
若sem_op== 0,程序阻塞直到訊號量的相應值為0:
當訊號量已經為0,函式立即返回。
如果訊號量的值不為0,則依據sem_flg決定函式動作:
sem_flg指定IPC_NOWAIT,則出錯返回EAGAIN。
sem_flg沒有指定IPC_NOWAIT,則將該訊號量的semncnt值加1,然後程序掛起直到下述情況發生:
訊號量值為0,將訊號量的semzcnt的值減1,函式semop成功返回;
此訊號量被刪除,函式smeop出錯返回EIDRM;
程序捕捉到訊號,並從訊號處理函式返回,在此情況將此訊號量的semncnt值減1,函式semop出錯返回EINTR。

在semctl函式中的命令有多種,這裡就說兩個常用的:
SETVAL:用於初始化訊號量為一個已知的值。所需要的值作為聯合semun的val成員來傳遞。在訊號量第一次使用之前需要設定訊號量。
IPC_RMID:刪除一個訊號量集合。如果不刪除訊號量,它將繼續在系統中存在,即使程式已經退出,它可能在你下次執行此程式時引發問題,而且訊號量是一種有限的資源。

3、例子
#include<stdio.h>
#include<stdlib.h>
#include<sys/sem.h>

// 聯合體,用於semctl初始化
union semun{
int val; /*for SETVAL*/
struct semid_ds *buf;
unsigned short *array;
};

// 初始化訊號量
int init_sem(int sem_id, int value){
union semun tmp;
tmp.val = value;
if(semctl(sem_id, 0, SETVAL, tmp) == -1){
perror("Init Semaphore Error");
return -1;
}
return 0;
}

// P操作:
// 若訊號量值為1,獲取資源並將訊號量值-1
// 若訊號量值為0,程序掛起等待
int sem_p(int sem_id){
struct sembuf sbuf;
sbuf.sem_num = 0; /*序號*/
sbuf.sem_op = -1; /*P操作*/
sbuf.sem_flg = SEM_UNDO;

if(semop(sem_id, &sbuf, 1) == -1){
perror("P operation Error");
return -1;
}
return 0;
}

//V操作:
//釋放資源並將訊號量值+1
//如果有程序正在掛起等待,則喚醒它們
int sem_v(int sem_id){
struct sembuf sbuf;
sbuf.sem_num = 0; /*序號*/
sbuf.sem_op = 1; /*V操作*/
sbuf.sem_flg = SEM_UNDO;

if(semop(sem_id, &sbuf, 1) == -1){
perror("V operation Error");
return -1;
}
return 0;
}

//刪除訊號量集
int del_sem(int sem_id){
union semun tmp;
if(semctl(sem_id, 0, IPC_RMID, tmp) == -1){
perror("Delete Semaphore Error");
return -1;
}
return 0;
}


int main(){
int sem_id; // 訊號量集ID
key_t key;
pid_t pid;

// 獲取key值
if((key = ftok(".", 'z')) < 0){
perror("ftok error");
exit(1);
}

// 建立訊號量集,其中只有一個訊號量
if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1){
perror("semget error");
exit(1);
}

// 初始化:初值設為0資源被佔用
init_sem(sem_id, 0);

if((pid = fork()) == -1)
perror("Fork Error");
else if(pid == 0){ /*子程序*/
sleep(2);
printf("Process child: pid=%d\n", getpid());
sem_v(sem_id); /*釋放資源*/
}
else{ /*父程序*/
sem_p(sem_id); /*等待資源*/
printf("Process father: pid=%d\n", getpid());
sem_v(sem_id); /*釋放資源*/
del_sem(sem_id); /*刪除訊號量集*/
}
return 0;
}

上面的例子如果不加訊號量,則父程序會先執行完畢。這裡加了訊號量讓父程序等待子程序執行完以後再執行。

-------------------------------
共享記憶體

共享記憶體(Shared Memory),指兩個或多個程序共享一個給定的儲存區。

1、特點
共享記憶體是最快的一種IPC,因為程序是直接對記憶體進行存取。
因為多個程序可以同時操作,所以需要進行同步。
訊號量+共享記憶體通常結合在一起使用,訊號量用來同步對共享記憶體的訪問。

2、原型
#include <sys/shm.h>
// 建立或獲取一個共享記憶體:成功返回共享記憶體ID,失敗返回-1
int shmget(key_t key, size_t size, int flag);
// 連線共享記憶體到當前程序的地址空間:成功返回指向共享記憶體的指標,失敗返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 斷開與共享記憶體的連線:成功返回0,失敗返回-1
int shmdt(void *addr);
// 控制共享記憶體的相關資訊:成功返回0,失敗返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

當用shmget函式建立一段共享記憶體時,必須指定其 size;而如果引用一個已存在的共享記憶體,則將 size 指定為0 。

當一段共享記憶體被建立以後,它並不能被任何程序訪問。必須使用shmat函式連線該共享記憶體到當前程序的地址空間,連線成功後把共享記憶體區物件對映到呼叫程序的地址空間,隨後可像本地空間一樣訪問。

shmdt函式是用來斷開shmat建立的連線的。注意,這並不是從系統中刪除該共享記憶體,只是當前程序不能再訪問該共享記憶體而已。

shmctl函式可以對共享記憶體執行多種操作,根據引數 cmd 執行相應的操作。常用的是IPC_RMID(從系統中刪除該共享記憶體)。

3、例子
下面這個例子,使用了【共享記憶體+訊號量+訊息佇列】的組合來實現伺服器程序與客戶程序間的通訊。
共享記憶體用來傳遞資料;
訊號量用來同步;
訊息佇列用來 在客戶端修改了共享記憶體後 通知伺服器讀取。

server.c

#include<stdio.h>
#include<stdlib.h>
#include<sys/shm.h> // shared memory
#include<sys/sem.h> // semaphore
#include<sys/msg.h> // message queue
#include<string.h> // memcpy

// 訊息佇列結構
struct msg_form {
long mtype;
char mtext;
};

// 聯合體,用於semctl初始化
union semun{
int val; /*for SETVAL*/
struct semid_ds *buf;
unsigned short *array;
};

// 初始化訊號量
int init_sem(int sem_id, int value){
union semun tmp;
tmp.val = value;
if(semctl(sem_id, 0, SETVAL, tmp) == -1){
perror("Init Semaphore Error");
return -1;
}
return 0;
}

// P操作:
// 若訊號量值為1,獲取資源並將訊號量值-1
// 若訊號量值為0,程序掛起等待
int sem_p(int sem_id){
struct sembuf sbuf;
sbuf.sem_num = 0; /*序號*/
sbuf.sem_op = -1; /*P操作*/
sbuf.sem_flg = SEM_UNDO;

if(semop(sem_id, &sbuf, 1) == -1){
perror("P operation Error");
return -1;
}
return 0;
}

// V操作:
// 釋放資源並將訊號量值+1
// 如果有程序正在掛起等待,則喚醒它們
int sem_v(int sem_id){
struct sembuf sbuf;
sbuf.sem_num = 0; /*序號*/
sbuf.sem_op = 1; /*V操作*/
sbuf.sem_flg = SEM_UNDO;

if(semop(sem_id, &sbuf, 1) == -1){
perror("V operation Error");
return -1;
}
return 0;
}

// 刪除訊號量集
int del_sem(int sem_id){
union semun tmp;
if(semctl(sem_id, 0, IPC_RMID, tmp) == -1){
perror("Delete Semaphore Error");
return -1;
}
return 0;
}

// 建立一個訊號量集
int creat_sem(key_t key){
int sem_id;
if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1){
perror("semget error");
exit(-1);
}
init_sem(sem_id, 1); /*初值設為1資源未佔用*/
return sem_id;
}


int main(){
key_t key;
int shmid, semid, msqid;
char *shm;
char data[] = "this is server";
struct shmid_ds buf1; /*用於刪除共享記憶體*/
struct msqid_ds buf2; /*用於刪除訊息佇列*/
struct msg_form msg; /*訊息佇列用於通知對方更新了共享記憶體*/

// 獲取key值
if((key = ftok(".", 'z')) < 0){
perror("ftok error");
exit(1);
}

// 建立共享記憶體
if((shmid = shmget(key, 1024, IPC_CREAT|0666)) == -1){
perror("Create Shared Memory Error");
exit(1);
}

// 連線共享記憶體
shm = (char*)shmat(shmid, 0, 0);
if((int)shm == -1){
perror("Attach Shared Memory Error");
exit(1);
}


// 建立訊息佇列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1){
perror("msgget error");
exit(1);
}

// 建立訊號量
semid = creat_sem(key);

// 讀資料
while(1){
msgrcv(msqid, &msg, 1, 888, 0); /*讀取型別為888的訊息*/
if(msg.mtext == 'q') /*quit - 跳出迴圈*/
break;
if(msg.mtext == 'r'){ /*read - 讀共享記憶體*/
sem_p(semid);
printf("%s\n",shm);
sem_v(semid);
}
}

// 斷開連線
shmdt(shm);

/*刪除共享記憶體、訊息佇列、訊號量*/
shmctl(shmid, IPC_RMID, &buf1);
msgctl(msqid, IPC_RMID, &buf2);
del_sem(semid);
return 0;
}


client.c

#include<stdio.h>
#include<stdlib.h>
#include<sys/shm.h> // shared memory
#include<sys/sem.h> // semaphore
#include<sys/msg.h> // message queue
#include<string.h> // memcpy

// 訊息佇列結構
struct msg_form{
long mtype;
char mtext;
};

// 聯合體,用於semctl初始化
union semun{
int val; /*for SETVAL*/
struct semid_ds *buf;
unsigned short *array;
};

// P操作:
// 若訊號量值為1,獲取資源並將訊號量值-1
// 若訊號量值為0,程序掛起等待
int sem_p(int sem_id){
struct sembuf sbuf;
sbuf.sem_num = 0; /*序號*/
sbuf.sem_op = -1; /*P操作*/
sbuf.sem_flg = SEM_UNDO;

if(semop(sem_id, &sbuf, 1) == -1){
perror("P operation Error");
return -1;
}
return 0;
}

// V操作:
// 釋放資源並將訊號量值+1
// 如果有程序正在掛起等待,則喚醒它們
int sem_v(int sem_id){
struct sembuf sbuf;
sbuf.sem_num = 0; /*序號*/
sbuf.sem_op = 1; /*V操作*/
sbuf.sem_flg = SEM_UNDO;

if(semop(sem_id, &sbuf, 1) == -1){
perror("V operation Error");
return -1;
}
return 0;
}


int main(){
key_t key;
int shmid, semid, msqid;
char *shm;
struct msg_form msg;
int flag = 1; /*while迴圈條件*/

// 獲取key值
if((key = ftok(".", 'z')) < 0){
perror("ftok error");
exit(1);
}

// 獲取共享記憶體
if((shmid = shmget(key, 1024, 0)) == -1){
perror("shmget error");
exit(1);
}

// 連線共享記憶體
shm = (char*)shmat(shmid, 0, 0);
if((int)shm == -1){
perror("Attach Shared Memory Error");
exit(1);
}

// 建立訊息佇列
if ((msqid = msgget(key, 0)) == -1){
perror("msgget error");
exit(1);
}

// 獲取訊號量
if((semid = semget(key, 0, 0)) == -1){
perror("semget error");
exit(1);
}

// 寫資料
printf("***************************************\n");
printf("* IPC *\n");
printf("* Input r to send data to server. *\n");
printf("* Input q to quit. *\n");
printf("***************************************\n");

while(flag){
char c;
printf("Please input command: ");
scanf("%c", &c);
switch(c){
case 'r':
printf("Data to send: ");
sem_p(semid); /*訪問資源*/
scanf("%s", shm);
sem_v(semid); /*釋放資源*/
/*清空標準輸入緩衝區*/
while((c=getchar())!='\n' && c!=EOF);
msg.mtype = 888;
msg.mtext = 'r'; /*傳送訊息通知伺服器讀資料*/
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
break;
case 'q':
msg.mtype = 888;
msg.mtext = 'q';
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
flag = 0;
break;
default:
printf("Wrong input!\n");
/*清空標準輸入緩衝區*/
while((c=getchar())!='\n' && c!=EOF);
}
}

// 斷開連線
shmdt(shm);

return 0;
}

注意:當scanf()輸入字元或字串時,緩衝區中遺留下了\n,所以每次輸入操作後都需要清空標準輸入的緩衝區。但是由於 gcc 編譯器不支援fflush(stdin)(它只是標準C的擴充套件),所以我們使用了替代方案:
while((c=getchar())!='\n' && c!=EOF);

四、總結

接⼝
建立IPC(msgqueue, shm, sem), ipcget
刪除IPC(msgqueue, shm, sem), ipcctl + IPC_RMID
每種IPC都有⾃⼰個性化的操作接⼝

命令
ipcs : 顯示IPC資源
ipcs -m 檢視共享記憶體
ipcs -s 檢視訊號
ipcs -q 檢視訊息佇列
ipcrm : 手動刪除IPC資源
ipcrm -m 檢視共享記憶體
ipcrm -s 檢視訊號
ipcrm -q 檢視訊息佇列


五、參考資料

程序間通訊IPC (InterProcess Communication)

Unix域套接字(Unix Domain Socket)


Linux程序間通訊之管道與訊息佇列實踐

linux下的多程序通訊(IPC)原理及實現方案(管道、佇列、訊號量、共享記憶體)


linux程序間通訊(IPC)的五種方式(管道、FIFO、共享記憶體、訊號量、訊息佇列)

程式設計是個人愛好