1. 程式人生 > >Linux下的程序概論與程式設計三(程序間通訊的5種方式)

Linux下的程序概論與程式設計三(程序間通訊的5種方式)

一、程序間通訊

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

2、程序間通訊的本質
每個程序各自有不同的使用者地址空間,任何一個程序的全域性變數在另一個程序中都看不到。讓不同程序能夠看到一份公共資源,此公共資源就可以實現二者之間的通訊,公共資源不屬於任何程序,它由作業系統核心提供,屬於作業系統。

二、管道(pipe)

1、管道,也叫匿名管道,是一種最基本的IPC機制,由pipe函式(系統呼叫)建立:

#include <unistd.h>
int pipe(int filedes[2]);

返回值:pipe函式呼叫成功返回0,呼叫失敗返回-1。
引數:filedes輸出型引數,在pipe內部對filedes進行修改,會改變呼叫它的函式

呼叫pipe函式時在核心中開闢一塊緩衝區(稱為管道)用於通訊,它有一個讀端一個寫端,然後通過filedes引數傳出給使用者程式兩個檔案描述符, filedes[0]指向管道的讀端,filedes[1]指向管道的寫端(很好記,就像0是標準輸入1是標準輸出一樣)。所以管道在使用者程式看起來就像一個開啟的檔案,通過read(filedes[0]);或者write(filedes[1]);向這個檔案讀寫資料其實是在讀寫核心緩衝區

2、開闢管道之後的程序通訊步驟
這裡寫圖片描述

  1. 父程序呼叫pipe開闢管道,得到兩個檔案描述符指向管道的兩端。
  2. 父程序呼叫fork建立子程序,那麼子程序也有兩個檔案描述符指向同一管道。
  3. 父程序關閉管道讀端,子程序關閉管道寫端。父程序可以往管道里寫,子程序可以從管道里讀,管道是用環形佇列實現的,資料從寫端流入從讀端流出,這樣就實現了程序間通訊。

3、管道通訊的程式碼示例
父程序關閉寫端,從管道中讀資料,子程序關閉讀端,向管道中寫資料。

  1 /**************************************
  2 *檔案說明:pipe.c
  3 *作者:段曉雪
  4 *建立時間:2017年05月23日 星期二 17時00分59秒
  5 *開發環境:Kali Linux/g++ v6.3.0
  6 ****************************************/
7 #include<stdio.h> 8 #include<unistd.h> 9 #include<string.h> 10 #include<sys/wait.h> 11 int main() 12 { 13 int _fd[2]; 14 int ret = pipe(_fd);//建立管道 15 if(0 == ret) 16 { 17 printf("create pipe success.\n"); 18 } 19 else 20 { 21 printf("create pipe failure.\n"); 22 return 0; 23 } 24 25 pid_t pid = fork();//建立子程序 26 if(pid == -1) 27 { 28 perror("fork"); 29 return 1; 30 } 31 else if(pid == 0)//child 32 { 33 close(_fd[0]);//子程序關閉管道的讀端 34 while(1) 35 { 36 char *str = "i am a child,i am writing."; 37 ssize_t s = write(_fd[1],str,strlen(str)); 38 if(s == -1) 39 { 40 perror("write"); 41 return 2; 42 } 43 sleep(1); 44 } 45 } 46 else//father 47 { 48 close(_fd[1]);//父程序關閉管道的寫端 49 while(1) 50 { 51 char buff[1024]; 52 printf("i am a father,i am reading.\n"); 53 ssize_t s = read(_fd[0],buff,sizeof(buff)-1); 54 if(s > 0) 55 { 56 buff[s] = 0; 57 printf("read success:%s\n",buff); 58 } 59 else if(s == -1) 60 { 61 printf("read failure:"); 62 perror("read"); 63 return 3; 64 } 65 } 66 wait(NULL); 67 } 68 return 0; 69 }

執行結果:
這裡寫圖片描述

4、匿名管道通訊的特點
1>兩個程序通過一個管道只能進行單向通訊
2>只能用於具有血緣關係之間的程序通訊(缺點),通常用於父子程序
3>管道的生命週期隨程序,隨著程序終止而結束
4>管道之間進行通訊向上層提供的讀寫操作面向位元組流的通訊服務(流:與傳送資料時傳送資料 的格式無關TCP UDP)
5>管道自帶同步機制

5、一些相關概念
臨界資源:兩個程序同時訪問一個資源
臨界區:訪問臨界資源的程式碼
同步:讓不同程序訪問臨界資源以不同的順序進行訪問稱為程序的同步(以互斥基本前提)
二義性:多程序同時訪問臨界資源可能會產生二義性,即結果的不確定性
互斥:任何時刻有且只能有一個程序在訪問臨界資源
原子性:一件事情要麼做了要麼沒做,沒有中間狀態
飢餓:一個程序想要訪問某種資源,但是由於優先順序或其他原因一直無法訪問該資源,此狀態稱為程序的飢餓狀態
常駐程序:一直在記憶體中執行的記憶體,最怕記憶體洩漏

6、與管道相關的4種狀態:
1>寫端一直在寫資料,讀端一直不讀但又不關閉自己的檔案描述符,直到管道寫滿,寫端進行等待
這裡寫圖片描述

執行結果:
這裡寫圖片描述

2>寫端不光不寫,還不關閉自己的檔案描述符,此時讀端進行等待
這裡寫圖片描述

執行結果:
這裡寫圖片描述
第一點和第二點驗證了管道自帶同步機制

3>讀端不光不讀,還關閉自己的檔案描述符,作業系統會終止寫端的程序(異常終止)–〉讀端進入殭屍狀態
這裡寫圖片描述

執行結果:
這裡寫圖片描述

4>寫端不光不寫 ,還關閉自己的檔案描述符,讀完管道中的資料之後就返回0值,就像讀到檔案末尾一樣。
這裡寫圖片描述

執行結果:
這裡寫圖片描述

三、命名管道(FIFO)

1、概念
管道的一個不足之處是沒有名字,因此,只能用於具有親緣關係的程序間通訊,在命名管道(named pipe或FIFO)提出後,該限制得到了克服。FIFO不同於管道之處在於它提供一個路徑名與之關聯,以FIFO的檔案形式儲存於檔案系統中。命名管道是一個裝置檔案,因此,即使程序與建立FIFO的程序不存在親緣關係,只要可以訪問該路徑,就能夠通過FIFO相互通訊。值得注意的是,FIFO(first input first output)總是按照先進先出的原則工作,第一個被寫入的資料將首先從管道中讀出。

2、建立命名管道
Linux下有兩種方式建立命名管道。一是在Shell下互動地建立一個命名管道,二是在程式中使用系統函式建立命名管道。
1>Shell方式下可使用mknod或mkfifo命令建立命名管道
如:

mknod namedpipe

2>建立命名管道的系統函式:mknod和mkfifo

#include <sys/types.h>
#include <sys/stat.h>
int mknod(const char *path,mode_t mod,dev_t dev);
int mkfifo(const char *path,mode_t mode);

引數path為建立的命名管道的全路徑名;
mod為建立的命名管道的模式,指明其存取許可權;
dev為裝置值,該值取決於檔案建立的種類,它只在建立裝置檔案時才會用到。
這兩個函式呼叫成功都返回0,失敗都返回-1。

mknod是比較老的函式,而使用mkfifo函式更加簡單和規範,所以建議在可能的情況下,儘量使用mkfifo而不是mknod。

3>命名管道建立後就可以使用了,命名管道和管道的使用方法基本是相同的。只是使用命名管道時,必須先呼叫open()將其開啟。因為命名管道是一個存在於硬碟上的檔案,而管道是存在於記憶體中的特殊檔案

需要注意的是,呼叫open()開啟命名管道的程序可能會被阻塞。
但如果同時用讀寫方式(O_RDWR)開啟,則一定不會導致阻塞;
如果以只讀方式(O_RDONLY)開啟,則呼叫open()函式的程序將會被阻塞直到有寫方開啟管道;
同樣以寫方式(O_WRONLY)開啟也會阻塞直到有讀方式開啟管道。

3、命名管道的程式碼舉例
開啟兩個終端,分別模擬客戶端和伺服器進行管道通訊,客戶端通過管道不斷向伺服器傳送資料,伺服器從管道接收由客戶端發來的資料。

client.c:

  1 /**************************************
  2 *檔案說明:client.c
  3 *作者:段曉雪
  4 *建立時間:2017年05月23日 星期二 20時35分11秒
  5 *開發環境:Kali Linux/g++ v6.3.0
  6 ****************************************/
  7 #include<stdio.h>
  8 #include<sys/types.h>
  9 #include<sys/stat.h>
 10 #include<fcntl.h>
 11 #include<unistd.h>
 12 #include<string.h>
 13 
 14 int main()
 15 {
 16     umask(0);//設定預設的檔案許可權為0
 17     int p = mkfifo("./fifo",S_IFIFO | 0666);//建立命名管道,成功返回0,失敗返回-1\
 18                                           “S_IFIFO|0666”指明建立⼀一個命名管道且存取許可權為0    666
 19     if(p == -1)//creat named pipe failed
 20     {
 21         perror("mkfifo error");
 22         return 1;
 23     }
 24     else//creat named pipe success
 25 
 26     {
 27         int fd = open("./fifo",O_RDWR);//以讀寫方式開啟,一定不會阻塞
 28         if(-1 == fd)//open failed
 29         {
 30             perror("open error");
 31             return 2;
 32         }
 33         else//open success
 34         {
 35             char buff[1024];
 36             while(1)
 37             {
 38                 buff[0] = '\0';
 39                 printf("client write#");
 40                 fflush(stdout);
 41                 //scanf("%s",buff);//輸入不能有空格
 42                 ssize_t ss = read(0,buff,sizeof(buff)-1);//從標準輸入讀資料
 43                 if(ss > 0)
 44                     buff[ss-1] = 0;
 45                 ssize_t s = write(fd,buff,strlen(buff));//write data
 46                 if(s > 0)//write success
 47                 {
 48                     ;
 49                 }
 50                 else//write failed
 51                 {
 52                     perror("write error");
 53                     return 3;
 54                 }
 55                 if(strncmp(buff,"quit",4) == 0)
 56                     break;
 57             }   
 58         }       
 59         close(fd);
 60     }               
 61     return 0;   
 62 }           

server.c:

  1 /**************************************
  2 *檔案說明:server.c
  3 *作者:段曉雪
  4 *建立時間:2017年05月23日 星期二 21時00分21秒
  5 *開發環境:Kali Linux/g++ v6.3.0
  6 ****************************************/
  7 #include<stdio.h>
  8 #include<sys/types.h>
  9 #include<sys/stat.h>
 10 #include<fcntl.h>
 11 #include<unistd.h>
 12 #include<string.h>
 13 
 14 int main()
 15 {
 16     int fd = open("./fifo",O_RDONLY);
 17     if(fd == -1)//open failed
 18     {
 19         perror("open file");
 20         return 1;
 21     }
 22     else//open success
 23     {
 24         char buff[1024];
 25         while(1)
 26         {
 27             memset(buff,'\0',sizeof(buff));//清空緩衝區
 28             ssize_t rd = read(fd,buff,sizeof(buff)-1);
 29             if(-1 == rd)//read failed
 30             {
 31                 perror("read");
 32                 break;
 33             }
 34             else//read success
 35             {
 36                 printf("server read:%s\n",buff);
 37             }
 38             if(strncmp(buff,"quit",4) == 0)//結束讀 
 39                 break;
 40         }
 41     }
 42     close(fd);
 43     return 0;
 44 }

3、執行過程:
1>因為我已經執行過一次了,當我再次執行時,會出現如下結果:
這裡寫圖片描述

檢視系統中的管道或ls -al檢視當前檔案:
這裡寫圖片描述
ps aux | grep -E ‘fifo’:產看系統中名為fifo的命名管道檔案
從上圖中可以看出管道檔案已經存在,要想再次執行程式必須將其刪除方可。
刪除方法:
1> 用rm命令刪除fifo檔案
2>在makefile檔案clean時刪除fifo檔案
這裡寫圖片描述

2>再次執行程式:
client:
這裡寫圖片描述

server:
這裡寫圖片描述

四、訊息佇列

XSI IPC(訊息佇列,訊號量,共享記憶體)
1、什麼是訊息佇列
訊息佇列提供了一種從一個程序向另一個程序傳送一個數據塊的方法。 每個資料塊都被認為是有一個型別,接收者程序接收的資料塊可以有不同的型別值。我們可以通過傳送訊息來避免命名管道的同步和阻塞問題。
訊息佇列與管道不同的是,訊息佇列是基於訊息的,而管道是基於位元組流的,且訊息佇列的讀取不一定是先入先出。訊息佇列與命名管道有一樣的不足,就是每個訊息的最大長度是有上限的(MSGMAX),每個訊息佇列的總的位元組數是有上限的(MSGMNB),系統上訊息佇列的總數也有一個上限(MSGMNI)。
這裡寫圖片描述

每個訊息的最大長度MSGMAX:—->8192
每個訊息佇列總的位元組數MSGMNB:—->16384
系統訊息佇列的總數MSGMNI:—->32000

2、IPC物件的資料結構
核心為每個IPC物件維護一個數據結構(/usr/include/linux/ipc.h)

struct ipc_perm{
key_t __key; /* Key supplied to xxxget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};

key:每個訊息佇列都有唯一的標識
cuid:建立者ID
cgid:建立者組ID
mode:讀寫許可權

訊息佇列,共享記憶體和訊號量都有這樣一個共同的資料結構。

3、訊息佇列的資料結構(/usr/include/linux/msg.h)
這裡寫圖片描述

可以看到第一個條目就是IPC結構體,即是共有的,後面的都是訊息佇列所私有的成員。訊息佇列是用連結串列實現的。

4、訊息佇列的相關函式
1>建立訊息佇列或取得已存在的訊息佇列
原型

int msgget(key_t key, int msgflg);

引數
key:可以認為是一個埠號,也可以由函式ftok生成。
msgflg:
IPC_CREAT 如果IPC不存在,則建立一個IPC資源,否則開啟操作。
IPC_EXCL:只有在共享記憶體不存在的時候,新的共享記憶體才建立,否 則就產生錯誤。

如果單獨使用IPC_CREAT,XXXget()函式要麼返回一個已經存在的共享記憶體的操作符,要麼返回一個新建的共享記憶體的識別符號。
如果將IPC_CREAT和IPC_EXCL標誌一起使用,XXXget()將返回一個新建的IPC識別符號;如果該IPC資源已存在,或者返回-1。
IPC_EXEL標誌本身並沒有太大的意義,但是和IPC_CREAT標誌一起使用可以用來保證所得的物件是新建的,而不是開啟已有的物件。

2>向訊息佇列讀/寫訊息
原型
msgrcv從佇列中取用訊息:

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgsnd將資料放到訊息佇列中:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

引數
msqid:訊息佇列的標識碼
msgp:指向訊息緩衝區的指標,此位置用來暫時儲存傳送和接收的訊息,是一個使用者可定義的通用結構,形態如下:

struct msgstru{
long mtype; //大於0,訊息型別
char mtext[使用者指定大小];
};

msgsz:訊息的大小。
msgtyp:從訊息佇列內讀取的訊息形態。如果值為零,則表示訊息佇列中的所有訊息都會被讀取。
msgflg:用來指明核心程式在佇列沒有資料的情況下所應採取的行動。如果msgflg和常數IPC_NOWAIT合用,則在msgsnd()執行時若是訊息佇列已滿,則msgsnd()將不會阻塞,而會立即返回-1,如果執行的是msgrcv(),則在訊息佇列呈空時,不做等待馬上返回-1,並設定錯誤碼為ENOMSG。當msgflg為0時,msgsnd()及msgrcv()在佇列呈滿或呈空的情形時,採取阻塞等待的處理模式。

3>設定訊息佇列的屬性
原型

int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );

引數:msgctl 系統呼叫對msgqid標識的訊息佇列執行cmd操作,系統定義了3種cmd操作 :
IPC_STAT : 該命令用來獲取訊息佇列對應的 msqid_ds 資料結構,並將其儲存到 buf 指 定的地址空間。
IPC_SET : 該命令用來設定訊息佇列的屬性,要設定的屬性儲存在buf中。
IPC_RMID : 從核心中刪除 msqid 標識的訊息佇列。

4>key_t鍵
System V IPC使用key_t值作為它們的名字,在Redhat linux(後續驗證預設都在該平臺下)下key_t被定義為int型別,追溯如下:

/usr/include/sys/ipc.h
#ifndef __key_t_defined
typedef __key_t key_t;
#define __key_t_defined
#endif
/usr/include/bits/types.h
typedef __DADDR_T_TYPE __daddr_t; /* The type of a disk address. */
typedef __SWBLK_T_TYPE __swblk_t; /* Type of a swap block maybe? */
typedef __KEY_T_TYPE __key_t; /* Type of an IPC key */
/usr/include/bits/typesizes.h
#define __KEY_T_TYPE __S32_TYPE
/usr/include/bits/types.h
#define __S32_TYPE int

ftok函式:
函式ftok把一個已存在的路徑名和一個整數標識得轉換成一個key_t值,稱為IPC鍵:

# include <sys/types.h>
# include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

該函式把從pathname匯出的資訊與id的低序8位組合成一個整數IPC鍵。

5、訊息佇列例項
同上,開啟兩個人終端,分別模擬client和server,剛開始由伺服器先給客戶端傳送資料,客戶端接收來自伺服器傳送的資料,緊接著由客戶端發資料,伺服器接收資料,後續客戶端和伺服器交替的進行收發資料。

同命名管道一樣,在每次執行之前必須將系統已有的訊息佇列刪除。
ipcs -q:檢視系統中的訊息佇列(ipcs:檢視系統的資源 -q:檢視訊息佇列)
ipcrm -q 0:刪除0號訊息佇列
訊息佇列的生命週期隨核心,直到刪除或系統關機

common.h:

  1 /**************************************
  2 *檔案說明:common.h
  3 *作者:段曉雪
  4 *建立時間:2017年05月24日 星期三 15時39分36秒
  5 *開發環境:Kali Linux/g++ v6.3.0
  6 ****************************************/
  7 #ifndef  __COMMON__
  8 #define __COMMON__
  9 
 10 #include<stdio.h>
 11 #include<unistd.h>
 12 #include<stdlib.h>
 13 #include<sys/ipc.h>
 14 #include<sys/msg.h>
 15 #include<sys/types.h>
 16 #include<string.h>
 17 #include<time.h>
 18 
 19 #define MSG_SIZE 1024
 20 #define FILEPATH "."
 21 #define ID 0
 22 #define SERVER_TYPE 1
 23 #define CLIENT_TYPE 2
 24 
 25 typedef struct msg_info
 26 {
 27     long mtype;
 28     char mtext[MSG_SIZE];
 29 }msginfo;
 30 
 31 int CreatMessageQueue();//建立訊息佇列
 32 int GetMessageQueue();//獲取已經存在的訊息佇列
 33 int DeleteMessageQueue(int msg_id);//刪除訊息佇列
 34 int SendDataToMessageQueue(int msg_id,int send_type,char* msg);//向訊息佇列中發訊息
 35 int ReceiveDataFromMessageQueue(int mag_id,int receive_type,char* out);//向訊息佇列裡獲取消36 
 37 #endif
 38 

common.c:

  1 /**************************************
  2 *檔案說明:common.c
  3 *作者:段曉雪
  4 *建立時間:2017年05月24日 星期三 16時42分04秒
  5 *開發環境:Kali Linux/g++ v6.3.0
  6 ****************************************/
  7 #include"common.h"
  8 
  9 static int CommonMessageQueue(int flags)//建立或獲取訊息佇列,實現程式碼複用
 10 {
 11     key_t _key = ftok(FILEPATH,ID);//建立鍵值
 12     if(_key == -1)
 13     {
 14        perror("ftok error");
 15        return 1;
 16     }
 17     int _msg_id = msgget(_key,flags);//建立或獲取訊息佇列
 18     if(_msg_id < 0)
 19     {
 20         perror("msgget error");
 21         return 2;
 22     }
 23     return _msg_id;
 24 }
 25 
 26 int CreatMessageQueue()//建立訊息佇列
 27 {
 28     return CommonMessageQueue(IPC_CREAT | IPC_EXCL | 0666);//如果將IPC_CREAT和IPC_EXCL標誌>    一起\
 29                        使用,XXXget()將返回一個新建的IPC識別符號;如果該IPC資源已存在,或者返
    回-130 }
 31 
 32 int GetMessageQueue()//獲取訊息佇列
 33 {
 34     return CommonMessageQueue(IPC_CREAT);//單獨使用IPC_CREAT,XXXget()函式要麼返回一個\
 35                            已經存在的共享記憶體的操作符,要麼返回一個新建的共享記憶體的識別符號
 36 }
 37 
 38 int DeleteMessageQueue(int msg_id)//刪除訊息佇列
 39 {
 40     if(msgctl(msg_id,IPC_RMID,NULL) < 0)//刪除失敗
 41         return -1;
 42     return 0;//刪除成功
 43 }
 44 
 45 int SendDataToMessageQueue(int msg_id,int send_type,char*msg)//傳送訊息到訊息佇列
 46 {
 47     msginfo buff;
 48     buff.mtype = send_type;
 49     strcpy(buff.mtext,msg);
 50     int msg_snd = msgsnd(msg_id,(void*)&buff,sizeof(buff),0);//將資料放到訊息佇列中
 51     if(msg_snd < 0)
 52     {
 53         perror("msgsnd error");
 54         return -3;
 55     }
 56     return 0;
 57 }
 58 
 59 int ReceiveDataFromMessageQueue(int msg_id,int receive_type,char* out)//從訊息佇列中取訊息
 60 {
 61     msginfo buff;
 62     int msg_rcv = msgrcv(msg_id,(void*)&buff,sizeof(buff),receive_type,0);
 63     if(msg_rcv < 0)
 64     {
 65         perror("msg_rcv error");
 66         return -4;
 67     }
 68     strcpy(out,buff.mtext);
 69     return 0;
 70 }                                                                                           

client.c:

  1 /**************************************
  2 *檔案說明:client.c
  3 *作者:段曉雪
  4 *建立時間:2017年05月24日 星期三 21時14分41秒
  5 *開發環境:Kali Linux/g++ v6.3.0
  6 ****************************************/
  7 
  8 //client:第一次先接收訊息,然後再向訊息佇列中發訊息
  9 #include"common.h"
 10 
 11 int main()
 12 {
 13     char buff[MSG_SIZE];
 14     int msg_id = GetMessageQueue();//建立訊息佇列
 15     while(1)
 16     {
 17         //receive data
 18         ReceiveDataFromMessageQueue(msg_id,SERVER_TYPE,buff);//client receive data
 19         printf("from server:%s\n",buff);
 20 
 21         //send data
 22         printf("client please enter# ");
 23         fflush(stdout);
 24         ssize_t s = read(0,buff,sizeof(buff)-1);//從標準輸入中讀資料到buff
 25         if(s <= 0)
 26         {
 27             perror("read error");
 28             return 1;
 29         }
 30         else
 31         {
 32             buff[s-1] = 0;//去掉換行符
 33             SendDataToMessageQueue(msg_id,CLIENT_TYPE,buff);//client send data
 34             printf("data has sended,wait receive......\n");
 35         }
 36     }
 37     //DeleteMessageQueue(msg_id);//刪除訊息佇列
 38     return 0;
 39 }

server.c:

  1 /**************************************
  2 *檔案說明:server.c
  3 *作者:段曉雪
  4 *建立時間:2017年05月24日 星期三 20時35分32秒
  5 *開發環境:Kali Linux/g++ v6.3.0
  6 ****************************************/
  7 
  8 //server:第一次先向訊息佇列中傳送訊息,緊接著再接收訊息...
  9 #include"common.h"
 10 
 11 int main()
 12 {
 13     char buff[MSG_SIZE];
 14     int msg_id = CreatMessageQueue();//獲取訊息佇列
 15 
 16     while(1)
 17     {
 18         //send data
 19         printf("server please enter# ");
 20         fflush(stdout);//重新整理緩衝區,列印輸入提示
 21         ssize_t s = read(0,buff,sizeof(buff)-1);//從標準輸入中讀資料到訊息佇列
 22         if(s > 0)
 23         {
 24             buff[s-1] = 0;//去掉換行符
 25             SendDataToMessageQueue(msg_id,SERVER_TYPE,buff);//server send data
 26             printf("data has sended,wait receive......\n");
 27         }
 28         else
 29         {
 30             perror("read error");
 31             return 1;
 32         }
 33 
 34         //receive data
 35         ReceiveDataFromMessageQueue(msg_id,CLIENT_TYPE,buff);//server receive data
 36         printf("from client: %s\n",buff);
 37     }
 38     DeleteMessageQueue(msg_id);//刪除訊息佇列
 39 
 40     return 0;
 41 }

makefile:

 1 .PHONY:all
  2 all:client server
  3 
  4 client:common.c client.c
  5     gcc -o [email protected] $^
  6 server:common.c server.c
  7     gcc -o [email protected] $^
  8 
  9 .PHONY:clean
 10 clean:
 11     rm -rf client server

執行結果:
server:
這裡寫圖片描述

client:
這裡寫圖片描述

五、訊號量

1、訊號量的引入
訊號量的本質是一種資料操作鎖,它本身不具有資料交換的功能,而是通過控制其他的通訊資源(檔案,外部裝置)來實現程序間通訊,它本身只是一種外部資源的標識。訊號量在此過程中負責資料操作的互斥、同步等功能。

當請求一個使用訊號量來表示的資源時,程序需要先讀取訊號量的值來判斷資源是否可用。大於0,資源可以請求,等於0,無資源可用,程序會進入睡眠狀態直至資源可用。
當程序不再使用一個訊號量控制的共享資源時,訊號量的值+1,對訊號量的值進行的增減
操作均為原子操作,這是由於訊號量主要的作用是維護資源的互斥或多程序的同步訪問。而在訊號量的建立及初始化上,不能保證操作均為原子性。

2、為什麼要使用訊號量(保護臨界資源)
為了防止出現因多個程式同時訪問一個共享資源而引發的一系列問題,我們需要一種方法,它可以通過生成並使用令牌來授權,在任一時刻只能有一個執行執行緒訪問程式碼的臨界區域。臨界區域是指執行資料更新的程式碼需要獨佔式地執行。而訊號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個執行緒在訪問它, 也就是說訊號量是用來調協程序對共享資源的訪問的。其中共享記憶體的使用就要用到訊號量。

3、訊號量的工作原理
由於訊號量只能進行兩種操作等待和傳送訊號,即P(sv)和V(sv),他們的行為是這樣的:
P(sv):如果sv的值大於零,就給它減1;如果它的值為零,就掛起該程序的執行
V(sv):如果有其他程序因等待sv而被掛起,就讓它恢復執行,如果沒有程序因等待sv而掛起,就給它加1

舉個例子,就是兩個程序共享訊號量sv,一旦其中一個程序執行了P(sv)操作,它將得到訊號量,並可以進入臨界區,使sv減1。而第二個程序將被阻止進入臨界區,因為當它試圖執行P(sv)時,sv為0,它會被掛起以等待第一個程序離開臨界區域並執行V(sv)釋放訊號量,這時
第二個程序就可以恢復執行。

4、Linux的訊號量機制
1>Linux提供了一組精心設計的訊號量介面來對訊號量進行操作,它們不只是針對二進位制訊號量,下面將會對這些函式進行介紹,但請注意,這些函式都是用來對成組的訊號量值進行操作的。它們宣告在標頭檔案sys/sem.h中。

【訊號量的意圖在於程序間同步,互斥鎖和條件變數的意圖則在於執行緒間同步。但是訊號量也可用於執行緒間,互斥鎖和條件變數也可用於程序間。我們應該使用適合具體應用的那組原語。】

2>訊號量的相關概念
訊號量:進行同步或互斥的一把利器
訊號量本質:計數器,記錄或統計臨界資源中資源數目的個數
二元訊號量:該訊號量計數器的值要麼為0要麼為1,有效或無效
互斥鎖:同一時間只能有一個程序訪問臨界資源
訊號量本身也是一種臨界資源,必須保證訊號量的加減操作為原子操作(保護訊號量)
P操作:對訊號量– V操作:對訊號量++ (必須保證原子性)
而在訊號量的建立及初始化上,不能保證操作均為原子性。
目的:保護臨界區(臨界資源)

3>訊號量的相關函式
(1)得到一個訊號量集識別符號或建立一個訊號量集物件並返回訊號量集識別符號

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key,int nsems,int semflg);

semget函式說明:
返回值:返回訊號量集識別符號
申請訊號量時,申請的基本單位是訊號量集(申請一個,nsems=1)看似是集合,實則是陣列。

ipcs -s:檢視系統的訊號量
ipcrm -s 0:刪除系統的0號訊號量

訊號量陣列:訊號量集的組織形式
訊號量集的宣告週期隨核心(system v的IPC:訊號量集、訊息佇列)

(2)在指定的訊號集或訊號集內的某個訊號上執行控制操作

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

semctl() 在 semid 標識的訊號量集上,或者該集合的第 semnum 個訊號量上執行 cmd 指定的控制命令。(訊號量集合索引起始於零。)根據 cmd 不同,這個函式有三個或四個引數。當有四個引數時,第四個引數的型別是union。
呼叫程式必須按照下面方式定義這個聯合體:

union semun {
int val; // 使⽤用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使⽤用快取區
unsigned short *array; // GETALL,、SETALL 使⽤用的陣列