Linux 進程間通信之管道、消息隊列實踐

1、進程間通信簡述

進程間通信的幾種方式:無名管道、有名管道、消息隊列、共享內存、信號、信號量、套接字(socket)。

進程間通信是不同進程直接進行的一些接觸,這種接觸有簡單,有複雜。機制不同,複雜度也不同。通信是一個廣義上的意 義,不僅指大批量數據傳送,還包括控制信息的傳送,但是使用的方法都是大同小異的。

如圖所示進程不是孤立的,不同的進程需要進行信息的交互和狀態的傳遞等,因此需要進程間通信。

2、管道

管道分爲無名管道和有名管道兩種方式。管道是一種半雙工的通信方式,數據只能單向流動,但是無名管道和有名管道的區別是無名管道只能在具有親緣關係的進程間通信,有名管道則是在無親緣關係進程間通信。進程的親緣關係通常是指父子進程關係。管道是 Linux 支持的最初 Unix IPC 形式之一,管道與管道之間通信其實就是一個文件,但它不是一個普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統而且只存在內存中。當一個進程向管道中寫的內容被管道另一端的進程讀出;寫入的內容每次都會被添加到管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出數據。如下圖所示。

那麼,如何創建一條管道呢?下面,我們就來了解下 FIFO 函數。

FIFO 不同於 pipe 函數,因爲它提供了一個路徑名與之關聯,以 FIFO 的文件形式存在於文件系統中,這樣,即使與 FIFO 的創建進程不存在親緣關係的進程,只要可以訪問該路徑就能夠彼此通過 FIFO 互相通信,因此,通過 FIFO 不相關的進程也能交換數據。值得注意的是,FIFO 嚴格遵循先進後出,和棧的原則一樣,對管道以及 FIFO 的讀總是從開始處返回數據,對它們的寫則把數據添加到末尾。它們不支持諸如 lseek() 等文件定位操作。

需要包含的頭文件如下:

FIFO 函數創建:

函數原型:

int mkfifo(const char *pathname,mode_t mode);

函數返回值 :

成功 0,失敗 - 1

參數含義:

pathname 爲路徑名,創建管道的名字(該函數的第一個參數是一個普通的路徑名,也就是創建後 FIFO 的名字)。mode 爲創建 fifo 的權限(第二個參數與打開普通文件的 open() 函數中的 mode 參數相同)。

注:如果 mkfido 的第一個參數已經是一個已經存在的路徑名時,就會返回 EEXIST 錯誤,所以當我們調用的時候首先會檢查是否返回該錯誤,如果返回該錯誤那我們只需要直接調用打開 FIFO 的函數即可。

FIFO 比 pipe 函數打開的時候多了一個打開操作 open;如果當時打開操作時爲讀而打開 FIFO 時,若已經有相應進程爲寫而打開該 FIFO,則當前打開操作將返回成功;否則,可能阻塞到有相應進程爲寫而打開該 FIFO;或者,成功返回。另一種情況就是爲寫而打開 FIFO 時,若已經有相應進程爲讀而打開該 FIFO,則當前打開操作將成功返回;否則可能會阻塞直到有相應進程爲讀而打開該 FIFO;或者,返回 ENIO 錯誤。

下面我們使用 FIFO 實現進程間的通信。

(1) 打開一個文件,管道的寫入端向文件寫入數據;管道的讀取端從文件中讀取出數據。

fifo_write.c

#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define P_FIFO  "txt"

int main()
{
    int fd;
    //要寫入有名管道的數據
    char buf[20] = "hello write_fifo";
    int ret=0;
    //創建有名管道,並賦予訪問有名管道的權限
    ret = mkfifo(P_FIFO,0777);
    //創建失敗
    if(ret < 0)
    {
        printf("create named pipe failed.\n");
        return -1;
    }
    fd = open(P_FIFO,O_WRONLY);
    if(fd < 0)
    {
        printf("open failed.\n");
        return -2;
    }
    //寫入數據到有名管道
    //第一個參數爲有名管道文件描述符
    //第二個參數爲寫入有名管道的數據
    //第三個參數爲寫入有名管道的數據長度
    write(fd,buf,sizeof(buf));
    //關閉有名管道
    close(fd);
    return 0;
}

fifo_read.c

#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define P_FIFO  "txt"

int main()
{
    int ret;
    int fd;
    char buf[20];
    //打開有名管道
    //第一個參數爲有名管道文件路徑
    //第二個參數表明是以讀取方式並以非阻塞方式打開有名管道
    //O_RDONLY讀取模式
    //O_NONBLOCK非阻塞方式
    fd = open(P_FIFO,O_RDONLY);
    if(fd<0)
    {
        printf("open fail\n");
        return -1 ;
    }
    //循環讀取有名管道
    while(1)
    {
        memset(buf,0,sizeof(buf));
        if(read(fd,buf,sizeof(buf)) == 0)
        {
            printf("nodata.\n");
        }
        else
        {
            printf("getdata:%s\n",buf);
            break;
        }
   }
   close(fd);
   return 0;
}

下面先將 fifo_write.c 和 fifo_read.c 分別編譯成 fifo_write 和 fifo_read 兩個可執行程序:

接下來,先運行 fifo_write,然後打開另一個終端,接着運行 fifo_read,運行 fifo_write 的時候,可以看到程序阻塞在終端:

下面打開另外一個終端運行 fifo_read

切換到另外一個終端,在終端輸入ls –l可以看到由於 fifo_write 中創建了管道文件 txt,從前面的字串prwxr-xr-x中的 p 可以知道,這是一個管道文件,如下圖所示:

運行 fifo_read,這時候,可以看到從管道中獲取的字符串 hello write_fifo,如下圖所示:

管道讀取結束後,fifo_write 這個程序也就不會在阻塞在終端了,如下圖所示:

寫管道程序還要注意,一旦我們創建了 FIFO,就可以用 open 去打開它,可以使用 open、read、close 等去操作 FIFO 和 pipe 有相同之處,當打開 FIFO 時,非阻塞標誌(O_NONBLOCK)將會對讀寫產生如下影響:

3、消息隊列

消息隊列(也叫做報文隊列)提供了一個進程向另一個進程發送一個數據塊的方法。每個數據塊都被認爲含有一個類型,接收進程可以獨立地接收含有不同類型的數據結構。我們可以通過發送消息來避免命名管道的同步和阻塞問題。但是消息隊列與命名管道一樣,每個數據塊都有一個最大長度的限制。

打開或者創建消息隊列的內核持續性要求每個消息隊列都在系統範圍內對應唯一的鍵值,所以,要獲得一個消息隊列的描述字,只需要提供該消息隊列的鍵值即可。

消息讀寫操作非常簡單,對於開發人員來說,每個消息都類似如下的數據結構:

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

3.1、msgget 函數

該函數用來創建或者訪問一個消息隊列。

int msgget(key_t key, int msgflg);

與其他的 IPC 機制一樣,程序必須提供一個鍵來命名某個特定的消息隊列。msgflg 是一個權限標誌,表示消息隊列的訪問權限,它與文件的訪問權限一樣。msgflg 可以與 IPC_CREAT 做或操作,表示當 key 所命名的消息隊列不存在時創建一個消息隊列,如果 key 所命名的消息隊列存在時,IPC_CREAT 標誌會被忽略,成功則返回一個以 key 命名的消息隊列的標識符(非零整數),失敗時返回 - 1。

3.2、msgsnd 函數

該函數用來向消息隊列發送一個消息。

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

將發送的消息存儲在 msgp 指向的 msgbuf 結構中,消息大小由 msgsz 指定。對發送的消息來說,有意義的 msgflg 標準爲 IPC_NOWAIT,指明在消息隊列沒有足夠的空間容納要發送的消息時,msgsnd 是否等待。造成 msgsnd() 等待的條件有兩種:當前消息的大小與當前消息隊列中的字節數之和超過了消息隊列的總容量;當前消息隊列的消息數不小於消息隊列的總容量,此時,雖然消息隊列中的消息數目並不多,但基本上都只有一個字節。調用成功的時候返回 0,失敗返回 - 1.

3.3、msgrcv 函數

該函數用來從一個消息隊列獲取消息。

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

msgrcv 函數前面三個參數和 msgsnd 函數的三個參數一樣不做講解。msgtype 可以實現一種簡單的接收優先級。如果 msgtype 爲 0,就獲取隊列中的第一個消息。如果它的值大於零,將獲取具有相同消息類型的第一個信息。如果它小於零,就獲取類型等於或小於 msgtype 的絕對值的第一個消息。msgflg 用於控制當隊列中沒有相應類型的消息可以接收時將發生的事情。當調用成功時,該函數返回放到接收緩存區中的字節數,消息被複制到由 msg_ptr 指向的用戶分配的緩存區中,然後刪除消息隊列中對應的消息;失敗則返回 - 1.

3.4、msgctl 函數

該函數用來控制消息隊列。

int msgctl(int msgid, int command, struct msgid_ds *buf);

該系統調用對由 msqid 標識的消息隊列執行 cmd 操作,共有三種 cmd 操作:

通過上面的函數我們清楚如何去創建一個消息隊列那我們簡單的來看一個案例。

(1) 創建一條消息隊列 msg_get.c

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

int main(void)
{
 int  msgid ; 
 //創建消息隊列,注意,創建後面要有IPC_CREAT標誌
 msgid = msgget(0x123456 , IPC_CREAT | 0777);
 if(msgid < 0)
 {
  perror("msgget fail");
  return -1 ; 
 }
 printf("success ... ! \n");
 return 0  ;
}

運行結果:

那消息隊列呢?怎麼查看?使用 ipcs –q 命令可以查看到剛剛我們創建的消息隊列 0x123456。

(2) 向消息隊列發送消息 msgsend.c

#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main(void)
{
    int msgid ;
    msgid = msgget(0x123456 , 0);
    if(msgid == -1)
    {
        perror("create msg queue fail");
        return -1 ;
    }
    printf("open msg success ... \n");
    int ret ;
    char *p = "hello world" ;
   //發送hello world到消息隊列0x123456
   //在這裏可以直接發送
    ret = msgsnd(msgid , p , strlen(p) , 0);
    if(ret == -1)
    {
        perror("send msgid fail");
        return -2 ;
    }
    return 0 ;
}

運行結果:

使用ipcs –p命令查看:

(3) 獲取消息隊列中的信息 msgrecv.c 在上面 msgsend.c 的基礎上,這個例程將上面發送到消息隊列的信息讀取回來。

#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main(void)
{
    int msgid ;
    msgid = msgget(0x123456 , 0);
    if(msgid == -1)
    {
        perror("create msg queue fail");
        return -1 ;
    }
    printf("open msg success ... \n");
    int ret ;
    char buffer[1024] = {0};
    //接收消息隊列中的信息
    ret = msgrcv(msgid , buffer , 11 , 0 , 0);
    if(ret == -1)
    {
        perror("recv msgid fail");
        return -2 ;
    }
    printf("ret: %d  buffer:%s \n" , ret , buffer);
    return 0 ;
}

運行結果,如圖所示:

那麼,如何刪除一個消息隊列呢?先用ipcs –q查看消息隊列,如圖所示:

有兩種方法:

(4) 刪除消息隊列 msgrm.c

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

int main(void)
{
    int  msgid ;
    msgid = msgget(0x123456 , 0);
    if(msgid < 0)
    {
        perror("msgget fail");
        return -1 ;
    }
    printf("success ... ! msgid:%d \n" , msgid);
    //寫IPC_RMID標誌
    if(msgctl(msgid , IPC_RMID , NULL) == 0)
    {
        printf("remove success ... \n");
    }
    return 0  ;
}

運行結果,如圖所示:

使用系統提供的 API 的方式,可以將消息隊列刪除。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點,相對於管道通信有很大的改觀,而且消息隊列對數據的順序處理也是非常有條理性的不會產生混雜性。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/L4jal294ppztPmKS22kf8Q