一、進程間通信簡述
每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核,在內核中開辟一塊緩沖區,進程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、共享內存、信號量、消息隊列)